【TCP\UDP与可靠传输】TCP 的三次握手与四次挥手:把时间线讲清楚
本文详细解析了TCP协议的三次握手与四次挥手机制。三次握手通过SYN、SYN+ACK、ACK三个步骤确认通信双方的连接状态、初始序列号和收发准备;四次挥手则因TCP双向独立关闭特性需要分别确认。文章通过示例演示了正常流程,分析了两次握手和强制关闭的问题,并提供了Wireshark抓包调试技巧。同时介绍了Python实现的TCP服务器观察方法,以及处理TIME_WAIT、NAT等实际问题的解决方案。
TCP 的三次握手与四次挥手:把时间线讲清楚(含示例、实战、调试技巧)
1. 前言:为什么 TCP 要设计握手和挥手?
TCP 是一个“面向连接”的协议,两台机器正式通信前,需要双方同步下面三样东西:
- 是否在线?(Hello?能听见吗?)
- 初始序列号是多少?(后续数据包编号要对齐)
- 双方是否已经准备好收发数据?
这就是“三次握手”的任务。
通信结束后,双方又必须确认:
- 数据已经发送完
- 对方也准备好关闭
- 不会误关闭
于是出现“四次挥手”。
TCP 的设计有点像两个人对话:开始前要点头三次,结束时要挥手四次。
2. 三次握手到底确认了什么?
先给出经典时间线(从客户端视角):
客户端 → 服务器:SYN
客户端 ← 服务器:SYN + ACK
客户端 → 服务器:ACK
三次握手确认了三件事:
- 我能发给你(客户端 SYN)
- 你能收并能发回来(服务器 SYN+ACK)
- 我能收到你的回应(客户端 ACK)
✔ 示例:一次标准握手
客户端 → 服务端:
Seq=1000, SYN=1
服务端 → 客户端:
Seq=5000, Ack=1001, SYN=1, ACK=1
客户端 → 服务端:
Seq=1001, Ack=5001, ACK=1
双方序列号完成同步。
❌ 错误示例:两次握手可不可以?
假设去掉第三次 ACK:
客户端 → 服务器:SYN
服务器 → 客户端:SYN + ACK
(客户端不发 ACK)
问题来了:
- 服务器不知道客户端是否收到
- 可能造成半开连接(Half-open)
- 僵尸连接会占用服务器资源
因此必须要“第三次 ACK”作为最终确认。
🛠 调试技巧:Wireshark 抓包分析三次握手
过滤表达式:
tcp.flags.syn==1
你可以观察:
- SYN 和 SYN+ACK 的初始序列号通常是随机的
- 握手发生在端口建立之前
- 如果 ACK 丢失,服务器会重发 SYN+ACK
在调试连不上的端口时,先看 SYN 有没有被应答就能查出大半原因。
3. 四次挥手为什么比握手多一次?
原因非常简单:TCP 的收和发是独立的两个方向,关闭需要分别确认。
时间线如下:
- 客户端:
FIN=1(我不发了) - 服务端:
ACK=1(知道了) - 服务端:
FIN=1(我也不发了) - 客户端:
ACK=1(知道了)
✔ 正常四次挥手示例
客户端 → 服务端:FIN (Seq=2000)
客户端 ← 服务端:ACK (Ack=2001)
客户端 ← 服务端:FIN (Seq=8000)
客户端 → 服务端:ACK (Ack=8001)
❗ 为什么客户端会进入 TIME_WAIT?
TIME_WAIT 的意义:
- 防止旧连接的数据包乱入新连接
- 确保对方收到最后一个 ACK
TIME_WAIT 默认 2MSL(报文最大生存时间),一般为 60 秒。
服务器大量 TIME_WAIT 是常见现象,不一定是 bug。
❌ 错误示例:直接 close() 会怎样?
如果你在代码里直接强行 close() socket,可能会触发:
- RST 重置连接
- 数据未发送完毕
- 客户端报错:
Connection reset by peer
工作中遇到这种问题常见于:
- Python 多线程服务粗暴关闭连接
- Web 服务器回收不当
- Nginx upstream reset
🛠 调试技巧:观察挥手过程
Wireshark 过滤:
tcp.flags.fin1 or tcp.flags.reset1
你可以看到:
- 谁主动关闭
- 是否出现 RST
- 是否 TIME_WAIT 很多
4. 项目实战:用 Python 写一个可观察握手/挥手的最小服务器
下面是一个可以观察连接状态的“迷你 TCP 回显服务器”:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 9999))
server.listen(5)
print("Server listening on 9999...")
while True:
conn, addr = server.accept()
print("Connected:", addr)
try:
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
finally:
conn.close()
print("Connection closed:", addr)
你可以:
- 打开 Wireshark
- 用
telnet localhost 9999连接 - 观察握手/收发/挥手全过程
非常适合作为学习网络协议的“肉眼可视化教材”。
5. 高级技巧:减少 TIME_WAIT、避免端口耗尽
在高并发服务器上,TIME_WAIT 会占据大量端口。
解决方案包括:
✔ 1. 启用端口复用
Linux:
sysctl -w net.ipv4.tcp_tw_reuse=1
让 TIME_WAIT 连接能更快重用。
✔ 2. 使用 SO_REUSEADDR
服务端代码:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
避免“地址已经被占用”错误。
✔ 3. 使用负载均衡分散连接关闭方向
例如让后端主动关闭避免前端 TIME_WAIT 累积。
6. 实际工作中的坑
NAT 会改变握手行为
通过 NAT 的 TCP 握手情况如下:
- NAT 会建立映射表
- 如果 SYN 不回来,多半是路由/NAT 错误
- NAT 释放映射导致连接被断开
最常见的现象:
连接建立正常,但几分钟无数据就断开
解决:增加 keepalive。
负载均衡会导致 TIME_WAIT 集中在某一边
例如 Nginx 默认“后端先主动关闭”,导致 TIME_WAIT 集中在后端机器。
很多工程师以为服务器“有内存泄漏”,其实就是 TIME_WAIT。
代理会修改序列号
某些透明代理会重新封装 TCP 包,导致调试困难。
排查方法:
- 抓包要抓客户端和服务器两端
- 比较序列号是否一致
7. 拓展:更多深入机制
TCP Fast Open(TFO)
某些场景下,握手可以减少为“一次往返”。
工作方式:
- 服务端提前发 cookie 给客户端
- 下次连接时客户端携带 cookie 和首包数据一起发出
- 节约半个 RTT
用于 HTTPS、CDN 等时延敏感场景。
SYN Flood 攻击与半开连接
攻击者伪造大量 SYN,让服务器保持半开连接,资源耗尽。
解决方案:
- SYN Cookie
- 防火墙限速
- 负载均衡清洗
8. 总结
本篇文章完整讲解了:
- 三次握手确认的三件关键事项
- 四次挥手为何比握手多一次
- 握手和挥手的真实时间线
- NAT、负载均衡、代理对连接的实际影响
- Python 实战观察连接全流程
- 工作中常见的 TIME_WAIT、RST 问题
- 相关扩展(SYN Cookie、TCP Fast Open)
AI 创作声明:本文部分内容由 AI 辅助生成,并经人工整理与验证,仅供参考学习,欢迎指出错误与不足之处。
更多推荐

所有评论(0)