目录
  1. 1. 一、TCP 的设计目标
  2. 2. 二、TCP 报文头部格式
    1. 2.1. 2.1 完整头部结构
    2. 2.2. 2.2 各字段详解
    3. 2.3. 2.3 TCP 伪头部(用于校验和计算)
    4. 2.4. 2.4 重要 TCP 选项
  3. 3. 三、TCP 连接管理状态机
    1. 3.1. 3.1 十一种状态
    2. 3.2. 3.2 三次握手(Three-Way Handshake)
    3. 3.3. 3.3 四次挥手(Four-Way Handshake)
    4. 3.4. 3.4 TIME-WAIT 状态
  4. 4. 四、TCP 流量控制
    1. 4.1. 4.1 滑动窗口
    2. 4.2. 4.2 零窗口探测
    3. 4.3. 4.3 糊涂窗口综合征与 Nagle 算法
  5. 5. 五、TCP 拥塞控制
    1. 5.1. 5.1 拥塞控制的基本概念
    2. 5.2. 5.2 TCP Tahoe
    3. 5.3. 5.3 TCP Reno
    4. 5.4. 5.4 TCP NewReno
    5. 5.5. 5.5 TCP CUBIC
    6. 5.6. 5.6 BBR(Bottleneck Bandwidth and Round-trip propagation time)
  6. 6. 六、TCP 定时器
    1. 6.1. 6.1 RTO 计算(Karn 算法)
  7. 7. 七、tcpdump 实战分析
    1. 7.1. 7.1 捕获三次握手
    2. 7.2. 7.2 捕获数据传输与 RTT
    3. 7.3. 7.3 使用 ss 命令查看连接状态
  8. 8. 八、Android 中的 TCP 调优
    1. 8.1. 8.1 关键 sysctl 参数
    2. 8.2. 8.2 OkHttp 的 TCP 连接管理
  9. 9. 面试常考问题
  10. 10. 九、TCP 的字节流语义
    1. 10.1. 9.1 没有消息边界
    2. 10.2. 9.2 Nagle 算法与交互式应用
    3. 10.3. 9.3 TCP_CORK(Linux 特有)
  11. 11. 十、TCP 连接终止的边界情况
    1. 11.1. 10.1 半关闭(Half-Close)
    2. 11.2. 10.2 同时关闭(Simultaneous Close)
    3. 11.3. 10.3 RST 报文的使用场景
  12. 12. 十一、TCP 序列号攻击与防御
    1. 12.1. 11.1 TCP 序列号预测攻击
    2. 12.2. 11.2 SYN Flood 攻击与防御
  13. 13. 十二、TCP 的性能优化
    1. 13.1. 12.1 延迟确认(Delayed ACK)
    2. 13.2. 12.2 TCP 快速打开(TCP Fast Open, TFO)
    3. 13.3. 12.3 带宽延迟积(BDP)与缓冲区调优
  14. 14. 十三、TCP 在 Linux 内核中的实现概览
    1. 14.1. 13.1 核心数据结构
    2. 14.2. 13.2 接收路径(关键函数调用链)
    3. 14.3. 13.3 发送路径(关键函数调用链)
  15. 15. 十四、TCP 的优雅关闭模式总结
  16. 16. 十五、总结
  17. 17. 十六、TCP 与其他传输协议的对比
    1. 17.1. 16.1 TCP vs UDP
    2. 17.2. 16.2 TCP vs QUIC
    3. 17.3. 16.3 SCTP 简介
  18. 18. 十七、实战:从抓包到问题诊断
    1. 18.1. 17.1 数据包分析工作流
    2. 18.2. 17.2 常见网络问题诊断
    3. 18.3. 17.3 Android 设备上的网络诊断
  19. 19. 十八、TCP 协议的最新发展
    1. 19.1. 18.1 TCP Timestamps 与 PAWS
    2. 19.2. 18.2 TCP ECN(Explicit Congestion Notification)
    3. 19.3. 18.3 TCP User Timeout
    4. 19.4. 18.4 MPTCP(Multipath TCP)
  20. 20. 十九、TCP 重传统计与监控
    1. 20.1. 19.1 重传率
    2. 20.2. 19.2 在 Android 应用中监控
  21. 21. 二十、参考资料
【深入理解TCP协议】TCP协议介绍

TCP(Transmission Control Protocol,传输控制协议)是互联网最核心的协议之一。它提供了可靠、有序、面向连接的字节流传输服务。理解 TCP 的头部格式、连接管理状态机、流量控制、拥塞控制和各类定时器,是深入理解网络编程的基础。本文从 AOSP 内核和协议栈源码出发,系统梳理 TCP 协议的方方面面。

一、TCP 的设计目标

TCP 协议在 RFC 793(1981 年)中首次标准化,它的设计目标:

  1. 可靠性:数据不丢失、不重复、不乱序。通过序列号、ACK 确认、超时重传保证。
  2. 有序传输:发送端按序发送,接收端按序向应用层交付。通过序列号和重组缓冲区实现。
  3. 流量控制:防止发送方发送过快,压倒慢速的接收方。通过滑动窗口实现。
  4. 拥塞控制:防止网络中的过多数据导致拥塞崩溃。通过拥塞窗口和多种算法(CUBIC、BBR)实现。
  5. 全双工通信:同一连接中数据可同时双向传输。通过独立的序列号空间(每个方向有独立的 seq/ack)实现。
  6. 面向连接:通信前需要通过三次握手建立连接,通信后通过四次挥手断开连接。

TCP 不提供什么:

  • 不保证传输延迟(实时性)→ 使用 UDP 或 QUIC
  • 不保证最小带宽 → 由应用层协商或使用 QoS
  • 不保留消息边界(它是字节流,不是消息流)→ 应用层需自行设计消息边界(如 HTTP 的 Content-Length 或分块编码)

二、TCP 报文头部格式

2.1 完整头部结构

 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 | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window Size | 4 字节
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if data offset > 5) | 可变
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

2.2 各字段详解

