2026/10/5 7:30:08

串口文件传输中停止等待协议的实现与避坑指南

串口文件传输中停止等待协议的实现与避坑指南 简介一套面向高校计算机网络课程的实验指导文档聚焦“利用停止等待协议传输数据文件”这一实验配有实验目的、实验环境、实验概述与简化BSC协议实现说明适合网络原理课程配套实训或自学串行口编程的读者。文档重点剖析停止等待协议的工作过程包括ACK/NAK应答、超时重传、数据包与确认信息丢失的处理、0/1编号去重机制并介绍BSC中SOH、STX、ETX、EOT、DLE等控制字符及报文格式便于学生按步骤完成从链路建立到数据传输的完整流程。资源包共1个doc文件约81KB内容图文结合含过程示意图和控制字符表可直接用于实验预习、课堂讲解或课后复习。这份实验指导已有87人学习对于希望理解可靠传输原理并提升网络编程与排错能力的学生具有较高的参考价值。1. 为什么停等协议值得在串口上复现一遍做计算机网络实验的时候很多人第一反应是去抓以太网包、看 TCP 重传觉得那才算接触到了“真实网络”。但如果你想真正搞懂可靠传输是怎么一点一点被设计出来的停止等待协议反而是最好的切入点——它足够简单简单到你能在串口上亲眼看到每一个 ACK、NAK 和超时重传又足够完整涵盖编号、确认、超时、重传所有要素。这份实验指导把停等协议落到串口文件传输上不依赖任何交换机或路由器一根交叉串口线、两台电脑就能把数据链路层的核心机制跑通。适合做网络课程设计、准备考研复试动手题、或者刚接触串口编程想找一份完整参考的从业者。2. 停等协议的核心机制从 ACK/NAK 到超时重传2.1 协议状态机与两套编号停止等待协议的本质是“发一个、等一个、确认后再发下一个”。发送方维护一个状态发送数据包后进入等待确认状态只有收到 ACK 才进入下一个包的发送收到 NAK 则重发当前包。接收方的状态更简单正确收到就回 ACK校验失败就回 NAK 并丢弃该包。这里最容易忽略的一点是编号机制。协议要求至少用 0 和 1 两个编号交替而不是给每个包唯一编号。为什么两个就够因为停等协议里链路上最多只有一个“在途”的数据包接收方只需要区分“当前包”和“上一个包”。当它连续收到两个相同编号的包就能判定这是重复包丢弃并重发确认信息。ACK 本身也需要编号。实验指导里有个容易绕晕的约定ACK1 表示“已正确收到编号为 0 的数据包请发送编号为 1 的包”ACK0 则相反。也就是说 ACKn 里的 n 表示下一个期望收到的数据包编号而不是刚确认的那个编号。初写程序时很容易把这个搞反导致发送方一直等、接收方一直丢。确认信息丢失场景下这个约定就能看出作用发送方发出编号 0 的包超时后重发编号 0 的包接收方第一次已经收正确了第二次收到相同编号 0 的包知道这是重复于是丢弃并再次回复 ACK1。这样双方通过编号就能从“确认丢失”中恢复不会出现数据重复写入文件的问题。2.2 超时重传与定时器参数设定数据包丢失时接收方根本不会回任何确认因为对方压根不知道有数据发过来。所以协议必须引入定时器发送方发完一个数据包就启动计时如果超时仍未收到任何回应就认定数据包丢失重发同一编号的包。重传次数一般要设置上限比如 3 到 5 次超过后判定信道故障结束本次传输。定时器的时长是实验里最值得调的一个参数。串口通信和以太网不同延迟主要由波特率和数据包长度决定而不是链路物理距离。例如波特率 9600一个 1500 字节的数据包串行发送耗时大约 1500 × 8 / 9600 1.25 秒。这还没算接收方处理、回 ACK 的时间。超时时间如果设得比这还短就会出现“明明数据还在路上发送方却开始重传”的问题接收方会收到大量重复包效率骤降设得太长数据包真丢了要等很久才发现。常见做法是先测一次空载往返时间再在这个基础上乘以 1.5 到 2 倍作为超时值。实验指导里提到“线路延迟 0 微秒”就是在模拟零延迟的理想链路但实际串口传输中仅波特率导致的发送延迟就不可忽略。我一般会把超时设为“发送一包耗时 接收方处理耗时 回 ACK 耗时”之和的 1.5 倍这样既不误判又能在数据包丢失后较快恢复。定时器还要注意一个细节必须在发送完成后再启动而不是在调用串口写入函数时就启动。否则定时器可能先于数据发送完毕触发导致重复重传。另一种常见错误是在等待 ACK 期间不断重置定时器这会让超时判断失效数据包丢了也不重发。2.3 BSC 协议面向字符的停等实例BSC 是典型的面向字符停等协议利用 ASCII 控制字符组合来表达状态。核心控制字符及其功能可以整理成一张表编程时直接对照控制字符ASCII 编码功能SOH01H报头开始STX02H正文开始ETX03H正文结束EOT04H传输结束ENQ05H请求建立链路ACK06H正确确认NAK15H负确认DLE10H转义SYN16H同步ETB17H组传输结束BSC 的链路过程分三阶段发送 ENQ 询问接收方是否可通信接收方回 ACK 表示同意建立链路然后逐帧传数据每帧由 SYN 同步、可选的 SOH 报头、STX 正文、ETB/ETX 结尾、BCC 校验组成全部传完发 EOT 拆除链路。这里最值得注意的设计是 DLE 转义。因为正文是字符数据很可能出现与 ETX03H、STX02H等控制字符编码相同的数据字节接收方会误判。BSC 的做法是在正文里遇到与控制字符相同的字节时前面加一个 DLE。接收方看到 DLE 就知道下一个字节是数据而不是控制字符。你需要记住的是DLE 本身出现在正文中时也要再加一个 DLE 转义也就是 DLE DLE 表示一个真实的数据字节 0x10。简化版实验没有保留完整的 DLE 处理只用 6 个控制字符STX、ETX、EOT、ENQ、ACK、DLE。这更贴近课程设计的可操作性但理解 BSC 的完整控制字符体系能帮你写对检查逻辑。3. 简化版实验实现报文格式与程序骨架3.1 报文格式STX 编号 正文 BCC ETX实验指导给出的简化数据报文格式是STX 开头编号 0 或 1可变长正文BCC 校验ETX 结束。正文长度通常取 256、512、1024、2048 字节。这个格式去掉报头、减少了控制字符数量但保留了停等协议最关键的三要素帧定界、编号、校验。帧定界靠 STX 和 ETX 两个特殊字符完成。接收方的状态机大概是看到 STX 进入接收态记录之后的编号和正文字节直到遇到 ETX 才认为一帧接收完毕。之所以要“直到遇到 ETX”是因为正文长度在程序里虽然已知但接收方不知道发送方这次到底发了多长——只有 ETX 是明确的帧结束标志。接收判定逻辑按以下顺序执行收到 STX 后读取编号字段存为当前帧编号。持续读正文并累计 BCC 校验值直到收到 ETX。收到 ETX 后对比累计 BCC 与报文中携带的 BCC 值。BCC 正确且编号是新编号与上一帧不同写入文件并回 ACK编号为期望的下一帧号。BCC 错误则直接丢弃不回任何确认等发送方超时重传。编号与上一帧相同说明是重复包丢弃但回 ACK因为之前那个 ACK 可能丢失了。这里有个需要提醒的细节第 4 步和第 6 步都回 ACK但回的内容不同。第 4 步回的是对当前帧的确认第 6 步回的是对“上一个帧”的重复确认。很多同学在这两步写成一个逻辑导致连续收到重复帧时 ACK 编号错乱。校验方面实验默认用奇偶校验。但单字节奇偶校验对多字节差错无能为力实操中至少要用纵向校验——也就是把所有字节按位异或得到的 XOR 值作为 BCC。这样做虽然仍不如 CRC 可靠但能查出大多数错误。思考题里建议改成 CRC那需要引入查表法的 CRC-16代码量会明显增加。3.2 发送端与接收端程序骨架发送端核心逻辑可以整理成下面这段伪代码风格的程序骨架写在串口发送函数里// 发送一个数据包包号只能是 0 或 1 void SendPacket(BYTE packetNo, BYTE* data, int dataLen) { BYTE frame[MAX_FRAME_LEN]; int idx 0; frame[idx] STX; // 帧起始 frame[idx] packetNo; // 包编号 0 或 1 BYTE bcc 0; for (int i 0; i dataLen; i) { frame[idx] data[i]; bcc ^ data[i]; // 纵向异或校验或奇偶校验 } frame[idx] bcc; // 校验字段 frame[idx] ETX; // 帧结束 WriteFile(hSerial, frame, idx, written, NULL); StartTimer(FRAME_TIMEOUT); // 发送完成后启动超时定时器 }注意最后一个动作——StartTimer必须放在WriteFile之后否则定时器可能先触发导致同一帧被重复发送。packetNo取 0 或 1每成功收到一个对应 ACK 后翻转。接收端核心逻辑是一个状态机按接收到的每个字节推进// 接收一个字节返回 0 表示帧未完成1 表示收到完整帧 int ByteReceiver(BYTE ch, BYTE* outData, int* outLen) { switch (rxState) { case WAIT_STX: if (ch STX) { rxState WAIT_NO; } break; case WAIT_NO: rxNo ch 0x01; // 取最低位作为包编号 rxState WAIT_DATA; rxBcc 0; rxDataLen 0; break; case WAIT_DATA: if (ch ETX) { rxState WAIT_STX; *outLen rxDataLen; if (rxBcc rxFrameBcc) { // 校验通过 return 1; } return 0; // 校验失败丢弃 } outData[rxDataLen] ch; // 存入数据并累计校验 rxBcc ^ ch; break; } return 0; }这段状态机里有个容易出错的点帧结束判定必须在WAIT_DATA状态中先检查ch ETX而不是先写入数据。否则正文中的 ETX 会被误当作数据存入正文末尾的正常 ETX 就不会被识别成帧边界。完整帧返回后调用方需要比较rxNo与lastNo判断是否重复并生成对应的 ACK 响应。3.3 CFileDialog 与 CFile文件选择与读写实验界面要求用 CFileDialog 选择发送文件和保存接收文件。它的构造函数参数比较多值得逐一说明参数作用实验中最常用设置bOpenFileDialogTRUE 为“打开”FALSE 为“保存”发送用 TRUE接收用 FALSElpszDefExt默认扩展名.txtlpszFileName初始文件名NULL 或 receive.txtdwFlags对话框行为标志OFN_HIDEREADONLY 或默认lpszFilter文件类型过滤器文本文件*.txt|*.*pParentWnd父窗口指针NULLdwSizeMFC 内部使用0构造对话框对象后调用DoModal()显示返回IDOK时用GetPathName()取完整路径。今天界面库丰富但 CFileDialog 的逻辑依旧有参考价值——它的文件过滤器和默认扩展名机制可以迁移到任何桌面程序里。CFile 类的使用套路是 Open、Read/Write、Close。关键参数nOpenFlags决定文件打开模式modeRead只读、modeWrite只写、modeReadWrite读写typeBinary二进制方式打开。实验中最容易翻车的地方是接收文件没有用typeBinary打开——文本模式下 CFile 会遇到特殊字节比如 0x1A就截断导致接收到的文件比原始文件短。CFile recvFile; if (recvFile.Open(recvPath, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary)) { recvFile.Write(buffer, actualLen); // 写入校验、去重后的一帧正文 recvFile.Close(); }注意modeCreate标志它在文件已存在时会清空重写。接收端如果没加这个标志第二次运行时文件会累积旧内容造成字节数不对。发送端则用modeRead | typeBinary打开源文件避免文本模式下的换行转换把二进制文件改坏。3.4 日志列表与统计信息实验指导特别强调界面必须有日志列表记录整个发送和接收过程。这不仅是展示也是调试工具——协议卡住时看一眼日志里最后一条消息是“发送 ENQ”还是“等待 ACK”就能定位问题。日志至少要记录以下事件进入发送/接收状态发送 ENQ收到 ACK 0 或 NAK发送数据包编号 0/1收到 ACK 1/ACK 0超时重发数据包编号发送 EOT返回空闲状态界面上还要求显示统计信息数据速率、数据包长、线路延迟、数据长度、传输耗时、传输效率。其中传输效率的计算方法是实验的隐藏考点见第 5 章。4. 避坑清单串口文件传输的五个高频问题4.1 接收文件比源文件短随机丢尾部字节现象传完文件后对比大小接收端文件总是少十几个字节而且每次少的量不同。原因多数情况是文本模式打开文件导致的。CFile 以typeText打开时会把 0x1A 当作 EOF 处理而二进制文件或文本文件里恰好有 0x1A 字节会被截断。串口收到的数据本身是完整的文件写入环节出了问题。解决接收文件和发送文件都必须用typeBinary打开。发送端读文件、接收端写文件都加上这个标志接收到的字节原样落盘。4.2 收到连续重复帧文件内容被重复写入现象日志显示发送方不断重发编号 0 的数据包接收方日志显示连续收到编号 0文件里出现两遍同样的数据。原因ACK 在返回途中丢失发送方超时重发接收方第一次已经写入了文件第二次收到同编号帧时不知道这是重复帧。解决接收端在写文件前必须检查帧编号。实现时要单独用一个变量lastWriteNo记录上一次写入文件的帧编号收到新帧时先比较编号相同则是重复帧丢弃但不写文件同时回 ACK 让对方继续编号不同才写入文件。4.3 正文里出现 ETX 字符帧被提前截断现象接收方收到的文件中间断掉或者数据显示到一半就触发了“帧结束”逻辑。原因正文是任意字节完全可能包含 0x03ETX。接收端在WAIT_DATA状态没区分“数据中的 0x03”和“帧尾的 0x03”直接判定帧结束。解决引入 DLE 转义。发送端遇到正文中的 0x03 或 0x10 时在前面补一个 DLE接收端收到 DLE 后跳过转义处理把下一个字节当作普通数据写入。这是 BSC 协议的标准做法简化实验里为了省事可以不做但在正文可能含任意字节的通用传输里必须处理。4.4 9600 波特率下偶发校验错误重传频繁现象日志里大量出现 NAK 和重传传输效率极低但文件最终能传完。原因一种可能是波特率不匹配——两台机器实际波特率有微小偏差导致采样点偏移另一种可能是 USB 转串口线的驱动缓冲区过小在系统调度延迟时丢字节。前者是硬件问题后者是驱动配置。解决先确认两台机器波特率设置完全一致测试时用SetCommState配置不要依赖默认值。如果使用 USB 转串口线查看设备管理器里串口属性把接收缓冲区调大关闭“FIFO 缓冲区溢出时中断”。另外在串口打开后调用PurgeComm清空缓冲区再开始传输。4.5 发送方收不到任何 ACK程序卡死现象界面日志停在“发送ENQ”或“发送数据包 0”之后一直等不到任何响应。原因最常见的是链路建立阶段就失败——接收方没有进入接收状态对 ENQ 无响应或者收发两根线接反交叉线做成了直连线数据根本没到对端。解决先用一个最简单的串口回环程序验证物理链路发送端发一个字节如果接收端能收到说明线路没问题。再做协议握手测试单独发 ENQ看对端是否回 ACK。链路不通时不要继续调协议逻辑先解决物理层。5. 收尾技巧用线路延迟参数验证停等协议效率实验指导里有个容易被忽略的统计项“线路延迟0 微秒”。很多同学直接忽略它但它是验证停等协议理论效率的关键参数。停等协议的效率公式是U td / (td ta)其中 td 是发送一个数据包的时间ta 是从发送完成到收到 ACK 的等待时间包括线路传播延迟、接收方处理延迟和 ACK 自身的发送时间。当线路延迟为 0 时效率主要取决于 td 和 ACK 发送时间的比值当线路延迟增大效率会急剧下降。最直观的实验方法是写一个支持“模拟线路延迟”的程序版本。发送方发完一帧后不立刻等待 ACK而是先休眠一个可配置的毫秒数再进入等待状态。通过改变这个延迟参数观察传输效率和耗时如何变化模拟线路延迟传输耗时统计效率现象0 微秒5 秒89.1%效率较高重传少1000 微秒接近翻倍明显下降每次等待时间拉长5000 微秒显著增大低于 50%大量时间花在等待上另一个值得做的实验是改变数据包长度。包长取 256 字节时一帧发送耗时短但同样的文件被拆成更多帧确认次数更多吞吐率可能反而降低包长取 2048 字节时帧发送耗时变长超时时间也要跟着调否则误重传。你可以固定波特率 9600、文件长度 3300 字节分别跑 256、512、1024、2048 四组数据记录效率和耗时画出包长-效率曲线。这个曲线能直观看到停等协议在短包和长包之间存在一个最优区间。从那以后我每次复现这个实验都会把“效率验证”作为收尾动作强制自己改一遍包长和延迟参数并记录下来。不是因为它能提高成绩而是因为只有亲手看到效率曲线掉下来才算真正理解为什么后来会出现连续 ARQ 和滑动窗口——停等协议的问题不在正确性而在效率。希望这个习惯对你也有帮助。本文还有配套的精品资源点击获取