Appearance
TCP 可靠传输与序号机制
2026 大纲 五(三)3 TCP 可靠传输。RTO 的递推公式与 Karn 算法、SACK 的容量推导、发送窗口的三条边界与滑动规则集中在本篇;rwnd 怎么产生见《TCP 流量控制》,cwnd 怎么变见《TCP 拥塞控制》。
一、四根支柱,序号是地基
IP 只承诺尽最大努力交付:会丢、会错、会重复、会失序。TCP 要在这样一条信道上做出"无差错、不丢失、不重复、按序到达",靠的是四样东西——
字节编号 + 累积确认:解决乱序、重复,并发现丢失。 超时重传:把丢的数据补回来。 校验和:对付比特差错(算法与 UDP 完全相同,见《UDP 协议》)。 滑动窗口:提高效率,同时承载流量控制与拥塞控制。
其中序号是地基:没有它,确认、排序、去重、窗口全都建不起来。
累积确认与它可以量化的代价
确认号 ack 的语义是期望收到的下一个字节的编号,隐含"之前的所有字节都已正确收到"。TCP 要求接收方必须有累积确认的功能。
这套语义容错性很好——中间丢了一个确认,后面的确认会把它一起捎上。但它不够精确,而且代价可以算出来:接收方收到了 1~500 与 601~700,却只能回一个 ack=501;发送方无从知道 601~700 已经到了,丢 100 字节最坏要重传 200 字节。
补这一格的是 SACK(选择确认),第四节展开。
确认还有两条时限要记:确认推迟的时间不应超过 0.5 秒;收到一连串具有最大长度的报文段时,必须每隔一个报文段就发送一个确认。另有一条与直觉相反的事实——捎带确认实际上并不经常发生,因为大多数应用很少同时在两个方向上发数据。
交互可视化
二、发送窗口:四段、三个指针
发送缓存里的字节被三条分界线切成四段:
已确认 │ 已发送未确认 │ 可以发送 │ 不能发送
─────────────────┤◄──────────── 发送窗口 ────────────►├──────────
后沿 前沿已确认的可以从发送缓存中删除;已发送未确认的在收到确认之前必须暂时保留,以备超时重传;可以发送的是窗口内还没发的;不能发送的在窗口之外——因为接收方没有为这部分数据保留缓存空间。
三条分界线就是教材的三个指针,都指向字节的序号:
自检恒等式:
推动三个指针的力量各不相同,这一条最值得单独记:
拿一个小例子对一遍:某时刻 ack=121, rwnd=60:后沿前移到
可用窗口 = 发送窗口 − 已发送未确认的字节数,它才是"现在还能发多少"。窗口满了(
两条边界规则
后沿绝不可能左移——左移等于撤销已收到的确认,逻辑上不可能。
前沿可以收缩,但标准强烈不赞成——逻辑上可能(对方通告的窗口变小了),但发送方很可能已经把那部分数据发出去了,会产生错误。
前沿"不动"有两种情况:没收到新确认且对方通知的窗口不变;或收到了新确认但对方通知的窗口缩小了,两个变化恰好抵消。
三处容易混的边界
缓存不等于窗口。 发送缓存存的是"待发的 + 已发未确认的",发送窗口通常只是发送缓存的一部分,两者后沿重合;接收缓存存的是"按序到达但尚未被读取的 + 未按序到达的"。
TCP 的失序处理更像 SR 而不是 GBN。 教材写得很谨慎:对于不按序到达的数据应如何处理,TCP 标准并无明确规定,但 TCP 通常先临时存放在接收窗口中,等缺少的字节收到后再按序交付。确认方式像 GBN、重传行为却接近 SR。
A 的发送窗口不总等于 B 的接收窗口。 两个原因:① 窗口值经网络传送有时间滞后;② A 还可能按网络拥塞情况主动减小。这正是那个公式的来历——
rwnd 代表"对方接不接得下",cwnd 代表"网络扛不扛得住",两个约束同时成立才能发。
发送窗口滑动的逐步走查,与数据链路层滑动窗口的对照(想看清后沿、前沿各被什么推动时展开)
设 B 通告 rwnd=20、ack=31("到序号 30 为止我都收到了,从 31 开始还能收 20 个字节")。
| 时刻 | 事件 | 后沿 | 前沿 | 窗口内已发未确认 | 可用窗口 |
|---|---|---|---|---|---|
A 收到 ack=31, rwnd=20,构造发送窗口 | 31 | 51 | — | 31~50,共 20 | |
| A 发送序号 31~41 共 11 字节 | 31 | 51 | 31~41 | 42~50,共 9 | |
| A 再发送 42~50 共 9 字节 | 31 | 51 | 31~50 | 0(窗口已满) | |
收到 ack=34, rwnd=20(31~33 已确认) | 34 | 54 | 34~50 | 51~53,共 3 | |
收到 ack=34, rwnd=10(窗口缩小) | 34 | 44 | 34~43(44~50 变成"不能发送") | 0 |
这张表同时解释三件事:① 窗口"滑动"的动力只来自收到确认(后沿前移);② 窗口"张大缩小"的动力来自对方通告的窗口值(前沿位置);③ 可用窗口 = 窗口大小 − 已发送未确认的字节数。
与数据链路层滑动窗口的区别:
| 对比项 | 数据链路层 | TCP |
|---|---|---|
| 窗口单位 | 帧 | 字节 |
| 窗口大小 | 通常固定 | 动态调整(受 rwnd 与 cwnd 双重约束) |
| 编号空间 | 较小(如 3 位 = 0~7),窗口上限受 | 32 位( |
| 失序数据 | GBN 一律丢弃,SR 缓存 | 通常缓存(标准未强制规定) |
| 确认方式 | 逐帧确认或累积确认 | 累积确认(+ 可选 SACK) |
三、超时重传时间怎么定
重传时间的选择是 TCP 最复杂的问题之一。 RTO 两头的后果都很实在:太短会引起很多不必要的重传,使网络负荷增大;太长又使网络的空闲时间增大,降低传输效率。
三个式子:
首次测量不套递推式:
光有平均 RTT 为什么不够
两条链路:甲的 RTT 稳定在 100 ms 上下、波动 ±2 ms;乙平均也是 100 ms,但在 20 ms 与 180 ms 之间剧烈跳。两者的
"平均值"描述不了"抖不抖",所以要再引入描述抖动的
重传把测量搅乱了:Karn 算法
重传的报文段和原来的完全一样,收到确认后源主机无法判断它在确认哪一次发送。两种误判都是发散的:把"对重传的确认"当成"对原报文段的确认",样本偏大(整个超时时间被算了进去),RTO 越来越长、丢包后只能干等;反过来样本偏小,RTO 越来越短、报文段过多地重传。一旦偏了,下一次更容易再偏同一个方向。
Karn 算法有三件事,缺一不可:① 只要报文段重传了,就不采用其往返时间样本;② 每重传一次就把 RTO 增大一些,典型做法是取旧值的 2 倍;③ 当不再发生重传时才按公式重新计算。
第 ② 条不是补丁,是必需的。 只做第 ① 条会出新问题:时延突然增大时样本全被丢弃,RTO 卡在旧值上,于是每个报文段都超时、都重传、样本又全被丢弃——死循环。指数退避正是为此:既然测不到新样本,那就不测了——直接假定网络变慢,把 RTO 翻倍,翻到某一次终于不用重传,测量随即恢复正常。翻倍而非"加固定值",是因为加固定值在网络严重恶化时追不上。
Karn 丢弃的是"样本"不是"确认":重传报文段的确认照样有效(数据确实到了、窗口照样滑动),只是该 RTT 样本不用来更新
更彻底的办法是给每次发送打上时间标签,即时间戳选项的第一个功能——有了它重传报文段的样本也能安全使用。
RTO 的四轮完整递推(想手动跑一遍两条递推式、看清偏差项怎么收窄时展开)
依次测得四个有效的 RTT 样本:40 ms、60 ms、30 ms、50 ms,取
、 , 的更新使用本次更新后的 。
第 1 步:第一个样本用初值规定,不套递推式。
递推式里要用"旧值",而此刻还没有旧值。直接套公式是这类题最容易踩的坑。
第 2 步:第二个样本 60 ms。
偏差式里的
要用本轮更新后的 ,所以必须先算 再算 。
第 3 步:第三个样本 30 ms。
第 4 步:第四个样本 50 ms。
| 次序 | RTT 样本 | RTO | ||
|---|---|---|---|---|
| 1 | 40 | 40 | 20 | 120 |
| 2 | 60 | 42.5 | 19.375 | 120 |
| 3 | 30 | 40.9375 | 17.2656 | 110 |
| 4 | 50 | 42.0703 | 14.9316 | 101.797 |
验证趋势方向。 四个样本在 30~60 之间摆动,
一处口径提醒:教材式 (5-6) 写的是
,其中的 通常按本轮更新后的值理解(本篇即按此计算)。若题面明确要求用更新前的旧值,按题面走即可,方法完全一样,只是代入的数不同。
四、SACK 与两条丢包判据
SACK 补的是累积确认那个"丢 100 要重传 200"的缺口:它在选项里报告已经收到的那些不连续字节块的边界。
边界有个差一规则要记准:左边界指出字节块的第一个字节的序号,但右边界减 1 才是最后一个序号——收到 1501~3000 时右边界写 3001。这与"确认号是期望收到的下一个"其实是同一条规则的两个面。
最多报 4 个块:每个边界 4 字节、每块 8 字节,另加 2 字节种类与长度,
三条现实边界别忽略:① 建连时双方必须商定"允许 SACK";② 确认号字段的用法仍然不变(SACK 是附加信息,不是替代品);③ 文档没有指明发送方应当怎样响应 SACK,因此大多数实现还是重传所有未被确认的数据块。
最后一条是本节的落点:TCP 有两条互相独立的丢包判据。
超时是强信号——可能网络严重拥塞。收到 3 个重复确认是弱信号——只是个别报文段丢了,后面的都到了,说明网络还通着。
两者触发的动作不同:超时触发超时重传,3 个重复确认触发快速重传(不等计时器到期,立即重传那个缺失的报文段)。
而且同一个丢包事件会同时触发两套机制:可靠传输负责把丢的补回来,拥塞控制负责以后发慢一点。两条判据强弱不同,拥塞控制那边的反应也就不同——这是下一篇的内容。
本节小结
- 序号是地基,确认、排序、去重、窗口都建在它上面。累积确认容错性好但不够精确——丢 100 字节最坏要重传 200 字节,这一格由 SACK 补上(右边界减 1 才是块内最后一个序号,最多报 4 个块)。
- 发送窗口的动力学:
由收到的确认推动、 由自己发出的数据推动、 由 加对方通告的窗口值决定;后沿绝不可能左移,前沿收缩标准强烈不赞成;可用窗口 = 发送窗口 − 已发送未确认。 - RTO 光有均值不够,均值相同而抖动不同的链路需要不同余量,故取
,首次测量直接赋初值、每轮先更新 再算 。重传使"这个 ACK 对应哪次发送"无法判断,故 Karn 不采用重传样本 + 每重传一次 RTO 翻倍 + 不再重传后恢复计算,三件事缺一不可。
考点速记
本篇真题里被考过的形式有两种,一种算确认号,一种算"还能发多少"。两种都只用第一、二节那几条规则,一条公式都不用背。
A 组:给几个报文段,算确认号。三道题层层加码。
cn-2009-38 是基准款:甲连发两段,载荷 300 B 和 500 B,第一段序号 200,乙两段都正确收到,问确认号。
第一段占
cn-2011-40 加了一个丢包:甲连发三段,载荷 300 / 400 / 500 B,第 3 段的序号是 900,乙只收到第 1 和第 3 段,问确认号。
先倒推前两段的序号:第 3 段 seq = 900,那么第 2 段占
再用累积确认的语义判:乙收到了第 1 段(
这道题是"累积确认代价"那一条的直接考查:第 3 段明明已经收到了,却一个字节都确认不了。选项里的 1400 就是给"把收到的都算上"的人准备的。
cn-2013-39 换成双向:甲收到乙的一个段,seq = 1913、ack_seq = 2046、载荷 100 B,问甲立即发给乙的段的序号和确认序号。
两个字段各查各的,别串。甲的 seq 由乙的 ack_seq 决定——乙说"我期望 2046",那甲接下来就该从 2046 发。甲的 ack_seq 由乙的 seq + 载荷长度决定——乙那段占
四个选项正是 2046/2047 × 2012/2013 的笛卡儿积,两处都要 +1 或都不 +1 的人各错一半。判据只有一句:ack 填"期望的下一个",seq 填"我要发的第一个"。
B 组:给一张时序图,问"还能发多少 / 什么时候重传"。
cn-2021-40:甲在 seq=501 的段(200 B),seq=601, ack_seq=501, rcvwnd=500,问甲在收到新确认之前可以继续发送的数据序号范围。
关键在读懂 ack_seq=501:它表示乙期望 501,也就是说甲刚发出去的 501~700 还没有被确认。
于是套第二节那三个指针:发送窗口左沿 = 501(等于 ack_seq),窗口大小 = rcvwnd = 500 → 窗口覆盖
选项 A(501~1000)是把整个窗口当成"还能发",忘了扣掉已发未确认的部分;B 和 D 则是把窗口左沿错当成了乙的 seq 601——乙的 seq 是它自己发数据的序号,与甲的窗口毫无关系,这是全题最刻意的一处干扰。
cn-2019-38 考的是快速重传的触发点:客户在 ack_seq=100 并发出 seq=100 的段,该段丢失,问支持快速重传时客户重发该段的时刻。
数重复确认要从"第一次之后"开始数。 ack=100 是新确认,不算重复;此后
这道题唯一的坑就是把
本篇练习区里还会出现三道题,机制讲在别处:cn-2015-39 与 cn-2025-38 考接收窗口封顶与拥塞窗口增长,讲在 TCP 流量控制与拥塞控制逐轮推演;cn-2017-39 考慢开始涨到指定窗口要多久,讲在 TCP 拥塞控制。
本篇的 RTO 递推式与 Karn 算法在真题里不单独成题,但它们回答的是"重传计时器凭什么定成那个值"——知道
易错:确认号填"期望收到的下一个字节",等于"最后一个已收字节 + 1"。
易错:累积确认只能确认到第一个空洞之前。 空洞之后即使收到了也确认不了。
易错:双向通信时 seq 和 ack 各查各的:我的 seq 来自对方的 ack_seq,我的 ack 来自对方的 seq + 载荷长度。
易错:发送窗口的左沿是 ack_seq,不是对方的 seq。 对方的 seq 是它自己发数据用的。
易错:"还能发多少" = 窗口大小 − 已发送未确认,不是整个窗口。
易错:快速重传要 3 个重复确认,第一次收到的那个不算重复。
易错:后沿绝不可能左移(撤销不了已收到的确认);前沿收缩逻辑上可能但标准强烈不赞成。
易错:Karn 丢弃的是 RTT 样本,不是确认本身。 数据确实到了,窗口照样滑动。
易错:RTO 首次测量不套递推式:
取样本值、 取样本值的一半。
易错:SACK 的右边界减 1 才是块内最后一个序号,收到 1501~3000 要写 3001。
教材出处
- 谢希仁《计算机网络》(第 8 版)p230(5.6.1 以字节为单位的滑动窗口):发送窗口边界规则的原文——"凡是已经发送过的数据,在未收到确认之前都必须暂时保留,以便在超时重传时使用";"发送窗口前沿的前面部分表示不允许发送,因为接收方没有为这部分数据保留临时存放的缓存空间";"发送窗口后沿不可能向后移动,因为不能撤销掉已收到的确认。发送窗口前沿通常是不断向前移动的,但也有可能不动……发送窗口前沿也有可能向后收缩……但 TCP 的标准强烈不赞成这样做"。
- 同书 p231(图 5-15 及正文):三个指针的定义与三个差值——"要描述一个发送窗口的状态需要三个指针:
, 和 ……指针都指向字节的序号。 之前的数据是已发送并已收到确认的部分。 之后的数据是不允许发送的部分。 A 的发送窗口…… 已发送但尚未收到确认的字节数…… 允许发送但当前尚未发送的字节数(又称为可用窗口或有效窗口)"。 - 同书 p232(图 5-18 及正文):缓存与窗口的关系——发送缓存存放"发送应用程序传送给发送方 TCP 准备发送的数据"与"TCP 已发送出但尚未收到确认的数据","发送窗口通常只是发送缓存的一部分……发送缓存和发送窗口的后沿是重合的";接收缓存存放"按序到达的、但尚未被接收应用程序读取的数据"与"未按序到达的数据"。
- 同书 p233:三点强调——"虽然 A 的发送窗口是根据 B 的接收窗口设置的,但在同一时刻,A 的发送窗口并不总是和 B 的接收窗口一样大"(原因是窗口值传送的时间滞后与发送方按拥塞情况的主动减小);"对于不按序到达的数据应如何处理,TCP 标准并无明确规定……因此 TCP 通常是把不按序到达的数据先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程";"TCP 要求接收方必须有累积确认的功能……TCP 标准规定,确认推迟的时间不应超过 0.5 秒。若收到一连串具有最大长度的报文段,则必须每隔一个报文段就发送一个确认……捎带确认实际上并不经常发生,因为大多数应用程序很少同时在两个方向上发送数据"。
- 同书 p233–p234(5.6.2 超时重传时间的选择):定性与公式——"重传时间的选择却是 TCP 最复杂的问题之一";RTO 太短"会引起很多报文段的不必要的重传,使网络负荷增大"、太长"又使网络的空闲时间增大,降低了传输效率";式 (5-4) 的
语义"若 很接近于零,表示新的 值和旧的 值相比变化不大……若选择 接近于 1,则表示新的 值受新的 RTT 样本的影响较大",推荐 ;"每当第一次测量到 RTT 样本时, 值就取为所测量到的 RTT 样本值";式 (5-5) 及"RTO 应略大于上面得出的加权平均往返时间 ";式 (5-6) 及初值"当第一次测量时, 值取为所测量到的 RTT 样本值的一半", 推荐 1/4。 - 同书 p234:两种误判的完整推演——"若收到的确认是对重传报文段的确认,但却被源主机当成是对原来的报文段的确认,则这样计算出的
和超时重传时间 RTO 就会偏大……同样,若收到的确认是对原来的报文段的确认,但被当成是对重传报文段的确认,则由此计算出的 和 RTO 都会偏小。这就必然导致报文段过多地重传";Karn 算法原文"在计算加权平均 时,只要报文段重传了,就不采用其往返时间样本";以及修正"报文段每重传一次,就把超时重传时间 RTO 增大一些。典型的做法是取新的重传时间为旧的重传时间的 2 倍。当不再发生报文段的重传时,才根据式 (5-5) 计算超时重传时间"。 - 同书 p235(5.6.3 选择确认 SACK):边界的差一规则——"第一个字节块的左边界
,但右边界 而不是 3000。这就是说,左边界指出字节块的第一个字节的序号,但右边界减 1 才是字节块的最后一个序号";容量推导——"由于首部选项的长度最多只有 40 字节,而指明一个边界就要用掉 4 字节……因此在选项中最多只能指明 4 个字节块的边界信息……如果要报告 5 个字节块的边界信息,那么至少需要 42 字节。这就超过了选项长度 40 字节的上限";三条边界条件——"在建立 TCP 连接时,就要在 TCP 首部的选项中加上'允许 SACK'的选项,而双方必须都事先商定好"、"原来首部中的'确认号字段'的用法仍然不变"、"SACK 文档并没有指明发送方应当怎样响应 SACK。因此大多数的实现还是重传所有未被确认的数据块"。