3. TCP三次握手与四次挥手:握手挥手过程详解、状态变迁图、常见异常场景分析

TCP 的连接管理,说白了就是三次握手建连、四次挥手断连。这两个过程是网络故障排查的基石。我见过不少同事,抓了包看不懂状态机,遇到问题就懵。今天咱们就把这两个过程彻底掰开揉碎,再结合我踩过的坑,让你以后抓包一眼就能看出问题在哪。

3.1 三次握手:建立连接的过程

三次握手的核心目的就两个:确认双方收发能力正常,以及同步初始序列号(ISN)。你想想看,如果连对方能不能收到包都不确定,后续的数据传输就是瞎搞。

3.1.1 握手流程详解

我用一个最经典的场景来说明:客户端(通常叫主动方)向服务器(被动方)发起连接。

  1. 第一次握手(SYN):客户端发送一个 SYN 包,序列号设为 x(随机值)。此时客户端进入 SYN_SENT 状态。
  2. 第二次握手(SYN+ACK):服务器收到后,回复 SYN+ACK 包。确认号 ack = x+1,自己的序列号设为 y(随机值)。服务器进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):客户端收到后,发送 ACK 包。确认号 ack = y+1。此时客户端进入 ESTABLISHED 状态。服务器收到这个 ACK 后,也进入 ESTABLISHED 状态。

嗯,这里要注意:第三次握手是可以携带数据的。但前两次不行,因为连接还没建立。

核心要点:三次握手最少需要 2 个 RTT(往返时间)才能完成。如果网络延迟大,建连就会慢。

3.1.2 状态变迁图

下面这张图是我自己画的,把三次握手的状态变化画得一目了然。你抓包时对照着看,哪个状态卡住了,问题就在哪。

TCP 三次握手状态变迁图 客户端 CLOSED SYN_SENT ESTABLISHED 服务器 LISTEN SYN_RCVD ESTABLISHED ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) ③ ACK (ack=y+1) 关键说明 前两次握手不能携带数据 第三次握手可以携带数据

3.1.3 常见异常场景

我在项目中遇到过几次典型的握手失败,这里列出来给你参考:

异常场景 抓包特征 可能原因
SYN 超时重传 客户端连续发 SYN,无响应 服务器未启动、防火墙拦截、路由不可达
SYN+ACK 重传 服务器发 SYN+ACK,客户端无 ACK 客户端未收到 SYN+ACK、客户端已关闭
RST 重置 收到 RST 包,连接立即终止 端口未监听、连接被拒绝、半连接队列满
半连接攻击 大量 SYN 包,无后续 ACK 恶意攻击、服务器 SYN 队列耗尽

避坑指南:我曾经排查过一个线上故障,客户端连不上服务器,抓包发现客户端一直在发 SYN,服务器没回。查了半天,结果是防火墙把 SYN+ACK 包给丢了。所以遇到 SYN 超时,别光看客户端,也要看服务器端有没有发出 SYN+ACK。

3.2 四次挥手:断开连接的过程

四次挥手比握手复杂一点,因为 TCP 是全双工的,每一方的关闭都需要单独确认。说白了,就是两个人打电话,一个人说“我挂了”,另一个人说“知道了”,然后另一个人也说“我挂了”,第一个人再说“知道了”。

3.2.1 挥手流程详解

假设客户端主动发起关闭:

  1. 第一次挥手(FIN):客户端发送 FIN 包,序列号为 u。客户端进入 FIN_WAIT_1 状态。
  2. 第二次挥手(ACK):服务器回复 ACK,确认号 ack = u+1。服务器进入 CLOSE_WAIT 状态。客户端收到后进入 FIN_WAIT_2 状态。
  3. 第三次挥手(FIN):服务器发送 FIN 包,序列号为 v。服务器进入 LAST_ACK 状态。
  4. 第四次挥手(ACK):客户端回复 ACK,确认号 ack = v+1。客户端进入 TIME_WAIT 状态,等待 2MSL 后关闭。服务器收到 ACK 后进入 CLOSED 状态。

个人经验:我习惯在抓包时重点关注 CLOSE_WAIT 和 TIME_WAIT 这两个状态。CLOSE_WAIT 太多,说明服务器程序没调用 close();TIME_WAIT 太多,说明短连接太频繁。

3.2.2 状态变迁图

这张图把四次挥手的状态变化画清楚了。你抓包时看到哪个状态卡住,就能快速定位问题。

TCP 四次挥手状态变迁图 客户端 ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT CLOSED 服务器 ESTABLISHED CLOSE_WAIT LAST_ACK CLOSED ① FIN (seq=u) ② ACK (ack=u+1) ③ FIN (seq=v) ④ ACK (ack=v+1) 关键说明 TIME_WAIT 持续 2MSL(约 2 分钟) CLOSE_WAIT 表示服务器等待应用程序关闭

3.2.3 常见异常场景

挥手阶段的异常比握手更隐蔽,我踩过不少坑:

异常场景 抓包特征 可能原因
CLOSE_WAIT 堆积 服务器大量连接处于 CLOSE_WAIT 应用程序未调用 close(),资源泄漏
TIME_WAIT 过多 客户端大量连接处于 TIME_WAIT 短连接频繁,系统参数未优化
FIN 重传 一方反复发 FIN,无 ACK 网络丢包、对端已崩溃
半关闭状态 一方关闭,另一方未关闭 应用程序逻辑错误、协议实现差异

避坑指南:我曾经遇到过一个 Web 服务器,运行几天后连接数暴涨,最后 OOM 挂了。抓包一看,全是 CLOSE_WAIT 状态的连接。原因是服务器代码里有个异常分支没调 close(),导致连接一直挂着。所以排查 CLOSE_WAIT 时,直接去查应用程序的 close() 调用是否完整。

3.3 抓包实战:如何快速定位问题

说了这么多理论,咱们来点实际的。你抓包时,重点关注以下几点:

  • 看状态码:SYN、SYN+ACK、ACK、FIN、RST 这些标志位,一眼就能看出当前处于哪个阶段。
  • 看序列号:确认号是否正确,有没有丢包重传。
  • 看时间戳:每次握手/挥手之间的时间间隔,如果超过 1 秒,大概率有问题。
  • 看重传:同一个包出现多次,说明网络不稳定或对端没收到。

个人习惯:我抓包时喜欢用 Wireshark 的过滤表达式。比如只看三次握手,就用 tcp.flags.syn == 1 and tcp.flags.ack == 0。只看四次挥手,就用 tcp.flags.fin == 1。这样能快速过滤出关键包,省得在几千个包里翻来翻去。

3.4 总结