☰
滑动窗口、后退N帧与选择重传:数据链路层可靠传输详解
2026/10/26 3:46:33 网站建设 项目流程

这套系列课跟到第 8 天,进度条正好卡在第三章数据链路层最“费脑子”的地方:3.4 节的第二个部分。上一节把停止-等待协议翻来覆去讲透了,一帧发出去、等确认、超时重传,逻辑简单到几乎不用动脑;可一旦把这套东西放到真实链路上,问题立刻暴露——发送方大部分时间在干等,链路明明有 1Gbps 的带宽,实际吞吐可能只有几 Mbps。这一节要解决的,就是怎么把“一发一等”改成“流水线作业”。核心关键词说穿了就三个:滑动窗口、后退 N 帧(GBN)、选择重传(SR)。它们共同构成了数据链路层的可靠传输机制,决定了帧丢了怎么补、补多少、补完之后窗口怎么滑。这篇内容适合两类人:一类是正在啃这门课、被“窗口大小为什么是 2^n-1”卡住的同学;另一类是已经工作几年、天天调网络但没系统梳理过 ARQ 细节的同行。我会把公式推导、窗口数量的计算过程、协议状态机的每个变量,还有我自己写模拟程序时的踩坑记录全部摊开讲,代码可以直接复制跑起来。

1. 3.4 节第二部分的定位:滑动窗口到底在解决什么问题

1.1 从“帧丢了怎么办”倒推出的三个待答问题

数据链路层做的事,说白了就一句话:把网络层交下来的数据包封装成帧,然后在相邻两个节点之间可靠地送过去。注意“相邻”这两个字,它不是端到端的,只是链路上的一跳。可就是这一跳,会出各种乱子。我在纸上列过一张清单,实际链路会发生的坏事无外乎四类:帧在传输途中被噪声打坏,接收端校验和算不过,这是比特差错;帧整块消失在链路里,接收端压根没收到,这是帧丢失;接收端收到了,也回了确认,但确认帧在路上没了,发送方以为没送到又重发一遍,这是帧重复;还有一种是物理层多路径或者缓冲调度导致的到达顺序错乱,这是帧失序。

面对这四种情况,协议设计者必须回答三个问题:接收端怎么判断这一帧是新的还是重复的?发送方怎么知道哪一帧该重传?重传的时候是只补那一帧,还是把它后面的一串都补上?前两个问题靠序号 + 确认 + 超时定时器这套组合拳解决,第三个问题就是这一节的分水岭——补一帧的叫选择重传,补一串的叫后退 N 帧。

这里有个容易被忽略的认知:序号不是无限长的,它存在帧头里,占几个比特就是几个比特。3 个比特只能表示 0 到 7 共 8 个序号,用完得绕回来。这个“绕回来”的动作,直接决定了窗口能开多大,也是后面所有公式的源头。

1.2 链路层和传输层都在做可靠传输,谁该负责

我在第一次学的时候特别困惑:传输层的 TCP 不是已经保证了可靠吗,链路层再搞一套重传,不是重复劳动吗?后来查资料加上自己琢磨,才算想明白。TCP 的可靠是端到端的,它管的是从我这台机器到对面那台机器,中间经过多少个路由器、多少段链路,它一概不关心。可有些链路的误码率就是高得离谱,比如早期的铜缆、无线链路、卫星链路,误码率能到 10^-3 甚至更差。如果这种链路上不做事先的纠错和重传,让错帧一路往上抛,最后靠 TCP 的重传兜底,那代价是什么?是整条端到端路径上所有中间节点都白转发了一遍,是拥塞窗口被无谓地打小,是时延成倍放大。

所以工程上的分工很清楚:误码率低的光纤链路,链路层基本不做重传,错了就丢弃,交给上层;误码率高的链路(无线、电力线、卫星),链路层必须做本地重传,把错误在最小范围内消化掉。这也是为什么你在有线以太网的实际抓包里,几乎看不到链路层的重传行为,因为现在的双绞线和光纤质量太好了,误码率低到 10^-12 量级,做重传反而是浪费。

注意:考试和面试里经常问“链路层可靠传输是不是必须的”,标准答案是“不是必须的,取决于链路质量”。别一上来就说“必须”,那是把教材的理想模型当成了工程现实。

