news 2026/10/19 10:52:20

UDP之上实现可靠传输:停等、GBN与SR协议详解与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UDP之上实现可靠传输:停等、GBN与SR协议详解与避坑指南

简介:面向计算机网络课程设计中可靠数据传输协议实验,这是一份基于Python实现的完整方案,覆盖停等协议、GBN(后退N帧)与SR(选择重传)三种机制的代码与文档。资源共14个文件,包括8个py源码(分protocol、server、client、util等模块)、3个txt数据文件、1份docx实验报告及README与License说明,压缩包仅723KB,结构清晰便于按需阅读和部署。目前已有596人学习下载,说明其在UDP可靠传输实验场景中具有一定参考价值。方案以基于UDP设计停等协议为起点,逐步实现单向可靠传输、丢包模拟验证、双向数据传输与C/S文件传输,随后将停等扩展为GBN并继续改进为SR协议,完整呈现协议迭代过程。配套的README和实验报告可辅助理解协议设计思路与关键实现,适合需要完成课程设计或深入理解滑动窗口、可靠传输原理的高校学生对照学习。

1. UDP 之上手动实现可靠传输:为什么"重传"是唯一靠谱的答案

你有没有过这种体验:UDP 把一个数据报交给 socket 就再也不管了,另一端的进程等得再久也收不到一声"我丢了"。TCP 把可靠传输做成了黑匣子,而课程设计要求你亲手在 UDP 上重建这套机制——从停等协议(Stop-and-Wait)到 GBN(Go-Back-N),再到 SR(Selective Repeat)。以 Python 为语言、以 UDP 为传输底座去实现这三个协议,本质上就是在回答同一个问题:当网络随时可能丢包、损坏、乱序时,协议靠什么保证"接收方拿到的数据等于发送方发出的数据"。答案的核心是重传,但怎么重传、什么时候重传、重传多少,决定了协议是简单、高效还是复杂。搞懂这三个协议,你就拥有了可靠数据传输的完整底层认知,也就能看穿 TCP 那些确认、超时、滑动窗口设计背后的真实动机。

2. 停等协议:为什么"发一个等一个"是最简单也最可靠的机制

2.1 校验和、序号与超时:可靠传输的最小完备集

停等协议的策略用一句话就能说完:发送方每发一个数据包,就停下来等接收方的确认(ACK),确认到了才发下一个。但这个"停下来等"的动作背后,必须同时配备三个机制——校验和负责识别数据是否在传输中被改坏,序号负责区分这个包是新的还是重传的旧包,超时负责在 ACK 迟迟不来时兜底触发重传。

缺任何一个都会出事。只有校验和没有超时:数据包在链路上被交换机丢弃,发送方永远等不到 ACK,协议直接卡死。只有超时没有序号:发送方重传的旧包到达后,接收方无法判断它是重复包,文件里会出现两段相同的数据。只有序号没有校验和:被损坏的包如果恰好序号正确,接收方会把错误数据交给上层应用。所以这个三位一体不是一个"建议配置",而是可靠数据传输的最小完备集。

为什么停等只需要 1 比特序号?因为任意时刻信道上在途的包最多只有一个。接收方期待的值只有两个状态:期待 0 或者期待 1。收到期待的序号,说明是新包;收到另一个序号,说明是发送方超时重传的旧包,丢弃并再回一个 ACK 就行。这比你想的还要省——1 比特序号空间恰好等于"在途包数 + 1",不多不少。

2.2 状态机视角:双方各自维护什么状态

从实现角度看,停等协议是两个极其简单的状态机在互相驱动。发送方只有两个状态:就绪和等待确认。收到上层数据时进入"就绪 + 发当前包",发出后进入"等待确认";等待期间如果收到确认且 ACK 序号正确,回到就绪准备发下一个包;如果超时,就重发当前包并继续保持等待。接收方也只有两个状态:期待 0 和期待 1,收到正确序号的数据包就把数据交付上层、回 ACK、翻转期待值,收到重复包就丢弃并再回一个 ACK。

