三次握手与连接建立
为什么需要握手
TCP 是面向连接的协议,通信前必须确认三件事:
- 双方在线:对方主机是否可达、端口是否监听
- 序列号同步:双方交换初始序列号(ISN),后续数据传输以此为基础
- 参数协商:交换 MSS、窗口缩放因子、SACK 支持等选项
为什么是三次而非两次? 两次握手存在"历史失效连接"问题:客户端发送的 SYN 因网络延迟长期滞留,待客户端已放弃后该 SYN 到达服务端,服务端会错误建立连接并分配资源,而客户端对此毫不知情,导致服务端维护大量无效连接。
三次握手完整流程
第一步:SYN
客户端发送:
SYN=1, Seq=ISN_c(客户端初始序列号)
Flags: 0x002 (SYN)
选项: MSS, Window Scale, SACK Permitted, Timestamp...
客户端进入 SYN_SENT 状态。
第二步:SYN-ACK
服务端回复:
SYN=1, ACK=1
Seq=ISN_s(服务端初始序列号)
Ack=ISN_c + 1(期望收到客户端的下一个字节)
Flags: 0x012 (SYN+ACK)
选项: MSS, Window Scale, SACK Permitted, Timestamp...
服务端进入 SYN_RCVD 状态。
第三步:ACK
客户端发送:
ACK=1
Seq=ISN_c + 1(SYN 消耗了一个序列号)
Ack=ISN_s + 1(期望收到服务端的下一个字节)
Flags: 0x010 (ACK)
客户端进入 ESTABLISHED 状态;服务端收到后也进入 ESTABLISHED。
关键细节:
- 第三步的 ACK 可以携带数据(如果双方都支持 TFO,甚至第一步 SYN 就能带数据)
- 第三步的 ACK 如果丢失,服务端会重发 SYN-ACK(超时机制)
序列号变化图解
- SYN 和 FIN 各消耗 1 个序列号(虽然没有数据载荷)
- 纯 ACK 不消耗序列号(除非携带数据)
- 后续数据传输:客户端 Seq 从 1001 开始,服务端 Seq 从 5001 开始
SYN Flood 攻击与防御
攻击原理
攻击者伪造大量源 IP,向服务端发送 SYN,但不回复最后的 ACK:
服务端为每个 SYN-RCVD 状态的连接分配资源(TCB,Transmission Control Block),半连接队列(SYN Queue)满后,正常用户的 SYN 被拒绝。
防御:SYN Cookie(RFC 4987)
服务端收到 SYN 后,不分配 TCB,而是计算一个 Cookie 放入 SYN-ACK 的序列号中:
Cookie = Hash(客户端IP, 客户端端口, 服务端IP, 服务端端口, 时间戳, 秘密密钥)
如果客户端是合法的,会回复 ACK(Ack = Cookie + 1),服务端验证 Cookie 有效后才分配 TCB。这样半连接队列不再受攻击影响。
Linux 开启 SYN Cookie:
sysctl net.ipv4.tcp_syncookies=1
TCP Fast Open(TFO,RFC 7413)
正常三次握手需要 1 个 RTT 才能发送数据。TFO 允许在首次 SYN 中携带数据,减少 1 个 RTT:
TFO Cookie 由服务端在首次连接时生成,客户端缓存后复用。对 Web 短连接(如 REST API)性能提升显著。
本篇小结
- 三次握手目的:确认双方在线、同步 ISN、协商选项
- 三次而非两次:防止历史失效连接导致服务端资源浪费
- 序列号变化:SYN/ACK 各消耗 1 个序列号,纯 ACK 不消耗
- SYN Flood:伪造大量 SYN 占满半连接队列
- SYN Cookie:不分配 TCB,用 Cookie 验证合法性
- TFO:首次握手获取 Cookie,后续 SYN 直接带数据,省 1 RTT
动手实践
Wireshark 抓包观察三次握手:
- 过滤
tcp.flags.syn==1 && tcp.flags.ack==0找第一个 SYN - 观察 ISN、Ack、选项协商
- 过滤
查看 Linux 半连接队列状态:
ss -tan | grep SYN-RECV | wc -l cat /proc/net/sockstat | grep TCP检查 SYN Cookie 是否开启:
sysctl net.ipv4.tcp_syncookies思考:如果网络中同时存在两个相同的五元组连接(如客户端快速重连),旧的延迟 SYN 到达服务端会怎样?TIME_WAIT 状态如何防止这种情况?