理解了这层分工,再看滑动窗口就有味道了:它不是为了让网络“更可靠”,而是为了让“可靠”这件事更便宜。停止-等待也能保证可靠,代价是吞吐惨不忍睹;滑动窗口用同样的可靠性,换来了几十上百倍的吞吐提升。这笔账,下一节我算给你看。

2. 先算一笔账:停止-等待协议的天花板在哪里

2.1 一次发送要等多久:信道利用率的完整推导

要理解窗口存在的意义,必须先量化停止-等待有多亏。我把这个过程拆成时间轴:发送方把一帧的最后一个比特推上链路,耗时记为Td(发送时延);这一帧在链路上传播到接收方,耗时Tp(传播时延);接收方处理完、生成确认帧、再推上链路,确认帧传回发送方,又耗时Tp。所以从发送方开始发送,到它收到确认、可以发下一帧,中间的总时间是Td + 2Tp(这里忽略确认帧本身的发送时延和处理时延,因为它们通常远小于数据帧)。

于是信道利用率就是:

U = Td / (Td + 2Tp)

这个公式很朴素,但它揭示的东西很残酷。取一组接近现实的参数:链路速率 1Gbps,帧长 1200 字节,也就是 9600 bit,那么 Td = 9600 / 1e9 =9.6 微秒。假设这条链路的往返时延 RTT = 2Tp =2 毫秒。代进去:

U = 9.6 / (9.6 + 2000) ≈0.478%

也就是说,一条 1Gbps 的链路,用停止-等待跑到满负荷,实际有效吞吐只有 1Gbps × 0.478% ≈4.78Mbps。剩下 99.5% 的时间,发送方在干什么?在发呆。链路在干什么?在空转。这就是为什么必须上流水线。

我把这个计算过程单独拎出来强调,是因为很多人背下了公式却不会用。你可以记住一个判断法则:当 Td 远小于 RTT 时,停止-等待就是灾难。反过来,如果链路速率很低、帧很长,比如 9600bps 的串口发 1500 字节的帧,Td 有 1.25 秒,RTT 才几十毫秒,那停止-等待的利用率反而接近 100%,根本不值得上窗口。这个判断在选型时非常实用。

2.2 把等待时间填满:窗口数量怎么算出来

既然发送方在等确认的这段时间里闲着,那就让它多塞几帧出去。塞多少?刚好把“等待时间”填满就行。这就是窗口大小的物理含义:窗口 N 表示发送方最多可以连续发出 N 个还没被确认的帧。

从时间轴上看,如果连续发 N 帧,总发送时间是 N×Td。只要这个时间大于等于 Td + 2Tp(第一帧的确认回来之前,发送方一直有活干),链路就不会空闲。所以:

N × Td ≥ Td + 2Tp,解得N ≥ 1 + 2Tp/Td

回到上面的参数:N ≥ 1 + 2000/9.6 ≈ 209.3,向上取整N = 210。也就是说,窗口开到 210 帧,这条链路才能被真正喂饱,利用率逼近 100%。停止-等待相当于 N=1,利用率 0.478%,两者差了 209 倍,这就是流水线的价值。

这里还有一个更直观的等价算法,叫带宽时延积(BDP):链路上“能装下”多少比特的数据 = 链路速率 × RTT = 1e9 × 2e-3 = 2×10^6 bit = 250KB。换算成 1200 字节的帧,正好 208.3 帧,和上面的 210 基本吻合(差异来自第一帧自身的发送时延)。我个人更推荐用 BDP 来估窗口,因为它物理意义明确:窗口要能覆盖“管道”里的容量,链路才不会断流。

提示:这两个算法在考试里经常只给一个,要会互相换算。BDP 算出来的是“在途比特数”,除以帧长得到帧数,再加 1 到 2 的余量就是窗口下限。

2.3 序号空间才是真正的约束:2^n 与 2^n-1

算出来窗口要 210,那序号字段给几个比特合适?这里有个坑,很多人以为 2^n ≥ N 就够了,其实不对,取模运算会带来“新旧混淆”。