为什么画清楚状态机对写代码这么重要?因为事件的触发是异步的:超时、正确 ACK、重复 ACK、损坏包这四个事件可能在任何一个时刻到达。不把状态转换表列出来,你写的if/else就会漏掉"重复包也要重新回 ACK"这类边界路径。很多同学第一次实现停等时,接收方收到损坏包直接continue,收到重复包也直接continue——漏了重复包的回 ACK,结果发送方一直在等确认,双方死等。这个"收到重复包必须回 ACK"的细节,在后面 GBN 和 SR 里同样成立,而且是丢包模拟实验能否跑通的关键。

2.3 代码拆解:UDP 套接字上的停等发送端

停等发送端的核心循环只需要一个while加一个socket.timeout分支,但每一行都有讲究:

# protocol/stop_and_wait.py 发送端核心逻辑(结构简化) import socket, struct, hashlib def make_pkt(seq: int, data: bytes) -> bytes: # 包格式:2 字节序号 + 4 字节校验和 + 数据 # 校验和取 data 的 SHA-256 前 4 字节,够课程设计使用 checksum = hashlib.sha256(data).digest()[:4] return struct.pack('!H4s', seq, checksum) + data def sw_send(sock: socket.socket, addr, data_list, timeout=1.0): seq = 0 for data in data_list: pkt = make_pkt(seq, data) while True: sock.sendto(pkt, addr) # 发数据,然后停下来等 sock.settimeout(timeout) # 等待 ACK 的时间上限 try: ack, _ = sock.recvfrom(2048) ack_seq, = struct.unpack('!H', ack) if ack_seq == seq: # 只认当前序号的 ACK break # 确认成功,进入下一个数据 except socket.timeout: continue # 超时:重传当前包 seq = 1 - seq # 序号 0/1 交替

make_pkt里!H4s表示大端序的无符号短整数加 4 字节校验和,序号字段是 16 比特,但停等只用 0 和 1——字段空间大不代表你可以乱用序号,一旦同时发出多个不同序号的包,协议前提就被破坏了。收到 ACK 后必须校验ack_seq == seq,这个判断不能省:如果省略,上一轮迟到的 ACK 会把下一轮数据包误确认,本该重传的包被跳过,数据就丢了。

超时分支的continue是协议正确性的最后防线。此时发送方根本不知道是数据包丢了、ACK 丢了还是 ACK 还在路上,唯一能做的就是重新发一遍。验证协议是否真的可靠,可以打开工程里的丢包模拟,把丢包率调到 0.3,然后对比data/server_data.txt和recv/client_recv.txt的哈希值——只要校验和、序号、超时三个机制都在,内容必然一致。

2.4 停等的性能瓶颈:为什么非要用滑动窗口

停等唯一的问题就是慢。假设链路往返时延 RTT 是 100 毫秒,一个数据包从发出到确认回来要 100 毫秒,那么无论链路带宽多大,发送方的吞吐上限都是一秒钟 10 个包。如果用 1KB 的包算,理论吞吐只有 80Kbps,连一条 10Mbps 链路的 1% 都不到。公式可以写成:信道利用率 = 发送时间 / (发送时间 + RTT)。RTT 越大,利用率越低。

这就是滑动窗口协议出现的原因。GBN 和 SR 的全部改进,本质上都是在"等待 ACK"这段时间里把更多的包塞进链路,用并发在途包数换取吞吐量。而从停等的 1 个在途包扩展到 N 个在途包,第一个要解决的问题就是:序号怎么标、窗口怎么推、丢了怎么重传。

3. GBN 滑动窗口:为什么丢一个包要回退 N 个

3.1 从停等到流水线:窗口的本质是"允许 N 个包同时在路上"

GBN 又叫 Go-Back-N,在很多课程资料里也写作 GBN 或 GBM,机制完全一样。它的核心把"发一个等一个"改成了"发一堆等确认":发送方维护一个长度为 N 的窗口,只要窗口里的包发出去后还没有收到确认,就可以继续发下一个,直到窗口填满。N 是在途包数的上限,也是吞吐量相对停等的倍数。RTT 100 毫秒、包发送时间 1 毫秒的例子里,窗口 N=10 时理论利用率就能从 1% 提升到接近 10%。

理解窗口最好的方式是把窗口看成两个指针之间的区间。base指向窗口中最早发出但还没被确认的包,next_seq指向下一个要发送的新包。当next_seq - base < N时,发送方可以继续发包;每发一个包next_seq加一;每收到一个确认,base向前推进。窗口在链路上看起来就像一条"允许在途包的流水线",只要流水线不满,就不必停下来等。