字段 位宽 说明
Source Port 16 bits 源端口号。与源 IP 组成 socket。0 是保留值(永不使用)
Destination Port 16 bits 目标端口号。与目标 IP 组成 socket
Sequence Number 32 bits 本报文段第一个字节的序列号。初始序列号 ISN 随机生成(防止序列号攻击)
Acknowledgment Number 32 bits 期望收到的下一个字节的序列号。仅在 ACK 标志置位时有效
Data Offset 4 bits TCP 头部长度,以 4 字节为单位(4 bits 最大值 15,头部最长 60 字节)
Reserved 6 bits 保留位(3 bits 用于 ECN:CWR、ECE;3 bits 预留未来使用)
URG 1 bit 紧急指针有效。数据包含”紧急数据”。极少使用
ACK 1 bit 确认号有效。除 SYN 报文外几乎所有报文都置位
PSH 1 bit 推送。接收方应尽快将数据交给应用层,不要缓存
RST 1 bit 复位连接。连接出现严重错误或拒绝连接请求
SYN 1 bit 同步序列号。仅在三次握手的 SYN 和 SYN-ACK 报文中置位
FIN 1 bit 结束连接。发送方已完成数据发送,请求断开连接
Window Size 16 bits 接收窗口大小。告诉对方自己还有多少接收缓冲区。最大 65535 字节(无窗口缩放时)
Checksum 16 bits 校验和。覆盖 TCP 头部 + TCP 数据 + 伪头部(IP 源/目标地址 + 协议号 + TCP 长度)
Urgent Pointer 16 bits 紧急数据指针。指向紧急数据的末尾(仅在 URG 置位时有效)
Options 可变 TCP 选项。常见:MSS、窗口缩放、SACK、时间戳

2.3 TCP 伪头部(用于校验和计算)

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source IP Address | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination IP Address | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Zero | Protocol | TCP Length | 4 字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

TCP 校验和用伪头部做一部分输入,这提供了额外的端到端保护:如果 IP 层在路由过程中修改了源或目的 IP(如在 NAT 场景),TCP 校验和会失败,连接被丢弃。这也是为什么 NAT 设备通常也需要重新计算 TCP 校验和。

2.4 重要 TCP 选项

MSS(Maximum Segment Size):在 SYN 报文中协商,告知对方自己能接收的最大报文段大小。典型值:以太网上为 1460 字节(MTU 1500 - IP 头 20 - TCP 头 20)。

Window Scale:将 16 位窗口字段(最大 65,535 字节)扩展。窗口缩放因子是 2 的幂次,最大 2^14,使窗口可达 1GB。仅在 SYN 和 SYN-ACK 中交换。

SACK(Selective Acknowledgment):允许接收方告知发送方”哪些字节已经收到”,而非仅 ACK 连续收到的最后一个字节。在丢包场景下,SACK 使发送方只需重传丢失的段,而非整个窗口。

Timestamp(TSopt):包含 TSval(发送方时间戳)和 TSecr(回显时间戳)。用于 RTT 测量和 PAWS(Protection Against Wrapped Sequences),防止高速网络上的序列号回绕。

// Linux 内核中 TCP 选项的主要常量 (include/net/tcp.h)
#define TCPOPT_MSS 2 // Maximum Segment Size
#define TCPOPT_WINDOW 3 // Window Scaling
#define TCPOPT_SACK_PERM 4 // SACK Permitted (协商阶段)
#define TCPOPT_SACK 5 // SACK (实际使用)
#define TCPOPT_TIMESTAMP 8 // Timestamps

三、TCP 连接管理状态机

3.1 十一种状态

TCP 连接两端在生命周期中经历 11 种状态。完整的 TCP 状态机:

                          +---------+ ---------\      active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN, ACK | | snd SYN +----------+
| |<---------------- | | |
| SYN | V | SYN |
| RCVD |<------------------------------------------| SENT |
| | rcv SYN | |
+---------+ rcv ACK of SYN -------- +----------+
| x | snd ACK | |
| V | rcv ACK of SYN |
| +---------+ | -------------- |
| | ESTAB | | x |
| +---------+ | snd SYN, ACK |
| CLOSE | | rcv FIN |
| ------- | | ------- |
| snd FIN | | snd ACK |
| V V |
| +---------+ |
| | FIN | |
| | WAIT-1 | |
| +---------+ |
| rcv FIN | | rcv ACK of FIN |
| ------- | | -------------- |
| snd ACK | V x |
| | +-----------+ |
| V | CLOSING | |
| +----------+ | |
| | FIN | | |
| | WAIT-2 | | |
| +----------+ | |
| rcv FIN | | | Timeout=2MSL |
| ------- | | | -------------- |
| snd ACK | V V |
| | +---------+ delete TCB |
| V \ / |
| +------------+ |
| | TIME WAIT | |
| +------------+ |

状态速查表:

状态 含义 谁进入此状态
CLOSED 无连接 初始状态 / 最终状态
LISTEN 服务器等待连接请求 服务器
SYN-SENT 已发送 SYN,等待 SYN-ACK 客户端(主动打开)
SYN-RCVD 收到 SYN,已发送 SYN-ACK 服务器
ESTABLISHED 连接已建立,正常数据传输 双向
FIN-WAIT-1 已发送 FIN 主动关闭方
FIN-WAIT-2 已收到 FIN 的 ACK 主动关闭方
CLOSE-WAIT 已收到对端 FIN,等待本地应用关闭 被动关闭方
CLOSING 双方同时关闭 双方同时发送 FIN
LAST-ACK 已发送 FIN,等待最后的 ACK 被动关闭方
TIME-WAIT 等待 2MSL,确保最后的 ACK 被对端收到 主动关闭方

3.2 三次握手(Three-Way Handshake)

Client                               Server
| |
| SYN, seq=x, MSS=1460, ... |
| ----------------------------------> |
| |
| SYN+ACK, seq=y, ack=x+1, MSS=1460 |
| <---------------------------------- |
| |
| ACK, seq=x+1, ack=y+1 |
| ----------------------------------> |
| |
| <------ ESTABLISHED 连接建立 ------> |
| |

为什么需要三次握手,而不是两次?

核心原因:防止已失效的连接请求报文到达服务端产生错误连接。

考虑场景:客户端发送一个 SYN 报文,由于网络延迟,客户端超时重传了 SYN,建立了连接,通信完毕并关闭了连接。此时,最初那个”迟到的 SYN”到达了服务端。

  • 如果只有两次握手:服务端收到这个迟到的 SYN,直接进入 ESTABLISHED 状态,等待客户端发数据。但客户端已经关闭了,不会响应。服务端资源被浪费。
  • 三次握手解决了这个问题:服务端收到迟到的 SYN,回复 SYN-ACK,但客户端不会回复 ACK(因为它知道这不是自己发起的连接),服务端不会进入 ESTABLISHED。

3.3 四次挥手(Four-Way Handshake)

