☰
可靠数据传输原理与工程实践:从rdt1.0到GBN/SR及Python模拟
2026/10/27 11:42:51 网站建设 项目流程

可靠数据传输这五个字,我在教材上第一次看到的时候觉得挺唬人,真正被它折腾到是在自己写模拟器的时候:明明按着状态图画了代码,跑起来还是卡死,日志翻半天才发现是确认报文丢了、发送方在原地干等。这门计算机网络课讲到这一章,是整个课程里第一块真正需要"照着状态机推演"的内容,也是面试里最爱问、实验里最容易翻车的一块。

这篇记录写给两类人:一类是正在跟着课堂进度啃可靠数据传输原理的读者,另一类是想直接上手写代码、把停等协议、回退 N 步、选择重传跑通的人。我会先把问题拆开,再按 rdt1.0 到 rdt3.0 的演进顺序讲清每一步为什么加零件,然后落到滑动窗口的窗口大小约束推导、带宽时延积的实际计算,最后给一份能直接运行的 Python 模拟代码和一份排错速查表。

1. 先把问题拆干净:可靠数据传输到底在跟什么较劲

1.1 理想信道和真实信道之间隔着什么

教材喜欢先假设底层服务是可靠的,再一层层把假设打破,这个顺序初看啰嗦,其实是唯一不让人迷路的方式。真实的传输链路至少会干四件坏事:比特位在传输中翻转(误码)、整个分组在链路上凭空消失(丢包)、分组没丢但是比预期晚很多才到(延迟抖动)、同一个分组被复制成两份抵达接收方(重复)。

我们在网络层以下拿到的就这些东西,没有任何一种物理介质敢承诺"绝对只送达一次、且内容一字不差"。可靠数据传输要做的,就是在这样一个不可靠的下层服务之上,造出一个对上层来说"不丢、不重、不乱、不错"的逻辑通道。上层应用拿到这个通道以后,写代码时就再也不用关心第 37 个字节到底有没有到、是不是到了两次。

我用一个生活化的类比来解释这件事:寄挂号信。普通平信就是不可靠信道,可能丢、可能是空信封;挂号信相当于加了确认机制,寄出后有回执,收件人还得签字。但即便如此,回执本身也可能在路上丢了,收件人签字也可能被写错,所以还得靠编号、靠保留存根、靠超过一定时间就重新寄一份。

1.2 五个零件:校验、序号、确认、重传、定时器

整个可靠数据传输的原理,说到底就是在组装五个零件,后面所有协议的差别只是这五个零件怎么摆放。

  • 校验和:解决"内容对不对"的问题。发送方算一个摘要附在分组里,接收方重算一遍,对不上就断定这个分组在路上坏了,直接丢弃。
  • 序号:解决"这段数据是新数据还是老数据"的问题。每个分组编个号,接收方能分辨重复分组。
  • 确认:解决"对方到底收到没"的问题。接收方回一个确认报文,告诉发送方哪个序号已经安全落地。
  • 重传:解决"没收到怎么办"的问题。发送方在没拿到确认时,把数据再发一遍。
  • 定时器:解决"等多久算没收到"的问题。没有定时器,发送方可能永远等下去;定了定时器,超时就重发。

这五个零件里,最容易被认为是"多此一举"的是序号,但恰恰是它撑起了整栋楼。没有序号,接收方收到一个分组后回了个确认,确认丢了,发送方重传,接收方就会把同一份数据交给上层两次。上层可能是银行转账逻辑,重复一次就是灾难。

我当年第一次做这部分实验就吃过这个亏:为了图省事,只做了"收到就回 ACK、超时就重传",完全没加序号,结果在丢包率设成 20% 的时候,最终写入文件的数据比原始数据多出好几百字节,查了半天才发现是重复写入。

1.3 教材为什么非要一层层递进地讲

很多人看到 rdt1.0、rdt2.0、rdt2.1、rdt2.2、rdt3.0 这五连发第一反应是"直接讲 rdt3.0 不就行了"。我自己的体会是,这套递进其实是在做一件事:每次只引入一个新的复杂度,让读者能看清"这个新问题是谁带来的"。

如果一上来就丢出 rdt3.0 的完整状态机,你会面对"既处理误码、又处理丢包、还要管重复和定时器"的一锅粥,出了问题根本定位不到是哪一层逻辑错了。而按递进顺序走,你脑子里会留下一条非常清晰的因果链:引入确认 → 确认本身会坏 → 所以加重传和序号 → 重传需要触发条件 → 所以加定时器 → 定时器带来性能问题 → 所以上流水线。

