TCP三次握手与四次挥手的详细过程

描述

一、概述

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 实现比读任何二手资料都准确

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分