三次握手原理: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了,咱们开始聊吧」
三次握手完整流程
我习惯把三次握手拆成三个步骤来讲,每个步骤对应一个报文段:
- 第一次握手:客户端 → 服务器 (SYN)
- 客户端发送SYN报文,初始序列号设为
seq=x - 客户端进入
SYN_SENT状态 - 此时客户端不能发送数据,只能等待
- 客户端发送SYN报文,初始序列号设为
- 第二次握手:服务器 → 客户端 (SYN-ACK)
- 服务器收到SYN,回复SYN-ACK报文
- 确认号
ack=x+1,自身序列号seq=y - 服务器进入
SYN_RCVD状态
- 第三次握手:客户端 → 服务器 (ACK)
- 客户端收到SYN-ACK,回复ACK报文
- 确认号
ack=y+1,序列号seq=x+1 - 客户端进入
ESTABLISHED状态 - 服务器收到ACK后,也进入
ESTABLISHED状态
核心要点:第三次握手是可以携带数据的。客户端在发送ACK的同时,可以直接捎带上应用层数据。但前两次握手不能带数据——这是TCP协议明确规定的。
状态变迁图:从生到连的完整路径
下面这张图我画了很多遍,每次讲课时都会拿出来。它展示了客户端和服务器各自的状态流转:
嗯,这里要注意一个细节:服务器从 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流时,第一件事就是看三次握手的序列号——如果序列号不连续,说明中间有丢包或重传。
嗯,关于状态变迁和标志位,今天就先聊这么多。记住这张状态图,后面讲超时重传和连接故障定位时,你会反复用到它。