Network · #network#tcp#connection

TCP连接管理:三次握手到TIME_WAIT优化

2025.04.05 6 min 2.2k
// 目录 · contents

前言

TCP(Transmission Control Protocol)是互联网最核心的传输层协议之一。理解TCP连接管理机制对于网络性能优化、排查连接问题至关重要。本文将从三次握手、四次挥手、TIME_WAIT优化、Keepalive机制和连接池化等方面深入分析TCP连接的完整生命周期。

TCP连接状态机

TCP连接的本质是一个有限状态机。理解各状态之间的转换关系是掌握TCP连接管理的基础。

stateDiagram-v2
    [*] --> CLOSED
    CLOSED --> LISTEN: passive open
    CLOSED --> SYN_SENT: active open / send SYN
    LISTEN --> SYN_RCVD: recv SYN / send SYN+ACK
    SYN_SENT --> ESTABLISHED: recv SYN+ACK / send ACK
    SYN_RCVD --> ESTABLISHED: recv ACK
    ESTABLISHED --> FIN_WAIT_1: close / send FIN
    ESTABLISHED --> CLOSE_WAIT: recv FIN / send ACK
    FIN_WAIT_1 --> FIN_WAIT_2: recv ACK
    FIN_WAIT_1 --> CLOSING: recv FIN / send ACK
    FIN_WAIT_2 --> TIME_WAIT: recv FIN / send ACK
    CLOSING --> TIME_WAIT: recv ACK
    CLOSE_WAIT --> LAST_ACK: close / send FIN
    LAST_ACK --> CLOSED: recv ACK
    TIME_WAIT --> CLOSED: 2MSL timeout

三次握手(Three-Way Handshake)

握手过程详解

三次握手的目的是在客户端和服务端之间建立可靠连接,同步序列号和确认号。

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over S: LISTEN

    C->>S: SYN (seq=x)
    Note over C: SYN_SENT
    Note over S: SYN_RCVD

    S->>C: SYN+ACK (seq=y, ack=x+1)

    C->>S: ACK (seq=x+1, ack=y+1)
    Note over C: ESTABLISHED
    Note over S: ESTABLISHED

    Note over C,S: Data Transfer Begins

为什么需要三次握手?

核心原因是防止历史连接的建立。假设只有两次握手:

  1. 客户端发送SYN(seq=90),由于网络延迟滞留
  2. 客户端超时重发SYN(seq=100),服务端收到并建立连接
  3. 之后滞留的SYN(seq=90)到达服务端,服务端误认为是新连接

三次握手通过第三个ACK让客户端确认服务端的SYN,避免了历史连接问题。

SYN Flood攻击与防御

SYN Flood是经典的DoS攻击方式,攻击者发送大量伪造源IP的SYN包,消耗服务端的半连接队列资源。

1
2
3
4
5
6
7
8
9
10
11
# 查看半连接队列大小
cat /proc/sys/net/ipv4/tcp_max_syn_backlog

# 开启SYN Cookie防护
echo 1 > /proc/sys/net/ipv4/tcp_syncookies

# 调整半连接队列大小
echo 4096 > /proc/sys/net/ipv4/tcp_max_syn_backlog

# 减少SYN+ACK重传次数(默认5次,约63秒)
echo 2 > /proc/sys/net/ipv4/tcp_synack_retries

SYN Cookie的原理是不使用半连接队列来保存状态,而是将连接信息编码到SYN+ACK的序列号中:

1
2
// SYN Cookie 编码(简化)
// cookie = hash(saddr, daddr, sport, dport, secret) + (time << 24) + mss_index

TCP Fast Open (TFO)

TFO允许在SYN包中携带数据,减少一个RTT:

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: 首次连接(获取Cookie)
    C->>S: SYN + TFO Cookie Request
    S->>C: SYN+ACK + TFO Cookie
    C->>S: ACK

    Note over C,S: 后续连接(携带数据)
    C->>S: SYN + TFO Cookie + Data
    Note over S: 验证Cookie,处理Data
    S->>C: SYN+ACK + Response Data
    C->>S: ACK
1
2
3
# 服务端开启TFO
echo 3 > /proc/sys/net/ipv4/tcp_fastopen
# 0: 禁用, 1: 客户端, 2: 服务端, 3: 客户端+服务端

四次挥手(Four-Way Teardown)

挥手过程详解

sequenceDiagram
    participant C as Client (Active Close)
    participant S as Server (Passive Close)

    Note over C: ESTABLISHED
    Note over S: ESTABLISHED

    C->>S: FIN (seq=u)
    Note over C: FIN_WAIT_1

    S->>C: ACK (ack=u+1)
    Note over C: FIN_WAIT_2
    Note over S: CLOSE_WAIT

    Note over S: Server may still send data...

    S->>C: FIN (seq=w)
    Note over S: LAST_ACK

    C->>S: ACK (ack=w+1)
    Note over C: TIME_WAIT (2MSL)
    Note over S: CLOSED

    Note over C: Wait 2MSL...
    Note over C: CLOSED

为什么是四次而非三次?

TCP是全双工协议,关闭需要双方各自发送FIN。服务端收到FIN后可能还有数据要发送,因此ACK和FIN分开发送。不过在没有待发数据时,内核会将ACK和FIN合并为一个包,实际变成”三次挥手”。

TIME_WAIT状态深度分析

TIME_WAIT的作用

TIME_WAIT持续2MSL(Maximum Segment Lifetime,Linux默认60秒),有两个关键作用:

  1. 确保最后的ACK被对方收到:如果ACK丢失,对方会重发FIN,TIME_WAIT状态可以正确响应
  2. 防止旧连接的延迟数据被新连接接收:确保旧连接的所有报文段在网络中消失

