TCP

本页目录

应用调用两次 send,接收方可能一次读出全部,也可能分多次才读完。TCP 保持的是有序字节流,不保留应用写入的消息边界。理解它可以沿一笔数据追踪:序号标识字节,ACK 表示接收进度,窗口约束可发送范围,丢失检测触发恢复,拥塞控制决定向网络施加多少负载。

需要快速定位本篇重点
“粘包”、短读和 ACK 的含义字节流与应用交付
SYN/FIN 为何让 ACK 加一序号空间与连接生命周期
收到后面的数据为何还在重复 ACK累计 ACK、SACK 与缺口
带宽很大却发不快rwnd、cwnd、在途量与 BDP
重传何时发生重复 ACK、RACK-TLP 与 RTO
CUBIC 和 BBR 如何理解曲线算例、模型和版本边界

字节流:可靠接收不等于业务完成

TCP 在连接仍可维持时通过确认、重传和有序交付恢复数据,无法在永久断网时保证最终成功。send 成功通常说明数据进入本地发送路径;TCP ACK 说明对端 TCP 已接收相应字节,不证明对端应用已读、写盘或提交事务。业务确认仍应由应用协议表达。

例如两次写入 abc 和 def,接收端可能读到 ab、cdef。长度前缀、固定长度或分隔符等 framing 才定义消息边界,PSH 不是可靠的消息分隔标志。TCP 也不自带加密或服务器身份认证,TLS 等机制承担另一层职责。RFC 9293:TCP 基础规范

头部字段怎样连接到行为

字段位数/单位实际含义
源/目的端口各 16 bit与地址、协议及网络上下文定位端点
Sequence Number32 bit当前段起始序号,含 SYN 时需考虑 SYN 占位
Acknowledgment Number32 bitACK 有效时,期待下一个连续序号
Data Offset4 bit,单位 4 BTCP 头长,通常 20~60 B
控制标志如 SYN、ACK、FIN、RST、PSH、ECE、CWR建连、确认、关闭、复位与拥塞反馈等
Window16 bit,经协商可缩放对端通告的接收窗口,不是原样暴露全部 socket 内存
Checksum16 bit覆盖伪首部、TCP 头与数据,不是密码学认证
Urgent Pointer16 bit配合 URG 的历史紧急数据语义,不能当独立优先通道

TCP 长度需由 IP 层长度和实际头链推出,不应拿 Data Offset 当整段长度。32 bit 序号会回绕,比较应按协议的模序号规则和窗口约束处理,普通整数大小比较不能覆盖全部状态。

选项:有些只在建连时协商

选项作用边界
MSS,Kind 2通告本端可接收的 TCP 数据段上限只在 SYN 中出现;不包含 TCP/IP 头
Window Scale,Kind 3本端后续窗口字段的缩放位数,最大 14在 SYN 中协商,SYN 自身窗口字段不缩放
SACK Permitted,Kind 4表示允许对端使用 SACK与实际 SACK 区间报告分开
SACK,Kind 5报告非连续接收区间并非重新定义累计 ACK
Timestamp,Kind 8支持 RTT 测量与 PAWS 等需按协商及规范使用,不能认为所有 TCP 包天然携带

窗口缩放是分方向的:C 通告的 shift 用于解释 C 后续发出的接收窗口,限制 S 向 C 的发送。MSS 同样约束对端发给本端的数据,且实际发送还会受 PMTU、选项、拥塞与接收窗口限制。RFC 7323:缩放与时间戳

握手:确认序号与连接状态

握手同步两个方向的序号;数据确认按字节累计 客户端 C 服务端 S SYN:seq=1000 SYN + ACK:seq=5000,ack=1001 ACK:seq=1001,ack=5001 100 B 数据:seq=1001,ack=5001 ACK:seq=5001,ack=1101 SYN 占一个序号;100 B 数据覆盖 [1001,1101)。纯 ACK 不占序号,两个方向各有独立序号空间。