先看最简单的例子。假设序号只有 1 个比特,能表示 0 和 1。如果窗口开到 2,发送方连发帧 0 和帧 1,两个都到了接收端,接收端都正确接收并回了 ACK0 和 ACK1。结果两个 ACK 全丢了,发送方超时,重发帧 0。接收端一看序号是 0——它刚刚已经收过 0 了,但它无从判断这是“新的帧 0”(序号绕了一圈回来的)还是“旧的帧 0 的重传”,只能照单全收,于是上层收到了一份重复数据。

问题的本质是:窗口大小必须小于序号空间,才能让接收端通过序号区分“新帧”和“旧帧的重传”。对后退 N 帧来说,接收端只按序接收,它的判据是“序号等于期望值就收”,所以约束是:

发送窗口 W ≤ 2^n - 1

减 1 的原因是:如果 W = 2^n,那么窗口的“尾”和“头”会在序号空间里重合,接收端无法区分。比如 n=3,序号 0~7,W 最大只能是 7;一旦开到 8,发送方发出 0~7 全部 8 帧,接收端回 ACK7 后窗口滑动,再发新的帧 0,此时若 ACK7 丢失,发送方重传 0,接收端无法分辨这是新 0 还是旧 0。

对选择重传来说,约束更紧,是W ≤ 2^(n-1)。原因在于 SR 的接收端会缓存乱序到达的帧,接收窗口和发送窗口加起来不能超过序号空间,否则同样会出现新旧混淆。n=3 时 SR 窗口最大只能到 4,是 GBN 的一半。这个“SR 更吃序号”的结论,直接影响了后面第 4 章的选型对比:同样的链路、同样的窗口需求,SR 往往需要比 GBN 多 1 个比特的序号字段。

我把这三组关系整理成表,复习的时候扫一眼就够:

协议发送窗口上限n=3 时最大窗口序号利用率典型代价
停止-等待11极低吞吐极差,实现最简单
后退 N 帧2^n - 17较高一错回退,重传放大
选择重传2^(n-1)4最低需要接收缓存,逻辑复杂

回到我那个 210 帧的例子:GBN 需要 2^n - 1 ≥ 210,也就是 n ≥ 8(2^8-1 = 255)。如果换成 SR,需要 2^(n-1) ≥ 210,n ≥ 9(2^8 = 256,n-1=8 刚好够,所以 n=9)。多一个比特,就是每个帧头多 1 bit 的开销,在 1200 字节的帧上微不足道,但在小帧场景(比如 IoT 的几十字节帧)就值得掂量了。

3. 后退 N 帧协议:累积确认与一错回退

3.1 接收端只有一个变量:期望序号计数器的全部工作

GBN 的接收端简单到令人发指,整个状态机只需要维护一个变量:expected(期望收到的序号)。它的逻辑只有三行:收到的帧序号等于 expected,就收下、交给上层、expected 加 1、回一个累积确认;序号不等于 expected,不管是小了(重复帧)还是大了(乱序帧),一律丢弃,并且重发上一次正确收到的那个帧的 ACK。

这个“只收按序帧”的设计,是 GBN 全部优缺点的根源。优点是接收端不需要任何缓存,收到即交付,内存开销是常数;缺点是一旦某一帧丢了,它后面所有已经正确到达的帧全部作废,白传了。我上课时老师打了个比方,说这就像排队进场,前面一个人票找不到,后面所有人不管有没有票都得退出去重新排,这个比喻虽然糙,但把 GBN 的暴力美学说清楚了。

“重发上一次的 ACK”这个动作还有个专业名字叫累积确认:ACKn 的含义不是“我收到了第 n 帧”,而是“第 n 帧以及它之前的全部帧我都收到了”。累积确认的好处是确认帧可以合并,不用每收一帧回一个 ACK,减少了反向链路的压力;坏处是确认信息“粒度太粗”,一个 ACK 丢失的影响被放大,而且发送方无法知道到底哪一帧出了问题,只能整体回退。

3.2 发送端的三段式逻辑:发送、等 ACK、超时回退

发送端比接收端复杂一些,要维护三个指针:base(窗口左沿,最早未确认的帧)、next_seq(下一个可用的序号)、窗口上限 W。整个运行逻辑可以拆成三段:

