TCP(Transmission Control Protocol,传输控制协议)是互联网最核心的协议之一。它提供了可靠、有序、面向连接的字节流传输服务。理解 TCP 的头部格式、连接管理状态机、流量控制、拥塞控制和各类定时器,是深入理解网络编程的基础。本文从 AOSP 内核和协议栈源码出发,系统梳理 TCP 协议的方方面面。
一、TCP 的设计目标
TCP 协议在 RFC 793(1981 年)中首次标准化,它的设计目标:
- 可靠性:数据不丢失、不重复、不乱序。通过序列号、ACK 确认、超时重传保证。
- 有序传输:发送端按序发送,接收端按序向应用层交付。通过序列号和重组缓冲区实现。
- 流量控制:防止发送方发送过快,压倒慢速的接收方。通过滑动窗口实现。
- 拥塞控制:防止网络中的过多数据导致拥塞崩溃。通过拥塞窗口和多种算法(CUBIC、BBR)实现。
- 全双工通信:同一连接中数据可同时双向传输。通过独立的序列号空间(每个方向有独立的 seq/ack)实现。
- 面向连接:通信前需要通过三次握手建立连接,通信后通过四次挥手断开连接。
TCP 不提供什么:
- 不保证传输延迟(实时性)→ 使用 UDP 或 QUIC
- 不保证最小带宽 → 由应用层协商或使用 QoS
- 不保留消息边界(它是字节流,不是消息流)→ 应用层需自行设计消息边界(如 HTTP 的 Content-Length 或分块编码)
二、TCP 报文头部格式
2.1 完整头部结构
0 1 2 3 |
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 伪头部(用于校验和计算)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
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) |
三、TCP 连接管理状态机
3.1 十一种状态
TCP 连接两端在生命周期中经历 11 种状态。完整的 TCP 状态机:
+---------+ ---------\ active OPEN |
状态速查表:
| 状态 | 含义 | 谁进入此状态 |
|---|---|---|
| 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 报文,由于网络延迟,客户端超时重传了 SYN,建立了连接,通信完毕并关闭了连接。此时,最初那个”迟到的 SYN”到达了服务端。
- 如果只有两次握手:服务端收到这个迟到的 SYN,直接进入 ESTABLISHED 状态,等待客户端发数据。但客户端已经关闭了,不会响应。服务端资源被浪费。
- 三次握手解决了这个问题:服务端收到迟到的 SYN,回复 SYN-ACK,但客户端不会回复 ACK(因为它知道这不是自己发起的连接),服务端不会进入 ESTABLISHED。
3.3 四次挥手(Four-Way Handshake)
Client (主动关闭) Server (被动关闭) |
为什么挥手需要四次?
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 存在的两个原因:
- 保证最后的 ACK 能到达对端:如果最后的 ACK 丢失,被动方会超时重传 FIN,主动方重新发送 ACK。这个过程持续 2MSL。
- 防止旧连接的报文”复活”到新连接:确保本连接中所有报文都在网络中消亡。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 使用滑动窗口协议进行流量控制,确保发送方不会溢出接收方的缓冲区:
发送方窗口 |
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 |
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 时: |
Reno 在单次丢包时非常高效(避免了慢启动的重新攀升),但在一个窗口内丢多个包时表现不佳。Reno 需要 Duplicate ACK 来触发快速重传,如果多个丢包在同一个窗口内,后续的丢包只能通过超时重传(回到 cwnd=1)。
5.4 TCP NewReno
NewReno(RFC 6582, 2012)改进了 Reno 处理同一窗口中多个丢包的能力:
在快速恢复期间,维持一个”恢复点”概念:只有当所有在进入快速恢复之前发送的报文都被确认后,才退出快速恢复。部分 ACK(确认了部分而非全部 outstanding 数据)不退出快速恢复,而是继续重传下一个未确认的报文。
// Linux 内核中 NewReno 的核心逻辑 (简化, net/ipv4/tcp_input.c) |
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 |
CUBIC 的三个阶段:
- 凹区域(t < K):快速接近 W_max,但增长率逐渐减慢
- 凸区域(t ≈ K):在 W_max 附近窗口增长非常缓慢(稳定性)
- 凸区域(t > K):窗口加速增长,探测更多可用带宽
// Linux 内核中 CUBIC 的核心窗口计算 (net/ipv4/tcp_cubic.c) |
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) |
BBR 持续估计两个参数:
- **Bottleneck Bandwidth (BtlBw)**:过去 10 个 RTT 内的最大送达速率
- **Minimum RTT (RTprop)**:过去 10 秒内的最小 RTT
BBR 的状态机:
- STARTUP:指数增加发送速率(类似慢启动),直到吞吐量不再增长(即填满了瓶颈缓冲)
- DRAIN:降低发送速率,排空缓冲区中多余的报文
- PROBE_BW:在主状态下持续探测更多带宽(周期性小幅增加/减少速率)
- 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 估计) |
默认值:α = 1/8(RFC 6298),β = 1/4。
Karn 算法:如果一个报文被重传了,收到 ACK 时无法区分这个 ACK 是对原始报文的还是对重传报文的。因此,不对重传报文的 RTT 进行采样(直接丢弃该 RTT 样本),只对未重传的报文进行 RTT 估计。同时,对重传报文的 RTO 做退避处理(double RTO)。
七、tcpdump 实战分析
7.1 捕获三次握手
# 抓取本地 8080 端口的 TCP 握手 |
7.2 捕获数据传输与 RTT
# 带相对时间戳和序列号,观察 RTT |
7.3 使用 ss 命令查看连接状态
# 查看所有 TCP 连接状态统计 |
八、Android 中的 TCP 调优
8.1 关键 sysctl 参数
# Linux/Android 内核 TCP 参数 (可通过 sysctl 或 /proc/sys/net/ipv4/ 查看) |
8.2 OkHttp 的 TCP 连接管理
// OkHttp 使用连接池管理 TCP 连接 |
面试常考问题
Q1:TCP 三次握手为什么不是两次或四次?
两次握手的致命问题:已失效的连接请求报文(在网络中延迟到达)可能导致服务端单方面建立连接,浪费资源。客户端明确知道自己不需要这个连接(未发起),但服务端不知道。
四次握手不必要:三次已经足够完成双向的初始化序列号交换和确认。SYN-ACK 合并了”对 SYN 的 ACK”和”服务端的 SYN”,减少了一次往返。
Q2:TIME-WAIT 状态为什么是 2MSL?
MSL(Maximum Segment Lifetime)是报文在网络中最长的存活时间(通常 60 秒)。2MSL 确保了:
- 最后一个 ACK 有足够时间到达对端,且如果 ACK 丢失,对端重传的 FIN 也能在 2MSL 内到达
- 本连接中所有”迷路”的报文在 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 必须自己通过序列号来:
- 检测丢包(序列号不连续)
- 重组乱序报文(按序列号排序)
- 检测重复报文(相同序列号重复到达)
- 对每个字节进行确认(ACK 携带期望的下一个字节序列号)
九、TCP 的字节流语义
9.1 没有消息边界
TCP 是一个字节流协议,不保留应用层的消息边界:
发送端 write(): "Hello" (5 bytes) |
应用层协议必须自己处理消息边界。常见策略:
- 固定长度消息:每个消息固定 N 字节
- 长度前缀:消息头包含消息体长度(如 HTTP/1.1 的 Content-Length)
- 分隔符:用特殊字符分隔消息(如 HTTP/1.1 的
\r\n\r\n,SMTP 的\r\n.\r\n) - 分块编码:每个 chunk 以长度开头(如 HTTP/1.1 的 Chunked Transfer Encoding)
9.2 Nagle 算法与交互式应用
Nagle 算法的核心规则:当一个 TCP 连接有已发送但未确认的数据时,小于 MSS 的报文不能发送,必须等 ACK 到来或积累到 MSS。
// Nagle 算法的伪代码 |
对于交互式应用(SSH、telnet、游戏),每个按键都会触发一个小数据包。如果开启 Nagle,这些按键会被”合并”后才发送,导致明显的延迟。解决方案:
int flag = 1; |
或者在应用层合并小写入(writev、coalescing writes)。
9.3 TCP_CORK(Linux 特有)
Linux 提供了比 TCP_NODELAY 更强的选项 TCP_CORK:在清除 CORK 标志之前,所有小的写入都被”堵住”(corked),只在以下时机发送:
- 手动清除 CORK 标志
- CORK 开启超过 200ms
- 缓冲区中的数据达到 MSS
这比 Nagle 更激进,适合 HTTP 响应头+响应体需要一起发送的场景:
// 伪代码 |
十、TCP 连接终止的边界情况
10.1 半关闭(Half-Close)
Client Server |
半关闭在 HTTP/1.0 中常用:客户端发送请求后关闭写端(FIN),表示请求数据已完成,但保持读端打开以接收响应。
10.2 同时关闭(Simultaneous Close)
当双方同时发送 FIN 时,状态转换为 CLOSING,而非 FIN-WAIT-2:
Client Server |
10.3 RST 报文的使用场景
RST(Reset)是 TCP 的”异常终止”信号。典型场景:
- 连接请求被拒绝:客户端尝试连接一个服务端没有监听的端口 → 服务端回复 RST
- 半开连接(Half-Open Connection):一方崩溃后重启,收到旧连接的报文 → 回复 RST
- **SO_LINGER=0 的 close()**:不经过四次挥手,直接发送 RST 断开连接
- 接收到的报文不属于任何连接:防火墙可能注入 RST 来切断不允许的连接
// SO_LINGER 示例:立即关闭,不优雅挥手 |
十一、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 |
11.2 SYN Flood 攻击与防御
SYN Flood 是最经典的 DDoS 攻击:攻击者发送大量 SYN 报文(通常伪造源 IP),服务端为每个 SYN 分配 TCB(传输控制块),回复 SYN-ACK 并等待 ACK。由于源 IP 是伪造的,ACK 永远不会到达,服务端的半连接队列(SYN queue)被填满,无法为正常用户建立连接。
防御手段:
- SYN Cookies:服务端不在 SYN-RCVD 阶段分配 TCB,而是将连接信息(MSS、序列号、时间戳)通过加密哈希编码到 SYN-ACK 报文的 ISN 中。收到合法的 ACK 时,从 ACK 的序列号中解码回连接信息。Linux 默认启用。
// Linux 内核 SYN cookie 的实现 (简化, net/ipv4/tcp_ipv4.c) |
tcp_syncookies:Linux 的 sysctl 参数。当半连接队列满时,自动启用 SYN Cookies。
增加 SYN 队列长度:
net.core.somaxconn和tcp_max_syn_backlog。
十二、TCP 的性能优化
12.1 延迟确认(Delayed ACK)
为了减少网络中纯 ACK 报文的数量,TCP 使用延迟确认:收到数据后不立即发送 ACK,而是等待最多 500ms(Linux 中为 40ms-200ms),希望在等待期间:
- 自己有数据要发给对端(可以顺便捎带 ACK,即 piggyback)
- 收到更多的数据(可以累积确认)
// Linux: net/ipv4/tcp_input.c |
副作用:延迟确认增加了 RTT。在请求-响应模式(HTTP)中,延迟确认 + Nagle 算法的交互可能导致200ms+ 的额外延迟。
12.2 TCP 快速打开(TCP Fast Open, TFO)
TCP 快速打开(RFC 7413)允许在三次握手期间就开始传输数据,消除连接建立的一个 RTT 延迟:
传统 TCP: TFO: |
// 在 Android 客户端启用 TFO |
TFO 在 Android 9+ 默认启用,需要服务端支持。
12.3 带宽延迟积(BDP)与缓冲区调优
BDP = bandwidth × RTT。TCP 的发送/接收缓冲区至少应等于 BDP 才能充分利用网络:
BDP 示例: |
Linux 内核从 2.6 开始通过 tcp_rmem 和 tcp_wmem 支持自动调优缓冲区(auto-tuning)。Android 继承了这个特性。
# 查看当前接收/发送缓冲区设置 |
十三、TCP 在 Linux 内核中的实现概览
13.1 核心数据结构
// 传输控制块 (include/net/tcp.h) |
13.2 接收路径(关键函数调用链)
网卡中断 |
13.3 发送路径(关键函数调用链)
用户态 write() / send() |
十四、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 抓取原始流量 |
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 协议的最新发展
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 秒 |
对于移动应用,设置合理的 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%:严重丢包,连接可能不可用
# 查看系统级重传统计 |
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 协议本身。
二十、参考资料
- RFC 793 - Transmission Control Protocol (原始标准, 1981)
- RFC 1122 - Requirements for Internet Hosts (TCP 要求, 1989)
- RFC 1323 - TCP Extensions for High Performance (窗口缩放、时间戳、PAWS, 1992)
- RFC 2018 - TCP Selective Acknowledgment Options (SACK, 1996)
- RFC 2581 - TCP Congestion Control (Tahoe/Reno 标准化, 1999)
- RFC 5681 - TCP Congestion Control (更新版, 2009)
- RFC 6298 - Computing TCP’s Retransmission Timer (RTO 计算, 2011)
- RFC 6582 - The NewReno Modification to TCP’s Fast Recovery (2012)
- RFC 7323 - TCP Extensions for High Performance (更新版, 2014)
- RFC 7413 - TCP Fast Open (TFO, 2014)
- RFC 8312 - CUBIC for Fast Long-Distance Networks (2018)
- RFC 8684 - TCP Extensions for Multipath Operation (MPTCP, 2020)
- RFC 9000 - QUIC: A UDP-Based Multiplexed and Secure Transport (HTTP/3 基础, 2021)
- Linux 内核源码: net/ipv4/tcp*.c (tcp_input.c, tcp_output.c, tcp_cong.c, tcp_cubic.c)
- Jacobson, V. (1988). Congestion Avoidance and Control. ACM SIGCOMM.
- Cardwell et al. (2016). BBR: Congestion-Based Congestion Control. ACM Queue.:为什么第一次请求慢(DNS + TCP 握手 + TLS 握手 = 3+ RTT)?为什么在某些网络环境下下载速度慢(高丢包 → 拥塞控制回退 → cwnd 减小)?为什么服务端需要 TIME-WAIT 状态的连接复用(SO_REUSEADDR + 连接池 + Keep-Alive)?这些问题的答案都在 TCP 协议本身。

