资讯动态

基于Python实现可靠数据传输协议:从UDP到RDT/GBN/SR核心机制

发布时间:2026/9/28 11:56:28 来源:尧图企业网站定制
简介这份基于Python实现可靠数据传输协议的资源针对计算机网络课程设计中停等协议、GBN与SR协议的原理理解与代码实现需求面向高校网络工程、计算机专业学生可帮助读者从零完成可靠UDP传输实验与文件传输应用。压缩包共14个文件、约723KB以8个Python源文件为核心覆盖server.py、client.py、util工具、config配置等模块并附带docx实验报告、README说明及测试用文本数据便于对照文档运行与验证。资源按协议实现、工具模块、数据文件清晰分层支持模拟丢包验证协议有效性并给出由单向到双向传输、由停等到GBN再到SR协议的递进式改造思路适合作为课程设计参考或自学实践素材。已有595人学习属于轻量但结构完整的实验资源包。1. 基于Python实现可靠数据传输协议到底在解决哪一层的问题大多数Python开发者的第一反应是socket.send()发出去的数据对方就应该能完整收到。直到拿“基于Python实现可靠数据传输协议”这种题目动手时换成UDP socket才发现完全不是这么回事——丢包、乱序、重复、数据损坏全都冒出来了send出去的报文像断了线的风筝。这个项目要做的是在应用层凭空补出一个“可靠层”靠序号、校验和、ACK、超时重传这四板斧把一个默认不可靠的UDP信道包装成行为接近TCP的可靠信道之后文件收发、消息确认这类需求都能复用同一套思路。适合正在做计网课设、或者想搞明白TCP底层为什么这么设计的人零基础入门选手也能顺着代码反推机制。2. 可靠数据传输协议的四根支柱序号、校验和、ACK与超时重传2.1 为什么UDP信道里要由应用层自己补可靠语义TCP的可靠性不是凭空出现的它由校验和、序号、确认应答、超时重传、滑动窗口和拥塞控制共同拼成。在Python里你没法直接改动内核的TCP协议栈常见做法就是自定义一套协议跑在UDP之上收发两端都认同一份二进制报文格式然后各自实现发送/接收状态机。UDP本身不负责可靠但它有一个对实验非常友好的特性——数据报边界。一个sendto对应一个recvfrom天然省掉了TCP那种自己拆包、处理粘包的麻烦。所以这道题选UDP做底层不是给自己加难度而是把协议设计的部分完整留给你。真正要补的可靠语义一共四条检测数据有没有被改坏校验和、区分新旧分组序号、告诉发送方收到了什么ACK、以及确认一直没来时怎么办超时重传。这四条就是可靠数据传输协议的最小骨架。先明确这一点再去写代码就不会被各种版本定义绕晕。2.2 自定义报文结构与struct二进制打包协议设计第一步是确定报文格式。我习惯用定长二进制头部而不是JSON或者文本行理由是定长头能直接用struct拆包性能好也不会出现字段边界歧义。这里选!BBHHH这种格式网络字节序大端、报文类型占1字节、标志位占1字节、序号占2字节、确认号占2字节、校验和占2字节后面再跟数据载荷。import struct import binascii # 头部固定7字节类型 标志 序号 确认号 校验和 HEADER struct.Struct(!BBHHH) FLAG_DATA, FLAG_ACK 1, 2 def crc16(data: bytes) - int: 课设级校验截取CRC32的低16位生产级建议换RFC1071检验和 return binascii.crc32(data) 0xFFFF def build_pkt(ptype: int, seq: int, ack: int, payload: bytes) - bytes: # 先把校验和填0算出一个基准值再把真正的校验和填进头部 head0 HEADER.pack(ptype, 0, seq, ack, 0) cksum crc16(head0 payload) return HEADER.pack(ptype, 0, seq, ack, cksum) payload def parse_pkt(raw: bytes): 解析失败返回None调用方直接丢弃即可 if len(raw) HEADER.size: return None ptype, _, seq, ack, cksum HEADER.unpack(raw[:HEADER.size]) payload raw[HEADER.size:] head0 HEADER.pack(ptype, 0, seq, ack, 0) if crc16(head0 payload) ! cksum: return None return ptype, seq, ack, payload代码里有三个参数值得说明。首先是!它指定网络字节序保证跨机器解析一致其次是H代表无符号16位整数所以序号空间是0到65535后面调滑动窗口时这个上限会变成约束最后是校验和的计算范围必须固定为“头部校验和字段置零载荷”收发两端保持一致。如果发送端只算payload、接收端连头部一起算典型的症状是短报文没问题、长报文全部被判损坏。这类问题不报错只会表现为接收端一直沉默排查起来相当费神。2.3 可靠数据传输协议的核心时序演进如果按教科书顺序看可靠数据传输协议是这样一步步长出来的假设信道不丢不坏就是RDT 1.0加入校验和与ACK/NAK解决位错误变成RDT 2.0再加序号解决分组重复和ACK损坏变成RDT 2.1把NAK省掉、用最近一次的ACK代替变成RDT 2.2最后加超时重传解决丢包就是RDT 3.0。课设里最常见的“报序号校验ACK超时重传”方案本质上就是RDT 3.0。版本新增机制解决的问题RDT 1.0底层假设可靠无RDT 2.0校验和、ACK/NAK数据位翻转RDT 2.1序号重复分组、ACK损坏RDT 2.2只发ACK不发NAK减少报文类型RDT 3.0超时重传丢包为什么RDT 2.2之后能用“沉默”替代NAK因为接收端收到损坏分组时什么都不回应发送端等不到ACK就会超时重传。这比单独维护NAK状态的逻辑更简洁实现时少一条分支。理解了这个演进顺序写代码就不用纠结“损坏包到底回不回包”只要记住校验失败就丢超时交给发送端处理。2.4 为什么停等协议会卡在效率上RDT 3.0默认是停等协议发一个包等一个ACK再发下一个。它逻辑最简单但效率瓶颈一眼就能算出来——假设RTT是10msMSS是1200字节停等协议的理论吞吐大约就是1200B除以10ms等于120KB/s左右这还没算处理开销。要提速就得引入流水线允许窗口里有多个分组同时在空中飞。这就引出后面两个协议GBN回退N步和SR选择重传。在Python里做课设先跑通RDT 3.0再改GBN/SR是成本最低的路线。3. 用UDP实现最小可靠传输收发两端代码与停等协议跑通3.1 发送端停等协议的最小发送循环下面这段代码是RDT 3.0发送端的完整循环。重点不是写得多漂亮而是把“发—等—超时重发”这个状态机转成Python逻辑。注意数据要先按MSS切块因为UDP一个数据报塞不了大文件切块大小就是协议里的一个关键参数。import socket MSS 1200 # 单包载荷上限建议明显小于1472 TIMEOUT 0.5 # 初始超时稳定后可换成RTT估算值 def rdt_send(sock: socket.socket, dst, data_chunks): seq 0 for chunk in data_chunks: pkt build_pkt(FLAG_DATA, seq, 0, chunk) acked False while not acked: sock.sendto(pkt, dst) sock.settimeout(TIMEOUT) try: resp, _ sock.recvfrom(2048) parsed parse_pkt(resp) if parsed and parsed[0] FLAG_ACK and parsed[2] seq: acked True except socket.timeout: # 超时说明ACK没回来原包重发继续等 continue seq ^ 1 # 停等协议只需要0/1两个序号这段代码里seq ^ 1是停等协议的精髓序号只在0和1之间翻转。为什么两个序号就够因为停等一端同时最多只有一个未确认分组接收端只要判断“本次序号跟期望是否相同”就能区分新数据和重复数据。如果试图用更多序号反而要注意回绕问题得不偿失。TIMEOUT0.5是保守初始值本地回环可以很小但真实链路建议先用大值跑通再按第四章的方法收紧。3.2 接收端校验、去重与应答接收端逻辑同样是一段无限循环收包、解析、验校验和、对序号、落数据、回ACK。这里最容易写错的地方是“重复分组”分支。收到重复分组说明ACK丢失或延迟了这时候不能静默必须重新回一次上一次的ACK否则发送端会一直重传形成死循环。def rdt_recv(sock: socket.socket, write_fn): expected 0 while True: raw, src sock.recvfrom(2048) parsed parse_pkt(raw) if parsed is None: # 校验失败或长度不对直接丢弃等待发送端超时重传 continue ptype, seq, ack, payload parsed if ptype FLAG_ACK: # 当前接收端只处理DATAACK报文在发送端处理 continue if seq expected: write_fn(payload) sock.sendto(build_pkt(FLAG_ACK, 0, seq, b), src) expected ^ 1 else: # 重复分组重发上一次的ACK注意确认号是1-expected sock.sendto(build_pkt(FLAG_ACK, 0, 1 - expected, b), src)接收端这段代码有个容易被忽略的点校验失败时直接continue什么都不回。这跟前面RDT 2.2演进里说的“用沉默替代NAK”对应上了。如果你在parsed is None分支回一个ACK或者NAK都会让协议状态复杂化。另一个细节是重复分组时回ACK的序号必须填1 - expected也就是上一个成功接收的序号不能填当前收到的重复序号。3.3 把文件传输的主流程组装起来停等协议收发两端分别跑在两条进程或两台机器上UDP socket用SOCK_DGRAM创建。主流程就是读文件、切块、逐块调用rdt_send接收端每收到一个块就往目标文件里写一个块。def send_file(sock: socket.socket, dst, path: str): with open(path, rb) as f: chunks [] while True: chunk f.read(MSS) if not chunk: break chunks.append(chunk) rdt_send(sock, dst, chunks) def recv_file(sock: socket.socket, path: str): with open(path, wb) as f: rdt_recv(sock, f.write)chunks列表一次性读进来对超大文件不友好但课设和大多数文件传输演示完全够用。想优化就改成逐块读取、逐块发送注意保持chunk长度不超过MSS即可。recv_file里把f.write作为write_fn传进去是因为接收端回调只关心“给我一个完整payload”这比在循环里堆业务代码清爽得多。3.4 从停等升级到GBN与SR的改造点停等跑通以后最值得做的升级是改成流水线。GBNGo-Back-N的思路是发送端维护一个窗口窗口内连续发送收到累积ACK就把窗口整体前移一旦超时从窗口左沿开始重发所有未确认分组。SRSelective Repeat则更进一步接收端缓存乱序到达的分组发送端只重传真正丢失的那几个。协议ACK语义接收端缓存超时重传范围实现难度停等每包独立ACK无当前包最低GBN累积ACK不缓存窗口内全部中等SR选择ACK缓存乱序包仅丢失的包较高从停等改到GBN发送端变化最大。原来的seq ^ 1要换成维护base和next_seq两个指针base表示窗口左沿next_seq表示下一个待发序号。收到ACK后base max(base, ack 1)超时后把base到next_seq之间的分组全部重发。接收端反而变简单因为GBN不缓存乱序包丢弃即可但需要回累积ACK。SR的接收端要维护一个缓存数组和一个expected指针代码量明显增加做课设时建议先把GBN吃透再上SR。4. 超时、窗口与载荷上限RDT能跑多快由这三个数决定4.1 超时时间别拍脑袋用RTT估算替代固定值很多人在本地回环测试时把超时设成0.01秒跑起来飞快一放到真实网络就疯狂重传。原因是本地回环RTT是亚毫秒级真实链路上有排队、抖动和带宽限制。固定超时是玄学不如按TCP的思路动态估算。RFC 6298里给了一组公式用EWMA平滑采样RTT并估算抖动。import time ALPHA, BETA 0.125, 0.25 estimated_rtt None dev_rtt 0.0 def sample_rtt(rtt: float) - float: global estimated_rtt, dev_rtt if estimated_rtt is None: estimated_rtt rtt dev_rtt rtt / 2 return estimated_rtt 4 * dev_rtt diff abs(rtt - estimated_rtt) estimated_rtt (1 - ALPHA) * estimated_rtt ALPHA * rtt dev_rtt (1 - BETA) * dev_rtt BETA * diff return estimated_rtt 4 * dev_rtt用法是在发送端记录t0 time.monotonic()收到对应ACK后把time.monotonic() - t0作为采样RTT喂给sample_rtt返回值就是下一轮超时时间。这里有两个参数值得记ALPHA0.125控制EstimatedRTT的平滑速度BETA0.25控制抖动估计的响应速度它们就是TCP本身用的取值。第一次没有样本时超时取initial EstimatedRTT 4*DevRTT实际代码里我习惯先给0.5秒兜底避免初始链路慢导致早重传。4.2 窗口大小与载荷上限带宽时延积决定并发度停等协议一个包一个ACK窗口就是1。GBN和SR的窗口不是越大越好理论上限是带宽时延积W 带宽(bps) × RTT(s) / 单包比特数。比如10ms延迟、100Mbps带宽、1200字节载荷算出来窗口大约104个包。课程实验里没必要追求这个极限8到16的窗口足够展示流水线效果。载荷上限MSS1200的选择也有依据。标准以太网MTU是1500字节减去IPv4头20字节和UDP头8字节UDP载荷理论最大1472字节超过1472就会触发IP分片分片丢失会让整个数据报作废可靠性代价极大。所以MSS取1200到1400之间比较稳妥既避开分片又留出协议头扩展余量。想调大的人可以继续用1472这个上限做压测但要做好跨路由器时MTU不一致导致分片翻车的准备。4.3 链路模拟器在本地复现真实网络的丢包和损坏本地回环测试最大的谎言是“零丢包”。代码有问题也照跑不误因为内核根本不给你的UDP包制造麻烦。我一般会写一个模拟器挂在发送端sendto之前用概率决定这个包是正常发送、丢弃还是翻转一个字节。这样不用上真实环境就能验证协议逻辑。import random def disturb(pkt: bytes, loss0.1, corrupt0.05): 10%丢包、5%损坏返回值是最终要发出的包 r random.random() if r loss: return None if r loss corrupt: data bytearray(pkt) pos random.randrange(len(data)) data[pos] ^ 0xFF return bytes(data) return pkt配合模拟器验证时关注三个指标传输完成耗时、总重传次数、首传成功率。把这些指标在不同丢包率比如0%、5%、10%、20%下各跑一轮对比停等和GBN的耗时差距比任何理论推导都直观。注意disturb要放在sendto之前调用返回None时直接跳过发送模拟丢包返回损坏包时正常sendto让对端靠校验和丢弃它。5. 实现过程中容易翻车的五个细节现象、原因与解决办法5.1 校验和计算范围不一致长包全部被拒收现象短报文收发正常一旦数据超过几十字节接收端就一直不回应。原因crc16在发送端只对payload计算接收端却对header加payload一起验证两边对不上所有带载荷的包都被判损坏。解决统一约定“头部校验和字段置零载荷”作为计算范围写一个公共函数收发两端都只调它。5.2 序号回绕窗口一开大就丢数据现象GBN或SR把窗口调到8以上后传输中途突然大量重传甚至收尾时多出一块重复数据。原因16位序号空间只有65536窗口超过一半时接收端无法区分新分组和回绕后的老分组。解决要么把序号字段扩展到32位要么限制窗口大小满足窗口 ≤ 2^16 / 2。停等协议只用0和1永远不踩这个坑这也是它适合起步的原因。5.3 用time.time()做超时计时Windows下重传次数异常现象同样的代码在Linux上稳定在Windows上偶发重复ACK。这就是典型的环境坑。原因time.time()返回墙上时间Windows上可能受系统校时影响出现微小回退导致RTT计算出现负值或异常小值。解决统一改用time.monotonic()它只增不减专门为测量间隔设计。这个改动一行就能完成但很多人把问题定位到协议逻辑上查半天。5.4 recvfrom阻塞导致超时失效程序卡在等待里现象发送端设置了sock.settimeout(TIMEOUT)但程序仍然卡死不在except socket.timeout里触发重传。原因settimeout只对阻塞式recvfrom生效而且超时抛出的确实是socket.timeout异常真正翻车的是有人把它写成sock.setblocking(False)后直接用裸recvfrom没判BlockingIOError程序在空轮询里忙转。解决停等协议最简单就用sock.settimeout(TIMEOUT)加try/except socket.timeout用了非阻塞模式就一定要配合select或poll二者只能选一种策略。5.5 回环测试掩盖问题真机一跑就现原形现象本机收发一切正常发给另一台机器就连不上或者极慢。原因回环地址127.0.0.1不走物理网卡几乎没有丢包和延迟定时器和窗口参数都是基于假象调出来的。解决把第4.3节的链路模拟器打开人为设置10%丢包、10ms延迟跑一轮再决定超时和窗口取值。真机环境还可以在两台机器之间加防火墙限速但最可复现的做法仍然是模拟器。6. 从能用走向可用快速重传、流量控制与端到端验证把停等或GBN跑通只能算“能演示”离“可用”还差两个机制快速重传和流量控制。快速重传的思路是接收端每收到一个乱序分组就重发当前期望序号的ACK发送端连续收到三个相同ACK时不等超时立刻重传疑似丢失的分组。这条路径在TCP里是标配能显著降低高丢包链路的恢复延迟。改造点很集中在3.1节的发送循环里加一个字典统计各序号的重复ACK次数计数器达到3就触发重传同时把超时定时器复位。流量控制则是把接收端的接收能力反馈给发送端。常见做法是在报文头的flags字段旁边再扩一个rwnd字段接收端每次回ACK时带上自己的空闲缓冲区大小发送端取min(拥塞窗口, rwnd)作为实际可用窗口。前面定义的头部里预留了1字节flags扩展成2字节窗口字段不会破坏已有格式只要收发两端同步更新HEADER结构体即可。加入这两个机制后协议才真正具备对抗真实网络的条件。整个实验的验证方法我建议固定成一套链路模拟器开10%丢包、5%损坏、20ms固定延迟文件选取一个10MB左右的二进制文件用MD5校验收发一致性。先跑停等再跑GBN最后跑“GBN快速重传流量控制”的完整版记录各自耗时。完整的协议代码可以让传输从几百KB/s提升几个量级但前提是你清楚每一步在对抗什么问题。交这类作业或落地类似需求前我会习惯性做最后一道检查把模拟器丢包率调到20%看程序会不会真的抛异常、会不会出现重复文件块、会不会在日志里刷出大量连续超时。这些都会在真机上变成血泪教训。这个小习惯救过我很多次也希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