尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

停止等待协议详解:基于UDP实现可靠传输的完整指南

发布时间:2026/9/29 1:23:21

资讯中心
01
ARTICLE

停止等待协议详解:基于UDP实现可靠传输的完整指南

停止等待协议详解:基于UDP实现可靠传输的完整指南
简介计算机网络实验指导系列中的实验二围绕利用停止等待协议实现数据文件可靠传输展开适合高校网络工程、计算机等专业学生及教师作为实验参考。这份资源为单份doc文档共1个文件压缩包大小约81KB内容覆盖实验目的、实验环境、停止等待协议的基本工作过程、数据包丢失与确认信息丢失的处理、BSC协议的报文格式与控制字符功能以及简化停止等待协议实验要求等模块。文档结构清晰图文配合既可用作教师备课与实验指导也可供学生课前预习、实验时对照操作或课后复习巩固。目前已有87人浏览学习。通过阅读这份文档读者可深入理解ACK/NAK确认机制、定时器超时重传、数据包编号去重等核心要点掌握SOH、STX、ETX、EOT等BSC控制字符的具体用途并学会在模拟数据包丢失、确认信息出错等异常场景下设计可靠的数据文件传输方案对网络协议实验教学与自学均具有实用价值。1. 停止等待协议实验先搞懂它到底在训什么一份 2 MB 的二进制文件要从 A 机器完整传到 B 机器字节一个都不能错还要求在传输过程中丢包、乱序、比特翻转的情况下依然能成功——这需求听着像 TCP 的活但大多数学校的计算机网络实验二偏偏让你用 UDP 自己实现一个停止等待协议去干这件事。我就是从这个实验开始才真正明白可靠传输不是协议自带的魔法而是“确认 重传 序列号”这三件套堆出来的。哪怕你背熟了谢希仁教材里数据链路层那一章到写代码时照样会在 ACK 丢失、超时重传、文件收尾这些细节上反复翻车。这篇笔记适合三类读者正在做课程实验、被老师要求“必须用 UDP 模拟停等协议”的本科生自学计算机网络、想验证自己对可靠传输理解的考研党以及做网络仿真或嵌入式开发、需要自己实现轻量可靠传输协议的工程师。我会从协议机制拆起给出一套可复现的 Python 实现再讲帧长、超时、重传次数这几个关键参数怎么调最后把四年实验里见过的踩坑记录都摊开给你看。2. 停止等待协议的三件套序列号、校验码、超时重传2.1 发送端状态机一帧一确认、一超时一重发停止等待协议英文叫 Stop-and-Wait核心思想极朴素发送方每发出去一帧数据就停下来等接收方回一个确认收到确认后才发下一帧如果等了很久没收到确认就原封不动地把刚才那帧再发一遍。这里有个关键细节超时重传的前提是“帧必须能被识别”。发送方重传的还是同一个帧但接收方怎么知道这是重传的旧帧还是恰好序列号相同的下一帧呢答案是给帧编号。停等协议里序列号只需要 0 和 1 两个值交替使用就够了——因为任何时候信道上在途的只有一帧不会出现“多个未确认帧”需要区分的情况。这个交替序列号机制也叫“停止等待 ARQ”比你想的更容易写错很多人做完实验发现前几帧正常、传大文件时偶发重复字节多半就是序列号回绕或 ACK 映射那里出了岔子。湖科大教书匠的动画把这套时序画得很清楚发送方有两个状态一个是“发送帧并等待 ACK”另一个是“收到 ACK 后反转序列号继续发”。你要是做实验前先看一遍那个时序图就不会把 ACK0 和 ACK1 的对应关系画反。2.2 接收端只需要做三件事查错、确认、去重接收方的逻辑比发送方简单但简单不等于好写。它收到一帧后依次做三件事第一是校验。UDP 提供的是“尽力而为”的传输包可能完好到达也可能在某个路由器上被比特翻转。CRC 校验循环冗余校验就是为了检测这类比特差错而加的。常见的做法是用 CRC16 对帧的 payload 部分计算校验值放在帧头里一起发给接收方接收方重算一遍比对结果不一致就丢弃。第二是确认。接收方校验通过后回一个 ACK 帧。注意 ACK 里携带的序列号应该是对应收到的数据帧的序列号而不是“我期望的下一个序列号”。这两者在停等协议里等价但表达方式不同写代码时很容易搞混我见过有人把 ACK 的序列号写成了expect_seq ^ 1结果对方永远等不到确认。第三是去重。如果发送方因为 ACK 丢失而重传了同一帧接收方会再次收到一个序列号和上一帧相同的帧。此时它应该丢弃这个重复帧但必须照样回 ACK——不然发送方会以为这个重复帧没被收到继续重传形成死循环。去重这个逻辑放在代码里就两行但它是整个协议正确性的最后一道闸门。很多人做完实验正常网络环境下测试全部通过一加丢包模拟就崩80% 是这个去重分支写漏了。2.3 效率账为什么停等协议“慢”却是必做实验停等协议慢是毋庸置疑的。假设信道带宽是 100 Mbps往返时延 RTT 是 40 ms一帧数据 1024 字节发送一帧需要的时间约 0.08 ms而等 ACK 却要等 40 ms信道利用率不到 0.2%。这就是为什么实际网络里用的是滑动窗口协议允许连续发送多帧。那为什么实验还要做停等协议因为它是理解所有可靠传输协议的“最短路”。滑动窗口、选择性重传、Go-Back-N本质上都是在“停等”基础上增加了窗口并发TCP 的可靠传输也是 ACK、序列号、超时重传这些机制的扩展。把停等协议做透了你再去看 TCP 的发送缓冲区管理和快重传算法就会觉得那些机制是自然长出来的而不是一堆概念需要死记。顺便说如果你在准备 408 考试这个实验做完之后数据链路层那些选择题基本能秒答。考试考的是“停等协议的信道利用率是多少”你亲手测过一遍就知道决定利用率的不是带宽而是 RTT 与传输时延的比值。湖科大教书匠的课件和王道计算机网络的讲义里都有这道公式题但自己跑过数据后对公式的理解会深一层。3. 在 UDP 上实现停等传文件发送端与接收端的完整代码3.1 帧结构定义1 字节序列号、2 字节长度、2 字节 CRC16实验指导书一般只要求“利用停止等待协议传输数据文件”没有规定帧格式这其实留了设计空间。我的做法是自定义一个最小帧头按固定字节序打包字段字节数说明seq1序列号取 0 或 1payload_len2payload 的实际字节数网络字节序crc162对 payload 计算 CRC16 结果payload可变文件分帧后的数据块为什么选 CRC16 而不是校验和UDP 头里已经有 Internet 校验和了但它只覆盖头部和一个伪头不保证 payload 可靠。CRC16 能检测出绝大多数 2 比特以内的随机差错实验场景完全够用。Python 里可以直接用binascii.crc_hqx(payload, 0xFFFF)它返回的就是一个 16 位 CRC 值不用自己实现查表法。帧长要单独放一个字段不能靠len(payload)推断吗不行因为你最后收尾时会发一个空 payload 的特殊帧payload_len 0这是文件长度恰好为帧长整数倍时的结束标记。光靠接收方“收到的字节数不足一帧”来判断文件结尾遇到这种边界情况就会卡死。3.2 发送端代码分帧、重传与超时控制下面这套发送端代码我基于 Python 的 UDP socket 实现逻辑重点在“发送—等 ACK—超时重发”这个循环里import socket import struct import time import binascii HOST 127.0.0.1 PORT 9001 FRAME_LEN 1024 # payload 最大字节数 RTO 0.5 # 超时重传时间单位秒 MAX_RETRY 3 # 每帧最大重传次数 def make_frame(seq: int, payload: bytes) - bytes: 按 122payload_len 的格式拼帧 crc binascii.crc_hqx(payload, 0xFFFF) header struct.pack(!BHH, seq 0xFF, len(payload), crc) return header payload sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(RTO) data open(send_file.bin, rb).read() offset 0 seq 0 sent_count 0 while offset len(data) or offset 0 and len(data) 0: payload data[offset:offset FRAME_LEN] frame make_frame(seq, payload) for retry in range(MAX_RETRY 1): sock.sendto(frame, (HOST, PORT)) sent_count 1 try: ack, _ sock.recvfrom(1) # ACK 帧只回 1 字节 if ack[0] seq: # 确认号与当前帧序列号匹配 seq ^ 1 # 序列号翻转 offset len(payload) break except socket.timeout: continue else: raise RuntimeError(f帧超时重传超过 {MAX_RETRY} 次传输失败) # 若最后一块正好等于一帧长度补发一个空结束帧 if offset len(data) and len(data) - offset 0: continue if offset len(data) and len(payload) FRAME_LEN: end_frame make_frame(seq, b) for retry in range(MAX_RETRY 1): sock.sendto(end_frame, (HOST, PORT)) try: ack, _ sock.recvfrom(1) if ack[0] seq: break except socket.timeout: continue break sock.close() print(f发送完成共发送 {sent_count} 个 UDP 包)注意几个容易写错的地方ack[0] seq这个条件很多人会写成ack[0] (seq ^ 1)那就永远匹配不上。接收方确认的是“我收到了你刚发来的序列号”不是“我期望下一个”。MAX_RETRY 1表示总共尝试MAX_RETRY 1次发送初始发送算一次重传算MAX_RETRY次。当尝试次数耗尽时for...else里的else分支会触发抛出RuntimeError。文件恰好是帧长整数倍的场景len(payload) FRAME_LEN且offset len(data)时补发一个 payload_len 为 0 的空帧接收方收到后写文件、回 ACK并据此判定传输结束。这个逻辑漏掉的话接收方会永远等下一帧实验报告写“传大文件时偶尔卡住”的九成是这个问题。发送方还需要注意发完一帧后不要立刻调用recvfrom做阻塞等待UDP 接收缓冲区有限如果接收方回 ACK 稍慢跟你下一次sendto撞在一起就会发生缓冲区覆盖。设置非阻塞超时settimeout是必须的不是可选项。3.3 接收端代码校验、写盘与去重、回 ACK接收端相对简单但有一个设计决定会影响整个协议的正确性——损坏的帧要不要回 ACK。我在实验指导里的做法是CRC 校验失败的帧直接丢弃不回任何 ACK让发送方超时重传。这模拟的是“帧在信道中损坏导致接收方收不到有效数据”的语义跟“帧丢失”在发送方眼里是一样的处理方式都表现为超时。import socket import struct import binascii HOST 127.0.0.1 PORT 9001 FRAME_LEN 1024 EXPECT_END False sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((HOST, PORT)) sock.settimeout(5) # 5 秒无新帧视为连接超时 out_file open(recv_file.bin, wb) expect_seq 0 while True: try: frame, addr sock.recvfrom(FRAME_LEN 32) except socket.timeout: print(接收超时异常结束) break seq frame[0] payload_len struct.unpack(!H, frame[1:3])[0] crc_recv struct.unpack(!H, frame[3:5])[0] payload frame[5:] # 1. 校验 crc_calc binascii.crc_hqx(payload, 0xFFFF) if crc_calc ! crc_recv: continue # 损坏帧丢弃不发 ACK # 2. 去重重复帧也回 ACK但不写文件 if seq expect_seq: out_file.write(payload) expect_seq ^ 1 # 3. 确认确认号就是收到的帧的序列号 sock.sendto(bytes([seq]), addr) # 4. 结束条件 if payload_len 0: break out_file.close() sock.close() print(接收完成)这段代码里最关键的是“去重也回 ACK”这个分支。如果接收方收到了重复帧却默默丢弃不回 ACK发送方会无限重传最后触发MAX_RETRY上限整个传输失败。这种情况在网络正常时不会出现但一旦你在发送端加了“模拟 ACK 丢失”的测试代码立刻原形毕露。接收缓冲大小recvfrom(FRAME_LEN 32)至少要比最大帧长多出帧头长度。如果接收方用recvfrom(1024)去收一个实际 1049 字节的帧多余部分会被丢弃UDP 会报告“buffer too small”并且悄悄把整个包丢掉——这个坑我排了一整个下午才定位到。最后结束条件用payload_len 0而不是seq 1是因为空帧是一个合法的特殊结束标记不需要依赖序列号来判断。如果你用“收到 seq 再翻转回 0 就结束”这种条件当文件长度恰为帧长整数倍时会被误判。4. 三个必调参数帧长、超时重传时间、最大重传次数4.1 帧长与 UDP 缓冲区为什么 1024 字节比 1400 更稳帧长的选择受三层约束UDP 负载上限65507 字节、以太网 MTU1500 字节减去 IP 头和 UDP 头后实际可用约 1472 字节、以及接收端缓冲区大小。常见做法是在 512 到 1400 字节之间取一个值我用的是 1024 字节。理由很简单第一1024 是 2 的幂分帧计算的offset每次加固定长度直观不易错第二1024 字节的 UDP 包在大多数路由器上不会触发 IP 分片丢包率显著低于超过 MTU 的包第三接收端缓冲区只要开到 1056 字节就能收下完整帧不会碰触到 UDP 接收缓冲的边界。如果你把帧长调到 2048 字节在跨网段传输时就会碰见 IP 分片。分片后的 UDP 报文中只要丢了一个分片整个 UDP 包就废了你的协议要重传整个 2048 字节而重传的分片还可能再丢——这种“重传放大”效应会让传输效率雪崩。所以实验场景里1024 是一个“不会出错”的选择。4.2 超时时间 RTO固定值还是自适应怎么量级算超时时间RTORetransmission Timeout是停等协议里最容易拍脑袋的参数。设小了ACK 还没回来你就重发导致大量重复帧设大了真正的丢包要等很久才被感知吞吐量惨不忍睹。最稳的办法是先测一次往返时延发送方发一个小包接收方收到后立刻回 ACK发送方记录从发出到收到 ACK 的时间差这就是当前网络的 RTT。RTO 至少取 2 倍 RTT留出足够的抖动余量。在同一台机器上跑实验127.0.0.1RTT 通常只有几十微秒所以实验代码里设 0.5 秒已经足够大不会误重传但如果你在虚拟机里跨宿主机跑RTT 可能到几毫秒甚至几十毫秒0.5 秒依然 safe。也可以做自适应 RTO每次收到 ACK 后更新rtt smoothed_rtt然后rto max(rtt * 2, 0.1)。但实验指导书不会要求这个我建议在第一版实现里用固定值等整条链路通了再回来调自适应。过早引入动态参数出了问题你分不清是协议逻辑错还是 RTO 估算错。4.3 重传上限不设上限会死循环设太小会误判失败发送方的MAX_RETRY设多少取决于你对网络质量的预期。实验环境本机回环下丢包率接近零重传上限设个 35 次就够但如果指导书要求模拟随机丢包率 10% 甚至 30%你就需要把上限调大同时记录重传次数作为实验报告里的“重传率”指标。注意一个容易被忽略的问题重传次数上限耗尽时代码抛RuntimeError整个传输失败。但实际网络里偶尔一次丢满 3 次不代表真的不通可能只是路由器缓存了较大的突发流量。更稳妥的做法是记录当前累计丢包次数当超过 5 次时才放弃而不是每一帧都从零计数。参数默认值影响实验环境建议FRAME_LEN1024 字节分片、缓冲、效率5121400RTO0.5 秒误重传率 vs 丢包感知速度2×实测 RTT下限 0.1sMAX_RETRY3 次单帧失败容忍度本机 3模拟丢包 1030接收缓冲帧长32 字节收不下则丢包不小于帧长帧头5. 停止等待协议实验的避坑记录现象、原因、解决办法5.1 文件大小对、md5 不对只写 payload 没按长度截断现象传输完成后接收方文件的大小与源文件完全一致但md5sum对不上多出或缺少若干字节。原因recvfrom返回的payload实际长度跟payload_len可能不一致。当最后一帧不满 1024 字节时UDP 发送的包就是实际长度没问题但如果你在接收端用固定缓冲去收recvfrom返回的 bytes 对象是“实际收到的长度”不是缓冲大小。问题出在发送端你在拼帧时struct.pack(!BHH, ...)后追加的payload长度就是实际数据长度发送没问题。而接收端如果把整个frame[5:]都写进文件而发送端这一帧的 payload 长度字段是 1024但实际上后面还混入了下一帧的数据UDP 不会粘包所以这个不太常见——真正常见的是你用recvfrom(1024)去收一个 1049 字节的完整帧UDP 层把多余部分截断但你写入文件时用了payload frame[5:5payload_len]硬取多出来的字节是上一个包的残留。解决接收端写入文件前强制截断out_file.write(payload[:payload_len])。同时recvfrom的缓冲大小必须大于最大帧长否则高字节部分会被静默丢弃。5.2 传几十 KB 就卡死接收缓冲设太小UDP 悄悄丢包现象传小文件一切正常文件一超过 100 KB 就卡住或者传输失败。原因接收端的 socket 接收缓冲区有大小限制默认约 64 KB。发送端的sendto是异步非阻塞的它把数据交给内核就返回了不等待接收端真正收走。当发送速率快于接收处理速率时内核缓冲区满了新来的 UDP 包直接被丢弃发送方等不到 ACK超时重传但重传的包也可能被丢弃最终卡死。解决这是实验里最隐蔽的坑。接收端加一段代码sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 20)把接收缓冲区扩到 1 MB。更重要是发送端在每帧后加一个极短的time.sleep(0.001)或time.sleep(0.0001)人为限制发送速率让接收端有时间处理。这不算作弊——真实链路里 RTT 天然限速只有本机回环环境会出现这种“零延迟导致缓冲区爆掉”的假象。5.3 加了随机丢包后程序假死把损坏帧继续当有效帧处理现象按照指导书要求在发送端加了随机丢包模拟比如 10% 概率不发送结果接收端跑几帧后不动了日志里一直显示发送端在重试但接收端不崩溃也没输出。原因你只模拟了“数据帧丢失”但接收端处理时把 CRC 校验写成了“校验失败也照常写文件”。更常见的是丢包模拟加在了错误位置发送端代码里random.random() 0.1时跳过了sendto但跳过后没有进入超时分支而是直接走了“发送成功”的逻辑导致 ACK 永远等不到发送端也不重试。解决丢包模拟要加在sendto调用处并且必须让发送逻辑感知到这次“发送失败”——最干净的做法是丢包时直接continue进下一个 retry 循环而不是跳过sendto继续往下走。接收端的 CRC 校验失败分支要写清楚continue绝不写文件、绝不回 ACK。所有日志里要打印帧号方便你看清是丢在哪一帧。5.4 超时时间设太短日志疯狂刷 retry文件却毫无进展现象发送端日志每秒刷几十条“retry”接收端文件大小迟迟不增长偶尔涨一点。原因RTO 设置比实际 ACK 返回时间还短。比如本机回环下settimeout(0.001)接收端还没来得及处理并把 ACK send 回来发送端就认为超时开始重传。然后网络里全是重复帧发送端和接收端都在互相挤占缓冲区效率极低。解决把 RTO 设到实测 RTT 的 2 倍以上。本机回环实测 RTT 通常小于 1 ms所以 0.1 秒到 0.5 秒之间都是安全区间。如果你在虚拟机上做实验先打印一次 RTT 看看量级再定 RTO。避免“拍脑袋 0.01 秒看着挺快”这种玄学调参。5.5 空帧结束条件触发后接收端又收到了旧帧现象接收端收到空帧后break退出了循环波形正常但过了一会发送端报错“超时重传超限”。原因接收端收到空帧后立即close()socket但发送端在 break 之前还有可能在等待这个空帧的 ACK。收端 socket 一关ACK 发不出去发送端收不到确认重试到超限。解决接收端在break后不要立刻关 socket先time.sleep(0.2)确保最后一个 ACK 已经发出或者直接不优雅处理——发送端重试到超限时报错虽然难看但文件内容其实已经完整接收。更规范的做法是在空帧 ACK 发出后再 break代码里把sock.sendto(bytes([seq]), addr)放在结束条件判断之前让空帧也走正常的确认流程。6. 可靠性验证方法差错注入、md5 比对与滑窗进阶6.1 写一个丢包注入器来压测你的协议实验指导书里最容易被忽略的一步是“验证协议在丢包环境下依然可靠”。你可能会说我都模拟丢包了怎么可能可靠但这正是这个实验的核心价值——你的协议必须能对抗丢包而不是靠网络不出错。我的验证脚本是这样的发送端加一个loss_rate参数sendto前random.random() loss_rate时跳过发送同时打印一行[DROP] seq0。接收端记录每次收到帧时的 CRC 校验结果和序列号变化。传输完成后运行md5sum send_file.bin recv_file.bin对比校验值。你会看到丢包率 10% 时重传次数明显增加但文件内容完全一致丢包率超过 30% 时MAX_RETRY 3大概率触发超限你需要上调重试次数才能稳定传完。这个测试结果要写进实验报告比任何原理分析都有说服力。6.2 进阶方向从停等移动到滑动窗口做完停等协议一个自然的进阶题是把它改成“流水线传输”发送方连续发 N 帧接收方累计确认窗口大小设为 4。这就变成 Go-Back-N 了你只需要把序列号从 1 位扩到 3 位把发送端的状态从“一帧一等”改成“窗口内连发”接收端补一个“乱序帧直接丢弃”的分支。我建议有精力的同学把这个扩展版也写了因为面试或复试时聊“你亲手实现的可靠传输协议”停等版最多证明你懂原理滑窗版能证明你真正理解了吞吐量瓶颈在哪。最后一个教训做这个实验千万别只测一次就交。我的习惯是至少跑 10 次传输每次用不同大小的文件1 KB、1 MB、5 MB以及一个恰好 1024 字节整数倍的文件随机种子换三次记录每次的重传次数和耗时。这个习惯帮我抓住过一个只在文件大小恰好等于整数倍帧长时才会触发的收尾 bug——而那种 bug 在课程提交截止前最后一天被抓住是最幸运的事。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。