TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT
文章目录
- TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT
- 一、先给出最核心的结论
- 二、分析前的序号约定
- 两条序号规则
- 三、第一次挥手:主动关闭方发送 FIN
- 1. 报文方向
- 2. 典型首部字段
- 3. 状态变化
- 4. FIN 的真实含义
- 四、第二次挥手:被动关闭方确认 FIN
- 1. 报文方向
- 2. 典型首部字段
- 3. 状态变化
- 4. 此时形成半关闭状态
- 5. CLOSE_WAIT 与应用程序的关系
- 五、第三次挥手:被动关闭方发送自己的 FIN
- 1. 报文方向
- 2. 典型首部字段
- 3. 状态变化
- 六、第四次挥手:主动关闭方发送最终 ACK
- 1. 报文方向
- 2. 典型首部字段
- 3. 状态变化
- 七、为什么主动关闭方要进入 TIME_WAIT
- 原因一:保证最后一个 ACK 可以被补发
- 原因二:让旧连接的重复报文在网络中消失
- 不要把 2MSL 固定理解成“四分钟”
- 八、为什么通常是四次,而建立连接是三次
- 九、状态迁移总表
- 状态名称的直观理解
- 十、丢包和重传时会发生什么
- 1. 第一次 FIN 丢失
- 2. 第二次 ACK 丢失
- 3. 第三次 FIN 丢失
- 4. 第四次 ACK 丢失
- 十一、同时关闭:双方都主动发送 FIN
- 十二、RST 不是四次挥手
- 十三、与 Linux Socket API 的对应关系
- 主动关闭方
- 被动关闭方
- 十四、常见错误理解
- 错误 1:客户端一定是主动关闭方
- 错误 2:收到 FIN 后不能再发送数据
- 错误 3:FIN 报文不能携带数据
- 错误 4:ACK 也会消耗一个序号
- 错误 5:TIME_WAIT 一定出现在客户端
- 错误 6:看到很多 TIME_WAIT 就说明程序泄漏
- 错误 7:看到很多 CLOSE_WAIT 是网络太慢
- 十五、一句话记忆
- 参考资料
TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT
本文以“一端主动关闭、另一端被动关闭”的正常释放流程为主线。配图截取自所提供课件《第5章 运输层.pptx》的 TCP 连接释放部分,并结合 RFC 9293 对序号、半关闭、状态迁移和异常情况进行补充修正。
一、先给出最核心的结论
TCP 是全双工字节流协议。一条 TCP 连接中实际上存在两个相互独立的数据传输方向:
主机 A ───────────────→ 主机 B 主机 A ←─────────────── 主机 B关闭其中一个方向,并不代表另一个方向也已经关闭。因此,正常释放连接时通常需要:
- 主动关闭方发送
FIN,表示“我没有更多数据要发送”; - 被动关闭方发送
ACK,确认已经收到这个FIN; - 被动关闭方处理完剩余数据后,也发送自己的
FIN; - 主动关闭方发送最后一个
ACK。
这就是通常所说的TCP 四次挥手。
需要注意:四次挥手是典型的正常关闭过程,不表示网络上任何一次 TCP 关闭都一定恰好出现四个独立报文段。如果第二次的ACK与第三次的FIN被合并发送,抓包中可能只看到三个报文段;如果报文丢失并发生重传,则可能看到五个甚至更多报文段。
二、分析前的序号约定
假设连接已经建立,主机 A 主动关闭,主机 B 被动关闭:
- A 当前下一个可用发送序号为
u; - A 当前期望从 B 收到的下一个字节序号为
v; - B 在半关闭期间可能继续发送数据,因此 B 最终发送
FIN时的序号记为w。
在不携带数据的典型情况下,四个报文段可写成:
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. 报文方向
主机 A ───────── FIN, ACK ─────────> 主机 B2. 典型首部字段
FIN = 1 ACK = 1 seq = u ack = v含义如下:
FIN=1:A 表示自己已经没有更多数据需要发送;ACK=1:连接已经处于已建立阶段,确认号字段有效;seq=u:该 FIN 使用 A 当前下一个发送序号;ack=v:A 仍然确认自己已经正确收到 B 在v之前的全部字节。
3. 状态变化
A:ESTABLISHED → FIN-WAIT-1 B:仍处于 ESTABLISHEDA 进入FIN-WAIT-1后,等待 B 对该 FIN 的确认。
4. FIN 的真实含义
FIN表示:
本端在这个发送方向上不会再发送新的字节。
它并不表示:
- 本端不能继续接收数据;
- 整条 TCP 连接已经立即消失;
- 对端必须立刻发送自己的 FIN;
- 当前 FIN 报文段不能携带数据。
如果 A 的 FIN 报文段还携带了N字节数据,且第一个数据字节序号为u,那么 B 对其完整确认时应发送:
ack = u + N + 1其中N来自数据,额外的1来自 FIN。
四、第二次挥手:被动关闭方确认 FIN
1. 报文方向
主机 A <──────────── ACK ─────────── 主机 B2. 典型首部字段
ACK = 1 seq = v ack = u + 1其中:
ack=u+1表示 B 已经收到 A 的 FIN;seq=v表示该 ACK 自身不消耗序号,B 当前下一个发送序号仍然是v,除非同时携带数据。
3. 状态变化
B 收到 FIN 并发送 ACK 后:
B:ESTABLISHED → CLOSE-WAITA 收到该 ACK 后:
A:FIN-WAIT-1 → FIN-WAIT-24. 此时形成半关闭状态
此时:
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. 报文方向
主机 A <──────── FIN, ACK ────────── 主机 B2. 典型首部字段
FIN = 1 ACK = 1 seq = w ack = u + 1其中:
seq=w:B 当前下一个可用的发送序号;w不一定等于第二次挥手时的v,因为 B 在半关闭期间可能又发送了数据;ack=u+1:继续确认 A 的 FIN 以及此前收到的数据。
3. 状态变化
B:CLOSE-WAIT → LAST-ACK A:仍处于 FIN-WAIT-2B 进入LAST-ACK,等待 A 对 B 的 FIN 进行最终确认。
六、第四次挥手:主动关闭方发送最终 ACK
1. 报文方向
主机 A ───────────── ACK ───────────> 主机 B2. 典型首部字段
ACK = 1 seq = u + 1 ack = w + 1其中:
seq=u+1:A 的第一个 FIN 已经占用了序号u;ack=w+1:B 的 FIN 占用了序号w,所以确认号增加 1。
3. 状态变化
A 发送最终 ACK 后:
A:FIN-WAIT-2 → TIME-WAITB 收到最终 ACK 后:
B:LAST-ACK → CLOSEDA 并不会立即进入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 连接由以下四元组标识:
源 IP、源端口、目的 IP、目的端口如果旧连接刚刚关闭,同一个四元组立刻建立新连接,而旧连接中的延迟或重复报文仍滞留在网络中,这些旧报文可能干扰新连接。
保留TIME-WAIT可以为旧报文提供足够的自然消亡时间,降低旧连接报文进入新连接实例的风险。
不要把 2MSL 固定理解成“四分钟”
早期 TCP 规范将 MSL 任意定义为 2 分钟,因此教材中常得到2MSL = 4 分钟。但实际操作系统的具体等待时间和连接复用策略可能不同。学习协议时应记住的是:
TIME-WAIT 的协议语义是等待 2MSL, 而不是所有系统都固定等待四分钟。八、为什么通常是四次,而建立连接是三次
三次握手建立连接时,服务器可以把:
确认客户端 SYN 的 ACK + 服务器自己的 SYN合并成一个SYN+ACK报文段。
但释放连接时,被动关闭方收到 FIN 后,内核应尽快确认;与此同时,被动关闭方的应用程序可能还有数据尚未发送,暂时不能发送自己的 FIN。因此:
对第一个 FIN 的 ACK与:
被动关闭方自己的 FIN通常分开发送,形成四个报文段。
但如果 B 收到 A 的 FIN 时已经没有任何数据需要发送,并且应用程序也立即关闭,B 可以把 ACK 与自己的 FIN 合并成一个FIN+ACK。此时抓包中可能看到:
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 可能在网络中交叉:
A:ESTABLISHED → FIN-WAIT-1 B:ESTABLISHED → FIN-WAIT-1双方在尚未收到自己 FIN 的确认前先收到了对方 FIN,可能进入:
CLOSING随后双方分别确认对方 FIN,并进入TIME-WAIT,最后进入CLOSED。
这属于simultaneous close(同时关闭),状态迁移与普通主动/被动关闭不同,但本质仍然是两个方向分别发送 FIN 并被确认。
十二、RST 不是四次挥手
TCP 还可以通过RST异常终止连接。RST 的特点是:
- 不执行正常的双向 FIN 关闭;
- 通常立即丢弃连接状态;
- 可能导致尚未读取或尚未确认的数据丢失;
- 应用程序通常得到“连接被重置”等错误,而不是正常 EOF。
因此:
FIN:有序、正常、可确认的关闭 RST:异常、立即的连接中止十三、与 Linux Socket API 的对应关系
主动关闭方
// 停止发送新数据,但仍保留接收能力。shutdown(fd,SHUT_WR);// 继续读取对端在半关闭期间发来的剩余数据。charbuf[1024];ssize_tn;while((n=recv(fd,buf,sizeof(buf),0))>0){// 处理对端剩余数据}if(n==0){// 收到对端 FIN,输入方向到达 EOF}close(fd);shutdown(fd, SHUT_WR)更直观地表达 TCP 半关闭:本端不再发送,但仍可接收。
被动关闭方
charbuf[1024];ssize_tn;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(),或者工作线程被阻塞、异常路径遗漏了资源释放。
十五、一句话记忆
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 连接释放相关页面。
