TCP通信服務器端實現第四步:調用accept 網絡API,被動監聽客戶端的連接。【linux】(zzw)

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和端口打印出來,進行驗證。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章