深入理解 TCP 三次握手与四次挥手:从状态机到抓包实战

一、引言:连接的生命周期

TCP 是面向连接的协议。在数据真正开始传输之前,通信双方必须先建立一条虚拟通道——这就是三次握手(Three-Way Handshake);数据传输完毕后,双方需要优雅地释放这条通道——这就是四次挥手(Four-Way Wave)。

如果你用过 Wireshark 抓包,一定见过 SYN、SYN+ACK、FIN 这些标志位;如果你排查过线上问题,大概率遇到过 TIME_WAIT 堆积或 CLOSE_WAIT 泄漏。本文的目标是从报文结构到状态机,从理论到抓包,把"握手与挥手"这件事彻底讲透。

二、前置知识:TCP 报文头部

在理解握手之前,必须先把 TCP 报文头部的结构印在脑子里。TCP 头部最小 20 字节,最大 60 字节(含选项):

请添加图片描述

核心字段速览:

字段 位宽 作用
源端口 / 目标端口 各 16 bit 标识发送端和接收端应用进程
序列号(Sequence Number) 32 bit 本报文段数据第一个字节的编号
确认号(Acknowledgment Number) 32 bit 期望收到的下一个字节的序列号
数据偏移 4 bit TCP 头部长度 / 4,即头部有多少个 32-bit 字
标志位(Flags) 6 bit URG / ACK / PSH / RST / SYN / FIN
窗口大小 16 bit 接收窗口大小,用于流量控制
校验和 16 bit 校验整个报文段(含伪首部)
紧急指针 16 bit URG=1 时有效,指向紧急数据末尾

六个标志位是握手与挥手的主角:

标志位 全称 含义
SYN Synchronize 请求建立连接,同步序列号
ACK Acknowledgment 确认号字段有效(除第一个 SYN 外都要置 1)
FIN Finish 发送方数据已发完,请求释放连接
RST Reset 强制重置连接(异常终止)
PSH Push 提示接收方尽快将数据交付应用层
URG Urgent 紧急指针有效

关键规则:除第一个 SYN 报文外,TCP 要求所有正常通信报文 ACK=1(RST 报文除外——RST 是否带 ACK 取决于触发场景)。因此四次挥手时所有正常报文 ACK=1,区别在于 FIN 标志位的设置。

三、三次握手:逐包拆解

请添加图片描述

3.1 报文层面拆解

Step 1:Client → Server [SYN]
seq = x        (Client 随机生成的初始序列号 ISN)
ack = 0        (ACK 标志位为 0,确认号无意义)
flags = SYN
  • Client 状态:CLOSED → SYN_SENT
  • Server 收到后:分配半连接队列条目,状态 LISTEN → SYN_RCVD
Step 2:Server → Client [SYN, ACK]
seq = y        (Server 随机生成的 ISN)
ack = x + 1    (确认 Client 的 SYN,期望下一个字节序号为 x+1)
flags = SYN | ACK
  • Server 状态:SYN_RCVD(已收到 SYN,已发出 SYN+ACK)
  • Client 收到后:状态 SYN_SENT → ESTABLISHED
Step 3:Client → Server [ACK]
seq = x + 1    (Client 的第一个数据字节序号)
ack = y + 1    (确认 Server 的 SYN)
flags = ACK
  • Client 状态:ESTABLISHED
  • Server 收到后:状态 SYN_RCVD → ESTABLISHED,半连接条目移入全连接队列(accept queue)

此时连接建立完成,双方进入 ESTABLISHED,可以开始传输数据。

3.2 为什么是三次,不是两次?四次?

这是一个经典面试题,答案的核心在于:TCP 是全双工协议,需要双方各自确认对方的发送能力和接收能力正常。

  • 两次握手:Client 发送 SYN → Server 回复 SYN+ACK → 连接建立。但 Client 无法确认 Server 的接收能力是否正常(Server 的 SYN+ACK 可能在网络中丢失),Server 会一直维护半连接直到超时。更关键的是:防止已失效的连接请求报文段突然又传到了 Server。如果只有两次握手,一个在网络中滞留的旧 SYN 到达 Server 后,Server 就会错误地建立连接。

  • 三次握手:Client 的最后一次 ACK 确认了 Server 的 SYN,双方都确认了对端的收发能力。同时也让 Client 有机会拒绝/忽略旧的 SYN+ACK(不回复 ACK 即可)。

  • 四次握手:理论上可以拆成四次——Server 的 SYN 和 ACK 分开发送。但实际上 TCP 协议将其合并为一条报文(SYN+ACK),因为 SYN 和 ACK 之间没有时间依赖,合并可以减少一次网络往返。

