强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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_SENTSYN 已发送客户端发送 SYN
SYN_RCVDSYN 已接收服务器收到 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
$ 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 扩展阅读


下一章05 - TCP 可靠传输 - 确认机制、重传策略、滑动窗口