TCP 通讯时序、状态流转与异常排查

一、 TCP 通讯详细时序图

下图展示了TCP从建立连接(三次握手)、数据传输到断开连接(四次挥手)的完整过程。

sequenceDiagram participant Client participant Server Note over Client, Server: 1. Three-way Handshake Note right of Client: CLOSED→SYN_SENT Client->>+Server: SYN(seq=J) Note left of Server: LISTEN→SYN_RCVD Server->>-Client: SYN(seq=K), ACK(J+1) Note right of Client: SYN_SENT→ESTABLISHED Client->>+Server: ACK(K+1) Note left of Server: SYN_RCVD→ESTABLISHED Note over Client, Server: 2. Data Transfer Client->>Server: PSH, ACK (with Data) Server->>Client: ACK Server->>Client: PSH, ACK (with Data) Client->>Server: ACK Note over Client, Server: 3. Four-way Handshake Note right of Client: ESTABLISHED→FIN_WAIT_1 Client->>+Server: FIN(seq=M), ACK Note left of Server: ESTABLISHED→CLOSE_WAIT Server->>-Client: ACK(M+1) Note left of Server: CLOSE_WAIT→LAST_ACK Server->>+Client: FIN(seq=L), ACK(M+1) Note right of Client: FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT Client->>-Server: ACK(L+1) Note right of Client: TIME_WAIT→CLOSED Note left of Server: LAST_ACK→CLOSED

二、 TCP 状态流转详细表

2.1 三次握手状态流转

状态 描述 触发条件 下一个状态
CLOSED 初始状态,连接不存在。 客户端应用发起连接请求。 SYN_SENT
LISTEN 服务器端等待连接请求。 服务器应用启动并监听端口。 LISTEN
SYN_SENT 客户端已发送SYN报文,等待服务器确认。 发送SYN报文。 SYN_RCVD (收到SYN+ACK) / CLOSED (超时)
SYN_RCVD 服务器已收到SYN并发送了SYN+ACK,等待客户端确认。 收到客户端的SYN报文。 ESTABLISHED (收到ACK) / CLOSED (超时)
ESTABLISHED 连接已建立,可以进行数据传输。 完成三次握手。 FIN_WAIT_1 / CLOSE_WAIT

2.2 四次挥手状态流转

状态 描述 触发条件 下一个状态
ESTABLISHED 连接处于活动状态。 应用请求关闭连接 (主动关闭方)。 FIN_WAIT_1
FIN_WAIT_1 主动关闭方,已发送FIN,等待对方ACK。 发送FIN报文。 FIN_WAIT_2 (收到ACK) / CLOSING (收到FIN)
FIN_WAIT_2 主动关闭方,已收到对方ACK,等待对方FIN。 收到对己方FIN的ACK。 TIME_WAIT (收到FIN)
CLOSE_WAIT 被动关闭方,已收到FIN,等待应用层关闭。 收到对端的FIN报文。 LAST_ACK
CLOSING 双方同时关闭连接的特殊情况。 在FIN_WAIT_1状态下收到了FIN报文。 TIME_WAIT
LAST_ACK 被动关闭方,已发送FIN,等待最后一个ACK。 应用层关闭后,发送FIN报文。 CLOSED
TIME_WAIT 主动关闭方,等待2*MSL时间,确保连接正常关闭。 收到对方的FIN并发送了ACK。 CLOSED (等待2*MSL超时)
CLOSED 连接已完全关闭。 从LAST_ACK或TIME_WAIT转换而来。 -

三、 异常场景排查与解决

3.1 三次握手异常

场景一: SYN-Flood 攻击 (半连接攻击)

现象描述:

服务器端存在大量处于 SYN_RCVD 状态的连接。攻击者伪造源IP地址发送大量SYN包,服务器回应SYN+ACK后,由于源IP是假的,永远等不到客户端的ACK确认,导致服务器的半连接队列被占满,无法处理正常的连接请求。

排查方法 (Linux):

# 查看SYN_RCVD状态的连接数量
netstat -n -p TCP | grep SYN_RCVD | wc -l

# 或者使用ss命令,效率更高
ss -n state syn-recv | wc -l

# 查看哪些IP在大量连接
netstat -nt | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr | head

解决方法:

调整内核参数进行防御(需要修改 /etc/sysctl.conf 文件并执行 sysctl -p 使其生效)。

# 1. 启用SYN Cookies,有效防御SYN-Flood攻击
net.ipv4.tcp_syncookies = 1

# 2. 增大半连接队列 (SYN Backlog)
# 默认值可能较小,如1024。在高并发场景下可以适当增大
net.ipv4.tcp_max_syn_backlog = 2048

# 3. 减少重试次数
# 减少SYN+ACK的重传次数,尽快释放资源
net.ipv4.tcp_synack_retries = 2

