1. 停等协议在长肥链路上的尴尬:GBN 是被什么逼出来的
第一次接触后退N帧协议(Go-Back-N,下文统一简称 GBN)的人,往往把注意力放在那条“窗口最大只能取 2^n−1”的结论上,觉得白白浪费了一个序号。但只要亲手算过一次停等协议在高速链路上的实际吞吐,就会明白 GBN 这类滑动窗口协议不是锦上添花的设计,而是被物理条件逼出来的刚需。GBN 的核心思路可以概括成一句话:发送方可以连续甩出去一串帧,接收方只认按序到达的那一个,出了差错就从出错的位置整段重来。
它的定位很明确——解决“链路带宽很大、但一来一回确认太慢”这个矛盾。适合刚学完停等协议、准备进入连续 ARQ 阶段的学习者,也适合已经工作几年、想回头把可靠传输底层机制理顺的开发者。我把这套东西拆成几块来讲:它要解决什么问题、运行起来是什么样子、窗口长度的边界在哪里、习题怎么算、性能账本怎么算、以及它的硬伤到底在哪。
1.1 一次一停的代价:从公式上看停等的天花板
停等协议的工作节奏是:发一帧,等一个确认,确认到了再发下一帧。这个节奏下,一个帧从开始发送到确认返回,链路被占用的时间是t_f + 2t_p,其中t_f是帧的发送时延(帧长除以链路速率),t_p是单向传播时延。真正用来传数据的只有t_f那一小段,剩下的时间全在等。
所以停等协议的信道利用率是:
η_stop = t_f / (t_f + 2t_p) = 1 / (1 + 2a)这里的a = t_p / t_f,物理含义是“单向传播时延里能塞下多少个帧的发送时间”。这个参数是理解后面一切内容的钥匙。
拿一组具体数字感受一下。假设链路速率 1 Gbps,单向传播时延 20 ms,帧长 1000 字节即 8000 bit。那么t_f = 8000 / 1e9 = 8 微秒,a = 20000 微秒 / 8 微秒 = 2500。代入公式,利用率约为1 / 5001 ≈ 0.02%。也就是说,一条 1 Gbps 的链路,用停等协议跑,实际吞吐只有 200 kbps 左右。链路的绝大部分时间在做一件事:等。
这就是业内说的“长肥链路”困境——带宽大(肥)、往返时延高(长),乘积越大,停等协议浪费得越狠。要在这类链路上跑满,必须让链路上同时有足够多的帧在飞,也就是把“发一帧等一帧”改成“连续发一串,批量确认”。这就是流水线思想,也是 GBN 出场的理由。
1.2 流水线化改造与“回退”这个名字的由来
流水线化之后,发送方不再发一帧停一次,而是维护一个“允许发送但尚未确认”的区间,这个区间就是发送窗口。窗口内的帧可以一股脑全发出去,窗口外的帧必须等窗口滑动之后才能发。窗口滑动的触发条件是收到确认。
GBN 在流水线的基础上做了一个非常干脆的取舍:接收方不缓存乱序帧。如果接收方期待的是 2 号帧,结果 3 号帧先到了,它不会把 3 号先存起来等 2 号,而是直接把 3 号扔掉,然后重新发一次“我还在等 2 号”的确认。发送方那边如果迟迟等不到 2 号的确认,超时之后就把 2 号以及 2 号之后所有已经发出去的帧全部重传一遍。
“后退 N 帧”这个名字就是从这儿来的——一个帧出错,发送方要后退到出错的那个位置,把后面最多 N 个帧重新走一遍。这个 N 就是窗口长度。理解了这个命名,后面很多看似奇怪的行为就顺了:接收方的窗口大小恒等于 1,因为它在任何时刻只接受一个特定序号的帧,多一个都不要。
1.3 GBN 的三条铁律
把 GBN 的行为压缩,可以归纳成三条不可动摇的规则。第一条,发送方可以连续发送窗口内的多个帧,接收方窗口大小恒为 1。这条决定了接收端的实现极其简单,一个变量就够。
第二条,采用累积确认。接收方回一个ACK n,表达的意思不是“我收到了第 n 帧”,而是“第 n 号帧之前的所有帧我都收齐了,我现在期待第 n 号帧”。这个语义差异是很多初学者第一个栽跟头的地方——ACK n确认的是n−1及之前,而不是n本身。
第三条,发送方只维护一个定时器,为最老的未确认帧计时。这和后来的选择重传协议要为每个未确认帧各配一个定时器形成鲜明对比。单定时器的设计让发送端实现简单了一大截,但代价是一旦超时,重传的范围是整个窗口,而不是单个帧。
这三条规则合在一起,构成了 GBN 全部的行为逻辑。后面要聊的弊端、窗口边界、性能问题,都是这三条规则的直接推论。
2. 发送方与接收方各自在做什么:一次丢帧的完整回放
把 GBN 讲清楚,光靠规则描述是不够的,必须把一次真实的丢帧过程逐拍推演一遍。我在第一次学这块的时候,教材上那段文字来回看了三遍还是糊的,后来自己拿纸把每一拍的状态列成表,才真正搞明白“回退”到底退到哪、浪费在哪。
2.1 发送方手里的三样东西:base、nextseqnum、单一定时器
发送方需要维护的变量其实很少,但每一个都有明确职责。base记录当前窗口的左边界,也就是最老的、尚未被确认的帧序号;nextseqnum记录下一个要发出去的帧序号。窗口内的序号区间就是[base, base + W − 1],其中W是窗口长度。任何时刻,nextseqnum落在base和base + W之间。
发送方要响应三类事件。第一类是上层应用交下来一个待发数据,如果nextseqnum < base + W,说明窗口还有空位,直接封装成帧发出去,nextseqnum加一;如果窗口满了,要么拒收要么缓存,等窗口滑动再说。第二类是收到一个序号为 n 的确认,这时要做一次判断:如果n > base,说明这个确认带来了新信息,把base更新成n,窗口整体右移;如果n ≤ base,说明是重复确认或者过期确认,直接忽略。第三类是超时,这时要把[base, nextseqnum − 1]区间内所有已发未确认的帧全部重传一遍。
注意第三类事件的重传范围——不是只重传base那一个帧,而是从base一路重传到当前已经发出去的最远位置。这就是“回退 N 帧”在代码层面的具体体现。还有一点容易忽略:定时器的重启时机。每当收到一个有效的新确认(n > base)并且窗口里还有未确认帧时,定时器要重新开始计时;如果窗口已经空了(base == nextseqnum),就把定时器停掉。
2.2 接收方只有一个变量:expectedseqnum
接收方的简单程度经常让人不适应。它只需要一个变量expectedseqnum,表示“我下一个想收到的帧序号是多少”,初始值为 0。收到一个帧之后只有两种情况。
第一种,帧的序号正好等于expectedseqnum。这就是按序到达,把数据交给上层,expectedseqnum加一,然后回一个累积确认ACK expectedseqnum(注意是加一之后的值,表示新的期待)。第二种,帧的序号不等于expectedseqnum,也就是乱序。这时丢弃该帧,不缓存,然后重新发送上一次发出的那个确认。这个重复确认的作用是提醒发送方:“我这边还卡在原地,你那边别往前滑了。”
把这两段对照着看会发现一个很有意思的不对称:发送方要维护窗口、要判断、要重传,逻辑一大堆;接收方一个变量、两个分支就完事。这个不对称正是 GBN 的设计哲学——把复杂度压在发送端,换取接收端的极致简单。在早期硬件资源紧张的年代,这个取舍非常有吸引力。
2.3 帧 2 丢失后的逐拍推演
设发送窗口W = 4,序号用 3 位表示,取值范围 0 到 7。发送方连续发出 0、1、2、3 四帧,其中 2 号帧在链路上丢失了,其余正常到达。下面这张表是逐拍推演。
| 时刻 | 事件 | 发送方状态(base / nextseqnum) | 接收方状态(expectedseqnum) | 说明 |
|---|---|---|---|---|
| T1 | 发出 0,1,2,3 | 0 / 4 | 0 | 窗口填满,停发 |
| T2 | 收到 0 号帧 | 0 / 4 | 0 → 1 | 按序,交付,回 ACK 1 |
| T3 | 收到 1 号帧 | 0 / 4 | 1 → 2 | 按序,交付,回 ACK 2 |
| T4 | 收到 3 号帧 | 0 / 4 | 2 | 乱序,丢弃,重发 ACK 2 |
| T5 | 发送方收到 ACK 1 | 0 → 1 / 4 | 2 | 窗口滑到 [1,4],可发 4 |
| T6 | 发送方收到 ACK 2 | 1 → 2 / 4 | 2 | 窗口滑到 [2,5],可发 5 |
| T7 | 发送方发出 4,5 | 2 / 6 | 2 | 窗口再次填满 |
| T8 | 收到 4 号帧 | 2 / 6 | 2 | 乱序,丢弃,重发 ACK 2 |
| T9 | 收到 5 号帧 | 2 / 6 | 2 | 乱序,丢弃,重发 ACK 2 |
| T10 | 2 号帧超时 | 2 / 6 | 2 | 重传 2,3,4,5 |
| T11 | 收到 2 号帧 | 2 / 6 | 2 → 3 | 按序,回 ACK 3 |
| T12 | 收到 3 号帧 | 2 / 6 | 3 → 4 | 按序,回 ACK 4 |
| T13 | 收到 4 号帧 | 2 / 6 | 4 → 5 | 按序,回 ACK 5 |
| T14 | 收到 5 号帧 | 2 / 6 | 5 → 6 | 按序,回 ACK 6 |
这张表里最值得盯着看的是 T10 那一刻。发送方重传的是2、3、4、5四帧,而 2 号帧之后到 5 号帧之间的内容,链路上其实已经传过一次了——3 号在 T4 到达过(被丢弃),4 号和 5 号在 T8、T9 到达过(也被丢弃)。真正需要重传的只有 2 号。剩下三帧的第一次传输,全是白跑。
这就是 GBN 的成本结构:一次丢帧,代价是后面所有已经发出去的帧跟着重来。窗口越大,跟着重来的越多。这个结论在后面的性能分析里会变成一个具体的乘数因子,先记在这儿。
2.4 单定时器设计背后的取舍
为什么 GBN 只用一个定时器?如果给窗口里每个已发帧各配一个定时器,超时之后只需要重传出错的那一个,效率明显更高。但早期实现里,硬件定时器是稀缺资源,一个网卡上能并行跑的定时器数量有限,每帧一个定时器在高速链路上根本撑不住。
单定时器的逻辑是:只给base那个帧计时,它的确认一到,如果窗口里还有未确认帧,就把定时器挪到新的base上重新计时。这样做的好处是实现简单、资源占用恒定;坏处是定时器一旦触发,无法精确定位是哪个帧出的问题,只能保守地把整个窗口重发一遍。
这个取舍在今天的软件实现里已经不那么必要了,所以现代协议往往会走选择重传的路子。但作为理解滑动窗口演进的中间站,GBN 的单定时器模型非常有教学价值——它把“简单性”和“效率”的权衡摆在了明面上。
3. 窗口长度为什么卡在 2^n−1:一道反例讲透
如果只用 n 位二进制给帧编号,序号空间就是2^n个值,从 0 循环到2^n − 1再回到 0。后退N帧协议的发送窗口最大只能取2^n − 1,比序号空间少一个。这个“少一个”的规则,是考试里的高频考点,也是理解窗口与序号关系的核心。
3.1 序号空间、发送窗口、接收窗口三者的关系
三个量之间的关系可以用一个不等式概括:发送窗口 + 接收窗口 ≤ 序号空间。对 GBN 来说,接收窗口恒为 1,所以W ≤ 2^n − 1。对选择重传协议来说,发送窗口和接收窗口一般取相等,所以是W ≤ 2^n / 2 = 2^(n−1)。这组关系背后的逻辑是同一个:窗口内出现过的序号,不能和窗口将要使用的新序号撞车。
为什么撞车会出事?因为接收方区分“这是新帧”还是“这是重传的旧帧”,唯一的依据就是序号。如果新旧帧的序号可能相同,接收方就没有任何办法区分,只能靠猜。而协议是不能靠猜的。
W ≤ 2^n − 1这个约束的本质是:让发送窗口内的序号集合,在整个序号空间里至少留下一个空位。这个空位的作用是给接收方一个“安全参照”——只要接收方的期待序号落在窗口外面,它就不会把重传的旧帧误当成新帧。
3.2 把窗口开到 2^n 会撞上什么:一个 2 位序号的完整反例
光看不等式没有体感,必须构造一个反例让错误真实发生。取n = 2,序号空间是{0, 1, 2, 3},共 4 个值。现在故意违规,把窗口开到W = 4,也就是把整个序号空间都占满。
第一步,发送方连续发出 0、1、2、3 四帧,窗口填满。第二步,这四帧全部正确到达接收方,接收方一帧一帧交付给上层,expectedseqnum从 0 涨到 4,模 4 之后回绕成 0。也就是说,接收方现在期待的是 0 号帧。第三步,假设这四帧的确认在返回途中全部丢失了。第四步,发送方那边base还停在 0,超时之后重传 0 号帧。
关键就在第五步。接收方收到这个 0 号帧,看到它的序号正好等于自己期待的 0,于是判定为“新帧”,把它交付给上层。但实际上,这个 0 号帧是第一次传输的 0 号帧的副本,上层已经收过一遍了。重复交付发生了,而可靠传输协议最基本的承诺就是“不重复、不丢失”。
如果窗口取W = 3(即2^2 − 1),同样的场景会怎样?发送方发出 0、1、2 三帧,接收方全部收到,expectedseqnum变成 3,模 4 之后还是 3。此时发送方超时重传 0 号帧,接收方一看序号 0 不等于期待的 3,立刻判定为乱序帧,丢弃并重发ACK 3。发送方收到ACK 3,知道 0、1、2 都已经被正确接收,把base更新为 3,窗口滑到[3, 5],重传的 0 号帧被正确识别为冗余。整个流程没有出错。
差别就出在那一个空位上。W = 3时,接收方期待的 3 号帧处在窗口[0, 2]之外,新旧序号永远不会重叠。W = 4时窗口覆盖了全部序号,接收方的期待序号必然落在窗口内部,重叠就不可避免。
3.3 余量证明的另一种看法:窗口外必须留一个“安全的空位”
把上面的反例一般化,可以得到一个更形式化的表述。设发送窗口为[base, base + W − 1],这个区间在模2^n意义下覆盖W个序号。接收方的期待序号expectedseqnum在这个区间之外的充要条件是W < 2^n。只要W ≤ 2^n − 1,区间外面就至少还剩一个序号,接收方的期待值一旦落到这个剩余区域,任何重传的旧帧都会被判定为乱序而丢弃——这正是我们想要的行为。
反过来,如果W = 2^n,窗口覆盖整个序号空间,不存在“窗口外”这个概念,接收方的期待序号永远在窗口内,重传帧和期待帧的序号必然可能相同,错误就不可避免。
还有一个更直观的说法:发送方在没有收到任何新确认的情况下,能够连续发出的最大帧数就是W。为了让接收方能够区分“第 k 轮发送”和“第 k+1 轮重传”,两轮之间的序号必须错开至少一个位置。序号空间有2^n个位置,一轮占用W个,剩下2^n − W个必须至少为 1,所以W ≤ 2^n − 1。这个推导和反例是同一件事的两种说法,考试时用哪个都行。
注意:
W = 2^n − 1是上限,不是必须取满。实际协议设计中,窗口取多大还要看带宽时延积、接收端缓冲能力和序号位数的成本,2^n − 1只是在给定序号位数下允许的最大值。
4. 窗口长度类习题的四种常见问法与解法
这块内容在各类题目里出现的频率极高,题型变化不多,但每个题型都有固定的坑点。我把它们整理成四类,配上解题路径和容易翻车的地方。
4.1 第一种:给定序号位数,求窗口上限
这是最直接的一类。题干给出“序号字段占 n 位”,问发送窗口最大能取多少。解法就是把W ≤ 2^n − 1直接套进去。
几个常见数值必须记熟:n = 3时上限是 7;n = 4时上限是 15;n = 5时上限是 31;n = 8时上限是 255。这几个数字在实际题目里反复出现,记熟了能省不少时间。
这里最典型的坑是把2^n当成答案。题目越是简单,越容易手滑。我见过不少人前面推导写得头头是道,最后一步填了个2^n。建议在草稿上写成2^n − 1,把减号画粗一点,形成肌肉记忆。
4.2 第二种:给定窗口大小,反推最少序号位数
反过来问:要让发送窗口达到 W,序号至少需要几位?解法是解不等式2^n − 1 ≥ W,即2^n ≥ W + 1,然后对W + 1取以 2 为底的对数向上取整。
举几个例子。W = 8,需要2^n ≥ 9,n = 3时2^3 = 8 < 9不够,取n = 4,此时上限 15,够用。W = 7,需要2^n ≥ 8,n = 3正好,因为2^3 − 1 = 7恰好等于 8。这个边界值特别容易算错——W = 7用 3 位是够的,因为 3 位能开出最大 7 的窗口,刚好卡满。
这类题的核心是不要把W和W + 1搞混。判据是2^n − 1 ≥ W,移项得2^n ≥ W + 1。如果直接用2^n ≥ W去算,W = 8会得出n = 3(因为2^3 = 8),但 3 位序号的窗口上限只有 7,装不下 8,就错了。
4.3 第三种:结合信道参数算利用率
这类题给出链路速率、帧长、传播时延、窗口大小,要求算信道利用率。解题分三步走。
第一步算t_f = 帧长 / 链路速率,单位要统一,通常换成微秒或毫秒。第二步算a = t_p / t_f,注意t_p是单向传播时延,如果题目给的是往返时延 RTT,要先除以 2。这一步是重灾区,很多题故意给 RTT 来诱惑你直接把 RTT 当2t_p之外的量处理。第三步判断W和1 + 2a的大小关系:W ≥ 1 + 2a时利用率取 1(链路跑满);否则用η = W / (1 + 2a)。
举个完整的算例。链路速率 100 Mbps,帧长 1500 字节即 12000 bit,单向传播时延 1 ms,窗口W = 20。先算t_f = 12000 / 1e8 = 120 微秒,a = 1000 / 120 ≈ 8.33,1 + 2a ≈ 17.67。因为W = 20 > 17.67,所以利用率取 1,链路可以跑满。如果窗口只有 10,那η = 10 / 17.67 ≈ 56.6%。
这一步的判断逻辑要讲清楚:1 + 2a的物理含义是“让链路刚好不空闲所需的最小窗口”。第一个帧从开始发送到它的确认回来,需要t_f + 2t_p = t_f(1 + 2a)的时间;在这段时间里,发送方要能连续发出足够多的帧把链路填满,需要的帧数就是1 + 2a。窗口小于这个数,链路就会出现空闲气泡,利用率按比例打折。
4.4 第四种:比较停等与 GBN 达到同样利用率所需的窗口
这类题的问法通常是:“要让 GBN 的利用率达到停等协议的 k 倍,窗口至少取多少?”或者“给定窗口,GBN 相对停等提升了多少倍?”
解法很直接。停等的利用率是1 / (1 + 2a),GBN 是W / (1 + 2a),两者相除就是W。所以在W < 1 + 2a的区间内,GBN 相对停等的提升倍数恰好等于窗口大小。窗口取 10,就是 10 倍;窗口取 31,就是 31 倍。
这个结论有个隐含前提:W ≤ 1 + 2a。如果窗口已经超过1 + 2a,GBN 的利用率封顶在 1,再增大窗口也不会带来提升,这时提升倍数就是1 + 2a倍,而不是W倍。题目如果只给窗口不给链路参数,很可能漏掉了这个上限,答的时候最好把两种情况都写出来。
4.5 习题里最容易翻车的几个细节
总结下来有四个高频错误点,按踩坑频率排序。
第一个是累积确认的语义。ACK n表示n−1及之前的帧全部收到,不是“收到第 n 帧”。问“收到 ACK 5 时发送方窗口滑到哪里”这类题,答案要把base更新为 5,而不是 4 或 6。判断标准就一句话:ACK n之后,发送方期待被确认的最老帧就是 n。
第二个是单向与往返时延混淆。公式里的a用的是单向传播时延,题目给 RTT 时务必先除以 2。有些题目给的是“端到端时延”,这个词也有歧义,要看上下文,通常指单向,但也可能指往返。我遇到过不少题干本身写得含糊的,这种情况按单向理解,并在答案里注明假设。
第三个是窗口上限的边界值。W = 2^n − 1是可以取到的,W = 2^n不行。n = 3时窗口最大 7,取 7 合法,取 8 违规。边界题最忌讳想当然。
第四个是利用率封顶。W / (1 + 2a)算出来大于 1 时,答案就是 1,不能写个 120%。物理上不可能超过 100%,数学上要记得min一下。
5. 性能账本:理想效率、差错放大与那条分水岭
讲完规则和习题,得回到GBN协议性能分析这个正题上。GBN 的性能有两个完全不同的面孔:无差错时它几乎完美,有差错时它塌得比想象中快。这个反差是理解它适用场景的关键。
5.1 无差错时的利用率:W/(1+2a) 是怎么来的
无差错情况下,推导很干净。发送方连续发出W个帧,耗时W × t_f。这W个帧的确认会陆续回来,但只要W ≥ 1 + 2a,第一批确认到达时发送方还在发后面的帧,链路始终不空闲,利用率就是 1。
如果W < 1 + 2a,情况就变成“发完一批等一会儿”。一个批次的总周期是t_f + 2t_p,其中有效传输时间是W × t_f,所以:
η = W × t_f / (t_f + 2t_p) = W / (1 + 2a)这个公式在W = 1时退化成1 / (1 + 2a),正好是停等协议的公式。这也说明停等协议是 GBN 在W = 1时的特例,两者不是并列关系,而是包含关系。
再回到开头那组数字:1 Gbps 链路、20 ms 单向传播时延、8000 bit 帧长,a = 2500,要让链路跑满需要W ≥ 5001。此时对应的序号位数:2^n − 1 ≥ 5001,即2^n ≥ 5002,n = 13时2^13 = 8192够了,所以 13 位序号。这个结果说明:在高速长距链路上,窗口必须开到几千的量级才有意义。这也解释了为什么实际协议里序号字段动辄 32 位——不是浪费,是真需要。
5.2 引入差错率 p 之后:1+(W−1)p 这个放大因子
一旦引入差错,账本就完全变了。设每个帧独立地以概率p出错或丢失(为简化,忽略确认帧本身出错的情况)。考虑一个窗口里正在传的W个帧,如果第k个帧出了错(k从 1 数到W),那么从k开始往后的所有帧都要重传,重传数量是W − k + 1。
对所有可能的出错位置求平均,重传数量的期望是(W + 1) / 2。这个数字的含义很直观:如果错误均匀分布在窗口内,平均来说错误会出现在窗口正中间,那么后半截都要重来。
把这件事换个角度看,每一个成功交付的帧,平均需要被发送约1 + (W − 1)p次。这个因子就是 GBN 的“差错放大系数”。当p很小的时候它接近 1,没什么感觉;当p × W到了一定的量级,它就开始咬人了。
把它塞进利用率公式:
η ≈ W × (1 − p) / [(1 + 2a) × (1 + (W − 1) × p)]这个式子要做两处封顶:一是整体不超过 1;二是在p = 0时退化成W / (1 + 2a),逻辑自洽。当W = 1时它退化成(1 − p) / (1 + 2a),也就是停等协议在有差错情况下的效率——一帧传错了就重传这一帧,不存在“连累别人”的问题,所以放大系数是 1。
需要说明的是,这个公式属于工程近似模型,假设了帧错误独立、忽略确认帧错误、忽略重传帧带来的额外传播时延等细节。实际做题和工程估算用这个够了,精确推导要复杂得多。考试时按这个模型作答,思路和结果都是对的。
5.3 一组数值算例:W 从 1 到 31 的性能曲线
抽象公式不如数字直观。设a = 10,即1 + 2a = 21,取定序号位数 5 位(窗口上限 31),看不同窗口和不同差错率下的表现。
| 窗口 W | p = 0 | p = 0.001 | p = 0.01 | p = 0.05 |
|---|---|---|---|---|
| 1(停等) | 4.8% | 4.8% | 4.7% | 4.5% |
| 4 | 19.0% | 18.9% | 18.3% | 15.8% |
| 8 | 38.1% | 37.8% | 35.2% | 26.8% |
| 16 | 76.2% | 75.0% | 63.3% | 37.2% |
| 21 | 100% | 98.0% | 76.9% | 41.2% |
| 31 | 100% | 97.0% | 68.3% | 32.5% |
表格里有几件事值得单独拎出来说。
第一,p = 0那一列,W = 21就到顶了,再往上加窗口毫无意义。这印证了1 + 2a这条分水岭。
第二,p = 0.001那一列,W = 31反而比W = 21略低。这不是算错,而是真实规律:在存在差错的情况下,窗口开得过大反而会拖累性能。窗口越大,一次出错连累的帧越多,1 + (W − 1)p这个因子涨得比W快,最终把收益吃掉了。
第三,p = 0.05那一列,最优窗口出现在 20 附近,之后一路下滑。这说明在高误码链路上,GBN 存在一个最优窗口,不是越大越好。这个结论在实际组网时非常有用:无脑调大窗口,在质量差的链路上可能适得其反。
第四,从W = 1到W = 8,即使p = 0.05,效率也从 4.5% 涨到 26.8%,提升 6 倍。说明适度的流水线化在任何情况下都是划算的,问题只出在“过度”。
5.4 GBN、停等、选择重传的三方对照
把三个协议放在一起看,各自的位置就清楚了。
| 维度 | 停等协议 | 后退N帧(GBN) | 选择重传(SR) |
|---|---|---|---|
| 发送窗口 | 1 | W ≤ 2^n − 1 | W ≤ 2^(n−1) |
| 接收窗口 | 1 | 1 | 与发送窗口相同 |
| 接收端缓存 | 不需要 | 不需要 | 需要,缓存乱序帧 |
| 确认方式 | 逐帧确认 | 累积确认 | 逐帧确认 |
| 定时器数量 | 1 | 1 | 每个未确认帧一个 |
| 出错重传范围 | 单个帧 | 出错帧及之后所有已发帧 | 仅出错帧 |
| 无差错效率 | 1/(1+2a) | W/(1+2a),封顶 1 | W/(1+2a),封顶 1 |
| 高误码表现 | 一般 | 差 | 好 |
| 实现复杂度 | 最低 | 低 | 高 |
这张表里最容易被忽略的是序号空间那一行。GBN 的窗口上限是2^n − 1,SR 的窗口上限是2^(n−1),SR 反而更“费序号”。原因是 SR 的接收窗口不是 1,它也要缓存乱序帧,所以接收方也要用序号来区分新旧,占用的空间必须一起算进不等式发送窗口 + 接收窗口 ≤ 2^n,两边各分一半,各得2^(n−1)。
这个对比也解释了为什么 GBN 在早期很受欢迎:它用最少的序号空间、最简单的接收端实现,换来了远高于停等的吞吐。在误码率极低的有线链路上,这个交换非常划算。
6. 三个硬伤与它们的现实代价
讲完性能,得把 GBN 的弊端摆到台面上。它能在教学中占据重要位置,但真正在工程里大规模用的时候,问题都是实打实的。
6.1 硬伤一:重传放大
第一个弊端就是前面反复提到的重传放大。一个帧出错,后面所有已发未确认的帧全部重来。窗口开到 30,错一个帧平均要重传 15 个,这个乘法关系在高带宽时延积链路上会变成实打实的带宽浪费。
举个极端些的数字。链路 1 Gbps,帧长 8000 bit,窗口取 5001(就是前面算出能跑满的那个值),帧错误率p = 0.0001(万分之一)。代入公式,η ≈ 5001 × 0.9999 / (5001 × 1.5) ≈ 66.7%。也就是说,仅仅万分之一的帧出错率,就把一条本来能跑满的链路压到了三分之二。如果p升到 0.001,效率会掉到 30% 出头。而链路的误码率往往是受物理条件限制的,压不下去,这个损失就是硬损失。
这里面还有个隐性成本:重传的帧会挤占本可以用来发新帧的带宽,形成正反馈——传得越多、错得越多、重传越多。在接近饱和的链路上,这个正反馈会放大拥塞。
6.2 硬伤二:接收端不缓存
GBN 的接收端不缓存乱序帧,这是它“简单”的来源,也是它“浪费”的来源。前面推演里 3、4、5 三个帧明明已经完整无误地到达了接收端,却因为前面缺了一个 2 号帧而被整批丢掉,后面还得再传一遍。
从信息论的角度看,这三个帧携带的信息已经成功到达了,扔掉它们等于把已经付过成本的成果主动作废。选择重传协议正是针对这一点做的优化:接收端把乱序帧缓存起来,等缺口补上之后,按序一次性交付给上层,已经到过的帧不必再传。
这个差别在突发性丢帧的场景下尤其明显。如果链路上丢的不是孤立的一帧,而是一连串(比如一段链路抖动),GBN 的放大效应会被叠乘,而 SR 因为缓存了后面已经到的帧,只需要重传缺口那一段。
6.3 硬伤三:累积确认的连带效应
累积确认的好处是确认帧可以合并,节省反向带宽——不必为每一帧单独发一个 ACK。但代价是,一个确认帧丢失的损失比在逐帧确认协议里更大。
设想发送方发了 0 到 9 共十帧,接收方全部正确收到,回了ACK 1到ACK 10。如果ACK 9和ACK 10在返回途中丢失了,发送方只知道收到了ACK 8,窗口只滑到 8,9 和 10 这两个已经被正确接收的帧还挂在窗口里。等定时器超时,它们会被重传一遍,而接收方会把它们当作重复帧再丢弃一次。
这个场景揭示了一个不对称:累积确认让正向的确认信息可以压缩,但压缩之后每次丢失的代价被放大了。在反向链路质量好的时候这不是问题,反向链路拥塞或者无线环境下,这个放大器就很有杀伤力。
还有一个更隐蔽的问题:GBN 的重复确认机制在丢帧时会连续产生多个相同的 ACK。发送方收到的这些重复 ACK 里没有任何新信息,只能干等超时。有些实现会利用“收到若干个重复 ACK”来快速重传,那是后来在具体协议里加上的优化,不属于 GBN 本身。
6.4 什么时候该果断换成选择重传
判断要不要从 GBN 换到选择重传,看三个信号。
第一个信号是误码率或丢包率。如果是干净的有线链路,p在1e-6以下,GBN 的重传放大基本可以忽略,用它完全没问题,实现还简单。一旦p到1e-3的量级,或者链路是无线的、有移动性的,放大系数会变成 2 甚至更高,这时候 SR 的收益就非常明显了。
第二个信号是带宽时延积。窗口必须开得很大的链路上,一次错误的绝对代价很高(要重传几十上百个帧),GBN 的放大效应会体现得特别残酷。而窗口本来就很小的场景,两者差别不大,没必要为了 SR 增加接收端的缓存管理成本。
第三个信号是接收端资源。SR 需要缓存乱序帧,