这条因果链就是后面理解 TCP 为什么那样设计的基础。TCP 的序号、确认号、超时重传、快重传这些东西,本质上都是这条链上某个节点的工程实现,只是做得更聪明。所以这一章看着像"教科书式的理论推演",实际上是后面所有内容的索引。

2. 从 rdt1.0 一路加到 rdt3.0:每一版都在补前一版的窟窿

2.1 rdt1.0:完全可靠信道下的样板房

rdt1.0 的假设非常简单粗暴:底层信道完全可靠,不丢、不错、不乱序,连延迟都当作固定的。在这个前提下,发送方只有一件事做——把上层交下来的数据打包,发给下层;接收方也只有一件事——从下层收到的分组里取出数据,交给上层。

它的发送端只有两个状态,实际上连状态机都算不上,就是一条直线:等待上层调用 → 打包发送 → 回到等待。接收端同理。这一版存在的意义不是为了实用,而是为了给后面所有版本提供一个"基准骨架":发送方和接收方各自维护一个等待循环,一侧被调用就动作,动作完再回到等待。

我在给同学讲这一节的时候,常用一个比喻:这就像两个人面对面说话,中间没有任何障碍,你说一句他听一句,不需要确认、不需要编号、不需要重复。只要中间隔上一堵墙,这套方式立刻崩掉,而这堵墙就是下一节要引入的东西。

注意:rdt1.0 里那个"等待调用"的循环不是废话,它是后面所有状态机的母版。你后面看到"等待 ACK 0""等待 ACK 1"这些状态,都是在这个母版上挂出来的分支。

2.2 rdt2.0:校验和与 ACK/NAK,然后把 ACK 自己搞丢了

rdt2.0 打破了第一个假设:底层信道可能翻转比特。对策是加校验和。接收方收到分组后重算校验和,一致就把数据交给上层并回一个肯定确认(ACK),不一致就丢弃并回一个否定确认(NAK),要求发送方重发。

发送方此时进入"等确认"状态:发出分组后,如果收到 ACK,就回到等待上层调用的状态,准备发下一份数据;如果收到 NAK,就重发刚才那份。看起来天衣无缝,实际上有一个致命漏洞——ACK 和 NAK 报文本身也会在回程链路上损坏。

设想这样一种情况:接收方正确收到了数据,回了一个 ACK,但这个 ACK 在回程被打坏了,发送方收到的是一个校验不通过的报文,它根本没法判断"这是 ACK 坏了"还是"这是 NAK 坏了"。如果它当成 NAK 处理,会重发;如果它当成 ACK 处理,可能下一份数据还没发出去就以为成功了。更糟的是,如果它选择"收到坏报文就重传",接收方就会收到同一份数据两次,又会向上层交付两次。

这个死结靠什么解?靠序号。教材把这一步拆成两个小版本,正是为了让你看清"序号不是可有可无的优化,而是解决确认报文歧义的必需品"。

2.3 rdt2.1 与 rdt2.2:序号是关住重复分组的笼子

rdt2.1 的做法是给每个分组加一个 1 比特序号,取值 0 或 1。发送方维护两组状态:等待上层调用 0 / 等待 ACK 或 NAK 0,以及等待上层调用 1 / 等待 ACK 或 NAK 1。接收方也对应地维护"期望收到序号 0"和"期望收到序号 1"两个状态。

关键在于接收方遇到重复分组时的处理:如果它期望序号 1,却收到了一个序号 0 的分组,说明这是发送方因为确认丢失而做的重传,它不会把数据再交一次给上层,而是直接回一个 ACK 0,告诉发送方"你要的那个 0 我早收到了,别再发了"。这一步是整个可靠传输里最优雅的设计之一——用序号把"重复"这个幽灵关进了笼子。

rdt2.2 是在 rdt2.1 上做的减法:既然 ACK 和 NAK 都会坏,那干脆不要 NAK。接收方只在收到正确分组时回 ACK,并在 ACK 里带上被确认分组的序号;发送方收到一个"重复的 ACK"(比如它期望 ACK 1 却收到了 ACK 0),就推断出对方没收到新的分组,主动重传。这个思路在真实协议里被广泛采用,TCP 就是只用确认、不用否认确认的典型。

我自己的经验是,rdt2.2 这一段最容易被跳过去,但它其实是后面"三次重复 ACK 触发快重传"的源头。你在这里把"重复 ACK 意味着什么"想通了,后面 TCP 的快重传就不用背了。