第一段是发送。只要 next_seq < base + W 且还有数据要发,就把 next_seq 这一帧发出去,next_seq 加 1。注意这里 next_seq 是模序号空间递增的,到了 2^n 就绕回 0,这个绕回动作在代码里就是一个取模运算,但它是所有边界 bug 的来源,我在第 5 章会专门讲。

第二段是处理确认。收到 ACKn 时,判断这个 n 是不是“新的”(在 base 到 next_seq 之间),如果是,就把 base 推到 n+1。这里有个细节:累积确认的 n 可能比当前 base 还小(延迟到达的旧 ACK),这种要直接丢弃,否则 base 会往回滑,整个窗口就乱了。

第三段是超时处理。发送端为 base 这个帧维护一个定时器,收到新 ACK 就重置。定时器一旦超时,说明 base 这一帧(或者它的 ACK)出问题了,此时把 base 到 next_seq-1 之间的所有帧全部重发一遍,next_seq 回退到 base。这一个动作就是“后退 N 帧”这个名字的由来——一次超时,退回去重发一串。

注意:GBN 只用一个定时器,针对 base 那一帧。这和很多人直觉里的“每帧一个定时器”不一样。用单个定时器是合理的,因为只要 base 被确认了,它后面的帧必然已经被接收端正确按序接收(累积确认的语义保证),所以定时器只需要盯住最老的那个未确认帧。

3.3 重传放大效应:窗口越大,代价越高

GBN 最要命的问题叫重传放大。窗口开得越大,一次丢帧连累的帧数越多。我做过一个粗略的量化估算,虽然不是教材标准公式,但很能说明问题。

假设帧的丢失率为 p,窗口大小为 W。在一个窗口周期内,至少丢一帧的概率约为 1 - (1-p)^W。一旦丢了,平均要重传多少帧?如果丢的是窗口里第 k 个帧,后面 W-k 个都要重传,平均下来约为 (W+1)/2 个。

代入实际数字:W = 210,p = 10^-4(比较好的无线链路)。一个窗口出错概率 = 1 - 0.9999^210 ≈ 2.08%。看起来不高,但每次出错的代价是平均重传约 105 帧。折算下来,每传 210 帧,额外重传的期望帧数约为 0.0208 × 105 ≈ 2.2 帧,额外开销约1%。这个数字可以接受。

但如果链路质量差一点,p = 10^-3,出错概率升到 1 - 0.999^210 ≈ 18.9%,期望额外重传 0.189 × 105 ≈ 19.9 帧,开销接近9.5%。再差一点到 p = 10^-2,出错概率几乎 100%,开销直接爆表,链路基本瘫痪。

这组数字解释了为什么 GBN 在高误码率、大窗口的组合下表现很差,而在低误码率下几乎和 SR 没差别,因为很少出错,重传放大效应根本触发不了。这也解释了工程上的务实选择:链路质量好的时候用 GBN 甚至不用重传,质量差的时候才上 SR。

3.4 GBN 里那些反直觉的细节

有几个点我当初怎么想都想不通,后来动手写了模拟才恍然大悟,这里单独列出来。

第一个,接收端收到乱序帧时,要不要缓存?不要,直接丢。这一条是 GBN 和 SR 的分界线。很多人下意识觉得“都收到了丢掉多浪费”,但 GBN 的设计哲学就是“用带宽换简单”,接收端保持无状态,缓存的事情一概不干。

第二个,发送方收到重复 ACK 怎么办?比如收到了 ACK5 又来一个 ACK5,说明接收端又收到了一次帧 4(重传的),此时 base 已经滑到 6 了,这个 ACK5 是旧的,直接忽略。如果不加这个判断,base 会被错误地拉回去。

第三个,窗口满载但没数据可发时怎么办?这在真实实现里叫“零窗口”问题,发送方只能等待,要么等 ACK 来打开窗口,要么等上层送新数据。考试里一般不考,但写代码时忘了处理会导致死锁。

第四个,ACK 丢失和帧丢失的处理是一样的吗?从发送方角度是一样的,都是超时重传。但从效率上看差别很大:帧丢了,接收端后续帧全部丢弃;ACK 丢了,接收端其实已经正确收到了,发送方重传的那些帧,接收端会当成重复帧全部丢弃,纯属浪费。这也是为什么 GBN 在反向链路质量差的时候效率极低。

