TCP 三次握手:连接建立失败的那些坑

文章目录

引言

线上告警响起:大量请求超时,错误日志里清一色写着 Connection refused。SSH 登录服务器查了一圈,端口开着,服务在跑,防火墙也没动过------问题到底在哪?

连接建立失败的根因和表现症状之间往往隔着一层。这篇文章聚焦 TCP 三次握手,把全连接队列和半连接队列的区别讲清楚,把 ss -s 里那几个让人摸不着头脑的指标解释明白,再看几个真实故障场景是怎么一步步走到"Connection refused"的。


一、TCP 状态机:连接的生命周期

在说握手之前,先把 TCP 的状态机看清楚。整个 TCP 连接经历三个阶段:建立连接(三次握手)、数据传输、四次挥手。三次握手发生在 ESTABLISHED 之前,挥手发生在之后。每个阶段都对应着明确的状态,状态之间的转移规则是排查连接问题的底层依据。
关闭连接
建立连接
客户端

发送 SYN
服务端

收到 SYN 并回复
客户端

收到 SYN+SYN/ACK 并回复 ACK
服务端

收到 ACK
客户端发送 FIN
服务端发送 FIN
收到 ACK
收到 FIN
等待 2MSL
应用层

close()
收到 ACK
CLOSED
SYN_SENT
LISTEN
SYN_RCVD
ESTABLISHED
FIN_WAIT_1
FIN_WAIT_2
TIME_WAIT
CLOSE_WAIT
LAST_ACK

客户端的主动路径是 CLOSED → SYN_SENT → ESTABLISHED,服务端的被动路径是 CLOSED → LISTEN → SYN_RCVD → ESTABLISHED。LISTEN 状态是服务端独有的------进程调用 listen() 之后进入这个状态,等待客户端的 SYN 包到达。

TIME_WAIT 是一个容易被忽视的状态。四次挥手完成后,主动关闭的一方会停留在这个状态持续 2MSL(Maximum Segment Lifetime,通常是 60 秒)。这个等待时间有两个作用:一是确保对方收到最后的 ACK(如果丢失,对方会重发 FIN);二是让网络里所有本连接的延迟包过期,避免它们被后续新建的连接错误接收。下一篇文章会专门展开 TIME_WAIT 的处理策略。


二、三次握手:每一步的序列号与状态变化

三次握手不只是一个"SYN → SYN/ACK → ACK"的序列,它背后有完整的序列号协商和可靠性设计。
服务端 客户端 服务端 客户端 CLOSED,TCP 控制块 TCB 未分配 SYN_SENT,发送窗口打开 收到 SYN,进入 SYN_RCVD 半连接队列(SYN Queue)写入记录 发送 SYN/ACK 后 半连接队列等待 ACK 收到 SYN/ACK,进入 ESTABLISHED 发送 ACK,ack=s+1 收到 ACK,进入 ESTABLISHED 全连接队列(Accept Queue)写入已完成 socket 应用层 accept() 从全连接队列取走连接 SYN,seq=c SYN/ACK,seq=s,ack=c+1 ACK,ack=s+1

第一次握手(SYN):客户端随机生成初始序列号 ISN(Initial Sequence Number),发送 SYN 包。此时客户端为这个连接分配 TCP 控制块(TCB),进入 SYN_SENT 状态。

第二次握手(SYN/ACK) :服务端同样随机生成自己的 ISN,回复 SYN/ACK 包。ack=c+1 表示"我已收到你的 c,期待下一个字节是 c+1"------这是累计确认机制的起点。服务端将这个半连接(只完成了两次握手,还差最后一次 ACK)存入 半连接队列(SYN Queue),状态切换为 SYN_RCVD。

第三次握手(ACK) :客户端发送 ACK,ack=s+1 表示"我已收到你的 s,期待下一个字节是 s+1"。ACK 到达后,服务端将这个连接从半连接队列移到 全连接队列(Accept Queue) ,正式进入 ESTABLISHED 状态,等待应用层调用 accept() 取走。