Client (主动关闭)                    Server (被动关闭)
| |
| FIN, seq=u |
| ----------------------------------> |
| | ← 进入 CLOSE-WAIT
| ACK, seq=v, ack=u+1 |
| <---------------------------------- |
| |
| (Client 进入 FIN-WAIT-2) | (应用调用 close())
| |
| FIN, seq=w, ack=u+1 |
| <---------------------------------- |
| |
| ACK, seq=u+1, ack=w+1 |
| ----------------------------------> | ← 收到 ACK 进入 CLOSED
| |
| (Client 进入 TIME-WAIT, 2MSL 超时后进入 CLOSED) |

为什么挥手需要四次?

TCP 是全双工的,每个方向可以独立关闭。主动方发送 FIN 表示”我没有数据要发了”,但被动方可能还有数据要发。因此被动方先 ACK 确认收到 FIN,等自己也没数据了再发 FIN。如果是半关闭(Half-Close,如 shutdown(SHUT_WR)),被动方仍可继续发送数据。

3.4 TIME-WAIT 状态

TIME-WAIT 持续 2MSL(Maximum Segment Lifetime,通常在 Linux 中硬编码为 60 秒,所以 2MSL = 120 秒)。

TIME-WAIT 存在的两个原因:

  1. 保证最后的 ACK 能到达对端:如果最后的 ACK 丢失,被动方会超时重传 FIN,主动方重新发送 ACK。这个过程持续 2MSL。
  2. 防止旧连接的报文”复活”到新连接:确保本连接中所有报文都在网络中消亡。2MSL 后,相同四元组(src_ip:src_port, dst_ip:dst_port)的老报文都已超时被丢弃。

TIME-WAIT 导致的问题:高并发的 HTTP 服务器(短连接)会积累大量 TIME-WAIT 状态的连接,耗尽端口资源。解决方案:SO_REUSEADDR 允许绑定 TIME-WAIT 端口,HTTP Keep-Alive 复用连接,使用长连接。

四、TCP 流量控制

4.1 滑动窗口

TCP 使用滑动窗口协议进行流量控制,确保发送方不会溢出接收方的缓冲区:

                    发送方窗口
+-------+-------+-------+-------+
| 1 | 2 | 3 | 4 | 5 | 6 | ...
+-------+-------+-------+-------+
^ ^
| |
已发送并确认 窗口右边界(由接收方 window 字段控制)

|
下一个待发送字节

接收方在每个 ACK 报文中通过 Window Size 字段通告自己的接收缓冲区剩余大小。
发送方确保"已发送但未 ACK 的数据量"不超过接收方通告的窗口。

4.2 零窗口探测

当接收方缓冲区满时,它会发送一个 window=0 的 ACK,告诉发送方暂停发送。发送方启动零窗口探测定时器(Persist Timer),周期性地发送小探测包(称为 Zero Window Probe),检查接收方的窗口是否重新打开。

如果接收方有空间了,会发送一个带非零 window 的 ACK,发送方恢复发送。

4.3 糊涂窗口综合征与 Nagle 算法

糊涂窗口综合征(Silly Window Syndrome):接收方每次只释放很小的窗口(如 1 字节),导致发送方每次发送很少数据,TCP 头部开销占比极高,网络效率极低。

接收端解决方案(Clark’s Solution):接收方只有在缓冲区有足够空间时才通告窗口(通常为 MSS 或是缓冲区的一半)。发送端解决方案(Nagle’s Algorithm):当一个 TCP 连接有已发送但未确认的数据时,小的数据(< MSS)不能立即发送,必须等待之前的 ACK 到达或数据累积到 MSS。Nagle 算法可通过 TCP_NODELAY 选项关闭(对延迟敏感的应用,如 SSH、游戏)。

五、TCP 拥塞控制

5.1 拥塞控制的基本概念

拥塞控制与流量控制的根本区别:

  • 流量控制:防止发送方压倒接收方(端到端的限制)
  • 拥塞控制:防止发送方压倒网络(全局的限制)

TCP 通过一个拥塞窗口(cwnd, Congestion Window)来限制发送速率。实际发送窗口 = min(receiver_window, cwnd)。

5.2 TCP Tahoe

TCP Tahoe(Jacobson, 1988)是最早的拥塞控制算法,引入了三个关键机制:

1. 慢启动(Slow Start)

cwnd 初始化为 1 MSS
每收到一个 ACK:cwnd += 1 MSS (指数增长)
直到 cwnd 达到 ssthresh(慢启动阈值)或发生丢包

2. 拥塞避免(Congestion Avoidance)

每 RTT(而非每 ACK):cwnd += 1 MSS  (线性增长)

3. 快速重传(Fast Retransmit)
收到 3 个重复 ACK(Dup ACK)时,不等重传定时器超时,立即重传丢失的报文。

Tahoe 的缺点:每次丢包后,cwnd 回到 1,进入慢启动。对于单个丢包,这种”一刀切”的回退过于激进。

5.3 TCP Reno

TCP Reno(1990)在 Tahoe 基础上增加了快速恢复(Fast Recovery)

收到 3 个 Duplicate ACK 时:
1. ssthresh = cwnd / 2
2. cwnd = ssthresh + 3*MSS (而非回到 1)
3. 进入"快速恢复"状态
4. 每收到一个 Duplicate ACK:cwnd += 1 MSS (膨胀窗口以允许发送新数据)
5. 收到新数据的 ACK(恢复 ACK):cwnd = ssthresh,退出快速恢复

Reno 在单次丢包时非常高效(避免了慢启动的重新攀升),但在一个窗口内丢多个包时表现不佳。Reno 需要 Duplicate ACK 来触发快速重传,如果多个丢包在同一个窗口内,后续的丢包只能通过超时重传(回到 cwnd=1)。

5.4 TCP NewReno

NewReno(RFC 6582, 2012)改进了 Reno 处理同一窗口中多个丢包的能力:

在快速恢复期间,维持一个”恢复点”概念:只有当所有在进入快速恢复之前发送的报文都被确认后,才退出快速恢复。部分 ACK(确认了部分而非全部 outstanding 数据)不退出快速恢复,而是继续重传下一个未确认的报文。

// Linux 内核中 NewReno 的核心逻辑 (简化, net/ipv4/tcp_input.c)
static void tcp_newreno_enter_recovery(struct sock *sk) {
struct tcp_sock *tp = tcp_sk(sk);
tp->prior_ssthresh = 0;
tp->bytes_acked = 0;
// 设置恢复点为当前最高发送序列号
tp->high_seq = tp->snd_nxt;
}

5.5 TCP CUBIC

CUBIC(Rhee et al., 2008)是 Linux 内核自 2.6.19 起的默认拥塞控制算法,也是 Android 设备的默认算法。

CUBIC 的核心思想:用三次函数(cubic function)替代 Reno 的 AIMD(加性增加/乘性减少)窗口增长规则。