4. 选择重传协议:只补那一帧的代价与红利

4.1 接收缓存换来的精准重传

SR 的设计思路很直接:既然 GBN 的问题在于“一错全退”,那就让接收端把乱序到达的帧先缓存起来,只让发送方重传真正丢失的那一帧。为了实现这个,接收端要维护一个接收窗口,窗口内的每个位置对应一个缓存槽和一个标志位(收到了还是没收)。

具体逻辑:收到的帧如果落在接收窗口内且之前没收过,就存进对应的槽位,回一个逐帧确认 ACK(不是累积的);如果这个帧正好是窗口左沿(期望序号),那么窗口整体向右滑动,把连续收到的帧一起交付给上层。发送方那边,收到某个帧的 ACK 就把那一帧标记为已确认,只有超时的帧才重传,其余不动。

这个设计带来的收益是巨大的:丢一帧只重传一帧,窗口内其他已经正确到达的帧全部保留。回到上面的例子,W=210、p=10^-4 的场景,GBN 的额外开销约 1%,SR 的额外开销约 0.01%,差了 100 倍。当然这是在“窗口开满”的理想情况下,实际差距没有这么夸张。

代价也很明确:接收端要为每个窗口槽位准备缓冲区,W 个帧的缓存空间,这在内存紧张的嵌入式设备上是实打实的成本;逻辑复杂度也上去了,乱序交付、窗口滑动、重复帧判断,每一处都容易写错。我自己的模拟程序在 SR 上出的 bug 比 GBN 多了三倍不止。

4.2 窗口大小为什么卡死在 2^(n-1)

这是 3.4 节最容易考、也最容易记混的一个结论。我用一个具体例子把推导过程走一遍。

设 n = 3,序号空间是 0~7 共 8 个。假设发送窗口和接收窗口都开到 5,也就是 W = 5 > 2^(3-1) = 4。现在发送方连续发帧 0、1、2、3、4。接收端全部正确收到,但所有 ACK 都丢了。此时接收端的接收窗口已经滑到 5、6、7、0、1(滑动 5 格),它在等帧 5。

发送方超时,重传帧 0。接收端收到序号 0 的帧,一看在自己的接收窗口里(窗口里有 0),于是当成新帧收下。但实际上这个 0 是旧的 0 的重传,接收端把它当新数据交付给上层,数据重复。

如果 W = 4,情况就不同了。发送方发 0、1、2、3,全部正确接收但 ACK 全丢。接收窗口滑到 4、5、6、7,它在等帧 4。发送方重传帧 0,接收端一看 0 不在接收窗口(4~7)里,判定为旧帧,丢弃。没有重复交付。这就是为什么 SR 的窗口上限是序号空间的一半:发送窗口 + 接收窗口 ≤ 2^n,而 SR 里两个窗口大小相等,所以各占一半。

GBN 之所以能开到 2^n - 1,是因为它的接收窗口大小恒为 1,发送窗口只要满足“和接收窗口加起来不超过序号空间”,也就是 W + 1 ≤ 2^n,即 W ≤ 2^n - 1。这个统一视角比死记两个公式好用得多,考试时记不清,画个数轴就能推出来。

4.3 GBN 与 SR 的正面选型对比

学到这儿,最自然的问题就是:实际系统到底用哪个?我把两者的关键维度列成表,再讲讲背后的取舍逻辑。

对比维度后退 N 帧选择重传
接收端缓存不需要,常数开销需要 W 个帧的缓冲
确认方式累积确认逐帧确认
定时器数量1 个(盯 base)每帧 1 个,或启发式减少
窗口上限2^n - 12^(n-1)
出错代价重传整窗,放大明显只重传丢失帧
实现复杂度低,状态机简单高,乱序处理易错
适合场景误码率低、带宽便宜误码率高、带宽昂贵、RTT 大

