☰
基于UDP的可靠传输:GBN/SW/SR协议源码与滑动窗口实现解析
2026/10/1 10:34:59 网站建设 项目流程

简介:一份基于Python实现的可靠数据传输协议实验/课程设计资源,面向计算机网络课程学生与需要完成UDP可靠传输实验的开发者,覆盖停等协议、GBN协议、SR协议三个层次的协议设计与实现。资源内含完整设计报告(Word)、Python源码及测试数据文件,共14个文件,以8个.py源码为主,辅以3个txt数据文件、md说明、License等,整体仅493KB,适合直接阅读和运行验证。包内代码模块划分清晰,包含server/client、protocol(SW/GBN/SR)以及config、util、device等辅助模块,可帮助读者掌握基于UDP的单向/双向可靠数据传输、丢包模拟、文件传输C/S应用等关键点;设计报告与源码结合,便于对照原理、复现实验并完成课程报告。已有1014人学习/下载,适合需要快速上手网络协议仿真与课程设计的同学。

1. 可靠数据传输协议:一份能跑通的 UDP 课程设计与三套协议源码

如果你接触过计算机网络,一定听过“UDP 不可靠”这句话,可真要你在 UDP 之上实现一个可靠数据传输协议,很多人第一反应是懵的:套接字发出去就完事了,怎么重传、怎么确认、怎么处理乱序,教材里画得清楚,代码里全是坑。这份资源就是冲着这个来的——它是一套完整的基于 Python 的可靠数据传输协议工程,包含设计报告、可运行的 server/client 源码、GBN/SW/SR 三套协议实现,以及发送和接收两端的实测数据文件。实验任务从最基础的停等协议起步,逐步推进到 GBN 滑动窗口、SR 选择重传,最后扩展成支持双向数据传输的 C/S 文件传输应用。适合正在做计网课程设计、需要验证滑动窗口机制、或者想抄一份完整协议代码来改的学生和从业者。

2. 协议分层与代码包结构:先看清 GBN、SW、SR 三个协议的边界

2.1 从文件结构反推实验设计:src 下每个模块在干什么

拿到资源后先别急着跑,先把目录结构理清楚。这份资源的文件组织方式是实验导向的,根目录下有设计报告、数据目录、源码目录和接收结果目录,按照“数据准备 → 协议实现 → 收发验证”三段组织。

路径作用
设计报告.docx完整实验报告,包含停等/GBN/SR 的设计思路、验证数据
data/client_data.txt客户端侧数据文件,双向传输扩展时使用
data/server_data.txt服务器侧发送的源数据文件
recv/client_recv.txt客户端接收到的数据文件,用于与源文件对比
src/config.py全局参数:端口、缓冲区、丢包率、窗口大小、序号空间
src/util/device.py模拟网络层的丢包逻辑
src/data.py数据拆包与重组,负责把文件切分成定长载荷
src/server.py服务端入口,负责按协议发送数据
src/client.py客户端入口,负责接收并按序写入本地文件
src/protocol/SW.py停等协议实现,单包等待确认
src/protocol/GBN.pyGBN(后退 N 帧)实现,滑动窗口连续发送
src/protocol/SR.pySR(选择重传)实现,接收端缓存乱序包

实验任务顺序是:停等 → 验证丢包 → 改进为双向 → 升级为 GBN → 再升级为 SR。从文件结构能看出来,protocol/目录把三种协议完全隔离,server.py和client.py只是调用协议的入口,这种解耦方式值得照抄,因为后面你想单独验证某一个协议,只需要改入口函数的引用。

2.2 config.py 与 socket 二次封装:UDP 收发的统一入口

所有协议共用一个config.py,这是我的习惯——把丢包率、超时、窗口大小全部参数化,跑验证时只需要改数值,不用动协议代码。参数含义如下:

# config.py:协议参数与丢包模拟开关 HOST = "127.0.0.1" # 本机回环地址,便于课程设计验证 PORT = 8888 # 服务器监听端口 BUFFER_SIZE = 2048 # 单包载荷上限,决定数据拆包粒度 LOSS_RATE = 0.2 # 随机丢包率,0 表示不丢包 TIMEOUT = 1.0 # 超时重传等待时间,单位秒 WINDOW_SIZE = 8 # GBN / SR 公共窗口大小 MAX_SEQ = 31 # 序号空间上限,必须满足 >= 2 * WINDOW_SIZE