为什么要三次? 两次握手只能证明客户端的发送能力和服务端的接收能力。第三次握手额外证明了服务端的发送能力和客户端的接收能力------双方都确认了对方既有能力发送,也有能力接收,三次是确认"双向通道都畅通"的最小次数。

序列号的作用 不仅是确认,它还承担了防回绕(PAWS)的职责。每次连接使用不同的 ISN,网络中延迟的旧数据包不会被误认为是新连接的合法数据。ISN 不是从 0 或 1 开始的,而是通过哈希算法和时间戳生成的随机数。


三、两个队列:半连接与全连接

全连接队列和半连接队列是握手中最核心的两个数据结构,也是连接建立失败最常见的根因所在。

半连接队列(SYN Queue) 存在于服务端收到 SYN、回复 SYN/ACK、但尚未收到 ACK 的这段时间。这个队列的长度由 net.ipv4.tcp_max_syn_backlog 控制(受 somaxconn 上限约束)。如果队列满了,新到来的 SYN 会被直接丢弃------客户端会看到"连接超时"而不是"连接被拒"。

全连接队列(Accept Queue) 存在于三次握手完成、但应用层尚未调用 accept() 读取连接的阶段。队列长度由 listen(fd, backlog) 的 backlog 参数控制。这是生产环境最常见的坑 :代码里写的 listen(fd, 5) 实际上受 /proc/sys/net/core/somaxconn 上限约束,在 Linux 上默认是 128。如果应用层处理速度跟不上连接建立速度,全连接队列会堆积,新连接在三次握手完成后仍然被丢弃------客户端看到的是 "Connection refused" 而不是超时。

ss -ltnp 可以直接看到这两个队列的状态:

bash 复制代码
$ ss -ltnp
State      Recv-Q   Send-Q   Local Address:Port   Peer Address:Port   Process
LISTEN     0        128      0.0.0.0:22           0.0.0.0:*           users:(("sshd",pid=1234,fd=3))
LISTEN     0        511      0.0.0.0:443          0.0.0.0:*           users:(("nginx",pid=5678,fd=6))
  • Send-Q 是全连接队列的 backlog 上限(已与 somaxconn 取了最小值)
  • Recv-Q 是全连接队列当前的堆积数量(已排队等待 accept 的连接数)

如果 Recv-Q 持续大于 Send-Q 的 50%,说明应用层 accept() 跟不上连接建立速度,需要关注应用的并发处理能力或者扩大 backlog。

ss -s 输出里的两个关键指标揭示了更细粒度的问题:

bash 复制代码
$ ss -s
Total: 432 (kernel 612)
TCP:   284 (estab 12, closed 156, orphaned 0, synrecv 0, timewait 2)

Transport Total     IP        IPv6
*         612      -         -
RAW       0        0         0
UDP       6        3         3
TCP       284      138        146
INET      290      141        149
FRAG      0        0         0

synrecv 0 表示当前半连接队列中没有积压的连接(这个数字在 SYN Flood 攻击时会飙升)。timewait 2 表示当前有 2 个连接处于 TIME_WAIT 状态------这个数字在关闭连接后如果长期不下降,会消耗系统资源。


四、五种连接建立失败的根因

根因一:全连接队列满(最常见)

表现 :客户端收到 "Connection refused"(立即返回),服务端 ss -ltnp 显示Recv-Q 持续等于或接近 Send-Q。




三次握手完成

连接进入全连接队列
Recv-Q < Send-Q?
应用层 accept() 正常消费
全连接队列已满?
新 ACK 被丢弃
客户端:Connection refused
内核暂时无法处理

重传后可能成功

场景 :Nginx upstream 配置的 backlog 值太小,或者 PHP-FPM 的 pm.max_children 满了导致 PHP 进程无法及时 accept 新连接,而 Nginx 侧已经把 backlog 设得很大。