选型的核心判据其实是误码率和带宽时延积的组合。低误码率场景,GBN 的放大效应根本触发不了,SR 的复杂度纯属浪费;高误码率 + 大 BDP 的场景(典型就是卫星链路、远距离无线),一帧出错连累上百帧重传,带宽成本极高,SR 的复杂度就是值得的。这也是为什么 TCP 在实现上采用了 SR 的变体(SACK 选项),因为端到端的路径太长、丢包太随机,GBN 的放大效应根本扛不住。

5. 把协议跑起来:一份可复现的模拟实验与参数标定

5.1 最小可运行模型与关键变量

光看书容易产生“我懂了”的幻觉,我建议你用二三十行代码把 GBN 跑一遍,把丢帧、乱序、超时都打印出来看。下面这份是我自己攒的简化模型,去掉了很多工程细节,但协议的骨架是完整的。

import random def simulate_gbn(total_frames=8, loss_rate=0.2, n_bits=3, window=4, seed=2024): random.seed(seed) base = 0 # 窗口左沿,最早未确认的帧 next_seq = 0 # 下一个可用的序号 expected = 0 # 接收端期望的序号 log = [] tick = 0 while base < total_frames and tick < 100: tick += 1 # 第一段:把窗口内所有未发出的帧一次性推出 while next_seq < base + window and next_seq < total_frames: log.append(f"[T{tick}] 发送帧 {next_seq}") next_seq += 1 # 第二段:链路传输,模拟每一帧的随机丢失 delivered = None for seq in range(base, next_seq): if random.random() < loss_rate: log.append(f"[T{tick}] 帧 {seq} 在链路丢失") continue if seq == expected: expected += 1 delivered = seq else: log.append(f"[T{tick}] 帧 {seq} 序号不符(期望{expected}),接收端丢弃") # 第三段:累积确认,或者超时整体回退 if delivered is not None: ack = expected - 1 base = ack + 1 log.append(f"[T{tick}] 收到累积确认 ACK{ack},窗口左沿滑到 {base}") else: log.append(f"[T{tick}] 定时器超时,回退重传 base={base}") next_seq = base return log for line in simulate_gbn(): print(line)

几个变量的含义要盯准:base 和 next_seq 都是“绝对帧号”,不是取模后的序号。这是我自己写的时候特意做的简化,好处是逻辑清晰,坏处是丢失了序号绕回的行为。真正要实现模运算,得把比较逻辑改成(a - b) mod 2^n的形式,不能用简单的<和>直接比,否则序号一绕回,判断全错。这个坑我在第 6 章会展开说。

5.2 逐轮日志解读:丢失、乱序、累积确认

跑一遍,你大概率会看到类似这样的输出(具体内容随 seed 变化):

[T1] 发送帧 0 / 1 / 2 / 3 [T1] 帧 1 在链路丢失 [T1] 帧 0 被正确接收 [T1] 帧 2 序号不符(期望1),接收端丢弃 [T1] 帧 3 序号不符(期望1),接收端丢弃 [T1] 收到累积确认 ACK0,窗口左沿滑到 1 [T2] 发送帧 4 [T2] 定时器超时,回退重传 base=1 ...

从这份日志里能读到三件事。第一,帧 1 丢了,帧 2 和 3 虽然到达但被丢弃,这就是 GBN 的“连坐”,日志里那两行“序号不符”就是证据。第二,接收端只回了 ACK0,因为累积确认只能确认到第一个空洞之前,ACK0 的语义是“0 以及之前的都收到了”,它并没有说“2 和 3 也收到了”,尽管它确实收到了。第三,超时后 next_seq 回退到 base=1,接下来会把 1、2、3、4 全部重发一遍,包括那些之前已经到达过的 2 和 3。

如果你把 loss_rate 调到 0,会看到窗口一路顺畅滑动,日志干净漂亮;调到 0.5,会看到疯狂回退,几乎推不动。这个对比非常直观,比我讲一百遍“重传放大”都管用。

5.3 超时定时器与带宽时延积的标定方法

模拟里我把超时简化成了“这一轮没收到有效确认就超时”,真实实现当然不能这么干。超时时间(RTO)的设定是个技术活,定太短会误判(ACK 还在路上就重传,白费带宽),定太长会让恢复变慢(丢了帧要干等很久才发现)。