BUFFER_SIZE决定了文件被切成多大的载荷,字符串“可靠传输协议测试数据”每个字符在 UTF-8 下可能占 3 字节,所以 2048 并不等于能装 2048 个字符,实际拆包时要按文件读取的字节数算。MAX_SEQ = 31是经验值,等于 32 个不同序号,配合 8 的窗口大小,能保证序号环绕后新旧分组可区分,这个关系在避坑章节会专门展开。

UDP socket 的收发本身只有sendto和recvfrom,但可靠协议要求每一层都走统一的报文构造和解析,避免server.py里直接操作 socket。常见做法是封装一对收发函数,协议模块只负责拼报文,底层统一处理字节序和超时:

# util/device.py 中的统一收发封装(简化版) import socket import struct def send_unreliable(sock, packet, addr, loss_rate): """模拟不可靠链路:按概率丢弃数据包""" if loss_rate > 0 and random.random() < loss_rate: return False # 模拟丢包,实际未发送 sock.sendto(packet, addr) return True def recv_with_timeout(sock, timeout): """带超时的接收,超时返回 None 供上层触发重传""" sock.settimeout(timeout) try: data, addr = sock.recvfrom(2048) return data, addr except socket.timeout: return None, None

这段代码的逻辑很直白:send_unreliable在真正 sendto 之前按loss_rate丢弃一部分包,模拟网络层的随机丢包;recv_with_timeout把超时变成返回值,这样协议层不需要异常处理就能判断“该重传了”。注意丢包一定要放在应用层协议之下,也就是说 ACK 包同样会丢,如果只丢数据不丢 ACK,测出来的重传机制是假的。

2.3 报文格式与丢包模拟:ACK 和数据怎么区分

三种协议共用一套报文格式,这是整个资源里最值得复用的设计。报文用二进制结构体拼接,头部包含序号、类型标志和校验段,接收端先解析头部再决定走数据分支还是 ACK 分支:

# 报文格式:seq(4字节) + flag(1字节) + payload(可变) # flag 含义:0=DATA 数据包,1=ACK 确认包 def build_packet(seq, flag, payload=b""): header = struct.pack("!IB", seq, flag) return header + payload def parse_packet(packet): seq, flag = struct.unpack("!IB", packet[:5]) return seq, flag, packet[5:]

!IB分别是大端序的无符号整型和无符号字节,网络字节序统一用!,避免不同机器解析错位。seq是发送方分配的序号,flag决定报文类型,ACK 报文的 payload 为空,只携带确认序号。这个格式虽然简单,但足够支撑停等、GBN、SR 三种协议的演进,因为三种协议的区别只在窗口管理和确认策略上,报文本身不用换。

3. 停等协议到 GBN:把单包等待改成 8 格滑动窗口

3.1 停等协议的吞吐瓶颈:1 个包等一个 RTT

停等协议的逻辑是最容易写的:发送方发一个包,等 ACK,收到后再发下一个。它的正确性没问题,但性能有硬伤——每一轮传输都要空等一个完整的 RTT。假设带宽 10 Mbit/s、RTT 30ms、包长 2048 字节,链路利用率不到 3%,剩下 97% 的时间都在等。这就是题目要求“改进停等协议”的原因。

GBN 的改进思路是允许发送方在未收到 ACK 时连续发送多个包,用两个指针描述窗口:base是最老的未确认序号,next_seq是下一个待发送序号。窗口大小是 8,意味着最多允许 8 个包同时在链路上飞行。发送方一次把 8 个包全部塞进 socket,然后等待 ACK 推进窗口,这个推进过程就是协议的核心。

3.2 GBN 发送方:base/next_seq 与超时重发整个窗口

GBN 发送方维护发送窗口缓冲区,每个发出的包都存一份副本,超时后把窗口内所有未确认的包重新发一遍。从src/protocol/GBN.py的结构看,资源用的正是这个经典实现:

# protocol/GBN.py 发送方核心逻辑 class GBNSender: def __init__(self, sock, peer, window, max_seq): self.sock = sock self.peer = peer self.window = window self.mod = max_seq + 1 self.base = 0 # 窗口下沿,最老的未确认序号 self.next_seq = 0 # 窗口上沿,下一个待发序号 self.packets = {} # seq -> 完整报文,超时重传用 def send_one(self, payload): # 窗口已满:等待 ACK 推进再继续发送 while (self.next_seq - self.base) >= self.window: self._wait_ack_loop() seq = self.next_seq % self.mod packet = build_packet(seq=seq, flag=0, payload=payload) self.packets[seq] = packet self.sock.sendto(packet, self.peer) self.next_seq += 1 def _handle_ack(self, ack_seq): # 累积确认:ack_seq 表示该序号之前的包都已正确到达 self.base = max(self.base, (ack_seq + 1) % self.mod) # 清理窗口内已确认的缓冲区 for seq in list(self.packets.keys()): if (seq - self.base) % self.mod < self.window: continue self.packets.pop(seq, None) def _timeout_retransmit(self): # 超时重发 base 到 next_seq 之间所有未确认分组 for seq in range(self.base, self.next_seq): self.sock.sendto(self.packets[seq % self.mod], self.peer)

这段代码的while轮询是简化写法,真实工程里建议用select或threading.Timer做超时事件。(next_seq - base) >= window是窗口满的判据;_handle_ack里对seq做模运算,因为序号环绕后 31 后面是 0。_timeout_retransmit是 GBN 和停等的最大区别,停等只重发一个包,GBN 重发整个窗口——因为接收方只按序接收,窗口里一旦有包丢失,后续所有到达的包都会被丢弃,重发后续包没有意义。

3.3 GBN 接收方:累积确认与乱序丢弃

GBN 的接收方极其“挑剔”,它只接受恰好等于期望序号的包,其他全部丢弃并回传最后一个正确接收的 ACK。这样做的目的是告诉发送方“我已经确认到这个位置,再往后的都白发了”。

# protocol/GBN.py 接收方核心逻辑 def gbn_receive(sock, expected_seq, max_seq): packet, addr = sock.recvfrom(2048) seq, flag, payload = parse_packet(packet) if flag == 1: # ACK 包交给上层处理 return expected_seq, None, seq if seq == expected_seq: # 数据包按序到达,确认并推进 ack = build_packet(seq=expected_seq, flag=1) sock.sendto(ack, addr) expected_seq = (expected_seq + 1) % (max_seq + 1) return expected_seq, payload, None else: # 乱序包直接丢弃,重发累计确认 ack = build_packet(seq=(expected_seq - 1) % (max_seq + 1), flag=1) sock.sendto(ack, addr) return expected_seq, None, None

注意 ACK 携带的序号不是“刚收到的包的序号”,而是“期望下一个收到的序号”。当收到 seq=5 但期望是 6 时,回传的 ACK 序号是 5,表示“5 及 5 之前的都收到了,给我发 6”。这个累积确认语义是 GBN 的识别特征,到了 SR 协议会被彻底替换成选择性确认。

4. GBN 改进为 SR:接收端缓存乱序分组的编排方法

4.1 GBN 的重复重传代价与 SR 的窗口限制

GBN 在丢包率低的时候表现很好,因为偶尔丢一个包,重发整个窗口的成本可以接受。但 WAN 环境丢包率一旦超过 1%,GBN 的吞吐会断崖式下跌:最坏情况下,每个窗口都要整窗重发。SR 协议(Selective Repeat,选择重传)把“重发整个窗口”改成“只重发缺失的那一个包”,接收端需要把乱序到达的包缓存下来,等缺失包补齐后再整体交给上层。

教材里反复强调 SR 的约束:发送窗口和接收窗口的大小不能超过序号空间的一半。原因在于序号空间有限,如果窗口太大,接收方无法区分一个到达的序号是新包还是迟到的重传包。这份资源里MAX_SEQ = 31、WINDOW_SIZE = 8正好满足MAX_SEQ + 1 >= 2 * WINDOW_SIZE,也就是 32 大于等于 16,留了一倍冗余。

4.2 SR 发送方:独立计时器与选择性重传

SR 发送方不再“一超时全重发”,而是为每个在窗分组维护独立的超时状态。收到某个分组的 ACK 后,只移除该分组的缓冲区,窗口推进的条件是base指向的分组已经被确认。