W(t) = C * (t - K)^3 + W_max

其中:
W_max:上次丢包时的窗口大小
C:缩放因子(默认为 0.4)
β:乘法减少因子(默认为 0.2,丢包后 W_reduce = W_max * β)
K = ³√(W_max * β / C) 是三次函数的拐点

CUBIC 的三个阶段:

  1. 凹区域(t < K):快速接近 W_max,但增长率逐渐减慢
  2. 凸区域(t ≈ K):在 W_max 附近窗口增长非常缓慢(稳定性)
  3. 凸区域(t > K):窗口加速增长,探测更多可用带宽
// Linux 内核中 CUBIC 的核心窗口计算 (net/ipv4/tcp_cubic.c)
static inline u32 bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked) {
u64 offs;
u32 delta, t, bic_target, max_cnt;

ca->ack_cnt += acked;
if (ca->ack_cnt < cwnd)
return 0;
ca->ack_cnt = 0;

// 自上次丢包以来的时间
t = jiffies_to_msecs(jiffies - ca->epoch_start) / HZ;
// 计算三次函数 W(t)
offs = (t - ca->K) * (t - ca->K) * (t - ca->K);
// ... 计算目标窗口 bic_target
ca->cnt = max_cnt; // 控制每 ACK 的 cwnd 增量
}

CUBIC 的优势

  • 不依赖 RTT(对高带宽延迟积(BDP)网络友好)
  • 在高速长距离网络(如跨洲际光纤)上比 Reno 好数十倍
  • 稳定性好(在 W_max 附近停留时间长)

5.6 BBR(Bottleneck Bandwidth and Round-trip propagation time)

BBR(Cardwell et al., 2016, Google)是从根本上重新思考拥塞控制的算法,不依赖丢包信号:

BBR 的核心洞察:当瓶颈链路的缓冲区被填满(bufferbloat)时才会丢包,所以基于丢包的拥塞控制(如 Reno、CUBIC)总是在”缓冲区已满 → 丢包 → 减速”的循环中运行,无法充分利用带宽。

BBR 的模型

吞吐量 = min(bottleneck_bandwidth, cwnd / RTT)
最佳工作点:cwnd = BDP = bottleneck_bandwidth × RTT_prop (最小 RTT)

BBR 持续估计两个参数:

  • **Bottleneck Bandwidth (BtlBw)**:过去 10 个 RTT 内的最大送达速率
  • **Minimum RTT (RTprop)**:过去 10 秒内的最小 RTT

BBR 的状态机

  1. STARTUP:指数增加发送速率(类似慢启动),直到吞吐量不再增长(即填满了瓶颈缓冲)
  2. DRAIN:降低发送速率,排空缓冲区中多余的报文
  3. PROBE_BW:在主状态下持续探测更多带宽(周期性小幅增加/减少速率)
  4. PROBE_RTT:定期将 cwnd 降低到最小值(4 MSS),探测最小 RTT

BBR 在 YouTube 上部署后,吞吐量提升 4%(中等)、14%(发展中地区)。Google 内部已从 CUBIC 切换到 BBR。

六、TCP 定时器

定时器 作用 触发后果
重传定时器 如果发送的报文在 RTO 内未收到 ACK,触发重传 重传报文,更新 RTO(指数退避)
持久定时器 接收方窗口为 0 时,周期性探测窗口是否重新打开 窗口非零则恢复发送
保活定时器 连接长时间无数据交换时,检查对端是否仍存活 发送 keepalive 探测包(默认 2 小时后)
TIME-WAIT 定时器 TIME-WAIT 状态的 2MSL 计时 超时后连接进入 CLOSED

6.1 RTO 计算(Karn 算法)

RTO(Retransmission Timeout)的计算基于 RTT 的测量:

SRTT = α * SRTT + (1-α) * RTT_sample          (SRTT: 平滑 RTT 估计)
RTTVAR = β * RTTVAR + (1-β) * |SRTT - RTT_sample| (RTT 变化量估计)
RTO = SRTT + max(G, K * RTTVAR) (K = 4,G = 时钟粒度)

默认值:α = 1/8(RFC 6298),β = 1/4。

Karn 算法:如果一个报文被重传了,收到 ACK 时无法区分这个 ACK 是对原始报文的还是对重传报文的。因此,不对重传报文的 RTT 进行采样(直接丢弃该 RTT 样本),只对未重传的报文进行 RTT 估计。同时,对重传报文的 RTO 做退避处理(double RTO)。

七、tcpdump 实战分析

7.1 捕获三次握手

# 抓取本地 8080 端口的 TCP 握手
sudo tcpdump -i lo -nn 'tcp and port 8080' -v

# 输出分析:
# 1. 10:00:00.000000 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [S], seq 123456789, win 65495, options [mss 65495,sackOK,TS val 123 ecr 0,nop,wscale 7], length 0
# 2. 10:00:00.000010 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [S.], seq 987654321, ack 123456790, win 65495, options [mss 65495,sackOK,TS val 456 ecr 123,nop,wscale 7], length 0
# 3. 10:00:00.000020 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 987654322, win 512, options [nop,nop,TS val 124 ecr 456], length 0
#
# 解读:
# 1: [S] = SYN, seq=123456789, MSS=65495, window scale=7 (×128), timestamp enabled
# 2: [S.] = SYN+ACK, seq=987654321, ack=123456790 (客户端 seq+1)
# 3: [.] = ACK, ack=987654322 (服务器 seq+1),连接建立

7.2 捕获数据传输与 RTT

# 带相对时间戳和序列号,观察 RTT
sudo tcpdump -i any -nn 'tcp and host 93.184.216.34' -ttt -S

# 观察:
# seq=N:M 表示发送了从序列号 N 到 M 的字节
# ack=M 表示期望收到序列号 M 及以后的字节
# win=W 表示当前接收窗口为 W 字节

7.3 使用 ss 命令查看连接状态

# 查看所有 TCP 连接状态统计
ss -tan state time-wait | wc -l # TIME-WAIT 状态的连接数
ss -tan state established # ESTABLISHED 状态的连接
ss -tan state syn-recv # SYN-RECV(可能是 SYN flood)
ss -s # 汇总统计

# 查看 socket 级别的 TCP 参数
ss -tinfo state established # 包括 cwnd, rtt, mss 等

八、Android 中的 TCP 调优

8.1 关键 sysctl 参数

# Linux/Android 内核 TCP 参数 (可通过 sysctl 或 /proc/sys/net/ipv4/ 查看)

# 拥塞控制算法(Android 默认 cubic)
net.ipv4.tcp_congestion_control = cubic