我的经验做法是:RTO 取 RTT 的 1.2 到 1.5 倍,再加一个固定余量。链路层的 RTT 相对稳定,不像传输层那样需要复杂的自适应算法。以第 2 章那组参数为例,RTT = 2ms,RTO 取 2.4ms 到 3ms 比较合适。余量是留给接收端处理和确认帧排队的时间,别省。

窗口大小的标定则回到 BDP:W ≈ ceil(BDP / 帧长) + 2。加 2 是为了留一点缓冲,让链路不至于在等待确认的间隙断流。这个公式在考试里够用,在工程里也是个不错的起点,实际系统会根据实测的吞吐再做微调。

提示:如果你的模拟程序跑出来的吞吐远低于理论值,先别怀疑协议,先检查是不是窗口开小了(没喂饱链路),或者 RTO 定得太短(频繁误重传),这两个原因占了九成。

6. 常见问题速查与踩坑实录

6.1 六个高频疑问

问题一:为什么 GBN 的接收端不缓存乱序帧?因为缓存了就变成 SR 了。GBN 的设计取舍就是用“可能重传更多”换“接收端零状态”,两个特性是一体两面,不能只要好处不要代价。

问题二:序号字段能不能只给 1 个比特?能,但窗口只能开到 1,退化成停止-等待。要给 1 bit 又要窗口 2 以上,必然出现新旧帧混淆。

问题三:累积确认的 ACK 丢了,只能靠超时发现吗?是的,这是 GBN 的固有缺陷。后续的 ACK 虽然能继续推进窗口,但如果整批 ACK 全丢,只能等超时。这也是为什么高丢包链路不适合 GBN。

问题四:SR 需要每帧一个定时器吗?理论上需要,否则没法精确知道哪一帧该重传。工程上为了省资源,常用“每个窗口一个定时器 + 启发式判断”的近似方案,牺牲一点精确性换实现简单。

问题五:窗口大小一定要等于 2^n-1 吗?不一定,那是上限。实际窗口可以更小,只是不能更大。窗口太小喂不饱链路,窗口太大会撞上序号空间的约束,实际取值看 BDP。

问题六:链路层既然有重传,上层还要不要重传?要。链路层的重传只管一跳,到了下一跳又可能出错;而且链路层可能压根不做可靠传输(比如以太网)。可靠性最终必须由端到端机制兜底。

6.2 答题与实操速查表

现象 / 疑问可能原因排查或解决思路
吞吐远低于链路速率窗口小于 BDP按 BDP/帧长 重算窗口下限
大量重复帧被上层收到序号空间不足,新旧帧混淆检查 W 是否超过 2^n-1(GBN)或 2^(n-1)(SR)
频繁误重传RTO 小于实际 RTTRTO 提到 RTT 的 1.2~1.5 倍
丢一帧后长时间推不动GBN 重传放大 + 窗口大换 SR,或缩小窗口,或降低误码率
模拟程序序号回绕后逻辑错乱直接比较大小而未取模用 (a-b) mod 2^n 判断前后关系
接收端缓存溢出SR 窗口开太大,缓冲区不够限制接收窗口,或用 GBN 降级

最后再补一个踩坑记录。我第一版模拟程序里,判断“ACK 是不是新的”用的是if ack >= base,看起来没问题,一旦序号绕回就崩了:base 是 7、next_seq 是 1(模 8 之后),来一个 ACK2,用大小比较会判成旧 ACK 丢掉,窗口永远推不动。正确的写法是判断这个 ACK 是否落在“从 base 到 next_seq-1”的模空间区间内。这个 bug 我调了两个小时,最后是靠打印每一轮的 base、next_seq、ack 三个值才定位到的。

我个人在啃这一节时的体会是,公式和结论背下来很快,真正建立直觉还得靠动手跑。把 loss_rate 从 0 调到 0.5,把窗口从 1 调到 2^n-1,把序号位数从 3 改到 2,每改一个参数就盯着日志看它怎么变,那种“哦原来是这么回事”的感觉,是看多少遍推导都换不来的。下一步如果继续往下学,介质访问控制那一块会用到这里的窗口思维,只不过争用的对象从“序号”变成了“信道”,底层逻辑是相通的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询