news 2026/10/23 13:55:59

停止等待协议实现串口文件传输:从状态机到MD5校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
停止等待协议实现串口文件传输:从状态机到MD5校验

简介:这份文档资料面向高校计算机网络课程的学生与实验指导教师,聚焦实验二「利用停止等待协议传输数据文件」,帮助读者在串行口编程基础上理解可靠传输机制。内容围绕停等协议的基本工作过程展开,涵盖数据包丢失、确认信息丢失或出错时的定时器重传与0/1编号去重策略,并延伸介绍面向字符的BSC协议,包括SOH、STX、ETX、EOT、DLE等控制字符的功能、数据报文与控制报文格式、透明数据传输及链路建立、传输、拆除三阶段流程,最后给出简化停等协议的报文格式与编程要点。资源包内含1个doc文档,约81KB,结构紧凑,适合作为实验预习、课堂讲解与报告撰写的参考。目前已有87人学习,便于读者快速掌握协议原理并完成可靠文件传输的实验实现。

1. 停止等待协议到底在解决什么问题:从串口丢包说起

你在串口调试助手里敲下发送,对面设备回了“OK”,可文件内容却少了两百多字节——这种“明明发出去了,对方却没收到”的情况,在串行口文件传输里几乎每个工程师都遇到过。停止等待协议(Stop-and-Wait)就是为这种场景准备的最小可靠传输方案:发送方每发一帧数据,必须等接收方回一个确认帧(ACK),收到确认后才发下一帧;如果超时没等到确认,就重发当前帧。它不追求吞吐量,追求的是“每一帧都确认到达”。这篇文章围绕“利用停止等待协议传输数据文件”这个实验目标,把协议状态机、串口配置、文件分帧、BSC 控制字符、CFile 读写这几个环节串起来,让你能在本地把一条串口链路上的文件传输跑通,并且知道参数该设多少、翻车时该看哪里。适合正在做计算机网络实验、需要手写可靠传输逻辑的读者。

2. 停止等待协议的状态机与帧结构:为什么不能只发不等

停止等待协议看起来简单,但真正动手写的时候,很多人第一版代码就是“循环发送、不等确认”,结果在串口这种半双工或带缓冲的链路上直接翻车。要让它可靠工作,必须先把状态机和帧结构定清楚,再谈串口和文件读写。

2.1 发送方与接收方的四个核心状态

停止等待协议的本质是一个两状态交替的有限状态机。发送方只有两个稳定状态:等待上层数据(Ready)和等待确认(Wait for ACK)。接收方也只有两个:等待数据帧(Wait for Frame)和校验并回确认(Send ACK)。

发送方在 Ready 状态下从文件读一块数据,组帧后发出,同时启动超时定时器,进入 Wait for ACK。在 Wait for ACK 状态下收到正确 ACK,回到 Ready;收到错误 ACK 或超时,重发当前帧,定时器重置。接收方在 Wait for Frame 状态下收到帧,先校验帧头和校验和,正确则写入文件并回 ACK,错误则回 NAK 或直接丢弃等待重传。

这里有一个容易被忽略的点:ACK 本身也可能丢。如果接收方回了 ACK 但发送方没收到,发送方超时重发,接收方会收到重复帧。所以帧结构里必须带一个 1 位序号(0 和 1 交替),接收方据此判断是新帧还是重复帧。重复帧直接丢弃并重发 ACK,不能重复写文件。

提示:序号只有 1 位是停止等待协议的特点,因为任意时刻链路上最多只有一个未被确认的帧,0/1 交替足够区分。

2.2 帧格式设计与 BSC 控制字符的对应关系

很多教材会提到 BSC(Binary Synchronous Communication)控制字符,停止等待实验里常用的几个是 SOH、STX、ETX、ACK、NAK。它们的作用是给帧定界和确认。

一个可落地的帧格式如下:

字段长度说明
SOH1 字节帧起始,0x01
序号1 字节0 或 1
STX1 字节数据段起始,0x02
数据变长文件分块,建议 128~256 字节
ETX1 字节数据段结束,0x03
校验和1 字节对序号+数据做累加和取反
ACK/NAK1 字节接收方回送,0x06 或 0x15

