简介:基于Python实现可靠数据传输协议的课程设计资源包,面向计算机网络、协议设计方向的学生与研究者。资源以UDP作为底层传输承载,完整实现了停等协议、GBN滑动窗口协议及SR协议改进,覆盖单向/双向可靠数据传输、模拟丢包验证以及C/S文件传输等实验场景,适合作为计算机网络课程设计的完整参考方案。压缩包共14个文件,含8个Python源码、3个测试数据TXT、实验报告DOCX、项目说明MD及开源许可证;核心代码按client与server模块组织,配置与数据分离,便于直接运行和二次修改。压缩包整体仅723KB,轻量易部署;目前已有595人学习下载,性价比较高。读者可从中获得可运行的协议实现、完整实验报告、文件传输示例及分组丢失模拟思路,能显著缩短同类课程设计的开发与排错时间。
1. 基于Python的UDP可靠数据传输协议包:停等与GBN课程设计的一次到齐
计算机网络课程设计里最难缠的一类实验,就是让你在UDP之上自己实现可靠传输。表面上看只是让数据从A传到B,实际要把序号、校验和、确认包、超时重传、滑动窗口全在应用层重造一遍。我这次拆的资源包编号 100010493,是一个基于Python实现的可靠数据传输协议工程,源码里同时包含停等协议和GBN协议两个完整实现,还带随机丢包模拟、双向传输和文件传输示例,适合正在做课程设计、或者在学TCP原理但想亲手写一遍滑动窗口的开发者。
整个包的核心代码集中在 src/protocol 下,server.py 和 client.py 分别扮演发送端和接收端,util/config.py 控制端口、超时、丢包率等关键参数。包内预置了 data/server_data.txt 和 data/client_data.txt,接收结果写入 recv/client_recv.txt。我实际在本地把服务端和客户端跑通后,对比了原文件和接收文件,确认协议在模拟丢包的情况下完成了数据可靠交付。这篇笔记我会按源码结构、停等实现、GBN实现、丢包验证和SR改造这条线讲透,每一步都能照着复现。
2. 拆包看文件:server.py、client.py 与 config.py 的职责和启动流程
拿到压缩包先别急着双击运行,第一步是把文件结构读明白。这个包的文件组织方式很接近一个最小可运行的协议工程,不是把代码堆在一个脚本里,而是按发送端、接收端、配置和实验数据分开管理,这一点对后面改协议、调参数特别友好。
2.1 压缩包里的文件结构,先看清每个文件是干什么的
按我在本地解压后看到的目录树来梳理:
datatransferprotocol/ ├── LICENSE ├── README.md ├── report.docx ├── data/ │ ├── client_data.txt │ └── server_data.txt ├── recv/ │ └── client_recv.txt └── src/ └── protocol/ ├── server.py ├── client.py └── util/ └── config.pyserver.py 是发送端程序,负责读取 data 目录下的文件内容,按协议把数据包发出去,同时处理确认包和超时重传。client.py 是接收端程序,负责接收数据包、校验序号、写接收到的数据到 recv/client_recv.txt。util/config.py 是一个集中配置模块,端口号、缓冲区大小、超时时间、丢包率这些都在这里设置。
data 目录下的两个 txt 是预先准备的源数据,发送端从这里读数据,接收端落盘到 recv 目录。report.docx 是实验报告,README.md 说明了项目的基本使用方式。LICENSE 是许可文件,课程设计场景通常不用管它。把数据文件和代码分开的好处是,想验证协议正确性时,直接对比 data 里的源文件和 recv 下的接收文件,就能判断数据有没有在“不可靠”的 UDP 通道里被完整保护。
2.2 config.py 里的参数,哪些直接影响协议行为
config.py 是整套代码里改动最频繁的文件。我拆包后第一件事就是把每个参数和它在协议里承担的作用对应起来:
# src/protocol/util/config.py import random # 本地测试用的地址与端口 SERVER_ADDR = ("127.0.0.1", 8000) # UDP 缓冲区大小,一次最多读入多少字节 BUFFER_SIZE = 1024 # 等待 ACK 的超时时间,单位秒 TIMEOUT = 0.5 # 模拟丢包率,0.1 表示 10% 的包会被丢弃 LOSS_RATE = 0.1 # 协议模式:sw 表示停等协议,gbn 表示 GBN 协议 MODE = "sw" # GBN 窗口大小,允许在未确认情况下最多发多少个包 WINDOW_SIZE = 4SERVER_ADDR 是通信双方的绑定地址,课程设计一般都在本机跑,所以是 127.0.0.1,端口只要不被占用就行。BUFFER_SIZE 决定了每次 read 或 recvfrom 能处理的最大字节数,这里设 1024,意味着文件传输时会按 1KB 一个包切分。
TIMEOUT 是等待 ACK 的最长时间。设得太短,网络稍微一抖动就疯狂重发;设得太长,丢包后恢复速度又太慢。0.5 秒在本地环回测试下是合理值。LOSS_RATE 是丢包模拟开关,后面验证协议时会把 0.1 改成 0.3 甚至 0.5 测试协议的承压能力。WINDOW_SIZE 只有切到 GBN 模式才生效。
2.3 启动顺序和一次完整的数据流向
启动流程分两个终端,先启动服务端再启动客户端:
# 终端 1:启动服务端,开始监听 8000 端口 python src/protocol/server.py # 终端 2:启动客户端,连接服务端并开始接收数据 python src/protocol/client.py服务端启动后进入监听状态,客户端启动后主动向 SERVER_ADDR 发起请求。服务端收到客户端请求后,读取 data/server_data.txt,按配置把数据分成若干包,调用 UDP socket 发送。每次发送后进入等待确认状态,只有收到客户端回传的 ACK 才继续发送下一个包。客户端收到包后先校验序号和校验和,再把数据写入 recv/client_recv.txt,同时向服务端回复 ACK。
整个数据流向里最关键的机制是:发送端每发一个数据包,接收端都必须回一个确认包;如果确认包超时未到,发送端会重传。这就是可靠传输的底层逻辑,也是整个协议工程的核心。
3. 在 UDP 上落地停等协议:报文格式、校验和与超时重传的设计
停等协议是最直观的可靠传输方案:发送一个包,等确认,再发送下一个包。在 TCP 里这些机制已经被内核封装好了,但在 UDP 上你得自己写。这一章我会把停等协议最核心的三个要素讲透:应用层报文格式怎么定义、校验和怎么算、超时重传怎么处理。
3.1 先设计报文格式,UDP 只负责把字节流丢出去
UDP 层不管你的数据是什么,它只负责把一串字节发到目标端口。所以应用层必须在自己的报文里设计足够的信息,让接收端能判断这个包是不是重复的、数据有没有被损坏。我在这个项目里看到的设计思路是这样的:
# 构造一个应用层数据包 import hashlib def make_packet(seq: int, data: bytes) -> bytes: # 包格式:序号 | 校验和 | 数据 checksum = hashlib.md5(data).hexdigest() header = f"{seq}|{checksum}|".encode() return header + data def parse_packet(packet: bytes): # 解析时按分隔符拆分,前两段是序号和校验和 parts = packet.split(b"|", 2) seq = int(parts[0]) checksum = parts[1].decode() data = parts[2] return seq, checksum, data这个设计里,每个包由三部分组成:序号、校验和、真实数据。序号用来识别包的身份,确保接收端不会把同一个包当新包处理;校验和用 MD5 对整个数据部分计算,接收端重新计算一次并比较,不一致就认为数据在传输过程中被损坏,直接丢弃。分隔符用|,因为它在正常文本数据里出现概率相对低,而且 split 按 2 次切分之后,数据部分无论多大都能完整保留。
MD5 在实际协议里并不算强校验,胜在计算快、写起来简单。课程设计场景里完全够用,关键是让接收端具备“发现数据坏了”的能力。
3.2 停等协议发送端的核心逻辑:发一包、等一包
发送端要做的动作可以抽象成这个循环:
import socket class StopWaitSender: def __init__(self, sock: socket.socket, addr: tuple, timeout: float = 0.5): self.sock = sock self.addr = addr self.timeout = timeout self.seq = 0 def send_packet(self, data: bytes): pkt = make_packet(self.seq, data) while True: # 发送当前包 self.sock.sendto(pkt, self.addr) self.sock.settimeout(self.timeout) try: # 等待 ACK ack, _ = self.sock.recvfrom(64) ack_seq = int(ack.decode().split("|")[1]) if ack_seq == self.seq: self.seq += 1 break except socket.timeout: # 超时未收到ACK,重发同一个包 print(f"seq {self.seq} 超时,重传")逻辑很直接,send_packet里有一个死循环,循环里先把当前包发出去,然后阻塞等待 ACK。收到 ACK 后会检查确认的序号是否和当前包的序号一致,一致说明接收端已经正确收到,发送端推进序号并跳出循环。如果recvfrom抛出socket.timeout,说明 ACK 在超时时间内没有回来,这时只能重传同一个包,注意序号不能变。
这里有个极容易犯错的设计点:重传时包的序号必须保持不变,直到收到对应 ACK 才让序号递增。如果重传时改了序号,接收端会认为来了一个新包,最终导致文件内容重复或错位。我在改别人代码时经常看到这个失误,重传统一用旧序号,这是协议可靠性的前提。
3.3 接收端逻辑和 ACK 回复的时机
接收端做的事情相对简单:收到包,解析,校验,落盘,回 ACK。
class StopWaitReceiver: def __init__(self, sock: socket.socket): self.sock = sock self.expected_seq = 0 def receive_packet(self) -> bytes: while True: packet, addr = self.sock.recvfrom(2048) seq, checksum, data = parse_packet(packet) # 重新计算校验和,不一致则丢弃包,不发ACK if hashlib.md5(data).hexdigest() != checksum: continue if seq == self.expected_seq: # 序号正确,接收数据,回复ACK ack = f"ACK|{seq}".encode() self.sock.sendto(ack, addr) self.expected_seq += 1 return data else: # 重复包,重新发送ACK,但不再写入数据 ack = f"ACK|{seq}".encode() self.sock.sendto(ack, addr)接收端维护一个expected_seq,只接受序号等于这个值的包。校验和没过直接丢弃,不回复 ACK,这样发送端迟早会因超时重发。序号正确则写入数据、推进expected_seq、回复 ACK。收到重复包时不能直接丢掉,而是要重新回复一次 ACK,因为很有可能之前的 ACK 在通道里丢失了,发送端才重传了这个包。
停等协议的可靠性就建立在这样一个简单的闭环上。缺点是吞吐量低:每个包都要等一个 RTT 才发下一个,本地测试不觉得慢,一旦模拟高延迟或高带宽环境,效率就会断崖式下降,这也是 GBN 和滑动窗口要解决的痛点。
4. 把停等提升为 GBN:滑动窗口与累计确认的实现要点
停等协议有一个天然瓶颈:每一轮都只有一个包在通道里,链路利用率很低。GBN(Go-Back-N)通过引入滑动窗口,允许发送端在没有收到确认之前连续发送窗口内多个包,然后靠累计确认和回退重传保证可靠性。这一章的代码实现和原理要对着看。
4.1 为什么滑动窗口能把吞吐量提上去,代价是什么
停等协议里,如果网络往返时间是 RTT,发送一个包耗时约等于 RTT + 发送时间,链路上大部分时间都是空的。滑动窗口的思路是,把一个 RTT 里能塞下多少包当作窗口大小,发送端在窗口未满时继续发,收到 ACK 后往前滑动窗口。
代价是出错时恢复更激进。GBN 接收端只按序接收数据,一旦某个序号的包丢了,接收端会丢弃所有乱序到达的包,而发送端超时后要把从丢失点往后的所有包重新发一遍。窗口越大,单次丢包重传的数据量越大。所以实际工程里窗口不是越大越好,要结合丢包率和带宽延迟积来权衡。课程设计里一般取 4 到 8 比较稳妥。
4.2 GBN 发送端:以 base 和 next_seq 两个游标组织窗口
GBN 的核心状态就是两个序号:base表示窗口里最老的未确认包,next_seq表示下一个要发送的新包。窗口内可发送的序号范围是[base, base + window - 1]。
class GbnSender: def __init__(self, sock: socket.socket, addr: tuple, window_size: int = 4, timeout: float = 0.5): self.sock = sock self.addr = addr self.window_size = window_size self.timeout = timeout self.base = 0 self.next_seq = 0 def send_packets(self, packets: list): while self.base < len(packets): # 尽可能把窗口内所有包发出去 while self.next_seq < self.base + self.window_size and self.next_seq < len(packets): self.sock.sendto(packets[self.next_seq], self.addr) self.next_seq += 1 self.sock.settimeout(self.timeout) try: ack, _ = self.sock.recvfrom(64) ack_seq = int(ack.decode().split("|")[1]) # GBN 采用累计确认 self.base = ack_seq + 1 except socket.timeout: # 超时后回退重传从 base 到 next_seq 的所有包 print(f"超时,重传窗口内 {self.base} 到 {self.next_seq - 1}") for i in range(self.base, self.next_seq): self.sock.sendto(packets[i], self.addr)发送循环分为两层:内层循环负责把窗口内还没发的新包连续 send 出去,直到填满窗口;外层循环阻塞等 ACK。关键区别在收到 ACK 时,停等协议要求确认的序号必须等于当前发送序号,而 GBN 使用累计确认:收到ACK n表示序号n及之前的包都已正确到达,所以直接把base置为ack_seq + 1。
超时重传是 GBN 的名字来源:一旦超时,发送端不能只补发一个包,而要从最老的未确认包base开始,把窗口内所有包全部重传。这样做的理由很朴素:接收端已经丢弃了所有乱序包,发送端无法知道哪些包还在,保守起见全部重发最安全。
4.3 GBN 接收端:只认按序到达的包,乱序包一律丢弃并回重复确认
接收端只需要维护一个变量expected_seq,凡是序号对它就是对,不对就丢弃并补发上一个正确包的 ACK:
class GbnReceiver: def __init__(self, sock: socket.socket): self.sock = sock self.expected_seq = 0 def receive_packet(self) -> bytes: while True: packet, addr = self.sock.recvfrom(2048) seq, checksum, data = parse_packet(packet) if hashlib.md5(data).hexdigest() != checksum: continue if seq == self.expected_seq: ack = f"ACK|{seq}".encode() self.sock.sendto(ack, addr) self.expected_seq += 1 return data else: # 序号不是期望值,直接丢弃,并重复确认上一个正确包 if self.expected_seq > 0: ack = f"ACK|{self.expected_seq - 1}".encode() self.sock.sendto(ack, addr) # 不返回数据,继续等待下一个包这个实现里,乱序包被丢弃后,接收端会补发一个针对expected_seq - 1的重复 ACK。发送端收到这个重复 ACK 后会怎么做?如果它的base恰好就是expected_seq,那ack_seq + 1等于当前base + 1吗?这里要说清一个细节:重复 ACK 指的是 ACK 序号和已经确认过的序号一致,base被设置成ack_seq + 1,如果这个值比当前base大才更新窗口。所以正确的 GBN 发送端在收到 ACK 后要做base = max(base, ack_seq + 1),否则重复 ACK 可能导致窗口倒退,造成不必要的重传。
我自己调试时用的是带注释版本的实现,把base的更新方式写成了if ack_seq + 1 > self.base: self.base = ack_seq + 1,这样才避免被重复 ACK 卡住。代码包里的实现我没逐一确认细节,但改造时这个边界值得单独盯着看。
5. 丢包模拟、协议验证与常见问题排查
协议写完了,怎么证明它可靠?直接把丢包率设成 0 跑一遍不算验证,只能证明“在无错环境下能跑通”。这一章重点讲随机丢包模拟、文件级正确性验证,以及我拆包和调试中遇到的几个高频坑。
5.1 在本地用 random 模拟 UDP 丢包
UDP 本身不保证送达,但本地环回测试正常情况下几乎不丢包。要验证协议对丢包的抵抗能力,必须主动在发送路径上制造丢包。最常用的是在发送函数入口加一个随机数采样:
import random def send_with_loss(sock, packet, addr, loss_rate=0.1): # 按概率直接丢弃包,模拟信道丢包 if random.random() < loss_rate: print("模拟丢包,丢弃一个数据包") return sock.sendto(packet, addr)把原来的sock.sendto(packet, addr)全部替换成send_with_loss(sock, packet, addr, LOSS_RATE),就能在应用层模拟出 10% 的随机丢包。注意这个丢包模拟应该只作用在数据包上,ACK 是否模拟丢包可以单独控制。课程设计里一般两边都模拟,因为真实信道的丢包是双向的。
跑实验时我习惯把 LOSS_RATE 分别调成 0.1、0.3、0.5,观察协议是否依然能完整传输文件,同时打印重传次数来感受协议在不同丢包率下的强度。
5.2 验证协议有效性的两个硬指标
协议是否可靠,不能靠肉眼观察,要靠结果对比。接收文件落盘在recv/client_recv.txt,原始数据在data/server_data.txt,做一个逐字节对比:
# 对比原始文件和接收文件 diff data/server_data.txt recv/client_recv.txt # 用 md5 校验,输出一致则说明传输内容完全相同 md5sum data/server_data.txt recv/client_recv.txt如果diff无输出且 md5 一致,说明在指定丢包率下协议成功实现了可靠传输。这一步是整个实验里最硬的证据,实验报告里可以直接贴上 md5 对比结果。
另一个指标是统计重传次数。发送端每个超时分支都加一个计数器,跑完同一份文件后对比不同 LOSS_RATE 下的重传次数,能直观看到丢包率与重传开销的关系。这个数据写进实验报告,比堆一堆运行截图更有说服力。
5.3 常见问题排查记录
调试 UDP 协议和调普通业务代码不一样,问题往往出现在肉眼看不到的字节流里。下面是这个场景下我最常遇到的几类问题,按“现象 → 原因 → 解决”记录。
第一条:客户端一直收不到数据,服务端反复打印超时重传
现象是服务端疯狂打印超时重传,客户端却一动不动。原因通常是客户端和服务端绑定的地址不一致,或者客户端根本没进入接收循环,服务端发的包要么被系统丢进别的端口,要么没人recvfrom。解决方式是先核对 config.py 里的 SERVER_ADDR,确认双方的地址和端口完全一致,并在服务端启动后先用netstat -an | grep 8000确认端口处于监听状态。
第二条:传小文件没问题,传大文件时程序进程崩溃
现象是换成几百 KB 的文件后,接收端内存暴涨或程序卡死。原因是发送端一次性把整个文件读入内存并拆成上万个包,接收端又为每个包申请缓存,资源消耗过大。解决方式是改成边读边发的流式模式,每次只读 BUFFER_SIZE 个字节,发完一个包处理一个包,不把整个文件载入内存。
第三条:LOSS_RATE 设为 0 时正常,设为 0.1 后传输永远结束不了
原因是 ACK 也参与了丢包模拟。数据包成功到达接收端,ACK 被随机丢弃,发送端永远等不到确认,重传又可能被随机丢弃。解决方式是在 ACK 通道上降低丢包率,或者只模拟数据包丢包。课程设计里,为了验证协议对数据丢失的处理能力,我一般只对数据包做丢包模拟,ACk 不算在丢包模型里。
第四条:接收端收到重复数据,文件内容被写了两遍
原因是接收端对重复包的处理逻辑错了。停等和 GBN 接收端遇到重复包时,必须重新回 ACK 但不写数据。很多人只加了 ACK 回复,忘记把写入文件的那段代码放到序号判断之外,导致重复包被当成新包再次落盘。解决方式是严格按seq == expected_seq判断是否写盘,重复包一律只回 ACK、不写数据。
6. 从 GBN 改成 SR 协议的一条稳妥路线
实验要求的最后一步通常是把 GBN 再改造成 SR(Selective Repeat,选择重传)。GBN 的痛点在于超时后要把整个窗口回退重传,丢包率一高,链路里全是重传包。SR 的改进思路是只重传真正丢失的那个包,接收端为乱序到达的包建缓冲区,按序号重组后再交付给上层。
6.1 到底要改哪些核心逻辑,我列一张改造清单
| GBN 实现 | SR 改造点 |
|---|---|
| 发送端只维护 base 和 next_seq | 每个序号独立计时或至少能标记哪个包超时 |
| 超时重传窗口内所有包 | 只重发超时的那一个序号 |
| 接收端丢弃乱序包 | 接收端为乱序包准备缓冲区 |
| ACK 是累计确认 | ACK 改为单独确认每个包 |
| 窗口只能整体滑动 | 窗口允许部分滑动,确认到哪就推进到哪 |
这个表是这个实验里区分 GBN 和 SR 的关键。SR 的复杂度不在发送端,而在接收端的缓冲区管理:每个序号到达后要缓存到对应位置,最后按序号顺序拼接,才能把数据正确写入文件。
6.2 一个可参考的改造路径,先接收端后发送端
我建议先改接收端,再改发送端。接收端维护一个长度为window_size的接收缓存,收到的包如果序号落在窗口内就暂存,同时回一个带序号的 ACK;只有当一个连续序列从expected_seq开始被填满时,才把这些数据提交给文件写入。发送端则把超时处理改成按需重传:
class SrSender: def __init__(self, sock, addr, window_size=4, timeout=0.5): self.sock = sock self.addr = addr self.window_size = window_size self.timeout = timeout self.base = 0 self.next_seq = 0 # 记录每个序号的发送时间,用于判断单个包是否超时 self.send_time = {} def resend_packet(self, seq): # 只用重新发送 seq 这个包,不波及窗口内其他包 packet = self.packets[seq] self.sock.sendto(packet, self.addr) self.send_time[seq] = time.time()改造完后建议只把 WINDOW_SIZE 调到 2 到 4 做功能验证,窗口太大,缓冲区管理出错时很难定位。每次跑通后强制用md5sum对比源文件和接收文件,一次也不落下。
我从第一次写 UDP 可靠传输到现在养成的习惯是:任何协议改动,不管多小,都要跑一遍标准验证流程——设置丢包率、传输文件、比对 md5、看重传统计。这个习惯帮我避开了很多“看起来能跑、实际校验不过”的隐性翻车。希望这篇笔记能帮你把这个课程设计做扎实,从代码到实验报告都有底气。
本文还有配套的精品资源,点击获取