4、TCP四次挥手:FIN、ACK的交互过程、TIME_WAIT状态观察、抓包验证

TCP连接关闭,很多人觉得比建立连接简单。其实不然。

我见过不少新手,写服务端代码时只关注数据收发,连接关闭这块随便写写。结果线上出现大量 TIME_WAIT,端口不够用,服务直接挂掉。嗯,今天我们就用 Wireshark 把四次挥手彻底看透。

4.1 四次挥手的标准流程

TCP是全双工的,所以关闭连接需要双方都同意。说白了,就是四次报文的交换。

核心流程:

  1. 主动关闭方 → 被动关闭方:FIN(Seq=M)
    主动方说:我没数据要发了。
  2. 被动关闭方 → 主动关闭方:ACK(Seq=N, Ack=M+1)
    被动方说:收到,但我还有数据要发。
  3. 被动关闭方 → 主动关闭方:FIN(Seq=N, Ack=M+1)
    被动方说:我的数据也发完了,可以关了。
  4. 主动关闭方 → 被动关闭方:ACK(Seq=M+1, Ack=N+1)
    主动方说:收到,开始等待 2MSL。

你想想看,为什么是四次?因为第二步和第三步不能合并。被动方收到 FIN 后,可能还有数据要发,所以先回 ACK 确认,等自己数据发完再发 FIN。这就是全双工关闭的代价。

4.2 抓包实战:用 Wireshark 看四次挥手

我习惯用 Python 写一个简单的测试脚本,然后抓包观察。这样最直观。

服务端代码(server.py):

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8888))
server.listen(1)
print('Server listening on port 8888...')

conn, addr = server.accept()
print(f'Connected by {addr}')

# 接收数据
data = conn.recv(1024)
print(f'Received: {data.decode()}')

# 服务端主动关闭
conn.close()
server.close()

客户端代码(client.py):

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))

# 发送数据
client.send(b'Hello, Server!')

# 客户端主动关闭
client.close()

运行后,在 Wireshark 中过滤 tcp.port == 8888,你会看到清晰的四次挥手过程。

抓包结果解读:

序号 源 → 目的 Flags Seq Ack 说明
1 Server → Client FIN, ACK M N 服务端发起关闭(第一次挥手)
2 Client → Server ACK N M+1 客户端确认(第二次挥手)
3 Client → Server FIN, ACK N M+1 客户端发起关闭(第三次挥手)
4 Server → Client ACK M+1 N+1 服务端确认(第四次挥手)

小技巧:在 Wireshark 中右键点击 FIN 报文,选择 Follow → TCP Stream,可以直观看到整个连接的生命周期,包括挥手过程的时间线。

4.3 TIME_WAIT 状态:为什么需要等 2MSL?

四次挥手完成后,主动关闭方会进入 TIME_WAIT 状态,持续 2MSL(Maximum Segment Lifetime,最大报文段生存时间)。

为什么会这样?我当年也困惑过。其实有两个原因:

  • 保证最后一个 ACK 能到达被动方。如果 ACK 丢了,被动方会重发 FIN,主动方需要能重新发送 ACK。如果没有 TIME_WAIT,主动方直接关闭,收到重发的 FIN 后会回复 RST,导致被动方异常。
  • 让网络中所有残留报文都消失。2MSL 时间足够让本次连接的所有报文在网络中消亡,避免影响后续使用相同端口的新连接。

我曾经踩过的坑:有一次线上服务频繁重启,发现大量端口处于 TIME_WAIT 状态,导致新连接无法建立。后来加了 SO_REUSEADDR 选项才解决。如果你写高并发服务,一定要考虑 TIME_WAIT 对端口资源的影响。

4.4 用 Wireshark 观察 TIME_WAIT

在抓包中,TIME_WAIT 状态不会直接显示在报文里。但你可以通过观察时间戳来判断。

操作步骤:

  1. 在 Wireshark 中过滤出你的 TCP 流。
  2. 找到第四次挥手(最后一个 ACK)的报文。
  3. 查看该报文之后,是否还有来自被动方的重传 FIN。
  4. 如果没有重传,说明主动方处于 TIME_WAIT 状态,等待 2MSL 后自动关闭。

你可以在 Wireshark 的 Statistics → TCP Stream Graph → Time-Sequence (Stevens) 中看到连接关闭后的时间线,TIME_WAIT 表现为一段空白期。

4.5 四次挥手的异常情况

实际项目中,挥手过程并不总是这么顺利。我遇到过几种常见异常:

  • 半关闭(Half-Close):一方关闭写通道,但还可以读。对应 shutdown(SHUT_WR) 调用。抓包中表现为只发 FIN,不发 RST。
  • 同时关闭:双方同时发送 FIN。这种情况很少见,但 Wireshark 会显示两个 FIN 报文交错。
  • RST 复位:如果一方异常退出,会直接发 RST 而不是 FIN。抓包中看到 RST 标志,说明连接被强制终止。

避坑指南:我曾经调试过一个 HTTP 代理程序,客户端频繁报错 Connection reset by peer。抓包后发现是服务端在收到 FIN 后没有正常完成四次挥手,而是直接发了 RST。原因是服务端代码中调用了 close() 但没处理未发送完的数据。解决方案是在 close() 前先调用 shutdown() 确保数据发送完毕。

4.6 四次挥手知识体系图

下面这张图总结了四次挥手的完整流程、状态变化和关键点。建议你保存下来,调试时对照着看。

TCP四次挥手流程图 TCP四次挥手流程图 主动关闭方 被动关闭方 ESTABLISHED ESTABLISHED ① FIN (Seq=M) FIN_WAIT_1 CLOSE_WAIT ② ACK (Ack=M+1) FIN_WAIT_2 ③ FIN (Seq=N, Ack=M+1) LAST_ACK ④ ACK (Ack=N+1) TIME_WAIT CLOSED 等待 2MSL 2MSL 后自动关闭

4.7 总结

四次挥手是 TCP 连接关闭的标准流程,理解它对于调试网络程序至关重要。记住几个关键点:

  • 主动关闭方进入 TIME_WAIT,等待 2MSL。
  • 被动关闭方收到 FIN 后先回 ACK,再发自己的 FIN。
  • 抓包时注意 FIN、ACK 标志位和序列号的变化。
  • 异常情况(RST、半关闭)需要单独处理。

下次你遇到连接关闭相关的问题,打开 Wireshark 抓个包,对照着这张流程图看,问题基本就能定位了。