TCP 四次挥手彻底解析:FIN、ACK、SEQ、半关闭与 TIME-WAIT

文章目录

  • [TCP 四次挥手彻底解析:FIN、ACK、SEQ、半关闭与 TIME-WAIT](#TCP 四次挥手彻底解析:FIN、ACK、SEQ、半关闭与 TIME-WAIT)
  • 一、四次挥手前必须掌握的核心规则
    • [1. TCP 是全双工协议](#1. TCP 是全双工协议)
    • [2. FIN 表示本端不再发送数据](#2. FIN 表示本端不再发送数据)
    • [3. FIN 会消耗一个序号](#3. FIN 会消耗一个序号)
    • [4. 纯 ACK 不消耗新的序号](#4. 纯 ACK 不消耗新的序号)
    • [5. ACK 标志位和 ack 确认号不是同一个概念](#5. ACK 标志位和 ack 确认号不是同一个概念)
  • [二、变量 U、V、W 的准确含义](#二、变量 U、V、W 的准确含义)
  • [三、TCP 四次挥手完整过程](#三、TCP 四次挥手完整过程)
  • [四、第一次挥手:客户端发送 FIN](#四、第一次挥手:客户端发送 FIN)
    • [1. 报文方向](#1. 报文方向)
    • [2. 标志位](#2. 标志位)
    • [3. 序号](#3. 序号)
    • [4. 确认号](#4. 确认号)
    • [5. 状态变化](#5. 状态变化)
    • [6. 第一次挥手表达的含义](#6. 第一次挥手表达的含义)
  • [五、第二次挥手:服务器确认客户端 FIN](#五、第二次挥手:服务器确认客户端 FIN)
    • [1. 报文方向](#1. 报文方向)
    • [2. 标志位](#2. 标志位)
    • [3. 序号](#3. 序号)
    • [4. 确认号](#4. 确认号)
    • [5. 状态变化](#5. 状态变化)
    • [6. 第二次挥手表达的含义](#6. 第二次挥手表达的含义)
  • 六、为什么第二次和第三次挥手不能总是合并
  • [七、第三次挥手:服务器发送 FIN](#七、第三次挥手:服务器发送 FIN)
    • [1. 报文方向](#1. 报文方向)
    • [2. 标志位](#2. 标志位)
    • [3. 序号](#3. 序号)
    • [4. 确认号](#4. 确认号)
    • [5. 状态变化](#5. 状态变化)
    • [6. 第三次挥手表达的含义](#6. 第三次挥手表达的含义)
  • [八、第四次挥手:客户端确认服务器 FIN](#八、第四次挥手:客户端确认服务器 FIN)
    • [1. 报文方向](#1. 报文方向)
    • [2. 标志位](#2. 标志位)
    • [3. 序号](#3. 序号)
    • [4. 确认号](#4. 确认号)
    • [5. 状态变化](#5. 状态变化)
  • 九、四次挥手字段汇总
  • 十、代入具体数值理解
  • [十一、为什么主动关闭方需要 TIME-WAIT](#十一、为什么主动关闭方需要 TIME-WAIT)
    • [1. 保证最终 ACK 可以重新发送](#1. 保证最终 ACK 可以重新发送)
    • [2. 让旧连接中的重复报文在网络中消失](#2. 让旧连接中的重复报文在网络中消失)
  • [十二、为什么等待时间是 2MSL](#十二、为什么等待时间是 2MSL)
  • [十三、为什么 TIME-WAIT 通常出现在主动关闭方](#十三、为什么 TIME-WAIT 通常出现在主动关闭方)
  • 十四、四次挥手一定正好有四个报文吗
    • [1. 第二次和第三次挥手可能合并](#1. 第二次和第三次挥手可能合并)
    • [2. 报文重传会使实际数量超过四个](#2. 报文重传会使实际数量超过四个)
    • [3. 双方可能同时关闭](#3. 双方可能同时关闭)
  • 十五、四次挥手中的丢包与重传
    • [1. 第一次 FIN 丢失](#1. 第一次 FIN 丢失)
    • [2. 第二次 ACK 丢失](#2. 第二次 ACK 丢失)
    • [3. 第三次 FIN 丢失](#3. 第三次 FIN 丢失)
    • [4. 第四次 ACK 丢失](#4. 第四次 ACK 丢失)
  • [十六、FIN 和 RST 的区别](#十六、FIN 和 RST 的区别)
  • [十七、CLOSE-WAIT 过多通常意味着什么](#十七、CLOSE-WAIT 过多通常意味着什么)
  • 十八、常见错误逐项修正
    • [错误一:把 FIN 写成 F2N](#错误一:把 FIN 写成 F2N)
    • [错误二:第一次挥手 ACK 通常为 0](#错误二:第一次挥手 ACK 通常为 0)
    • [错误三:SEQ=U 是最后一个数据字节序号](#错误三:SEQ=U 是最后一个数据字节序号)
    • [错误四:第二次挥手不需要 ack](#错误四:第二次挥手不需要 ack)
    • [错误五:W 一定等于 V](#错误五:W 一定等于 V)
    • [错误六:第四次挥手后客户端立即 CLOSED](#错误六:第四次挥手后客户端立即 CLOSED)
    • [错误七:TIME-WAIT 是为了等待服务器确认最终 ACK](#错误七:TIME-WAIT 是为了等待服务器确认最终 ACK)
    • [错误八:客户端一定进入 TIME-WAIT](#错误八:客户端一定进入 TIME-WAIT)
    • 错误九:四次挥手一定是四个网络包
  • 十九、完整状态迁移
  • 二十、最终结论

TCP 四次挥手彻底解析:FIN、ACK、SEQ、半关闭与 TIME-WAIT

前言

TCP 建立连接时通常使用三次握手,而关闭连接时通常需要四次报文交换,这个过程一般称为:

TCP 四次挥手,Four-Way Handshake。

典型流程如下:

text 复制代码
客户端                                      服务器
ESTABLISHED                              ESTABLISHED
    |                                         |
    | -------- FIN + ACK -------------------> |
    | <----------- ACK ---------------------- |
    |                                         |
    | <-------- FIN + ACK ------------------- |
    | ------------ ACK ---------------------> |
    |                                         |
 TIME-WAIT                                 CLOSED
    |
    | 等待 2MSL
    v
 CLOSED

虽然这张图的整体结构并不复杂,但在解释具体字段时,经常出现以下错误:

  • FIN 写成 F2N
  • 认为第一次挥手的 ACK 通常为 0;
  • SEQ=U 解释为"最后一个数据字节的序号";
  • 忽略第二次和第四次挥手中的确认号;
  • 认为第四次挥手发送后客户端立即进入 CLOSED
  • 认为 TCP 关闭连接在任何情况下都严格需要四个报文;
  • 不理解为什么主动关闭方需要进入 TIME-WAIT

本文假设:

客户端主动关闭连接,服务器被动关闭连接。

我们使用 UVW 表示双方关闭过程中的序号,并逐字段分析四次挥手。


一、四次挥手前必须掌握的核心规则

1. TCP 是全双工协议

一条 TCP 连接中存在两个相互独立的数据传输方向:

text 复制代码
客户端 → 服务器
服务器 → 客户端

客户端不再向服务器发送数据,并不意味着服务器也已经没有数据需要发送。

因此,关闭 TCP 连接实际上需要分别关闭两个方向:

text 复制代码
关闭客户端 → 服务器方向
关闭服务器 → 客户端方向

这正是 TCP 通常需要四次挥手,而不是像三次握手那样只交换三个报文的根本原因。


2. FIN 表示本端不再发送数据

当某一端发送:

text 复制代码
FIN = 1

表达的不是:

text 复制代码
整个 TCP 连接立即销毁。

而是:

text 复制代码
本端已经没有更多数据需要发送,
请求关闭本端到对端的发送方向。

发送 FIN 后,本端通常不能再发送新的应用数据,但仍然可以继续接收对端发送的数据。

RFC 9293 明确规定,主动关闭方发送 FIN 后进入 FIN-WAIT-1,不再接受新的发送请求,但仍然允许继续接收数据;收到 FIN 的一端需要先确认该 FIN,直到本地应用也关闭连接后,才发送自己的 FIN。(RFC 编辑器)

这种状态称为:

半关闭,Half-Close。


3. FIN 会消耗一个序号

与 SYN 一样,FIN 虽然不是应用层数据,但会占用 TCP 序号空间中的一个位置。

TCP 报文段占用的序号长度可以表示为:

text 复制代码
SEG.LEN = 数据长度 + SYN + FIN

其中:

text 复制代码
SYN=1 时计入 1
FIN=1 时计入 1

RFC 9293 将 SEG.SEQ 定义为报文段的第一个序号,将 SEG.ACK 定义为接收端下一次期望收到的序号,并明确规定报文段长度需要将 SYN 和 FIN 计算在内。FIN 在序号空间中的位置位于该报文所携带数据之后。(RFC 编辑器)

假设发送一个不携带数据的 FIN:

text 复制代码
SEQ = U
FIN = 1

那么对端确认时必须发送:

text 复制代码
ack = U + 1

4. 纯 ACK 不消耗新的序号

如果一个 TCP 报文:

text 复制代码
ACK = 1
数据长度 = 0
SYN = 0
FIN = 0

那么它不会占用新的序号空间。

例如:

text 复制代码
SEQ = V
ACK = 1
ack = U + 1

发送完这个纯 ACK 后,本端下一次发送数据时仍然可以使用:

text 复制代码
SEQ = V

只有以下内容会推动发送序号向前移动:

text 复制代码
应用数据:按字节数推进
SYN:推进 1
FIN:推进 1

5. ACK 标志位和 ack 确认号不是同一个概念

为了避免混淆,本文采用以下写法:

text 复制代码
ACK        TCP 首部中的 ACK 标志位
ack        TCP 首部中的确认号字段

例如:

text 复制代码
ACK = 1
ack = U + 1

表示:

  • ACK 标志位已经置 1;
  • 确认号字段有效;
  • 对端下一次期望收到的序号是 U+1

确认号本质上表示:

接收端下一次期望收到的序号。


二、变量 U、V、W 的准确含义

为了避免将"数据序号"和"FIN 序号"混在一起,本文作如下约定。

U:客户端 FIN 占用的序号

假设第一次挥手是一个不携带应用数据的纯 FIN,则:

text 复制代码
客户端第一次挥手:
SEQ = U

这里的 U 表示:

客户端发送 FIN 时所使用的序号。

因此,服务器确认该 FIN 时:

text 复制代码
ack = U + 1

不能简单地把 U 描述为:

text 复制代码
客户端最后一个数据字节的序号。

如果客户端最后一个数据字节的序号是 U-1,那么 FIN 才使用序号 U


V:服务器发送第二次挥手时的当前序号

服务器确认客户端 FIN 时,假设该 ACK 不携带数据:

text 复制代码
SEQ = V

因为纯 ACK 不消耗序号,所以服务器后续如果立即发送数据,仍然从 V 开始。


W:服务器发送 FIN 时的序号

服务器在第二次和第三次挥手之间可能继续发送剩余数据。

假设服务器从 V 开始又发送了 N 字节数据,那么:

text 复制代码
W = V + N

服务器发送自己的 FIN 时:

text 复制代码
SEQ = W

如果服务器没有发送任何剩余数据,则:

text 复制代码
W = V

因此,使用不同的 VW 可以清晰表达:

服务器在确认客户端 FIN 后,仍然可能继续发送数据。


三、TCP 四次挥手完整过程

假设关闭前:

text 复制代码
客户端状态:ESTABLISHED
服务器状态:ESTABLISHED

完整过程如下:

text 复制代码
客户端                                               服务器
ESTABLISHED                                       ESTABLISHED
    |                                                  |
    |  FIN=1, ACK=1                                    |
    |  SEQ=U, ack=V                                    |
    | ------------------------------------------------>|
    |                                                  |
FIN-WAIT-1                                         CLOSE-WAIT
    |                                                  |
    |                     ACK=1                        |
    |                     SEQ=V, ack=U+1               |
    |<-------------------------------------------------|
    |                                                  |
FIN-WAIT-2                                         CLOSE-WAIT
    |                                                  |
    |            服务器可能继续发送剩余数据             |
    |<-------------------------------------------------|
    |                                                  |
    |                     FIN=1, ACK=1                 |
    |                     SEQ=W, ack=U+1               |
    |<-------------------------------------------------|
    |                                                  |
TIME-WAIT                                          LAST-ACK
    |                                                  |
    |  ACK=1                                           |
    |  SEQ=U+1, ack=W+1                                |
    | ------------------------------------------------>|
    |                                                  |
TIME-WAIT                                           CLOSED
    |
    | 等待 2MSL
    v
 CLOSED

RFC 9293 给出的标准正常关闭示例同样采用了 FIN,ACK → ACK → FIN,ACK → ACK 的报文顺序,并展示了 FIN-WAIT-1FIN-WAIT-2CLOSE-WAITLAST-ACKTIME-WAITCLOSED 等状态迁移。(RFC 编辑器)


四、第一次挥手:客户端发送 FIN

1. 报文方向

text 复制代码
客户端 → 服务器

客户端主动发起关闭。


2. 标志位

典型正常关闭中的标志位是:

text 复制代码
FIN = 1
ACK = 1

这里需要修正一个常见错误:

text 复制代码
第一次挥手 ACK=0

这种写法并不适用于已经建立的正常 TCP 连接。

TCP 连接处于 ESTABLISHED 状态时,双方已经同步了序号空间,客户端发送 FIN 时通常仍然需要携带当前有效确认信息。因此,标准正常关闭示例中的第一次挥手是:

text 复制代码
FIN + ACK

而不是:

text 复制代码
只有 FIN,ACK=0

RFC 9293 的正常关闭示例中,主动关闭方发送的第一个关闭报文明确为 CTL=FIN,ACK。(RFC 编辑器)


3. 序号

假设 FIN 不携带数据:

text 复制代码
SEQ = U

U 是客户端当前下一个可使用的发送序号,也是这个 FIN 占用的序号。

如果客户端在 FIN 报文中同时携带数据,那么 SEQ 实际上表示该报文中第一个数据字节的序号,而 FIN 位于这些数据之后。

例如:

text 复制代码
SEQ = 1000
数据长度 = 100
FIN = 1

那么:

text 复制代码
数据占用序号:1000~1099
FIN 占用序号:1100
对端确认号:1101

为简化四次挥手分析,本文假设 FIN 报文不携带应用数据,所以直接写作:

text 复制代码
SEQ = U

4. 确认号

正常建立状态下,第一次挥手通常还会携带:

text 复制代码
ack = V

这里的 V 表示客户端下一次期望从服务器收到的序号。

因此,完整报文可以写为:

text 复制代码
FIN = 1
ACK = 1
SEQ = U
ack = V

5. 状态变化

客户端发送 FIN 后,从:

text 复制代码
ESTABLISHED

进入:

text 复制代码
FIN-WAIT-1

含义是:

text 复制代码
客户端的 FIN 已经发送,
正在等待服务器确认该 FIN。

此时客户端不能再发送新的应用数据,但仍然可以继续接收服务器发送的数据。(RFC 编辑器)


6. 第一次挥手表达的含义

客户端实际上是在告诉服务器:

text 复制代码
我已经没有新的数据需要发送。

客户端到服务器这个方向可以关闭。

我的 FIN 使用序号 U,
请通过 ack=U+1 对它进行确认。

但我仍然可以接收你发送的数据。

五、第二次挥手:服务器确认客户端 FIN

服务器收到客户端的 FIN 后,需要立即确认。

1. 报文方向

text 复制代码
服务器 → 客户端

2. 标志位

第二次挥手通常是一个纯 ACK:

text 复制代码
ACK = 1
FIN = 0

它只负责确认客户端的 FIN,并不一定表示服务器也准备立即关闭发送方向。


3. 序号

假设服务器当前下一个可用序号是 V

text 复制代码
SEQ = V

因为这个报文不携带数据,也没有 SYN 或 FIN,所以不会消耗序号。

服务器之后仍然可以从:

text 复制代码
SEQ = V

开始发送剩余数据。


4. 确认号

客户端第一次挥手发送:

text 复制代码
SEQ = U
FIN = 1

FIN 占用一个序号,因此服务器回复:

text 复制代码
ack = U + 1

完整报文为:

text 复制代码
ACK = 1
FIN = 0
SEQ = V
ack = U + 1

5. 状态变化

服务器收到客户端 FIN 后,从:

text 复制代码
ESTABLISHED

进入:

text 复制代码
CLOSE-WAIT

服务器发送 ACK 后,客户端收到这个 ACK,从:

text 复制代码
FIN-WAIT-1

进入:

text 复制代码
FIN-WAIT-2

状态含义分别是:

text 复制代码
CLOSE-WAIT:
已经收到对端的关闭请求,
等待本地应用程序也关闭连接。

FIN-WAIT-2:
本端发送的 FIN 已经得到确认,
等待对端发送自己的 FIN。

6. 第二次挥手表达的含义

服务器是在告诉客户端:

text 复制代码
你的 FIN 我已经收到。

客户端到服务器方向已经关闭。

我下一次期望你的序号是 U+1。

但是服务器到客户端方向暂时仍然可以发送数据。

收到 FIN 的 TCP 端点需要确认 FIN,但不会自动立即发送自己的 FIN;它通常需要等待本地应用完成剩余工作并主动关闭。(RFC 编辑器)


六、为什么第二次和第三次挥手不能总是合并

服务器收到客户端 FIN 时,只能说明:

text 复制代码
客户端没有更多数据需要发送。

它不能说明:

text 复制代码
服务器也已经没有数据需要发送。

服务器可能仍然需要:

  • 处理客户端刚刚发送完的请求;
  • 生成响应;
  • 把发送缓冲区中的剩余数据发送出去;
  • 等待应用程序调用关闭操作;
  • 完成日志、协议尾部或业务结果发送。

因此,服务器先发送 ACK 确认客户端 FIN,然后继续发送数据。

等本地应用明确表示不再发送数据后,服务器才发送自己的 FIN。

这就是第二次和第三次挥手通常分开的原因。


七、第三次挥手:服务器发送 FIN

服务器完成剩余数据发送后,关闭自己的发送方向。

1. 报文方向

text 复制代码
服务器 → 客户端

2. 标志位

第三次挥手通常为:

text 复制代码
FIN = 1
ACK = 1

其中:

  • FIN=1:服务器不再发送新数据;
  • ACK=1:确认号字段有效。

3. 序号

服务器发送 FIN 时:

text 复制代码
SEQ = W

如果第二次和第三次挥手之间,服务器发送了 N 字节数据,那么:

text 复制代码
W = V + N

如果没有发送数据,则:

text 复制代码
W = V

因此不能无条件认为:

text 复制代码
第三次挥手 SEQ 一定等于第二次挥手 SEQ。

它是否相等取决于服务器在此期间是否发送了数据。


4. 确认号

由于客户端发送 FIN 后不能继续发送新的应用数据,服务器仍然期望客户端的下一个序号为:

text 复制代码
ack = U + 1

完整报文为:

text 复制代码
FIN = 1
ACK = 1
SEQ = W
ack = U + 1

5. 状态变化

服务器发送 FIN 后,从:

text 复制代码
CLOSE-WAIT

进入:

text 复制代码
LAST-ACK

含义是:

text 复制代码
服务器已经发送自己的 FIN,
现在等待客户端的最终确认。

RFC 9293 的状态机规定,处于 CLOSE-WAIT 的端点在本地用户关闭连接、并且之前所有待发送数据均已处理后发送 FIN,然后进入 LAST-ACK。(RFC 编辑器)


6. 第三次挥手表达的含义

服务器是在告诉客户端:

text 复制代码
服务器剩余的数据已经发送完毕。

服务器到客户端方向也可以关闭。

我的 FIN 使用序号 W,
请通过 ack=W+1 对它进行确认。

八、第四次挥手:客户端确认服务器 FIN

客户端收到服务器 FIN 后,发送最终 ACK。

1. 报文方向

text 复制代码
客户端 → 服务器

2. 标志位

第四次挥手通常为:

text 复制代码
ACK = 1
FIN = 0

客户端第一次挥手时已经发送过自己的 FIN,因此第四次不需要再次设置 FIN。


3. 序号

客户端第一次挥手的 FIN 使用:

text 复制代码
SEQ = U

由于 FIN 消耗一个序号,客户端当前下一个序号为:

text 复制代码
SEQ = U + 1

因此第四次挥手通常写为:

text 复制代码
SEQ = U + 1

4. 确认号

服务器第三次挥手发送:

text 复制代码
SEQ = W
FIN = 1

因为 FIN 消耗一个序号,所以客户端确认:

text 复制代码
ack = W + 1

完整报文为:

text 复制代码
ACK = 1
FIN = 0
SEQ = U + 1
ack = W + 1

5. 状态变化

客户端发送最终 ACK 后,不会立即进入 CLOSED,而是进入:

text 复制代码
TIME-WAIT

服务器收到最终 ACK 后,从:

text 复制代码
LAST-ACK

进入:

text 复制代码
CLOSED

客户端等待 2MSL 后,才从:

text 复制代码
TIME-WAIT

进入:

text 复制代码
CLOSED

因此,下面这句话并不准确:

text 复制代码
客户端发送第四次挥手后,连接立即彻底关闭。

更准确的说法是:

服务器收到第四次挥手后可以删除该连接状态;客户端仍然需要在内核中保留一段时间的 TIME-WAIT 状态,等待 2MSL 后才最终进入 CLOSED。


九、四次挥手字段汇总

在以下假设下:

  • 客户端主动关闭;
  • 四个控制报文均不携带应用数据;
  • 服务器可能在第二次和第三次挥手之间发送剩余数据;

完整字段如下:

次数 方向 标志位 SEQ ack 状态变化
第一次 客户端 → 服务器 FIN=1, ACK=1 U V 客户端进入 FIN-WAIT-1
第二次 服务器 → 客户端 ACK=1 V U+1 服务器进入 CLOSE-WAIT,客户端进入 FIN-WAIT-2
第三次 服务器 → 客户端 FIN=1, ACK=1 W U+1 服务器进入 LAST-ACK
第四次 客户端 → 服务器 ACK=1 U+1 W+1 客户端进入 TIME-WAIT,服务器进入 CLOSED

简化表示:

text 复制代码
第一次挥手:
客户端 → 服务器
FIN=1, ACK=1, SEQ=U, ack=V

第二次挥手:
服务器 → 客户端
ACK=1, SEQ=V, ack=U+1

第三次挥手:
服务器 → 客户端
FIN=1, ACK=1, SEQ=W, ack=U+1

第四次挥手:
客户端 → 服务器
ACK=1, SEQ=U+1, ack=W+1

十、代入具体数值理解

假设关闭前:

text 复制代码
客户端下一个发送序号:1000
客户端下一次期望服务器序号:5000

因此:

text 复制代码
U = 1000
V = 5000

第一次挥手

客户端发送:

text 复制代码
FIN = 1
ACK = 1
SEQ = 1000
ack = 5000

FIN 占用序号 1000,所以客户端后续序号变为:

text 复制代码
1001

第二次挥手

服务器确认:

text 复制代码
ACK = 1
SEQ = 5000
ack = 1001

这个 ACK 不消耗序号,因此服务器的下一个数据仍然可以从:

text 复制代码
SEQ = 5000

开始。


服务器继续发送数据

假设服务器又发送了 200 字节数据:

text 复制代码
SEQ = 5000
数据长度 = 200

这些数据占用:

text 复制代码
5000~5199

服务器下一个序号变成:

text 复制代码
5200

因此:

text 复制代码
W = 5200

第三次挥手

服务器发送:

text 复制代码
FIN = 1
ACK = 1
SEQ = 5200
ack = 1001

服务器 FIN 占用序号 5200。


第四次挥手

客户端确认:

text 复制代码
ACK = 1
SEQ = 1001
ack = 5201

最终:

text 复制代码
客户端进入 TIME-WAIT
服务器收到 ACK 后进入 CLOSED

十一、为什么主动关闭方需要 TIME-WAIT

TIME-WAIT 不是无意义地浪费连接资源,它主要解决两个问题。

1. 保证最终 ACK 可以重新发送

TCP 的第四次挥手是一个 ACK。

ACK 本身通常不会再被确认,因此客户端无法直接知道:

text 复制代码
服务器是否真正收到了最终 ACK。

假设最终 ACK 丢失:

text 复制代码
客户端 ---- 最终 ACK ----X----> 服务器

服务器仍然停留在:

text 复制代码
LAST-ACK

等待确认。

超时后,服务器会重新发送 FIN:

text 复制代码
服务器 -------- FIN --------> 客户端

如果客户端仍处于 TIME-WAIT,就可以再次发送:

text 复制代码
ACK = 1
ack = W + 1

RFC 9293 规定,处于 TIME-WAIT 的端点收到对端重传的 FIN 时,需要再次确认该 FIN,并重新启动 2MSL 定时器。(RFC 编辑器)

如果客户端发送最终 ACK 后立即删除所有连接状态,那么服务器重传 FIN 时,客户端可能无法正确识别和确认该报文。


2. 让旧连接中的重复报文在网络中消失

TCP 连接通常由以下四元组标识:

text 复制代码
源 IP
源端口
目的 IP
目的端口

一条连接关闭后,未来可能重新创建相同四元组的新连接。

如果旧连接中的延迟报文仍然存在于网络中,而新连接立即复用了相同四元组,那么旧报文可能干扰新连接。

TIME-WAIT 为旧重复报文提供足够时间,使其在网络中失效,降低旧连接报文进入新连接序号空间的风险。RFC 1337 将消除旧重复报文列为 TIME-WAIT 的核心功能之一。(IETF Datatracker2)


十二、为什么等待时间是 2MSL

MSL 是:

text 复制代码
Maximum Segment Lifetime
最大报文段生存时间

它表示一个 TCP 报文段在网络中可能存活的最大时间。

等待 2MSL 可以覆盖一个可能的往返过程:

text 复制代码
服务器重传 FIN:
服务器 → 客户端,最多经历 1MSL

客户端重新发送 ACK:
客户端 → 服务器,最多经历 1MSL

因此:

text 复制代码
总等待时间 = 2MSL

这里的 2MSL 不是:

  • 两倍 RTT;
  • 固定等于某个统一秒数;
  • 应用层配置的请求超时。

它是 TCP 为关闭阶段保留连接状态时使用的协议时间概念。RFC 9293 的正常关闭序列要求主动关闭端在 TIME-WAIT 中等待 2MSL。(RFC 编辑器)


十三、为什么 TIME-WAIT 通常出现在主动关闭方

在正常四次挥手中:

  1. 主动关闭方先发送 FIN;
  2. 被动关闭方确认 FIN;
  3. 被动关闭方发送自己的 FIN;
  4. 主动关闭方发送最终 ACK。

发送最终 ACK 的一方需要保留状态,以便:

  • 对重传的 FIN 再次发送 ACK;
  • 等待旧重复报文失效。

因此,正常情况下进入 TIME-WAIT 的通常是:

主动关闭连接的一方。

但是不能简单地死记:

text 复制代码
客户端一定进入 TIME-WAIT。

真正决定 TIME-WAIT 归属的是:

谁负责发送关闭过程中的最终 ACK。

服务器也可以主动关闭连接。如果服务器先发送 FIN,那么服务器就可能成为进入 TIME-WAIT 的一方。


十四、四次挥手一定正好有四个报文吗

不一定。

"四次挥手"是 TCP 正常关闭过程的典型逻辑模型,不代表抓包时一定严格看到四个独立报文。


1. 第二次和第三次挥手可能合并

如果服务器收到客户端 FIN 时,服务器应用程序也已经没有数据需要发送,那么服务器可能将:

text 复制代码
确认客户端 FIN 的 ACK

和:

text 复制代码
服务器自己的 FIN

合并为一个:

text 复制代码
FIN + ACK

此时抓包可能只看到三个主要关闭报文:

text 复制代码
客户端 → 服务器:FIN + ACK
服务器 → 客户端:FIN + ACK
客户端 → 服务器:ACK

这并不表示协议逻辑缺少一次确认,只是:

第二次确认和第三次关闭请求被放进了同一个 TCP 报文段。


2. 报文重传会使实际数量超过四个

如果以下报文发生丢失:

  • 第一次 FIN;
  • 第二次 ACK;
  • 第三次 FIN;
  • 第四次 ACK;

TCP 都可能触发相应重传。

因此,抓包中可能看到:

text 复制代码
5 个、6 个甚至更多关闭相关报文。

"四次"描述的是正常、无丢包、ACK 与 FIN 分开发送时的逻辑过程。


3. 双方可能同时关闭

如果客户端和服务器几乎同时执行关闭,双方都可能先发送 FIN。

此时双方都会进入:

text 复制代码
FIN-WAIT-1

收到对方 FIN 但自己的 FIN 尚未被确认时,可能进入:

text 复制代码
CLOSING

双方 FIN 均被确认后,再进入:

text 复制代码
TIME-WAIT

RFC 9293 单独定义了 simultaneous close,即同时关闭场景,并展示了双方进入 CLOSINGTIME-WAIT 的状态过程。(RFC 编辑器)

因此,普通的:

text 复制代码
FIN-WAIT-1 → FIN-WAIT-2 → TIME-WAIT

只适用于典型的单方主动关闭场景。


十五、四次挥手中的丢包与重传

1. 第一次 FIN 丢失

客户端停留在:

text 复制代码
FIN-WAIT-1

超时后重新发送 FIN。


2. 第二次 ACK 丢失

客户端没有收到对自己 FIN 的确认,因此会重新发送 FIN。

服务器再次收到重复 FIN 后,重新发送 ACK。


3. 第三次 FIN 丢失

服务器停留在:

text 复制代码
LAST-ACK

超时后重新发送 FIN。


4. 第四次 ACK 丢失

服务器没有收到最终确认,继续停留在:

text 复制代码
LAST-ACK

超时后重新发送 FIN。

客户端仍在:

text 复制代码
TIME-WAIT

因此可以再次确认服务器 FIN,并重新启动 2MSL 定时器。(RFC 编辑器)

这也是不能删除 TIME-WAIT 的重要原因之一。


十六、FIN 和 RST 的区别

TCP 连接不仅可以通过 FIN 正常关闭,也可能通过 RST 异常终止。

FIN:正常、优雅关闭

FIN 表示:

text 复制代码
我已经没有更多数据需要发送,
但之前已经发送的数据仍应可靠到达。

特点包括:

  • 通常需要确认;
  • 支持半关闭;
  • 允许对端继续发送剩余数据;
  • 按照 TCP 状态机逐步关闭;
  • 可能产生 TIME-WAIT。

RST:立即中止连接

RST 表示:

text 复制代码
立即终止当前连接。

它通常出现在:

  • 向不存在的端口建立连接;
  • 向已经失效的连接发送报文;
  • 应用或协议栈主动异常终止连接;
  • 收到无法接受的连接状态报文。

RST 与 FIN 的语义完全不同:

text 复制代码
FIN:我正常发送完了。
RST:这个连接立即作废。

因此,不能把收到 RST 的过程称为标准四次挥手。


十七、CLOSE-WAIT 过多通常意味着什么

当服务器收到客户端 FIN 后,会进入:

text 复制代码
CLOSE-WAIT

此时 TCP 协议栈已经确认了客户端的 FIN,但服务器只有在本地应用程序关闭对应连接后,才能发送自己的 FIN 并进入 LAST-ACK

因此,大量连接长期停留在 CLOSE-WAIT,通常意味着:

text 复制代码
应用程序没有及时关闭已经收到 EOF 的连接。

常见原因包括:

  • 读取到对端关闭后忘记调用关闭操作;
  • 异常路径没有释放套接字;
  • 线程被阻塞,无法执行清理逻辑;
  • 连接对象仍然被错误引用;
  • 业务处理迟迟没有结束。

需要区分:

text 复制代码
大量 TIME-WAIT:
通常与主动关闭频繁、短连接较多有关。

大量 CLOSE-WAIT:
通常与被动关闭方的应用程序没有及时释放连接有关。

十八、常见错误逐项修正

错误一:把 FIN 写成 F2N

错误:

text 复制代码
F2N

正确:

text 复制代码
FIN

FIN 是 Finish 的缩写,用于表示发送方向结束。


错误二:第一次挥手 ACK 通常为 0

错误写法:

text 复制代码
FIN = 1
ACK = 0

典型正常关闭应写为:

text 复制代码
FIN = 1
ACK = 1

因为连接已经处于同步状态,FIN 报文通常还需要携带当前有效的累计确认号。RFC 9293 的正常关闭示例明确使用 FIN,ACK。(RFC 编辑器)


错误三:SEQ=U 是最后一个数据字节序号

不准确:

text 复制代码
U 是客户端最后一个数据字节的序号。

准确表述:

text 复制代码
在不携带数据的 FIN 报文中,
U 是 FIN 自己占用的序号。

如果最后一个数据字节序号是 U-1,那么 FIN 使用 U


错误四:第二次挥手不需要 ack

错误。

第二次挥手需要确认客户端 FIN:

text 复制代码
ack = U + 1

因为 FIN 占用一个序号。


错误五:W 一定等于 V

不一定。

如果服务器在第二次和第三次挥手之间发送了 N 字节数据:

text 复制代码
W = V + N

只有服务器没有发送任何新数据时:

text 复制代码
W = V

错误六:第四次挥手后客户端立即 CLOSED

错误。

客户端发送最终 ACK 后进入:

text 复制代码
TIME-WAIT

等待 2MSL 后才进入:

text 复制代码
CLOSED

服务器收到最终 ACK 后才直接进入 CLOSED


错误七:TIME-WAIT 是为了等待服务器确认最终 ACK

错误。

服务器不会再发送一个"第五次挥手"来确认最终 ACK。

TIME-WAIT 的主要作用是:

  • 处理服务器 FIN 的可能重传;
  • 再次发送最终 ACK;
  • 等待旧重复报文失效。

错误八:客户端一定进入 TIME-WAIT

错误。

更准确的说法是:

text 复制代码
正常关闭过程中,发送最终 ACK 的一方通常进入 TIME-WAIT。

谁主动关闭,谁通常进入 TIME-WAIT。

客户端只是教学示例中经常被假设为主动关闭方。


错误九:四次挥手一定是四个网络包

错误。

实际抓包可能出现:

text 复制代码
3 个报文:ACK 与 FIN 被合并
4 个报文:典型正常关闭
超过 4 个报文:存在重传或其他数据交互

"四次挥手"描述的是关闭两个传输方向所需的逻辑动作。


十九、完整状态迁移

客户端主动关闭

text 复制代码
ESTABLISHED
     |
     | 发送 FIN
     v
FIN-WAIT-1
     |
     | 收到对 FIN 的 ACK
     v
FIN-WAIT-2
     |
     | 收到服务器 FIN,发送最终 ACK
     v
TIME-WAIT
     |
     | 等待 2MSL
     v
CLOSED

服务器被动关闭

text 复制代码
ESTABLISHED
     |
     | 收到客户端 FIN,发送 ACK
     v
CLOSE-WAIT
     |
     | 本地应用完成剩余工作并关闭
     | 发送 FIN
     v
LAST-ACK
     |
     | 收到最终 ACK
     v
CLOSED

二十、最终结论

标准的 TCP 四次挥手可以概括为:

text 复制代码
第一次挥手:
客户端 → 服务器
FIN=1, ACK=1, SEQ=U, ack=V

第二次挥手:
服务器 → 客户端
ACK=1, SEQ=V, ack=U+1

第三次挥手:
服务器 → 客户端
FIN=1, ACK=1, SEQ=W, ack=U+1

第四次挥手:
客户端 → 服务器
ACK=1, SEQ=U+1, ack=W+1

需要牢记以下规则:

text 复制代码
1. TCP 是全双工协议,两个发送方向需要分别关闭。

2. FIN 表示本端不再发送数据,不代表整个连接立即销毁。

3. FIN 会占用一个序号,因此确认号需要加 1。

4. 纯 ACK 不占用新的序号。

5. 第一次挥手在正常已建立连接中通常是 FIN+ACK,而不是 ACK=0。

6. U 是客户端 FIN 占用的序号,不是最后一个数据字节的序号。

7. V 是服务器确认 FIN 时的当前序号。

8. W 是服务器发送自己的 FIN 时的序号。

9. 如果服务器继续发送了数据,那么 W 会大于 V。

10. 客户端发送最终 ACK 后进入 TIME-WAIT,而不是立即 CLOSED。

11. TIME-WAIT 用于处理 FIN 重传并隔离旧连接中的延迟报文。

12. 四次挥手是典型逻辑模型,实际抓包可能是三个、四个或更多报文。

理解四次挥手最关键的一句话是:

text 复制代码
FIN 只关闭一个方向。

客户端说"我不再发送",
不等于服务器也已经发送完毕。

正因为 TCP 两个方向可以独立关闭,所以服务器需要先确认客户端的 FIN,完成剩余数据发送后,再单独发送自己的 FIN。这就是 TCP 通常需要四次挥手的根本原因。

该版本可以与前面的"三次握手"博客组成同一系列,变量、字段命名和讲解结构已经统一。

2: https://datatracker.ietf.org/doc/html/rfc1337 "

复制代码
            RFC 1337 - TIME-WAIT Assassination Hazards in TCP
        
    "
相关推荐
楷哥爱开发10 小时前
如何使用 Claude Fable 5 进行网页抓取?2026最新实战教程
大数据·网络·人工智能
Sirius Wu10 小时前
OpenClaw Skill:Matplotlib 可视化技能 + 沙箱双层隔离完整详解
服务器·网络·人工智能·安全·ai·架构·aigc
测试员周周11 小时前
【skills5】一份 spec 生成 k6/JMeter/Locust:性能压测 + 全站截图,上线前的双保险
功能测试·网络协议·测试工具·jmeter·http·自动化·集成测试
酣大智11 小时前
常用 IP 协议号大全(IP 层承载的上层协议)
网络·网络协议·tcp/ip
大公产经晚间消息14 小时前
《寻访独角兽》首期走进太仓,探访小科智行的硬科技“突围”
网络·人工智能·科技
JoyCong199815 小时前
ToDesk新功能科普:屏幕墙、协作模式、设备别名、USB映射...
网络·科技·电脑·远程工作·远程操作
数智化管理手记16 小时前
智能财务如何减少财务加班?智能财务落地需要哪些工具支撑?
大数据·网络·数据库·数据挖掘·精益工程
paopaokaka_luck18 小时前
基于Springboot3+vue3的旅游景区点评系统(AI审核、webSocket聊天、协同过滤算法、Echarts图形化分析)
websocket·网络协议·旅游
其实防守也摸鱼19 小时前
运维--安全与数据库
网络·数据库·学习·安全·安全威胁分析·数据库架构·软件安全