排查命令

bash 复制代码
# 持续观察全连接队列堆积
watch -n 1 'ss -ltnp state established'

# 查看 ListenOverflows 计数(半连接队列溢出)
ss -s

解决 :调大应用层 backlog(listen(fd, 4096)),同时确保 somaxconn 不构成上限(sysctl -w net.core.somaxconn=4096)。

根因二:半连接队列溢出(SYN Flood 攻击)

表现 :客户端看到"连接超时"(等待很久后返回),服务端 ss -s 显示 synrecv 数字持续偏高,dmesg 可能出现 possible SYN flooding on port X 提示。

场景:恶意客户端发送大量 SYN 但不回复 SYN/ACK,半连接队列被伪造的连接占满,合法用户的 SYN 被丢弃。

解决 :开启 net.ipv4.tcp_syncookies=1,用 SYN Cookie 机制代替半连接队列------服务端不存储半连接状态,而是把连接信息编码进 SYN/ACK 的序列号里,第三次握手到来时再验证。这个机制有代价:某些 TCP 扩展(如窗口缩放、SACK)无法在 Cookie 里编码,因此会被降级禁用。具体内容在 #11 SYN Flood 攻防文章中展开。

根因三:客户端端口耗尽

表现 :客户端建立大量短连接时,新连接开始报错 Cannot assign requested address,或者连接延迟突然增加。

原理:客户端每次发起连接,内核分配一个临时源端口(通常是 32768~60999范围的随机端口)。单机对外发起连接的并发数上限就是这个范围内的可用端口数量,约 28000 个。TIME_WAIT 堆积会进一步减少可用端口。

bash 复制代码
# 查看当前可用端口范围
$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768    60999

# 快速查看 TIME_WAIT 连接数
$ ss -s | grep timewait
TCP:   284 (estab 12, closed 156, orphaned 0, synrecv 0, timewait 156)

解决 :开启 net.ipv4.tcp_tw_reuse=1 允许新连接复用处于 TIME_WAIT 的端口(需要双方都支持 RFC 1323 时间戳选项);或者调大端口范围 sysctl -w net.ipv4.ip_local_port_range="1024 65535"(但太小的端口可能被其他服务占用)。

根因四:PAWS 与时间戳导致的连接建立异常

原理:PAWS(Protect Against Wrapped Sequence numbers)依赖 TCP 时间戳选项(RFC 1323),在高速网络中防止旧的重复数据包被误认为属于新连接。

异常场景 :某些老旧防火墙或 NAT 设备在转发时会错误地修改或剥离 TCP 时间戳选项,导致 PAWS 验证失败。即便三次握手成功,后续数据传输阶段可能出现奇怪的 ACK 不被接受、数据重传不断触发等问题。Linux 默认开启 tcp_timestamps=1,可以通过 sysctl net.ipv4.tcp_timestamps 确认。

排查tcpdump 抓包观察 SYN/SYN-ACK 包中是否包含时间戳选项(以 [PAWS] 标记),以及是否有重传异常。

根因五:TCP Fast Open 首次握手失败

原理:TCP Fast Open(TFO)允许在第一次握手的 SYN 包里携带 HTTP 请求数据,省掉一次 RTT。但这个优化有一个前提:客户端必须已经成功完成过到同一个服务端的 TFO 握手,服务端将这个客户端的信息记录在 TFO cookie 里。

失败场景:首次连接 TFO cookie 不存在,服务端降级为普通三次握手,客户端收到服务端发送的 TFO cookie 并缓存。之后再次连接时,客户端在 SYN 包中携带缓存的 cookie,如果 cookie 有效,服务端直接发送数据(如果还没收到 ACK,会先发 SYN/ACK)。

确认命令

bash 复制代码
# 查看 TFO 启用状态
$ sysctl net.ipv4.tcp_fastopen
net.ipv4.tcp_fastopen = 3   # 3 = 客户端和服务器端都启用