# 启用 TCP 窗口缩放
net.ipv4.tcp_window_scaling = 1

# 启用 TCP 时间戳
net.ipv4.tcp_timestamps = 1

# 启用 SACK
net.ipv4.tcp_sack = 1

# TCP 快速打开(TFO, TCP Fast Open, RFC 7413)
net.ipv4.tcp_fastopen = 3 # 0=关闭, 1=客户端, 2=服务端, 3=两者

# FIN-WAIT-2 超时时间(秒)
net.ipv4.tcp_fin_timeout = 60

# TIME-WAIT 状态可被重用的最大 socket 数
net.ipv4.tcp_tw_reuse = 2 # 0=关闭, 1=全局, 2=按条件

# keepalive 相关
net.ipv4.tcp_keepalive_time = 7200 # 空闲 2 小时后开始 probe
net.ipv4.tcp_keepalive_intvl = 75 # probe 间隔 75 秒
net.ipv4.tcp_keepalive_probes = 9 # 重试 9 次

8.2 OkHttp 的 TCP 连接管理

// OkHttp 使用连接池管理 TCP 连接
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // TCP 连接超时
.readTimeout(30, TimeUnit.SECONDS) // 读超时
.writeTimeout(30, TimeUnit.SECONDS) // 写超时
.connectionPool(
ConnectionPool(
maxIdleConnections = 5, // 最大空闲连接数
keepAliveDuration = 5, TimeUnit.MINUTES // 空闲连接存活时间
)
)
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build()

面试常考问题

Q1:TCP 三次握手为什么不是两次或四次?

两次握手的致命问题:已失效的连接请求报文(在网络中延迟到达)可能导致服务端单方面建立连接,浪费资源。客户端明确知道自己不需要这个连接(未发起),但服务端不知道。

四次握手不必要:三次已经足够完成双向的初始化序列号交换和确认。SYN-ACK 合并了”对 SYN 的 ACK”和”服务端的 SYN”,减少了一次往返。

Q2:TIME-WAIT 状态为什么是 2MSL?

MSL(Maximum Segment Lifetime)是报文在网络中最长的存活时间(通常 60 秒)。2MSL 确保了:

  1. 最后一个 ACK 有足够时间到达对端,且如果 ACK 丢失,对端重传的 FIN 也能在 2MSL 内到达
  2. 本连接中所有”迷路”的报文在 2MSL 时间内被网络丢弃,确保不会”复活”到同四元组的新连接中

Q3:TCP 拥塞控制与流量控制的本质区别?

  • 流量控制是端到端的:接收方告诉发送方”我还有多少缓冲区”,防止发送方压倒慢的接收方
  • 拥塞控制是全局的:发送方通过丢包或 RTT 变化推断网络拥塞程度,动态调整发送速率,防止发送方(们)压倒瓶颈链路的缓冲区
  • 实际发送窗口 = min(receiver_window, cwnd)。两者同时约束发送方。

Q4:为什么需要快速重传?不能等着超时重传吗?

超时重传的 RTO 通常为几百毫秒到几秒。在这段时间内,TCP 连接处于”静默”状态 —— 没有新的 ACK、没有新的数据可以发送。快速重传(3 Duplicate ACKs 后立即重传)将恢复时间从 RTO 缩短到若干 RTT,大幅提升吞吐量。

Q5:CUBIC vs BBR 什么时候选择?

  • CUBIC:可靠的默认选择。在绝大多数场景表现良好。不需要任何配置。
  • BBR:适合高 BDP 网络(长距离高带宽)。在有 bufferbloat 的链路上显著优于 CUBIC。需要内核版本 4.9+。可能在某些有线-无线混合网络中导致自竞争(self-contention)。
  • Android 默认使用 CUBIC。Google 服务器到客户端的链路已经切换到 BBR。

Q6:为什么要使用序列号而不是依赖 IP 层的顺序?

IP 层是无连接、尽力而为的——它不保证顺序。数据报可能乱序、重复、丢失。TCP 必须自己通过序列号来:

  1. 检测丢包(序列号不连续)
  2. 重组乱序报文(按序列号排序)
  3. 检测重复报文(相同序列号重复到达)
  4. 对每个字节进行确认(ACK 携带期望的下一个字节序列号)

九、TCP 的字节流语义

9.1 没有消息边界

TCP 是一个字节流协议,不保留应用层的消息边界:

发送端 write():  "Hello" (5 bytes)
write(): "World" (5 bytes)

接收端 read(): 可能读到 "Hel" (3 bytes)
read(): 可能读到 "loWorld" (7 bytes)

read(): 可能读到 "HelloWorld" (10 bytes,一次性)

应用层协议必须自己处理消息边界。常见策略:

  1. 固定长度消息:每个消息固定 N 字节
  2. 长度前缀:消息头包含消息体长度(如 HTTP/1.1 的 Content-Length)
  3. 分隔符:用特殊字符分隔消息(如 HTTP/1.1 的 \r\n\r\n,SMTP 的 \r\n.\r\n
  4. 分块编码:每个 chunk 以长度开头(如 HTTP/1.1 的 Chunked Transfer Encoding)

9.2 Nagle 算法与交互式应用

Nagle 算法的核心规则:当一个 TCP 连接有已发送但未确认的数据时,小于 MSS 的报文不能发送,必须等 ACK 到来或积累到 MSS。

// Nagle 算法的伪代码
if (有未确认数据 && 新数据 < MSS) {
缓冲新数据,暂不发送
} else {
立即发送
}

对于交互式应用(SSH、telnet、游戏),每个按键都会触发一个小数据包。如果开启 Nagle,这些按键会被”合并”后才发送,导致明显的延迟。解决方案:

int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&flag, sizeof(flag));

或者在应用层合并小写入(writev、coalescing writes)。

9.3 TCP_CORK(Linux 特有)

Linux 提供了比 TCP_NODELAY 更强的选项 TCP_CORK:在清除 CORK 标志之前,所有小的写入都被”堵住”(corked),只在以下时机发送:

  • 手动清除 CORK 标志
  • CORK 开启超过 200ms
  • 缓冲区中的数据达到 MSS

这比 Nagle 更激进,适合 HTTP 响应头+响应体需要一起发送的场景:

// 伪代码
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &flag, sizeof(flag));
write(fd, http_header, header_len); // 不立即发送
write(fd, http_body, body_len); // 不立即发送
flag = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &flag, sizeof(flag)); // 现在发送

十、TCP 连接终止的边界情况

10.1 半关闭(Half-Close)