数据块大小不是越大越好。串口接收缓冲区常见是 256 字节或 4096 字节,如果一帧数据超过缓冲区,接收方还没处理完就被下一帧覆盖,表现为“随机丢帧”。我一般把单帧数据控制在 128 字节,配合 9600 或 115200 波特率都够用。

2.3 用 Python 写一个最小停止等待发送端

下面这段代码只保留协议核心逻辑,串口用 pyserial,文件用内置 open 模拟 CFile 的读写行为。你可以直接改成 C++ 的 CFile 或 FILE* 版本。

import serial import time SOH, STX, ETX, ACK, NAK = 0x01, 0x02, 0x03, 0x06, 0x15 TIMEOUT = 1.0 # 超时秒数 MAX_RETRY = 5 # 最大重传次数 BLOCK_SIZE = 128 # 每帧数据字节数 def checksum(seq, data): s = seq + sum(data) return (~s) & 0xFF def build_frame(seq, data): return bytes([SOH, seq, STX]) + data + bytes([ETX, checksum(seq, data)]) def send_file(port, filepath): ser = serial.Serial(port, 115200, timeout=TIMEOUT) seq = 0 with open(filepath, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if not block: break frame = build_frame(seq, block) retry = 0 while retry < MAX_RETRY: ser.write(frame) resp = ser.read(1) if resp and resp[0] == ACK: seq ^= 1 # 序号翻转 break retry += 1 if retry >= MAX_RETRY: raise RuntimeError('重传次数超限,链路可能断开') ser.close()

逻辑说明:build_frame按帧格式拼字节;send_file每次读 128 字节,发帧后阻塞等 1 字节响应。收到 ACK 才翻转序号并读下一块;收到 NAK 或超时则重发同一帧。MAX_RETRY防止链路彻底断开时死循环。

参数说明:TIMEOUT要大于“一帧传输时间 + 接收方处理时间 + ACK 回传时间”。115200 波特率下 128 字节约 11ms,1 秒超时非常宽松。BLOCK_SIZE如果调到 1024,在 9600 波特率下单帧就要 1 秒以上,超时必须同步加大,否则会误重传。

2.4 接收端如何避免重复写文件

接收端比发送端多一个判断:收到帧后先看序号是否等于期望序号。如果等于,写入文件、翻转期望序号、回 ACK;如果不等于,说明是重复帧,直接回 ACK 但不写文件。

def recv_file(port, savepath): ser = serial.Serial(port, 115200, timeout=TIMEOUT) expected_seq = 0 with open(savepath, 'wb') as f: while True: head = ser.read(3) if len(head) < 3 or head[0] != SOH: continue seq = head[1] data = b'' while True: ch = ser.read(1) if not ch: break if ch[0] == ETX: break data += ch chk = ser.read(1) if not chk: continue if checksum(seq, data) != chk[0]: ser.write(bytes([NAK])) continue if seq == expected_seq: f.write(data) expected_seq ^= 1 ser.write(bytes([ACK]))

这段代码里expected_seq就是接收方的状态。重复帧到达时seq != expected_seq,仍然回 ACK,因为发送方需要这个 ACK 才能继续。如果不回,发送方会一直重传,链路卡死。

3. 串行口配置与文件分帧:参数不对,协议白写

协议逻辑写对只是第一步,串口参数和文件分帧策略直接决定实验能不能一次跑通。这一章把串口配置、流控、文件读写和 CFile 的对应关系讲清楚。

3.1 串口参数怎么设:波特率、数据位、停止位、校验位

串口两端必须完全一致,否则收到的全是乱码。常见配置是 115200-8-N-1,即波特率 115200、数据位 8、无校验、停止位 1。如果你用的是老式设备或单片机,9600-8-N-1 更稳。

参数常用值注意事项
波特率9600 / 115200两端必须相同,误差超过 2% 会丢帧
数据位87 位在传二进制文件时会丢高位
校验位None协议层已有校验和,串口校验可关
停止位1部分设备要求 2,以对端手册为准
流控无文件传输建议关流控,由协议层控制节奏

