Skip to content

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 后才能验证是否有效)
  • 由于半连接的"生存"时间其实很短,只有在第一次和第三次握手间,如果半连接都满了,说明服务端疯狂收到第一次握手请求,一般是遇到了 SYN Flood 攻击

    • 即攻击方模拟客户端疯狂发第一次握手请求过来,服务端回复第二次握手之后,客户端却一直不发第三次握手。
  • 防御SYN攻击的方法:

    • 增大半连接队列;

    • 开启 tcp_syncookies 功能

    • 减少 SYN+ACK 重传次数

没有 accept,能建立 TCP 连接吗?

  • 建立连接的过程中根本不需要 accept() 参与, 执行 accept() 只是为了从全连接队列里取出一条连接。
    • 如果全连接队列为空,accept会阻塞
  • 给全连接队列中的连接发送数据,服务端内核会正常回复 ACK 包,服务端在执行accept方法后能正常接收到对应的消息

正在精进