场景二: 服务端accept队列满

现象描述:

客户端连接超时(Connection Timed Out),但服务端端口处于LISTEN状态。这是因为三次握手已经完成,但服务端的accept队列(全连接队列)已满,导致内核无法将已完成的连接移交给应用程序处理。内核会默认丢弃这些连接。

排查方法 (Linux):

# 查看全连接队列溢出情况
# 如果 `ListenDrops` 或 `ListenOverflows` 列的数值持续增长,说明队列满了
netstat -s | grep "listen drops"
# 或
nstat -az | grep ListenDrops

# 查看当前队列大小和最大值
ss -lnt
# Recv-Q: 当前全连接队列中的连接数
# Send-Q: 全连接队列的最大容量

解决方法:

1. 增大内核参数: 修改 /etc/sysctl.conf 并执行 sysctl -p

# 增大somaxconn参数,这是全连接队列的全局最大值
net.core.somaxconn = 65535

2. 修改应用程序: 在调用 listen() 函数时,传入更大的 backlog 参数。注意,这个值不能超过 net.core.somaxconn 的限制。

3. 优化应用性能: 检查应用程序为何处理连接如此之慢,是否CPU、I/O或锁等存在瓶颈。

3.2 四次挥手异常

场景一: 大量 TIME_WAIT 状态

现象描述:

产生原因: TIME_WAIT 状态是由**主动关闭**连接的一方进入的。在TCP四次挥手过程中,主动关闭方在发送完最后一个ACK报文后,会进入TIME_WAIT状态。它不会立即关闭连接,而是会“等待”一段时间。

状态作用 (为何需要等待): 这个等待期(2*MSL,通常为1-4分钟)至关重要,主要有两个目的:
1. 确保连接可靠关闭: 如果主动方发送的最后一个ACK报文在网络中丢失,被动关闭方会因为没有收到确认而重发FIN报文。由于主动方仍处于TIME_WAIT状态,它可以重新发送ACK,从而使对方能够正常关闭,防止其长时间停留在LAST_ACK状态。
2. 防止旧连接的“迷途报文”干扰新连接: 等待2*MSL可以确保本次连接中产生的所有报文都已在网络中消失。这样,即使稍后在同一个端口上立即建立一个新连接,也不会收到上一次连接遗留下来的、迟到的数据包,避免了数据混淆。

在高并发短连接的场景(如HTTP服务器),由于频繁地主动关闭连接,就可能积压大量 TIME_WAIT 状态的连接,占用端口和内存资源。

排查方法 (Linux):

# 查看TIME_WAIT状态的连接数量
netstat -n | grep TIME_WAIT | wc -l
# 或
ss -n state time-wait | wc -l

解决方法 (内核参数优化):

修改 /etc/sysctl.conf 并执行 sysctl -p

# 1. 开启TIME_WAIT状态连接的复用
# 允许将TIME_WAIT sockets重新用于新的TCP连接,这个参数是推荐开启的
net.ipv4.tcp_tw_reuse = 1

# 2. 开启TIME_WAIT快速回收 (慎用!)
# 在NAT环境下可能导致问题,不推荐在公网服务器上开启
# net.ipv4.tcp_tw_recycle = 1  (Linux 4.12后已移除)

# 3. 缩短TIME_WAIT超时时间 (不推荐)
# 这样做违反TCP/IP协议,可能导致数据包在网络中迷路后被新连接接收
# net.ipv4.tcp_fin_timeout = 30  (这是FIN_WAIT_2的超时,不是TIME_WAIT)

最佳实践: 通常开启 tcp_tw_reuse 是最安全且有效的方法。同时,考虑使用HTTP Keep-Alive(长连接)来减少频繁的建连和断连。

场景二: 大量 CLOSE_WAIT 状态

现象描述:

CLOSE_WAIT 状态出现在被动关闭方。当收到对方的FIN后,TCP层会回应ACK并进入CLOSE_WAIT状态,此时需要等待应用程序调用 close() 来发送自己的FIN。如果应用层代码有Bug(如忘记关闭socket),连接就会一直停留在CLOSE_WAIT,最终耗尽文件描述符。

排查方法 (Linux):

# 查看CLOSE_WAIT状态的连接数量和对应的进程
netstat -antp | grep CLOSE_WAIT
# 或
ss -antp | grep CLOSE_WAIT

解决方法:

这是应用程序的Bug,不是内核参数问题!

1. 定位问题代码: 使用 netstatss 命令找到处于 CLOSE_WAIT 状态的连接属于哪个进程 (PID)。

2. 审查代码: 检查该进程的代码逻辑,确保在所有分支(包括异常处理)中,使用完毕的socket都被正确关闭了。例如,在Java中,要确保 socket.close()finally 块中被调用。

3. 资源管理: 确保代码没有文件描述符泄漏的问题。