3.3 序列号为什么要随机(ISN)

ISN(Initial Sequence Number)不是从 0 或 1 开始,而是由一个基于时钟的随机算法生成。原因有三:

  1. 防止旧报文混淆:如果 ISN 固定,网络中滞留的旧 TCP 报文可能被误认为是新连接的合法数据
  2. 防止序列号预测攻击:如果 ISN 可预测,攻击者可以伪造 RST 报文强制断开连接,或注入伪造数据
  3. 避免端口复用冲突(src_ip, src_port, dst_ip, dst_port) 四元组可能被快速复用,随机 ISN 确保前后连接不会混淆

3.4 SYN Flood 攻击与 SYN Cookies

在这里插入图片描述

SYN Flood 是经典的 DDoS 攻击方式:攻击者发送大量 SYN 报文但不完成握手,导致 Server 的半连接队列被占满,正常用户的连接请求被拒绝。

防御方案:SYN Cookies

SYN Cookies 的核心思想是无状态握手——Server 不在本地分配任何资源给半连接,而是将连接信息加密编码到 SYN+ACK 的序列号 y 中:

cookie = Hash(src_ip, src_port, dst_ip, dst_port, timestamp, secret_key)
y = cookie(编码进 ISN)

当 Client 回复 ACK 时(ack = y + 1 = cookie + 1),Server 从 ack-1 中解码出 cookie 并验证其有效性。校验通过才正式分配连接资源。

关键优势:即使面对海量 SYN Flood,Server 也不消耗内存,只在收到合法的第三次 ACK 时才创建连接。

Linux 内核参数:net.ipv4.tcp_syncookies = 1 开启 SYN Cookies。当半连接队列溢出时自动启用。

四、四次挥手:逐包拆解

请添加图片描述

TCP 连接是全双工的,每个方向都需要独立关闭。四次挥手的本质是两个方向的两次 FIN+ACK,共四条报文

4.1 报文层面拆解

Step 1:Active Closer → Passive Closer [FIN, ACK]
seq = u        (当前发送方已发送数据的最后一个字节序号 + 1)
ack = v        (确认已收到的数据)
flags = FIN | ACK
  • Active Closer 状态:ESTABLISHED → FIN_WAIT_1
  • 含义:“我的数据发完了,但还可以收数据。”
Step 2:Passive Closer → Active Closer [ACK]
seq = v
ack = u + 1    (确认对方的 FIN)
flags = ACK
  • Passive Closer 状态:ESTABLISHED → CLOSE_WAIT
  • Active Closer 收到后:FIN_WAIT_1 → FIN_WAIT_2

CLOSE_WAIT 是"被动关闭方等待应用层调用 close()"的状态。 如果应用层迟迟不调用 close(),连接会一直停留在 CLOSE_WAIT,这就是生产环境中"CLOSE_WAIT 泄漏"的根源。

Step 3:Passive Closer → Active Closer [FIN, ACK]
seq = w        (Passive Closer 可能还在 Step 2 后发了一些数据)
ack = u + 1    (对方没有再发数据,确认号不变)
flags = FIN | ACK
  • Passive Closer 状态:CLOSE_WAIT → LAST_ACK
  • Active Closer 收到后:FIN_WAIT_2 → TIME_WAIT
Step 4:Active Closer → Passive Closer [ACK]
seq = u + 1
ack = w + 1    (确认对方的 FIN)
flags = ACK
  • Passive Closer 收到后:LAST_ACK → CLOSED
  • Active Closer 进入 TIME_WAIT,等待 2MSL 后自动进入 CLOSED

4.2 TIME_WAIT 为什么是 2MSL?

MSL(Maximum Segment Lifetime)是报文段在网络中的最大存活时间,RFC 793 建议为 2 分钟,Linux 默认为 30 秒。2MSL = 最大往返时间的两倍。

TIME_WAIT 的存在有两个关键目的:

目的 1:确保最后一个 ACK 被对方收到

