ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python实现UDP可靠传输:停等协议与GBN滑动窗口课程设计拆解

基于Python实现UDP可靠传输:停等协议与GBN滑动窗口课程设计拆解 简介基于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(fseq {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 fACK|{seq}.encode() self.sock.sendto(ack, addr) self.expected_seq 1 return data else: # 重复包重新发送ACK但不再写入数据 ack fACK|{seq}.encode() self.sock.sendto(ack, addr)接收端维护一个expected_seq只接受序号等于这个值的包。校验和没过直接丢弃不回复 ACK这样发送端迟早会因超时重发。序号正确则写入数据、推进expected_seq、回复 ACK。收到重复包时不能直接丢掉而是要重新回复一次 ACK因为很有可能之前的 ACK 在通道里丢失了发送端才重传了这个包。停等协议的可靠性就建立在这样一个简单的闭环上。缺点是吞吐量低每个包都要等一个 RTT 才发下一个本地测试不觉得慢一旦模拟高延迟或高带宽环境效率就会断崖式下降这也是 GBN 和滑动窗口要解决的痛点。4. 把停等提升为 GBN滑动窗口与累计确认的实现要点停等协议有一个天然瓶颈每一轮都只有一个包在通道里链路利用率很低。GBNGo-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凡是序号对它就是对不对就丢弃并补发上一个正确包的 ACKclass 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 fACK|{seq}.encode() self.sock.sendto(ack, addr) self.expected_seq 1 return data else: # 序号不是期望值直接丢弃并重复确认上一个正确包 if self.expected_seq 0: ack fACK|{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_rate0.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 再改造成 SRSelective 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_size4, timeout0.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、看重传统计。这个习惯帮我避开了很多“看起来能跑、实际校验不过”的隐性翻车。希望这篇笔记能帮你把这个课程设计做扎实从代码到实验报告都有底气。本文还有配套的精品资源点击获取
返回列表