ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python手写可靠传输协议:UDP实现停等与GBN

Python手写可靠传输协议:UDP实现停等与GBN 简介本资源是一份面向计算机网络课程设计与协议实践学习者的Python编程实战项目聚焦UDP底层可靠传输机制的原理理解与代码实现。内容覆盖停等协议、GBN回退N帧及SR选择重传三大经典可靠传输协议的设计与演进通过服务器-客户端双向通信、数据包丢失模拟、文件传输应用等实验环节帮助学习者深入掌握协议状态机、超时重传、确认机制与窗口管理等核心概念。资源包共14个文件含8个Python源码server.py/client.py/protocol模块等、3个测试数据文本、1份Word实验报告、1份README说明及LICENSE许可文件整体723KB结构清晰、模块解耦便于逐层调试与功能扩展。已有595人学习下载提供完整可运行代码、配置参数说明与典型错误处理逻辑适合高校网络课程实践、协议原理课设及自学进阶者快速上手并验证理论。1. 基于Python实现可靠数据传输协议这不是UDP封装而是亲手造一个“带确认的邮局”你写过socket.sendto()也抓过 Wireshark 里密密麻麻的 UDP 包——但当你发现服务端发了 100 个包客户端只收到 87 个且顺序全乱、重传无响应时才真正意识到UDP 本身不负责可靠而“可靠”这件事必须由你用 Python 一行行代码去定义、去兜底、去容错。这份编号为100010493的课程设计资源不是教你怎么调requests而是带你从零手搓两个工业级协议内核停等协议Stop-and-Wait和回退 N 帧GBN全部基于原始 UDP socket不依赖任何网络库黑盒。它包含完整可运行的server.py/client.py、真实模拟丢包/乱序的util.py、结构清晰的config.py参数控制层甚至附带一份.docx实验报告模板——这意味着你不仅能跑通还能直接填进课程作业。适合正在啃《计算机网络自顶向下方法》第3章、被 ACK 超时机制绕晕的新手也适合想验证 GBN 窗口滑动边界条件、调试base与nextseqnum竞态关系的进阶者。它不讲大道理只给你能python server.py启动、能改LOSS_RATE0.3看重传、能加print()日志定位丢包位置的实打实代码。2. 协议选型与架构拆解为什么用 UDP 实现可靠传输停等 vs GBN 的本质分水岭在哪2.1 UDP 是“裸金属”可靠是“操作系统内核”——协议栈分层的硬约束很多人第一反应是“TCP 不就可靠吗干嘛自己造” —— 这恰恰是本项目最核心的教学锚点。TCP 是内核态协议你无法修改其重传逻辑、窗口策略或 ACK 生成时机而本项目强制使用 UDP等于把协议栈“剥开一层”让你在用户态亲手实现超时重传RTO、序列号管理、ACK 确认、滑动窗口、累计确认这五根支柱。server.py中的send_packet()不是简单发数据而是先构造含seq_num,ack_num,checksum,data的自定义报文头client.py的recv_packet()则要校验 checksum、解析 seq/ack、触发重传定时器——这和 Linux 内核里tcp_transmit_skb()的职责完全同构只是用 Python 字节操作替代了 C 语言指针偏移。提示所有协议报文都采用固定长度头部12 字节前 4 字节为seq_numint32中间 4 字节为ack_numint32后 4 字节为checksumint32。data部分紧随其后最大长度由config.MAX_PACKET_SIZE1024限定。这种设计规避了 TLV 解析的复杂性让新手能专注逻辑而非序列化。2.2 停等协议单帧流水线的“最简可靠模型”停等协议是理解可靠传输的起点它的核心约束极其朴素发送方每发一帧必须等待接收方返回 ACK才能发下一帧。在src/protocol/stop_and_wait.py中这一逻辑被压缩为三个关键状态WAIT_FOR_CALL_0: 发送方空闲等待上层调用rdt_send()WAIT_FOR_ACK_0: 已发 SEQ0启动定时器等待 ACK0WAIT_FOR_CALL_1: 收到 ACK0切换至发送 SEQ1# src/protocol/stop_and_wait.py 片段 def rdt_send(self, data): if self.state WAIT_FOR_CALL_0: packet make_pkt(0, data) # 构造 SEQ0 报文 self.udt_send(packet) self.start_timer(5.0) # 启动 5 秒超时 self.state WAIT_FOR_ACK_0 return True return False # 非空闲态拒绝发送这段代码暴露了停等协议的致命瓶颈信道利用率 1 / (1 2×RTT/TP)。当 RTT100ms、TP1ms 时利用率仅约 0.5%。但正是这种“低效”让你一眼看穿可靠性的代价——没有并发、没有窗口、没有累积确认只有最赤裸的“发-等-收”循环。client_data.txt和server_data.txt的内容差异如故意删掉某行会直接触发重传这是你调试的第一块试金石。2.3 GBN 协议滑动窗口的“批量确认引擎”当停等协议的吞吐量让你窒息时GBNGo-Back-N就是必然的进化。它的突破在于发送方维持一个大小为 N 的发送窗口可连续发送多帧而不必等待 ACK接收方只按序接收对失序帧直接丢弃并重复发送最新收到的 ACK即累计确认。在src/protocol/gbn.py中base指向窗口最左边界最早未确认帧nextseqnum指向窗口右边界下一个待发帧二者差值即为当前窗口大小# src/protocol/gbn.py 关键状态管理 def rdt_send(self, data): if self.nextseqnum self.base self.WINDOW_SIZE: # 窗口未满 packet make_pkt(self.nextseqnum, data) self.sndpkt[self.nextseqnum % self.WINDOW_SIZE] packet self.udt_send(packet) if self.base self.nextseqnum: # 若是窗口首帧启动定时器 self.start_timer(self.TIMEOUT) self.nextseqnum 1 return True return False # 窗口满阻塞发送注意self.sndpkt是一个环形缓冲区大小 WINDOW_SIZE用于暂存已发未确认的报文。当base推进时收到新 ACK对应缓冲区位置会被清空。config.WINDOW_SIZE4是默认值你可将其改为 1 来退化为停等协议——这正是本项目设计的精妙之处GBN 代码天然兼容停等只需改一个参数。2.4 双向传输改造ACK 不再是“影子”而是独立数据通道原始实验要求“服务器到客户单向传输”但真实场景必然是双向。项目中server.py和client.py的handle_packet()方法均支持双工处理当收到非 ACK 报文时调用rdt_send()回复业务数据当收到 ACK 时调用rdt_rcv()更新窗口状态。关键在于util.py中的create_ack_packet()函数# util.py def create_ack_packet(ack_num, checksumNone): if checksum is None: checksum compute_checksum(ack_num.to_bytes(4, big)) # ACK 报文seq_num0无意义ack_num实际确认号datab return struct.pack(!II, 0, ack_num) checksum.to_bytes(4, big)这里seq_num0是刻意为之——因为 ACK 本身不携带业务数据其序列号无业务意义但必须填充以保持报文格式统一。接收方通过packet[4:8]提取ack_num再与本地base比较决定是否滑动窗口。这种设计避免了为 ACK 单独建一套解析逻辑大幅降低耦合度。3. 运行环境与参数配置如何用 3 行命令启动一个可调丢包率的仿真环境3.1 最小依赖纯 Python 3.7零第三方库本项目刻意规避asyncio、twisted等异步框架所有 socket 操作均为阻塞式确保新手能用print()直接跟踪每一步。唯一依赖是标准库socket: UDP 通信基石struct: 二进制报文打包/解包!II表示大端 2 个 uint32time: 定时器控制time.time()记录超时起点threading: 模拟并发Timer类实现超时回调# 验证环境无需 pip install python -c import socket, struct, time, threading; print(✅ 环境就绪)注意Windows 用户若遇OSError: [WinError 10013]请以管理员身份运行 CMDLinux/macOS 用户需确保端口未被占用默认config.PORT12000。3.2 核心配置文件config.py是你的协议“控制台”所有可调参数集中于config.py这是你掌控仿真的唯一入口参数名默认值作用说明修改建议PORT12000UDP 端口号多实例运行时需修改避免冲突MAX_PACKET_SIZE1024单包最大载荷字节调小可测试分片逻辑调大测吞吐TIMEOUT5.0超时重传阈值秒网络延迟高时调大避免误重传WINDOW_SIZE4GBN 发送窗口大小设为 1 退化为停等设为 10 测高并发LOSS_RATE0.1丢包率0.0~1.00.0关闭丢包0.3强压力测试CORRUPT_RATE0.05数据损坏率校验和失效0.0关闭损坏0.1测 checksum# config.py 示例激进测试配置 PORT 12001 TIMEOUT 2.0 # 缩短超时加速失败反馈 WINDOW_SIZE 8 LOSS_RATE 0.3 # 30% 丢包足够触发重传风暴 CORRUPT_RATE 0.0 # 先关损坏聚焦丢包逻辑修改后无需重启server.py和client.py会自动读取新值。这是课程设计区别于玩具项目的标志参数即实验变量每一次运行都是可控的网络压力测试。3.3 启动流程从单向传输到双向文件交换的四步演进步骤 1验证停等协议单向传输基础# 终端1启动服务端监听 12000 端口 cd src python server.py --mode stop_and_wait # 终端2启动客户端连接 12000 端口 cd src python client.py --mode stop_and_wait观察server_data.txt是否完整复制到client_recv.txt。若失败检查util.py的lossy_channel()是否生效。步骤 2注入丢包观察重传日志在config.py中设LOSS_RATE0.2重启服务端。你会看到服务端日志出现[SERVER] Sent packet SEQ5, waiting for ACK5... [SERVER] Timeout! Resending SEQ5...这证明超时重传机制已激活。步骤 3切换 GBN 模式对比吞吐差异# 终端1GBN 服务端 cd src python server.py --mode gbn # 终端2GBN 客户端 cd src python client.py --mode gbn对比相同server_data.txt1MB的传输耗时停等可能需 40sGBN 通常 15s。WINDOW_SIZE4时服务端会连续发 SEQ0,1,2,3客户端收到 SEQ0 后立即回 ACK1服务端收到后滑动窗口发 SEQ4——这就是并行化的威力。步骤 4双向文件传输实战将client_data.txt写入业务数据如Hello from client\n修改client.py的rdt_send()调用位置在收到服务端数据后主动发送# client.py 片段收到服务端数据后回传 client_data.txt if packet_type DATA: self.rdt_rcv(packet) # ✅ 新增主动回传 with open(client_data.txt, rb) as f: self.rdt_send(f.read())此时服务端client_recv.txt将同时包含服务端原始数据和客户端回传数据验证双向可靠性。4. 避坑指南五个血泪教训专治“明明代码没错却死活不通”的玄学问题4.1 现象客户端收不到任何数据Wireshark 显示服务端发包正常原因UDP 端口绑定错误。server.py默认bind((, PORT))监听所有接口但若客户端指定127.0.0.1而服务端绑定了localhostIPv6 ::1则 IPv4/IPv6 栈不互通。解决强制服务端绑定 IPv4 地址。修改server.py的socket.bind()# 替换原 bind((, PORT)) sock.bind((0.0.0.0, config.PORT)) # 显式 IPv4 通配提示0.0.0.0表示监听本机所有 IPv4 接口比更明确避免双栈歧义。4.2 现象GBN 模式下窗口卡死base不推进持续重传同一帧原因ACK 累计确认逻辑缺陷。GBN 要求接收方对SEQ base的帧一律回复ACKbase-1但代码中误将ACKnextseqnum-1即最新发送号导致发送方收到ACK10却期望ACK7窗口无法滑动。解决严格遵循 GBN 规范在client.py的make_ack()中# ✅ 正确ACK 总是确认 base 之前的所有帧 def make_ack(self): return create_ack_packet(self.base - 1) # 不是 nextseqnum-14.3 现象client_recv.txt文件末尾出现乱码或截断原因文件读取未按块对齐。server.py直接f.read()整个文件若文件大小非MAX_PACKET_SIZE整数倍则最后一包data长度不足但接收方仍按满包解析导致后续包错位。解决在server.py发送前添加长度头或更简单——在config.py中设MAX_PACKET_SIZE1020预留 4 字节放数据长度发送时# server.py 发送逻辑 file_size os.path.getsize(filename) header struct.pack(!I, file_size) # 4 字节文件总长 packet header data # 拼接接收方先解析 header 得总长再按需拼接。4.4 现象TIMEOUT2.0时频繁误重传网络良好却卡顿原因定时器未取消导致“幽灵重传”。当 ACK 到达后应立即timer.cancel()但代码中仅timer Timer(...)创建未保存引用无法取消。解决在类中维护self.timer属性# server.py 中 def start_timer(self, interval): self.timer threading.Timer(interval, self.timeout_handler) self.timer.start() def stop_timer(self): if hasattr(self, timer) and self.timer.is_alive(): self.timer.cancel()并在rdt_rcv()收到 ACK 后调用self.stop_timer()。4.5 现象LOSS_RATE0.0时传输成功但0.1就彻底失败原因丢包模拟函数lossy_channel()作用对象错误。该函数应在udt_send()中对待发送报文做丢弃判断但错误地放在udt_recv()中对已接收报文判断导致丢包发生在接收端发送端根本不知情超时逻辑失效。解决定位util.py的udt_send()函数确保丢包逻辑在此处# ✅ 正确位置发送前决策 def udt_send(sock, packet, addr): if random.random() config.LOSS_RATE: print(f[UTIL] Packet dropped (loss rate {config.LOSS_RATE})) return # 直接丢弃不调用 sendto sock.sendto(packet, addr)5. 协议健壮性压测用stress_test.py跑出你的 GBN 吞吐极限5.1 构建压力测试脚本自动化百万包发送与校验项目未提供stress_test.py但这是你验证协议工业价值的关键一步。我基于src/protocol/gbn.py扩展了一个轻量级压测器可直接粘贴到src/目录下# stress_test.py import time import random from protocol.gbn import GBNSender, GBNReceiver from config import WINDOW_SIZE, MAX_PACKET_SIZE def generate_test_data(size_mb): 生成 size_mb MB 的随机测试数据 return bytes([random.randint(0, 255) for _ in range(size_mb * 1024 * 1024)]) def run_stress_test(): # 初始化发送方模拟服务端 sender GBNSender() sender.WINDOW_SIZE WINDOW_SIZE sender.MAX_PACKET_SIZE MAX_PACKET_SIZE # 初始化接收方模拟客户端 receiver GBNReceiver() # 生成 5MB 测试数据 test_data generate_test_data(5) print(f[STRESS] 开始发送 {len(test_data)} 字节...) start_time time.time() # 分块发送 for i in range(0, len(test_data), MAX_PACKET_SIZE): chunk test_data[i:iMAX_PACKET_SIZE] sender.rdt_send(chunk) # 等待所有 ACK while sender.base len(test_data) // MAX_PACKET_SIZE 1: time.sleep(0.01) # 短暂休眠避免忙等 end_time time.time() duration end_time - start_time throughput len(test_data) / duration / 1024 / 1024 # MB/s print(f[STRESS] 完成耗时 {duration:.2f}s吞吐 {throughput:.2f} MB/s) print(f[STRESS] 总发送 {sender.pkt_sent} 包重传 {sender.pkt_retransmitted} 次) if __name__ __main__: run_stress_test()此脚本绕过 socket直接调用GBNSender.rdt_send()规避网络抖动干扰纯粹测试协议栈性能。运行后你会得到精确的吞吐量和重传率——这才是 GBN 真正的“成绩单”。5.2 关键参数调优表不同场景下的最优配置组合场景推荐WINDOW_SIZE推荐TIMEOUT说明预期效果局域网RTT≈1ms160.1s大窗口榨干带宽短超时快速响应吞吐 80 MB/s校园网RTT≈30ms80.2s平衡窗口与超时避免过度重传吞吐 15~25 MB/s高丢包移动网络LOSS_RATE0.241.0s小窗口降低重传风暴长超时容忍延迟重传率 15%教学演示可视化逻辑23.0s窗口极小超时极长便于观察每帧交互每步print()清晰可见提示WINDOW_SIZE并非越大越好。当WINDOW_SIZE bandwidth × RTT即带宽时延积时窗口再大也无法提升吞吐反而增加内存占用和重传开销。用ping -n 10 example.com测 RTT用iperf3测带宽代入公式即可估算理论最优窗口。5.3 校验数据完整性MD5 是你的“后悔药”传输完成后的client_recv.txt是否 100% 一致别靠肉眼比对。在server.py发送前计算源文件 MD5在client.py接收后计算目标文件 MD5二者比对# server.py 发送前 import hashlib with open(server_data.txt, rb) as f: md5_hash hashlib.md5(f.read()).hexdigest() print(f[SERVER] Source MD5: {md5_hash}) # client.py 接收后 with open(client_recv.txt, rb) as f: recv_hash hashlib.md5(f.read()).hexdigest() print(f[CLIENT] Received MD5: {recv_hash}) assert md5_hash recv_hash, ❌ 数据损坏MD5 不匹配这是我从第一次课程设计翻车后养成的习惯任何协议实现没有端到端校验都不算完成。从那以后我每次写网络代码都强制走一遍hashlib.md5()校验哪怕只是 10 行 demo。希望帮到你。本文还有配套的精品资源点击获取
返回列表