7.4 KiB
7.4 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-05-18 01:40 |
三次握手与四次挥手
概述
TCP 连接的建立和拆除是其可靠性机制的核心环节。理解每次交互的目的,能帮助你从协议层面判断连接为什么失败或延迟高。
三次握手(Three-Way Handshake)
完整流程
sequenceDiagram
participant Client as 客户端<br/>ISN=c, seq=c
participant Server as 服务端<br/>ISN=s, seq=s
Note over Client: state=SYN_SENT<br/>allocate TCBSocket
Client->>Server: SYN (seq=c, flags=SYN)
Note over Server: state=LISTEN → SYN_RCVD<br/>allocate TCB<br/>window=w, MSS=m
Server-->>Client: SYN-ACK (seq=s, ack=c+1, flags=SYN|ACK)<br/>window=w, MSS=m
Note over Client: state=SYN_SENT → ESTABLISHED
Client->>Server: ACK (seq=c+1, ack=s+1, flags=ACK)
Note over Server: state=SYN_RCVD → ESTABLISHED
Note over Client,Server: ← ESTABLISHED ✅ 连接就绪 -->
Note over Client: send() buffer flushed
Note over Server: accept()/recv() returns data
为什么必须三次?两次够吗?
| 假设 | 问题 |
|---|---|
| 如果只要两次(SYN + ACK) | 服务器发出 SYN-ACK 后就认为对方已收到——但服务器不知道客户端是否真的收到了 SYN-ACK。如果 SYN-ACK 丢失,客户端不会发起第三次 ACK,连接永远不会进入 ESTABLISHED |
| 如果是两次且客户端重传 SYN | 服务器收到重复 SYN 会创建新的连接(没有确认上一轮的机制),导致资源泄漏 |
| 三次解决了什么 | 双方都确认了彼此的收发能力:客户端确认服务器收到了自己的 SYN;服务器确认客户端收到了自己的 SYN-ACK |
[!question] SYN-ACK 丢失后会发生什么? 客户端超时重传 SYN(通常 3s、6s、12s... 指数退避)。服务器收到重传的 SYN 后会回复新的 SYN-ACK。整个过程由内核完成,应用层无感知。
各阶段的窗口大小
握手期间还没有实际数据传输,窗口信息用于协商参数:
| 阶段 | 携带的信息 |
|---|---|
| SYN | MSS (Maximum Segment Size)、Window Scale、Timestamps |
| SYN-ACK | MSS、Window Scale、SACK Permitted |
| ACK | 不携带新选项(可选 Window Scale 确认) |
# 通过 tcpdump 观察握手
$ sudo tcpdump -nn 'tcp port 80 and (tcp[13] & 2 != 0)'
IP 192.168.1.100.54321 > 93.184.216.34.80: S 1000:1000(0) win 64240 <mss 1460,nop,wscale 7,sackOK,timestamp 123456>
↑ SYN ↑ Options
IP 93.184.216.34.80 > 192.168.1.100.54321: S 2000:2000(0) ack 1001 win 65535 <mss 1460,sackOK,timestamp 98765>
↑ SYN-ACK ↑ ACK number = ISN+1
IP 192.168.1.100.54321 > 93.184.216.34.80: . 1001:1001(0) ack 2001 win 512
↑ ACK
四次挥手(Four-Way Teardown)
为什么需要四次?
因为 TCP 是全双工的——每条方向的连接独立关闭。A 说"我完了"和 B 说"我也完了"是两个独立的动作。
sequenceDiagram
participant A as 主动关闭方 (Client)
participant B as 被动关闭方 (Server)
Note over A: Application calls close()
A->>B: FIN (seq=x) ──→ Step 1: FIN
Note over B: state=ESTABLISHED → CLOSE_WAIT
Note over B: "Client is done, but I may still have data!"
B->>A: ACK (ack=x+1) ──→ Step 2: ACK (确认FIN)
Note over B: Application calls close() after sending remaining data
B->>A: FIN (seq=y) ──→ Step 3: FIN
Note over A: state=FIN_WAIT_2 → FIN_WAIT_1
Note over A: "OK, I'm done too."
A->>B: ACK (ack=y+1) ──→ Step 4: ACK
Note over A: → TIME_WAIT (wait 2*MSL)
Note over B: → CLOSED ✅
Note over A: After 2*MSL timeout → CLOSED ✅
关键时机说明
Time A (主动方) B (被动方)
──── ───────────────────────────────────────────────────────────
t=0 close() → send(FIN) ESTABLISHED
→ FIN_WAIT_1 ↓ recv(FIN)
t=1 → CLOSE_WAIT (等应用调用close)
← ACK(FIN) send(...) remaining data
FIN_WAIT_2 ↓ close()
t=2 → send(FIN)
← FIN → LAST_ACK
t=3 ACK(FIN) → TIME_WAIT → CLOSED
(等待 2*MSL ≈ 120s)
t=120 TIME_WAIT → CLOSED ✅
TIME_WAIT 的深度解析
为什么要等 2 * MSL?
| 原因 | 说明 |
|---|---|
| 确保最后一个 ACK 到达 | 如果 ACK 丢失,对方会重发 FIN — TIME_WAIT 状态下可以重新回复 ACK |
| 让旧连接的报文在网络中自然消亡 | 避免旧连接的数据包误入新连接(同一四元组) |
MSL(Maximum Segment Lifetime)是多少?
- RFC 标准未定义具体值
- Linux / Windows / macOS 通常设为 60 秒
- 所以
2 * MSL ≈ 120 秒
TIME_WAIT 堆积的影响与应对
# 查看 TIME_WAIT 数量
$ ss -tan state time-wait | wc -l
15234
# TIME_WAIT 的源端口分布
$ ss -tan state time-wait src :80 | awk '{print $5}' | cut -d: -f2 | sort | uniq -c | sort -rn | head
# 解决方案
| 方案 | 说明 | 风险 |
|---|---|---|
| 增加可用端口数 | /proc/sys/net/ipv4/ip_local_port_range 调整为 1024 65535 |
安全扫描器可能利用短连接 |
| 启用 TIME_WAIT 复用 | net.ipv4.tcp_tw_reuse = 1 |
只能复用在 outbound 连接上(RFC 兼容性) |
| 快速回收 | net.ipv4.tcp_tw_recycle = 0(已从内核移除) |
❌ 不安全,会破坏 NAT 场景 |
| 使用 keepalive 缩短生命周期 | 降低 keepalive 超时时间 | 效果有限 |
| 架构改造 | gRPC 长连接代替 HTTP 短连接 | 最佳方案 |
[!warning] tcp_tw_recycle 已被移除 该选项在 IPv4 NAT 场景下有缺陷(不同内部主机映射到同一公网 IP 时,TS 字段冲突)。Linux 4.12 起已从内核彻底移除。
半开连接(Half-Open Connection)
SYN_RECV 状态 = 三次握手中途被卡住的状态
# 查看 SYN_RECV 数量
$ ss -tan state syn-recv
State Recv-Q Send-Q Local Port Peer Port
SYN-RECV 0 0 :80 203.0.113.5:12345
# 大量 SYN_RECV = SYN Flood 攻击!
防御措施:
tcp_syncookies = 1(开启 SYN Cookie)- 限制每秒 SYN 速率
- CDN/WAF 前置过滤
Go 中的实践
// 客户端设置 connect timeout
conn, err := net.DialTimeout("tcp", "example.com:80", 5*time.Second)
// 服务端设置 accept timeout (Go 1.19+)
ln, _ := net.Listen("tcp", ":8080")
for {
c, err := ln.Accept() // 阻塞等待
if err != nil {
// 处理 Accept 超时
break
}
go handleConn(c) // goroutine 处理每个连接
}
关联笔记
- hhs/NETWORK/TCP段结构与状态机 — 状态机的详细定义
- hhs/NETWORK/TCP可靠传输机制 — 序列号如何工作
- hhs/NETWORK/TCP内核参数调优 — TIME_WAIT 相关的内核参数