accept函數
函數原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
被動監聽客戶端發起三次握手的連接請求,三次握手成功,即建立連接成功。
accept函數被動監聽客戶連接的過程,其實也被稱爲監聽客戶上線的過程。
對於那些只連接了一半,還未連接完成的客戶,會被記錄到未完成連接的隊列中,隊列的容量由listen函數的第二個參數(backlog)來指定。
真正用於監聽的是accept函數,listen函數只是用來把套接字文件描述符從主動轉變爲被動。轉爲被動之後交給accept函數使用。accept函數用來被動監聽客戶端的連接。所以accept函數纔是監聽函數。
服務器調用accept函數監聽客戶連接,而客戶端則調用connect函數來主動發起連接請求。
服務器端的accept函數和客戶端的connect函數是對應關係。
一旦連接成功,服務器這邊的TCP協議會記錄客戶端的IP和端口,如果是跨網通信,記錄ip的就是客戶所在路由器的公網Ip。
返回值
成功
返回一個通信描述符,專門用於與該連接成功的客戶的通信,總之後續服務器與該客戶間正式通信,使用的就是accept函數返回的“通信描述符”來實現的。
失敗
返回-1,errno被設置。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參數
sockefd
已經被listen函數轉爲了被動描述符的“套接字文件描述符”,專門用於被動監聽客戶的連接。
如果sockfd沒有被listen函數轉爲被動描述符的話,accept函數是無法將其用來監聽客戶連接的。
有關套接字描述符的阻塞與非阻塞問題
int socket(int domain, int type, int protocol);
服務器程序調用socket函數獲得“套接字文件描述符”時,如果socket的第2個參數type沒有指定SOCK_NONBLOCK的話,socket函數所返回的 “套接字文件描述符”默認就是阻塞的,所以使用accept函數來監聽客戶連接時,如果沒有客戶請求連接的話,accept函數就會阻塞,直到有客戶連接爲止。
如果你不想阻塞,我們就可以在調用socket函數時,給type指定SOCK_NONBLOCK宏。這個時候accept函數使用這個描述符來監聽客戶連接的時候,如果沒有客戶連接的話accept函數就不會阻塞。
在TCP/IP協議族中,只有TCP協議纔有建立連接的要求,那麼accept函數怎麼知道你用的就是TCP協議呢?
accept函數的第一個參數是socket函數所返回的“套接字文件描述符”,它指向了socket函數所創建的套接字文件,創建套接字文件時,我們會通過參數1和參數2指定你所使用的協議。
int socket(int domain, int type, int protocol);
比如將其指定爲TCP協議,所以只要你在調用socket函數創建套接字文件時有指定通信協議,accept函數就可以通過“套接字文件描述符”知道套接字文件所使用的是不是TCP協議。
因爲只有tcp協議纔有連接,只有當有連接的時候我們才能夠去調用accept函數去等待連接。如果使用的協議不需要連接的話,調用accept函數會導致錯誤。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr
用於記錄發起連接請求的客戶端的IP和端口(port)。
前面說過建立連接時,服務器這邊的TCP協議會自動記錄客戶端的ip和端口,如果是跨網通信的話,記錄的就是客戶端的公網IP。
如果服務器應用層需要用到客戶ip和端口的話,可以給accept函數指定第二個參數addr,以獲取TCP在連接時所自動記錄客戶IP和端口,如果服務器應用層不需要用的話就寫NULL。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr爲struct sockaddr
結構體類型,有關這個結構體,我們在前面博客已經說明過。
不過我們前面說過,雖然下層(內核)實際使用的是struct sockaddr
結構體,但是由於這個結構體用起來不方便,因此應用層會使用更加便於操作的結構體,比如使用TCP/IP協議族通信時,應用層使用的就是struct sockaddr_in
這個更加方便操作的結構體。
所以我們應該定義struct sockaddr_in
類型的addr,傳遞給accept函數時,將其強制轉爲struct sockaddr
結構體類型即可,這個過程與我們說明bind函數時的用法類似。
例子:
創建結構體類型變量
struct sockaddr_in clnaddr = {0};
獲得結構體的大小
int clnsize = sizeof(clnaddr);
調用accept函數返回通信文件描述符,cfd 用於保存accept 函數調用成功所返回的通信文件描述符。
cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnsize);
accept 函數的第一個參數寫被轉爲被動的套接字文件描述符。
第二個參數將 clnaddr 強制轉化爲 struct sockaddr 類型。
第三個參數指定結構體變量的大小,只不過這裏是取地址。
將clnaddr傳遞給accept函數後,accept函數會自動的將TCP記錄的客戶ip和端口設置到clnaddr中,我們就可以得到客戶的ip和端口。
通過之前對struct sockaddr_in
成員的瞭解可知,clnaddr第二個成員放的是客戶端的端口號,第三個成員放的是客戶的IP。
struct sockaddr_in
{
sa_family_t sin_family; //設置AF_***(地址族)
__be16 sin_port; //設置端口號
struct in_addr sin_addr; //設置Ip
/* 設置IP和端口時,這個成員用不到,這個成員的作用後面再解釋, */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};
addrlen
第二參數addr的大小,不過要求給的是地址。
如何給地址?
我們在下面代碼中進行演示。
代碼演示:調用accept函數
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPROT 5006
#define SIP "192.168.31.162"
void print_err(char * str,int line,int err_no)//出錯處理函數
{
printf("%d,%s: %s\n",line,str,strerror(err_no));
exit(-1);
}
int main(void)
{
int ret = -1;
int sockfd = -1; //存放套接字文件描述符
/*創建使用TCP協議通信的套接字文件*/
sockfd = socket(PF_INET,SOCK_STREAM,0); //指定TCP協議
if(-1 == sockfd) //進行出錯處理
print_err("socket fail",__LINE__,errno);
/*調用bind函數綁定套接字文件/ip/端口*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//指定ip格式
saddr.sin_port= htons(SPROT);//指定端口
saddr.sin_addr.s_addr = inet_addr(SIP);//設置ip
ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定
if(-1 ==ret)
print_err("bind fail",__LINE__,errno);
/*將主動的套接字文件描述符轉換爲被動的文件描述符,用於被動監聽客戶連接。*/
ret = listen(sockfd,3);
if(-1 ==ret)
print_err("listen fail",__LINE__,errno);
/*調用accep函數,被動監聽客戶的連接*/
int cfd = 0; /*存放與客戶通信的通信文件描述符*/
struct sockaddr_in clnaddr = {0};/*用來保存連接客戶端的IP和端口*/
int clnaddr_size = sizeof(clnaddr);/*存放結構體變量的大小*/
cfd = accept(sockfd,(struct sockaddr *)&clnaddr,&clnaddr_size);
if(-1 ==ret)
print_err("listen fail",__LINE__,errno);
return 0;
}
如何使用得到的客戶ip和端口
比如我這裏的使用方式是打印客戶端的ip和端口進行查看,此時必須調用ntohs函數和inet_ntoa函數進行端序轉換。
爲什麼要進行端序轉換?
客戶的端口和ip是服務器這邊的TCP協議,從客戶端發送的網絡數據包中提取出來的,網絡數據包的端序屬於網絡端序,主機接收到數據後,如果你想要使用的話,就必須從網絡端序轉爲主機端序。
ntohs函數
是htons函數功能相反的函數,ntohs函數的作用就是把客戶端的端口從網絡端序轉爲主機端序。
htons 函數我們在bind函數綁定中使用過,htons函數的作用就是把服務器的端口從主機端序轉化爲網絡端序。
inet_ntoa函數
inet_ntoa函數功能與inet_addr函數剛好相反,inet_ntoa函數有兩個功能。
· 將IPV4的32位無符號整形數的ip,從網絡端序轉爲主機端序。
· 將實際所用的無符號整型數的ip,轉成人能識別的字符串形式的ip,也就是點分十進制的形式。
inet_addr 函數的功能就是把點分十進制的ip轉換爲32位無符號整型的ip,然後把32位無符號整型的ip從主機端序轉換爲網絡端序。
使用舉例:
//用來存放客戶端ip和端口
struct sockaddr_in clnaddr = {0};
//存放clnaddr 結構體的大小
int clnaddr_size = sizeof(clnaddr)
//調用accept函數
cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnaddr_size);
當把clnaddr結構體傳給accept函數之後,accept函數就會自動的把tcp協議所記錄的客戶端ip和端口寫到clnaddr結構體中,應用層就通過結構體變量得到了客戶端的ip和端口就可以直接使用,我們這裏的簡單使用就是打印客戶端的ip和端口。
打印客戶端的ip和端口:
printf("cln_port=%d, cln_addr=%s\n",ntohs(clnaddr.sin_port), inet_ntoa(clnaddr.sin_addr));
上面的printf函數裏面,把clnaddr.sin_port 端口通過 ntohs 函數從網絡端序轉換爲主機端序。然後再打印出來。把clnaddr.sin_addr 的ip地址首先從網絡端序轉爲主機端序,然後再把轉換後的主機端序ip從無符號的32位整型ip轉換爲字符串的點分十進制ip便於查看。
代碼演示:打印客戶端ip和端口
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPROT 5006
#define SIP "192.168.31.162"
void print_err(char * str,int line,int err_no)//出錯處理函數
{
printf("%d,%s: %s\n",line,str,strerror(err_no));
exit(-1);
}
int main(void)
{
int ret = -1;
int sockfd = -1; //存放套接字文件描述符
/*創建使用TCP協議通信的套接字文件*/
sockfd = socket(PF_INET,SOCK_STREAM,0); //指定TCP協議
if(-1 == sockfd) //進行出錯處理
print_err("socket fail",__LINE__,errno);
/*調用bind函數綁定套接字文件/ip/端口*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//指定ip格式
saddr.sin_port= htons(SPROT);//指定端口
saddr.sin_addr.s_addr = inet_addr(SIP);//設置ip
ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定
if(-1 ==ret)
print_err("bind fail",__LINE__,errno);
/*將主動的套接字文件描述符轉換爲被動的文件描述符,用於被動監聽客戶連接。*/
ret = listen(sockfd,3);
if(-1 ==ret)
print_err("listen fail",__LINE__,errno);
/*調用accep函數,被動監聽客戶的連接*/
int cfd = 0; /*存放與客戶通信的通信文件描述符*/
struct sockaddr_in clnaddr = {0};/*用來保存連接客戶端的IP和端口*/
int clnaddr_size = sizeof(clnaddr);/*存放結構體變量的大小*/
cfd = accept(sockfd,(struct sockaddr *)&clnaddr,&clnaddr_size);
if(-1 ==ret)
print_err("accept fail",__LINE__,errno);
/*打印客戶端的端口和ip,一定要記得進行端口轉換*/
printf("client_port = %d\n,client_ip = %s\n",ntohs(clnaddr.sin_port),inet_ntoa(clnaddr.sin_addr));
return 0;
}
不過目前,我們並沒有辦法來驗證,因爲我們還沒有寫客戶端程序。也就沒有客戶端請求連接,之後把客戶端程序寫完之後,當客戶端請求連接完成之後printf函數就會把客戶端的ip和端口打印出來,進行驗證。