提示:rdt2.2 的接收方必须记住"上一次正确交付的序号",否则它无法判断一个重复分组该回哪一个 ACK。这个"记住上一次"的能力,在很多同学的实现里被漏掉,导致重复 ACK 永远发不出来。

2.4 rdt3.0:定时器上场,比特交替协议成型

rdt3.0 打破了最后一个假设:底层信道会丢包。丢包导致的最直接后果是——发送方可能永远等不到 ACK,也不能永远等下去,因为上层还等着它干活。

对策是给发送方加一个倒计时定时器:每发出一个分组就启动计时,如果在超时前收到对应序号的 ACK,就停表;如果超时了,就判定分组或 ACK 丢失,重传该分组。这个机制配上 1 比特序号,就是大名鼎鼎的比特交替协议(alternating-bit protocol)。

它有个容易被忽略的细节:超时重传时,发送方不能乱改序号,必须发回原来的序号。因为接收方可能已经收到了原分组、只是 ACK 在路上丢了,此时它正处在"期望下一个序号"的状态,收到一个旧序号会判定为重复并重发 ACK;发送方收到这个 ACK 后就能继续推进。如果重传时换了序号,接收方会把它当成新数据交付给上层,直接造成数据错乱。

版本假设的缺陷新增机制遗留问题
rdt1.0无无一旦有误码就全崩
rdt2.0比特误码校验和、ACK/NAK、重传确认报文自身可能损坏
rdt2.1比特误码 + 确认损坏1 比特序号、重复检测依然依赖 NAK
rdt2.2同上去掉 NAK,用重复 ACK 代替尚未处理丢包
rdt3.0误码 + 丢包倒计时定时器、超时重传停等导致利用率极低

这张表我建议自己手画一遍,把每一行当成一个"因为什么、所以加了什么、还剩什么"的因果记录。考试前扫一眼,比背五个状态机图有效得多。

3. 停等协议为什么在真实网络里跑不动:带宽时延积算给你看

3.1 一个 1 Gbps 链路上的尴尬数字

rdt3.0 逻辑上完美无缺,但它有一个致命的工程问题:发一个分组、等一个确认,再发下一个。这就是停等协议。它的性能有多差,我们拿数字算一遍就明白了。

假设链路速率 R = 1 Gbps,单向传播时延 15 ms,分组长度 L = 1000 字节 = 8000 比特。发送时延 = L / R = 8000 / 10^9 = 8 微秒。往返时延 RTT = 30 ms = 30000 微秒。

停等协议下,发送方发出一个分组后必须等整整一个 RTT,才能把下一个分组推上链路。一个周期是 8 + 30000 = 30008 微秒,其中真正用来传数据的只有 8 微秒,链路利用率 = 8 / 30008 ≈ 0.0266%,大约是一比三千七百五十一。

换句话说,这条千兆链路被跑成了不到 300 kbps。这不是理论瑕疵,这是实打实的性能灾难。我当年拿到这个数字的第一反应是"这算错了吧",后来反复核对才确认:链路速率越高、传播时延越大,停等协议浪费得越狠。

3.2 流水线:把利用率从 0.027% 拉回满格

要让链路跑满,就必须在等确认的同时继续发数据。这个思路叫流水线。具体来说,发送方允许连续发出多个分组,不必等前一个被确认;接收方也可以缓存一定数量的乱序到达分组。这样一来,链路上同时"在飞"的数据量就够了。

那到底要发多少个才够?这就是带宽时延积(bandwidth-delay product)的概念。它等于链路速率乘以往返时延,表示"为了让链路跑满,管道里至少要塞多少比特"。上面的例子里,1 Gbps × 30 ms = 30 Mbit,折算成 1000 字节的分组,大约是 3750 个分组。

也就是说,窗口至少要开到 3750 左右,这条链路才可能不被浪费。这个数字非常反直觉:一个理论上完美的可靠传输协议,到了工程上需要三千多倍的并发度才能发挥链路能力。

链路场景速率RTT带宽时延积按 1000B 分组折算
同城机房互联1 Gbps1 ms1 Mbit约 125 个分组
跨区域长距链路1 Gbps30 ms30 Mbit约 3750 个分组
家庭宽带典型值100 Mbps40 ms4 Mbit约 500 个分组