3.2 累计确认与回退语义:接收方为什么可以不做缓存

GBN 最重要的设计选择是累计确认。接收方返回的ack = k,含义不是"第 k 个包我收到了",而是"序号小于 k 的所有包我都收到了"。这个确认语义有三个好处:第一,ACK 报文只需要一个序号字段,开销极小;第二,即使中间有几个 ACK 丢了,只要后面来了序号更大的 ACK,发送方就知道前面的包其实全到了;第三,发送方确认逻辑极其简单,收到一个大于base的 ACK 就直接把窗口下沿推过去。

代价是接收方被剥夺了缓存能力。GBN 接收方只接受恰好落在期待窗口中的包,乱序到达的包一律丢弃——因为它知道发送方超时后会从base开始把整个窗口重传一遍,缓存乱序包纯属浪费。这是一个用带宽换实现简单的典型取舍:接收方不需要任何缓存,只需维护一个"期待序号"的变量。

但"丢一个包就丢弃后续所有包"的代价是灾难性的连锁反应。假设窗口 N=10,发送方一口气发了 0 到 9 号包,2 号包在途中丢失,接收方收到 3、4、……9 时,它们全部落在期待窗口之外,全部被丢弃。发送方要等到超时才发现 2 号包没被确认,然后从 2 号开始把 2 到 9 号全部重发。也就是说,丢一个包实际造成的是 N 个包的重传开销。这就是 Go-Back-N 名字的由来:回退 N 步。

3.3 代码拆解:窗口推进与超时后的整体回退

# protocol/gbn.py 发送端核心逻辑(结构简化) def gbn_send(sock, addr, data_list, window_size=4, timeout=1.0): base, next_seq = 0, 0 # base: 窗口下沿; next_seq: 下一个新包序号 n = len(data_list) while base < n: # 窗口未满且还有数据,就连续发送,不等 ACK while next_seq < base + window_size and next_seq < n: sock.sendto(make_pkt(next_seq, data_list[next_seq]), addr) next_seq += 1 sock.settimeout(timeout) # 简化实现:用阻塞读超时近似公共定时器 try: ack, _ = sock.recvfrom(2048) ack_seq, = struct.unpack('!H', ack) if ack_seq > base: # 累计确认:小于 ack_seq 的包全部收到 base = ack_seq # 窗口下沿前移 except socket.timeout: # 超时:回退到 base,重传整个窗口 next_seq = base for i in range(base, min(base + window_size, n)): sock.sendto(make_pkt(i, data_list[i]), addr)

代码里ack_seq > base这个判断是关键。GBN 的确认是累计的,如果收到ack_seq = 5而base还在 2,说明 2、3、4、5 号包全部被确认,窗口可以一次性推进到 5。反过来如果收到重复 ACK(比如ack_seq = 2),它不大于base,窗口不推,发送方继续等待新 ACK。这个比较在数学上等价于"只接受比我预期更大的确认",天然容忍 ACK 重复和 ACK 乱序。

标准实现的 GBN 通常只维护一个公共超时定时器:窗口内最老的包发出时启动,收到累计 ACK 后重置。这里用sock.settimeout(timeout)+recvfrom是课程设计里常见的近似写法——每次等 ACK 最多等 timeout 秒,等不到就统一回退。注意超时分支里next_seq = base是整个 GBN 的标志性动作:不是只重传超时的那一个包,而是把窗口里的包全部重发,因为接收方大概率已经把乱序包扔掉了。工程里util/config.py的WINDOW_SIZE就是用来调这个权衡的参数:窗口越大吞吐越高,但丢包率不变时,一次回退重传的浪费也越大。

3.4 一个必须提前知道的坑:窗口大小与序号空间

GBN 的窗口不是随便设的。假设序号只有 0 到 7 三个比特、窗口调到 6,当发送方发出 7 号包后新包序号回绕到 0,此时窗口里既有旧包 2 的迟到 ACK,又有新包 0 的确认,发送方无法区分这两个语义。所以可靠传输协议的黄金法则是:序号空间大小必须至少是窗口大小的两倍。停等窗口是 1,所以 1 比特序号就够;GBN 窗口 N 至少需要 2N 个不同序号。代码里序号字段是 16 比特,N=4 完全没有回绕风险,但如果你把窗口调成 30000,就会撞上这个隐蔽的边界。这一点是后面所有滑动窗口协议的共性约束,SR 的边界条件更复杂,稍后在避坑部分再展开。