客户端 SYN 使用序号 1000,服务端用 ACK=1001 确认它,并提出自身 ISN=5000;客户端再确认 5001。第三步让服务端知道自己的序号已被对端确认,有助于排除旧的重复连接尝试。握手确认的是当时协议状态,不保证之后网络永远可达。

SYN 和 FIN 各占一个序号,让控制事件能够统一纳入有序确认和重传;不能反推为“不占序号的消息在任何协议里都无法重传”。纯 ACK 不占序号,也不会为每个 ACK 再专门确认一次。

常规客户端经过 SYN-SENT,服务端经过 SYN-RECEIVED,再进入 ESTABLISHED;同时打开、携带数据的扩展和异常复位另有规则。SYN cookies 是抵抗某些半连接资源压力的实现机制,有协商能力和使用条件,不能取代正常队列规划或全部防护。RFC 9293:连接建立

累计 ACK、SACK 与重传区间

累计 ACK 停在缺口,SACK 另外报告已经收到的区间 [1001,2001) 已连续收到 [2001,3001) 缺口 [3001,4001) 已收到,暂不能有序交付 累计 ACK = 2001 SACK = [3001,4001) SACK 右边界不包含在区间内;补齐中间 1000 B 后,累计 ACK 可推进到 4001。

假设收到 [1001,2001) 和 [3001,4001),中间 [2001,3001) 丢失。累计 ACK 只能为 2001;SACK 额外报告 [3001,4001)。补齐缺口后,连续范围到 4001,ACK 才能跳到那里。

SACK 表达接收事实,发送端还需要维护记分板、判断丢失并安排重传;“启用 SACK 就永远只重发真正丢失的一份数据”过于绝对,乱序和误判会影响恢复。接收方也有撤回已 SACK 数据的边界,因此发送方不能只因 SACK 就在累计确认之前丢掉所有重传副本。RFC 2018:SACK

丢失检测与拥塞控制是两个相关但不同的问题:前者决定哪些数据可能丢了,后者决定接下来允许的负载。经典快速重传利用重复 ACK 等信号;现代 RACK 根据发送时间与后续确认推断丢失,TLP 用尾部探测改善某些尾丢失恢复,不必都等三次重复 ACK。RFC 8985:RACK-TLP

RTO 为什么不能直接等于 RTT

RTO 要给时延变化留余量。RFC 6298 的基础估计为 SRTT + max(G,4×RTTVAR),并配合最小值与退避规则。第一次样本 R=100 ms 时,SRTT=100 ms、RTTVAR=50 ms,原始计算值 300 ms;按该 RFC 建议的 1 s 下限得到 1 s,实际内核的下限实现应另行核对。

下一样本 120 ms 时,先用旧 SRTT 更新 RTTVAR:0.75×50+0.25×20=42.5 ms;再更新 SRTT:0.875×100+0.125×120=102.5 ms。原始 RTO=272.5 ms。重传后的 ACK 可能无法确定确认的是哪一次发送,Karn 规则及时间戳用于处理相应采样歧义;超时还会触发退避。RFC 6298:重传计时

ACK 到了哪里,应用读到了哪里?

先送第三段,再送第一段,累计确认只会跨过真正连续的部分。补齐第二段时 ACK 可以一次前移两段;最后点击应用读取,观察网络确认与应用消费是两个位置。图中的 SACK 区间按教学分段列出,相邻区间可合并。

正在呈现知识画面
ACK 到了哪里,应用读到了哪里?

累计 ACK 指向下一个期待字节;乱序数据可暂存,但不能跳过缺口交给字节流应用。应用读取不会重新定义 ACK。

流控与拥塞窗口:先扣除已在途的量

在普通、非恢复的简化模型中,发送端受接收窗口 rwnd 与拥塞窗口 cwnd 共同限制。min(rwnd,cwnd) 是在途规模约束,不是每次都能再发送这么多;还需扣除已发未确认数据,再考虑 pacing、应用可用数据与实际实现。