Client                         Server
| |
| shutdown(SHUT_WR) |
| FIN, seq=u |
| ---------------------------> |
| |
| ACK, seq=v, ack=u+1 |
| <--------------------------- |
| |
| (Client 不能发数据了) | (Server 仍可以发送数据)
| <=== data ==== | (Client 仍可以接收)
| |
| ... (Server 发完所有数据) |
| |
| shutdown(SHUT_WR) |
| FIN, seq=w |
| <--------------------------- |
| |
| ACK |
| ---------------------------> |

半关闭在 HTTP/1.0 中常用:客户端发送请求后关闭写端(FIN),表示请求数据已完成,但保持读端打开以接收响应。

10.2 同时关闭(Simultaneous Close)

当双方同时发送 FIN 时,状态转换为 CLOSING,而非 FIN-WAIT-2:

Client                           Server
FIN-WAIT-1 --> FIN --> FIN-WAIT-1
<-- FIN <--
CLOSING CLOSING
(收到 FIN, 发送 ACK)
<-- ACK -->
TIME-WAIT CLOSED (收到 ACK)
(2MSL 超时后 CLOSED)

10.3 RST 报文的使用场景

RST(Reset)是 TCP 的”异常终止”信号。典型场景:

  1. 连接请求被拒绝:客户端尝试连接一个服务端没有监听的端口 → 服务端回复 RST
  2. 半开连接(Half-Open Connection):一方崩溃后重启,收到旧连接的报文 → 回复 RST
  3. **SO_LINGER=0 的 close()**:不经过四次挥手,直接发送 RST 断开连接
  4. 接收到的报文不属于任何连接:防火墙可能注入 RST 来切断不允许的连接
// SO_LINGER 示例:立即关闭,不优雅挥手
struct linger l = { .l_onoff = 1, .l_linger = 0 };
setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));
close(fd); // 发送 RST 而非 FIN

十一、TCP 序列号攻击与防御

11.1 TCP 序列号预测攻击

在 TCP 的原始设计中(RFC 793),ISN(Initial Sequence Number)是通过一个全局时钟每 4 微秒递增一次。攻击者可以预测 ISN,注入伪造的 TCP 报文。

Kevin Mitnick 在 1994 年的著名攻击就是利用了这种可预测的 ISN:他伪造了来自信任主机的 TCP 连接(利用 IP 源地址欺骗 + ISN 预测),成功入侵了 Tsutomu Shimomura 的机器。

现代防御:ISN 使用加密安全的随机数生成器(CSPRNG),每个连接独立随机。Linux 内核通过 secure_tcp_seq() 函数生成 ISN:

// Linux 内核: net/core/secure_seq.c
u32 secure_tcp_seq(__be32 saddr, __be32 daddr,
__be16 sport, __be16 dport) {
u32 hash;
net_secret_init(); // 初始化随机密钥
hash = siphash_3u32((__force u32)saddr, (__force u32)daddr,
(__force u32)sport << 16 | (__force u32)dport,
&net_secret);
return seq_scale(hash);
}

11.2 SYN Flood 攻击与防御

SYN Flood 是最经典的 DDoS 攻击:攻击者发送大量 SYN 报文(通常伪造源 IP),服务端为每个 SYN 分配 TCB(传输控制块),回复 SYN-ACK 并等待 ACK。由于源 IP 是伪造的,ACK 永远不会到达,服务端的半连接队列(SYN queue)被填满,无法为正常用户建立连接。

防御手段:

  1. SYN Cookies:服务端不在 SYN-RCVD 阶段分配 TCB,而是将连接信息(MSS、序列号、时间戳)通过加密哈希编码到 SYN-ACK 报文的 ISN 中。收到合法的 ACK 时,从 ACK 的序列号中解码回连接信息。Linux 默认启用。
// Linux 内核 SYN cookie 的实现 (简化, net/ipv4/tcp_ipv4.c)
static __u32 cookie_v4_init_sequence(const struct sock *sk,
const struct sk_buff *skb, __u16 *mssp) {
// 哈希输入: 源/目标 IP、端口、时间戳、密钥
// 输出: 可验证的序列号(内嵌 MSS 信息)
return (cookie_hash(saddr, daddr, sport, dport, 0, 0)
+ data // data 包含 MSS 索引和时间戳
) & 0xFFFFFFFF;
}
  1. tcp_syncookies:Linux 的 sysctl 参数。当半连接队列满时,自动启用 SYN Cookies。

  2. 增加 SYN 队列长度net.core.somaxconntcp_max_syn_backlog

十二、TCP 的性能优化

12.1 延迟确认(Delayed ACK)

为了减少网络中纯 ACK 报文的数量,TCP 使用延迟确认:收到数据后不立即发送 ACK,而是等待最多 500ms(Linux 中为 40ms-200ms),希望在等待期间:

  • 自己有数据要发给对端(可以顺便捎带 ACK,即 piggyback)
  • 收到更多的数据(可以累积确认)
// Linux: net/ipv4/tcp_input.c
// tcp_send_delayed_ack() 实现延迟确认
// 默认上限为 HZ/2 (通常 200ms),最小为 HZ/25 (通常 40ms)

副作用:延迟确认增加了 RTT。在请求-响应模式(HTTP)中,延迟确认 + Nagle 算法的交互可能导致200ms+ 的额外延迟

12.2 TCP 快速打开(TCP Fast Open, TFO)

TCP 快速打开(RFC 7413)允许在三次握手期间就开始传输数据,消除连接建立的一个 RTT 延迟:

传统 TCP:        TFO:
Client SYN → Client SYN + TFO Cookie + Data →
Server SYN+ACK ← Server SYN+ACK + Data ←
Client ACK + Data →
RTT = 1.5 RTT = 1 (节省 1 个 RTT)
// 在 Android 客户端启用 TFO
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, &flag, sizeof(flag));

TFO 在 Android 9+ 默认启用,需要服务端支持。

12.3 带宽延迟积(BDP)与缓冲区调优

BDP = bandwidth × RTT。TCP 的发送/接收缓冲区至少应等于 BDP 才能充分利用网络:

BDP 示例:
100 Mbps × 100ms RTT = 10 Mb = 1.25 MB
→ 建议缓冲区 >= 1.25 MB

1 Gbps × 50ms RTT = 50 Mb = 6.25 MB
→ 建议缓冲区 >= 6.25 MB

Linux 内核从 2.6 开始通过 tcp_rmemtcp_wmem 支持自动调优缓冲区(auto-tuning)。Android 继承了这个特性。

# 查看当前接收/发送缓冲区设置
cat /proc/sys/net/ipv4/tcp_rmem
# 输出: 4096(min) 87380(default) 6291456(max) (单位: bytes)

