04-TCP 连接管理
04 - TCP 连接管理
4.1 TCP 状态机
┌──────────┐
主动打开/ │ │
发送 SYN ──→│ SYN_SENT │───── 收到 SYN+ACK ──┐
└──────────┘ │
│ ▼
┌────┴────┐ ┌──────────────┐
收到 SYN │ │ │ │
发 SYN+ACK│ LISTEN │ │ ESTABLISHED │
│ │ │ │
└────┬────┘ └──────┬───────┘
│ │
▼ │
┌──────────────────────────┐ │
│ SYN_RCVD │ │
│ (收到 SYN,已回 SYN+ACK) │ │
└────────────┬─────────────┘ │
│ 收到 ACK │
▼ │
┌──────────────┐ │
│ ESTABLISHED │←────────────────────┘
└──────┬───────┘
│
┌─────────────────┼─────────────────┐
│ 主动关闭 │ 被动关闭 │
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│ FIN_WAIT_1 │ │ CLOSE_WAIT │ │
└──────┬───────┘ └──────┬───────┘ │
│ 收到 ACK │ 发送 FIN │
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│ FIN_WAIT_2 │ │ LAST_ACK │ │
└──────┬───────┘ └──────┬───────┘ │
│ 收到 FIN │ 收到 ACK │
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│ TIME_WAIT │ │ CLOSED │ │
└──────┬───────┘ └──────────────┘ │
│ 2MSL 超时 │
▼ │
┌──────────────┐ │
│ CLOSED │ │
└──────────────┘ │
4.2 TCP 状态说明
| 状态 | 说明 | 触发条件 |
|---|---|---|
| CLOSED | 关闭状态 | 初始/最终状态 |
| LISTEN | 监听状态 | 服务器调用 listen() |
| SYN_SENT | SYN 已发送 | 客户端发送 SYN |
| SYN_RCVD | SYN 已接收 | 服务器收到 SYN |
| ESTABLISHED | 连接已建立 | 收到 ACK 完成握手 |
| FIN_WAIT_1 | 等待 FIN 确认 | 主动关闭方发送 FIN |
| FIN_WAIT_2 | 等待对方 FIN | 收到 FIN 的 ACK |
| CLOSE_WAIT | 等待应用关闭 | 被动关闭方收到 FIN |
| LAST_ACK | 等待最后 ACK | 被动关闭方发送 FIN |
| TIME_WAIT | 等待 2MSL | 主动关闭方收到 FIN |
4.3 三次握手 (Three-Way Handshake)
Client Server
│ │
│ ① SYN (seq=x) │
│ ─────────────────────────────────→ │ SYN_RCVD
│ │
│ ② SYN+ACK (seq=y, ack=x+1) │
│ ←───────────────────────────────── │ SYN_RCVD
│ │
│ ③ ACK (ack=y+1) │ ESTABLISHED
│ ─────────────────────────────────→ │ ESTABLISHED
│ │
│ 数据传输开始 │
为什么需要三次握手?
问题场景:如果只有两次握手
旧的 SYN 包延迟到达:
1. 客户端发送 SYN(旧连接的)
2. 服务器收到后以为是新连接,回复 SYN+ACK
3. 服务器立即进入 ESTABLISHED 状态
4. 客户端发现这是旧连接,忽略
→ 服务器白白分配了资源!
三次握手的解决方案:
1. 客户端发送 SYN
2. 服务器回复 SYN+ACK
3. 客户端验证 ACK 正确后才确认连接
→ 防止历史连接的初始化
三次握手代码示例
import socket
import threading
import time
def server():
"""服务端:模拟三次握手"""
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('0.0.0.0', 8888))
srv.listen(1)
print("[Server] LISTEN 状态,等待连接...")
conn, addr = srv.accept() # 内核完成三次握手
print(f"[Server] ESTABLISHED,客户端: {addr}")
data = conn.recv(1024)
print(f"[Server] 收到: {data.decode()}")
conn.sendall(b"Hello from server")
conn.close()
srv.close()
def client():
"""客户端:模拟三次握手"""
time.sleep(0.5)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("[Client] 发送 SYN...")
sock.connect(('127.0.0.1', 8888)) # 内核完成三次握手
print("[Client] ESTABLISHED")
sock.sendall(b"Hello from client")
response = sock.recv(1024)
print(f"[Client] 收到: {response.decode()}")
sock.close()
# 运行
t = threading.Thread(target=server)
t.start()
client()
t.join()
三次握手的异常情况
| 异常 | 说明 | 结果 |
|---|---|---|
| SYN 丢失 | 客户端发送的 SYN 未到达服务器 | 客户端超时重传 |
| SYN+ACK 丢失 | 服务器回复未到达客户端 | 服务器超时重传 SYN+ACK |
| ACK 丢失 | 客户端回复的 ACK 未到达服务器 | 服务器超时重传 SYN+ACK |
| SYN Flood | 大量伪造 SYN 消耗服务器资源 | SYN Cookie 防护 |
4.4 四次挥手 (Four-Way Termination)
Client Server
│ │
│ ① FIN (seq=u, ack=v) │ CLOSE_WAIT
│ ─────────────────────────────────→ │
│ │
│ ② ACK (ack=u+1) │
│ ←───────────────────────────────── │
│ │
│ (服务器可能继续发送数据) │
│ │
│ ③ FIN (seq=w, ack=u+1) │ LAST_ACK
│ ←───────────────────────────────── │
│ │
│ ④ ACK (ack=w+1) │ CLOSED
│ ─────────────────────────────────→ │
│ │
│ TIME_WAIT (等待 2MSL) │
│ │
为什么需要四次挥手?
TCP 是全双工的:
- 发送和接收是独立的通道
- 关闭连接需要分别关闭两个方向
场景:
1. 客户端说"我没有数据要发了"(FIN)
2. 服务器说"收到"(ACK)
3. 服务器可能还有数据要发,继续发送
4. 服务器说"我也没有数据要发了"(FIN)
5. 客户端说"收到"(ACK)
因此需要四次交互
4.5 TIME_WAIT 状态
TIME_WAIT 的作用
TIME_WAIT 持续时间:2MSL(Maximum Segment Lifetime)
Linux 默认 MSL = 60 秒,所以 TIME_WAIT = 120 秒
两个目的:
1. 确保最后的 ACK 能到达服务器
- 如果最后的 ACK 丢失,服务器会重传 FIN
- 客户端在 TIME_WAIT 期间可以重新发送 ACK
2. 让旧连接的数据包在网络中消失
- 防止延迟的数据包被新连接接收
# 查看 TIME_WAIT 连接
import subprocess
def check_time_wait():
"""检查 TIME_WAIT 连接数量"""
result = subprocess.run(['ss', '-tan', 'state', 'time-wait'],
capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
count = len(lines) - 1 # 减去标题行
return count
print(f"TIME_WAIT 连接数: {check_time_wait()}")
TIME_WAIT 过多的问题
# 查看各种状态的连接数
$ ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
150 ESTAB
45 TIME-WAIT
12 CLOSE-WAIT
3 FIN-WAIT-2
1 LISTEN
解决 TIME_WAIT 过多
# 方法1:启用 TIME_WAIT 重用
$ sysctl -w net.ipv4.tcp_tw_reuse=1
# 方法2:减小 TIME_WAIT 超时时间
$ sysctl -w net.ipv4.tcp_fin_timeout=30
# 方法3:扩大端口范围
$ sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 方法4:使用 SO_REUSEADDR 套接字选项
# Python 中使用 SO_REUSEADDR
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', 8080))
4.6 CLOSE_WAIT 状态
CLOSE_WAIT 过多的原因
CLOSE_WAIT 是被动关闭方的状态
表示:收到 FIN,但应用没有调用 close()
常见原因:
1. 应用代码没有正确关闭连接
2. 应用阻塞在某个操作上
3. 连接泄漏(忘记关闭)
# 错误示例:忘记关闭连接
def bad_handler(conn):
data = conn.recv(1024)
process(data)
# 忘记 conn.close() → 连接保持 CLOSE_WAIT
# 正确示例
def good_handler(conn):
try:
data = conn.recv(1024)
process(data)
finally:
conn.close() # 确保关闭
# 更好的方式:使用 with 语句
def better_handler(conn):
with conn:
data = conn.recv(1024)
process(data)
4.7 半连接队列与全连接队列
┌─────────────────────────────────────┐
│ 服务器 │
│ │
SYN ──────────→ │ ┌─────────────────────┐ │
│ │ 半连接队列 │ │
SYN+ACK ←────── │ │ (SYN Queue) │ │
│ │ max_syn_backlog │ │
ACK ──────────→ │ └──────────┬──────────┘ │
│ │ 三次握手完成 │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 全连接队列 │ ← accept()│
│ │ (Accept Queue) │ │
│ │ somaxconn │ │
│ └─────────────────────┘ │
└─────────────────────────────────────┘
# 查看和调整队列大小
# 半连接队列最大值
$ sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 1024
# 全连接队列最大值
$ sysctl net.core.somaxconn
net.core.somaxconn = 128
# 增大队列
$ sysctl -w net.ipv4.tcp_max_syn_backlog=2048
$ sysctl -w net.core.somaxconn=4096
# listen() 的 backlog 参数决定全连接队列大小
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 8080))
sock.listen(128) # backlog=128
4.8 SYN Flood 攻击与防护
攻击原理
攻击者 ──── SYN ────→ 服务器
攻击者 ──── SYN ────→ 服务器
攻击者 ──── SYN ────→ 服务器
... (大量 SYN)
服务器半连接队列被填满
正常用户无法建立连接
SYN Cookie 防护
# 启用 SYN Cookie
$ sysctl -w net.ipv4.tcp_syncookies=1
SYN Cookie 原理:
1. 收到 SYN 时不分配资源
2. 将连接信息编码到 SYN+ACK 的序列号中
3. 收到 ACK 时验证编码信息
4. 验证通过才分配资源
序列号编码:
┌──────────┬──────────┬──────────┐
│ 时间戳 │ MSS │ 其他信息 │
│ (5 bits) │ (3 bits) │ │
└──────────┴──────────┴──────────┘
4.9 抓包分析
# 抓取三次握手过程
$ sudo tcpdump -i lo 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0' -nn
# 输出示例:
# 10:00:00.000001 IP 127.0.0.1.45678 > 127.0.0.1.8080: Flags [S], seq 12345
# 10:00:00.000100 IP 127.0.0.1.8080 > 127.0.0.1.45678: Flags [S.], seq 67890, ack 12346
# 10:00:00.000200 IP 127.0.0.1.45678 > 127.0.0.1.8080: Flags [.], ack 67891
# 抓取四次挥手过程
$ sudo tcpdump -i lo 'tcp[tcpflags] & (tcp-fin) != 0' -nn
4.10 注意事项
⚠️ 不要手动杀掉 TIME_WAIT:TIME_WAIT 是正常状态,使用
tcp_tw_reuse而非tcp_tw_recycle
⚠️ 监控 CLOSE_WAIT:CLOSE_WAIT 过多通常是应用 Bug
⚠️ listen backlog:不是连接数上限,而是等待 accept() 的队列长度
⚠️ RST 收到时机:对端发送 RST 表示连接不存在或被拒绝
4.11 扩展阅读
- RFC 793 - TCP Connection Management
- RFC 6528 - Defending against Sequence Number Attacks
- Linux TCP 参数调优
下一章:05 - TCP 可靠传输 - 确认机制、重传策略、滑动窗口