设 SND.UNA=1001、SND.NXT=4001,则已有 3000 B 未确认。rwnd=8000 B、cwnd=5000 B 时,简化的新数据余量为 min(8000,5000)−3000=2000 B。把 cwnd 提升到 20000 B 后,约束变为接收窗口,新余量是 5000 B。

限制来源典型证据增大另一项是否解决
接收端读取慢,rwnd 小通告窗口缩小、零窗口单独增大 cwnd 无效
网络拥塞或恢复,cwnd 小拥塞事件、窗口变化、重传单独增大接收缓冲无效
应用供数不足发送队列空、应用停顿增大任一窗口都可能无效
pacing 或主机处理能力发送节奏、CPU/队列证据不能仅由窗口大小解释

零窗口后,发送端的 persist 机制探测窗口是否重新打开,防止丢失窗口更新导致长期停滞;它与空闲连接 keepalive、丢包重传计时器是不同用途。Nagle 则限制存在未确认数据时的小段发送,配合延迟 ACK 可能增加小交互等待;TCP_NODELAY 不会恢复应用消息边界。

带宽时延积复算

1 Gbit/s、RTT=50 ms 的 BDP 为 10⁹/8×0.05=6,250,000 B。未缩放的 65535 B 窗口,即使忽略其他限制,窗口/RTT 给出的上限也只有约 10.486 Mbit/s。scale=7 时最大表示 8,388,480 B,约 8 MiB,才可能覆盖这个 BDP;这只是必要预算,不是吞吐保证。

窗口不是还能发送的字节数

三个滑块控制不同的状态量。把在途量拖到两种窗口之间,可以看出较大的那个窗口不能补偿较小的约束;再把窗口缩到在途量以下,观察后续发送暂停,而已有在途量仍留在图中。

正在呈现知识画面
窗口不是还能发送的字节数

先取两种约束的较小值,再扣除已在途数据。窗口收缩到在途量以下时,新的发送额度为零,不会把已经发出的数据变成负数。

关闭:两个方向独立结束

A 发 FIN 后不再发送新的应用数据,但仍可接收 P 的数据。P 确认后可能停留 CLOSE-WAIT,直到应用结束自己的发送方向;随后 P 发 FIN,A 确认。ACK 与 FIN 可以合并,所以关闭并非必须正好四个包。同时关闭也会改变状态路径。

典型主动关闭方经过 FIN-WAIT-1、FIN-WAIT-2、TIME-WAIT;被动一侧经过 CLOSE-WAIT、LAST-ACK。大量 CLOSE-WAIT 应检查应用为何尚未结束连接,而不是用 TIME_WAIT 参数解释。

TIME-WAIT 为响应重传 FIN 和避免旧连接报文干扰新连接保留时间,不保证最后 ACK 一定送达。规范用 2MSL 描述,Linux 7.2 的 TCP_TIMEWAIT_LEN 为 60×HZ,即 60 s;应区分规范模型与实现计时。tcp_fin_timeout 针对相应孤儿 FIN-WAIT-2 状态,不是 TIME-WAIT 时长开关。

连接复用可降低建连/关闭频率;SO_REUSEADDR 影响符合条件的绑定,不能消除全部四元组冲突。tcp_tw_reuse 也有安全条件和版本配置含义,应先区分状态与资源瓶颈,再谈参数。RFC 9293:关闭、Linux TCP sysctl

CUBIC:从保留比例理解增长曲线

网络可以用丢包、ECN 等信号反映拥塞,不能说拥塞控制只有“猜”且网络绝不主动反馈。CUBIC 的一个核心目标曲线是:

W(t) = C×(t−K)³ + Wmax

当 β 表示拥塞后保留比例时,K = ∛(Wmax×(1−β)/C)。取 β=0.7、C=0.4、Wmax=100 段,K≈4.22 s;W(0)=70 段,W(K)=100 段。旧公式把保留比例 0.7 直接放入 K 的分子,会使 W(0) 错成 30 段。RFC 9438:CUBIC

CUBIC 教学曲线:β=0.7,Wmax=100 段,C=0.4 段 t / s 70 85 100 115 W(0)=70 W(K)=100,K≈4.22 s 窗口单位是 SMSS 段,时间单位为秒;这是目标曲线,完整实现还结合 ACK、恢复与 Reno 友好区域。