# 用 curl 测试 TFO(需要服务端支持)
$ curl --fastopen -v https://example.com/

五、连接建立完整流程图

把上面的知识点整合成一张完整的决策图,作为排查握手问题的参照:












客户端发起连接

SYN 发出
服务端收到 SYN?
客户端:连接超时

检查网络路由
服务端 SYN/ACK 回复

半连接队列写入
半连接队列满?
SYN 丢弃

SYN Cookie 启用?
第三次 ACK 到达?
SYN Cookie 机制

降级处理
连接移入

全连接队列
全连接队列满?
ACK 丢弃

客户端:Connection refused
应用层 accept()?
队列持续堆积

Recv-Q > Send-Q
连接交付应用

ESTABLISHED
排查:ping/路由/

防火墙规则
排查:ss -ltnp /

应用层 backlog / somaxconn


六、知识框架

TCP 三次握手

与连接失败
TCP 状态机
CLOSED / LISTEN / SYN_SENT / SYN_RCVD
ESTABLISHED / FIN_WAIT_1/2 / TIME_WAIT
CLOSE_WAIT / LAST_ACK
状态转移规则
三次握手详解
ISN 初始序列号生成
序列号与确认机制
SYN / SYN+ACK / ACK 三次交互
为什么需要三次
两个队列
半连接队列(SYN Queue)
全连接队列(Accept Queue)
somaxconn 上限约束
ss -ltnp 状态解读
连接失败根因
全连接队列满 · Connection refused
半连接队列溢出 · SYN Cookie
客户端端口耗尽 · tcp_tw_reuse
PAWS 时间戳 · tcp_timestamps
TCP Fast Open · TFO Cookie
排查命令
ss -ltnp · ss -s
tcpdump SYN 包
sysctl tcp_* 参数
netstat -an | grep TIME_WAIT


总结

三次握手不是"SYN → SYN/ACK → ACK"三个包那么简单。它的背后是序列号协商、两个独立队列、半连接与全连接的状态转移,以及多个可调节的内核参数。

Connection refused 和连接超时对应着完全不同的根因:前者是三次握手完成后、全连接队列满了;后者是 SYN 包根本没到或者半连接队列满了。搞清楚这个区别,排查效率能提升一个量级。

下一篇文章继续深入 TCP:四次挥手与 TIME_WAIT。TIME_WAIT 堆积什么时候是问题、什么时候不是问题,tw_reuse 到底有没有用,SO_REUSEADDR 和 SO_REUSEPORT 的区别是什么------这些问题搞清楚了,连接关闭相关的故障就不再是黑盒。

相关推荐
Neolnfra2 小时前
华为ensp交换机与路由器常用命令速查手册
网络协议·ensp·华为ensp
我叫张土豆3 小时前
从 SSE 到 Streamable HTTP:AI 时代的协议演进之路
人工智能·网络协议·http
code tsunami3 小时前
如何在车辆数据自动化中解决Cloudflare Turnstile
运维·microsoft·自动化
翼龙云_cloud4 小时前
亚马逊云代理商:CloudWatch Agent 全解析 5 步实现服务器监控
运维·服务器·云计算·aws·云服务器
Cyber4K4 小时前
【Nginx专项】基础入门篇:状态页、微更新、内容替换、读取、压缩及防盗链
linux·运维·服务器·nginx·github
北京耐用通信5 小时前
国产优选:耐达讯自动化EtherCAT转RS232在工业协议转换中的卓越表现
人工智能·科技·物联网·网络协议·自动化
门思科技6 小时前
LoRaWAN项目无需NS和平台?一体化网关如何简化部署与成本
服务器·网络·物联网
Bruce_Liuxiaowei6 小时前
顺藤摸瓜:一次从防火墙告警到设备实物的溯源实战
运维·网络·网络协议·安全
IpdataCloud6 小时前
效果广告中点击IP与转化IP不一致?用IP查询怎么做归因分析?
运维·服务器·网络