socket编程中listen()函数到底做了什么

基于TCP的网络编程分为客户端和服务器端,以C为例,常见的核心步骤流程为:

11111.jpg

那么我们就一一细数其过程。

connect()

这是客户端调用的函数,其功能是为客户端主动连接服务器,建立三次握手,这个过程是由内核完成的,这个函数的作用仅仅是通知内核,让内核自动完成TCP三次握手,最后把连接的结果返回给这个函数的返回值。

通常,客户端的connect()函数默认会i一直阻塞,直到三次握手成功或超时失败才返回。

listen()

对于服务器,它是被动连接的。相当于是等待客户端的主动连接,这个过程,需要用到listen()函数。

#include <sys/socket.h>
int listen(int sockfd, int backlog);

listen()函数的主要作用就是将sockfd变成被动的连接sockfd(被动等待客户端的连接),参数backlog的作用是设置内核中连接队列的长度,TCP三次握手也不是由这个函数完成的,listen()的作用仅仅告诉内核一些信息。

listen()函数是不会阻塞的,其主要做的事,将该sockfd和sockfd对应的连接队列长度告诉linux内核,然后,listen函数结束。

这样的话,当有一个客户端主动connect(),linux就自动完成三次握手,将建立好的链接自动存储到队列中,如此重复。

所以,只要服务端调用了listen(), 客户端就能通过connect()与服务器建立连接,而这个连接的过程是由内核完成的。

server.c

#include <stdlib.h>
#include <string.h>						
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>				
int main(int argc, char *argv[])
{
	unsigned short port = 8000;	
 
	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
	if(sockfd < 0)
	{
		perror("socket");
		exit(-1);
	}
	
	struct sockaddr_in my_addr;
	bzero(&my_addr, sizeof(my_addr));	     
	my_addr.sin_family = AF_INET;
	my_addr.sin_port   = htons(port);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if( err_log != 0)
	{
		perror("binding");
		close(sockfd);		
		exit(-1);
	}
	err_log = listen(sockfd, 10);
	if(err_log != 0)
	{
		perror("listen");
		close(sockfd);		
		exit(-1);
	}	
	
	printf("listen client @port=%d...\n",port);
	
	sleep(10);	// 延时10s
 
	system("netstat -an | grep 8000");	// 查看连接状态
	
	return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
	unsigned short port = 8000;        		// 服务器的端口号
	char *server_ip = "10.221.20.12";    	// 服务器ip地址
	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
	if(sockfd < 0)
	{
		perror("socket");
		exit(-1);
	}

	struct sockaddr_in server_addr;
	bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(port);
	inet_pton(AF_INET, server_ip, &server_addr.sin_addr);

	int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));      // 主动连接服务器
	if(err_log != 0)
	{
		perror("connect");
		close(sockfd);
		exit(-1);
	}

	system("netstat -an | grep 8000");	// 查看连接状态

	while(1);

	return 0;

}

三次握手的连接队列

这里主要说的的是listen函数的backlog的作用:告诉内核连接队列的长度。

为了更好的理解 backlog 参数,必须认识到内核为任何一个给定的监听套接口维护两个队列:

  • 未完成连接队列(incomplete connection queue), 每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口处于 SYN_RCVD 状态。
  • 已完成连接队列(completed connection queue), 每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态。

当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。

如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。

backlog历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,当服务器把这个完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡,但在高并发 web 服务器中此值显然不够。

accept()

accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。

如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器不会对再对建立新连接的SYN进行应答,所以客户端的 connect 就会返回 ETIMEDOUT。但实际上Linux的并不是这样的!

下面为测试代码,服务器 listen() 函数只指定队列长度为 2,客户端有 6 个不同的套接字主动连接服务器,同时,保证客户端的 6 个 connect()函数都先调用完毕,服务器的 accpet() 才开始调用。

server.c

#include <stdlib.h>
#include <string.h>						
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>	
			
int main(int argc, char *argv[])
{
	unsigned short port = 8000;			
	
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);   
	if(sockfd < 0)
	{
		perror("socket");
		exit(-1);
	}
	struct sockaddr_in my_addr;
	bzero(&my_addr, sizeof(my_addr));	     
	my_addr.sin_family = AF_INET;
	my_addr.sin_port   = htons(port);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if( err_log != 0)
	{
		perror("binding");
		close(sockfd);		
		exit(-1);
	}

	err_log = listen(sockfd, 2);	// 等待队列为2
	if(err_log != 0)
	{
		perror("listen");
		close(sockfd);		
		exit(-1);
	}	
	printf("after listen\n");
	
	sleep(20);	//延时 20秒
	
	printf("listen client @port=%d...\n",port);
 
	int i = 0;
	
	while(1)
	{	
		struct sockaddr_in client_addr;		   
		char cli_ip[INET_ADDRSTRLEN] = "";	   
		socklen_t cliaddr_len = sizeof(client_addr);    
		
		int connfd;
		connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);       
		if(connfd < 0)
		{
			perror("accept");
			continue;
		}
 
		inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
		printf("-----------%d------\n", ++i);
		printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
		
		char recv_buf[512] = {0};
		while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 )
		{
			printf("recv data ==%s\n",recv_buf);
			break;
		}
		
		close(connfd);     //关闭已连接套接字
		//printf("client closed!\n");
	}
	close(sockfd);         //关闭监听套接字
	return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
void test_connect()
{
	unsigned short port = 8000;        		// 服务器的端口号
	char *server_ip = "10.221.20.12";    	// 服务器ip地址
	
	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
	if(sockfd < 0)
	{
	perror("socket");
		exit(-1);
	}
	
	struct sockaddr_in server_addr;
	bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(port);
	inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
	
	int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));      // 主动连接服务器
	if(err_log != 0)
	{
		perror("connect");
		close(sockfd);
		exit(-1);
	}
	printf("err_log ========= %d\n", err_log);
	
	char send_buf[100]="this is for test";
	send(sockfd, send_buf, strlen(send_buf), 0);   // 向服务器发送信息
	
	system("netstat -an | grep 8000");  // 查看连接状态
	
	//close(sockfd);
}

int main(int argc, char *argv[])
{
	pid_t pid;
	pid = fork();
	
	if(0 == pid){
 
		test_connect();		// 1
		
		pid_t pid = fork();
		if(0 == pid){
			test_connect();	// 2
		}else if(pid > 0){
			test_connect();	// 3
		}
		
	}else if(pid > 0){
	
		test_connect();	// 4
		
		pid_t pid = fork();
		if(0 == pid){
			test_connect();	// 5
		}else if(pid > 0){
			test_connect();	// 6
		}
	
	}
 
	while(1);
	
	return 0;
}

按照 UNP 的说法,连接队列满后(这里设置长度为 2,发了 6 个连接), 以后再调用 connect() 应该统统超时失败,但实际上测试结果是:有的 connect()立刻成功返回了,有的经过明显延迟后成功返回了。对于服务器 accpet() 函数也是这样的结果:有的立马成功返回,有的延迟后成功返回。

TCP 的连接队列满后,Linux 不会如书中所说的拒绝连接,只是有些会延时连接,而且accept()未必能把已经建立好的连接全部取出来(如:当队列的长度指定为 0 ),写程序时服务器的 listen() 的第二个参数最好还是根据需要填写,写太大不好(具体可以看cat /proc/sys/net/core/somaxconn,默认最大值限制是 128),浪费资源,写太小也不好,延时建立连接。


标题:socket编程中listen()函数到底做了什么
作者:reyren
地址:https://www.reyren.cn/articles/2021/06/03/1622701065551.html

    评论
    0 评论
avatar

取消