Files
cs-note/hhs/NETWORK/04-传输层/02-TCP三次握手与四次挥手.md
T
2026-05-24 11:42:38 +08:00

200 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [计算机网络, TCP握手, TCP挥手, TIME_WAIT]
create time: 2026-05-18 01:40
---
# 三次握手与四次挥手
## 概述
TCP 连接的建立和拆除是其可靠性机制的核心环节。理解每次交互的目的,能帮助你从协议层面判断连接为什么失败或延迟高。
## 三次握手(Three-Way Handshake)
### 完整流程
```mermaid
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 确认)|
```bash
# 通过 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 说"我也完了"是两个独立的动作。
```mermaid
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 堆积的影响与应对
```bash
# 查看 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 状态 = 三次握手中途被卡住的状态
```
```bash
# 查看 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 中的实践
```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 相关的内核参数