流控这里有个血泪经验:如果开了 RTS/CTS 但接线不对,串口会直接阻塞,表现为ser.write不返回或超时。实验环境下建议先关流控,确认协议跑通后再按需开启。

3.2 文件分帧与 CFile 读写:二进制模式不能省

文件传输必须用二进制模式打开。如果用文本模式,Windows 下会把0x0D 0x0A转换成0x0A,接收端写出的文件和原文件大小不一致,校验和也对不上。

C++ 里用 CFile 的写法:

CFile srcFile(_T("source.bin"), CFile::modeRead | CFile::typeBinary); CFile dstFile(_T("dest.bin"), CFile::modeCreate | CFile::modeWrite | CFile::typeBinary); BYTE buffer[128]; UINT nRead = srcFile.Read(buffer, 128); while (nRead > 0) { // 组帧发送 buffer 中前 nRead 字节 // 等待 ACK 后继续 nRead = srcFile.Read(buffer, 128); }

CFile::typeBinary是关键,不加这个标志,MFC 默认按文本处理,遇到 0x1A 还会当 EOF。Python 里对应的是open(path, 'rb')和open(path, 'wb'),不要用'r'和'w'。

分帧时最后一块可能不足 128 字节,接收端按实际收到的数据长度写入即可,不需要补齐。但帧头里的长度字段如果设计了,要按实际长度填。

3.3 超时与重传次数的工程取值

超时时间不是拍脑袋定的。按公式:TIMEOUT > 帧传输时间 + 接收处理时间 + ACK 传输时间 + 余量。

以 115200 波特率、128 字节数据为例:一帧约 134 字节(含帧头帧尾),传输时间约 11.6ms;接收方处理加写文件约 1ms;ACK 1 字节约 0.09ms。总时间不到 13ms。工程上取 200ms 到 1s 都合理。如果取 10ms,会因为接收方还没处理完就超时重传,导致大量重复帧。

重传次数一般取 3 到 5 次。超过这个次数还没收到 ACK,说明链路已经断开或对端程序崩溃,继续重传没有意义,应该报错退出。

注意:超时时间设得太短会让协议“假重传”,吞吐量暴跌;设得太长则链路断开后要等很久才报错。建议先用示波器或串口助手观察单帧往返时间,再定超时。

4. 避坑与排查:停止等待协议实验里最容易翻车的 5 个点

这一章按“现象 → 原因 → 解决”整理我在串口文件传输实验里踩过的坑,每条都对应真实调试场景。

4.1 现象:接收端文件比源文件大,尾部多出重复数据

原因:接收方没有判断序号,把重传的重复帧也写入了文件。发送方超时重传后,接收方收到相同序号的帧,仍然执行了写操作。

解决:接收方维护expected_seq,只有seq == expected_seq时才写文件并翻转序号;重复帧只回 ACK,不写文件。上面 2.4 的代码已经处理了这一点。

4.2 现象:发送方一直重传,接收方毫无反应

原因:串口参数不一致,最常见的是波特率不同或数据位不同。接收方收到的字节全是乱码,帧头 SOH 匹配不上,直接丢弃。

解决:两端打印各自的串口配置,逐项核对。用串口助手手动发一帧,看接收端能否正确解析。如果串口助手能通,说明是程序配置问题;如果串口助手也不通,检查线序和地线。

4.3 现象:传输到一半卡死,双方都在等

原因:ACK 丢失后发送方超时重传,但接收方已经翻转了expected_seq,把重传帧当重复帧处理,回了 ACK。如果这个 ACK 又丢失,发送方继续重传,接收方继续回 ACK,理论上能恢复。但如果接收方在等数据时收到了自己发的 ACK 回显(串口短接或回环),状态机会混乱。

解决:确认串口没有本地回环。如果是 RS485 半双工,发送和接收要分时切换,不能同时使能。软件上增加“收到非预期字节直接丢弃”的逻辑。

4.4 现象:小文件能传,大文件传到固定位置就出错

原因:文件分帧时用了文本模式,遇到 0x1A(Ctrl+Z)被当作 EOF,或者 0x0D 0x0A 被转换。固定位置出错通常对应文件中某个特殊字节。

解决:发送端和接收端都用二进制模式打开文件。CFile 加typeBinary,Python 用'rb'/'wb'。传输前可以先算源文件 MD5,接收完再算一次对比。

4.5 现象:校验和偶尔对不上,重传后又能过

原因:串口线过长或波特率过高导致误码。停止等待协议能通过重传纠正,但频繁重传说明物理层质量差。

解决:降低波特率到 9600 试试;缩短串口线;检查地线是否接好。如果必须高速传输,考虑加屏蔽线或改用带校验位的串口配置。协议层的校验和只能发现错误,不能纠正,物理层稳定才是根本。

5. 进阶技巧:用日志和 MD5 验证传输完整性

停止等待协议跑通之后,真正要交付的是一个“可验证可靠”的传输结果。我一般会在发送端和接收端各加一份日志,记录每一帧的序号、重传次数和时间戳,最后用 MD5 做端到端校验。

日志格式可以很简单:

import hashlib, time def log_frame(logfile, direction, seq, retry): with open(logfile, 'a') as lf: lf.write(f"{time.time():.3f} {direction} seq={seq} retry={retry}\n") def md5_of_file(path): h = hashlib.md5() with open(path, 'rb') as f: while True: chunk = f.read(4096) if not chunk: break h.update(chunk) return h.hexdigest()

发送前算源文件 MD5,接收完算目标文件 MD5,两者一致才认为传输成功。日志里如果发现某一帧retry次数超过 2,说明链路质量在下降,需要检查串口线或降低波特率。

一个具体技巧:把BLOCK_SIZE从 128 逐步调到 256、512,观察重传率变化。如果调大后重传率明显上升,说明接收方缓冲区或处理速度跟不上,应该退回小分块。停止等待协议的吞吐量本来就低,分块大小对总时间的影响远小于重传带来的开销。

我自己的习惯是:任何串口文件传输实验,先跑 1KB 小文件验证协议,再跑 1MB 文件看稳定性,最后用 MD5 确认。不要一上来就传大文件,出了问题连日志都来不及看。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 15:44:21

从零构建 Music_nano:极简命令行音乐播放器的设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 15:44:21

基于RK3588J的边缘网关:煤矿与风电数据采集实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 15:44:21

ASP.NET B/S三层架构大学生交流管理网站源码解析与部署指南

简介&#xff1a;这套大学生交流管理网站源码基于ASP.NET BS三层架构开发&#xff0c;覆盖项目发布、计划管理、成果展示、注册登录等校园交流核心模块&#xff0c;既适合新手学习网站开发流程&#xff0c;也适合有经验者作为课程设计或毕设框架参考。压缩包内共507个文件&…

作者头像 李华
网站建设 2026/10/23 15:44:19

给现有网络加两个“先验“就够了:SSC-Priors 源码级拆解

从今天觉醒,技术赋予每一个人数字生命给现有网络加两个"先验"就够了&#xff1a;SSC-Priors 源码级拆解 ① 技术背景&#xff1a;SSC 到底在解决什么问题 LiDAR 语义场景完成&#xff08;Semantic Scene Completion, SSC&#xff09;要做的事&#xff0c;可以一句话概…

作者头像 李华
网站建设 2026/10/23 15:44:22

EtherNet/IP工业以太网协议实战:从CIP架构到PLC与SCADA联调配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 15:44:21

Ad hoc网络MAC协议修改的OPNET仿真实践指南

简介&#xff1a;这是一份基于OPNET仿真平台的无线自组织网络介质访问控制层协议修改与仿真源程序包&#xff0c;面向网络协议研究人员、通信专业学生及仿真工程师&#xff0c;适用于教学和科研场景&#xff0c;解决无中心节点网络中信道接入机制定制与性能评估的难题。资源以自…

作者头像 李华