TCP 的三次握手与四次挥手:把时间线讲清楚(含示例、实战、调试技巧)


1. 前言:为什么 TCP 要设计握手和挥手?

TCP 是一个“面向连接”的协议,两台机器正式通信前,需要双方同步下面三样东西:

  • 是否在线?(Hello?能听见吗?)
  • 初始序列号是多少?(后续数据包编号要对齐)
  • 双方是否已经准备好收发数据?

这就是“三次握手”的任务。

通信结束后,双方又必须确认:

  • 数据已经发送完
  • 对方也准备好关闭
  • 不会误关闭

于是出现“四次挥手”。

TCP 的设计有点像两个人对话:开始前要点头三次,结束时要挥手四次。


2. 三次握手到底确认了什么?

先给出经典时间线(从客户端视角):

客户端 → 服务器:SYN
客户端 ← 服务器:SYN + ACK
客户端 → 服务器:ACK

三次握手确认了三件事:

  1. 我能发给你(客户端 SYN)
  2. 你能收并能发回来(服务器 SYN+ACK)
  3. 我能收到你的回应(客户端 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 的收和发是独立的两个方向,关闭需要分别确认。

时间线如下:

  1. 客户端:FIN=1(我不发了)
  2. 服务端:ACK=1(知道了)
  3. 服务端:FIN=1(我也不发了)
  4. 客户端:ACK=1(知道了)

✔ 正常四次挥手示例

客户端 → 服务端:FIN (Seq=2000)
客户端 ← 服务端:ACK (Ack=2001)

客户端 ← 服务端:FIN (Seq=8000)
客户端 → 服务端:ACK (Ack=8001)


❗ 为什么客户端会进入 TIME_WAIT?

TIME_WAIT 的意义:

  1. 防止旧连接的数据包乱入新连接
  2. 确保对方收到最后一个 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)

你可以:

  1. 打开 Wireshark
  2. telnet localhost 9999 连接
  3. 观察握手/收发/挥手全过程

非常适合作为学习网络协议的“肉眼可视化教材”。


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 辅助生成,并经人工整理与验证,仅供参考学习,欢迎指出错误与不足之处。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