# protocol/SR.py 发送方核心逻辑 class SRSender: def __init__(self, sock, peer, window, max_seq, timeout): self.sock = sock self.peer = peer self.window = window self.mod = max_seq + 1 self.base = 0 self.next_seq = 0 self.packets = {} # seq -> (packet, sent_time) self.timeout = timeout def _handle_ack(self, ack_seq): # 选择性确认:只移除 ack_seq 对应的分组 if ack_seq in self.packets: del self.packets[ack_seq] # 推进 base:从当前 base 开始连续确认过的序号 while self.base not in self.packets and self.base != self.next_seq: self.base = (self.base + 1) % self.mod def _check_timeout(self): # 每个超时的分组单独重发,不影响其他分组 now = time.time() for seq, (packet, sent_time) in list(self.packets.items()): if now - sent_time > self.timeout: self.sock.sendto(packet, self.peer) self.packets[seq] = (packet, now)

和 GBN 的_handle_ack对比,差别在确认粒度:GBN 收到 ACK=5 会把 base 直接推进到 6,SR 收到 ACK=5 只移除 5,如果 4 还没被确认,窗口还是不会动。_check_timeout里的字典遍历是逐包检查,真实实现里可以用最小堆或红黑树管理超时时间,避免每次全量扫描。

4.3 SR 接收方:缓存去重与按序交付

SR 的接收方比 GBN 复杂得多,它要维护一个接收窗口、一张缓存表和rcv_base指针。合法到达的分组落在窗口内才缓存,窗口外直接丢弃;缓存命中后从rcv_base开始连续交付给上层文件写入模块。

# protocol/SR.py 接收方核心逻辑 class SRReceiver: def __init__(self, window, max_seq): self.window = window self.mod = max_seq + 1 self.rcv_base = 0 # 接收窗口下沿 self.cache = {} # seq -> payload def accept(self, seq, payload, addr, sock): # 去重:窗口内已经缓存过的分组直接忽略 if seq in self.cache: return [] distance = (seq - self.rcv_base) % self.mod if 0 <= distance < self.window: self.cache[seq] = payload ack = build_packet(seq=seq, flag=1) sock.sendto(ack, addr) # 从 rcv_base 开始连续取出可交付的分组 delivered = [] while self.rcv_base in self.cache: delivered.append(self.cache.pop(self.rcv_base)) self.rcv_base = (self.rcv_base + 1) % self.mod return delivered

这段代码的distance = (seq - self.rcv_base) % self.mod是判断 seq 是否落在接收窗口内的标准写法,窗口内条件0 <= distance < self.window保证了对环形序号空间的处理。注意到 ACK 回传的是当前收到的分组本身的 seq,而不是 GBN 里的“期望序号”,这正是选择性确认的核心。delivered返回的是按序排列的数据块列表,上层可以安全地按顺序写文件了。

5. 避坑与常见问题:丢包率、序号空间与文件写入脏数据

5.1 序号空间死锁:窗口边界判断错误导致协议假死

现象:大文件传输过半后,server 端疯狂重传,client 端不再回复 ACK,日志里重复出现相同 seq,最后整个传输卡死。

原因:MAX_SEQ设置太小,窗口大小和序号空间的比例失衡。比如MAX_SEQ=7、WINDOW_SIZE=8,序号空间只有 8 个数,窗口也是 8,序号环绕之后接收方完全无法判断收到的是新分组还是超时重传的旧分组,于是去重和确认逻辑全部错乱。

解决:序号空间必须不小于窗口大小的两倍。GBN 和 SR 都要求MAX_SEQ + 1 >= 2 * WINDOW_SIZE,直接用资源里的MAX_SEQ=31配WINDOW_SIZE=8就没问题。我一般会把 MAX_SEQ 写成2 * WINDOW_SIZE * 2 - 1,留更多余量。

5.2 固定超时导致吞吐崩盘:丢包率一高就重传风暴

现象:LOSS_RATE=0.1时一切正常,调到 0.3 后收到大量重复 ACK,重传次数几十倍上涨,吞吐接近归零。

原因:超时时间固定为 1 秒,丢包率升高后实际 RTT 波动加大,重传分组本身也被丢弃,发送方等不到 ACK,只能继续叠加重传。固定超时没有感知链路实时状态,重传风暴不可避免。

解决:做一个简单的 RTO 估算,每次收到 ACK 后更新超时时间。常见做法是加权平均:rto = (1 - alpha) * rto + alpha * rtt,alpha 取 0.125,超时触发后把 rto 乘 1.5 做指数退避,避免同一包的多次重传互相碰撞。

5.3 重复分组写入脏数据:文件比源文件大,尾部多出一截

