2. TCP三次握手原理:SYN、SYN-ACK、ACK详解、握手状态机、握手失败与超时的关系
说到TCP连接超时,绕不开的就是三次握手。很多刚入行的朋友觉得握手就是“发三个包”,其实没那么简单。我见过不少线上故障,根因就是握手阶段出了问题。今天咱们把这层窗户纸捅破。
2.1 三次握手到底在干什么?
说白了,三次握手就是通信双方互相确认“我准备好了,你呢?”的过程。它要解决三个核心问题:
- 确认对方在线——不是僵尸进程或网络不可达
- 交换初始序列号——后续数据包的编号起点
- 协商窗口大小等参数——双方能收多少数据
你想想看,如果只有两次握手,服务端怎么知道客户端收到了自己的SYN-ACK?万一丢了,服务端就得一直傻等。三次握手是经过实践检验的最小可靠方案。
2.2 三个报文,一个都不能少
| 步骤 | 报文 | 方向 | 核心作用 |
|---|---|---|---|
| 第1步 | SYN | 客户端 → 服务端 | 请求建立连接,携带初始序列号(ISN) |
| 第2步 | SYN-ACK | 服务端 → 客户端 | 确认收到SYN,同时发送自己的SYN |
| 第3步 | ACK | 客户端 → 服务端 | 确认收到SYN-ACK,连接建立完成 |
这里有个细节:SYN和ACK标志位是同时存在的。SYN-ACK报文里两个标志位都置1,表示“我收到了你的SYN,同时我也要发起连接”。
抓包要点:在Wireshark里过滤 tcp.flags.syn == 1 就能看到所有SYN包。但要注意,SYN-ACK包也会被匹配到,因为它的SYN位也是1。如果想只看纯SYN,用 tcp.flags.syn == 1 and tcp.flags.ack == 0。
2.3 握手状态机:从CLOSED到ESTABLISHED
TCP连接的状态变化,就像一部电梯的楼层按钮。每个状态都对应一个特定的阶段。我个人习惯在排查问题时,先看两端的状态,再决定抓包方向。
这张图我建议你保存下来。每次排查连接超时,我都会在脑子里过一遍这个状态机,看两端分别卡在哪个状态。
2.4 握手失败与超时的关系
握手失败,说白了就是三个报文没凑齐。最常见的场景:
- SYN丢了:客户端发了SYN,石沉大海。客户端会等一个RTO(初始重传超时),默认3秒,然后重传SYN。
- SYN-ACK丢了:服务端回了SYN-ACK,客户端没收到。客户端继续重传SYN,服务端收到重复SYN后重新回复SYN-ACK。
- ACK丢了:客户端发了ACK,服务端没收到。服务端会重传SYN-ACK,客户端收到后重新回复ACK。
注意:我曾经遇到过一个案例,客户端一直报连接超时,抓包发现客户端发了SYN,服务端也回了SYN-ACK,但客户端就是没反应。最后发现是客户端的防火墙把SYN-ACK当成“外部主动连接”给拦截了。所以抓包一定要在两端都抓,只看一端容易误判。
2.5 超时参数与重传策略
| 参数 | 默认值(Linux) | 说明 |
|---|---|---|
tcp_syn_retries |
6 | SYN重传次数,超过后放弃连接 |
tcp_synack_retries |
5 | SYN-ACK重传次数,超过后放弃 |
tcp_retries1 |
3 | 尝试重传阈值,超过后通知上层 |
tcp_retries2 |
15 | 最终重传阈值,超过后断开连接 |
嗯,这里要注意:重传超时时间是指数退避的。第一次重传等3秒,第二次等6秒,第三次12秒……所以如果网络真的不通,客户端可能要等上几分钟才会报错。这就是为什么很多应用层超时设置得比TCP超时短——不能傻等。
实战技巧:在排查连接超时时,我习惯先看抓包里的 [SYN] 包有没有被重传。如果看到多个SYN包,说明客户端没收到SYN-ACK。这时候去服务端抓包,看服务端有没有收到SYN、有没有回复SYN-ACK。一对比,问题出在哪段链路就清楚了。
2.6 握手超时的典型抓包特征
在Wireshark里,握手超时通常表现为:
- 只有SYN,没有SYN-ACK:客户端发了SYN,然后不断重传。这通常是网络不通或服务端没监听端口。
- 有SYN和SYN-ACK,但没有最后的ACK:服务端在重传SYN-ACK。这通常是客户端的ACK丢了,或者客户端已经放弃连接。
- 有完整的三个包,但紧接着就是RST:连接建立后立即被重置。这通常是应用层拒绝连接,比如防火墙策略或服务端连接数满。
你想想看,如果抓包看到的是第一种情况,你该查什么?先确认服务端端口是否监听,再检查中间防火墙有没有拦截。如果是第二种,重点查客户端为什么没回ACK——是不是客户端已经超时退出了?
好了,三次握手的原理就聊到这儿。记住这个状态机,后面讲抓包实战时,我们会反复用到它。