优雅告别:TCP四次挥手完整解析
-
- 前言
- 一、为什么是四次挥手?
- 二、四次挥手完整流程图
- 三、每一步的详细拆解
-
- 第一步:主动关闭方发送 FIN
- 第二步:被动关闭方回复 ACK
- 第三步:被动关闭方发送 FIN
- 第四步:主动关闭方回复 ACK
- 四、半关闭状态详解
- 五、状态流转完整图解
- 六、TIME_WAIT 为什么存在?
-
- 6.1 TIME_WAIT 的两个作用
- 6.2 2MSL 是多少?
- 6.3 TIME_WAIT 过多有什么问题?
- 七、为什么挥手需要四次而不是三次?
- 八、抓包实战分析
-
- 8.1 使用 netcat 模拟连接
- 8.2 抓包结果
- 九、常见面试题
-
- Q1:四次挥手中哪一次可能丢失?如何应对?
- Q2:为什么 TIME_WAIT 在主动关闭方,而不是被动关闭方?
- Q3:服务器产生大量 TIME_WAIT 说明什么?
- Q4:服务器产生大量 CLOSE_WAIT 说明什么?
- Q5:为什么握手是三次,挥手是四次?
- 十、总结速查表
|
🌺The Begin🌺点点关注,收藏不迷路🌺 ⬇ ⬇ 底部 ⬇ ⬇ |
前言
我们之前详细聊过 TCP 的 三次握手——如何建立连接。但有连接就有断开。
TCP 断开连接的过程,被称为 四次挥手(Four-Way Wavehand)。
相比三次握手的相对简单,四次挥手涉及更多的细节:半关闭状态、TIME_WAIT 的作用、为什么是四次而不是三次……这些都是面试中的高频考点。
今天这篇文章,我们从 挥手流程 → 每一步详解 → 状态变化 → 核心问题解答 → 抓包实战,彻底讲透 TCP 四次挥手。
一、为什么是四次挥手?
先回忆一下:三次握手用了 3 个包建立连接,那断开为什么需要 4 个包?
核心原因:TCP 是全双工的。
- 每个方向的数据传输是独立的
- 每一端都需要单独关闭自己的发送通道
换句话说:
- 客户端说“我发完了”(FIN)
- 服务器回复“我知道你发完了”(ACK)
- 服务器说“我也发完了”(FIN)
- 客户端回复“我知道你发完了”(ACK)
所以,四次挥手 = 两个方向的独立关闭,每个方向需要 1 次 FIN + 1 次 ACK。
二、四次挥手完整流程图
Client (客户端) Server (服务端)
│ │
│ 数据发送完毕 │
│ │
│ 1. FIN (seq=u) │
│ ─────────────────────────────► │
│ │
│ 2. ACK (ack=u+1, seq=v) │
│ ◄───────────────────────────── │
│ │
│ 【半关闭状态】 │
│ 客户端不再发送数据 │
│ 但可以接收数据 │
│ │
│ 3. FIN (seq=w) │
│ ◄───────────────────────────── │
│ │
│ 4. ACK (ack=w+1, seq=u+1) │
│ ─────────────────────────────► │
│ │
│ 等待 2MSL 后完全关闭 │
三、每一步的详细拆解
第一步:主动关闭方发送 FIN
发起方:通常是客户端,但服务器也可以主动关闭
报文内容:
- FIN = 1(结束位,表示不再发送数据)
- seq = u(当前序列号)
发生时机:主动关闭方调用 close() 或 shutdown(SHUT_WR)
状态变化:主动关闭方 → FIN_WAIT_1
含义:“我的数据已经发完了,我要关闭连接了。”
第二步:被动关闭方回复 ACK
被动关闭方收到 FIN 后:
- 回复 ACK = 1
- ack = u + 1(确认收到 FIN)
- seq = v(自己的序列号)
状态变化:被动关闭方 → CLOSE_WAIT
状态变化:主动关闭方收到 ACK 后 → FIN_WAIT_2
含义:“我知道你发完了,但我可能还有数据要发(你先等着)。”
此时连接进入 半关闭(Half-Close) 状态:
- 主动关闭方:不再发送数据,但还能接收数据
- 被动关闭方:还可以继续发送数据
第三步:被动关闭方发送 FIN
当被动关闭方也发送完所有数据后:
- 发送 FIN = 1
- seq = w(可能是 v 加上已发送的数据长度)
- ack 仍然为 u + 1(可省略)
状态变化:被动关闭方 → LAST_ACK
状态变化:主动关闭方收到 FIN 后 → TIME_WAIT
含义:“我也没有数据要发了,彻底关闭吧。”
第四步:主动关闭方回复 ACK
主动关闭方收到 FIN 后:
- 发送 ACK = 1
- ack = w + 1
- seq = u + 1
状态变化:主动关闭方 → 等待 2MSL 后 → CLOSED
状态变化:被动关闭方收到 ACK 后 → CLOSED
含义:“我知道你也发完了,连接正式关闭。”
四、半关闭状态详解
半关闭(Half-Close) 是 TCP 的一个特性:允许一方关闭自己的发送通道,但仍可以接收对方的数据。
Client Server
│ │
│ FIN ─────────────────────────► │
│ ACK ◄───────────────────────── │
│ │
│ 【半关闭】 │
│ 客户端不能再发数据 │
│ 但可以收数据 │
│ │
│ ◄──────────── 服务器继续发送数据 ────┤
│ │
│ FIN ◄───────────────────────── │
│ ACK ─────────────────────────► │
使用场景:
- 类似 shutdown(SHUT_WR) 的场景
- 某些协议(如 HTTP/1.0)可以利用半关闭
五、状态流转完整图解
主动关闭方 被动关闭方
(Client) (Server)
ESTABLISHED ESTABLISHED
│ │
│ 发送 FIN │
▼ │
FIN_WAIT_1 ──────────────────────────► CLOSE_WAIT
│ │
│ 收到 ACK │
▼ │
FIN_WAIT_2 │
│ │
│ ◄─────────── 发送 FIN │
│ ▼
│ LAST_ACK
│ │
│ 发送 ACK │
▼ │
TIME_WAIT │
│ │
│ 等待 2MSL ▼
▼ CLOSED
CLOSED
状态速查表:
| FIN_WAIT_1 | 已发送 FIN,等待 ACK | 主动关闭方 |
| FIN_WAIT_2 | 已收到 ACK,等待 FIN | 主动关闭方 |
| CLOSE_WAIT | 收到 FIN,等待本方关闭 | 被动关闭方 |
| LAST_ACK | 已发送 FIN,等待 ACK | 被动关闭方 |
| TIME_WAIT | 已发送 ACK,等待 2MSL | 主动关闭方 |
| CLOSED | 完全关闭 | 双方 |
六、TIME_WAIT 为什么存在?
TIME_WAIT 是四次挥手中最容易被问到的问题。
6.1 TIME_WAIT 的两个作用
如果最后的 ACK 丢失了:
Client ── FIN ──► Server
Client ◄── ACK ── Server
Client ◄── FIN ── Server
Client ── ACK ──► Server 【这个包丢了】
如果 Client 立刻关闭:
Server 会一直等 ACK(LAST_ACK 状态),超时后会重发 FIN
如果 Client 在 TIME_WAIT 状态:
– 收到 Server 重发的 FIN
– 重新发送 ACK
2MSL = 2 × Maximum Segment Lifetime(报文最大生存时间,通常为 2 分钟)
保证本次连接的包全部在网络中消亡,避免被新连接误认。
6.2 2MSL 是多少?
| Linux | 60 秒 |
| Windows | 240 秒 |
| macOS | 30 秒 |
查看 Linux 配置:
cat /proc/sys/net/ipv4/tcp_fin_timeout
# 输出 60(单位:秒,实际是 FIN_WAIT_2 超时,TIME_WAIT 相关参数需看其他配置)
6.3 TIME_WAIT 过多有什么问题?
每个 TIME_WAIT 连接会占用一个端口(主动关闭方)。高并发短连接场景下,端口被耗尽可能导致 Cannot assign requested address。
解决方案:
# 开启端口重用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 快速回收(Linux 4.12 后被移除)
# 不推荐,可能导致 NAT 问题
# 调整本地端口范围
echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range
// Java 中开启端口重用
ServerSocket socket = new ServerSocket();
socket.setReuseAddress(true);
七、为什么挥手需要四次而不是三次?
问题:第二次挥手(ACK)和第三次挥手(FIN)能否合并为一个包?
答案:通常不能,但有可能。
在特殊情况下(被动关闭方没有数据要发),确实可以合并:
Client ── FIN ──► Server
Client ◄── FIN+ACK ── Server (合并了)
Client ── ACK ──► Server
这种情况仍然只有三个包,但通常不称为“三次挥手”,因为这是优化后的特殊情况。
为什么大多数情况下是四次?
因为被动关闭方收到 FIN 后:
这两步在时间上是分离的,所以需要两个包。
八、抓包实战分析
8.1 使用 netcat 模拟连接
# 终端1 – 服务器
nc -l 8888
# 终端2 – 客户端
nc localhost 8888
按 Ctrl+C 关闭客户端,用 tcpdump 抓包:
tcpdump -i lo port 8888 -nn -S
8.2 抓包结果
1 0.000000 127.0.0.1.54321 → 127.0.0.1.8888 TCP [FIN, ACK] Seq=101 Ack=201
2 0.000015 127.0.0.1.8888 → 127.0.0.1.54321 TCP [ACK] Seq=201 Ack=102
3 0.000089 127.0.0.1.8888 → 127.0.0.1.54321 TCP [FIN, ACK] Seq=201 Ack=102
4 0.000099 127.0.0.1.54321 → 127.0.0.1.8888 TCP [ACK] Seq=102 Ack=202
分析:
- 包1:客户端发送 FIN+ACK(seq=101)
- 包2:服务器回复 ACK(确认收到 FIN)
- 包3:服务器发送 FIN+ACK(seq=201)
- 包4:客户端回复 ACK
九、常见面试题
Q1:四次挥手中哪一次可能丢失?如何应对?
| 第1次 FIN | 服务器不知道要关闭 | 客户端重传 FIN |
| 第2次 ACK | 客户端一直等 ACK | 客户端重传 FIN |
| 第3次 FIN | 客户端一直等 FIN | 服务器重传 FIN |
| 第4次 ACK | 服务器一直等 ACK | 服务器重传 FIN(客户端在 TIME_WAIT 中响应) |
Q2:为什么 TIME_WAIT 在主动关闭方,而不是被动关闭方?
因为主动关闭方发送了最后的 ACK,它需要确认这个 ACK 是否到达。被动关闭方发送的是 FIN,不需要等待 ACK 是否丢失(重传机制由主动方处理)。
Q3:服务器产生大量 TIME_WAIT 说明什么?
说明 服务器作为主动关闭方。常见场景:
- 服务器主动关闭空闲连接
- 短连接场景(如 HTTP/1.0 默认关闭)
解决方案:使用长连接(HTTP Keep-Alive)或调整内核参数。
Q4:服务器产生大量 CLOSE_WAIT 说明什么?
说明 服务器作为被动关闭方,但应用层没有及时调用 close()。
这是代码 BUG——socket 泄漏。排查方法:
# 查看进程的 socket 状态
lsof -p <pid> | grep CLOSE_WAIT
Q5:为什么握手是三次,挥手是四次?
握手时,服务器收到 SYN 可以立即回复 SYN+ACK(没有数据延迟问题)。挥手时,服务器收到 FIN 后需要先回复 ACK,等应用准备好后才能发送 FIN,这两个动作不能合并。
十、总结速查表
| 1 | 主动 → 被动 | FIN | EST → FIN_WAIT_1 | EST → CLOSE_WAIT |
| 2 | 被动 → 主动 | ACK | FIN_WAIT_1 → FIN_WAIT_2 | CLOSE_WAIT(不变) |
| 3 | 被动 → 主动 | FIN | FIN_WAIT_2 → TIME_WAIT | CLOSE_WAIT → LAST_ACK |
| 4 | 主动 → 被动 | ACK | TIME_WAIT → CLOSED(2MSL后) | LAST_ACK → CLOSED |
一句话记忆:
主动发 FIN,对方回 ACK;对方发 FIN,我回 ACK;等待 2MSL,连接正式关闭。
核心要点:
| 为什么四次 | 全双工,两个独立关闭 |
| 半关闭 | 一方关闭发送,仍可接收 |
| TIME_WAIT | 确保最后 ACK 到达 + 旧包消失 |
| 2MSL | 通常 1~4 分钟 |
| CLOSE_WAIT 过多 | 代码忘了 close socket |

|
🌺The End🌺点点关注,收藏不迷路🌺 ⬆ ⬆ 顶部 ⬆ ⬆ |


