4.4 TCP 半连接队列和全连接队列
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
半连接队列,也称 SYN 队列;
- 服务端收到客户端发器的SYN请求,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK
全连接队列,也称 accept 队列;
- 服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列
- 可以设置 accept 队伍满了服务器是直接丢弃 client 发过来的 ack 包还是发送一个
RST包给 client,表示废掉这个握手过程和这个连接;(默认丢弃)- 通过设置为发送RST包也能确认TCP无法连接是不是因为accept 队列满了,但是发送RST包,会占用一定的资源
- 默认丢弃,如果服务器一定时间内能够有空闲的队伍位置,由于客户端会重发ACK包,那么也会完成连接的建立,所以 可以提高连接建立的成功率(丢包后会一直处于半连接状态)
- 进入这个队列之后,需要进程及时调用accept函数取走这个连接
两者都有最大长度显示,超过限制时,内核会直接丢弃,或返回 RST 包。
两者都可以通过参数设置进行扩容
执行
listen方法时,内核会自动会创建半连接队列和全连接队列, 所以客户端没有执行listen方法是没有这两个队列的。
ss -lnt(l: listen 监听, n : 不解析服务名称, t:只显示TCP socket)可以查看全连接队列大小
- 带l参数看到的
- Recv-Q:当前全连接队列的大小
- Send-Q:当前全连接最大队列长度
- 不带 l 参数看到的
- Recv-Q:已收到但未被应用进程读取的字节数;
- Send-Q:已发送但未收到确认的字节数;
虽然两者都叫做队列,但其实全连接队列(icsk_accept_queue)是个链表,而半连接队列(syn_table)是个哈希表。

为什么半连接队列要设计成哈希表
先对比下全连接里队列,他本质是个链表,因为也是线性结构,说它是个队列也没毛病。它里面放的都是已经建立完成的连接,这些连接正等待被取走。而服务端取走连接的过程中,并不关心具体是哪个连接,只要是个连接就行,所以直接从队列头取就行了。这个过程算法复杂度为 O(1)。
而半连接队列却不太一样,因为队列里的都是不完整的连接,嗷嗷等待着第三次握手的到来。那么现在有一个第三次握手来了,则需要从队列里把相应 IP 端口的连接取出,如果半连接队列还是个链表,那我们就需要依次遍历,才能拿到我们想要的那个连接,算法复杂度就是 O(n)。
而如果将半连接队列设计成哈希表,那么查找半连接的算法复杂度就回到 O(1) 了。
因此出于效率考虑,全连接队列被设计成链表,而半连接队列被设计为哈希表。
两个队列满了会怎么做
全连接队列:默认会丢弃客户端的第三次握手ACK,即不会再把连接从半连接队列中移动到全连接队列中
- 可以通过
/proc/sys/net/ipv4/tcp_abort_on_overflow参数修改- 设置为 0 ,会丢弃第三次握手ACK,并且开启定时器,重传第二次握手的 SYN+ACK,如果重传超过一定限制次数,还会把对应的半连接队列里的连接给删掉。
- 设置为
1,全连接队列满了之后,就直接发 RST 给客户端,效果上看就是连接断了。而如果服务端端口未监听服务端也会返回RST,所以这种设定,无法区分到底是端口未监听,还是全连接队列满了。
半连接队列:一般是丢弃
可以通过
/proc/sys/net/ipv4/tcp_syncookies参数控制- 设置为 1 的话,客户端发来第一次握手 SYN 时,服务端不会将其放入半连接队列中,而是直接生成一个
cookies,这个cookies会跟着第二次握手,发回客户端。客户端在发第三次握手的时候带上这个cookies,服务端验证到它就是当初发出去的那个,就会建立连接并放入到全连接队列中。跳过了半连接队列。- 是没有
cookie队列的,否则和半连接队列一样,会被 SYN Flood 攻击打满。 - 这个
cookies是通过通信双方的 IP 地址端口、时间戳、MSS等信息进行实时计算的,保存在 TCP 报头的seq里。第三次握手时服务端会进行验证。 - 不过因为没有队列保存连接信息,如果第二次握手数据丢失,服务端不会重新发送第二次握手的信息
- 同时编码解码
cookies,都是比较耗 CPU 的,攻击者通过大量ACK 包去消耗服务端资源的攻击( ACK 攻击),服务器可能会因为 CPU 资源耗尽导致没能响应正经请求。(因为每次解码cookie后才能验证是否有效)
- 是没有
- 设置为 1 的话,客户端发来第一次握手 SYN 时,服务端不会将其放入半连接队列中,而是直接生成一个
由于半连接的"生存"时间其实很短,只有在第一次和第三次握手间,如果半连接都满了,说明服务端疯狂收到第一次握手请求,一般是遇到了 SYN Flood 攻击。
- 即攻击方模拟客户端疯狂发第一次握手请求过来,服务端回复第二次握手之后,客户端却一直不发第三次握手。
防御SYN攻击的方法:
增大半连接队列;
开启 tcp_syncookies 功能
减少 SYN+ACK 重传次数
没有 accept,能建立 TCP 连接吗?
- 建立连接的过程中根本不需要
accept()参与, 执行accept()只是为了从全连接队列里取出一条连接。- 如果全连接队列为空,accept会阻塞
- 给全连接队列中的连接发送数据,服务端内核会正常回复 ACK 包,服务端在执行accept方法后能正常接收到对应的消息
