三次握手原理:SYN、SYN-ACK、ACK标志位详解与状态变迁

聊TCP三次握手,说白了就是两个端点互相确认「我准备好了,你呢?」的过程。我刚开始学网络时,总觉得这三次握手有点啰嗦——为什么非得来回三次?两次不行吗?后来在项目里踩过坑才明白,少一次都不行。

TCP标志位:握手的「暗号」

TCP头部里有一组标志位,握手阶段主要用到三个:SYN、ACK、FIN。FIN我们后面讲挥手时再说,今天先聚焦前两个。

标志位 全称 含义 握手中的角色
SYN Synchronize 同步序列号 发起连接请求
ACK Acknowledgment 确认收到 确认对方报文
SYN-ACK SYN + ACK 同步+确认 响应连接请求并同步自身

你想想看,SYN和ACK就像两个人见面打招呼:

  • SYN = 「嘿,你在吗?这是我的序列号X」
  • SYN-ACK = 「我在!收到你的X了,这是我的序列号Y」
  • ACK = 「收到你的Y了,咱们开始聊吧」

三次握手完整流程

我习惯把三次握手拆成三个步骤来讲,每个步骤对应一个报文段:

  1. 第一次握手:客户端 → 服务器 (SYN)
    • 客户端发送SYN报文,初始序列号设为seq=x
    • 客户端进入 SYN_SENT 状态
    • 此时客户端不能发送数据,只能等待
  2. 第二次握手:服务器 → 客户端 (SYN-ACK)
    • 服务器收到SYN,回复SYN-ACK报文
    • 确认号 ack=x+1,自身序列号 seq=y
    • 服务器进入 SYN_RCVD 状态
  3. 第三次握手:客户端 → 服务器 (ACK)
    • 客户端收到SYN-ACK,回复ACK报文
    • 确认号 ack=y+1,序列号 seq=x+1
    • 客户端进入 ESTABLISHED 状态
    • 服务器收到ACK后,也进入 ESTABLISHED 状态

核心要点:第三次握手是可以携带数据的。客户端在发送ACK的同时,可以直接捎带上应用层数据。但前两次握手不能带数据——这是TCP协议明确规定的。

状态变迁图:从生到连的完整路径

下面这张图我画了很多遍,每次讲课时都会拿出来。它展示了客户端和服务器各自的状态流转:

TCP三次握手状态变迁图 客户端 服务器 CLOSED SYN_SENT ESTABLISHED CLOSED LISTEN SYN_RCVD ESTABLISHED ① SYN (seq=x) ② SYN-ACK (seq=y, ack=x+1) ③ ACK (seq=x+1, ack=y+1) 说明: 客户端主动发起连接,服务器被动监听。三次握手完成后,双方都进入 ESTABLISHED 状态,开始传输数据。

嗯,这里要注意一个细节:服务器从 CLOSED 到 LISTEN 这一步,不是握手触发的,而是应用层调用了 listen() 系统调用。我在排查问题时,经常看到有人把 LISTEN 状态误当成「已经建立连接」,其实它只是在等别人来连。

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

这个问题我面试时经常问。说白了,两次握手无法防止「已失效的连接请求」突然到达服务器。

举个例子:客户端发了个SYN,网络拥堵导致它超时重传。服务器收到重传的SYN,回复SYN-ACK,连接建立。但此时第一个SYN其实还在路上——它只是延迟了。如果只有两次握手,服务器收到这个延迟的SYN后会直接建立连接,白白浪费资源。

三次握手解决了这个问题:客户端收到SYN-ACK后,会检查确认号是否匹配。如果匹配不上(比如是旧连接的),客户端会发送RST复位报文,告诉服务器「别等了,这是个假请求」。

我的经验:有一次排查线上连接超时,发现服务器 SYN_RCVD 状态的连接堆积了几千个。抓包一看,客户端发了SYN后没收到SYN-ACK,一直在重传。最后定位到是服务器端的半连接队列满了——这就是典型的「SYN洪水」现象。遇到这种情况,先看 netstat -s | grep LISTEN 里的溢出计数。

状态变迁速查表

为了方便你在Wireshark里快速定位,我整理了一张状态对应表:

状态 谁的状态 触发条件 Wireshark过滤
CLOSED 双方 初始状态,或连接关闭后 无(抓不到)
LISTEN 服务器 调用 listen() tcp.flags.syn == 1 and tcp.flags.ack == 0
SYN_SENT 客户端 发送SYN后 tcp.flags.syn == 1 and tcp.flags.ack == 0
SYN_RCVD 服务器 收到SYN,回复SYN-ACK后 tcp.flags.syn == 1 and tcp.flags.ack == 1
ESTABLISHED 双方 三次握手完成 tcp.flags.ack == 1 and tcp.flags.syn == 0

避坑指南:我曾经在抓包时看到大量 SYN_RCVD 状态的连接,以为是服务器性能问题。后来才发现是客户端防火墙把SYN-ACK给丢了。记住:SYN_RCVD 堆积不一定是服务器的问题,也可能是中间链路或对端的问题。抓包要两端都抓,才能定位到底是谁没收到报文。

序列号与确认号:握手的「对账本」

三次握手除了建立连接,还有一个重要任务——同步初始序列号。每个TCP连接都有两个序列号:客户端发数据的序列号(ISN_C)和服务器发数据的序列号(ISN_S)。

握手过程中:

  • 客户端在SYN里告诉服务器:「我的初始序列号是X」
  • 服务器在SYN-ACK里告诉客户端:「我的初始序列号是Y,而且我确认收到了你的X」
  • 客户端在ACK里确认:「我收到你的Y了」

你想想看,如果没有这个同步过程,双方都不知道对方从哪里开始编号,数据收发就会乱套。我在Wireshark里看TCP流时,第一件事就是看三次握手的序列号——如果序列号不连续,说明中间有丢包或重传。

嗯,关于状态变迁和标志位,今天就先聊这么多。记住这张状态图,后面讲超时重传和连接故障定位时,你会反复用到它。