天天看点

基础回顾----listen函数的backlog参数的本质

在初学网络编程这块时,对listen函数的第二个参数(backlog)简单理解为服务器允许同时连接的客户端个数,但后面总是感觉对这个参数的意义很模糊,在查阅资料后才发现这个参数远远没有表面上那么简单,因此写篇博客总结、记录、巩固一下。

先提出两个概念半连接状态队列(syns queue)和完全连接状态队列(accept queue),通过下图可以看到这两个队列的作用:

基础回顾----listen函数的backlog参数的本质

图片来源:关于TCP 半连接队列和全连接队列

可以看到linux会将处于SYN_RCVD状态的socket放在半连接队列中,将已经建议好连接处于ESTABLISHED状态的socket从半连接队列拿出放在全连接队列中,accept系统调用返回的socket是从全连接队列中获取的。

现在我们可以讨论backlog的作用了,在内核版本2.2之前,backlog参数是指半连接队列和全连接队列的大小。在2.2版本之后,backlog参数只表示全连接队列的大小(accept队列有一个上限值,由系统参数somaxconn指定,也就是全连接队列的大小等于min(somaxconn, backlog)),半连接队列的大小为max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。backlog的经典值为5。

实验:

上述理论我们可以通过代码来验证:

#include<stdio.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<arpa/inet.h>
int main(int argc,const char *argv[])
{
	if(argc < 3)
	{
		printf("usage:%s ip_address port_number\n",argv[0]);
		exit(1);
	}

	const char* ip = argv[1];
	int port = atoi(argv[2]);

	struct sockaddr_in listenaddr,connaddr;
	socklen_t len = sizeof(connaddr);
	listenaddr.sin_family = AF_INET;
	listenaddr.sin_port = htons(port);
	inet_pton(AF_INET,ip,&listenaddr.sin_addr);

	int listenfd = socket(AF_INET,SOCK_STREAM,0);
	
	bind(listenfd,(struct sockaddr*)&listenaddr,sizeof(listenaddr));

	listen(listenfd,3);

	sleep(30);//睡眠20秒,以便积累多个客户端连接
	while(true)
	{
		//接受客户端连接,从全连接队列中取出一个socket
		int connfd = accept(listenfd,(struct sockaddr*)&connaddr,&len);
		printf("hava a accept\n");
		sleep(10);//每10秒从全连接队列中取出一个socket
	}
	return 0;
}
           

上述代码在服务器调用listen后,先不accept,这样多个客户端连接过来,我们便可以通过netstat命令清楚地看到可以有多少个ESTABLISHED状态的连接即全连接队列的长度。

当有七个连接请求到达时会出现下面这种情况。

基础回顾----listen函数的backlog参数的本质

我们可以看到对于服务端来说总共有4个处于ESTABLISHED状态的连接,说明全连接队列的长度是我们设置的backlog参数+1(不同的系统会有所不同,不过全连接队列的大小通常会比backlog值略大),但是server端并没有其他3个连接应有的SYN_RCVD状态,client端的状态停留在SYN_SENT状态,也就是说server并没有对这3个client发送SYN+ACK,这很怪异,因为server端理应发送SYN+ACK,并将这3个client的连接放到半连接队列中。通过tcpdump抓包可以看到,这3个client一直向server重发SYN请求,在多次重发SYN得不到回应时,会返回Connection timed out。

基础回顾----listen函数的backlog参数的本质
基础回顾----listen函数的backlog参数的本质

上述代码,每隔10s就会调用一次accept函数,也就是每隔10s就会从全连接队列中取出一个socket,这时全连接队列就会空出一个位置,这时server才会对到来的SYN回应建立连接,效果如下图:

基础回顾----listen函数的backlog参数的本质
基础回顾----listen函数的backlog参数的本质

貌似在全连接队列满后,server不会对再来的SYN做出响应,当全连接队列有空位时,server才会对到来的SYN响应建立连接,整个过程没看到server的SYN_RCVD状态,没有看到半连接队列的身影。这个结果并不符合理论。

对于上述问题,我查看了很多文档,也没有找到原因,如果有大佬知道,可以写在评论区,谢谢!

若全连接队列已经满了,此时服务端收到了一个客户端的ACK应答会发生什么(或者说有连接需要从SYN队列转移到accept队列)?

这个时候系统会根据/proc/sys/net/ipv4/tcp_abort_on_overflow内核参数的值来做出应对。

  1. 若tcp_abort_on_overflow值为0,则server会扔掉client发来的ACK应答,当定时器超时时,服务端会重传SYN+ACK给client(重新走三次握手的第二步),如果重传次数超过synack重传的阀值(/proc/sys/net/ipv4/tcp_synack_retries),则会把该连接从半连接队列中删除。
  2. 若tcp_abort_on_overflow值为1,server会发送rst给client,并删除掉这个连接。

client发送完ACK后就会进入ESTABLISHED状态,也就是connect函数返回,但是服务端因为全连接队列满了,所以对应连接实际没有准备好,这个时候如果client发数据给server,server会怎么处理呢?

我们看下面这个图片:

基础回顾----listen函数的backlog参数的本质

图片来源Linux协议栈accept和syn队列问题

150166号包是三次握手中的第三步client发送ack给server,然后150167号包中client发送了一个长度为816的包给server,因为在这个时候client认为连接建立成功,但是server上这个连接实际没有ready,所以server没有回复,一段时间后client认为丢包了然后重传这816个字节的包,一直到超时,client主动发fin包断开该连接。

所以通常情况下,应当把 tcp_abort_on_overflow 设置为 0,因为这样更有利于应对突发流量。因为client每次发送数据都会带一个对接受到server最近一次数据包的ACK应答,在这里就是server发送的SYN+ACK,所以若在client重传期间,server的全连接队列出现空位,则这条连接就会建立成功

参考:

阿里中间件团队博客:关于TCP 半连接队列和全连接队列

tcp的半连接与完全连接队列

https://www.cnblogs.com/xiaolincoding/p/13067971.html

Linux协议栈accept和syn队列问题

深入探索 Linux listen() 函数 backlog 的含义

http://blog.csdn.net/yangbodong22011/article/details/60468820

继续阅读