这张表还有一层意义:它解释了为什么本地局域网里即使协议写得糙也感觉不到问题,而一旦跨区域传输就开始"明明带宽够却跑不快"。原因往往不在带宽,在于窗口开得太小、管道填不满。

注意:带宽时延积算出来的是"理论最小在途数据量"。实际设计窗口时还要考虑接收方缓存大小、序号空间、拥塞状况,不可能无限开大。后面的窗口大小约束推导就是讲这件事。

4. GBN 与 SR:两种滑动窗口的取舍

4.1 GBN:累积确认省事,但重传代价大

回退 N 步(Go-Back-N,简称 GBN)是流水线最简单的实现方式。发送方维护一个长度为 N 的窗口,窗口内的分组可以连续发出,不需要逐个等确认;接收方窗口长度固定为 1,只按顺序接收,收到乱序分组一律丢弃,并且对"最后一个按序正确接收的分组"重复发送确认。

发送方收到的确认是累积确认:收到 ACK n,意味着序号 n 及之前的所有分组都已安全抵达。这个设计的好处是,即使某个 ACK 在路上丢了,只要后一个 ACK 到了,前面的也能一并推进,容错性很好。发送方只需维护一个定时器,对应窗口中最老的未确认分组,超时就把整个窗口内的分组全部重发。

代价也很明显:只要窗口里有一个分组丢了,后面所有已经正确到达接收方、只是被丢弃的分组,都得陪着重传一遍。在丢包率较高的链路上,这种"一人犯错全家连坐"的重传策略会显著浪费带宽。

我做过一个粗略的对比实验:丢包率 5% 的链路上,同样传 1000 个分组,GBN 实际发出的分组数比 SR 多出将近三成。丢包率越高,差距越大。所以在无线这类误码率较高的环境里,GBN 并不划算。

4.2 SR:逐个确认,接收方要备缓存

选择重传(Selective Repeat,简称 SR)针对 GBN 的浪费做了优化:接收方也开一个长度为 N 的窗口,把乱序到达但校验正确的分组缓存起来,等缺口被补上以后,再把一连串分组按序一次交付给上层。发送方每个分组都有独立的逻辑定时器,只重传那些确实超时未确认的分组。

这样做的收益很直接:只需要重传真正丢失的那一个,其他已经到达的分组不用陪跑。代价是复杂度上来了——接收方需要缓存,发送方需要为每个在途分组维护定时器,确认报文也变成了逐个确认(虽然实现上常常用累积确认做兼容优化)。

在真实系统里,纯粹的 GBN 和纯粹的 SR 都不常见,更多是混合方案:用累积确认减少确认报文数量,同时在接收方保留缓存、允许一定程度的乱序接收,重传时只挑缺失的补。TCP 的 SACK 选项就是往 SR 方向靠的典型做法。

4.3 窗口大小为什么不能随便开

窗口不是想开多大就开多大,它受序号空间的硬约束,这是这一章最容易被考、也最容易在实现里埋雷的地方。

先说 GBN。设序号用 k 个比特表示,序号空间是 2^k。发送窗口最大只能取 2^k − 1。原因:假如窗口取到 2^k,发送方一口气发完 0 到 2^k−1 全部 2^k 个分组,接收方全部正确收到并交付给上层,但所有 ACK 都在回程丢了。发送方超时后重传 0 号分组,此时接收方的期望序号正好绕了一圈回到 0,它会把重传的老分组当成新分组交付,造成重复。窗口取 2^k − 1 时,发送方最多发到 2^k − 2,重传的 0 落在接收方期望窗口之外,会被正确识别为重复。

再说 SR。约束变成发送窗口大小 + 接收窗口大小 ≤ 2^k。假设 k = 3,序号 0 到 7,如果发送窗口和接收窗口各取 4,两者之和等于 8,正好卡在边界上不出错;一旦各取 5,和超过 8,就会出现"发送方重传的老分组落入接收方新窗口"的歧义,接收方无法分辨它是新数据还是旧数据的副本,从而错误交付。

实际实现中通常两边各取 2^(k−1),也就是序号空间的一半,这样既满足约束,又让两侧窗口对称,推理最省心。

协议接收窗口确认方式定时器窗口上限
停等1逐个1 个1
GBN1累积1 个(对应 base)2^k − 1
SRN逐个(可辅以累积)每个在途分组各一个发送+接收 ≤ 2^k

提示:面试里问"序号 3 位、窗口能开多大",GBN 答 7、SR 答 4(对称取法),这两个数字要能脱口而出,并且能当场把反例讲出来。