cat /proc/sys/net/ipv4/tcp_wmem
# 输出: 4096 16384 4194304

十三、TCP 在 Linux 内核中的实现概览

13.1 核心数据结构

// 传输控制块 (include/net/tcp.h)
struct tcp_sock {
// 序列号空间
u32 snd_una; // 首个未确认的序列号
u32 snd_nxt; // 下一个发送序列号
u32 snd_wnd; // 发送窗口(接收方通告的)
u32 rcv_nxt; // 下一个期望接收的序列号
u32 rcv_wnd; // 接收窗口

// 拥塞控制
u32 snd_cwnd; // 拥塞窗口
u32 snd_ssthresh; // 慢启动阈值
u32 prior_cwnd; // 快速恢复前的 cwnd

// RTT 估计
u32 srtt_us; // 平滑 RTT (微秒)
u32 mdev_us; // RTT 平均偏差 (微秒)
u32 rttvar_us; // RTT 方差
u32 rto; // 重传超时 (jiffies)

// 定时器
struct timer_list icsk_retransmit_timer;
struct timer_list icsk_delack_timer;
struct timer_list sk_timer; // keepalive / TIME-WAIT
unsigned long icsk_timeout;

// 选项
u16 advmss; // 通告的 MSS
u8 rcv_wscale; // 接收窗口缩放因子
u8 snd_wscale; // 发送窗口缩放因子

// 统计
u64 bytes_acked; // 已确认的字节数
u32 total_retrans; // 总重传次数
u32 packets_out; // 已发送未确认的报文数
u32 retrans_out; // 重传且未确认的报文数
};

13.2 接收路径(关键函数调用链)

网卡中断
→ napi_schedule()
→ netif_receive_skb()
→ ip_rcv() // IP 层处理
→ ip_local_deliver() // 本地投递
→ tcp_v4_rcv() // TCP 层入口
→ tcp_v4_do_rcv() // 主处理函数
├── ESTABLISHED 状态: tcp_rcv_established() // 快速路径
└── 其他状态: tcp_rcv_state_process() // 慢速路径

13.3 发送路径(关键函数调用链)

用户态 write() / send()
→ sys_write() / sys_sendto()
→ sock_sendmsg()
→ tcp_sendmsg() // TCP 写主函数
→ tcp_push() // 推送数据
→ tcp_write_xmit() // 传输决策(拥塞控制、Nagle 算法)
→ tcp_transmit_skb() // 构造 TCP 头部
→ ip_queue_xmit() // 交由 IP 层发送

十四、TCP 的优雅关闭模式总结

关闭方式 系统调用 TCP 行为 数据安全 TIME-WAIT
默认 close() close(fd) 发送 FIN,四次挥手 内核缓冲区数据会被发送 主动关闭方
SO_LINGER=0 setsockopt(SO_LINGER, {1,0}) + close() 发送 RST 缓冲区数据丢失
shutdown(SHUT_WR) shutdown(fd, SHUT_WR) 发送 FIN,但仍可读 数据安全 主动方
shutdown(SHUT_RDWR) shutdown(fd, SHUT_RDWR) 双向关闭(同 close 类似) 数据安全 主动方

最佳实践:在 HTTP 客户端,建议使用 shutdown(SHUT_WR) 而非 close() 来结束请求,以便在发送完请求后仍能接收服务器的响应。


十五、总结

TCP 是一个经历了 40+ 年实战打磨的可靠传输协议。理解 TCP 不仅仅是背诵三次握手、四次挥手——它背后是一整套精心设计的机制:序列号空间的可靠性保障、滑动窗口的流量控制、从 Tahoe 到 BBR 的拥塞控制演进、各类定时器的协同工作、以及面对攻击和异常场景的防御策略。

十六、TCP 与其他传输协议的对比

16.1 TCP vs UDP

特性 TCP UDP
连接 面向连接(三次握手) 无连接
可靠性 可靠(ACK + 重传) 不可靠(无保证)
顺序 有序交付 无顺序保证
流量控制 滑动窗口
拥塞控制 多种算法(CUBIC, BBR) 无(由应用层实现)
头部开销 20-60 字节 8 字节
适用场景 HTTP, FTP, SSH, SMTP DNS, VoIP, 视频流, 游戏
多播/广播 不支持 支持

16.2 TCP vs QUIC

QUIC(Quick UDP Internet Connections)是 Google 设计的下一代传输协议,已被 IETF 标准化为 HTTP/3 的基础:

特性 TCP QUIC
基础协议 IP 协议号 6 基于 UDP
握手延迟 1.5 RTT (TCP) + 1 RTT (TLS) = 2.5 RTT 0-RTT (重连) / 1-RTT (首次)
队头阻塞(HoL) TCP 流级别(丢包阻塞整个流) 无(每个流独立)
连接迁移 不支持(IP/端口变化 = 断开) 支持(Connection ID 迁移)
部署 内核态(难以升级) 用户态(快速迭代)
加密 可选(通常用 TLS) 强制内置(TLS 1.3)
Android 支持 原生支持 Android 11+ (Cronet)

16.3 SCTP 简介

SCTP(Stream Control Transmission Protocol)结合了 TCP 和 UDP 的优点:

  • 多流(multi-streaming):一个连接中多条逻辑流,无队头阻塞
  • 多宿主(multi-homing):一个端点可绑定多个 IP 地址(故障切换)
  • 面向消息(保留消息边界),不像 TCP 是字节流
  • 四次握手(防御 SYN Flood)

Android 内核支持 SCTP,但应用层 API 受限(需要 libsctp 或使用系统调用)。

十七、实战:从抓包到问题诊断

17.1 数据包分析工作流

# Step 1: 使用 tcpdump 抓取原始流量
sudo tcpdump -i eth0 -w capture.pcap 'tcp and port 443'

# Step 2: 使用 Wireshark 打开 capture.pcap,分析:
# - Statistics → Flow Graph:可视化 TCP 连接的数据流
# - Statistics → IO Graph:查看吞吐量随时间的变化
# - Analyze → Expert Info:自动检测 TCP 异常(重传、重复 ACK、窗口为零等)

# Step 3: 常见问题的 Wireshark 过滤表达式
tcp.analysis.retransmission # 重传的报文
tcp.analysis.duplicate_ack # 重复 ACK
tcp.analysis.zero_window # 接收方通告零窗口
tcp.analysis.lost_segment # 疑似丢失的报文
tcp.analysis.fast_retransmission # 快速重传
tcp.flags.reset == 1 # RST 报文
tcp.window_size == 0 # 零窗口通告

17.2 常见网络问题诊断

