TCP 四次挥手完整解析:报文、序号、状态与 TIME_WAIT

文章目录

  • [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)
    • 八、为什么通常是四次,而建立连接是三次
    • 九、状态迁移总表
    • 十、丢包和重传时会发生什么
      • [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

关闭其中一个方向,并不代表另一个方向也已经关闭。因此,正常释放连接时通常需要:

  1. 主动关闭方发送 FIN,表示"我没有更多数据要发送";
  2. 被动关闭方发送 ACK,确认已经收到这个 FIN
  3. 被动关闭方处理完剩余数据后,也发送自己的 FIN
  4. 主动关闭方发送最后一个 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                    │
│────────────────────────────────────────────────────>│
│                                                     │

两条序号规则

  1. FIN 会占用一个序号。

    因此,对 seq=uFIN 进行确认时,确认号是 u+1

  2. ACK 不占用序号。

    第二次和第四次挥手若不携带数据,其发送序号不会因为发送 ACK 而增加。

RFC 9293 将 SYNFIN 都计入 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 丢失:

  1. B 一直处于 LAST-ACK,没有收到对自己 FIN 的确认;
  2. B 的 FIN 重传计时器到期;
  3. B 重新发送 FIN;
  4. A 如果仍在 TIME-WAIT,就能再次发送 ACK;
  5. 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 不消耗序号;SYNFIN 各占用一个序号,数据按字节占用序号。

错误 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。


参考资料

  1. RFC 9293 --- Transmission Control Protocol (TCP)
    https://www.rfc-editor.org/rfc/rfc9293.html
  2. RFC 1337 --- TIME-WAIT Assassination Hazards in TCP
    https://www.rfc-editor.org/rfc/rfc1337.html
  3. 用户提供课件:《第5章 运输层.pptx》,TCP 连接释放相关页面。
相关推荐
田里的水稻9 小时前
FA_数字相机成像原理、分级和分类
运维·数码相机·机器人·自动驾驶
智灵鸟科技10 小时前
封闭网络怎么安全地接外部能力:三条通道穷举、威胁封堵矩阵与残余风险
网络·安全·矩阵
信仰87410 小时前
HCIA-华为数通基础理论与实践10
网络·华为·智能路由器
信仰87410 小时前
HCIA-华为数通基础理论与实践06
网络·华为
VX_1810 小时前
域名SSL证书监测平台
网络·网络协议·ssl
极客先躯10 小时前
高级java每日一道面试题-2026年04月18日-实战篇[Docker]-如何处理金融行业的时序数据容器化?
java·运维·docker·容器·金融·时序数据·高级面试
运维大师11 小时前
【K8S 运维实战】01-K8s架构深度剖析
运维·架构·kubernetes
liulilittle11 小时前
论无知:分布式
服务器·网络·分布式·并发·通信·竞态
Web极客码11 小时前
MariaDB 将成为 cPanel 中的默认数据库
服务器·mariadb·cpanel
HackTwoHub11 小时前
等级保护现场测评系统重磅更新,支持 AI 接入,可录入全品类资产清单,自动化巡检核查,批量导出测评归档文件
运维·人工智能·安全·web安全·网络安全·自动化·系统安全