TCP半连接与全连接队列及accept建立连接

我们知道当服务器绑定、监听了指定端口后,内核通常会为每一个LISTEN状态的socket维护两个队列:

  1. SYN队列(半连接队列):由/proc/sys/net/ipv4/tcp_max_syn_backlog指定,表示处于 SYN_RECV 状态的队列
  2. ACCEPT队列(全连接队列):由listen()函数的第二个参数 backlog 指定,内核硬限制由 net.core.somaxconn 限制,即实际的值由min(backlog,somaxconn) 来决定。表示已完成连接的队列,等待被 accept系统调用取走。

这里写图片描述

TCP三次握手如何与accept交互呢?

看下面这张图:

这里写图片描述

客户端使用connect向服务器发送TCP连接,三次握手就发生了。当1.1步骤 客户端首先发送SYN到达服务端后,内核会把连接信息放到SYN队列中,同时回一个SYN+ACK包给客户端。一段时间后,客户端再次发来ACK包后,内核会把连接从SYN队列中取出,再把这个连接放到ACCEPT队列中。服务器调用accept时,其实就是直接从ACCEPT队列中取出已经建立成功的连接套接字。

还有一张图看下TCP握手过程中建连接的流程和队列:

这里写图片描述


队满时会发生什么

Q:为什么 有的应用服务器进程,会单独使用一个线程去调用accept来建立服务端和客户端的连接,比如tomcat;而有的则是一个线程做所有的事,即获取连接后还会做I/O等其他操作?

A:首先SYN队列和ACCEPT队列都不是无限大小的,上面已经提到。既然队列长度是有限的,那就有满的时候。比如当上图中步骤1的执行速度大于第2步执行的速度,SYN队列就会不断增大,直到队列满;第2步的执行速度远大于第3步的执行速度,ACCEPT队列同样会满。第1、2步不是程序可控的,第3步是程序的行为。

先看第一种情况:SYN队列满。如果SYN队列满,则会直接丢弃连接请求。
比如syn floods 攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,server需要一个超时时间把这个连接断开,否则大量这样的连接导致server上这个队列满其它正常请求无法进来。

第二种情况:ACCEPT队列满。这种情况貌似比较复杂,我查了学长的博客,发现学长讨论过这个问题。accept队列满的讨论
如果ACCEPT队列满了,server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 来决定如何返回:

  1. tcp_abort_on_overflow 为 0,不会把连接从SYN队列中移除,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),这样来回重发几次,次数由 /proc/sys/net/ipv4/tcp_synack_retries(centos默认为 5 ) 指定,如果三次握手第三步的时候 ACCEPT 队列一直是满,那么server扔掉client 发过来的 ACK(在server端认为连接还没建立起来);

  2. tcp_abort_on_overflow 为 1 表示第三步的时候如果 ACCEPT 队列满了,server发送一个RST包给client,表示废掉这个握手过程和这个连接(本来在server端这个连接就还没建立起来),客户端会出现 connection reset by peer 的异常。

PS:还有一个事实是,一般 overflowed 表示全连接队列溢出次数,socket ignored 表示半连接队列溢出次数,这两个是同步增加的,overflow的时候一定会socket ignored++。

所以对应用服务器进程来说,如果ACCEPT队列中有已经建立好的TCP连接,却没有及时的把它取出来,这样一旦导致两个队列满后就会使客户端不能再建立新的连接,引发严重问题。所以,比如tomcat等服务器会用独立的线程只做accept获取连接这一件事,以防止不能及时的去accept获取连接。


Q:但我们知道比如像Ngnix等服务器,在一个线程内做accept的同时,还会做其他IO操作,这是为什么呢?它不用关心上面的问题吗?

A:应用程序可以把listen时的监听套接字设置为非阻塞模式(默认为阻塞模式),这两种模式会导致accept方法有不同行为。
阻塞套接字,accept行为如下:

这里写图片描述

它会一直等待ACCEPT队列不为空,ACCEPT队列是否为空,由客户端是否发起connect请求而定。

非阻塞套接字,accept行为如下:

这里写图片描述

非阻塞模式下,不存在等待ACCEPT队列不为空的阶段,直接返回,要么返回连接套接字,要么返回错误。

所以,企业级的服务器进程中,若某一线程既使用accept获取新连接,又继续在这个连接上读、写字符流,那么,这个连接对应的套接字通常要设为非阻塞。


接着上面全连接队列满继续想:这时候client走完第三步,那么在client看来连接已经建立好了,但是server上实际没有准备好,如果这时候client发数据给server,server怎么处理呢?
A:client发送一个数据包给server后,server上的这个连接实际没有ready,server一直没有回复,一段时间后client认为丢包了,然后重传这个包,一直到超时,client主动发FIN包断开连接。


参考链接:
1. 关于TCP 半连接队列和全连接队列
2. accept建立连接
3. 深入探索 Linux listen() 函数 backlog 的含义