现象:接收端收到的数据总量超过源文件大小,client_recv.txt和server_data.txt对比时尾部多出重复内容。

原因:ACK 丢失触发重传,接收方收到重复分组。GBN 接收方因为只接受期望序号,天然完成去重;但换成 SR 后,缓存表虽然去重了,上层文件写入逻辑如果没有按“交付顺序”写,而是遇到数据就追写,重复交付的数据就从缓存里漏进了文件。

解决:文件写入必须和协议交付挂钩。SR 接收方的accept返回delivered列表,上层只对这个列表里的数据做顺序写入,缓存的乱序包不落盘。另外可以在文件写入时按 seq 映射偏移:offset = seq * BUFFER_SIZE,这样即使重复写入,覆盖的是同一个位置,不会让文件变大。

5.4 丢包模拟放在错误的位置:丢包率设置后测不出任何重传

现象:把LOSS_RATE=1,理论上全部丢包,但运行结果却是一切正常,文件完整接收,重传次数为零。

原因:丢包逻辑被写在了协议层之上的业务函数里,比如 server 发完数据后自己造了个“发送成功”的日志,真正的sendto从来没有链路层的丢包。这种错误最常见的原因是直接在server.py里做随机丢弃,而util/device.py的丢包函数没有被调用。

解决:丢包必须放在统一收发函数里,并且要影响两个方向——数据包和 ACK 包都要经过丢包判断。用send_unreliable替换所有直接sendto的调用点。验证方法是把LOSS_RATE调成 0.3 跑一次,统计重传次数,如果不为 0 说明丢包通路生效。

5.5 双向传输时 ACK 和数据互相污染

现象:按题目要求把停等协议改成双向传输后,一个方向数据全部超时,另一个方向收到的数据出现乱码。

原因:双向传输共用一个 socket,接收循环里没有区分包类型。数据包和 ACK 包到达后都被当成数据处理,ACK 的代码被写进文件。

解决:接收后先解析 flag,flag==1走 ACK 处理分支,flag==0才进入数据交付。不要靠“包长度”判断类型,因为载荷长度可能为 0,也会和 ACK 混淆。按照parse_packet返回的三元组做分支,是最可靠的。

6. 验证协议正确性:丢包梯度对照与文件一致性校验

6.1 回归验证流程:先跑零丢包基线,再跑梯度丢包率

协议改完,第一步不是调参数,而是先验证正确性。我习惯把验证流程固定成三步:先把LOSS_RATE改成 0 跑一遍,确认无丢包环境下三种协议都能完整传输;再逐步把丢包率调成 0.1、0.2、0.3,观察重传次数是否随之上升;最后对比收发文件的哈希值,只有字节级一致才算通过。

# 第一步:零丢包基线验证 python src/server.py python src/client.py # 第二步:对比源文件与接收文件的一致性 python -c " import hashlib for path in ['data/server_data.txt', 'recv/client_recv.txt']: h = hashlib.sha256(open(path, 'rb').read()).hexdigest() print(path, h) "

跑通后把config.py里的LOSS_RATE改成 0.2 重跑一遍,如果协议实现正确,传输耗时和重传次数会明显上升,但文件哈希必须一致。这一步能同时验证丢包模拟是否生效、重传机制是否工作、去重和按序交付是否正确。记住:丢包率改到 0.3 以上时,一定要同步检查有没有触发序号环绕问题,这也是我坚持用MAX_SEQ = 31配WINDOW_SIZE = 8的原因。

6.2 把设计报告和实测数据做成对照表

课程设计答辩时最怕的是“代码能跑但说不出验证过程”。资源里的设计报告.docx提供了报告框架,我建议在报告里补一张三协议对照表:停等/GBN/SR 在相同丢包率下的重传次数、传输耗时、文件哈希是否一致。数据不用多,0、0.1、0.2 三档足够证明协议行为。实际测试下来,GBN 在低丢包率下超时重传次数略高于 SR,但接收方逻辑简单;SR 在高丢包率下重传包数明显更少,代价是缓存管理和交付逻辑复杂度翻倍。

从那以后我每次改协议,都强制自己先跑一遍零丢包基线,再跑梯度丢包,最后比对哈希。这套流程救过我很多次,尤其是从 GBN 改成 SR 那次,缓存去重逻辑折腾了一晚上,最后就是因为漏了基线测试,把 bug 归到了丢包上。希望帮到你。

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

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

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

立即咨询