TCP 三次握手与四次挥手
深入理解 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 开始,而是由一个基于时钟的随机算法生成。原因有三:
- 防止旧报文混淆:如果 ISN 固定,网络中滞留的旧 TCP 报文可能被误认为是新连接的合法数据
- 防止序列号预测攻击:如果 ISN 可预测,攻击者可以伪造 RST 报文强制断开连接,或注入伪造数据
- 避免端口复用冲突:
(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()。
排查思路:
netstat -anp | grep CLOSE_WAIT确认数量和进程- 检查应用代码中
read()返回 0(对端关闭)后,是否正确调用了close() - 常见原因:代码逻辑中忽略了
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 的增长反映的正是发送了多少字节数据。
解读要点:
- seq 和 ack 的关系:
ack = 对方的 seq + 1(握手阶段,因为 SYN 消耗一个序列号);数据传输阶段ack = 对方的 seq + payload_length。 - SYN 消耗序列号:ISN=100 的 SYN 报文,下一个数据字节从 101 开始。因此 Step 2 的 ack 是 101。
- FIN 也消耗序列号:seq=5000 的 FIN 报文,ack 确认它是 5001。这是很多人混淆的点——FIN 虽然没有数据载荷,但仍然占用一个序列号。
- 四次挥手中间可能夹数据: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 文件,包含三个模块:
- 握手模拟器:逐步骤推进三次握手和四次挥手,观察状态变化
- 状态机探索器:悬停/点击 11 个 TCP 状态查看详细说明
- 抓包解析器:输入 seq/ack/标志位,实时判断报文所处阶段
您可以直接在下方操作交互页面,无需下载:
在线体验:TCP Explorer 交互工具
原创技术博客,转载请注明出处。
所有配图和交互页面均为自绘,可自由用于学习和分享。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)