症状 可能原因 诊断命令
连接建立慢(>100ms) 网络延迟大,或服务端半连接队列满 ping, traceroute, ss -s
吞吐量低 丢包导致 cwnd 减小,或接收窗口小 iperf3, tcpdump + Wireshark IO Graph
周期性卡顿 Bufferbloat(缓冲区膨胀导致高延迟) ping -f (flood ping), 检查 RTT 方差
连接意外断开 中间设备(NAT/防火墙)超时注入 RST tcpdump 'tcp[tcpflags] & tcp-rst != 0'
端口耗尽 大量 TIME-WAIT 连接 ss -tan state time-wait | wc -l

17.3 Android 设备上的网络诊断

# adb shell 中的网络诊断命令

# 查看 TCP 连接
cat /proc/net/tcp # IPv4 TCP 连接表
cat /proc/net/tcp6 # IPv6 TCP 连接表

# 内核 TCP 统计
cat /proc/net/snmp # SNMP 统计(含 TCP 分段统计)
cat /proc/net/netstat # 扩展网络统计

# 查看路由表和接口
ip route show
ip addr show

# DNS 解析测试
nslookup example.com
ping -c 4 8.8.8.8 # 测试网络可达性
traceroute 8.8.8.8 # 查看路由路径

# 在 root 设备上使用 tcpdump
/data/local/tmp/tcpdump -i wlan0 -c 100 -w /sdcard/capture.pcap

十八、TCP 协议的最新发展

18.1 TCP Timestamps 与 PAWS

PAWS(Protection Against Wrapped Sequence numbers, RFC 7323)使用 TCP Timestamps 选项来防止序列号回绕(在高速网络上,32 位序列号可能在数分钟内回绕)。PAWS 检查报文的 timestamp 是否在可接受的窗口内;如果 timestamp 太旧,该报文被丢弃。

18.2 TCP ECN(Explicit Congestion Notification)

ECN(RFC 3168)允许路由器在发生拥塞时标记报文(设置 CE 位),而不是丢弃报文。端点收到 ECN 标记后降低发送速率,就像检测到丢包一样。相比依赖丢包信号的拥塞控制(被动隐式),ECN 是主动显式的拥塞信号,减少了不必要的重传。

Linux/Android 默认启用 ECN:net.ipv4.tcp_ecn = 2(0=关闭, 1=主动请求, 2=主动请求+被动接受)

18.3 TCP User Timeout

传统 TCP 的重传行为完全由 RTO 和退避算法决定。TCP User Timeout(RFC 5482)允许应用设置一个上限:如果数据在指定的时间内无法被确认(无论重传多少次),TCP 果断放弃连接。可通过 TCP_USER_TIMEOUT socket 选项设置:

unsigned int timeout = 10000;  // 10 秒
setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout));

对于移动应用,设置合理的 User Timeout 可以比依赖系统默认的无限重传(可能长达数十分钟)更快地感知网络故障。

18.4 MPTCP(Multipath TCP)

MPTCP(RFC 8684)允许一个 TCP 连接同时使用多条网络路径(如 Wi-Fi + 蜂窝网络),实现无缝网络切换和带宽聚合。iOS 自 iOS 7 起支持 MPTCP(用于 Siri),Android 12+ 也开始原生支持。

十九、TCP 重传统计与监控

19.1 重传率

TCP 重传率 = 重传报文数 / 总发送报文数。重传率是网络质量的核心指标:

  • < 0.1%:网络质量良好
  • 0.1% - 1%:轻度丢包,可能感知到偶尔卡顿
  • 1% - 5%:中度丢包,用户体验明显下降
  • 5%:严重丢包,连接可能不可用

# 查看系统级重传统计
netstat -s | grep retrans
# 输出: 1234 segments retransmitted
# 567 fast retransmits

19.2 在 Android 应用中监控

可以通过 TrafficStats 或 Android 9+ 的 NetworkStatsManager 获取应用的网络统计,进而计算重传率。对于生产应用,建议集成网络性能监控 SDK(如 Firebase Performance Monitoring 的 Network Monitoring 功能)。

对于 Android 开发者而言,TCP 位于 OkHttp/Retrofit 之下,理解 TCP 行为有助于诊断网络问题:为什么第一次请求慢(DNS + TCP 握手 + TLS 握手 = 3+ RTT)?为什么在某些网络环境下下载速度慢(高丢包 → 拥塞控制回退 → cwnd 减小)?为什么服务端需要 TIME-WAIT 状态的连接复用(SO_REUSEADDR + 连接池 + Keep-Alive)?这些问题的答案都在 TCP 协议本身。

二十、参考资料

  1. RFC 793 - Transmission Control Protocol (原始标准, 1981)
  2. RFC 1122 - Requirements for Internet Hosts (TCP 要求, 1989)
  3. RFC 1323 - TCP Extensions for High Performance (窗口缩放、时间戳、PAWS, 1992)
  4. RFC 2018 - TCP Selective Acknowledgment Options (SACK, 1996)
  5. RFC 2581 - TCP Congestion Control (Tahoe/Reno 标准化, 1999)
  6. RFC 5681 - TCP Congestion Control (更新版, 2009)
  7. RFC 6298 - Computing TCP’s Retransmission Timer (RTO 计算, 2011)
  8. RFC 6582 - The NewReno Modification to TCP’s Fast Recovery (2012)
  9. RFC 7323 - TCP Extensions for High Performance (更新版, 2014)
  10. RFC 7413 - TCP Fast Open (TFO, 2014)
  11. RFC 8312 - CUBIC for Fast Long-Distance Networks (2018)
  12. RFC 8684 - TCP Extensions for Multipath Operation (MPTCP, 2020)
  13. RFC 9000 - QUIC: A UDP-Based Multiplexed and Secure Transport (HTTP/3 基础, 2021)
  14. Linux 内核源码: net/ipv4/tcp*.c (tcp_input.c, tcp_output.c, tcp_cong.c, tcp_cubic.c)
  15. Jacobson, V. (1988). Congestion Avoidance and Control. ACM SIGCOMM.
  16. Cardwell et al. (2016). BBR: Congestion-Based Congestion Control. ACM Queue.:为什么第一次请求慢(DNS + TCP 握手 + TLS 握手 = 3+ RTT)?为什么在某些网络环境下下载速度慢(高丢包 → 拥塞控制回退 → cwnd 减小)?为什么服务端需要 TIME-WAIT 状态的连接复用(SO_REUSEADDR + 连接池 + Keep-Alive)?这些问题的答案都在 TCP 协议本身。
打赏
  • 微信
  • 支付宝

评论