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和端口打印出来,进行验证。

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