4、TCP四次挥手:从挥手到告别
TCP连接建立要三次握手,断开连接呢?四次挥手。
我刚开始学网络时,总觉得挥手比握手复杂。握手是双方确认,挥手却是双方都要说再见。你想想看,一个连接要断开,得双方都同意才行。这就是为什么需要四次交互。
4.1 四次挥手的完整过程
假设客户端主动发起断开连接。整个过程是这样的:
- 第一次挥手(FIN):客户端发送 FIN 包,告诉服务器“我没数据要发了”。此时客户端进入 FIN_WAIT_1 状态。
- 第二次挥手(ACK):服务器回复 ACK,表示“收到你的 FIN”。服务器进入 CLOSE_WAIT 状态。客户端收到 ACK 后进入 FIN_WAIT_2 状态。
- 第三次挥手(FIN):服务器处理完剩余数据后,发送 FIN 包,告诉客户端“我也没数据要发了”。服务器进入 LAST_ACK 状态。
- 第四次挥手(ACK):客户端回复 ACK,表示“收到你的 FIN”。客户端进入 TIME_WAIT 状态。服务器收到 ACK 后关闭连接。
核心要点:四次挥手的关键在于,服务器收到 FIN 后不会立即关闭,而是先回复 ACK,等自己数据发完再发 FIN。这个“半关闭”状态就是 CLOSE_WAIT。
我遇到过一个问题:服务器上 CLOSE_WAIT 状态的连接堆积如山。排查后发现,是应用层忘记调用 close() 了。嗯,这个坑后面会细说。
4.2 状态变迁与 TIME_WAIT 详解
四次挥手涉及的状态变化,我整理了一张表:
| 状态 | 谁进入 | 触发条件 | 说明 |
|---|---|---|---|
| FIN_WAIT_1 | 主动关闭方 | 发送 FIN 后 | 等待对方的 ACK |
| CLOSE_WAIT | 被动关闭方 | 收到 FIN 后 | 等待应用层关闭 |
| FIN_WAIT_2 | 主动关闭方 | 收到 ACK 后 | 等待对方的 FIN |
| LAST_ACK | 被动关闭方 | 发送 FIN 后 | 等待对方的 ACK |
| TIME_WAIT | 主动关闭方 | 发送 ACK 后 | 等待 2MSL 后关闭 |
| CLOSED | 双方 | 收到 ACK 或超时 | 连接彻底结束 |
这里最让人头疼的就是 TIME_WAIT。为什么要有这个状态?
两个原因:
- 保证最后一个 ACK 能到达对方。如果 ACK 丢了,服务器会重发 FIN,客户端需要重新回复 ACK。
- 让旧连接的报文在网络中消失。防止新连接收到旧连接的残留数据。
TIME_WAIT 持续 2MSL(Maximum Segment Lifetime)。MSL 一般是 30 秒或 1 分钟,所以 TIME_WAIT 通常持续 1 到 4 分钟。
注意:TIME_WAIT 过多会消耗系统资源。我曾经在压测时遇到端口耗尽,就是因为大量 TIME_WAIT 连接占用了端口。解决方案是调整内核参数,或者开启 SO_REUSEADDR。
4.3 使用 Wireshark 观察挥手过程
光看理论不过瘾。我们来抓个包看看。
我习惯用 curl 访问一个 HTTP 服务,然后抓包分析挥手过程。命令很简单:
# 在终端1启动抓包
sudo tcpdump -i lo -w wave.pcap port 8080
# 在终端2发起请求
curl http://localhost:8080/test
# 停止抓包,用 Wireshark 打开 wave.pcap
在 Wireshark 中,过滤 tcp.flags.fin == 1 就能看到挥手包。典型的四次挥手长这样:
No. Time Source Destination Protocol Info
1 0.000 127.0.0.1 127.0.0.1 TCP 80 → 54321 [FIN, ACK] Seq=1 Ack=1
2 0.001 127.0.0.1 127.0.0.1 TCP 54321 → 80 [ACK] Seq=1 Ack=2
3 0.002 127.0.0.1 127.0.0.1 TCP 54321 → 80 [FIN, ACK] Seq=1 Ack=2
4 0.003 127.0.0.1 127.0.0.1 TCP 80 → 54321 [ACK] Seq=2 Ack=2
注意看:第一次挥手是 [FIN, ACK],不是单纯的 FIN。这是因为 TCP 允许在发送 FIN 的同时确认之前收到的数据。实际抓包中,很多实现会把 FIN 和 ACK 合并。
我建议你重点关注几个点:
- Seq 和 Ack 的变化:每次挥手后,序号和确认号如何递增。
- 时间间隔:正常情况下挥手很快,毫秒级完成。
- 是否有重传:如果 ACK 丢了,FIN 会重传。
4.4 挥手过程的完整流程图
下面这张 SVG 图展示了四次挥手的完整流程和状态变迁:
4.5 常见问题与避坑指南
实际工作中,挥手过程经常出问题。我总结几个常见场景:
场景一:CLOSE_WAIT 堆积
服务器收到 FIN 后进入 CLOSE_WAIT,但应用层没调用 close()。结果就是连接一直挂着。我曾经在 Java 应用里遇到过,原因是代码没正确处理 Socket 关闭。排查方法:用 netstat -anp | grep CLOSE_WAIT 看哪些进程在占用。
场景二:TIME_WAIT 过多
高并发短连接场景下,主动关闭方会产生大量 TIME_WAIT。我建议调整内核参数:
# 减少 TIME_WAIT 持续时间
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
# 开启 TIME_WAIT 复用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 开启快速回收(内核 4.12 后已移除)
# echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle
注意:tcp_tw_recycle 在 NAT 环境下有问题,新版内核已经去掉了。
场景三:挥手丢包
如果第四次挥手的 ACK 丢了,服务器会重发 FIN。客户端收到重发的 FIN 后,会重新回复 ACK 并重置 TIME_WAIT 计时器。这就是为什么 TIME_WAIT 要持续 2MSL——确保即使 ACK 丢了,也有足够时间处理重传。
4.6 抓包实战技巧
用 Wireshark 分析挥手时,我习惯这么做:
- 设置过滤条件:
tcp.flags.fin == 1 or tcp.flags.reset == 1,只看挥手和 RST 包。 - 关注时间列:正常挥手间隔很短。如果间隔超过几秒,说明有问题。
- 查看 Seq/Ack:确认序号变化是否符合预期。Seq 和 Ack 应该严格递增。
- 检查重传:Wireshark 会用黑色标记重传包。如果看到 FIN 重传,说明 ACK 丢了。
有一次,我发现一个 HTTP 服务响应特别慢。抓包一看,服务器收到 FIN 后过了 30 秒才回复。嗯,原来是应用层在处理完请求后忘记关闭连接,导致 CLOSE_WAIT 状态一直挂着。修复后,响应时间从 30 秒降到了 10 毫秒。
说白了,四次挥手就是双方互相确认“再见”的过程。理解状态变迁,学会抓包分析,你就能快速定位连接断开相关的问题。