5. 动手写:把 rdt3.0 和 GBN 用 Python 模拟出来

5.1 先搭一个能丢包、能出错的假信道

光看状态机不动手,理解永远是浮的。我建议第一件事实在点,先花二十分钟写一个"坏信道",让它随机丢包、随机打坏内容、随机复制分组、随机延迟。有了它,后面任何协议都能拿来做对照实验。

import random class BadChannel: """模拟一条会丢包、会误码、会重复、会乱序的链路""" def __init__(self, drop=0.15, corrupt=0.10, dup=0.05, jitter=(1, 3)): self.drop = drop self.corrupt = corrupt self.dup = dup self.jitter = jitter self.inflight = [] # 元素是 (due_time, pkt) self.now = 0 def send(self, pkt): self.now += 1 if random.random() < self.drop: return # 直接丢了 out = dict(pkt) if random.random() < self.corrupt: out['body'] = 'X' + out['body'][1:] # 打坏首字符 due = self.now + random.randint(*self.jitter) self.inflight.append((due, out)) if random.random() < self.dup: self.inflight.append((due + 1, dict(out))) def recv(self): ready = [p for p in self.inflight if p[0] <= self.now] if not ready: self.now += 1 return None ready.sort(key=lambda p: p[0]) # 延迟不同,天然乱序 _, pkt = ready[0] self.inflight.remove(ready[0]) return pkt

这里有几个设计考量值得说一下。时间用整数步进而不用真实时钟,是为了让模拟快、结果可复现,调好随机种子就能稳定重放同一个故障序列;乱序靠随机延迟自然产生,不需要额外写排序逻辑;重复分组的实现是"同一个包塞两份、到期时间差一步",非常贴近真实网络里重传副本晚到的情形。

5.2 停等版本的发送方与接收方

有了信道,rdt3.0 就非常好写了。校验和用一个极简的字符求和取模,够用且直观。

def checksum(s): return sum(map(ord, s)) % 256 def make_pkt(kind, seq, data): body = f"{kind}|{seq}|{data}" return {'seq': seq, 'body': body, 'ck': checksum(body)} def valid(pkt): return pkt['ck'] == checksum(pkt['body']) def rdt3_send(payloads, ch, timeout=6, retry_limit=20): seq = 0 for data in payloads: pkt = make_pkt('DATA', seq, data) for _ in range(retry_limit): ch.send(pkt) deadline = ch.now + timeout acked = False while ch.now <= deadline: reply = ch.recv() if reply is None or not valid(reply): continue # 坏报文直接丢弃,等超时 if reply['seq'] == seq: acked = True break if acked: break seq = 1 - seq # 只有确认成功才翻序号

接收端的逻辑要点在于"记住期望序号"。收到校验正确且序号等于期望值的分组,交付并回 ACK,然后翻转期望序号;收到序号不等于期望值的分组,说明是重复,直接回 ACK,但不交付。

def rdt3_recv(ch, expected, total, log): got = 0 while got < total: pkt = ch.recv() if pkt is None or not valid(pkt): continue if pkt['seq'] == expected: log.append(pkt['body']) got += 1 ch.send(make_pkt('ACK', pkt['seq'], '')) expected = 1 - expected else: # 重复分组:重发确认,但不重复交付 ch.send(make_pkt('ACK', pkt['seq'], '')) return expected

这两段加起来不到五十行,但它跑起来以后你对"为什么序号必须翻、为什么重传不能改序号"会有肌肉记忆级别的理解。

5.3 GBN 版本的关键改动

GBN 的改动集中在发送方:窗口、累积确认、单一超时定时器、超时整体回退。

def gbn_send(payloads, ch, window=4, seq_space=8, timeout=8): n = len(payloads) base, nxt = 0, 0 timer_on = False deadline = 0 while base < n: while nxt < base + window and nxt < n: ch.send(make_pkt('DATA', nxt % seq_space, payloads[nxt])) nxt += 1 if not timer_on: timer_on = True deadline = ch.now + timeout if timer_on and ch.now > deadline: nxt = base # 整体回退 timer_on = False continue reply = ch.recv() if reply and valid(reply): base = max(base, reply['seq'] + 1) # 累积确认推进 timer_on = base != nxt if timer_on: deadline = ch.now + timeout

