资讯动态

停止等待协议实现串口文件传输:帧格式、超时重传与避坑指南

发布时间:2026/10/9 6:20:29 来源:尧图企业网站定制
简介这份文档资料面向高校计算机网络课程的学习者与实验指导教师聚焦实验二「利用停止等待协议传输数据文件」帮助读者在串行口编程基础上理解可靠传输机制。内容围绕停等协议的基本工作过程展开涵盖数据包丢失、确认信息丢失或出错时的定时器重传与0/1序号编号处理并延伸至面向字符的BSC协议讲解SOH、STX、ETX、EOT、DLE等控制字符的功能、数据报文与控制报文格式、透明数据传输及链路建立、传输、拆除三阶段流程最后给出简化停等协议的报文格式与编程思路。资源包共1个doc文件约81KB为实验指导手册类文档结构紧凑、便于打印或对照实验环境使用。目前已有87人学习适合需要完成实验报告、理解流量控制协议原理或准备网络课程实践的学生参考。1. 停止等待协议到底在解决什么问题从一条串口线说起两台机器之间拉一根串口线想把一个几 MB 的数据文件从 A 端搬到 B 端最朴素的做法是 A 端一口气把文件字节全怼出去B 端收到多少算多少。真跑起来你会发现只要线路有一点噪声、接收缓冲区有一次没跟上文件就悄悄坏掉了——而且坏得毫无征兆校验和不对时你甚至不知道是哪一个字节出的问题。停止等待协议Stop-and-Wait就是为这种「不可靠链路上做可靠传输」的场景准备的最小可用方案发一帧等一个确认确认到了再发下一帧超时就重传。这个标题对应的实验本质是让你在串口或模拟串口上手工实现一套带帧边界、序号、确认和超时重传的传输逻辑把「文件传输」这件看起来理所当然的事拆开看。它适合两类人一类是刚学完计算机网络、想搞明白滑动窗口之前那一步到底怎么落地的新手另一类是要在嵌入式设备、工控板、老式仪器上做点对点文件搬运的工程师这类场景里 TCP 未必可用BSC 之类的同步协议又太重停止等待反而是最省心的选择。下面我按「协议怎么设计 → 代码怎么写 → 参数怎么调 → 坑在哪」的顺序讲代码用 Python 串口库演示逻辑换成 C 或 C# 的 CFile 读写也一样成立。2. 帧格式、序号与确认停止等待协议的最小设计2.1 为什么不能直接把文件字节流写进串口串口是字节流设备没有消息边界。你 write 了 100 字节对端 read 可能一次拿到 37 字节下一次拿到 63 字节也可能两次拿到 5050。如果直接把文件内容丢进去接收方根本不知道一帧从哪开始、到哪结束。所以第一件事是定帧格式帧头、序号、长度、数据、校验缺一不可。我一般用的最小帧结构是这样字段长度说明HEAD2 字节固定 0xAA 0x55用于帧同步SEQ1 字节0 或 1停止等待只需要 1 bit用 1 字节方便调试LEN2 字节数据段长度大端DATALEN 字节文件分片建议不超过 1024CRC2 字节对 SEQLENDATA 做 CRC16序号只用 0 和 1 交替这是停止等待协议的精髓接收方收到序号 0 的帧回 ACK0收到序号 1 的帧回 ACK1。如果收到重复序号的帧说明上一个 ACK 丢了发送方重传接收方丢弃数据但重新回一次 ACK这样发送方就能继续。这个「重复帧也要回 ACK」的细节是很多人第一次实现时最容易漏的地方漏了就会死锁。2.2 发送端状态机一个循环加一个超时发送端的逻辑可以写得很短核心就是「发一帧 → 等 ACK → 超时重发」import serial import struct import time ACK_TIMEOUT 0.5 # 秒串口波特率低时要调大 MAX_RETRY 10 # 单帧最大重传次数 def crc16(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b 8 for _ in range(8): crc (crc 1) ^ 0x1021 if crc 0x8000 else crc 1 crc 0xFFFF return crc def build_frame(seq: int, payload: bytes) - bytes: body struct.pack(BH, seq, len(payload)) payload return b\xAA\x55 body struct.pack(H, crc16(body)) def send_file(port: str, filepath: str): ser serial.Serial(port, 115200, timeout0.1) seq 0 with open(filepath, rb) as f: while True: chunk f.read(1024) if not chunk: break frame build_frame(seq, chunk) retry 0 while retry MAX_RETRY: ser.reset_input_buffer() # 关键清掉上一轮残留 ser.write(frame) deadline time.time() ACK_TIMEOUT ack wait_ack(ser, deadline) if ack seq: break retry 1 else: raise RuntimeError(fframe {seq} failed after {MAX_RETRY} retries) seq ^ 1 # 0/1 翻转 ser.close()wait_ack负责在 deadline 之前从串口读 ACK 帧ACK 帧可以简化成0xAA 0x55 ACK_SEQ三字节。这里有几个参数必须说清楚ACK_TIMEOUT不能拍脑袋定它至少要覆盖「对端处理一帧 回 ACK 线路往返」的时间115200 波特率下传 1024 字节大约 90ms加上对端写文件的时间0.5s 是保守值MAX_RETRY给 10 次是为了防止线路彻底断开时无限重传实际调试时可以设成 3 先看行为。ser.reset_input_buffer()这行是血泪经验如果不清上一轮超时后迟到的 ACK 会被这一轮误读导致序号错乱。2.3 接收端先校验再落盘重复帧只回 ACK接收端比发送端更需要小心因为它要同时处理「帧不完整」「CRC 错」「序号重复」三种情况def recv_file(port: str, outpath: str): ser serial.Serial(port, 115200, timeout0.1) expected 0 buf bytearray() with open(outpath, wb) as f: while True: frame read_frame(ser) # 按帧头同步读满一帧 if frame is None: continue # 超时或帧不完整继续等 seq, payload frame if crc16(struct.pack(BH, seq, len(payload)) payload) ! frame_crc: continue # CRC 错丢弃等发送方超时重传 if seq expected: f.write(payload) expected ^ 1 send_ack(ser, seq) # 无论新旧都回 ACK if len(payload) 1024: # 约定短帧代表文件结束 break ser.close()注意send_ack在if seq expected之外这是重复帧处理的关键。另外文件结束的判断我用「数据段小于约定分片大小」来标记比单独发一个 EOF 帧简单但要求发送端最后一帧必须短于 1024如果文件正好是 1024 的整数倍需要补一个 0 长度帧这个边界后面避坑章节会再提。3. 串口参数、缓冲区与文件读写把链路调通3.1 串口初始化参数怎么定串口配置错了后面所有协议逻辑都是白搭。常见做法是两端约定115200 8N1即波特率 115200、8 数据位、无校验、1 停止位。如果你用的是 USB 转串口模块还要注意流控默认rtsctsFalse除非你的线序确实接了 RTS/CTS。在 Python 里就是ser serial.Serial( portCOM3, # Linux 下是 /dev/ttyUSB0 baudrate115200, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout0.1, # 读超时决定 read_frame 多久返回一次 write_timeout1.0 )timeout0.1这个值影响接收端的轮询节奏太小会频繁空转太大则一帧读一半就返回、被误判为不完整帧。我一般设成「一帧传输时间的 1/5」左右115200 下 1024 字节约 90ms所以 0.1s 附近合适。如果你把分片调到 4096这个超时要跟着放大到 0.4s 以上否则永远读不满一帧。3.2 文件分片大小与内存的权衡分片大小直接决定吞吐和内存占用。停止等待协议每发一帧就要等一个 RTT所以分片越大有效吞吐越高但单帧出错重传的代价也越大。实测下来分片大小115200 下理论吞吐适用场景256 字节约 2.5 KB/s线路噪声大、MCU 内存紧张1024 字节约 8 KB/s常规串口文件传输4096 字节约 20 KB/s短距离、低误码、两端都有缓冲注意这是停止等待的硬上限因为每帧都要等 ACK利用率天然低。如果你要传几十 MB停止等待会慢到让你怀疑人生那时候就该上滑动窗口了——但这个实验的目的就是把停止等待跑通慢是预期内的。3.3 用 CFile 做落盘时的注意点如果接收端是 Windows 上的 MFC 程序用CFile写文件很常见CFile outFile(_T(recv.bin), CFile::modeCreate | CFile::modeWrite); outFile.Write(payload, payloadLen); outFile.Flush(); // 关键不 Flush 可能丢最后一块 outFile.Close();Flush()这行别省。串口接收线程和文件写线程如果不是同一个还要加锁否则Write交错会把文件写花。另外CFile::modeCreate默认会截断已有文件如果你要做断点续传得改成modeNoTruncate并自己管理偏移这属于进阶用法后面最后一章会提。4. 停止等待协议文件传输的避坑与排查4.1 现象传了几帧就卡死两端都在等原因ACK 帧被发送端的reset_input_buffer()清掉了或者接收端回 ACK 时发送端已经超时进入重传重传帧又被接收端当成新帧处理序号错位后双方永远对不上。解决发送端只在「开始等 ACK 之前」清一次缓冲区不要在重传循环里反复清接收端对重复序号必须回 ACK 而不是丢弃不理。4.2 现象文件末尾总是少几个字节原因最后一帧短于分片大小接收端read_frame按固定长度读读不满就返回 None被当成超时丢弃。解决read_frame要先读帧头再从 LEN 字段算出实际长度去读不能按最大分片长度死等同时发送端要保证最后一帧一定短于分片大小正好整除时补一个 0 长度帧。4.3 现象CRC 校验总是失败但数据看着没错原因CRC 计算范围两端不一致。常见的是发送端算了 HEADSEQLENDATA接收端只算了 SEQLENDATA或者字节序一个大端一个小端。解决把 CRC 覆盖范围写进协议文档两端用同一份crc16实现调试时先传一个固定字节如0x31验证 CRC 值是否一致。4.4 现象波特率一高就丢帧降速就好原因不是协议问题是串口线质量或驱动缓冲区太小。USB 转串口芯片在高波特率下对线材敏感劣质线在 115200 以上误码率飙升。解决先降到 57600 验证协议逻辑确认无误后再逐步升速同时把驱动接收缓冲区调大Windows 设备管理器里改Linux 用setserial减少溢出丢字节。4.5 现象接收端文件比发送端大原因重复帧被重复写入。发送端超时重传接收端收到相同序号帧如果没判断seq expected就直接写文件就会多出重复数据。解决严格按序号判断只有seq expected才写盘并翻转期望序号重复帧只回 ACK 不写。5. 从停止等待到实用文件传输几个能立刻用上的技巧把基础版跑通之后有几个小改动能让这套东西从「实验能过」变成「真能干活」。第一个是加一个简单的会话结束确认发送端传完最后一帧后等接收端回一个 FIN-ACK 再关闭串口避免最后一块数据还在缓冲区里程序就退出了。第二个是断点续传的雏形在文件头加 4 字节总长度和 4 字节已传偏移接收端启动时先读已有文件大小把偏移通过一个握手帧告诉发送端发送端seek到对应位置继续这样传大文件中途断了不用从头再来。第三个技巧是超时时间的自适应。固定ACK_TIMEOUT在链路状况变化时会翻车线路好时等太久浪费吞吐线路差时又不够。我一般会记录最近 5 帧的 ACK 往返时间取平均值的 2 倍作为动态超时下限 0.2s、上限 2s。代码上就是维护一个rtt_list每次成功收到 ACK 就append并裁剪长度超时判断用2 * sum(rtt_list) / len(rtt_list)。这个改动不大但在实际设备上能明显减少无谓重传。最后一个习惯每次改协议字段先写一个只传 1KB 固定内容的回环测试TX 和 RX 短接确认帧格式、CRC、序号翻转都对再上真实文件。我早期图省事直接传大文件结果一个 CRC 范围写错排查了一下午后来就老老实实先跑回环。这套停止等待的实现虽然简单但它是理解可靠传输的起点把它写扎实了后面看滑动窗口、看 TCP 的序号和确认都会顺很多。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