1. TCP 三次握手原理回顾:SYN、SYN-ACK、ACK 的交互过程与状态变迁

聊 TCP 超时排查,绕不开三次握手。很多兄弟遇到连接超时,第一反应就是抓包看有没有 SYN,但往往忽略了握手过程中的状态机细节。我个人习惯是,先把握手流程在脑子里过一遍,再去看抓包,这样定位问题会快很多。

1.1 三次握手到底在做什么?

说白了,三次握手就是客户端和服务端互相确认两件事:你的接收能力没问题,我的发送能力也没问题。你想想看,如果只有两次握手,服务端怎么知道客户端已经收到了自己的 SYN-ACK?万一丢了,服务端还傻等着,那就乱套了。

我刚开始做网络运维时,总觉得三次握手太啰嗦。直到有一次线上环境出现大量半连接,排查下来发现是客户端发完 SYN 就挂了,服务端资源被耗尽。嗯,从那以后我再也不敢小看这个流程了。

1.2 握手流程与状态变迁

标准的握手流程是这样的:

  1. 客户端 → 服务端:SYN
    客户端发送 SYN 报文,seq=x。此时客户端进入 SYN_SENT 状态。
  2. 服务端 → 客户端:SYN-ACK
    服务端收到 SYN,回复 SYN-ACK,seq=y,ack=x+1。服务端进入 SYN_RCVD 状态。
  3. 客户端 → 服务端:ACK
    客户端收到 SYN-ACK,回复 ACK,seq=x+1,ack=y+1。客户端进入 ESTABLISHED 状态。服务端收到 ACK 后,也进入 ESTABLISHED 状态。

这里有个关键点:第三步的 ACK 丢了怎么办? 服务端会重传 SYN-ACK,直到超时。我曾经遇到过一个问题,客户端已经认为连接建立成功了,开始发数据,但服务端还在 SYN_RCVD 状态,数据被丢弃,导致业务超时。这就是典型的「半连接」陷阱。

核心要点: 三次握手的本质是「一来一回一确认」。任何一步的报文丢失或延迟,都会导致连接建立失败或异常。

1.3 状态变迁图

下面这张图是我自己整理的,把三次握手的状态变迁画清楚了。你排查超时问题时,可以对照着看当前连接卡在哪个状态。

TCP 三次握手状态变迁图 客户端 CLOSED SYN_SENT ESTABLISHED 服务端 CLOSED SYN_RCVD ESTABLISHED ① SYN (seq=x) ② SYN-ACK (seq=y, ack=x+1) ③ ACK (seq=x+1, ack=y+1) ⚠ 注意:如果第三步 ACK 丢失,服务端会重传 SYN-ACK(默认最多重传 5 次)。 客户端此时已进入 ESTABLISHED,但服务端仍处于 SYN_RCVD,这就是「半连接」状态。

1.4 关键状态说明

状态 含义 常见超时场景
CLOSED 初始状态,连接未建立 服务端端口未监听,客户端发 SYN 后无响应
SYN_SENT 客户端已发 SYN,等待 SYN-ACK 网络丢包或服务端未响应,触发 SYN 重传
SYN_RCVD 服务端已收 SYN,已发 SYN-ACK,等待 ACK 客户端 ACK 丢失,服务端重传 SYN-ACK 直至超时
ESTABLISHED 连接建立成功,可以传输数据 正常状态,但若之前有丢包,可能已进入异常

个人经验: 排查超时问题时,我习惯先用 ss -tna 或 netstat -tna 看当前连接状态。如果发现大量 SYN_RCVD,基本可以断定是 ACK 丢失或客户端异常。如果大量 SYN_SENT,那就要检查网络连通性和服务端端口了。

1.5 握手过程中的超时参数

Linux 内核有几个关键参数控制握手超时行为。我列一下常用的:

参数 默认值 说明
tcp_syn_retries 6 客户端 SYN 重传次数,每次间隔翻倍(1s, 2s, 4s...)
tcp_synack_retries 5 服务端 SYN-ACK 重传次数
tcp_retries1 3 数据重传阈值,超过后通知上层
tcp_retries2 15 数据重传上限,超过后断开连接

避坑指南: 我曾经遇到过一个问题,客户端连接服务端总是超时,抓包发现 SYN 重传了 6 次才放弃,耗时 127 秒。业务方反馈说「连接卡死了」。后来发现是服务端防火墙把 SYN 包丢了。所以排查时,先确认 SYN 有没有到达服务端,别一上来就调内核参数。

1.6 抓包怎么看三次握手?

用 tcpdump 抓包时,重点关注这三个报文:

# 客户端抓包示例
tcpdump -i eth0 -nn 'tcp port 80 and (tcp[tcpflags] & (tcp-syn|tcp-ack) != 0)'

# 输出示例
# 1. 客户端 → 服务端:SYN
# 10:00:00.001 IP 192.168.1.2.50001 > 192.168.1.1.80: Flags [S], seq=1000

# 2. 服务端 → 客户端:SYN-ACK
# 10:00:00.005 IP 192.168.1.1.80 > 192.168.1.2.50001: Flags [S.], seq=2000, ack=1001

# 3. 客户端 → 服务端:ACK
# 10:00:00.006 IP 192.168.1.2.50001 > 192.168.1.1.80: Flags [.], ack=2001

注意看 Flags 字段:[S] 是 SYN,[S.] 是 SYN-ACK,[.] 是 ACK。如果只看到 [S] 没有后续,说明握手卡在了第一步。如果看到 [S.] 但没有 [.],说明 ACK 丢了。

1.7 常见问题速查

  • 问题: 客户端发 SYN 后无任何响应,重传几次后超时。
    可能原因: 服务端端口未监听、防火墙拦截、网络不通。
  • 问题: 客户端收到 SYN-ACK 后发 ACK,但服务端没进入 ESTABLISHED。
    可能原因: ACK 丢失、服务端 SYN_RCVD 队列满。
  • 问题: 握手成功,但后续数据交互超时。
    可能原因: 中间设备修改了 TCP 选项(如 MSS、窗口缩放),导致双方协商不一致。

嗯,三次握手的内容就这些。你把这些状态和参数记牢了,后面排查超时问题会顺手很多。