TIME_WAIT过多的问题

在高并发短连接场景下,大量TIME_WAIT会消耗: - 文件描述符 - 内存(每个约3.3KB) - 端口号(客户端受限于65535个端口)

1
2
3
4
5
# 查看各状态连接数
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn

# 查看TIME_WAIT数量
ss -ant state time-wait | wc -l

TIME_WAIT优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 开启TIME_WAIT重用(推荐)
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 允许将TIME_WAIT的socket用于新的连接(仅对客户端有效)
# 前提:开启tcp_timestamps

# 2. 开启时间戳(tcp_tw_reuse的前提)
echo 1 > /proc/sys/net/ipv4/tcp_timestamps

# 3. 增大本地端口范围
echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range

# 4. 调整最大TIME_WAIT数量
echo 20000 > /proc/sys/net/ipv4/tcp_max_tw_buckets

# 注意:tcp_tw_recycle 已在 Linux 4.12 中移除,不要使用!
# 在NAT环境下会导致严重问题

重要提示tcp_tw_recycle 因为在NAT场景下会丢弃合法连接,已被Linux内核移除。永远不要使用这个参数。

TCP Keepalive

TCP Keepalive用于检测空闲连接是否仍然存活:

1
2
3
4
5
6
7
8
# 连接空闲多久后开始探测(默认7200秒=2小时)
echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time

# 探测间隔(默认75秒)
echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl

# 探测次数(默认9次)
echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes

程序层面设置Keepalive:

1
2
3
4
5
// Go语言示例
conn, _ := net.DialTimeout("tcp", "example.com:80", 5*time.Second)
tcpConn := conn.(*net.TCPConn)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(60 * time.Second)
1
2
3
4
5
6
7
8
9
# Python示例
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Linux特有
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 600)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)

连接池化(Connection Pooling)

在高并发场景下,频繁建立和关闭TCP连接的开销非常大。连接池是必要的优化手段。

graph LR
    subgraph Application
        T1[Thread 1]
        T2[Thread 2]
        T3[Thread 3]
    end

    subgraph Connection Pool
        C1[Conn 1 - Active]
        C2[Conn 2 - Active]
        C3[Conn 3 - Idle]
        C4[Conn 4 - Idle]
    end

    subgraph Backend Servers
        S1[Server A]
        S2[Server B]
    end

    T1 --> C1
    T2 --> C2
    T3 -.-> C3
    C1 --> S1
    C2 --> S2
    C3 --> S1
    C4 --> S2

连接池的核心参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Go database/sql 连接池配置
db, _ := sql.Open("mysql", dsn)

// 最大打开连接数
db.SetMaxOpenConns(100)

// 最大空闲连接数
db.SetMaxIdleConns(25)

// 连接最大存活时间
db.SetConnMaxLifetime(5 * time.Minute)

// 连接最大空闲时间
db.SetConnMaxIdleTime(3 * time.Minute)

HTTP连接池(Keep-Alive)

HTTP/1.1默认开启Keep-Alive,复用底层TCP连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Go HTTP客户端连接池
transport := &http.Transport{
MaxIdleConns: 100, // 全局最大空闲连接
MaxIdleConnsPerHost: 10, // 每个Host最大空闲连接
MaxConnsPerHost: 50, // 每个Host最大连接数
IdleConnTimeout: 90 * time.Second, // 空闲超时
TLSHandshakeTimeout: 10 * time.Second,
}

client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}

关键内核参数汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# === 连接建立相关 ===
net.ipv4.tcp_syn_retries = 3 # SYN重传次数
net.ipv4.tcp_synack_retries = 2 # SYN+ACK重传次数
net.ipv4.tcp_max_syn_backlog = 4096 # 半连接队列大小
net.core.somaxconn = 4096 # 全连接队列大小
net.ipv4.tcp_syncookies = 1 # SYN Cookie
net.ipv4.tcp_fastopen = 3 # TCP Fast Open

# === 连接维持相关 ===
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5

# === 连接关闭相关 ===
net.ipv4.tcp_tw_reuse = 1 # TIME_WAIT重用
net.ipv4.tcp_timestamps = 1 # 时间戳
net.ipv4.tcp_max_tw_buckets = 20000 # 最大TIME_WAIT数
net.ipv4.tcp_fin_timeout = 30 # FIN_WAIT_2超时
net.ipv4.ip_local_port_range = 1024 65535

# === 缓冲区相关 ===
net.ipv4.tcp_rmem = 4096 87380 6291456 # 接收缓冲区
net.ipv4.tcp_wmem = 4096 65536 4194304 # 发送缓冲区
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

排查工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看连接状态分布
ss -s

# 查看特定端口的连接
ss -tnp sport = :8080

# 抓包分析握手过程
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0' -c 50

# 查看socket统计信息
cat /proc/net/sockstat

# 跟踪TCP连接事件
# 需要bcc-tools
tcpconnect # 跟踪主动连接
tcpaccept # 跟踪被动连接
tcpretrans # 跟踪重传

总结

TCP连接管理是网络编程的基础知识。核心要点:

  1. 三次握手确保双方同步序列号,防止历史连接干扰
  2. 四次挥手因为TCP全双工特性,需要双向关闭
  3. TIME_WAIT不是问题,是TCP可靠性的保证;通过tcp_tw_reuse和连接池来优化
  4. Keepalive用于检测死连接,但默认2小时太长,需要根据场景调整
  5. 连接池是高并发场景下的必备优化手段

理解这些机制后,在遇到连接超时、TIME_WAIT堆积、连接泄漏等问题时,就能快速定位和解决。

作者 · authorzt
发布 · date2025-04-05
篇幅 · length2.2k 字 · 6 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论