如果 Step 4 的 ACK 在网络中丢失,Passive Closer 会重传 FIN(LAST_ACK 状态下)。如果 Active Closer 已经进入 CLOSED,它将无法处理这个重传的 FIN,只能回复 RST,导致 Passive Closer 收到错误而非正常关闭。TIME_WAIT 状态下,Active Closer 可以接收重传的 FIN 并重新回复 ACK。

目的 2:防止旧连接的数据段混入新连接

等待 2MSL 确保本连接产生的所有报文段都从网络中消失。这样,当同一个四元组 (src_ip, src_port, dst_ip, dst_port) 被复用时,不会收到上一个连接的"幽灵报文"。

实际影响:高并发短连接场景下(如 HTTP 1.0 非 keep-alive),主动关闭方(通常是 Server)会产生大量 TIME_WAIT 状态的连接。Linux 可通过以下参数优化:

  • net.ipv4.tcp_tw_reuse = 1:允许 TIME_WAIT 连接被复用(仅客户端)
  • net.ipv4.tcp_fin_timeout:调整 FIN_WAIT_2 超时时间

4.3 CLOSE_WAIT 泄漏排查

如果服务器上出现大量 CLOSE_WAIT 连接不释放,说明应用程序收到对方的 FIN 后,一直没有调用 close() 或 shutdown()

排查思路:

  1. netstat -anp | grep CLOSE_WAIT 确认数量和进程
  2. 检查应用代码中 read() 返回 0(对端关闭)后,是否正确调用了 close()
  3. 常见原因:代码逻辑中忽略了 read() == 0 的 EOF 场景;或者资源清理异常处理不完整

五、状态机全景

TCP 连接共有 11 种状态。理解状态机是排障和面试的基础:

在这里插入图片描述

(截图自 TCP Explorer 交互页面 的状态机模块)

状态一览

状态 含义 典型场景
CLOSED 无连接 初始 / 最终
LISTEN 监听中 服务器等待连接
SYN_SENT 已发 SYN 客户端 connect() 后
SYN_RCVD 已收 SYN 并回复 服务器收到 SYN 后
ESTABLISHED 连接已建立 数据传输中
FIN_WAIT_1 主动关闭,已发 FIN close() 后
FIN_WAIT_2 已收到 ACK,等待对方 FIN 半关闭
CLOSING 双方同时关闭 同时发送 FIN(罕见)
TIME_WAIT 等待 2MSL 主动关闭方最终状态
CLOSE_WAIT 已收到 FIN,等待应用 close() 被动关闭方
LAST_ACK 被动关闭方已发 FIN 等待最后 ACK

同时打开与同时关闭

请添加图片描述

虽然少见,但 TCP 协议设计时就考虑了同时打开和同时关闭的场景:

  • 同时打开:双方都从 CLOSED 发出 SYN,各自进入 SYN_SENT。收到对方的 SYN 后(而非预期的 SYN+ACK),进入 SYN_RCVD,再各自回复 SYN+ACK。最终双方都进入 ESTABLISHED,共交换 4 条报文。路径:CLOSED → SYN_SENT → SYN_RCVD → ESTABLISHED

  • 同时关闭:双方同时发送 FIN,从 ESTABLISHED 进入 FIN_WAIT_1。收到对方的 FIN 后直接进入 CLOSING(跳过 FIN_WAIT_2),各自回复 ACK 后进入 TIME_WAIT。路径:ESTABLISHED → FIN_WAIT_1 → CLOSING → TIME_WAIT

六、实战:Wireshark 抓包解读

假设你用 Wireshark 抓到一个 TCP 流,显示过滤 tcp.stream eq 0,你会看到类似这样的序列:

No.  Src → Dst           Flags       seq     ack     Info
1    C → S               SYN         100     0       Client → Server SYN
2    S → C               SYN, ACK    200     101     Server → Client SYN+ACK
3    C → S               ACK         101     201     Client → Server ACK (handshake complete)
...  (data transfer with PSH, ACK) ...
100  C → S               FIN, ACK    5000    8000    Client → Server FIN
101  S → C               ACK         8000    5001    Server → Client ACK
102  S → C               FIN, ACK    8000    5001    Server → Client FIN
103  C → S               ACK         5001    8001    Client → Server ACK (final)

