一、概述
1.1 背景介绍
TCP 三次握手和四次挥手,大概是网络领域被问烂了的面试题。但真正能把状态变迁、序列号变化、抓包细节讲清楚的人并不多。很多人背了八股文,一到生产环境看 Wireshark 抓包就懵了——SYN_RECV 队列溢出怎么排查?TIME_WAIT 堆积几万个怎么处理?RST 到底是谁发的?这些问题光靠背书解决不了。
这篇文章从 TCP 报文格式讲起,逐字节拆解三次握手和四次挥手的完整过程,配合 Wireshark 和 tcpdump 的实际抓包输出,把每个状态变迁都对应到真实的网络包上。同时覆盖 TCP 重传机制、拥塞控制算法对比(BBR vs Cubic),以及生产环境中最常见的 TCP 问题排查和内核参数调优。
1.2 技术特点
报文级拆解:从 TCP Header 的每个字段出发,理解握手挥手的本质而非死记硬背
抓包驱动:所有理论都配合 Wireshark/tcpdump 的真实抓包输出验证
状态机完整:覆盖 TCP 全部 11 种状态及其转换条件
生产导向:重点放在 TIME_WAIT 优化、半连接队列溢出、RST 排查等实际问题
1.3 适用场景
场景一:面试准备,需要深入理解 TCP 连接管理机制而非停留在八股文层面
场景二:生产环境 TCP 连接异常排查,需要通过抓包定位具体问题
场景三:高并发服务的内核网络参数调优,解决 TIME_WAIT 堆积、SYN Flood 等问题
场景四:微服务架构下的连接管理优化,理解连接复用和优雅关闭
1.4 环境要求
| 组件 | 版本要求 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 24.04 LTS / RHEL 9.x | 内核 6.8+,BBR v3 支持 |
| Wireshark | 4.4+ | GUI 抓包分析工具 |
| tcpdump | 4.99+ | 命令行抓包工具 |
| ss / iproute2 | 6.x+ | 替代 netstat 的连接状态查看工具 |
| curl | 8.x+ | HTTP 请求测试 |
二、TCP 报文格式
在聊握手挥手之前,先把 TCP 报文头的结构搞清楚。不理解报文格式,后面看抓包就是看天书。
2.1 TCP Header 结构
TCP 报文头最小 20 字节,最大 60 字节(含 Options)。结构如下:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |C|E|U|A|P|R|S|F| | | Offset| Rsrvd |W|C|R|C|S|S|Y|I| Window | | | |R|E|G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options (variable) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2 关键字段解析
重点关注几个和握手挥手直接相关的字段:
序列号(Sequence Number,32 位):标识从 TCP 发送端向接收端发送的数据字节流中的第一个字节的编号。握手阶段的初始序列号(ISN)是随机生成的,不是从 0 开始,这是为了防止历史连接的残留包干扰新连接。
确认号(Acknowledgment Number,32 位):期望收到对方下一个报文段的第一个数据字节的序号。只有 ACK 标志位为 1 时这个字段才有效。
标志位(Flags):这 6 个标志位是握手挥手的核心:
| 标志位 | 含义 | 握手/挥手中的作用 |
|---|---|---|
| SYN | 同步序列号 | 发起连接,携带初始序列号 |
| ACK | 确认 | 确认收到对方的数据 |
| FIN | 结束 | 请求关闭连接 |
| RST | 重置 | 强制中断连接 |
| PSH | 推送 | 要求接收方立即将数据交给应用层 |
| URG | 紧急 | 紧急指针有效 |
窗口大小(Window,16 位):接收方告诉发送方自己还能接收多少字节的数据。配合 Window Scale 选项可以扩展到 30 位。
2.3 TCP Options
握手阶段的 SYN 包通常携带以下 Options:
MSS (Maximum Segment Size) : 通告对方自己能接收的最大报文段长度,通常 1460(以太网 MTU 1500 - IP头20 - TCP头20) Window Scale : 窗口缩放因子,允许窗口大小超过 65535 SACK Permitted : 告知对方支持选择性确认 Timestamps : 用于 RTT 计算和 PAWS(防止序列号回绕)
三、三次握手详细过程
3.1 握手全景
三次握手的本质是:双方交换初始序列号(ISN),并确认对方的接收能力。
客户端 服务端 | | | [CLOSED] [LISTEN] | | | |----------- SYN, Seq=x ----------------------->| | [SYN_SENT] | | [SYN_RCVD] | |<---------- SYN+ACK, Seq=y, Ack=x+1 ----------| | | |----------- ACK, Seq=x+1, Ack=y+1 ----------->| | [ESTABLISHED] [ESTABLISHED] | | |
3.2 逐包拆解
第一次握手:客户端发送 SYN
客户端调用 connect() 系统调用,内核构造一个 SYN 报文发出去:
标志位:SYN=1
序列号:Seq=x(随机生成的 ISN)
确认号:Ack=0(此时还没有需要确认的数据)
Options:MSS=1460, WS=7, SACK_PERM, TSval=xxx
客户端状态从 CLOSED 变为 SYN_SENT。
Wireshark 中看到的样子:
No. Time Source Destination Protocol Info 1 0.000000 192.168.1.10 10.0.0.1 TCP 54321 > 443 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 WS=128 SACK_PERM TSval=1234567
注意 Wireshark 默认显示的是相对序列号(从 0 开始),实际的 ISN 是一个 32 位随机数。可以在 Edit -> Preferences -> Protocols -> TCP 中关闭 "Relative sequence numbers" 查看真实值。
第二次握手:服务端回复 SYN+ACK
服务端收到 SYN 后,从半连接队列(SYN Queue)中分配一个条目,构造 SYN+ACK 报文:
标志位:SYN=1, ACK=1
序列号:Seq=y(服务端自己的 ISN)
确认号:Ack=x+1(确认收到客户端的 SYN,期望下一个字节是 x+1)
Options:MSS=1460, WS=7, SACK_PERM, TSval=yyy
服务端状态从 LISTEN 变为 SYN_RCVD。
No. Time Source Destination Protocol Info 2 0.000543 10.0.0.1 192.168.1.10 TCP 443 > 54321 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=128 SACK_PERM
第三次握手:客户端发送 ACK
客户端收到 SYN+ACK 后,connect() 返回成功,同时发送最后一个 ACK:
标志位:ACK=1
序列号:Seq=x+1
确认号:Ack=y+1(确认收到服务端的 SYN)
客户端状态变为 ESTABLISHED。服务端收到这个 ACK 后,从半连接队列移到全连接队列(Accept Queue),状态也变为 ESTABLISHED。
No. Time Source Destination Protocol Info 3 0.000012 192.168.1.10 10.0.0.1 TCP 54321 > 443 [ACK] Seq=1 Ack=1 Win=65536 Len=0
这个 ACK 包是可以携带数据的(TCP Fast Open 就利用了这一点),但通常情况下 Len=0。
3.3 为什么是三次而不是两次
这是面试高频问题,标准答案有两个层面:
层面一:防止历史连接的初始化
假设只有两次握手,客户端发了一个 SYN 包因为网络延迟滞留在网络中,客户端超时后重新发起连接并完成通信。之后那个滞留的 SYN 包到达服务端,服务端以为是新连接请求,直接进入 ESTABLISHED 状态分配资源——但客户端根本不知道这个连接的存在。三次握手中,服务端回复 SYN+ACK 后必须等客户端的 ACK 确认,客户端收到一个不认识的 SYN+ACK 会回复 RST,避免了这个问题。
层面二:双方都需要确认对方的收发能力
建立可靠连接需要确认四件事:客户端的发送能力、客户端的接收能力、服务端的发送能力、服务端的接收能力。三次握手刚好完成这四个确认:
第一次握手(SYN) :服务端确认 -> 客户端的发送能力 OK 第二次握手(SYN+ACK) :客户端确认 -> 服务端的接收能力 OK + 服务端的发送能力 OK 第三次握手(ACK) :服务端确认 -> 客户端的接收能力 OK
两次握手少了最后一步,服务端无法确认客户端的接收能力。
3.4 半连接队列与全连接队列
这两个队列是理解 SYN Flood 攻击和连接建立失败的关键:
半连接队列(SYN Queue):存放收到 SYN 但还没收到第三次 ACK 的连接,状态为 SYN_RCVD。队列长度由 tcp_max_syn_backlog 控制。
全连接队列(Accept Queue):存放已完成三次握手但还没被 accept() 取走的连接,状态为 ESTABLISHED。队列长度由 min(backlog, somaxconn) 决定。
# 查看半连接队列溢出次数 netstat -s | grep "SYNs to LISTEN" # 查看全连接队列溢出次数 netstat -s | grep "overflowed" # 用 ss 查看当前队列状态 # Recv-Q: 当前全连接队列中的连接数 # Send-Q: 全连接队列的最大长度 ss -lnt | grep :443
队列溢出时的表现:半连接队列满了,新的 SYN 包会被直接丢弃(客户端表现为连接超时);全连接队列满了,服务端会根据 tcp_abort_on_overflow 参数决定是丢弃 ACK 还是发送 RST。
四、四次挥手详细过程
4.1 挥手全景
TCP 连接的关闭需要四次挥手,因为 TCP 是全双工的——每个方向的关闭需要独立进行。主动关闭方发 FIN 只是说"我不再发数据了",但还可以接收数据,对方可能还有数据没发完。
主动关闭方 被动关闭方 | | | [ESTABLISHED] [ESTABLISHED] | | | |----------- FIN, Seq=u ----------------------->| | [FIN_WAIT_1] | | [CLOSE_WAIT] | |<---------- ACK, Seq=v, Ack=u+1 --------------| | [FIN_WAIT_2] | | | | (被动方继续发送剩余数据...) | | | |<---------- FIN, Seq=w, Ack=u+1 --------------| | [LAST_ACK] | | [TIME_WAIT] | |----------- ACK, Seq=u+1, Ack=w+1 ----------->| | [CLOSED] | | (等待 2MSL) | | [CLOSED] |
4.2 逐包拆解
第一次挥手:主动关闭方发送 FIN
应用层调用 close() 或 shutdown(SHUT_WR),内核发送 FIN 报文:
标志位:FIN=1, ACK=1
序列号:Seq=u(当前发送序列号)
确认号:Ack=v(确认对方最后收到的数据)
主动关闭方状态从 ESTABLISHED 变为 FIN_WAIT_1。
No. Time Source Destination Protocol Info 10 5.234100 192.168.1.10 10.0.0.1 TCP 54321 > 443 [FIN, ACK] Seq=500 Ack=800 Win=65536 Len=0
第二次挥手:被动关闭方回复 ACK
被动方内核收到 FIN 后自动回复 ACK,应用层通过 read() 返回 0 感知到对方关闭:
标志位:ACK=1
确认号:Ack=u+1
被动关闭方状态变为 CLOSE_WAIT,主动关闭方收到后变为 FIN_WAIT_2。
No. Time Source Destination Protocol Info 11 5.234650 10.0.0.1 192.168.1.10 TCP 443 > 54321 [ACK] Seq=800 Ack=501 Win=65536 Len=0
第三次挥手:被动关闭方发送 FIN
被动方处理完剩余数据后调用 close(),内核发送 FIN:
标志位:FIN=1, ACK=1
序列号:Seq=w(可能在第二次和第三次挥手之间还发送了数据)
被动关闭方状态变为 LAST_ACK。
No. Time Source Destination Protocol Info 15 5.340200 10.0.0.1 192.168.1.10 TCP 443 > 54321 [FIN, ACK] Seq=900 Ack=501 Win=65536 Len=0
第四次挥手:主动关闭方回复 ACK
主动方回复最后一个 ACK,进入 TIME_WAIT 状态:
No. Time Source Destination Protocol Info 16 5.340250 192.168.1.10 10.0.0.1 TCP 54321 > 443 [ACK] Seq=501 Ack=901 Win=65536 Len=0
被动方收到 ACK 后直接进入 CLOSED。主动方需要等待 2MSL(Maximum Segment Lifetime)后才进入 CLOSED。
4.3 四次挥手能变成三次吗
可以。如果被动关闭方在收到 FIN 时恰好也没有数据要发了,内核会把 ACK 和 FIN 合并成一个包(FIN+ACK),这就变成了三次挥手。在 Wireshark 中经常能看到这种情况,Linux 内核默认开启了 TCP 延迟确认(Delayed ACK),会尝试合并 ACK 和 FIN。
No. Time Source Destination Protocol Info 10 5.234100 192.168.1.10 10.0.0.1 TCP 54321 > 443 [FIN, ACK] Seq=500 Ack=800 11 5.234650 10.0.0.1 192.168.1.10 TCP 443 > 54321 [FIN, ACK] Seq=800 Ack=501 <-- ACK和FIN合并 12 5.234700 192.168.1.10 10.0.0.1 TCP 54321 > 443 [ACK] Seq=501 Ack=801
4.4 TIME_WAIT 状态详解
TIME_WAIT 是 TCP 状态机中最容易引发生产问题的状态。主动关闭方在发送最后一个 ACK 后进入 TIME_WAIT,持续 2MSL(Linux 上硬编码为 60 秒)。
为什么需要 TIME_WAIT:
确保最后一个 ACK 到达:如果最后的 ACK 丢失,被动方会重传 FIN,主动方需要在 TIME_WAIT 状态下重新发送 ACK。如果直接进入 CLOSED,收到重传的 FIN 后会回复 RST,导致被动方异常关闭。
让旧连接的残留包在网络中消亡:TCP 用四元组(源IP、源端口、目的IP、目的端口)标识连接。如果旧连接关闭后立即用相同四元组建立新连接,网络中残留的旧包可能被新连接错误接收。等待 2MSL 确保旧包全部过期。
TIME_WAIT 堆积的危害:
# 查看 TIME_WAIT 连接数量
ss -s
# 或者
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
# 典型输出
28453 TIME-WAIT
1024 ESTABLISHED
12 LISTEN
3 FIN-WAIT-2
每个 TIME_WAIT 连接占用约 0.25KB 内存(内核用 inet_timewait_sock 结构体,比完整的 tcp_sock 小得多),28000 个也就 7MB,内存不是主要问题。真正的问题是端口耗尽——客户端的临时端口范围默认是 32768-60999,总共 28232 个,如果对同一个目标 IP:Port 的 TIME_WAIT 连接占满了这个范围,新连接就建不了了。
TIME_WAIT 优化方案:
# 方案一:开启 tcp_tw_reuse(推荐) # 允许在 TIME_WAIT 状态的端口被新的出站连接复用 # 前提是新连接的 timestamp 大于旧连接的最后 timestamp sysctl -w net.ipv4.tcp_tw_reuse=1 # 方案二:缩短 FIN_TIMEOUT(影响 FIN_WAIT_2 超时,不影响 TIME_WAIT) sysctl -w net.ipv4.tcp_fin_timeout=15 # 方案三:扩大临时端口范围 sysctl -w net.ipv4.ip_local_port_range="1024 65535" # 方案四:使用连接池复用长连接(应用层方案,最推荐) # HTTP Keep-Alive / gRPC 长连接 / 数据库连接池
注意:tcp_tw_recycle 在 Linux 4.12 之后已经被移除了,不要再用这个参数。它在 NAT 环境下会导致大量连接失败。
五、Wireshark 抓包实战
5.1 抓包准备
# 在服务器上用 tcpdump 抓包保存为 pcap 文件,然后用 Wireshark 打开分析 # -i eth0: 指定网卡 # -s 0: 抓完整包 # -w: 保存到文件 # port 443: 只抓 443 端口的流量 sudo tcpdump -i eth0 -s 0 -w /tmp/tcp-handshake.pcap port 443 # 另一个终端发起请求 curl -v https://example.com # 抓完后 Ctrl+C 停止,把 pcap 文件下载到本地用 Wireshark 打开
5.2 Wireshark 过滤器速查
Wireshark 的显示过滤器是分析抓包的核心技能:
# 基础过滤 tcp.port == 443 # 源或目的端口是 443 tcp.dstport == 80 # 目的端口是 80 ip.addr == 192.168.1.10 # 源或目的 IP # 标志位过滤(抓握手挥手的关键) tcp.flags.syn == 1 && tcp.flags.ack == 0 # 只看 SYN 包(第一次握手) tcp.flags.syn == 1 && tcp.flags.ack == 1 # 只看 SYN+ACK 包(第二次握手) tcp.flags.fin == 1 # 只看 FIN 包 tcp.flags.reset == 1 # 只看 RST 包(排查异常断连) # 连接状态过滤 tcp.analysis.retransmission # 重传包 tcp.analysis.duplicate_ack # 重复 ACK tcp.analysis.zero_window # 零窗口(接收方缓冲区满) tcp.analysis.window_update # 窗口更新 # 组合过滤 tcp.flags.syn == 1 && ip.dst == 10.0.0.1 # 发往特定服务器的 SYN tcp.stream eq 5 # 只看第 5 条 TCP 流
5.3 TCP 流追踪
在 Wireshark 中右键任意一个 TCP 包,选择 Follow -> TCP Stream,可以看到整条连接从握手到挥手的完整生命周期。这是分析单个连接问题最高效的方式。
流追踪视图会用不同颜色区分两个方向的数据,底部可以切换显示格式(ASCII/Hex/Raw)。在 Stream 编号旁边的下拉框可以快速切换不同的 TCP 流。
5.4 实战:分析一次完整的 HTTPS 连接
用 Wireshark 打开抓包文件后,一次完整的 HTTPS 连接包含以下阶段:
包序号 方向 内容 说明 ------ ---- ---- ---- 1 Client -> Server [SYN] TCP 三次握手开始 2 Server -> Client [SYN, ACK] 3 Client -> Server [ACK] TCP 连接建立完成 4 Client -> Server Client Hello TLS 握手开始 5 Server -> Client Server Hello, Cert... 6 Client -> Server Key Exchange, Finished 7 Server -> Client Finished TLS 握手完成 8 Client -> Server Application Data 加密的 HTTP 请求 9 Server -> Client Application Data 加密的 HTTP 响应 10 Client -> Server [FIN, ACK] TCP 四次挥手开始 11 Server -> Client [FIN, ACK] 合并的 FIN+ACK 12 Client -> Server [ACK] 连接关闭完成
重点关注包 1-3 的时间差:第一次握手到第二次握手的时间差就是一个 RTT(Round Trip Time),这是评估网络延迟的直接指标。如果这个值超过 100ms,说明网络延迟较高,需要考虑就近部署或 CDN 加速。
六、tcpdump 命令行抓包技巧
生产服务器通常没有 GUI,tcpdump 是唯一的抓包手段。掌握 tcpdump 的过滤语法能大幅提升排查效率。
6.1 常用抓包命令
# 抓取指定端口的 SYN 包(只看新连接建立) sudo tcpdump -i any 'tcp[tcpflags] & (tcp-syn) != 0' and port 80 -nn # 抓取 RST 包(排查连接异常断开) sudo tcpdump -i any 'tcp[tcpflags] & (tcp-rst) != 0' -nn # 抓取 SYN 但不包含 ACK 的包(纯 SYN,第一次握手) sudo tcpdump -i any 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn' -nn # 抓取特定主机之间的流量并保存 sudo tcpdump -i eth0 host 10.0.0.1 and port 443 -s 0 -w /tmp/debug.pcap -c 10000 # 只抓包头不抓数据(节省磁盘空间) sudo tcpdump -i eth0 -s 96 port 3306 -w /tmp/mysql-headers.pcap # 实时查看 TCP 标志位和序列号 sudo tcpdump -i any port 80 -nn -S -tttt # -nn: 不解析主机名和端口名 # -S: 显示绝对序列号 # -tttt: 显示完整时间戳
6.2 tcpdump 输出解读
# 典型的三次握手输出 1401.123456 IP 192.168.1.10.54321 > 10.0.0.1.443: Flags [S], seq 1234567890, win 65535, options [mss 1460,sackOK,TS val 123456 ecr 0,nop,wscale 7], length 0 1401.124012 IP 10.0.0.1.443 > 192.168.1.10.54321: Flags [S.], seq 987654321, ack 1234567891, win 65535, options [mss 1460,sackOK,TS val 654321 ecr 123456,nop,wscale 7], length 0 1401.124050 IP 192.168.1.10.54321 > 10.0.0.1.443: Flags [.], ack 987654322, win 512, length 0
Flags 字段的含义:[S] = SYN,[S.] = SYN+ACK,[.] = ACK,[F.] = FIN+ACK,[R] = RST,[P.] = PSH+ACK。
6.3 高级过滤技巧
# 抓取 TCP 窗口为 0 的包(零窗口,接收方缓冲区满) sudo tcpdump -i any 'tcp[14:2] = 0' -nn # 抓取包含特定 TCP Option 的包(比如 Window Scale) sudo tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0 and tcp[20] = 3' -nn # 抓取大于指定长度的包(排查大包问题) sudo tcpdump -i any 'tcp and greater 1400' -nn # 按时间轮转保存(长时间抓包) # -G 3600: 每小时轮转一次 # -W 24: 最多保留 24 个文件 sudo tcpdump -i eth0 port 443 -w /tmp/capture-%Y%m%d-%H%M%S.pcap -G 3600 -W 24
七、TCP 重传机制
TCP 的可靠传输依赖重传机制。理解重传对排查网络丢包、延迟抖动等问题至关重要。
7.1 超时重传(RTO Retransmission)
发送方发出数据后启动一个重传定时器(RTO,Retransmission Timeout)。如果在 RTO 时间内没有收到 ACK,就重传该数据段。
RTO 的计算基于 RTT 的采样值,Linux 使用 Jacobson 算法动态调整:
SRTT = (1 - α) * SRTT + α * RTT_sample (α = 1/8) RTTVAR = (1 - β) * RTTVAR + β * |SRTT - RTT| (β = 1/4) RTO = SRTT + 4 * RTTVAR
RTO 的最小值是 200ms(TCP_RTO_MIN),最大值是 120s(TCP_RTO_MAX)。每次超时重传后 RTO 翻倍(指数退避),依次是 200ms、400ms、800ms、1.6s...直到达到最大重传次数(tcp_retries2 默认 15 次,总计约 15 分钟)。
# 查看当前连接的 RTO 值 ss -ti dst 10.0.0.1:443 # 输出中的 rto:204 表示当前 RTO 为 204ms
7.2 快速重传(Fast Retransmit)
等 RTO 超时太慢了。快速重传机制在收到 3 个重复 ACK(Duplicate ACK)时立即重传丢失的数据段,不用等定时器超时。
发送方 接收方 |--- Seq=1, Len=1000 ---> | |--- Seq=1001, Len=1000 ---> | (丢失) |--- Seq=2001, Len=1000 ---> | |<-- ACK=1001 (Dup ACK #1) ---------| 收到 Seq=2001 但缺 1001,回复 ACK=1001 |--- Seq=3001, Len=1000 ---> | |<-- ACK=1001 (Dup ACK #2) ---------| |--- Seq=4001, Len=1000 ---> | |<-- ACK=1001 (Dup ACK #3) ---------| 第 3 个重复 ACK |--- Seq=1001, Len=1000 ---> | 快速重传! |<-- ACK=5001 ----------------------| SACK 机制下一次确认所有已收到的数据
在 Wireshark 中,快速重传的包会被标记为 [TCP Fast Retransmission],重复 ACK 会被标记为 [TCP Dup ACK]。
7.3 SACK(选择性确认)
没有 SACK 的情况下,快速重传只能重传一个包,后续丢失的包还得等超时。SACK 允许接收方告诉发送方"我收到了哪些数据段",发送方只需要重传真正丢失的部分。
# 确认 SACK 是否启用 sysctl net.ipv4.tcp_sack # net.ipv4.tcp_sack = 1 (默认开启)
在 Wireshark 中查看 SACK 信息:过滤 tcp.options.sack,可以看到 SACK 块的左右边界,精确标识了接收方已收到的数据范围。
八、TCP 拥塞控制:BBR vs Cubic
拥塞控制决定了 TCP 发送数据的速率。选错算法,带宽利用率可能差几倍。
8.1 Cubic(Linux 默认)
Cubic 是基于丢包的拥塞控制算法,Linux 从 2.6.19 开始默认使用。核心思路是:没丢包就加速,丢包了就减速。
工作阶段:
慢启动(Slow Start):窗口从 initcwnd(默认 10 个 MSS)开始,每收到一个 ACK 窗口加 1,指数增长
拥塞避免(Congestion Avoidance):窗口超过 ssthresh 后改为线性增长
丢包响应:检测到丢包后,窗口乘以一个系数(Cubic 用三次函数恢复)
Cubic 的问题:
在高带宽高延迟(长肥管道)网络中,丢包恢复太慢,带宽利用率低
把丢包等同于拥塞,但现代网络中丢包可能是随机的(无线网络)
缓冲区膨胀(Bufferbloat):在路由器缓冲区很大的情况下,Cubic 会把缓冲区填满才触发丢包,导致延迟飙升
8.2 BBR(Bottleneck Bandwidth and RTT)
BBR 是 Google 在 2016 年提出的拥塞控制算法,Linux 4.9 开始支持。BBR v3 在 Linux 6.x 内核中已经相当成熟。
核心思路: 不依赖丢包信号,而是主动探测瓶颈带宽(BtlBw)和最小 RTT(RTprop),让发送速率 = BtlBw,inflight 数据量 = BtlBw * RTprop。
BBR 的优势:
高带宽利用率:在长肥管道中表现远优于 Cubic
低延迟:不会填满路由器缓冲区
抗随机丢包:不把随机丢包当作拥塞信号
BBR 的注意事项:
BBR 在与 Cubic 共存时可能抢占带宽(公平性问题),BBR v3 对此有改善
在低延迟局域网中,BBR 和 Cubic 差异不大
需要内核 6.x+ 才能用到 BBR v3 的完整特性
# 查看当前使用的拥塞控制算法 sysctl net.ipv4.tcp_congestion_control # 查看可用的算法 sysctl net.ipv4.tcp_available_congestion_control # 切换到 BBR sysctl -w net.ipv4.tcp_congestion_control=bbr # 持久化配置 cat >> /etc/sysctl.conf << 'EOF' net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr EOF sysctl -p
8.3 如何选择
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 跨地域/跨国传输 | BBR | 高延迟网络下带宽利用率远高于 Cubic |
| CDN 边缘节点 | BBR | 面对各种网络质量的客户端,BBR 适应性更强 |
| 数据中心内部 | Cubic | 低延迟环境差异不大,Cubic 公平性更好 |
| 视频流媒体 | BBR | 低延迟 + 高吞吐的组合适合实时传输 |
| 与大量 Cubic 流共存 | Cubic 或 BBR v3 | BBR v1/v2 的公平性问题在 v3 中有改善 |
九、最佳实践
9.1 连接管理最佳实践
服务端:
# /etc/sysctl.conf 推荐配置 # 全连接队列长度,配合应用的 listen backlog 使用 net.core.somaxconn = 65535 # 半连接队列长度 net.ipv4.tcp_max_syn_backlog = 65535 # 开启 SYN Cookie 防御 SYN Flood net.ipv4.tcp_syncookies = 1 # TIME_WAIT 相关 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_max_tw_buckets = 262144
应用层:
使用 HTTP/2 或 gRPC 多路复用,减少连接数
配置合理的 Keep-Alive 超时,避免空闲连接占用资源
使用连接池管理数据库和缓存连接
优雅关闭:先 shutdown(SHUT_WR) 再 close(),给对方发送剩余数据的机会
9.2 抓包分析最佳实践
抓包时用 BPF 过滤器缩小范围,避免抓到无关流量导致文件过大
生产环境抓包设置 -c(包数量限制)或 -G -W(时间轮转),防止磁盘写满
分析时先用 capinfos 查看 pcap 文件概况,再用 Wireshark 的 Statistics -> Conversations 看连接分布
用 tshark(Wireshark 的命令行版本)做批量分析和统计
# 用 tshark 统计重传率 tshark -r capture.pcap -q -z io,stat,0,"tcp.analysis.retransmission" # 用 tshark 导出特定流的数据 tshark -r capture.pcap -Y "tcp.stream eq 5" -w stream5.pcap
十、常见 TCP 问题排查
10.1 连接超时(Connection Timeout)
现象: 客户端 connect() 长时间无响应,最终超时报错。
排查思路:
# 第一步:确认服务端端口是否在监听 ss -lnt | grep :443 # 第二步:在客户端抓包看 SYN 是否发出去了 sudo tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' and dst host 10.0.0.1 -nn # 第三步:在服务端抓包看 SYN 是否到达 sudo tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' and dst port 443 -nn # 第四步:检查防火墙规则 iptables -L -n -v | grep 443 nft list ruleset | grep 443
常见原因和解决方案:
| 原因 | 诊断方法 | 解决方案 |
|---|---|---|
| 防火墙拦截 SYN | 客户端有 SYN 发出但服务端没收到 | 检查中间防火墙/安全组规则 |
| 半连接队列溢出 | netstat -s | grep "SYNs to LISTEN" 计数增长 | 增大 tcp_max_syn_backlog |
| 全连接队列溢出 | netstat -s | grep "overflowed" 计数增长 | 增大 somaxconn + 应用 backlog |
| 服务端 CPU 打满 | SYN+ACK 延迟回复 | 排查服务端性能问题 |
| 路由不通 | traceroute 中间断开 | 检查路由表和网络设备 |
10.2 RST 包排查
RST 是 TCP 的"紧急刹车",收到 RST 意味着连接被强制中断。排查 RST 的关键是搞清楚谁发的、为什么发。
常见 RST 场景:
# 场景一:连接不存在的端口 # 服务端没有进程监听该端口,内核直接回复 RST curl http://10.0.0.1:9999 # 抓包看到:SYN -> RST,ACK # 场景二:应用异常关闭连接 # 应用设置了 SO_LINGER l_onoff=1, l_linger=0,close() 时发 RST 而非 FIN # 或者应用在接收缓冲区还有未读数据时调用 close() # 场景三:防火墙/LB 超时 # 中间设备(防火墙、负载均衡器)的连接跟踪表超时,后续包被回复 RST # 典型表现:空闲一段时间后的第一个请求收到 RST # 场景四:全连接队列溢出且 tcp_abort_on_overflow=1 sysctl net.ipv4.tcp_abort_on_overflow # 如果为 1,队列满时服务端对第三次握手的 ACK 回复 RST
RST 排查命令:
# 抓取所有 RST 包并显示详细信息
sudo tcpdump -i any 'tcp[tcpflags] & tcp-rst != 0' -nn -tttt
# 统计 RST 包的来源分布
sudo tcpdump -i any 'tcp[tcpflags] & tcp-rst != 0' -nn -c 1000 2>/dev/null |
awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -rn
# 用 ss 查看连接被 RST 的统计
ss -s
# 关注 "reset" 相关的计数
10.3 半连接队列溢出(SYN Flood)
SYN Flood 攻击通过大量伪造源 IP 的 SYN 包填满服务端的半连接队列,导致正常连接无法建立。
检测方法:
# 查看 SYN_RECV 状态的连接数 ss -ant state syn-recv | wc -l # 查看半连接队列溢出计数(持续增长说明有问题) netstat -s | grep "SYNs to LISTEN" # 查看 SYN Cookie 触发次数 netstat -s | grep "SYN cookies" # 用 nstat 看增量统计(比 netstat -s 更适合监控) nstat -az TcpExtListenDrops TcpExtListenOverflows TcpExtSyncookiesSent
防御方案:
# 开启 SYN Cookie(必须开启) sysctl -w net.ipv4.tcp_syncookies=1 # 增大半连接队列 sysctl -w net.ipv4.tcp_max_syn_backlog=65535 # 减少 SYN+ACK 重传次数(加快清理无效半连接) sysctl -w net.ipv4.tcp_synack_retries=2 # 配合 iptables 限速 iptables -A INPUT -p tcp --syn -m limit --limit 500/s --limit-burst 1000 -j ACCEPT iptables -A INPUT -p tcp --syn -j DROP
十一、内核参数调优完整方案
11.1 生产环境推荐配置
以下是经过大量生产验证的 TCP 内核参数配置,适用于高并发 Web 服务场景:
# /etc/sysctl.d/99-tcp-tuning.conf # ============ 连接队列 ============ # 全连接队列最大长度(需要应用 listen backlog 配合) net.core.somaxconn = 65535 # 半连接队列最大长度 net.ipv4.tcp_max_syn_backlog = 65535 # ============ TIME_WAIT 优化 ============ # 允许 TIME_WAIT 端口复用(仅对出站连接有效) net.ipv4.tcp_tw_reuse = 1 # FIN_WAIT_2 超时时间(秒) net.ipv4.tcp_fin_timeout = 15 # TIME_WAIT 状态的最大数量 net.ipv4.tcp_max_tw_buckets = 262144 # 临时端口范围 net.ipv4.ip_local_port_range = 1024 65535 # ============ SYN Flood 防御 ============ net.ipv4.tcp_syncookies = 1 # SYN+ACK 重传次数 net.ipv4.tcp_synack_retries = 2 # SYN 重传次数(影响 connect 超时时间) net.ipv4.tcp_syn_retries = 3 # ============ Keep-Alive ============ # 空闲多久后开始发送 Keep-Alive 探测(秒) net.ipv4.tcp_keepalive_time = 600 # 探测间隔(秒) net.ipv4.tcp_keepalive_intvl = 15 # 探测次数,超过后断开连接 net.ipv4.tcp_keepalive_probes = 5 # ============ 缓冲区 ============ # TCP 接收缓冲区(最小值、默认值、最大值,单位字节) net.ipv4.tcp_rmem = 4096 87380 16777216 # TCP 发送缓冲区 net.ipv4.tcp_wmem = 4096 65536 16777216 # 全局接收/发送缓冲区 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 # ============ 拥塞控制 ============ net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr # ============ 其他优化 ============ # 开启 TCP Fast Open(客户端和服务端都支持) net.ipv4.tcp_fastopen = 3 # 开启 SACK net.ipv4.tcp_sack = 1 # 开启窗口缩放 net.ipv4.tcp_window_scaling = 1 # 全连接队列溢出时的行为(0=丢弃ACK,1=发RST) # 生产环境建议设为 0,让客户端重试而非收到 RST net.ipv4.tcp_abort_on_overflow = 0
# 应用配置 sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf # 验证配置生效 sysctl net.core.somaxconn net.ipv4.tcp_tw_reuse net.ipv4.tcp_congestion_control
11.2 参数调优注意事项
| 参数 | 踩坑点 | 建议 |
|---|---|---|
| somaxconn | 只改内核不改应用的 listen backlog 没用 | Nginx: listen 80 backlog=65535 |
| tcp_tw_reuse | 只对出站连接(客户端角色)有效 | 服务端 TIME_WAIT 多要从应用层解决 |
| tcp_max_tw_buckets | 超过限制后直接销毁 TIME_WAIT,会打日志 | 设大一点,别让它触发 |
| tcp_keepalive_time | 应用层的 Keep-Alive 设置会覆盖内核参数 | 优先在应用层配置 |
| tcp_rmem/wmem | 最大值设太大会导致内存占用过高 | 根据实际连接数和可用内存计算 |
| tcp_fastopen | 需要客户端和服务端都支持,且中间设备不能剥离 TFO Cookie | 先在非关键服务上测试 |
11.3 Kubernetes 环境特殊处理
在 K8s 环境中,Pod 的内核参数需要通过 securityContext 设置:
apiVersion: v1 kind:Pod metadata: name:web-server spec: securityContext: sysctls: -name:net.core.somaxconn value:"65535" -name:net.ipv4.tcp_tw_reuse value:"1" -name:net.ipv4.ip_local_port_range value:"1024 65535" containers: -name:nginx image:nginx:1.27
注意:K8s 默认只允许设置 "safe" sysctls(net.ipv4.ip_local_port_range 等),"unsafe" sysctls(如 net.ipv4.tcp_syncookies)需要在 kubelet 配置中显式允许。
十二、总结
12.1 技术要点回顾
TCP 报文格式:理解 Sequence Number、Acknowledgment Number、Flags 这三个字段是看懂抓包的基础
三次握手:本质是交换 ISN + 确认双方收发能力,半连接队列和全连接队列是生产问题的高发区
四次挥手:全双工关闭需要双向独立进行,TIME_WAIT 是主动关闭方的必经状态
TIME_WAIT 优化:tcp_tw_reuse + 扩大端口范围 + 连接池是三板斧,tcp_tw_recycle 已废弃
Wireshark 过滤器:tcp.flags.syn、tcp.flags.reset、tcp.analysis.retransmission 是排查三件套
tcpdump:-nn -S -tttt 是标准参数组合,BPF 过滤器语法必须掌握
重传机制:超时重传兜底,快速重传加速,SACK 精确定位丢失数据段
拥塞控制:跨地域选 BBR,数据中心内部 Cubic 够用,BBR v3 改善了公平性
内核调优:somaxconn + tcp_max_syn_backlog + tcp_tw_reuse 是高并发服务的必调参数
12.2 面试高频问题速查
| 问题 | 关键答案 |
|---|---|
| 为什么三次握手不是两次 | 防止历史连接初始化 + 确认双方收发能力 |
| 为什么挥手是四次不是三次 | 全双工关闭,被动方可能还有数据要发(但可以合并为三次) |
| TIME_WAIT 存在的意义 | 确保最后 ACK 到达 + 让旧包在网络中消亡 |
| SYN Flood 怎么防御 | SYN Cookie + 增大半连接队列 + 限速 |
| RST 和 FIN 的区别 | FIN 是优雅关闭(四次挥手),RST 是强制中断(不等对方确认) |
| BBR 和 Cubic 的区别 | Cubic 基于丢包,BBR 基于带宽和延迟探测 |
| 全连接队列满了会怎样 | 默认丢弃 ACK(客户端重试),tcp_abort_on_overflow=1 时发 RST |
12.3 参考资料
RFC 9293 - TCP 规范(2022 年更新版,合并了多个旧 RFC)
BBR Congestion Control - Google Research
Wireshark TCP Analysis Documentation
Linux Kernel Networking - TCP Implementation
tcpdump Manual Page
附录
A. TCP 状态机完整图
+---------+ --------- active OPEN | CLOSED | ----------- +---------+<--------- create TCB | ^ snd SYN passive OPEN | | CLOSE ------------ | | ---------- create TCB | | delete TCB V | V +----------+ +----------+ +---------+ | LISTEN | | FIN_WAIT | | SYN | +----------+ | _2 | | SENT | rcv SYN | | | +----------+ +---------+ ---------- | | | CLOSE | ^ rcv SYN+ACK | snd SYN,ACK/ | | ------- | | ---------- | / | | snd FIN | | snd ACK | V | V | | | +---------+ | +-------+ | | V |SYN_RCVD | | |CLOSING| | | +---------+ +---------+ | +-------+ | | | ESTAB | | rcv ACK | rcv ACK| | | +---------+ | ------- | -------| rcv FIN | CLOSE | rcv FIN | x | x | ------- | ------- | ------- V | V snd ACK | snd FIN V snd ACK +---------+ | +---------+ | +---------+ | ESTAB | | |TIME_WAIT| | |CLOSE_ | +---------+ | +---------+ | | WAIT | | | | 2MSL timeout | +---------+ | | | ---------- | CLOSE | | | | delete TCB | ------| | | V | snd FIN | | +---------+ | V | +>| CLOSED | | +---------+ | +---------+ +-------->|LAST_ACK | | +---------+ | rcv ACK | | --------| | x V | +---------+ +--------------------------------------->| CLOSED | +---------+
B. 命令速查表
# 连接状态查看 ss -ant # 查看所有 TCP 连接状态 ss -s # TCP 连接统计摘要 ss -lnt # 查看监听端口和队列状态 ss -ant state time-wait | wc -l # 统计 TIME_WAIT 数量 ss -ti dst 10.0.0.1 # 查看到特定目标的连接详情(含 RTT/RTO) # 抓包 tcpdump -i any port 80 -nn -S -tttt # 标准抓包参数组合 tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' -nn # 只抓 SYN 包 tcpdump -i any 'tcp[tcpflags] & tcp-rst != 0' -nn # 只抓 RST 包 # 内核统计 nstat -az TcpExtListenDrops # 全连接队列溢出次数 nstat -az TcpExtListenOverflows # 全连接队列溢出次数(另一个计数器) nstat -az TcpExtTCPSynRetrans # SYN 重传次数 nstat -az TcpExtTCPTimeouts # TCP 超时次数 # 内核参数查看 sysctl -a | grep tcp # 查看所有 TCP 相关参数 sysctl net.core.somaxconn # 查看全连接队列上限 sysctl net.ipv4.tcp_congestion_control # 查看拥塞控制算法
六、总结
6.1 技术要点回顾
三次握手/四次挥手的状态机是 TCP 排障的基本功。生产环境里遇到连接异常,第一反应应该是 ss -ant 看状态分布,而不是盲目抓包。SYN_RECV 堆积指向半连接队列溢出或 SYN Flood,CLOSE_WAIT 堆积说明应用层没有正确关闭连接,FIN_WAIT_2 长期存在则要检查对端是否还活着。状态机搞清楚了,排查方向就不会跑偏。
TIME_WAIT 不是病,但大量堆积需要干预。TIME_WAIT 存在的意义是防止旧连接的延迟报文干扰新连接,2MSL 的等待时间是协议设计的一部分。真正需要处理的是短连接高并发场景下 TIME_WAIT 数量达到几万甚至十几万的情况。优先考虑 tcp_tw_reuse(安全地复用 TIME_WAIT 端口),配合连接池和长连接从根源上减少连接创建频率。tcp_tw_recycle 在 NAT 环境下会导致丢包,内核 4.12 之后已经移除,别再用了。
BBR 拥塞控制在高延迟高丢包场景下优势明显。传统的 Cubic 算法基于丢包检测来调整窗口,在跨地域专线、卫星链路等高 RTT 场景下表现保守,带宽利用率上不去。BBR 通过主动探测瓶颈带宽和最小 RTT 来驱动发送速率,不依赖丢包信号,在这类场景下吞吐量提升显著。但 BBR 也不是银弹——在浅缓冲交换机环境下可能造成较高的排队延迟,部署前需要实测验证。
Wireshark 和 tcpdump 是网络排障的瑞士军刀。tcpdump 负责在服务器上轻量抓包,Wireshark 负责离线深度分析。掌握 BPF 过滤表达式、TCP 流追踪、IO Graph、RTT 统计这几个核心功能,绝大多数 TCP 层面的问题都能定位到根因。关键是养成习惯:先用 ss/nstat 看宏观统计,缩小范围后再针对性抓包,避免在海量数据里大海捞针。
6.2 进阶学习方向
QUIC 协议(HTTP/3 的传输层)
QUIC 把 TCP 的连接管理、TLS 握手、多路复用全部搬到了用户态 UDP 之上,从根本上消除了队头阻塞问题。三次握手 + TLS 1.3 握手合并为 1-RTT 甚至 0-RTT 建连,对移动网络和弱网环境的体验提升非常大。理解了 TCP 状态机之后再看 QUIC 的连接迁移(Connection Migration)和丢包恢复机制,会更容易理解它的设计取舍。
实践建议:用 Wireshark 4.x 抓取 HTTP/3 流量,对比同一请求在 TCP+TLS 1.3 和 QUIC 下的握手耗时差异。
eBPF 网络追踪(bpftrace / tcplife / tcpconnect)
传统的 tcpdump 抓包开销不小,而且只能看到报文层面的信息。eBPF 可以直接在内核态挂载探针,零拷贝地追踪 TCP 连接的生命周期、RTT 变化、重传事件,开销比抓包低一个数量级。BCC 工具集里的 tcplife 能实时输出每条连接的存活时间和传输字节数,tcpretrans 能精确定位重传发生在哪条流上,排障效率远超传统方式。
实践建议:从 bpftrace -e 'kprobe:tcp_retransmit_skb { printf("retrans: %s:%d ", ntop(args->sk->__sk_common.skc_daddr), args->sk->__sk_common.skc_dport); }' 这类单行脚本入手,逐步深入。
内核网络栈源码阅读
读过 net/ipv4/tcp_input.c 和 tcp_output.c 之后,很多之前靠经验判断的问题都能找到确定性答案。比如 SYN Cookie 的具体实现逻辑、快速重传的触发条件、窗口缩放因子的协商过程,这些在源码里都是几十行代码的事情。配合 ftrace 或 perf 跟踪内核函数调用路径,能把抓包现象和内核行为完整对应起来。
推荐从 Linux 6.x 内核入手,代码结构比早期版本清晰很多,注释也更完善。
6.3 参考资料
RFC 793 - Transmission Control Protocol - TCP 协议的原始规范,三次握手和四次挥手的权威定义,状态机图就出自这里
RFC 8312 - CUBIC for Fast Long-Distance Networks - Linux 默认拥塞控制算法 CUBIC 的规范,理解其凹凸函数窗口增长模型
RFC 9002 - QUIC Loss Detection and Congestion Control - QUIC 的丢包检测和拥塞控制机制,BBR 在 QUIC 中的应用参考
Wireshark User's Guide - Wireshark 官方用户手册,TCP 流分析和过滤器语法的权威参考
tcpdump & libpcap - tcpdump 手册页,BPF 过滤表达式的完整语法说明
《TCP/IP 详解 卷1:协议》(W. Richard Stevens 著) - TCP 协议学习的经典教材,第 17-24 章覆盖了 TCP 连接管理、重传、拥塞控制的完整细节
Linux 内核源码 net/ipv4/tcp*.c - Bootlin 提供的在线内核源码浏览,直接看 TCP 实现比读任何二手资料都准确
全部0条评论
快来发表一下你的评论吧 !