简介:计算机网络实验指导实验二围绕停止等待协议传输数据文件这一主题,面向高校计算机网络课程的学生与指导教师,适用于串行口通信实验、协议原理复习及实验报告撰写场景。文档从实验目的与实验环境入手,系统阐述了发送方等待ACK/NAK、超时重传、数据包编号等核心机制,并结合数据包丢失和确认信息出错两种典型异常,说明定时器与0/1编号如何保证传输可靠且不重复;同时整理了BSC协议中SOH、STX、ETX、EOT、ENQ、ACK、NAK、DLE等控制字符的用途,以及数据报文格式、BCC校验、透明数据转义和简化停等协议的设计方法,内容完整、条理清晰。资源包共1个doc文档,大小81KB,体量紧凑,方便直接阅读或打印,适合实验前预习和实验中对照。目前已有87人学习下载,对初次接触停等协议或需要完成串行文件传输实验的读者具有较强参考价值。
1. 停止等待协议实验到底在练什么
“停止等待协议”是计算机网络实验指导里几乎必然出现的第二个实验:发送方每发完一个数据块就停下来,等接收方回一个确认帧(ACK),收不到就超时重传,收到才发下一块,循环往复把文件传完。这个实验的目的从来不是让你背状态机,而是让你亲手把一个不可靠的 UDP 链路变成一条能传文件的可靠信道,顺带把 seq、ACK、帧丢失、重复帧、校验和这些词从教材挪进代码。适合正在赶实验报告、准备期末复习或想把协议真正跑起来的人;把停等跑明白,后面看滑动窗口和 TCP 重传会少踩很多坑。
2. 停止等待协议的确认与重传:状态机要比代码先成立
写代码之前,我建议先把协议的状态转换画出来。停止等待协议叫“停止等待”,是因为发送方在同一时刻只允许一个帧在链路上“在途”——发完就停,收到确认才继续。这个约束决定了它只需要一个非常简单的状态机,也决定了它在局域网里跑得挺快、在长链路上效率特别低。下面把帧交换的四个基本场景拆开,这四种情况就是后面调试时真正会遇到的四种路径。
2.1 四种帧交换场景:正常、丢帧、丢 ACK、重复帧
停等协议的最小闭环是“发送方发一个帧 → 接收方校验 → 回 ACK → 发送方收 ACK 再发下一帧”。按这个闭环展开,所有情况都能归进四种路径:正常传输、数据帧丢失、确认帧丢失、重复帧到达。教材里(比如谢希仁那本《计算机网络》的数据链路层章节)通常只画前三种,重复帧其实是“ACK 丢失导致重传”的必然结果,单独列出来反而更容易理解接收端为什么必须去重。
| 场景 | 发送方行为 | 接收方行为 | 结果 |
|---|---|---|---|
| 正常传输 | 发帧、启动定时器 | 校验通过、交付数据、回 ACK | 发送方收到 ACK,翻转序号继续下一帧 |
| 数据帧丢失 | 定时器超时、重发同一帧 | 没收到,无行为 | 重发后接收方正常处理 |
| ACK 丢失 | 定时器超时、重发同一帧 | 收到重复帧、丢弃数据但再回 ACK | 发送方收到新 ACK 后恢复 |
| 重复帧到达 | 无特殊行为 | 序号与期望不符、丢弃数据、仍回 ACK | 文件不会重复写入 |
这张表里值得反复咀嚼的是最后一行:接收方收到重复帧时,数据一定不能落盘,但 ACK 必须照发。很多同学在这里想当然,认为“重复帧都接收到了,就不用再回 ACK”,结果发送方等不到确认只能不停重传,最后触发重传上限直接崩溃。网络协议里的确认机制不是“收到才确认”,而是“无论收到新帧还是旧帧,都要让对端知道我还活着”。
2.2 单比特序号:为什么 0 和 1 交替就够了
停等协议里序号只需要 0 和 1 两个值来回翻转,这是个反直觉但非常精巧的设计。因为同一时刻链路上最多只有一个数据帧,接收方需要回答的问题只有一个:这个帧是“我期待的新帧”还是“上一次已经处理过的旧帧”?二值序号足够区分这两种状态。
接收方维护一个期望序号变量expected_seq(初始 0)。收到帧时,如果帧序号等于expected_seq,说明是新帧,写入文件并把expected_seq翻转为 1;如果不等,说明是重传的旧帧,丢弃数据但仍回 ACK。发送方维护一个seq,每收到一个正确的 ACK 就翻转一次。两个变量在各自端点上独立维护,不需要共享状态,这是协议能工作的关键。
我在给实验报告写讲解时常用一个类比:这就像两个人轮流拿编号 0、1、0、1 的票进场,检票员只管“这次来的票号是不是我预期的那张”,而不是去数这个票号出现过多少次。用状态变量做预期,比用“统计收到多少个包”去做重可靠得多,因为统计会受重传影响,而状态变量遵循的是协议逻辑。
理论教材里这一步通常对应自顶向下那套教材的 rdt2.2 / rdt3.0 状态图,湖科大教书匠那类讲得细的网课也会在这里画两个角色的状态机。我的建议是:动手写代码前,先在纸上把发送方和接收方的状态迁移各画一遍,代码只是把这张图翻译成 if-else。
2.3 超时重传的定时器:RTT 是标尺而不是拍脑袋
超时重传是停等协议从“不可靠链路”变成“可靠链路”的那只手。发送方发送一个数据帧后立即启动定时器,如果在定时器到期前没有收到对应 ACK,就重发同样的帧。这里最容易翻车的地方是定时器的时长选择。
时长必须大于当前的往返时间(RTT),否则会出现“伪重传”:帧其实已经到达,ACK 也在回来的路上,但发送方等不及就重发了。RTT 包含数据帧的上行传输时间、接收方处理时间、ACK 的下行传输时间,还有网络排队时间,所以它不是一个固定值。常见做法是先用一次探测获得初始 RTT,然后用加权移动平均来平滑:RTTs = 0.875 × 旧RTTs + 0.125 × 新采样RTT,这是 TCP 里经典的平滑公式,套到停等协议实验里也完全够用。定时器一般取平滑 RTT 的 2~3 倍,留足余量。
一个刚做实验的同学通常会问:能不能直接把超时时间设成 0.5 秒,省事?能,但在本机回环(loopback)上 RTT 只有零点几毫秒,0.5 秒意味着每丢一帧都要白白等 0.5 秒,丢包率稍高整个实验就慢得让人抓狂;如果放到跨机器的真实链路里,RTT 变成 50 毫秒,0.5 秒又可能不够。所以不要把超时时间写死,最好做成一个可以在命令行传入的参数,至少也要放在文件开头用变量声明,方便实验报告里做多组对比。
到这一步,理论部分基本够用了。下面直接用 Python 把它落地。
3. Python + UDP 实现文件传输:发送端、接收端与三个必调参数
这一章给出一个在本地就能完整跑通的实现。我选 Python 是因为它在 socket、struct、zlib 这几个标准库上几乎没有环境成本,而且实验报告里贴代码好讲;生产系统里换成 C/Jav 也是一样的逻辑。整个方案包含三个必须理解的参数:数据块大小、超时时间、最大重传次数。这三个参数分别对应“链路效率”“链路延迟”和“链路质量”,是后面调参实验的主线。
3.1 选 UDP 而不是 TCP:不可靠信道必须自己扛
实验名字里写的是“利用停止等待协议传输数据文件”,但没规定传输层用什么协议。我强烈建议用 UDP 套接字,而不是 TCP。原因很简单:TCP 自己就实现了序号、确认、超时重传和流量控制,应用层再用停等协议,等于让一个已经会游泳的人套着游泳圈表演游泳——你只能看到“一问一答”的外壳,真正需要验证的重传逻辑全部发生在系统协议栈里,抓包根本看不到。
UDP 把“不可靠”赤裸裸地暴露给应用层:数据报可能丢失、可能乱序、可能重复。停等协议要做的恰恰是在这个不可靠信道之上,通过“发送→等待→确认→重传”把可靠性一层层搭起来。这正是实验目的。在实际开发里,你几乎不会在 UDP 之上重新发明停等协议,因为 SCTP、QUIC 这些协议已经帮你做好了;但作为教学实验,你必须亲手实现一次,才能理解 TCP 的确认号为什么那样设计。
3.2 帧格式设计:头部、校验和与 EOF 终止标记
为了在应用层模拟“帧”,我们需要自己定义帧格式。这里用二进制结构而不是文本行,因为文件数据里可能出现的任意字节不能和分隔符混淆。我用的帧头是 8 字节定长:
seq:1 字节,帧序号,取值 0 或 1,交替翻转frame_type:1 字节,1 表示数据帧,2 表示结束帧(EOF)length:2 字节,payload 字节数,无符号短整型,最大 65535crc:4 字节,对“帧头前 6 字节 + payload”整体计算出的 CRC32 校验值
帧头之后直接跟 payload。EOF 帧的 payload 为空,它是文件传输结束的信号,接收端收到 EOF 帧后回一个 ACK 并关闭文件。
CRC32 用zlib.crc32,它比手写的“逐字节求和”可靠很多。逐字节求和的经典问题我在第 5 章会详细讲。这里先记住:校验范围必须包含头部里的 seq、frame_type、length,不能只对 payload 校验,否则头部在传输中被破坏时,接收端可能把一个错误位置的帧当成合法数据。
3.3 发送端代码:切片、超时重传与序号翻转
发送端逻辑分三层:读文件切片、发帧并等 ACK、超时重传。下面是完整实现。
import socket import struct import zlib import sys import time CHUNK_SIZE = 1024 # 每个数据块的字节数,建议不超过 1400 SERVER_ADDR = ("127.0.0.1", 8888) TIMEOUT = 1.0 # 超时重传阈值,单位秒 MAX_RETRY = 20 # 单帧最大重传次数 def make_frame(seq, frame_type, payload): # 先按 crc=0 填充帧头,算出校验和后再填进去 head = struct.pack("!BBHI", seq, frame_type, len(payload), 0) crc = zlib.crc32(head + payload) head = struct.pack("!BBHI", seq, frame_type, len(payload), crc) return head + payload def send_one(sock, seq, frame_type, payload): frame = make_frame(seq, frame_type, payload) retry = 0 while True: sock.sendto(frame, SERVER_ADDR) try: ack, _ = sock.recvfrom(256) if len(ack) >= 1 and ack[0] == seq: return # 确认帧中的 seq 与当前帧一致,才算成功 except socket.timeout: retry += 1 print("[timeout] seq=%d retry=%d" % (seq, retry)) if retry >= MAX_RETRY: raise RuntimeError("帧 seq=%d 重传超过上限" % seq) def send_file(sock, filepath): seq = 0 with open(filepath, "rb") as f: while True: payload = f.read(CHUNK_SIZE) if payload: send_one(sock, seq, 1, payload) seq = 1 - seq # 翻转比特序号 if not payload: # 文件读完了,发 EOF 帧 send_one(sock, seq, 2, b"") break if __name__ == "__main__": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(TIMEOUT) send_file(sock, sys.argv[1]) sock.close() print("发送完成")这段代码里需要注意三个地方。第一,make_frame里struct.pack("!BBHI", ...)用的网络字节序,!表示大端,B是无符号单字节,H是无符号双字节,I是无符号四字节,这样接收端在任意平台上解析结果一致。第二,send_one里确认条件用的ack[0] == seq,这里简单取了 ACK 帧的第一个字节,即 ACK 回显的是“被确认帧的序号”。实际工程中 ACK 帧也要校验 CRC,教学实现里为了突出重点先不校。第三,EOF 帧和普通数据帧走同一个send_one,也会等待 ACK。如果 EOF 帧的 ACK 丢失,发送方会重传 EOF,这时接收端可能已经退出,代码会走到重传上限报错。这个问题第 5 章有专门的排查思路。
CHUNK_SIZE我默认设 1024 字节,理由有两个:一是 1024 加上 8 字节帧头后远小于以太网 MTU 1500 字节,UDP 数据报不会分片;二是 1024 这个值在算文件总帧数时方便心算,例如 10MB 文件正好 10240 帧,报告里好写。如果实验环境在 PPPoE 这类 MTU 只有 1492 的链路上,建议把 CHUNK_SIZE 降到 1024 以下,避免分片导致的二次丢包。
3.4 接收端代码:校验、去重落盘与 ACK 回复
接收端逻辑比发送端多一个关键步骤:判断重复帧。无论当前帧是新帧还是重复帧,都要回 ACK,但只有新帧才写盘。
import socket import struct import zlib RECV_PORT = 8888 OUTPUT_FILE = "recv_file.bin" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("127.0.0.1", RECV_PORT)) expected_seq = 0 out = open(OUTPUT_FILE, "wb") while True: packet, addr = sock.recvfrom(65535) # UDP 数据报边界即帧边界 if len(packet) < 8: # 小于帧头长度直接丢弃 continue seq, frame_type, length, crc = struct.unpack("!BBHI", packet[:8]) payload = packet[8:8 + length] head = struct.pack("!BBHI", seq, frame_type, length, 0) if zlib.crc32(head + payload) != crc: # 校验失败,静默丢弃 print("[crc error] seq=%d len=%d" % (seq, length)) continue if frame_type in (1, 2): if seq == expected_seq: out.write(payload) # 只有新帧才写盘 out.flush() expected_seq = 1 - expected_seq else: print("[duplicate] seq=%d expected=%d" % (seq, expected_seq)) ack = struct.pack("!BBHI", seq, 3, 0, 0) ack_crc = zlib.crc32(ack) ack = struct.pack("!BBHI", seq, 3, 0, ack_crc) sock.sendto(ack, addr) # 重复帧同样要回 ACK if frame_type == 2: # EOF 帧,结束接收 break out.close() sock.close() print("接收完成: %s" % OUTPUT_FILE)接收端的核心是expected_seq这个变量。它只在“收到的 seq 等于期望值”时翻转,因此能精确识别重复帧。注意 EOF 帧不会被写入文件,因为frame_type == 2且 payload 为空,out.write写进去 0 字节,不影响结果,但它的expected_seq翻转会导致发送端如果重传 EOF,接收端判定为重复帧后仍回 ACK,协议状态一致。
跑通这个版本后,你可以直接用它传一个文本文件试试,再用diff或md5sum验证收发的文件一致。这是整个实验的基线版本。接下来要做的,是给它加上丢包和时延,让“可靠性”从理论变成看得见的数据。
4. 丢包模拟与效率测算:让实验数据能上答辩
很多人做完基线版本就停了,因为本机 loopback 上几乎不丢包,程序运行得又快又顺,报告里只能写“实验结果正确”。这样的实验报告经不起问。真正有价值的实验要在受控的丢包和时延条件下跑,并把重传次数、耗时、吞吐率这些数据量化出来。这一章讲怎么给你的代码注入“故障”,以及怎么把这些故障下的数据变成报告里的论据。
4.1 代码内置丢包率:比 tc netem 更可控的教学环境
在 Linux 上可以用tc qdisc加 netem 模拟链路丢包,例如tc qdisc add dev lo root netem loss 5%。但这个做法在教学环境里有三座大山:需要 root 权限,Windows 上根本没有这条命令,而且它作用在整个 loopback 上会影响你同时跑着的其他网络服务。我一般建议直接在代码里做丢包注入,好处是参数完全可控、可复现,实验报告可以精确写出“丢包率 5% 时重传了 137 次”,而不是“网络好像有点卡”。
最简洁的做法是在接收端收到数据帧后,按概率直接丢弃,不处理也不回 ACK。发送端自然超时重传。
import random LOSS_RATE = 0.05 # 丢包率 5% # 在 recvfrom 之后、处理 frame 之前插入 if random.random() < LOSS_RATE: print("[simulate loss] seq=%d" % seq) continue注意这段逻辑要放在 CRC 校验之前还是之后?我的建议是在校验之后、处理之前。因为我们要模拟的是“链路把帧丢了”,而不是“接收端校验失败丢弃”,区分这两者对统计重传原因有帮助。如果你想模拟 ACK 丢失,可以在发送端的recvfrom成功返回后,随机丢掉这个 ACK:
# 发送端收到 ACK 后,以同样概率假装没收到 if random.random() < LOSS_RATE: continue不过课堂实验通常只模拟一种丢包就够,两边都丢会让重传次数变得难以解释。我默认只在接收端丢数据帧,这样每次重传都能归因到“数据帧丢失”,报告好写。
4.2 信道利用率公式与会考的几个数
停等协议的效率瓶颈是它最大的特点,也是期末复习里反复出现的考点。在不考虑重传的理想情况下,发送方每发一帧要经历:数据帧发送时间 T_data、等待 ACK 到达的 RTT、ACK 帧自身的发送时间 T_ack。信道利用率 U 近似为:
U = T_data / (T_data + RTT + T_ack)
当链路带宽很高、数据块很小时,T_data 通常只有几十微秒,而 RTT 在跨地域链路上动辄几十毫秒,分母被 RTT 主导,U 会低到惨不忍睹。举个例子:100Mbps 链路上传一个 1024 字节的块,T_data ≈ 0.082ms;如果 RTT 是 100ms,U ≈ 0.082 / 100.164 ≈ 0.08%,也就是说链路 99.92% 的时间在空等。传 10MB 文件,理论耗时约 1024 秒,接近 17 分钟。这个数字在答辩现场一算,比任何文字都直观。
如果考虑丢包率 p,一个帧平均要发送 1/(1-p) 次才能成功,实际吞吐率近似为:
实际传输速率 ≈ U × 带宽 × (1-p)
这就是实验里为什么丢包率从 0 升到 5%,整个文件传输时间会翻倍甚至更大的原因——重传不光是多发了几个包,每个重传都要白白等一个超时周期,而超时时间通常比 RTT 大好几倍。
4.3 四个实验组:把参数组合设计成答辩证据
我建议你在实验报告里跑四组对比,每组都记录:完成时间、发送总帧数、重传次数、文件校验结果。这个组合能同时证明三点:协议在无故障下正常工作、协议在丢包下可靠恢复、超时参数对性能有决定性影响。
| 实验组 | 丢包率 | 模拟 RTT | TIMEOUT | 预期观察 |
|---|---|---|---|---|
| 组 1 | 0% | 0ms | 1.0s | 无重传,完成时间最短 |
| 组 2 | 5% | 0ms | 1.0s | 出现重传,文件仍正确 |
| 组 3 | 0% | 100ms | 1.0s | 无重传但完成时间变长,效率低 |
| 组 4 | 0% | 100ms | 0.05s | 伪重传刷屏,完成时间反而更长 |
模拟 RTT 不用真的跨机器,最简单的方法是在接收端回 ACK 前加一个time.sleep(RTT_MS / 1000.0 / 2),这等效于把链路往返时延拉长到目标值。代码改动只有一行,却能让“RTT 对停等协议效率的影响”变成可控变量,这是我在实验里常用的手法。
组 4 是整份报告里的“亮点实验”:当 TIMEOUT 小于实际 RTT 时,每一帧都会先超时重传再收到 ACK,重传次数等于总帧数,程序最终也能跑完但时间惨不忍睹。这个实验直观解释了定时器下限为什么要大于 RTT,比背一百遍公式都管用。
到这里,一个能复现、有数据、可答辩的停等文件传输实验已经成型。接下来看几个高频坑,大部分是学生在实现时反复踩过的问题。
5. 停止等待协议常见问题:现象、原因与排查方法
这一章是实验现场最常见的五个问题,每条都按“现象 → 原因 → 解决”的路径记录。你如果自己的实现出了类似问题,可以直接对标排查。
5.1 文件变大或变小:重复帧和 EOF 边界同时出了问题
现象:传输完成后,接收端文件大小和源文件不一致,多数情况是变大,偶尔变小;用diff比较,文件末尾或中间多出一段重复内容。
原因:最常见的是接收端没有区分新帧和重复帧,把所有收到的 payload 都写进了文件;另一个常见原因是 EOF 帧发送在逻辑上放在了“读完最后一块”之后,但最后一块的判定用了len(payload) < CHUNK_SIZE,当文件大小恰好是 CHUNK_SIZE 的整数倍时,最后一次读取返回完整块,EOF 帧永远发不出去,接收端等不到结束信号,文件缺少末尾数据。
解决:接收端必须用expected_seq变量判断“新帧才写盘”,重复帧只回 ACK 不写盘。发送端不要用数据块大小判断文件是否读完,而是用if not payload判断文件流是否耗尽,耗尽后再单独发 EOF 帧。改完后用 MD5 对比验证。
5.2 程序卡死:超时时间比 RTT 还短
现象:发送端 log 里全是[timeout]重传信息,同一帧连续重传十几次;接收端却显示已经收到这一帧并且回了 ACK;最后程序崩溃或陷入很长时间的等待。
原因:TIMEOUT 设置得太小,小于实际 RTT。数据帧到达接收端,ACK 也在返回途中,发送端就率先超时重传。重传的帧到达接收端后,接收端仍正常工作回 ACK,但发送端每次超时都会重发,形成“伪重传风暴”。如果 TIMEOUT 比 RTT 小很多,每一帧都会被重传,传输时间成倍拉长。
解决:把 TIMEOUT 设置为平滑 RTT 的 2~3 倍以上。最简单的办法是先跑一次无丢包基线实验,打印出每帧的确认耗时,取平均值再乘以 3。组 4 的对比数据也来自这个坑。
5.3 校验和失效:只算 payload 漏了头部
现象:文件能传输完,大小也对,但内容里偶尔有几个字节和源文件不同;打开 CRC 日志发现crc error很少甚至没有。
原因:很多人写的校验只对 payload 做求和,比如sum(payload) % 256,这种校验对“字节位置交换”和“一个字节变成另一个同 mod 值字节”完全无感;更严重的问题是没有把 seq、frame_type、length 这三块头部纳入校验,头部在链路中被篡改后,接收端可能把帧解析到错误位置,数据全错但校验依然通过。
解决:统一用zlib.crc32,并把整个帧头(crc 字段置 0)+ payload 一起计算校验值。接收端也必须按同样顺序重新计算再比对,任何字节不一致都会导致 CRC 不匹配。
5.4 用 TCP 套壳做停等:抓包根本看不到重传
现象:代码逻辑完全正确,程序也能跑,但用 Wireshark 抓包发现网络里根本没有重传,把丢包率调高也没有;实验报告里写“实现了超时重传”,答辩时却拿不出证据。
原因:用的是 TCP 套接字。TCP 协议栈自己在做确认和重传,应用层看起来是在一问一答,但链路上一旦丢包,是系统内核在重传,不会触发你写的应用层 TIMOUT 逻辑。你在应用层实现的“停止等待”只是个等待循环,真正的停止等待协议并没有参与可靠性保障。
解决:换用SOCK_DGRAM即 UDP,把“确认—超时—重传”的可靠性逻辑真正放到应用层代码里。如果课程要求必须基于 TCP,至少要在应用层自定义确认和重传机制,并禁用 TCP_NODELAY 之外的自动重传,但这种做法绕开了实验本意,不推荐。
5.5 文件恰好是 CHUNK_SIZE 整数倍:收尾永远等不来
现象:传一个大小正好是 1024KB 的文件时,发送端把文件读完了,程序却不退出;接收端也一直不结束,两边干等。
原因:发送端判断文件结束用的是“最后一块长度小于 CHUNK_SIZE”,但 1024KB 的文件最后一块正好是满块,不满足结束条件。发送端会继续读一次文件流,读到空字符串 b"",如果此时没有专门发 EOF 帧的逻辑,程序就会卡在读空串后的等待状态;接收端自然等不到结束信号。
解决:不要用数据块长度判断文件是否结束,而是用read()的返回值是否为空判断。读完空串后,单独发送一个frame_type=2的 EOF 帧,接收端以类型而不是长度来识别结束。代码在第 3 章已经是这个写法,如果你自己的实现卡在这,重点检查没读空串时是否跳过了 EOF 逻辑。
6. 验证实验效果:重传统计、窗口对比与抓包留证
实验跑通只是起点,能向别人证明“我真的理解了停等协议”才是终点。我的建议是给收发两端加统计埋点,再抓一次包,最后做一个对照组。
6.1 在收发两端埋点:统计重传次数与耗时
在发送端加三个变量:总发送帧数、重传次数、开始时间。超时重传时retry_count += 1,成功传输一个块后sent_count += 1,结束前打印耗时和吞吐率。有了这些数字,第 4 章的四组实验表格就有了数据来源。接收端可以在收到重复帧和 CRC 错误时打印日志,方便对照发送端重传日志验证“重传的帧到底有没有被接收端正确识别”。
6.2 做一组最小 GBN 对比:证明窗口的价值
停等协议效率低的根因是窗口为 1。为了证明这一点,可以实现一个最简化的回退 N 帧(GBN)协议:把发送端的“每发一帧等一个 ACK”改成“连续发 4 帧,再统一收 ACK”。代码改动很小,但吞吐率差异会非常大。这个对照组能让实验报告从“我复现了教材”升级为“我验证了教材的结论”。
6.3 用 Wireshark 抓包留证据
Wireshark 过滤条件用udp.port == 8888,就能看到全部数据帧、ACK 帧和重传帧。观察 Time 列的间隔:正常传输时帧间隔稳定;超时重传时会出现一个明显的跳变,间隔约等于你的 TIMEOUT 值。把抓包截图放实验报告里,比写一百句“实现了重传机制”都有说服力。
我做这个实验时也翻过车。第一次图省事用 TCP 套了个一问一答,报告里理直气壮写“实现了停等协议”,答辩时老师问我重传在哪,我说不清楚,全场沉默。后来换成 UDP 拆帧、加 CRC、把超时时间调到比 RTT 还短,看着 Wireshark 里整齐的重传记录,才真正明白确认和定时器不是两个孤立参数,而是一套配套的可靠性机制。这个坑如果你也踩过,那换 UDP 重跑一遍,体会会完全不同。希望帮到你。
本文还有配套的精品资源,点击获取