在这里插入图片描述

(截图自 TCP Explorer 交互页面 的抓包解析模块)

示例中 seq/ack 值的演变逻辑:Client ISN=x=100,Server ISN=y=200。握手完成后双方 seq 均+1。假设数据传输阶段 Client 发送了 4900 字节(seq 从 101 上升到 5000),Server 发送了 7800 字节(seq 从 201 上升到 8000),所以 FIN 报文 seq=5000/8000,ack=8000/5001。抓包时注意 seq 的增长反映的正是发送了多少字节数据。

解读要点:

  1. seq 和 ack 的关系ack = 对方的 seq + 1(握手阶段,因为 SYN 消耗一个序列号);数据传输阶段 ack = 对方的 seq + payload_length
  2. SYN 消耗序列号:ISN=100 的 SYN 报文,下一个数据字节从 101 开始。因此 Step 2 的 ack 是 101。
  3. FIN 也消耗序列号:seq=5000 的 FIN 报文,ack 确认它是 5001。这是很多人混淆的点——FIN 虽然没有数据载荷,但仍然占用一个序列号。
  4. 四次挥手中间可能夹数据:Step 2 的 ACK 和 Step 3 的 FIN 之间,Passive Closer 还可以发送数据(CLOSE_WAIT 状态下)。

七、总结

维度 三次握手 四次挥手
目的 建立全双工连接 释放两个方向的连接
报文数 3 条 4 条(可优化为 3 条如果双方同时关闭)
关键状态 CLOSED → SYN_SENT → ESTABLISHED FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
标志位 SYN → SYN+ACK → ACK FIN → ACK → FIN → ACK
耗时 1.5 RTT 2 RTT + 2MSL
安全隐患 SYN Flood → SYN Cookies 防御 TIME_WAIT 堆积 / CLOSE_WAIT 泄漏

面试高频 Q&A 速查

问题 一句话答案
为什么三次握手不是两次? 两次握手无法防止已失效的连接请求到达服务端导致错误建立连接,且无法让客户端确认服务端的接收能力
SYN 报文为什么消耗序列号? 因为 SYN 需要被可靠确认,消耗一个序列号才能用 ack=x+1 来确认它;ACK 不需确认所以不消耗
FIN 报文为什么也消耗序列号? 同理,FIN 需要被对方确认,占用一个序列号可以精确确认"我收到了你的 FIN"
TIME_WAIT 为什么是 2MSL? 1个 MSL 让最后的 ACK 到达对端,1个 MSL 让对端重传的 FIN 到达本端,合计确保所有残余报文消失
CLOSE_WAIT 太多怎么排查? netstat -anp | grep CLOSE_WAIT 定位进程,检查代码中 read()==0 后是否调用了 close()
SYN Flood 怎么防御? 开启 SYN Cookies(tcp_syncookies=1)、增大半连接队列、缩短 SYN Timeout、部署 SYN Proxy
能否三次挥手就关闭连接? 可以,如果被动关闭方在收到 FIN 时也没有数据要发送,可以将 ACK+FIN 合并为一条报文(变成三次挥手)
TCP Fast Open 是什么? 在 SYN 报文中携带数据(用 Cookie 验证),省去一次 RTT,将握手+首次数据传输压缩到 1 RTT

延伸阅读方向

  • TCP Fast Open (TFO):在 SYN 报文中携带数据,将握手 + 首次数据传输从 2 RTT 降到 1 RTT
  • TCP keepalive:长时间空闲连接的心跳探测机制
  • QUIC 协议:基于 UDP,将握手 + 加密协商合并为 1 RTT(甚至 0 RTT),从根本上解决了 TCP 三次握手的延迟问题

动手实践

本文配图使用的 TCP Explorer 交互页面 是一个独立的 HTML 文件,包含三个模块:

  1. 握手模拟器:逐步骤推进三次握手和四次挥手,观察状态变化
  2. 状态机探索器:悬停/点击 11 个 TCP 状态查看详细说明
  3. 抓包解析器:输入 seq/ack/标志位,实时判断报文所处阶段

您可以直接在下方操作交互页面,无需下载:

在线体验TCP Explorer 交互工具


原创技术博客,转载请注明出处。
所有配图和交互页面均为自绘,可自由用于学习和分享。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