4. SR 选择重传:如何做到"丢一个只补一个"

4.1 从回退到选择的动机:高丢包率下 GBN 的带宽浪费

GBN 在高丢包率的链路上表现很差。窗口 N=10、丢包率 20% 时,每 5 个包就会丢一个,每次超时都要重传大约 10 个包,有效吞吐可能只有理论值的一半以下。仔细想想就会发现一个矛盾:接收方真正缺的只有一个包,发送方却把整个窗口重发了一遍。SR(Selective Repeat)就是冲着这个浪费来的:发送方只重传那些确认超时的包,接收方则必须把乱序到达的包缓存起来,等缺口补齐再交付。

这个改动把重传粒度从"整个窗口"降到了"单个数据包"。代价是接收方要为窗口内的乱序包准备缓存,还要为每个包单独维护确认状态。SR 的确认方式也从累计确认变成了选择确认:接收方每收到一个落在窗口内的合法包,就单独回一个对应序号的 ACK。发送方不再根据"最大确认序号"推进窗口,而是根据"每个包各自的确认情况"推进。

4.2 每个包独立定时器:发送方的"后悔药"要分开吃

SR 发送方不能像 GBN 那样用一个公共超时器,否则超时时会把窗口内所有包重发一遍,那和 GBN 就没区别了。理论上的标准做法是为窗口内的每个包启动一个独立的定时器,哪个超时重发哪个。在实际的 Python 工程里,独立线程定时器太重,常见的简化方案是:用字典记录每个包的最近发送时刻,阻塞读超时后扫描窗口内所有未确认的包,凡是当前时间 - 发送时刻 >= timeout的,单独重发。

这种"统一超时 + 按需重传"的近似写法,在课程设计这种单机模拟环境下完全够用。它保留了 SR 的本质——重传是逐个发起的,而不是整体回退。同时要注意,SR 的窗口推进条件是"窗口内最老的包被确认",即base对应的包一旦收到 ACK,窗口才能向前滑动;如果中间某个包一直没确认,窗口就会卡在那里,后面的新包即使想发也发不出去,这正是窗口协议保证不乱序的底层机制。

4.3 代码拆解:接收缓存、乱序交付与窗口外重复包

SR 与 GBN 实现差距最大的地方在接收端:

# protocol/sr.py 接收端核心逻辑(结构简化) def sr_receive(sock, out: dict, window_size=4): base = 0 cache = {} # seq -> bytes,乱序包暂存区 while True: pkt, _ = sock.recvfrom(2048) seq, checksum, data = parse_pkt(pkt) if not verify(checksum, data): continue # 校验失败:静默丢弃 if base <= seq < base + window_size: cache[seq] = data # 落在窗口内,先缓存 send_ack(sock, seq) # 每个包单独确认 while base in cache: # 连续就位的包一次性交付 out[base] = cache.pop(base) base += 1 elif seq < base: # 序号已滑过窗口,说明是重复包,补发 ACK 防止死等 send_ack(sock, seq)

这段代码的三层分支对应 SR 接收方的三种输入。第一层,包落在窗口内:存入缓存并单独回 ACK,然后用while base in cache连续交付——假设窗口期待 2,但 3、4 先到了,缓存里有 3 和 4,base仍停在 2;等 2 一到,循环依次取出 2、3、4,base直接跳到 5。这个"连续才交付"的逻辑,是 SR 接收方与 GBN 接收方最本质的区别:GBN 丢弃乱序包,SR 缓存乱序包。

第二层,包序号小于base:说明这个包之前已经交付过,发送方重传它只是因为它的 ACK 丢了。此时绝不能静默丢弃——如果丢弃不回 ACK,发送方会一直等确认,形成死循环。正确做法是补发一个 ACK,告诉发送方"这个包我早收到了"。这个分支是 SR 特有、也是最容易被漏掉的边界路径。

发送端的简化实现如下:

# protocol/sr.py 发送端:独立超时字典(结构简化) def sr_send(sock, addr, data_list, window_size=4, timeout=1.0): base, next_seq = 0, 0 sent_time = {} # seq -> 最近发送时刻 unacked = set() # 未确认序号集合 while base < len(data_list): while next_seq < base + window_size and next_seq < len(data_list): sock.sendto(make_pkt(next_seq, data_list[next_seq]), addr) sent_time[next_seq] = time.time() unacked.add(next_seq) next_seq += 1 try: ack, _ = sock.recvfrom(2048) ack_seq, = struct.unpack('!H', ack) if ack_seq in unacked and base <= ack_seq < base + window_size: unacked.discard(ack_seq) while base not in unacked and base < next_seq: base += 1 # 窗口最老包被确认,整体前移 except socket.timeout: now = time.time() for seq in list(unacked): if now - sent_time[seq] >= timeout: # 只重发超时的包 sock.sendto(make_pkt(seq, data_list[seq]), addr) sent_time[seq] = now

发送端的关键在while base not in unacked and base < next_seq:窗口的推进不是看某一个 ACK 的大小,而是看base指向的最老包是否被确认。只要最老的包没确认,窗口就卡住不动,后面的包即使发送了,也不能无限制地继续发新包——这把窗口大小硬性约束在WINDOW_SIZE以内。超时扫描时,for seq in list(unacked)配合now - sent_time[seq] >= timeout做的是"精确打击":哪个包超时就重发哪个,不再像 GBN 那样next_seq = base整体回退。

5. 避坑:超时、序号回绕与双向传输的经典翻车现场

5.1 超时设太短:重传风暴;设太长:吞吐量腰斩

现象:把timeout设为 0.1 秒跑局域网传输,丢包率 0.1 时文件内容虽然正确,但完成时间反而比 timeout=1 秒更慢,终端里刷满重传日志。

原因:局域网 RTT 通常只有几毫秒,0.1 秒看起来绰绰有余,但机器一旦负载高、调度抖动大,ACK 偶尔延迟几十毫秒到达。发送方等不到 ACK 就误判丢包并重传,重传包加剧了网络拥塞,又让后续 ACK 更慢,陷入重传风暴。

解决:一个稳妥的做法是像 TCP 一样做平滑 RTT 估算,用srtt = 0.875 * srtt + 0.125 * sample_rtt维护平均往返时间,把超时设为2 * srtt以上。课程设计里更简单的方法是把初始超时设成 1 秒,先把功能跑通,再逐步调小观察吞吐变化。超时参数不是一个"越大越稳"或"越小越快"的单向问题,它必须适配当前链路的 RTT 分布。

5.2 序号回绕与窗口大小:为什么窗口不能超过序号空间的一半

现象:把 GBN 的window_size调到 6、序号改成 3 比特(0~7),传输中途数据错乱;校验和是对的,文件内容却对不上。

原因:这是滑动窗口协议最隐蔽的陷阱。序号只有 0~7,窗口大小为 6,发送方发完 7 号包后新包序号回绕到 0。此时窗口里既有旧包 2 的迟到 ACK,又有新包 0 的确认,发送方无法区分这两个序号到底代表哪个包——因为 2 和 0 在回绕后的位宽里语义重叠了。

解决:记住可靠传输协议的铁律:序号空间必须至少是窗口大小的两倍。停等窗口是 1,所以 1 比特序号就够;GBN 和 SR 的窗口是 N,序号空间至少需要 2N。工程里序号字段是!H(16 比特),窗口调到 4 完全安全,但如果你出于好奇把窗口调成 40000,就会重新撞上这个陷阱。SR 尤其危险,因为它每个包单独确认,窗口覆盖范围和序号回绕的判断边界更模糊,务必给WINDOW_SIZE设置一个远小于2^(SEQ_BITS-1)的上限。

5.3 双向停等的死锁:两个方向互相等 ACK

现象:把停等协议改成双向传输后,程序交互几轮后卡死:一侧阻塞在recvfrom,另一侧也阻塞在recvfrom,双方都收不到对方的包。

原因:双向传输的朴素实现是每个方向各自跑一个停等循环,但 Python 的recvfrom是阻塞调用。A 发完数据等 B 的 ACK 时,B 恰好也发完数据正在等 A 的 ACK;两个方向的数据包都躺在对方的接收缓冲区里,但双方都阻塞在读取上,没有人去取——这就是双向传输特有的死锁。

