
简介这套基于Python的可靠数据传输协议资源包面向计算机网络课程设计或网络编程学习者围绕UDP设计停等协议、GBN协议并进一步改进为SR协议实现单向至双向可靠传输及C/S文件传输应用适合需要完成类似实验、理解滑动窗口与丢包重传机制的人群。压缩包共14个文件以8个Python源码为核心配合设计报告docx、说明文档md、数据文件txt及license整体仅493KB结构紧凑便于按模块对照阅读。源码中整合了服务端与客户端、协议实现及工具模块并模拟数据包丢失来验证协议有效性便于学习者直观对比停等、GBN与SR的差异。已有1014人学习下载适合作为课程设计参考或协议算法复现的实用模板。1. 在UDP之上自己造一个可靠传输协议这个Python工程到底是什么一个Python写的可靠数据传输协议光看名字容易产生两种误解一是以为里面直接调了TCP的库二是以为我们要做的是HTTP这类上层应用。实际上这个zip的典型做法是用UDP套接字当底层信道在应用层自己补上确认、超时重传、序号校验这套机制让不可靠的UDP对外表现得像可靠链路。它是计算机网络课程设计里最常见的题目适合正在学socket编程、想搞明白TCP收发原理、或者需要交一个能跑通带界面的课设demo的人。它要解决的核心问题就一句话在网络随时会丢包、乱序、重复的前提下怎么保证文件内容一个字节都不错。只要电脑能装Python3环境就能复现代码几乎都基于标准库。2. 协议从定义到套接字先把可靠传输的机制选明白再写Python代码2.1 不直接用TCP你要实现的是协议本身不是调库UDP 的不可靠是四个现象叠加的结果每个现象都要有一个对应的代码手段UDP不可靠行为现象Python侧应对丢包sendto返回成功对端没收到超时重传乱序先发的包后到序号 丢弃或缓存重复同一个包到达两遍序号去重位损字节在链路中被破坏校验和所以你会看到这个zip里不会用socket.SOCK_STREAM而是清一色SOCK_DGRAM。可靠性逻辑全部写在 sendto 和 recvfrom 之间不在内核里。socket层只提供一个带超时的收发端点初始化方式一般是import socket # 用 UDP 模拟不可靠信道 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, 8888)) # 接收端固定监听端口 sock.settimeout(0.5) # 收包超时单位秒bind 到 0.0.0.0 表示接收来自本机任意网卡的UDP包timeout 必须设否则 recvfrom 在没数据时一直阻塞发送端的超时重传计时器根本转不动。协议的逻辑全部集中在包格式的定义和收发循环的状态管理里这就是“自己实现可靠传输”的含义。2.2 停等、GBN与SR三种机制的性能边界和代码量差异常见做法里可靠传输有三个递进实现档位选型直接决定后面代码量停等协议一个包发出去等到ACK回来再发下一个。实现最简单对应课程里的rdt3.0状态机核心是一个序号、一个超时定时器。缺点是同一时间网络上只有一个包在飞端到端延迟一高有效吞吐就很低。GBN回退N步发送端连续发N个包接收端只按序接收乱序包直接丢弃并重发最后一个确认。丢包时发送端要回退到最后一个确认包的后一个把它后面所有包全部重传。代码比停等多一个发送窗口和一个累计确认号但网络一拥塞重传浪费就上来了。SR选择重传接收端为乱序包准备缓存发送端只重传真正丢的那个。这个档位要多维护乱序缓存和一批定时器代码量和内存开销都明显高。课程设计里能写到这一档已经算进阶版。选择建议题目没提窗口概念做停等要表现流水线效果做GBN要展示更接近真实TCP的机制上SR。我写课设时会从停等开始状态机跑通了再往窗口方向改这样每一步的错误都容易定位。2.3 rdt状态机转成Python事件循环的映射方法教科书上的rdt画的是“等待0号包”“等待ACK0”这种圆圈和箭头落进Python里其实是状态变量加while循环。状态转移关系可以先列一张表再写码当前状态事件动作下一状态等待包0上层交付数据封装并发送启动定时器等待ACK0等待ACK0收到ACK0停止定时器交付上层等待包1等待ACK0定时器超时重发旧包重启定时器等待ACK0对应代码骨架STATE_WAIT_SEQ0 0 STATE_WAIT_ACK 1 state STATE_WAIT_SEQ0 next_seq 0 while not finished: if state STATE_WAIT_ACK: try: raw, addr sock.recvfrom(MAX_DATAGRAM_SIZE) ack_seq, flags parse_packet(raw) if flags FLAG_ACK and ack_seq next_seq: state STATE_WAIT_SEQ0 except socket.timeout: # 超时未确认重发同一包 sock.sendto(make_packet(next_seq, FLAG_DATA, data_buffer), addr) else: data_buffer read_next_block() sock.sendto(make_packet(next_seq, FLAG_DATA, data_buffer), addr) state STATE_WAIT_ACK这段代码把“等待事件”翻译成 recvfrom 的阻塞与超时把“超时转移”翻译成 except 分支里的重发。注意首次发送和超时重发必须共用同一个 data_buffer千万不要在重发时重新去读文件否则文件偏移已经往前走数据就对不上了。GBN和SR就是把这个结构从“一个待确认包”扩展成“一串待确认包”发送窗口用列表存收到累计ACK时整体滑动。3. 报文格式与校验设计在写传输逻辑前先定好这几个Python字段3.1 一个Packet类该有哪些字段序号、确认号、标志位、校验和、长度写协议的第一步不是写发送循环而是把报文格式用 struct 定死。格式不定好后面加一个字段要同时改发送端、接收端、校验函数改到怀疑人生。课程设计里常见的报文定义如下 packet 结构15字节头部 变长数据: seq : 无符号4字节数据包序号 ack : 无符号4字节确认序号 flags : 无符号1字节bit0FIN, bit1ACK, bit4DATA length : 无符号2字节data长度0表示纯控制包 reserved : 无符号2字节预留对齐 checksum : 4字节对整个头部data的md5前4字节 data : 变长最大由BUFFER_SIZE决定 seq 为什么用4字节而不是2字节算一笔账如果 BUFFER_SIZE 是1400字节2字节序号能覆盖 65535×1400≈91.7MB超过这个量序号回绕接收端会把新包当成重复包丢掉。传几百MB文件就直接翻车。用4字节序号只需要在struct.pack里多写两个大写H彻底避开这个边界。flags 用位运算是为了把数据包、确认包、结束包共用同一个解析函数不用再单开包类型字段。3.2 struct.pack打包与hashlib校验注意不要把校验和自身算进去收发两端必须用同一个字节序和格式Python里最标准的是 struct.pack。这段代码是这个zip里复用率最高的一段import hashlib import struct PACKET_HEADER struct.Struct(!IIHBH) # seq(4) ack(4) flags(1) length(2) reserved(2) CHECKSUM_LEN 4 HEADER_SIZE PACKET_HEADER.size CHECKSUM_LEN # 共15字节 def make_packet(seq, ack, flags, datab): header_no_checksum PACKET_HEADER.pack(seq, ack, flags, len(data), 0) checksum hashlib.md5(header_no_checksum data).digest()[:CHECKSUM_LEN] return header_no_checksum checksum data def parse_packet(raw): if len(raw) HEADER_SIZE: raise ValueError(packet too short) seq, ack, flags, length, reserved PACKET_HEADER.unpack(raw[:PACKET_HEADER.size]) checksum raw[PACKET_HEADER.size:HEADER_SIZE] data raw[HEADER_SIZE:] if hashlib.md5(raw[:PACKET_HEADER.size] data).digest()[:CHECKSUM_LEN] ! checksum: raise ValueError(checksum mismatch) if length ! len(data): raise ValueError(length field mismatch) return seq, ack, flags, data逻辑说明make_packet 先把头部打包成二进制再把“头部字节 数据”一起喂进 md5取前4字节放在头部后面。parse 时用同样的范围重新计算这里的关键是不要把校验和本身放进hash输入因为解析时拿到的是“可能已经被改过的checksum”把未知内容一起算会污染结果。包太短、鉴权失败、长度对不上任何一个检查失败都说明这个包要么被破坏要么被截断接收端应当直接丢弃并继续等待而不是把它写进文件。还有一个字节序细节!前缀表示网络字节序大端两端如果有一台用原生小端打包、另一台按网络字节序解包所有包都会过不了校验。Python在struct格式串里强制统一就不存在这个问题。3.3 三次握手与四次挥手在小项目里要不要做很多人会问不做握手行不行。对“可靠文件传输”这个核心诉求行。UDP本身没有连接概念握手的主要收益是防止旧连接的数据串到新连接里而课程设计大多是一次性收发做完程序就退出。但有一个教训如果不做结束包接收端写完文件直接退出发送端最后几个ACK或FIN的ACK丢失时没有重传机会整个程序会卡在超时循环里。所以哪怕不做三次握手也一定要有一个单向FIN标志表示“发完了”并且对FIN的重传和ACK要写对称。只做一个标志位的话就把这个预算留给FIN。4. 跑通一个最小文件传输发送端、接收端的Python骨架与参数调法4.1 发送端骨架分块、发送、等待ACK、超时重传给一个停等协议的最小可运行骨架只留核心逻辑import socket BUFFER_SIZE 1400 TIMEOUT 1.0 FLAG_DATA 0x08 FLAG_ACK 0x02 FLAG_FIN 0x01 def send_file(filename, server_addr): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(TIMEOUT) seq 0 with open(filename, rb) as f: while True: chunk f.read(BUFFER_SIZE) pkt make_packet(seq, 0, FLAG_DATA, chunk) while True: sock.sendto(pkt, server_addr) try: raw, _ sock.recvfrom(2048) _, ack, flags, _ parse_packet(raw) if flags FLAG_ACK and ack seq: break except socket.timeout: print(f[retransmit] seq{seq}) if not chunk: break seq 1 # 发完文件后发结束包同样要等确认 while True: fin make_packet(seq, 0, FLAG_FIN) sock.sendto(fin, server_addr) try: raw, _ sock.recvfrom(2048) _, ack, flags, _ parse_packet(raw) if flags FLAG_ACK and ack seq: break except socket.timeout: pass sock.close()逻辑说明发送端先读一块文件进入内层while循环反复 sendto 同一个包直到收到 ack 等于当前 seq 的确认才退出。sendto 放在循环开头意味着超时重传会原封不动重发同一包这正是可靠传输的冗余核心。注意读到空字节时这个实现会先发一个空DATA包再走FIN虽然功能没问题但更干净的写法是读到空直接跳出循环发FIN避免接收端在空包和FIN之间产生歧义。seq 字段只在收到确认后自增保证网络上不会同时有多个未确认的数据包。4.2 接收端骨架校验、按序写盘、回ACK、处理重复包接收端比发送端多两件事重复包检测和地址记忆。def recv_file(filename, listen_addr): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(listen_addr) sock.settimeout(1.0) expected_seq 0 sender_addr None with open(filename, wb) as f: while True: try: raw, addr sock.recvfrom(2048) except socket.timeout: continue try: seq, ack, flags, data parse_packet(raw) except ValueError: continue if not sender_addr: sender_addr addr if flags FLAG_FIN: sock.sendto(make_packet(0, seq, FLAG_ACK), sender_addr) break if flags FLAG_DATA: if seq expected_seq: f.write(data) f.flush() sock.sendto(make_packet(0, seq, FLAG_ACK), sender_addr) expected_seq 1 else: # 重复包或乱序包回上一次确认的ACK sock.sendto(make_packet(0, expected_seq - 1, FLAG_ACK), sender_addr) sock.close()逻辑说明接收端只把 seq 等于 expected_seq 的包写盘写完后回 ACK 并推进序号其他情况一律回复上一次成功确认的序号。不要小看 else 分支里这一行它就是窗口同步的基石发送端收到重复ACK会知道后面的包需要回退重传接收端不需要任何缓存就能让传输协议自我纠正。每次写入后调用 f.flush() 是为了防止程序在测试中突然被杀时文件内容还留在Python缓冲区里没落盘。sender_addr 在第一个包到达时存一次之后所有ACK都发到这个地址避免多网卡环境下回错IP。4.3 BUFFER_SIZE、TIMEOUT、WINDOW_SIZE 三个参数的设定经验三个参数决定了这个协议能不能商用先给常用起点参数常用起点值调整依据BUFFER_SIZE1400字节以太网MTU 1500留出IP头20字节UDP头8字节再大触发IP分片TIMEOUTmax(0.5, 2×SRTT)按实测RTT滑动平均更新不设固定值WINDOW_SIZE4个包(GBN)或8个包(SR)增大窗口能提吞吐但高丢包率时重传风暴反噬BUFFER_SIZE 顶到1472是理论极限我不这么干因为链路层可能还有额外封装开销1400是留余量的选择。TIMEOUT 固定设太小包还在路上就判定超时重传频率陡增固定设太大真丢一个包要傻等好几秒。至少要让它可配置或者做一点近似TCP的估算srtt 0.05 TIMEOUT max(0.5, 2 * srtt) # 每次收到ACK后用实测RTT更新平滑值 srtt 0.875 * srtt 0.125 * rtt TIMEOUT max(0.5, 2 * srtt)逻辑说明srtt 是平滑往返时间0.875和0.125是常用的指数加权系数新的采样只占1/8权重避免个别抖动把超时时间带跑偏。乘2是因为重传判定要容忍排队延迟至少留出一个往返的余量。5. 避坑Python可靠传输协议实现最容易翻车的5个问题5.1 传大文件巨慢不是代码逻辑问题是分片和超时在拖后腿现象传700MB文件速度从每秒几MB掉到每秒几十KB小文件完全正常。 原因两个问题叠加。一是 BUFFER_SIZE 超过UDP单包承载极限触发IP分片一个分片丢了整个IP分组作废丢包概率成倍放大二是 TIMEOUT 写死成了0.2秒实际RTT受网络负载影响后频繁误判超时大量流量浪费在重传上。 解决把 BUFFER_SIZE 降到1400以内再加4.3里的SRTT超时估算。先动这两个参数再动逻辑90%的“慢”都是参数问题不是机制问题。5.2 校验和没错但数据是乱的检查校验范围覆盖到seq没有现象文件收完打开中间有几百字节是重复或错位的程序全程没报过 checksum mismatch。 原因make_packet 里只对 data 段做哈希没有把 seq、ack、flags 这些头部字段纳入校验范围。seq 位翻转后校验值却算的是另一段内容接收端按旧校验放行序号错位被当成新包写进文件。 解决把哈希对象从 data 扩展成“头部字节 data”。改动量只是md5多算几个字节换来的是头部任何字段被破坏都能在parse阶段拦截。5.3 收尾阶段卡死ACK与FIN的丢包也要有重传策略现象文件内容全部收齐但程序最后卡5到10秒才退出极端时永不结束。 原因发送端发出FIN后等接收端回FIN的ACK这封ACK在中途丢了发送端就一直阻塞在recvfrom超时循环里接收端却已经退出。很多实现只给数据包写了重传逻辑控制包一点保护没有。 解决把FIN的发送也放进循环直到收到对FIN的ACK才退出接收端对重复FIN也必须回ACK。两边状态才能收敛。这就是4.1里FIN闭环存在的意义。5.4 窗口加大后性能反降GBN不是窗口越大越快现象WINDOW_SIZE 从4改到32文件传得更慢抓包看到大量重复包。 原因GBN一旦丢包要重传从丢失位置往后的所有包。窗口越大每个丢包引发的重传风暴越大。丢包率5%、窗口16时一个RTT里全部包都成功的概率只有 0.95^16≈0.44接近一半时间在重传带宽全被冗余流量吃掉。 解决先把丢包率测出来再定窗口。局域网低丢包场景可以调大丢包率5%以上时窗口超过8就明显反噬这时候该考虑换SR而不是继续加大窗口。5.5 本机正常换机器或系统就翻车地址绑定和平台差异现象收发两端都在本机跑一切正常换两台真机跑丢包率爆炸甚至完全收不到或者Windows上跑一段时间突然抛ConnectionResetError。 原因两部分。一是接收端创建socket后没有bind固定端口每次运行端口随机发送端回ACK的地址没对上Windows下UDP收到ICMP Port Unreachable还会被封装成ConnectionResetError抛到上层。 解决接收端必须在 recvfrom 循环前 bind 固定端口发送端在收到第一条报文时把 addr 存死之后所有ACK都发往这个地址recvfrom 外层套 try/except ConnectionResetError 跳过这次接收。这是平台差异不是协议逻辑错误先别怀疑重传机制。6. 验证协议是否可靠用丢包脚本和抓包工具做回归别只看传完没传完6.1 可控丢包率测试脚本1%丢包触发重传30%丢包测边界可靠性是概率指标单跑一次“成功了”说明不了任何事。我一般会在发送端加一个随机丢包开关把信道真实损耗模拟出来。做法是在 sendto 前加概率判断import random DROP_RATE 0.10 # 模拟10%丢包运行时可改 def send_maybe_drop(sock, pkt, addr): if random.random() DROP_RATE: print([drop] simulate packet loss) return sock.sendto(pkt, addr)逻辑说明这个函数把随机丢包注入点统一收敛到一处切换测试场景时不用满代码找sendto。跑回归时按丢包率分档0验证无错传输基线0.01验证超时重传被正常触发0.10验证大量重传下文件仍完整0.30验证极端场景不无限卡死。每个丢包率跑20次接收文件与源文件MD5一致才算过。6.2 看四个指标判断协议健康度比单纯传完更重要只靠“文件传完”分不清协议好坏。做定位时我会打印四个指标发送总包数、重传包数、接收重复包数、有效吞吐。重传率重传包数/总包数接近丢包率说明协议在正常兜底远高于丢包率说明超时参数太激进接收端重复包数大于重传包数说明ACK丢包严重要优先解决反向确认的可靠性。有效吞吐按文件大小除以总耗时计算比看带宽占用更贴近用户感受。动手验证时还可以打开Wireshark监听UDP端口过滤同一个seq的DATA包是否出现多次能直观看到重传行为。我自己验证协议时会先在1%丢包率下确认重传日志能触发再调到30%测边界这个动作能把“只重传数据包不重传FIN”的隐患顺手暴露出来。最后保持一个习惯把每次改动的丢包率、超时时间、窗口大小和四个指标写进实验记录下次调参不靠回忆。希望帮到你。本文还有配套的精品资源点击获取