【计算机网络考研】P45: TCP连接管理——四次挥手断开连接
目录
博主智算菩萨,专注于人工智能、Python编程、音视频处理及UI窗体程序设计等方向。致力于以通俗易懂的方式拆解前沿技术,从零基础入门到高阶实战,陪伴开发者共同成长。目前已开设五大技术专栏,累计发布多篇原创技术文章,深受读者好评。
📌 专栏导航
- 人工智能前沿知识(已更191篇):深度剖析Transformer架构、生成式AI、强化学习、具身智能、神经符号系统、大模型及智能体(Agent)技术,系统性解析AI核心技术体系与前沿趋势。
- Python基础小白编程(已更232篇):从零开始,以保姆式教程讲解变量、数据类型、流程控制、函数等核心语法,配有大量实战代码与避坑指南,真正做到学以致用。
- 机器学习与深度学习(125篇):系统化拆解线性模型、决策树、随机森林、梯度提升树、神经网络等算法原理与工程实践,覆盖从公式推导到代码实现的全链路内容。
- 音频、图像与视频处理理论与实战(81篇):涵盖FFmpeg多媒体处理、audio_shop开源工具、ComfyUI-WanVideoWrapper视频生成等实用技术,从基础操作到高级应用一应俱全。
- UI窗体程序设计实战(78篇):深入讲解UI设计、动态窗体生成、游戏UI框架设计等实战技巧,提供从配置到编码的完整解决方案。
智算菩萨,以代码为经,以算法为纬,在人工智能的星辰大海中,做你前行路上最可靠的导航者。本人最常用AI工具为AIGCBAR。
考研重点等级:⭐⭐⭐⭐⭐(核心考点)
TCP四次挥手与三次握手同等重要,是考研必考知识点。特别是TIME_WAIT状态及其2MSL超时时间的含义,几乎是每年必考的内容。理解四次挥手的关键在于理解"为什么需要四次"以及"TIME_WAIT状态的作用"。
1. TCP连接释放的背景
1.1 从"挂电话"理解连接释放
想象你和朋友通电话,通话结束时:
你:“好了,我要说的说完了。”(表明自己没有数据要发了)
朋友:“好的,我知道了。”(确认收到你的结束信号,但可能还有话要说)
朋友:“对了,还有一件事……好了,我也说完了。”(朋友的数据发完了,也想结束)
你:“好的,那我挂了。”(确认收到朋友的结束信号)
【等待一会儿,确认对方没有再说什么,才真正挂断】
这个"四次确认"的过程,就是TCP四次挥手的真实写照。与建立连接时双方"同时开始"不同,关闭连接时往往是一方先结束,另一方可能还有数据要发送,因此需要分别关闭两个方向的数据传输。
1.2 为什么关闭比建立更复杂?
| 对比项 | 连接建立(三次握手) | 连接释放(四次挥手) |
|---|---|---|
| 方向性 | 双方同时建立双向连接 | 可能一方先关闭,另一方还有数据 |
| 报文数 | 3个报文段 | 4个报文段 |
| 同时性 | 服务器将SYN和ACK合并 | FIN和ACK通常不能合并 |
| 最终状态 | ESTABLISHED | TIME_WAIT + CLOSED |
| 资源分配 | 分配资源 | 需要等待后才释放资源 |
💡 核心差异:三次握手中,服务器的SYN和ACK可以合并成一个报文段;但四次挥手中,被动方收到FIN后可能还有数据要发,所以不能立即发FIN,必须先发ACK确认,等数据发完再发FIN。这就导致了ACK和FIN不能合并,需要四次报文交换。
2. TCP四次挥手的详细过程
2.1 四次挥手全景概览
假设客户端先主动关闭连接(实际上,服务器也可以主动关闭,两种情况都需要掌握)。
【图1】TCP四次挥手完整时序图
2.2 状态转换图
【图2】TCP四次挥手完整状态转换图
🔥 注意:上图展示了客户端主动关闭的情况。如果服务器主动关闭,双方的状态名称会互换(服务器走FIN-WAIT路径,客户端走CLOSE-WAIT路径)。
2.3 第一次挥手:主动方发送FIN
报文内容:FIN=1, seq=u
详细解读:
当主动方(假设为客户端)的数据发送完毕后,调用close()或shutdown(),发送第一个FIN报文段。
- FIN=1:结束标志位(Finish),表示"我没有数据要发送了,请求关闭连接"。
- seq=u:序号。
u是客户端当前发送序号的一个值。FIN报文段不携带数据,但它消耗一个序号(与SYN类似)。
关键点:
- 发送FIN后,客户端进入
FIN-WAIT-1状态。 - 此时客户端仍然可以接收服务器发来的数据(半关闭状态)。
- FIN报文段不携带数据,但消耗序号
u。
2.4 第二次挥手:被动方回复ACK
报文内容:ACK=1, seq=v, ack=u+1
详细解读:
服务器收到客户端的FIN后,立即回复ACK确认。
- ACK=1:确认标志位置1。
- seq=v:服务器的序号。
v是服务器当前发送序号,等于服务器之前发送数据的最后一个字节序号+1。 - ack=u+1:确认号。由于FIN消耗了一个序号
u,所以ack=u+1,表示"我已收到序号u及之前的所有数据"。
关键点:
- 服务器发送ACK后,进入
CLOSE-WAIT状态。 - CLOSE-WAIT状态的含义:服务器已经知道客户端没有数据要发了,但服务器自己可能还有数据要发送。
- 在CLOSE-WAIT状态下,服务器仍然可以继续发送数据给客户端(这就是半关闭的含义)。
- 客户端收到这个ACK后,从
FIN-WAIT-1进入FIN-WAIT-2状态。
💡 类比理解:就像你说"我说完了",对方说"好的我知道了",但对方可能还有话要说,所以通话还不能完全结束。
2.5 第三次挥手:被动方发送FIN
报文内容:FIN=1, ACK=1, seq=w, ack=u+1
详细解读:
当服务器的数据也发送完毕后,发送自己的FIN报文段。
- FIN=1:服务器也没有数据要发送了。
- ACK=1:ACK标志仍然置1。
- seq=w:序号。注意
w可能不等于v!因为在CLOSE-WAIT状态期间,服务器可能还发送了一些数据,序号从v增长到了w。 - ack=u+1:确认号仍然是
u+1,因为从第二次挥手到现在,客户端没有发送新的数据。
关键点:
- 服务器发送FIN后,进入
LAST-ACK状态(最后确认状态)。 - 这是服务器最后一次发送报文,等待客户端的最终ACK。
2.6 第四次挥手:主动方回复ACK
报文内容:ACK=1, seq=u+1, ack=w+1
详细解读:
客户端收到服务器的FIN后,发送最后的ACK确认。
- ACK=1:确认标志位置1。
- seq=u+1:客户端的序号。由于第一次挥手的FIN消耗了序号
u,所以当前序号是u+1。 - ack=w+1:确认号,表示"我已收到序号
w及之前的所有数据(包括你的FIN)"。
关键点:
- 发送ACK后,客户端不立即关闭连接,而是进入
TIME-WAIT状态。 - 客户端在TIME-WAIT状态等待2MSL时间后,才进入CLOSED状态。
- 服务器收到这个ACK后,立即进入CLOSED状态。
- 如果服务器没有收到这个ACK(ACK丢失),服务器会重发FIN,客户端在TIME-WAIT状态能收到这个重发的FIN并重新发送ACK。
2.7 四次挥手报文汇总
| 挥手次数 | 发送方 | 报文类型 | FIN | ACK | seq值 | ack值 | 是否消耗序号 | 状态变化 |
|---|---|---|---|---|---|---|---|---|
| 第一次 | 主动方 | FIN | 1 | 0 | u | 无 | 消耗 | ESTAB→FIN-WAIT-1 |
| 第二次 | 被动方 | ACK | 0 | 1 | v | u+1 | 不消耗 | ESTAB→CLOSE-WAIT |
| 第三次 | 被动方 | FIN+ACK | 1 | 1 | w | u+1 | 消耗 | CLOSE-WAIT→LAST-ACK |
| 第四次 | 主动方 | ACK | 0 | 1 | u+1 | w+1 | 不消耗 | FIN-WAIT-2→TIME-WAIT |
3. TIME_WAIT状态详解(考研必考)
3.1 什么是TIME_WAIT?
TIME_WAIT是TCP连接释放过程中一个特殊的状态。主动关闭方在发送最后一个ACK后,不会立即关闭连接,而是进入TIME_WAIT状态,等待2MSL(2倍的最长报文段寿命)时间后才真正关闭。
3.2 TIME_WAIT的两个重要作用
作用1:保证主动方发送的最后一个ACK能被被动方收到
这是TIME_WAIT最直接的作用。考虑以下场景:
如果客户端发送最后一个ACK后立即关闭:
- 如果这个ACK丢失了,服务器会重发FIN
- 但客户端已经关闭了,不会响应这个FIN
- 服务器永远收不到确认,无法正确关闭
而在TIME_WAIT状态下:
- 如果ACK丢失,服务器重发的FIN会到达客户端
- 客户端重新发送ACK
- 重置2MSL计时器,继续等待
- 确保服务器能正确关闭
作用2:防止已失效的连接请求报文出现在后续连接中
这个作用与三次握手中防止失效连接的原理类似:
- 假设客户端发送最后一个ACK后立即关闭
- 网络中可能还有之前连接的延迟报文段
- 如果客户端立即用相同的端口建立新连接
- 这些"旧"报文段可能被新连接误认为是有效数据
TIME_WAIT等待2MSL确保了:
- 网络中所有来自旧连接的报文段都已经消失(因为它们的寿命最长为MSL)
- 新连接不会收到旧的、延迟的报文段
3.3 MSL的定义与取值
MSL(Maximum Segment Lifetime)是最长报文段寿命,即一个TCP报文段在网络中存活的最长时间。
| 参数 | 含义 | 典型值 |
|---|---|---|
| MSL | 最长报文段寿命 | 2分钟(RFC推荐) |
| 2MSL | TIME_WAIT等待时间 | 4分钟 |
| Linux默认值 | /proc/sys/net/ipv4/tcp_fin_timeout | 通常60秒 |
🔥 考研考点:TIME_WAIT等待时间是2MSL,不是MSL。为什么是2MSL?因为最坏情况下,最后一个ACK需要一个MSL到达对方,如果对方重发FIN,又需要一个MSL到达自己。所以总共需要2MSL来确保双方都能正确关闭。
3.4 TIME_WAIT的潜在问题
在高并发的服务器场景中,TIME_WAIT状态可能导致问题:
- 每个连接在主动关闭后都会占用一个TIME_WAIT状态的端口
- 如果TIME_WAIT连接过多,可能耗尽可用端口
- 解决方案:连接复用(Connection Reuse)、缩短TIME_WAIT时间、使用被动关闭策略
4. 为什么需要四次挥手?三次行不行?
这是考研中另一个高频问题。核心答案是:因为TCP是全双工通信,两个方向的关闭需要独立进行。
4.1 不能合并成三次的原因
关键问题:被动方收到FIN后,可能还有数据要发送。
第二次挥手的ACK和第三次挥手的FIN通常不能合并,因为:
- 服务器收到FIN后,必须立即回复ACK(确认收到)
- 但服务器可能还有数据没发完
- 等数据发完后,才能发送FIN
- 这两个操作有时间差,所以不能合并
4.2 特殊情况:三次挥手也是可能的
在某些特殊情况下,四次挥手可以变成三次:
- 如果被动方在收到FIN时,恰好也没有数据要发送了
- 那么ACK和FIN可以合并成一个报文段(捎带确认)
- 此时只需要三次报文交换
但这只是特殊情况,不是一般情况,所以TCP协议设计为四次挥手。
4.3 对比:三次握手vs四次挥手
| 对比项 | 三次握手 | 四次挥手 |
|---|---|---|
| 报文数 | 3 | 4 |
| SYN+ACK合并 | 可以(服务器同时发SYN和ACK) | FIN+ACK通常不能合并 |
| 不能合并的原因 | N/A | 被动方可能还有数据 |
| 最终等待 | 不需要 | TIME_WAIT (2MSL) |
| 双方状态 | 同时ESTABLISHED | 关闭有先后顺序 |
5. 优雅关闭 vs 强制关闭(RST)
5.1 优雅关闭(Graceful Close)
四次挥手就是优雅关闭的过程:
- 双方通过FIN/ACK协商关闭连接
- 所有已发送的数据都能被对方接收
- 连接有序地释放,不丢失数据
5.2 强制关闭(RST复位)
在某些情况下,TCP使用RST(Reset)标志位来强制关闭连接:
触发RST的常见场景:
| 场景 | 说明 |
|---|---|
| 端口未监听 | 向一个没有进程监听的端口发送SYN |
| 连接异常 | 收到一个不属于任何已知连接的报文段 |
| 超时过重置 | 某些实现中连接超时时发送RST |
| 主动异常关闭 | 程序崩溃或强制终止时 |
RST报文段的特点:
- RST=1,不消耗序号(特殊情况)
- 收到RST的一方立即关闭连接
- 不经过四次挥手的协商过程
- 可能导致数据丢失
6. 实战:Python模拟TCP四次挥手过程
下面用Python代码完整模拟TCP四次挥手的过程,包括TIME_WAIT状态的模拟。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TCP四次挥手模拟器
完整模拟TCP连接释放过程,包括:
1. 四次报文交换
2. 状态转换
3. TIME_WAIT状态和2MSL等待
4. 详细的调试输出
使用方法:
python tcp_disconnect.py
"""
import time
import threading
class TCPPacket:
"""TCP报文段模拟类(复用P44的定义)"""
def __init__(self, src_port, dst_port, seq, ack=0, syn=0,
ack_flag=0, fin=0, rst=0, data=""):
self.src_port = src_port
self.dst_port = dst_port
self.seq = seq
self.ack = ack
self.syn = syn
self.ack_flag = ack_flag
self.fin = fin
self.rst = rst
self.data = data
def __str__(self):
flags = []
if self.syn:
flags.append("SYN=1")
if self.ack_flag:
flags.append("ACK=1")
if self.fin:
flags.append("FIN=1")
if self.rst:
flags.append("RST=1")
flag_str = ", ".join(flags) if flags else "无标志位"
result = f"[{flag_str}]"
result += f", seq={self.seq}"
if self.ack_flag or self.syn:
result += f", ack={self.ack}"
if self.data:
result += f", 数据='{self.data}'({len(self.data)}字节)"
return result
class TCPDisconnectSimulator:
"""
TCP四次挥手模拟器
模拟客户端和服务器之间释放TCP连接的完整四次挥手过程。
包含完整的状态转换和TIME_WAIT状态模拟。
状态机(客户端主动关闭):
客户端: ESTAB -> FIN-WAIT-1 -> FIN-WAIT-2 -> TIME-WAIT -> CLOSED
服务器: ESTAB -> CLOSE-WAIT -> LAST-ACK -> CLOSED
"""
# 类常量:MSL时间(模拟用,实际为2分钟)
MSL = 2 # 模拟2秒为1个MSL,2MSL = 4秒
def __init__(self, client_port=54321, server_port=80):
"""
初始化模拟器
参数:
client_port: 客户端端口号
server_port: 服务器端口号
"""
# 客户端状态
self.client_port = client_port
self.client_seq = 1500 # 客户端当前序号
self.client_state = "ESTABLISHED"
# 服务器状态
self.server_port = server_port
self.server_seq = 2500 # 服务器当前序号
self.server_state = "ESTABLISHED"
# 数据传输标记
self.server_has_more_data = True # 服务器是否还有数据要发
print("=" * 70)
print(" TCP 四次挥手连接释放模拟器")
print("=" * 70)
print(f"\n📋 初始化参数:")
print(f" 客户端端口: {self.client_port}, 当前序号: {self.client_seq}")
print(f" 服务器端口: {self.server_port}, 当前序号: {self.server_seq}")
print(f" 客户端状态: {self.client_state}")
print(f" 服务器状态: {self.server_state}")
print(f" 模拟MSL时间: {self.MSL}秒")
print("=" * 70)
def log_step(self, step_num, title, detail=""):
"""打印步骤分隔符"""
print(f"\n{'─' * 70}")
print(f"【第{step_num}步】{title}")
if detail:
print(f" {detail}")
print(f"{'─' * 70}")
def log_state(self):
"""打印当前状态"""
print(f" [状态] 客户端: {self.client_state:<20} "
f"| 服务器: {self.server_state}")
# ==================== 第一次挥手 ====================
def client_send_fin(self):
"""
主动方(客户端)发送FIN报文段(第一次挥手)
报文内容: FIN=1, seq=u
不携带数据,但消耗一个序号
"""
self.log_step(1, "第一次挥手:客户端 → 服务器 (FIN)",
"客户端数据发送完毕,请求关闭连接")
u = self.client_seq
packet = TCPPacket(
src_port=self.client_port,
dst_port=self.server_port,
seq=u,
fin=1,
ack=0,
ack_flag=0
)
print(f" 📤 客户端发送: {packet}")
print(f" 💡 说明: FIN=1表示关闭请求,seq={u}")
print(f" 💡 说明: FIN报文段不携带数据,但消耗一个序号")
print(f" 💡 说明: 客户端→服务器方向的写通道关闭")
# 状态转换
self.client_state = "FIN-WAIT-1"
self.client_seq = u + 1 # FIN消耗一个序号
print(f" 🔄 客户端状态转换: ESTABLISHED → FIN-WAIT-1")
self.log_state()
return packet
def server_receive_fin(self, fin_packet):
"""
被动方(服务器)收到FIN,回复ACK(第二次挥手)
报文内容: ACK=1, seq=v, ack=u+1
参数:
fin_packet: 收到的FIN报文段
"""
self.log_step(2, "第二次挥手:服务器 → 客户端 (ACK)",
"服务器收到FIN,回复ACK确认")
u = fin_packet.seq
v = self.server_seq
packet = TCPPacket(
src_port=self.server_port,
dst_port=self.client_port,
seq=v,
ack=u + 1,
ack_flag=1
)
print(f" 📥 服务器收到: {fin_packet}")
print(f" 📤 服务器发送: {packet}")
print(f" 💡 说明: ACK=1, ack={u}+1={u+1} 确认收到客户端的FIN")
print(f" 💡 说明: 服务器 seq={v},还有数据要发送...")
print(f" 💡 说明: ⚠️ 注意:服务器仍可向客户端发送数据!")
# 状态转换
self.server_state = "CLOSE-WAIT"
print(f" 🔄 服务器状态转换: ESTABLISHED → CLOSE-WAIT")
self.log_state()
return packet
def server_send_data_during_close_wait(self):
"""
模拟服务器在CLOSE-WAIT状态下继续发送数据
这是四次挥手的关键——半关闭状态
"""
if not self.server_has_more_data:
return []
print(f"\n 📤 [服务器在CLOSE-WAIT状态继续发送数据]")
packets = []
for i, data in enumerate(["剩余数据1", "剩余数据2"]):
packet = TCPPacket(
src_port=self.server_port,
dst_port=self.client_port,
seq=self.server_seq,
ack_flag=1,
ack=self.client_seq, # 确认号
data=data
)
print(f" 发送数据: seq={self.server_seq}, "
f"data='{data}'")
self.server_seq += len(data)
packets.append(packet)
print(f" 💡 说明: 半关闭状态下,服务器→客户端方向仍然开放")
print(f" 💡 说明: 服务器序号从 {self.server_seq - sum(len(d) for d in ['剩余数据1', '剩余数据2'])} "
f"增长到 {self.server_seq}")
return packets
# ==================== 第三次挥手 ====================
def server_send_fin(self):
"""
被动方(服务器)数据发送完毕,发送FIN(第三次挥手)
报文内容: FIN=1, ACK=1, seq=w, ack=u+1
"""
self.log_step(3, "第三次挥手:服务器 → 客户端 (FIN+ACK)",
"服务器数据也发送完毕,发送FIN请求关闭")
w = self.server_seq
packet = TCPPacket(
src_port=self.server_port,
dst_port=self.client_port,
seq=w,
ack=self.client_seq,
fin=1,
ack_flag=1
)
print(f" 📤 服务器发送: {packet}")
print(f" 💡 说明: FIN=1, ACK=1,seq={w}")
print(f" 💡 说明: 注意seq={w} > 之前的seq值,因为中间发了数据")
print(f" 💡 说明: ack={self.client_seq},客户端没有新数据")
# 状态转换
self.server_state = "LAST-ACK"
self.server_seq = w + 1 # FIN消耗一个序号
print(f" 🔄 服务器状态转换: CLOSE-WAIT → LAST-ACK")
self.log_state()
return packet
def client_receive_ack(self, ack_packet):
"""
客户端收到第二次挥手的ACK
从FIN-WAIT-1进入FIN-WAIT-2
参数:
ack_packet: 收到的ACK报文段
"""
print(f"\n 📥 客户端收到: {ack_packet}")
print(f" 💡 说明: 服务器确认收到我的FIN了")
self.client_state = "FIN-WAIT-2"
print(f" 🔄 客户端状态转换: FIN-WAIT-1 → FIN-WAIT-2")
self.log_state()
# ==================== 第四次挥手 ====================
def client_receive_fin(self, fin_packet):
"""
主动方(客户端)收到FIN,回复ACK,进入TIME_WAIT(第四次挥手)
报文内容: ACK=1, seq=u+1, ack=w+1
参数:
fin_packet: 收到的FIN报文段
"""
self.log_step(4, "第四次挥手:客户端 → 服务器 (ACK)",
"客户端收到FIN,回复ACK,进入TIME_WAIT")
w = fin_packet.seq
packet = TCPPacket(
src_port=self.client_port,
dst_port=self.server_port,
seq=self.client_seq,
ack=w + 1,
ack_flag=1
)
print(f" 📥 客户端收到: {fin_packet}")
print(f" 📤 客户端发送: {packet}")
print(f" 💡 说明: ACK=1, ack={w}+1={w+1} 确认收到服务器的FIN")
print(f" 💡 说明: seq={self.client_seq}(FIN-WAIT-1之后没有新数据)")
# 状态转换 - 进入TIME_WAIT
self.client_state = "TIME-WAIT"
print(f" 🔄 客户端状态转换: FIN-WAIT-2 → TIME-WAIT")
self.log_state()
return packet
def server_receive_ack(self, ack_packet):
"""
服务器收到最后一个ACK,连接关闭
参数:
ack_packet: 收到的ACK报文段
"""
print(f"\n 📥 服务器收到: {ack_packet}")
print(f" ✅ 服务器收到最终ACK,连接关闭!")
self.server_state = "CLOSED"
print(f" 🔄 服务器状态转换: LAST-ACK → CLOSED")
self.log_state()
# ==================== TIME_WAIT等待 ====================
def time_wait_expire(self):
"""
TIME_WAIT超时(2MSL),连接完全关闭
这是连接释放的最后一步
"""
print(f"\n{'─' * 70}")
print(f"【TIME_WAIT阶段】等待2MSL = {2 * self.MSL}秒")
print(f"{'─' * 70}")
print(f" ⏱️ 客户端进入TIME_WAIT状态")
print(f" 💡 作用1: 保证最后一个ACK能被服务器收到")
print(f" 💡 如果ACK丢失,服务器会重发FIN")
print(f" 💡 客户端在TIME_WAIT状态仍能响应")
print(f" 💡 作用2: 防止旧连接的报文干扰新连接")
print(f" 💡 等待2MSL确保网络中所有旧报文消失")
# 模拟等待
print(f" ⏳ 开始等待 {2 * self.MSL}秒...")
# time.sleep(2 * self.MSL) # 实际运行时取消注释
# 模拟可能的重发FIN场景
print(f"\n 📋 场景模拟: 假设最后一个ACK丢失了...")
print(f" 📥 服务器在LAST-ACK状态超时,重发FIN")
print(f" 📤 客户端在TIME_WAIT状态收到FIN,重发ACK")
print(f" ✅ TIME_WAIT确保了这种容错能力!")
# 最终关闭
self.client_state = "CLOSED"
print(f"\n ⏰ {2 * self.MSL}秒时间到!")
print(f" ✅ 客户端连接完全关闭!")
print(f" 🔄 客户端状态转换: TIME-WAIT → CLOSED")
self.log_state()
# ==================== 完整模拟 ====================
def simulate(self):
"""
模拟完整的四次挥手过程
按顺序执行:
1. 客户端发送FIN(第一次挥手)
2. 服务器回复ACK(第二次挥手)
3. 服务器继续发送数据(半关闭状态演示)
4. 服务器发送FIN(第三次挥手)
5. 客户端回复ACK,进入TIME_WAIT(第四次挥手)
6. TIME_WAIT超时,连接关闭
"""
print("\n🔔 开始模拟TCP四次挥手...\n")
# 第一次挥手
fin = self.client_send_fin()
time.sleep(0.5)
# 第二次挥手
ack = self.server_receive_fin(fin)
time.sleep(0.5)
# 客户端收到ACK,进入FIN-WAIT-2
self.client_receive_ack(ack)
time.sleep(0.5)
# 服务器在CLOSE-WAIT状态下继续发送数据(半关闭演示)
self.server_send_data_during_close_wait()
time.sleep(0.5)
# 第三次挥手
fin2 = self.server_send_fin()
time.sleep(0.5)
# 第四次挥手
ack2 = self.client_receive_fin(fin2)
time.sleep(0.5)
# 服务器收到ACK,关闭
self.server_receive_ack(ack2)
# TIME_WAIT阶段
self.time_wait_expire()
# 打印总结
self.print_summary()
def print_summary(self):
"""打印挥手过程总结"""
print("\n" + "=" * 70)
print(" 挥手过程总结")
print("=" * 70)
print("\n📊 序号变化总结:")
print(f" 客户端: 初始seq = 1500, FIN后seq = 1501")
print(f" 服务器: 初始seq = 2500")
print(f" 发送数据后增长到 {self.server_seq - 1}")
print("\n🔄 客户端状态转换:")
print(" ESTABLISHED → FIN-WAIT-1 → FIN-WAIT-2 → TIME-WAIT → CLOSED")
print("\n🔄 服务器状态转换:")
print(" ESTABLISHED → CLOSE-WAIT → LAST-ACK → CLOSED")
print("\n📋 四次挥手核心要点:")
print(" 1. 第一次: FIN=1, seq=u(消耗一个序号)")
print(" 2. 第二次: ACK=1, seq=v, ack=u+1")
print(" 3. 第三次: FIN=1, ACK=1, seq=w, ack=u+1")
print(" 4. 第四次: ACK=1, seq=u+1, ack=w+1")
print("\n📋 TIME_WAIT要点:")
print(f" - 等待时间: 2MSL")
print(" - 作用1: 确保最后一个ACK能被收到")
print(" - 作用2: 防止旧连接报文干扰新连接")
print("=" * 70)
def main():
"""主程序:运行TCP四次挥手模拟器"""
simulator = TCPDisconnectSimulator(
client_port=54321,
server_port=80
)
simulator.simulate()
print("\n✅ 模拟完成!")
if __name__ == "__main__":
main()
运行结果示例:
======================================================================
TCP 四次挥手连接释放模拟器
======================================================================
📋 初始化参数:
客户端端口: 54321, 当前序号: 1500
服务器端口: 80, 当前序号: 2500
客户端状态: ESTABLISHED
服务器状态: ESTABLISHED
模拟MSL时间: 2秒
======================================================================
🔔 开始模拟TCP四次挥手...
----------------------------------------------------------------------
【第1步】第一次挥手:客户端 → 服务器 (FIN)
客户端数据发送完毕,请求关闭连接
----------------------------------------------------------------------
📤 客户端发送: [FIN=1], seq=1500
💡 说明: FIN=1表示关闭请求,seq=1500
💡 说明: FIN报文段不携带数据,但消耗一个序号
💡 说明: 客户端→服务器方向的写通道关闭
🔄 客户端状态转换: ESTABLISHED → FIN-WAIT-1
[状态] 客户端: FIN-WAIT-1 | 服务器: ESTABLISHED
----------------------------------------------------------------------
【第2步】第二次挥手:服务器 → 客户端 (ACK)
服务器收到FIN,回复ACK确认
----------------------------------------------------------------------
📥 服务器收到: [FIN=1], seq=1500
📤 服务器发送: [ACK=1], seq=2500, ack=1501
💡 说明: ACK=1, ack=1500+1=1501 确认收到客户端的FIN
💡 说明: 服务器 seq=2500,还有数据要发送...
💡 说明: ⚠️ 注意:服务器仍可向客户端发送数据!
🔄 服务器状态转换: ESTABLISHED → CLOSE-WAIT
[状态] 客户端: FIN-WAIT-1 | 服务器: CLOSE-WAIT
📥 客户端收到: [ACK=1], seq=2500, ack=1501
💡 说明: 服务器确认收到我的FIN了
🔄 客户端状态转换: FIN-WAIT-1 → FIN-WAIT-2
[状态] 客户端: FIN-WAIT-2 | 服务器: CLOSE-WAIT
📤 [服务器在CLOSE-WAIT状态继续发送数据]
发送数据: seq=2500, data='剩余数据1'
发送数据: seq=2510, data='剩余数据2'
💡 说明: 半关闭状态下,服务器→客户端方向仍然开放
💡 说明: 服务器序号从 2500 增长到 2520
----------------------------------------------------------------------
【第3步】第三次挥手:服务器 → 客户端 (FIN+ACK)
服务器数据也发送完毕,发送FIN请求关闭
----------------------------------------------------------------------
📤 服务器发送: [FIN=1, ACK=1], seq=2520, ack=1501
💡 说明: FIN=1, ACK=1,seq=2520
💡 说明: 注意seq=2520 > 之前的seq值,因为中间发了数据
💡 说明: ack=1501,客户端没有新数据
🔄 服务器状态转换: CLOSE-WAIT → LAST-ACK
[状态] 客户端: FIN-WAIT-2 | 服务器: LAST-ACK
----------------------------------------------------------------------
【第4步】第四次挥手:客户端 → 服务器 (ACK)
客户端收到FIN,回复ACK,进入TIME_WAIT
----------------------------------------------------------------------
📥 客户端收到: [FIN=1, ACK=1], seq=2520, ack=1501
📤 客户端发送: [ACK=1], seq=1501, ack=2521
💡 说明: ACK=1, ack=2520+1=2521 确认收到服务器的FIN
💡 说明: seq=1501(FIN-WAIT-1之后没有新数据)
🔄 客户端状态转换: FIN-WAIT-2 → TIME-WAIT
[状态] 客户端: TIME-WAIT | 服务器: LAST-ACK
📥 服务器收到: [ACK=1], seq=1501, ack=2521
✅ 服务器收到最终ACK,连接关闭!
🔄 服务器状态转换: LAST-ACK → CLOSED
[状态] 客户端: TIME-WAIT | 服务器: CLOSED
----------------------------------------------------------------------
【TIME_WAIT阶段】等待2MSL = 4秒
----------------------------------------------------------------------
⏱️ 客户端进入TIME_WAIT状态
💡 作用1: 保证最后一个ACK能被服务器收到
💡 如果ACK丢失,服务器会重发FIN
💡 客户端在TIME_WAIT状态仍能响应
💡 作用2: 防止旧连接的报文干扰新连接
💡 等待2MSL确保网络中所有旧报文消失
⏳ 开始等待 4秒...
📋 场景模拟: 假设最后一个ACK丢失了...
📥 服务器在LAST-ACK状态超时,重发FIN
📤 客户端在TIME_WAIT状态收到FIN,重发ACK
✅ TIME_WAIT确保了这种容错能力!
⏰ 4秒时间到!
✅ 客户端连接完全关闭!
🔄 客户端状态转换: TIME-WAIT → CLOSED
[状态] 客户端: CLOSED | 服务器: CLOSED
======================================================================
挥手过程总结
======================================================================
📊 序号变化总结:
客户端: 初始seq = 1500, FIN后seq = 1501
服务器: 初始seq = 2500
发送数据后增长到 2520
🔄 客户端状态转换:
ESTABLISHED → FIN-WAIT-1 → FIN-WAIT-2 → TIME-WAIT → CLOSED
🔄 服务器状态转换:
ESTABLISHED → CLOSE-WAIT → LAST-ACK → CLOSED
📋 四次挥手核心要点:
1. 第一次: FIN=1, seq=u(消耗一个序号)
2. 第二次: ACK=1, seq=v, ack=u+1
3. 第三次: FIN=1, ACK=1, seq=w, ack=u+1
4. 第四次: ACK=1, seq=u+1, ack=w+1
📋 TIME_WAIT要点:
- 等待时间: 2MSL
- 作用1: 确保最后一个ACK能被收到
- 作用2: 防止旧连接报文干扰新连接
======================================================================
✅ 模拟完成!
7. 经典例题解析
例题1(TIME_WAIT状态作用)
题目:TCP连接释放时,主动关闭方为什么要进入TIME_WAIT状态并等待2MSL时间?
标准答案:
TIME_WAIT状态有两个重要作用:
第一,保证主动关闭方发送的最后一个ACK能被被动关闭方收到。 如果最后一个ACK丢失,被动方会超时重发FIN报文段。主动方在TIME_WAIT状态下仍能收到这个重发的FIN并重新发送ACK。如果主动方发送ACK后立即关闭,就无法响应重发的FIN,导致被动方无法正常关闭。
第二,防止已失效的连接请求报文段出现在后续连接中。 等待2MSL可以确保网络中所有来自旧连接的报文段都已经消失(一个报文段的最长寿命为MSL),避免这些旧报文被后续使用相同端口的新连接误接收。
例题2(状态转换分析)
题目:假设客户端主动关闭TCP连接,请画出客户端和服务器的完整状态转换序列,并标注每个转换的触发条件。
解答:
客户端:
ESTABLISHED --(发送FIN)--> FIN-WAIT-1 --(收到ACK)--> FIN-WAIT-2
|
v
CLOSED <-- TIME-WAIT <--(收到FIN,发送ACK)--
服务器:
ESTABLISHED --(收到FIN,发送ACK)--> CLOSE-WAIT --(发送FIN)--> LAST-ACK --(收到ACK)--> CLOSED
例题3(序号与确认号计算)
题目:在TCP四次挥手中,若客户端当前序号seq=200,服务器当前序号seq=500。假设双方都没有再发送数据,请填写下表:
| 挥手次数 | 发送方 | FIN | ACK | seq | ack |
|---|---|---|---|---|---|
| 第一次 | 客户端 | 1 | 0 | ? | — |
| 第二次 | 服务器 | 0 | 1 | ? | ? |
| 第三次 | 服务器 | 1 | 1 | ? | ? |
| 第四次 | 客户端 | 0 | 1 | ? | ? |
解答:
| 挥手次数 | 发送方 | FIN | ACK | seq | ack |
|---|---|---|---|---|---|
| 第一次 | 客户端 | 1 | 0 | 200 | — |
| 第二次 | 服务器 | 0 | 1 | 500 | 201 |
| 第三次 | 服务器 | 1 | 1 | 500 | 201 |
| 第四次 | 客户端 | 0 | 1 | 201 | 501 |
计算说明:
- 第一次:
seq = 200,FIN消耗序号200 - 第二次:
seq = 500(服务器序号不变),ack = 200 + 1 = 201 - 第三次:
seq = 500(服务器没发数据,序号不变),ack = 201 - 第四次:
seq = 200 + 1 = 201,ack = 500 + 1 = 501
⚠️ 注意:本题假设"双方都没有再发送数据",所以服务器的seq始终是500。如果有数据传输,第三次的seq会大于500。
例题4(CLOSE-WAIT状态分析)
题目:某服务器发现大量连接处于CLOSE-WAIT状态,可能是什么原因?如何排查?
解答:
CLOSE-WAIT状态表示服务器已经收到了对方的FIN,但还没有发送自己的FIN。大量CLOSE-WAIT状态通常说明:
- 服务器程序有Bug:收到对方的关闭请求后,程序没有调用
close()来关闭连接 - 服务器还在处理数据:程序确实还在发送数据(正常情况,但应该尽快完成)
- 服务器端程序阻塞:程序卡在某个地方,无法执行关闭操作
排查方法:
- 检查服务器代码是否在收到EOF时正确调用了
close() - 使用
netstat -n | grep CLOSE_WAIT查看具体连接 - 检查服务器日志,看是否有异常或阻塞
8. 考研重点速记
8.1 必背要点
| 要点 | 内容 |
|---|---|
| 四次挥手报文 | ①FIN ②ACK ③FIN+ACK ④ACK |
| FIN报文段 | 不携带数据,但消耗一个序号 |
| 半关闭状态 | CLOSE-WAIT期间被动方仍可发送数据 |
| TIME_WAIT等待 | 2MSL,不是MSL |
| MSL含义 | Maximum Segment Lifetime,最长报文段寿命 |
| MSL典型值 | 2分钟,2MSL = 4分钟 |
| TIME_WAIT作用1 | 确保最后一个ACK能被收到(容错) |
| TIME_WAIT作用2 | 防止旧连接报文干扰新连接 |
| 为什么四次 | 被动方可能还有数据,ACK和FIN不能合并 |
8.2 三次握手 vs 四次挥手 终极对比
| 对比项 | 三次握手 | 四次挥手 |
|---|---|---|
| 报文数 | 3 | 4 |
| 第一个报文 | SYN | FIN |
| SYN+ACK合并 | ✅ 可以 | ❌ 通常不行 |
| 不合并原因 | N/A | 被动方可能还有数据 |
| 消耗序号的报文 | SYN, SYN+ACK | FIN, FIN+ACK |
| 最终状态 | ESTABLISHED | TIME_WAIT + CLOSED |
| 等待机制 | 无 | 2MSL |
| 双方同步性 | 同时就绪 | 有先后顺序 |
8.3 状态转换速查表
| 状态 | 含义 | 属于哪方 |
|---|---|---|
| LISTEN | 监听连接请求 | 服务器 |
| SYN-SENT | SYN已发送 | 客户端 |
| SYN-RCVD | SYN已收到 | 服务器 |
| ESTABLISHED | 连接已建立 | 双方 |
| FIN-WAIT-1 | FIN已发送 | 主动关闭方 |
| FIN-WAIT-2 | FIN已发送且ACK已收到 | 主动关闭方 |
| CLOSE-WAIT | 收到FIN,等待关闭 | 被动关闭方 |
| LAST-ACK | FIN已发送,等待最后ACK | 被动关闭方 |
| TIME-WAIT | 等待2MSL | 主动关闭方 |
| CLOSED | 连接关闭 | 双方 |
考研寄语:四次挥手的核心是理解"全双工分别关闭"和"TIME_WAIT的两个作用"。熟记状态转换图,做到看到一个状态就能立刻反应出它是哪一方、处于什么阶段、下一步会转换到什么状态。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)