解决:有三个方案。第一,改用select或poll同时监听套接字可读事件,有数据就处理数据,没数据就走自己的超时重传逻辑;第二,捎带确认(piggyback),把 ACK 字段塞进反向的数据包里,A 发数据的同时顺带确认 B 的数据,不再需要独立的 ACK 包;第三,用两个线程分别处理发送和接收。三种方案里,select改动最小,捎带确认最优雅,双线程最直观但要注意对同一个 socket 的并发读写需要加锁。

5.4 模拟丢包:固定随机种子,否则结果不可复现

现象:丢包率设成 0.5,实验结果完全不可复现;换一台机器跑,同一个丢包率下文件传输有时成功有时失败,调试时根本不知道是协议 bug 还是运气不好。

原因:测试代码直接调用了random.random(),Python 的随机数默认以系统时间为种子,每次运行丢包位置都不同。更隐蔽的是,独立丢包模型下重传的包也可能再次丢失,0.5 的丢包率意味着一个包平均要传 2 次才能成功,文件一大重传次数呈几何增长。

解决:在丢包模拟器里固定随机种子:

# util/lossy.py:在线路上做丢包欺骗(结构简化) import random class LossyChannel: def __init__(self, loss_rate, seed=42): self.rng = random.Random(seed) # 固定种子,实验可复现 self.loss_rate = loss_rate def send(self, sock, pkt, addr): if self.rng.random() < self.loss_rate: return # 数据报被"链路"吞掉,接收方完全无感知 sock.sendto(pkt, addr)

这是我在课程设计里踩过的坑:不固定种子,你根本不知道这次失败是协议 bug 还是随机丢包导致。丢包率控制在 0.1~0.3,既能验证协议有效性,又不会让重传次数爆炸。还要区分数据包丢包率和 ACK 丢包率:如果模拟的是单向链路丢包,ACK 通道通常是干净的;如果模拟双向干扰,ACK 独立丢包触发超时重传属于正常行为,别把它当成 bug。

5.5 文件传输分块:UDP 包不是无限大

现象:用停等协议传一个 10MB 的文件,发送端直接报OSError: [Errno 90] Message too long。

原因:UDP 数据报的理论上限是 65507 字节,超过就发不出去。即使不超上限,以太网 MTU 通常是 1500 字节,大包会被 IP 分片,而分片包只要丢一片,整个数据报就报废,可靠传输无从谈起。

解决:文件传输必须分块。数据块大小建议设在 1200~1400 字节,给 UDP 头和 IP 头留出空间,避免分片。发送端按固定块大小读文件,每块作为一个数据单元交给协议层;接收端按序号写回recv/client_recv.txt。还有一个容易忽略的坑:如果文件是文本且含中文等多字节字符,必须用二进制模式rb/wb读写,否则编码转换会悄悄改变字节内容,哈希比对不通过还查不到原因。

6. 停等、GBN、SR 的取舍:吞吐量、缓存与实现的三角博弈

6.1 三种协议的本质差异

三个协议在同一个目标下做出了完全不同的取舍。停等用"时间换实现":缓存几乎为零、代码最简单,但吞吐被锁死在 1 包/RTT。GBN 用"带宽换缓存":接收端不需要缓存,代价是丢包时回退 N 个包的重传浪费。SR 用"接收端内存换带宽":缓存乱序包、逐包重传,把丢一个包的代价降回 1 个包,但发送端要为每个包维护独立状态。三者没有谁绝对更优,只有链路条件决定谁更合适。

协议在途包数接收端缓存重传粒度确认方式实现复杂度
停等1无1 个包单包确认低
GBNN无窗口内 N 个包累计确认中
SRNN 个包单个超时包逐包确认高

真实网络中的 TCP 没有完全倒向 SR 那一极:它默认用累计确认(GBN 语义),再通过 SACK 扩展补上选择确认(SR 语义)。这说明协议设计不是选边站,而是根据链路丢包率、接收端内存成本和协议复杂度综合调参。工程里的进阶玩法是让WINDOW_SIZE跟随丢包率动态变化:丢包率低用大窗口冲吞吐,丢包率高就缩小窗口,减少回退或缓存压力。

