3. TCP三次握手与四次挥手:握手挥手过程详解、状态变迁图、常见异常场景分析
TCP 的连接管理,说白了就是三次握手建连、四次挥手断连。这两个过程是网络故障排查的基石。我见过不少同事,抓了包看不懂状态机,遇到问题就懵。今天咱们就把这两个过程彻底掰开揉碎,再结合我踩过的坑,让你以后抓包一眼就能看出问题在哪。
3.1 三次握手:建立连接的过程
三次握手的核心目的就两个:确认双方收发能力正常,以及同步初始序列号(ISN)。你想想看,如果连对方能不能收到包都不确定,后续的数据传输就是瞎搞。
3.1.1 握手流程详解
我用一个最经典的场景来说明:客户端(通常叫主动方)向服务器(被动方)发起连接。
- 第一次握手(SYN):客户端发送一个 SYN 包,序列号设为 x(随机值)。此时客户端进入
SYN_SENT状态。 - 第二次握手(SYN+ACK):服务器收到后,回复 SYN+ACK 包。确认号 ack = x+1,自己的序列号设为 y(随机值)。服务器进入
SYN_RCVD状态。 - 第三次握手(ACK):客户端收到后,发送 ACK 包。确认号 ack = y+1。此时客户端进入
ESTABLISHED状态。服务器收到这个 ACK 后,也进入ESTABLISHED状态。
嗯,这里要注意:第三次握手是可以携带数据的。但前两次不行,因为连接还没建立。
核心要点:三次握手最少需要 2 个 RTT(往返时间)才能完成。如果网络延迟大,建连就会慢。
3.1.2 状态变迁图
下面这张图是我自己画的,把三次握手的状态变化画得一目了然。你抓包时对照着看,哪个状态卡住了,问题就在哪。
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 挥手流程详解
假设客户端主动发起关闭:
- 第一次挥手(FIN):客户端发送 FIN 包,序列号为 u。客户端进入
FIN_WAIT_1状态。 - 第二次挥手(ACK):服务器回复 ACK,确认号 ack = u+1。服务器进入
CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态。 - 第三次挥手(FIN):服务器发送 FIN 包,序列号为 v。服务器进入
LAST_ACK状态。 - 第四次挥手(ACK):客户端回复 ACK,确认号 ack = v+1。客户端进入
TIME_WAIT状态,等待 2MSL 后关闭。服务器收到 ACK 后进入CLOSED状态。
个人经验:我习惯在抓包时重点关注 CLOSE_WAIT 和 TIME_WAIT 这两个状态。CLOSE_WAIT 太多,说明服务器程序没调用 close();TIME_WAIT 太多,说明短连接太频繁。
3.2.2 状态变迁图
这张图把四次挥手的状态变化画清楚了。你抓包时看到哪个状态卡住,就能快速定位问题。
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。这样能快速过滤出关键包,省得在几千个包里翻来翻去。