曲线在接近旧 Wmax 时增长放缓,越过后继续探测;时间是这个函数的自变量,但实际 TCP 仍依赖 ACK、RTT、Reno 友好区域和恢复规则,不能概括为吞吐完全不受 RTT 影响。启动时的近似指数增长也受 ACK 行为和限制条件影响,不是每条连接每个 RTT 必定精确翻倍。

CUBIC 的拥塞响应并不意味着每个单独丢包都独立乘 0.7;事件归并、快速恢复与超时具有不同处理。队列大小、AQM、ECN、随机丢包和竞争流都会影响最终延迟与吞吐。

拐点前后,增长为什么先慢后快?

让时间游标经过 K,观察窗口仍在增长,但斜率先降到零再上升。改变 β 会同时移动初始窗口与 K,而 W(K) 始终回到旧 Wmax。这比只记住三次方的外形更能解释公式中的平移项。

正在呈现知识画面
拐点前后,增长为什么先慢后快?

K 把曲线的平坦区放到旧 Wmax 附近;β 是丢失后的保留比例,不是减掉的比例。三次目标曲线不等于真实发送速率。

BBR:带宽与时延模型也需要验证

BBR 使用交付速率和 RTT 等观测估计路径容量,结合 pacing 和在途约束控制发送。典型过程包括快速探测容量、排空启动形成的队列、周期性探测带宽和更新最小时延估计。观测会受 ACK 聚合、应用受限、路径变化及其他流影响,模型不是直接读取真实物理瓶颈。

Linux 7.2 的 tcp_bbr.c 包含经典四模式、约 2.885 的启动 pacing gain,以及 1.25/0.75 等稳态增益;这不能推广为全部 BBR 版本的统一常数。带宽估计、cwnd 增益和 pacing gain 也不同,不能把在途量始终精确钉为一个 BDP。

Google 的 BBR 项目包含持续演进的实现与材料,应明确所比较版本。RFC 9438 定义 CUBIC;BBR 的算法与版本应查对应项目资料。不要用“BBR 从不因丢包降速”“必然无队列”或未经限定的部署百分比替代实验条件。Google BBR 项目

比较维度CUBICBBR 系列
核心理解入口窗口增长曲线与拥塞响应交付速率/时延模型与 pacing
必须说明的条件恢复算法、ECN/AQM、RTT 与负载版本、观测偏差、丢包/ECN 处理
不能保证同等带宽下永远公平或低延迟始终无排队、对所有竞争流公平
验证指标吞吐、RTT 分布、重传、队列与公平性同样需要这些指标,而非只看单流峰值

本地推演:字节区间、窗口与曲线

import math

received = [(1001,2001),(3001,4001)]
def cumulative(intervals, start=1001):
    ack = start
    for left,right in sorted(intervals):
        if left>ack:
            break
        ack = max(ack,right)
    return ack
assert cumulative(received) == 2001
assert cumulative(received+[(2001,3001)]) == 4001
assert min(8000,5000)-(4001-1001) == 2000
assert 1_000_000_000/8*.05 == 6_250_000
assert 65535<<7 == 8_388_480
assert math.isclose(65535*8/.05/1e6,10.4856)

srtt,variation = .1,.05
sample = .12
variation = .75*variation+.25*abs(srtt-sample)
srtt = .875*srtt+.125*sample
assert math.isclose(srtt+4*variation,.2725)

beta,C,Wmax = .7,.4,100
K = (Wmax*(1-beta)/C)**(1/3)
def target(t):
    return C*(t-K)**3+Wmax
assert math.isclose(target(0),70)
assert math.isclose(target(K),100)
assert target(K+1)>100
print('ACK 缺口、发送余量、BDP、RTO 与 CUBIC 边界检查通过')

这是局部数学模型,不模拟真实重传定时、内核拥塞状态或网络公平性;未修改拥塞控制和连接参数。

继续查阅