有人曾经问我socket编程中listen的第二个参数backlog是什么意思?什么值合适?我毫不犹豫的回答说是服务器能接受的最大并发请求数。但事实真的如此吗?TCP通过三次握手建立连接的过程大家应该很熟悉了。从服务器端来看,分为以下几个步骤:设置TCP状态为LISTEN状态,开始监听客户端的连接请求。TCP收到客户端发送的SYN报文后,状态切换到SYNRECEIVED并发送SYNACK报文收到客户端发送的ACK报文后,TCP三次握手完成,状态切换到ESTABLISHED。在Unix系统中,监听是通过listen来开启的。intlisten(intsockfd,intbacklog)listen有两个参数,第一个参数sockfd表示要设置的socket,本文主要关注它的第二个参数backlog;描述为已经完成的连接队列(ESTABLISHED)和未完成的连接队列(SYN_RCVD)之和的上限。一般我们把ESTABLISHED状态的连接称为全连接,SYN_RCVD状态的连接称为半连接。当服务器收到SYN时,它会创建一个子连接并将其添加到SYN_RCVD队列中。收到ACK后,将这个子连接移到ESTABLISHED队列中。最后,当用户调用accept()时,连接将从ESTABLISHED队列中取出。PosixisnotTCPlisten只是posix标准,不是TCP标准!不是TCP标准意味着不同的内核可以有自己独立的实现。POSIX是这样说的:backlog参数为实现提供了一个提示,实现应该使用它来限制套接字侦听队列中未完成连接的数量。Linux是什么行为?查看listen的手册页Linux2.2改变了TCP套接字上backlog参数的行为。现在它指定等待接受的完全建立的套接字的队列长度,而不是未完成的连接请求的数量。这意味着什么?也就是说,在Linux2.2之后,backlog只限制已经完成三次握手,处于ESTABLISHED状态等待接受的子连接数。是不是真的?所以我决定复制一个小程序来验证:服务器监听50001端口并设置backlog=4。注意我没有为了填满队列而调用accept。#include#include#include#include#include#defineBACKLOG4intmain(intargc,char**argv){intlistenfd;国际会议;结构sockaddr_in服务地址;listenfd=socket(PF_INET,SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_addr.s_addr=htonl(INADDR_ANY);servaddr.sin_port=htons(50001);绑定(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));听(listenfd,积压);while(1){睡眠(1);}return0;}客户端的代码#include#include#include#include#include#包括intmain(intargc,char**argv){intsockfd;结构sockaddr_in服务地址;sockfd=socket(PF_INET,SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));服务器地址in_family=AF_INET;servaddr.sin_port=htons(50001);servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");if(0!=connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr))){printf("连接失败!\n");}else{printf("连接成功!\n");}睡眠(30);return1;}为了排除syncookie的干扰,我先关闭了syncookie的功能echo0>/proc/sys/net/ipv4/tcp_syncookies由于我设置了backlog=4,服务器永远不会接受所以预计会有4个完整连接,但实际上是root@ubuntu-1:/home/user1/workspace/client#./client&[1]12798root@ubuntu-1:/home/user1/workspace/client#connectsucceed!。/client&[2]12799root@ubuntu-1:/home/user1/workspace/client#连接成功!/client&[3]12800root@ubuntu-1:/home/user1/workspace/client#连接成功!。/client&[4]12801root@ubuntu-1:/home/user1/workspace/client#连接成功!/client&[5]12802root@ubuntu-1:/home/user1/workspace/client#连接成功!。/client&[6]12803root@ubuntu-1:/home/user1/workspace/client#连接成功!/client&[7]12804root@ubuntu-1:/home/user1/workspace/client#连接成功!。/client&[8]12805root@ubuntu-1:/home/user1/workspace/client#连接成功!/client&[9]12806root@ubuntu-1:/home/user1/workspace/client#连接成功!。/client&[10]12807root@ubuntu-1:/home/user1/workspace/client#连接失败!看!客户端居然显示已经成功建立了9个连接!使用netstat查看TCP连接状态>netstat-tActiveInter网络连接(无服务器)ProtoRecv-QSend-QLocalAddressForeignAddressStatetcp00localhost:50001localhost:55792ESTABLISHEDtcp00localhost:55792localhost:50001ESTABLISHEDtcp00localhost:55798localhost:50001ESTABLISHEDtcp01localhost:55806localhost:50001SYN_SENTtcp00localhost:50001localhost:55784ESTABLISHEDtcp00localhost:50001localhost:55794SYN_RECVtcp00localhost:55786localhost:50001ESTABLISHEDtcp00localhost:55800localhost:50001ESTABLISHEDtcp00localhost:50001localhost:55786ESTABLISHEDtcp00localhost:50001localhost:55800SYN_RECVtcp00localhost:55784localhost:50001ESTABLISHEDtcp00localhost:50001localhost:55796SYN_RECVtcp00localhost:50001localhost:55788ESTABLISHEDtcp00localhost:55794localhost:50001ESTABLISHEDtcp00localhost:55788localhost:50001ESTABLISHEDtcp00localhost:50001localhost:55790ESTABLISHEDtcp00localhost:50001localhost:55798SYN_RECVtcp00localhost:55790localhost:50001ESTABLISHEDtcp00localhost:55796localhost:50001ESTABLISHED整理如下从上面可以看出,一共有5个连接对是ESTABLISHED<->ESTABLISHED连接,但是还有4个连接对是SYN_RECV<->ESTABLISHED连接,也就是说对于client的三次握手已经完成,但是对于server还没有完成!回顾一下TCP的三次握手的过程,这次连接唯一可能的原因是服务器发送给客户端的最后一次握手ACK被丢弃了!还有一个问题。我明明把backlog的值设置为4,为什么还是可以建立5个连接?!去内核找原因。我实验用的机器内核是4.4.0之前我提到了完成连接队列和未完成连接队列这两个概念。Linux有这两个队列吗?Linux两者兼而有之!据说内核中有两种连接长度;不是因为对于Linux来说,真正存在的只有完整的连接队列,不完整的连接队列只有长度的记录!每个处于LISTEN状态的socket都有一个structinet_connection_sock结构,里面的accept_queue从名字也可以看出已经完成三次握手的子连接队列。只有这个结构体还记录了半连接请求的长度!structinet_connection_sock{//省略代码structrequest_sock_queueicsk_accept_queue;//省略代码}structrequest_sock_queue{//省略代码atomic_tqlen;//半连接的长度atomic_tyoung;//一般来说,这个值=qlenstructrequest_sock*rskq_accept_head;//完成连接的队列的头部structrequest_sock*rskq_accept_tail;//已完成连接的队列的尾部//省略代码};,一般情况下是随着qlen而变化的,但有时也不会。代码中的注释说明了这一点,即半连接在1s定时器超时后没有收到对端的ACK就“成熟”了。通常所有的openreqs都是年轻的,并且在第一次超时时变得成熟(即转换为已建立的套接字)。如果synack在1秒内未被确认,则意味着以下情况之一:synack丢失、ack丢失、rtt高或没有人计划进行ack(即synflood)。所以一般来说,当连接建立后,服务器的变化过程是这样的:收到SYN报文,qlen++,young++收到ACK报文,完成三次握手,将连接加入accept队列,qlen--,young--用户使用accept将连接从accept中取出。下面看看内核收到SYN握手报文时的处理。由于我关闭了syncookie,一旦满足以下代码中的两个条件之一,它就会丢弃数据包net->ipv4.sysctl_tcp_syncookies==2||{//条件1:半连接>=backlogwant_cookie=tcp_syn_flood_action(sk,skb,rsk_ops->slab_name);if(!want_cookie)gotodrop;}if(sk_acceptq_is_full(sk)&&inet_csk_reqsk_queue_young(sk)>1){//条件2:全连接sock>半连接队列的backlog和youngfield>1NET_INC_STATS(sock_net(sk),LINUX_MIB_LISTENOVERFLOWS);转到下降;}//代码省略"半连接队列的young字段>1"表示网络很忙,SYNACK丢失(没有收到对端第三次握手的ACK),但是在我们简单的例子中,client的ACK总是及时的,所以这个条件不会满足,即丢弃SYN报文的条件2只剩下全连接的sock>backlog。下面是收到ACK握手报文时的处理。*dst,structrequest_sock*req_unhash,bool*own_req){//省略代码if(sk_acceptq_is_full(sk))//全连接>积压,丢弃gotoexit_overflow;newsk=tcp_create_openreq_child(sk,req,skb);//创建子套接字//代码省略}这样就可以解释实验现象了!前4次连接请求能成功创建子连接,全连接队列长度=backlog=4,半连接数=0,第5次连接请求,由于sk_acceptq_is_full的判断条件是>而不是>=,所以是仍然可以建立完整的连接。当第6-9次连接请求到来时,由于半连接数还没有超过积压,所以仍然可以继续回复SYNACK,但是不能再接收ACK再次创建子套接字,所以TCP状态还是SYN_RECV。同时,半连接数也增加到积压。对于client,由于可以收到SYNACK握手报文,可以将TCP状态改为ESTABLISHED,当第10个请求到来时,由于半连接数已经达到积压,SYN报文将被丢弃。内核的问题从以上现象和分析,我认为内核判断accept队列是否满存在以下问题>=比>更合适,从而体现backlog的作用。当accept队列满了,应该拒绝半连接,因为即使半连接握手完成,也不能加入accept队列,否则在SYN_RECV--ESTABLISHED状态下还会有连接的!这样的连接不能用于数据传输!2016补丁修改了问题2!所以如果你在较新版本的内核中进行同样的实验,你会发现客户端只能连接成功5次,当然这也需要先关闭syncookie,但是问题1还没有修改!以后要是修改了我也不会惊讶(完)REFhow-tcp-backlog-works-in-linux