bigywater.tech
网络协议 · 43

半连接队列与全连接队列:backlog 溢出时发生什么

listen 的 backlog 到底限制什么?队列满了新连接会怎样?半连接和全连接队列怎么区分?这篇用 netstat -L 逐项量,并抓包发现 macOS 在全连接队列满时的做法和 Linux 不一样:握手照常完成,然后被 RST。

N 个
listen(N) 之后不 accept,全连接队列实际容纳的连接数;listen(1000) 被截成 128
SYN → SYN+ACK → ACK → RST
全连接队列满时新连接的线上过程:客户端 connect() 成功,随后 send 得 EPIPE、recv 得 ECONNRESET
40 / 40 / 128
40 个并发握手期间 netstat -L 显示的 qlen/incqlen/maxqlen

backlog 的含义

实测 · 本机执行输出
  kern.ipc.somaxconn: 128
  listen(   0):发起  20 个 connect,1 秒后连上  20 个;netstat -L 显示 qlen/incqlen/maxqlen = 20/0/128
  listen(   1):发起  21 个 connect,1 秒后连上   1 个;netstat -L 显示 qlen/incqlen/maxqlen = 1/0/1
  listen(   5):发起  25 个 connect,1 秒后连上   5 个;netstat -L 显示 qlen/incqlen/maxqlen = 5/0/5
  listen(  10):发起  30 个 connect,1 秒后连上  10 个;netstat -L 显示 qlen/incqlen/maxqlen = 10/0/10
  listen(  50):发起  70 个 connect,1 秒后连上  50 个;netstat -L 显示 qlen/incqlen/maxqlen = 50/0/50
  listen( 128):发起 148 个 connect,1 秒后连上 128 个;netstat -L 显示 qlen/incqlen/maxqlen = 128/0/128
  listen(1000):发起 320 个 connect,1 秒后连上 128 个;netstat -L 显示 qlen/incqlen/maxqlen = 128/0/128
  结论:backlog 不是“队列刚好 N 个”:macOS 上 listen(N) 实际能容纳的已完成连接数和 N 的关系见上面(maxqlen 是内核记下的上限,实际连上的数量还多一点)

listen(N) 之后,全连接队列正好容纳 N 个(0 当成默认值 128),上限被 kern.ipc.somaxconn(128)卡住。

队列满了

实测 · 本机执行输出
  塞满以后 netstat -L = 5/0/5(qlen/incqlen/maxqlen)
  队列已满时的第 1 个客户端:connect 成功(用时 0.14 ms);随后 send BrokenPipeError[Errno 32];recv ConnectionResetError[Errno 54];线上:C→S SYN → S→C SYN+ACK → C→S ACK → S→C RST
  队列已满时的第 2 个客户端:connect 成功(用时 0.17 ms);随后 send BrokenPipeError[Errno 32];recv ConnectionResetError[Errno 54];线上:C→S SYN → S→C SYN+ACK → C→S ACK → S→C RST
  队列已满时的第 3 个客户端:connect 成功(用时 0.33 ms);随后 send BrokenPipeError[Errno 32];recv ConnectionResetError[Errno 54];线上:C→S SYN → S→C SYN+ACK → C→S ACK → S→C RST
  netstat -L 此刻 = 5/0/5(队列还是满的);netstat -s -p tcp 的“listen queue overflow”在这一段增加了 0
  服务端 accept 了 1 个腾出一个位置之后,再来一个客户端:连上、能发数据、没有被重置;netstat -L = 5/0/5
  macOS(BSD 系)的行为:队列满时服务端照常回 SYN-ACK,客户端 connect() 成功返回;收到第三个 ACK 时发现全连接队列放不下,发 RST 把这个连接丢掉。客户端以为连上了,第一次读写才发现被重置——和“SYN 被丢弃、客户端不停重传 SYN”(Linux 的常见行为)不同

macOS 不丢 SYN:仍回 SYN-ACK,客户端 connect 成功;第三个 ACK 到达时才发现放不下,用 RST 丢掉。客户端的第一次读写才会报错。netstat -s 的 TCP 计数在这台机器上全是 0,不可用。

两个队列

实测 · 本机执行输出
  listen(1000):400 个 connect,连上 128 个;netstat -L:qlen/incqlen/maxqlen = 128/0/128——maxqlen 被截到了 128,正好是 somaxconn

半连接队列(SYN_RCVD)与全连接队列(ESTABLISHED、等 accept)。netstat -Lan 显示 qlen/incqlen/maxqlen。

排查

实测 · 本机执行输出
  发起 40 个并发连接后,netstat -L 的 qlen/incqlen/maxqlen 随时间:6 ms 0/0/128;235 ms 0/0/128;472 ms 40/40/128;699 ms 40/40/128;928 ms 40/0/128;1150 ms 40/0/128;1388 ms 40/0/128
  incqlen 最大 40(正在握手中的半连接),qlen 最大 40(握手完成、等 accept 的全连接)
  所以有两个队列:半连接队列(SYN_RCVD,握手没完成)和全连接队列(已 ESTABLISHED,等应用 accept);listen 的 backlog 管全连接队列,应用 accept 太慢会让它满,新的 SYN 被丢弃

客户端偶尔 “connection reset by peer” 却没有任何服务端日志,可以先看 netstat -Lan 里这个端口的队列是不是满的。

分步演示

半连接与全连接队列
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在 lo0 上做;第四节的延迟由 netlab 注入;客户端用非阻塞 connect 一口气发起并在 1 秒后统计,个数用 select + SO_ERROR 判断,可能受时序影响一两个;队列满时的行为是 macOS 实测,其它系统不同
    • 没有验证:tcp_syncookies / syncache 的上限(下一篇 SYN Flood 再看);队列满时在 en0 真实网络上的表现;qlen 在握手期间就包含半连接的原因;net.inet.tcp 里和队列有关的其它参数;应用 accept() 很慢时连接的超时行为
    京ICP备2026067502号-1京公网安备11010502063487号