news 2026/5/30 5:18:30

TCP 三次握手与四次挥手

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 bitTCP 头部长度 / 4,即头部有多少个 32-bit 字
标志位(Flags)6 bitURG / ACK / PSH / RST / SYN / FIN
窗口大小16 bit接收窗口大小,用于流量控制
校验和16 bit校验整个报文段(含伪首部)
紧急指针16 bitURG=1 时有效,指向紧急数据末尾

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

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

关键规则:除第一个 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主动关闭,已发 FINclose() 后
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 → ESTABLISHEDFIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
标志位SYN → SYN+ACK → ACKFIN → ACK → FIN → ACK
耗时1.5 RTT2 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 交互工具


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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/30 5:18:30

UE5 C++ 新手避坑:为什么你的CreateWidget函数在Actor里编译不过?

UE5 C 新手避坑指南:CreateWidget函数在Actor中编译失败的深层解析当你第一次尝试在UE5的Actor类中使用CreateWidget函数时,可能会遇到一个令人困惑的编译错误。这个看似简单的UI创建函数背后隐藏着引擎设计的深层逻辑,理解这些规则将帮助你避…

作者头像 李华
网站建设 2026/5/30 5:14:38

机器学习工程化实战:跨越从原型到生产的四大核心挑战

1. 项目概述:从实验室到生产线的鸿沟在数据科学和机器学习领域待了十几年,我见过太多才华横溢的团队和令人眼前一亮的模型,最终却无声无息地“死”在了演示用的Jupyter Notebook里。大家津津乐道的,往往是Kaggle竞赛里那零点几个百…

作者头像 李华
网站建设 2026/5/30 5:13:53

AI搜索变革下企业营销策略重塑:从流量拦截到心智渗透

1. 项目概述:当搜索引擎开始“思考”最近,谷歌在搜索领域的一系列动作,让整个数字营销圈和商业世界都绷紧了神经。核心的变化,是搜索正在从一个“关键词匹配”的工具,向一个“理解意图并直接给出答案”的智能助手演变。…

作者头像 李华
网站建设 2026/5/30 5:13:09

从提示词到应用矩阵:基于GPT-4与Flask构建AI驱动型产品的实践指南

1. 从零到一:一个想法的诞生与GPT-4的初体验作为一名在软件行业摸爬滚打了十多年的开发者,我见过技术浪潮的起起落落,但像GPT-4这样能直接改变我们构建应用方式的东西,确实不多见。我的故事始于一个非常具体且普遍的问题&#xff…

作者头像 李华
网站建设 2026/5/30 5:12:53

Armv9-A架构中FEAT_RNG与FEAT_RME的依赖关系解析

1. Arm架构中FEAT_RNG/FEAT_RNG_TRAP与FEAT_RME的依赖关系解析在Armv9-A架构中,当处理器核心实现了FEAT_RME(Realm Management Extension)时,架构规范明确要求必须同时实现FEAT_RNG(Random Number Generation&#xff…

作者头像 李华