三处细节值得反复看:base = max(base, ...)这一句保证了迟到的重复 ACK 不会把窗口往回拽;timer_on = base != nxt表示"窗口里还有未确认的才需要定时器",全确认完了就关表;超时时nxt = base是整个 GBN 的核心动作,它把发送指针拉回窗口起点,下一轮循环会把窗口内所有分组重发一遍。

接收方反而比停等更简单:维护一个期望序号,收到对的就交付、期望值加一、回 ACK;收到不对的,丢弃、重复回上一次的 ACK。

5.4 超时时间与窗口大小的参数标定

这两个参数是最容易拍脑袋定的,也是最容易定错的。

超时时间的取值不能比 RTT 小太多,否则误判丢包、疯狂重传;也不能太大,否则真丢包了要等很久才恢复。工程上通常用指数加权移动平均来估计:每次拿到一个 RTT 样本 Sam,就更新 Est = (1 − α) × Est + α × Sam,偏差 Dev = (1 − β) × Dev + β × |Sam − Est|,最终超时值取 Est + 4 × Dev。α 常取 0.125,β 常取 0.25。这个 4 倍偏差的系数不是随手写的,它让超时阈值随着 RTT 抖动自动放宽,在抖动大的链路上不至于频繁误重传。

窗口大小则要同时满足三个条件:不超过序号空间约束(GBN 是 2^k − 1,SR 是 2^k 的一半)、不超过接收方缓存能力、不小于带宽时延积折算出的分组数(否则管道填不满)。三者取最小值,通常就是合理的窗口。我在本地模拟里因为延迟只有几步,窗口 4 就够;但要模拟长距链路,窗口至少得开到几十,否则测出来的吞吐完全不能看。

注意:调参时一次只改一个变量。我见过有人同时改丢包率和窗口大小,跑出来的结果根本解释不了是谁造成的。先用零丢包验证正确性,再逐步加丢包看鲁棒性,这个顺序不能反。

6. 常见问题与排查技巧实录

6.1 问题速查表

这套模拟跑了不下十遍,问题基本集中在下面几类,整理成表方便对照。

现象大概率原因处理方式
接收端数据比原始多缺序号或期望序号没翻转检查重复分组是否被二次交付
发送端卡死不动定时器没起或超时判断写反打印每次超时时刻与 base 值
传了一半就再也推不动序号比较用了绝对值而非取模统一用seq % seq_space比较
重传风暴、日志刷屏超时值远小于实际 RTT按 Est + 4 × Dev 重算阈值
GBN 吞吐很低窗口过小或丢包率过高算带宽时延积,按需调窗
SR 出现重复交付发送窗口 + 接收窗口 > 2^k两侧各取序号空间一半
确认收到了却不推进窗口累积确认取成了赋值而非取最大改用max(base, ack + 1)

6.2 三个我在实验里踩过的坑

第一个坑是序号回绕。序号用 3 位表示时,发到 7 之后回到 0。如果网络上还飘着一个很久以前的重传副本,而它的序号恰好和当前新分组的序号撞上,接收方就会误判。真实协议靠"分组最大寿命"来兜底——只要保证一个分组在网络中存活的时间短于序号绕一圈的时间,就不会撞车。TCP 里用时间戳选项来解决同一类问题。做模拟时,我建议一开始就把延迟抖动调小,别让老分组飘太久,否则你会怀疑人生。

第二个坑是 ACK 丢失造成的假死。早期版本我只在acked为真时才推进序号,结果 ACK 一丢就永远停在原地,因为retry_limit用完就静默退出了,日志里什么都看不出来。后来我加了两行打印:每次重传时输出当前序号和重试次数,一眼就看出是确认全丢还是逻辑写错。加日志永远比盯着代码猜快,这是我在这一章里最实在的收获。

第三个坑是定时器管理。GBN 只需要一个定时器,对应窗口最老的未确认分组,超时后重启;SR 则需要为每个在途分组维护逻辑定时器,实现上常用"每个分组记录一个截止时间、每轮扫描最早期满的那个"来做。我一开始在 SR 里给每个分组起了真实线程定时器,结果线程数爆炸、时序全乱。换成"记录截止时间 + 单线程扫描"以后立刻稳定。这一点在写任何带超时的逻辑时都适用:逻辑定时器比物理定时器好管一个量级。

至于后续怎么扩展,我自己的计划是把这份模拟接到一个真实 socket 上,让两个进程隔着本机回环通信,再把丢包注入换成按概率丢弃真实报文。到那一步,你就会发现这一章学到的东西不是纸上的状态机,而是一套能直接落地的工程方法。

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

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

立即咨询