6.2 验证协议可靠性的四个强制用例

很多同学跑通一次文件传输就认为协议可靠,但可靠传输的正确性至少需要四类场景验证:乱序场景(人为在接收后插入随机 sleep,看 GBN 是否正确丢弃、SR 是否正确缓存);重复包场景(把同一个包连续发送两次,看序号判断能否去重);ACK 丢失场景(只对 ACK 路径丢包,看超时机制能否兜底);校验和损坏场景(刻意篡改一个字节,看是否会被丢弃而不交付错误数据)。

我的一个习惯是:每种协议跑完实验后,先对比recv/client_recv.txt与data/server_data.txt的 SHA-256,再做一次"丢包率 0.3 + ACK 丢包率 0.2 + 最大重传次数限制"的极限测试,确认协议在重传耗尽时能主动报错而不是挂死。有一次在一个 GBN 实现里发现接收方把重复的乱序包交付给了应用层,我才意识到"文件内容一样"只是一个必要不充分条件——从那以后我每次改协议,都强制走一遍"乱序、重复、ACK 丢失、校验和损坏"四个用例,再干净的结果也经得起一次故意的破坏。这份资源的完整工程(编号 100010493)里,从停等到 GBN 再到 SR 的源码、丢包模拟工具、双向传输和文件传输应用都齐了,适合你拿去做同样的破坏性验证。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 10:52:13

ZigBee无线LED照明系统设计:从选型到部署的完整实践

做智能照明项目&#xff0c;很多人第一反应就是WiFi&#xff0c;结果灯一多就掉线、网关反复重启、手机App转圈半天没有反馈。我做这套无线LED照明系统的时候&#xff0c;前前后后几十个灯混在一个无线网络里&#xff0c;最后稳定跑起来的方案不是WiFi也不是蓝牙低功耗&#xf…

作者头像 李华
网站建设 2026/10/19 10:52:11

YOLOv8高空抛物监测项目:环境配置与运行全指南

简介&#xff1a;这套基于YOLOv8的社区高空抛物监测系统&#xff0c;以智慧社区安防为背景&#xff0c;面向计算机视觉方向的毕业设计、课程设计与项目初期演示&#xff0c;适合计科、人工智能、通信工程、自动化、电子信息等专业学生使用&#xff0c;也为零基础初学者提供了可…

作者头像 李华
网站建设 2026/10/19 10:48:39

C++与DirectX粒子系统实战:从零构建高性能烟花特效

1. 从零拆解一个烟花粒子系统的核心设计思路1.1 为什么选择粒子系统来做烟花在图形编程里&#xff0c;烟花特效几乎是每个想入门特效的人都会碰的一个经典项目。它看起来简单——不就是一堆点炸开然后落下来吗&#xff1f;但真正动手写过的人都知道&#xff0c;里面涉及的东西一…

作者头像 李华
网站建设 2026/10/19 10:46:22

循证收敛递归:让大模型推理过程可验证的开源方案

前阵子在一个AI技术社群里&#xff0c;看到有人提到“开源VeriLoop E2”这个名字&#xff0c;第一反应是这又是某个大模型评测框架的新版本。但真正把项目资料翻完之后&#xff0c;我发现自己之前的判断完全错了——它做的不是评测&#xff0c;而是远比评测更硬核的事情&#x…

作者头像 李华
网站建设 2026/10/19 10:46:15

Spring Boot 3配置文件加密实战:用Jasypt告别明文密码

做后端这几年&#xff0c;配置文件的明文凭据问题迟早会找上门。数据库密码、Redis口令、短信服务密钥&#xff0c;全部摊在application.yml里&#xff0c;稍微一不留神就跟着代码一起进仓库。我两年前在一个老项目里吃过这个亏&#xff0c;安全扫描一出来&#xff0c;审计那边…

作者头像 李华
网站建设 2026/10/19 10:45:44

DQL查询优化:从执行顺序到索引利用的实战指南

聊到数据库操作&#xff0c;DQL&#xff08;Data Query Language&#xff0c;数据查询语言&#xff09;是绝大多数开发者的第一站&#xff0c;也是日常开发中最容易出细节问题的一块。很多人能写出成百上千行的SELECT语句&#xff0c;却不一定会去琢磨一条查询在数据库内部是怎…

作者头像 李华