文章目录
- [TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT](#TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT)
-
- 一、先给出最核心的结论
- 二、分析前的序号约定
- [三、第一次挥手:主动关闭方发送 FIN](#三、第一次挥手:主动关闭方发送 FIN)
-
- [1. 报文方向](#1. 报文方向)
- [2. 典型首部字段](#2. 典型首部字段)
- [3. 状态变化](#3. 状态变化)
- [4. FIN 的真实含义](#4. FIN 的真实含义)
- [四、第二次挥手:被动关闭方确认 FIN](#四、第二次挥手:被动关闭方确认 FIN)
-
- [1. 报文方向](#1. 报文方向)
- [2. 典型首部字段](#2. 典型首部字段)
- [3. 状态变化](#3. 状态变化)
- [4. 此时形成半关闭状态](#4. 此时形成半关闭状态)
- [5. CLOSE_WAIT 与应用程序的关系](#5. CLOSE_WAIT 与应用程序的关系)
- [五、第三次挥手:被动关闭方发送自己的 FIN](#五、第三次挥手:被动关闭方发送自己的 FIN)
-
- [1. 报文方向](#1. 报文方向)
- [2. 典型首部字段](#2. 典型首部字段)
- [3. 状态变化](#3. 状态变化)
- [六、第四次挥手:主动关闭方发送最终 ACK](#六、第四次挥手:主动关闭方发送最终 ACK)
-
- [1. 报文方向](#1. 报文方向)
- [2. 典型首部字段](#2. 典型首部字段)
- [3. 状态变化](#3. 状态变化)
- [七、为什么主动关闭方要进入 TIME_WAIT](#七、为什么主动关闭方要进入 TIME_WAIT)
-
- [原因一:保证最后一个 ACK 可以被补发](#原因一:保证最后一个 ACK 可以被补发)
- 原因二:让旧连接的重复报文在网络中消失
- [不要把 2MSL 固定理解成"四分钟"](#不要把 2MSL 固定理解成“四分钟”)
- 八、为什么通常是四次,而建立连接是三次
- 九、状态迁移总表
- 十、丢包和重传时会发生什么
-
- [1. 第一次 FIN 丢失](#1. 第一次 FIN 丢失)
- [2. 第二次 ACK 丢失](#2. 第二次 ACK 丢失)
- [3. 第三次 FIN 丢失](#3. 第三次 FIN 丢失)
- [4. 第四次 ACK 丢失](#4. 第四次 ACK 丢失)
- [十一、同时关闭:双方都主动发送 FIN](#十一、同时关闭:双方都主动发送 FIN)
- [十二、RST 不是四次挥手](#十二、RST 不是四次挥手)
- [十三、与 Linux Socket API 的对应关系](#十三、与 Linux Socket API 的对应关系)
- 十四、常见错误理解
-
- [错误 1:客户端一定是主动关闭方](#错误 1:客户端一定是主动关闭方)
- [错误 2:收到 FIN 后不能再发送数据](#错误 2:收到 FIN 后不能再发送数据)
- [错误 3:FIN 报文不能携带数据](#错误 3:FIN 报文不能携带数据)
- [错误 4:ACK 也会消耗一个序号](#错误 4:ACK 也会消耗一个序号)
- [错误 5:TIME_WAIT 一定出现在客户端](#错误 5:TIME_WAIT 一定出现在客户端)
- [错误 6:看到很多 TIME_WAIT 就说明程序泄漏](#错误 6:看到很多 TIME_WAIT 就说明程序泄漏)
- [错误 7:看到很多 CLOSE_WAIT 是网络太慢](#错误 7:看到很多 CLOSE_WAIT 是网络太慢)
- 十五、一句话记忆
- 参考资料
TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT
本文以"一端主动关闭、另一端被动关闭"的正常释放流程为主线。配图截取自所提供课件《第5章 运输层.pptx》的 TCP 连接释放部分,并结合 RFC 9293 对序号、半关闭、状态迁移和异常情况进行补充修正。
一、先给出最核心的结论
TCP 是全双工字节流协议。一条 TCP 连接中实际上存在两个相互独立的数据传输方向:
text
主机 A ───────────────→ 主机 B
主机 A ←─────────────── 主机 B
关闭其中一个方向,并不代表另一个方向也已经关闭。因此,正常释放连接时通常需要:
- 主动关闭方发送
FIN,表示"我没有更多数据要发送"; - 被动关闭方发送
ACK,确认已经收到这个FIN; - 被动关闭方处理完剩余数据后,也发送自己的
FIN; - 主动关闭方发送最后一个
ACK。
这就是通常所说的 TCP 四次挥手。
需要注意:四次挥手是典型的正常关闭过程,不表示网络上任何一次 TCP 关闭都一定恰好出现四个独立报文段。 如果第二次的 ACK 与第三次的 FIN 被合并发送,抓包中可能只看到三个报文段;如果报文丢失并发生重传,则可能看到五个甚至更多报文段。
二、分析前的序号约定
假设连接已经建立,主机 A 主动关闭,主机 B 被动关闭:
- A 当前下一个可用发送序号为
u; - A 当前期望从 B 收到的下一个字节序号为
v; - B 在半关闭期间可能继续发送数据,因此 B 最终发送
FIN时的序号记为w。
在不携带数据的典型情况下,四个报文段可写成:
text
A B
│ │
│ FIN=1, ACK=1, seq=u, ack=v │
│────────────────────────────────────────────────────>│
│ │
│ ACK=1, seq=v, ack=u+1 │
│<────────────────────────────────────────────────────│
│ │
│ B 仍然可以继续向 A 发送剩余数据 │
│<────────────────────────────────────────────────────│
│ │
│ FIN=1, ACK=1, seq=w, ack=u+1 │
│<────────────────────────────────────────────────────│
│ │
│ ACK=1, seq=u+1, ack=w+1 │
│────────────────────────────────────────────────────>│
│ │
两条序号规则
-
FIN会占用一个序号。因此,对
seq=u的FIN进行确认时,确认号是u+1。 -
纯
ACK不占用序号。第二次和第四次挥手若不携带数据,其发送序号不会因为发送 ACK 而增加。
RFC 9293 将 SYN 和 FIN 都计入 TCP 序号空间。FIN 在逻辑上位于该报文段最后一个数据字节之后,因此 FIN 也可以和数据放在同一个 TCP 报文段中。
三、第一次挥手:主动关闭方发送 FIN
1. 报文方向
text
主机 A ───────── FIN, ACK ─────────> 主机 B
2. 典型首部字段
text
FIN = 1
ACK = 1
seq = u
ack = v
含义如下:
FIN=1:A 表示自己已经没有更多数据需要发送;ACK=1:连接已经处于已建立阶段,确认号字段有效;seq=u:该 FIN 使用 A 当前下一个发送序号;ack=v:A 仍然确认自己已经正确收到 B 在v之前的全部字节。
3. 状态变化
text
A:ESTABLISHED → FIN-WAIT-1
B:仍处于 ESTABLISHED
A 进入 FIN-WAIT-1 后,等待 B 对该 FIN 的确认。
4. FIN 的真实含义
FIN 表示:
本端在这个发送方向上不会再发送新的字节。
它并不表示:
- 本端不能继续接收数据;
- 整条 TCP 连接已经立即消失;
- 对端必须立刻发送自己的 FIN;
- 当前 FIN 报文段不能携带数据。
如果 A 的 FIN 报文段还携带了 N 字节数据,且第一个数据字节序号为 u,那么 B 对其完整确认时应发送:
text
ack = u + N + 1
其中 N 来自数据,额外的 1 来自 FIN。
四、第二次挥手:被动关闭方确认 FIN
1. 报文方向
text
主机 A <──────────── ACK ─────────── 主机 B
2. 典型首部字段
text
ACK = 1
seq = v
ack = u + 1
其中:
ack=u+1表示 B 已经收到 A 的 FIN;seq=v表示该 ACK 自身不消耗序号,B 当前下一个发送序号仍然是v,除非同时携带数据。
3. 状态变化
B 收到 FIN 并发送 ACK 后:
text
B:ESTABLISHED → CLOSE-WAIT
A 收到该 ACK 后:
text
A:FIN-WAIT-1 → FIN-WAIT-2
4. 此时形成半关闭状态
此时:
text
A → B:已经关闭
B → A:仍然可以传输数据
A 已经承诺不再发送数据,但仍然必须继续接收 B 发来的数据。B 的应用程序也可能还有响应、日志、文件尾部或协议结束信息尚未发送。

5. CLOSE_WAIT 与应用程序的关系
当 B 的 TCP 协议栈收到 A 的 FIN 后,B 端应用程序通常会在 recv() 或 read() 中得到返回值 0,表示:
对端已经完成有序关闭,接收方向到达 EOF。
此时内核已经发送 ACK,连接进入 CLOSE-WAIT。接下来何时发送 B 自己的 FIN,取决于 B 端应用程序何时调用 close() 或 shutdown(..., SHUT_WR)。
因此,大量连接长期停留在 CLOSE-WAIT,通常说明应用程序收到了对端关闭通知,却没有及时释放自己的套接字。这通常是应用层资源管理问题,而不是 TCP 协议没有发送报文。
五、第三次挥手:被动关闭方发送自己的 FIN
当 B 已经发送完剩余数据,并且应用程序决定关闭自己的发送方向时,B 发送 FIN。
1. 报文方向
text
主机 A <──────── FIN, ACK ────────── 主机 B
2. 典型首部字段
text
FIN = 1
ACK = 1
seq = w
ack = u + 1
其中:
seq=w:B 当前下一个可用的发送序号;w不一定等于第二次挥手时的v,因为 B 在半关闭期间可能又发送了数据;ack=u+1:继续确认 A 的 FIN 以及此前收到的数据。
3. 状态变化
text
B:CLOSE-WAIT → LAST-ACK
A:仍处于 FIN-WAIT-2
B 进入 LAST-ACK,等待 A 对 B 的 FIN 进行最终确认。
六、第四次挥手:主动关闭方发送最终 ACK
1. 报文方向
text
主机 A ───────────── ACK ───────────> 主机 B
2. 典型首部字段
text
ACK = 1
seq = u + 1
ack = w + 1
其中:
seq=u+1:A 的第一个 FIN 已经占用了序号u;ack=w+1:B 的 FIN 占用了序号w,所以确认号增加 1。
3. 状态变化
A 发送最终 ACK 后:
text
A:FIN-WAIT-2 → TIME-WAIT
B 收到最终 ACK 后:
text
B:LAST-ACK → CLOSED
A 并不会立即进入 CLOSED,而是先在 TIME-WAIT 中停留一段时间,然后才删除该连接的控制信息。

七、为什么主动关闭方要进入 TIME_WAIT
RFC 9293 规定,主动关闭连接的一端通常需要在 TIME-WAIT 状态停留 2 × MSL。MSL 是 Maximum Segment Lifetime,即一个 TCP 报文段在网络中可能存在的最长时间。
TIME-WAIT 主要解决两个问题。
原因一:保证最后一个 ACK 可以被补发
TCP 的纯 ACK 报文通常不会单独启动重传计时器。假设第四次挥手的 ACK 丢失:
- B 一直处于
LAST-ACK,没有收到对自己 FIN 的确认; - B 的 FIN 重传计时器到期;
- B 重新发送 FIN;
- A 如果仍在
TIME-WAIT,就能再次发送 ACK; - B 收到 ACK 后进入
CLOSED。
如果 A 发送最后一个 ACK 后立即删除连接状态,那么 B 重传 FIN 时,A 可能无法再按照原连接的序号状态正确处理它。

原因二:让旧连接的重复报文在网络中消失
TCP 连接由以下四元组标识:
text
源 IP、源端口、目的 IP、目的端口
如果旧连接刚刚关闭,同一个四元组立刻建立新连接,而旧连接中的延迟或重复报文仍滞留在网络中,这些旧报文可能干扰新连接。
保留 TIME-WAIT 可以为旧报文提供足够的自然消亡时间,降低旧连接报文进入新连接实例的风险。

不要把 2MSL 固定理解成"四分钟"
早期 TCP 规范将 MSL 任意定义为 2 分钟,因此教材中常得到 2MSL = 4 分钟。但实际操作系统的具体等待时间和连接复用策略可能不同。学习协议时应记住的是:
text
TIME-WAIT 的协议语义是等待 2MSL,
而不是所有系统都固定等待四分钟。
八、为什么通常是四次,而建立连接是三次
三次握手建立连接时,服务器可以把:
text
确认客户端 SYN 的 ACK
+
服务器自己的 SYN
合并成一个 SYN+ACK 报文段。
但释放连接时,被动关闭方收到 FIN 后,内核应尽快确认;与此同时,被动关闭方的应用程序可能还有数据尚未发送,暂时不能发送自己的 FIN。因此:
text
对第一个 FIN 的 ACK
与:
text
被动关闭方自己的 FIN
通常分开发送,形成四个报文段。
但如果 B 收到 A 的 FIN 时已经没有任何数据需要发送,并且应用程序也立即关闭,B 可以把 ACK 与自己的 FIN 合并成一个 FIN+ACK。此时抓包中可能看到:
text
A → B:FIN,ACK
B → A:FIN,ACK
A → B:ACK
因此,准确表述应是:
TCP 的正常关闭包含两个方向各自的 FIN/ACK 过程,典型情况下表现为四个报文段。
九、状态迁移总表
| 阶段 | 主动关闭方 A | 被动关闭方 B | 报文 |
|---|---|---|---|
| 初始 | ESTABLISHED |
ESTABLISHED |
双方正常传输 |
| 第一次挥手 | FIN-WAIT-1 |
ESTABLISHED |
A → B:FIN,ACK |
| 第二次挥手完成 | FIN-WAIT-2 |
CLOSE-WAIT |
B → A:ACK |
| 第三次挥手 | FIN-WAIT-2 |
LAST-ACK |
B → A:FIN,ACK |
| 第四次挥手 | TIME-WAIT |
CLOSED |
A → B:ACK |
| 等待结束 | CLOSED |
CLOSED |
A 等待 2MSL 后关闭 |
状态名称的直观理解
FIN-WAIT-1:已经发送 FIN,等待该 FIN 被确认;FIN-WAIT-2:自己的 FIN 已被确认,等待对端发送 FIN;CLOSE-WAIT:已经收到对端 FIN,等待本地应用程序关闭;LAST-ACK:已经发送本端最后一个 FIN,等待最终 ACK;TIME-WAIT:已经发送最终 ACK,保留连接状态以处理重传和旧报文;CLOSED:连接控制块已经被删除。
十、丢包和重传时会发生什么
1. 第一次 FIN 丢失
A 在 FIN-WAIT-1 中等待确认。重传计时器到期后,A 重新发送 FIN。
2. 第二次 ACK 丢失
A 没有收到对 FIN 的确认,会重传 FIN。B 收到重复 FIN 后,再次发送 ACK。
3. 第三次 FIN 丢失
B 在 LAST-ACK 中等待最终 ACK。计时器到期后,B 重传 FIN。
4. 第四次 ACK 丢失
B 重传 FIN;A 在 TIME-WAIT 中再次回复 ACK,并按照实现规则维持或重新开始等待时间。
因此,"四次挥手"描述的是逻辑阶段,并不是说整个网络过程中只能发送四个 TCP 报文段。
十一、同时关闭:双方都主动发送 FIN
如果 A 和 B 几乎同时调用关闭操作,双方的 FIN 可能在网络中交叉:
text
A:ESTABLISHED → FIN-WAIT-1
B:ESTABLISHED → FIN-WAIT-1
双方在尚未收到自己 FIN 的确认前先收到了对方 FIN,可能进入:
text
CLOSING
随后双方分别确认对方 FIN,并进入 TIME-WAIT,最后进入 CLOSED。
这属于 simultaneous close(同时关闭),状态迁移与普通主动/被动关闭不同,但本质仍然是两个方向分别发送 FIN 并被确认。
十二、RST 不是四次挥手
TCP 还可以通过 RST 异常终止连接。RST 的特点是:
- 不执行正常的双向 FIN 关闭;
- 通常立即丢弃连接状态;
- 可能导致尚未读取或尚未确认的数据丢失;
- 应用程序通常得到"连接被重置"等错误,而不是正常 EOF。
因此:
text
FIN:有序、正常、可确认的关闭
RST:异常、立即的连接中止
十三、与 Linux Socket API 的对应关系
主动关闭方
c
// 停止发送新数据,但仍保留接收能力。
shutdown(fd, SHUT_WR);
// 继续读取对端在半关闭期间发来的剩余数据。
char buf[1024];
ssize_t n;
while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
// 处理对端剩余数据
}
if (n == 0) {
// 收到对端 FIN,输入方向到达 EOF
}
close(fd);
shutdown(fd, SHUT_WR) 更直观地表达 TCP 半关闭:本端不再发送,但仍可接收。
被动关闭方
c
char buf[1024];
ssize_t n;
while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
// 处理收到的数据
}
if (n == 0) {
// 对端已发送 FIN。
// 此时协议栈已确认 FIN,连接处于 CLOSE-WAIT。
// 如有必要,仍可以发送剩余响应数据。
send(fd, "final response", 14, 0);
// 本地应用处理完成后关闭,触发本端 FIN。
close(fd);
}
需要注意,用户态系统调用与实际报文发送时刻之间由内核 TCP 协议栈协调;例如,内核可能先发送缓存中的数据,再发送 FIN。
十四、常见错误理解
错误 1:客户端一定是主动关闭方
不正确。客户端或服务器都可以主动关闭。谁先发送第一个 FIN,谁就是此次关闭过程中的主动关闭方。
错误 2:收到 FIN 后不能再发送数据
不正确。收到 FIN 只表示对端不会再发送数据。本端仍可在另一个方向继续发送,直到自己也发送 FIN。
错误 3:FIN 报文不能携带数据
不正确。FIN 可以和最后一批数据放在同一报文段中;FIN 在序号空间中位于这些数据之后。
错误 4:ACK 也会消耗一个序号
不正确。纯 ACK 不消耗序号;SYN 和 FIN 各占用一个序号,数据按字节占用序号。
错误 5:TIME_WAIT 一定出现在客户端
不正确。通常是主动关闭并发送最后 ACK 的一端 进入 TIME-WAIT。服务器同样可能主动关闭,因此服务器也可能出现大量 TIME-WAIT。
错误 6:看到很多 TIME_WAIT 就说明程序泄漏
不一定。TIME-WAIT 是 TCP 正常可靠关闭机制的一部分。是否异常,需要结合连接建立速率、端口使用、持续时间和业务模型分析。
错误 7:看到很多 CLOSE_WAIT 是网络太慢
通常不是。大量长期 CLOSE-WAIT 更常见的原因是应用程序没有在收到 EOF 后及时调用 close(),或者工作线程被阻塞、异常路径遗漏了资源释放。
十五、一句话记忆
text
A 说:我不再发了。 FIN
B 说:知道了,但我可能还要发。 ACK
B 说:我也发完了。 FIN
A 说:知道了,我再等一会儿。 ACK + TIME_WAIT
更严格地说:
TCP 关闭不是"关闭一根管道",而是分别关闭全双工连接的两个单向字节流;两个 FIN 分别终止两个发送方向,两个 ACK 分别确认这两个 FIN。
参考资料
- RFC 9293 --- Transmission Control Protocol (TCP)
https://www.rfc-editor.org/rfc/rfc9293.html - RFC 1337 --- TIME-WAIT Assassination Hazards in TCP
https://www.rfc-editor.org/rfc/rfc1337.html - 用户提供课件:《第5章 运输层.pptx》,TCP 连接释放相关页面。