news 2026/10/1 10:33:11

RDT 3.0教学源码:Java实现停等ARQ协议与状态机调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RDT 3.0教学源码:Java实现停等ARQ协议与状态机调试

简介:本资源是面向计算机网络课程学习者与教学实践者的RDT 3.0协议仿真实验包,聚焦可靠数据传输核心机制的教学理解与代码实现。资源完整呈现停等ARQ协议的关键逻辑:含序号管理、CRC校验、超时重传与确认应答等模块,适用于高校网络原理实验、协议编程实训及TCP底层机制深度剖析场景。压缩包共16个文件,主体为5个Java类文件(含发送端、接收端及协议核心逻辑)、4个Java源码(.java)用于编译运行,辅以2个说明/日志文本(txt)、Eclipse项目配置文件(.project、.classpath、.prefs)、TCP协议配置模板(ENCDA.tcp)及INI初始化配置,总大小1.04MB,结构清晰、开箱即用。目前已有442人学习下载,读者可直接导入IDE运行调试,结合recvData.txt等日志观察数据流与错误恢复过程,掌握RDT 3.0在丢包、校验失败等异常下的行为响应,夯实传输层协议设计与实现能力。

1. RDT 3.0 不是玩具协议:它用停等ARQ把「丢包、错包、乱序」全兜住,但真实跑起来会卡死、重传爆炸、校验失效——这份 TCP-RDT3.0.zip 是教学级可调试源码,不是 demo 演示包,适合网络协议课设、毕设底层协议复现、Wireshark 抓包对照验证者,尤其适合被「为什么 ACK 没回来」「重发了三次还卡住」逼到崩溃的本科生和刚转岗的嵌入式/协议栈工程师

你写完 RDT 2.0,以为加个序号+校验就稳了?RDT 3.0 的真实门槛根本不在逻辑图上——它强制你直面「超时判定不准」带来的雪崩重传、「ACK 丢失后发送方疯发」导致的接收方状态错乱、「校验位被篡改却未触发重传」这类玄学问题。这个TCP-RDT3.0.zip不是 PPT 里的流程图,而是 Eclipse 工程结构完整的 Java 实现:含.project和.classpath,src/com/下分层封装了Sender、Receiver、UDPSocket、Packet四大核心类,bin/里有编译好的 class 文件,recvData.txt是接收端落地数据,Log.txt记录每帧收发时间戳与校验结果,Config.ini控制超时阈值、丢包率、错误注入开关。它不依赖任何第三方网络库,纯 JDK 1.8 UDP socket + 手动序列化 + CRC-8 校验,所有协议状态机(WAIT_FOR_CALL_0、WAIT_FOR_ACK_0、WAIT_FOR_CALL_1、WAIT_FOR_ACK_1)全部显式编码,连org.eclipse.jdt.core.prefs都保留着原始编码设置。这不是让你“跑通就行”的玩具,而是能进 debugger 单步跟踪 ACK 超时分支、修改Config.ini注入 15% 丢包、对比Log.txt与 Wireshark 抓包时间线的硬核实验基座。

1.1 它解决的不是「理论对不对」,而是「为什么我写的 RDT 总在第 7 帧卡死」

很多同学实现 RDT 3.0 后发现:前 6 帧正常,第 7 帧突然停住,Log.txt显示Sent PKT#7, timeout=500ms,但recvData.txt空空如也。这不是代码漏写了,而是 RDT 3.0 的停等本质决定了它对「超时精度」极度敏感——当网络延迟抖动超过设定 timeout,就会误判丢包并触发重传;而重传又可能撞上原包迟到,造成接收方重复 ACK 或状态错乱。这个压缩包的价值在于:它的Config.ini允许你把timeout_ms=500改成1200,再配合Log.txt中精确到毫秒的时间戳(格式:[2024-03-15 14:22:03.872] SENT PKT#7 SEQ=7 CHK=0x3A),你能肉眼比对「发送时刻 vs ACK 到达时刻 vs timeout 触发时刻」,从而定位到底是网络抖动、JVM GC 暂停,还是你自己的 CRC 计算逻辑偏差。它不教你怎么背 ARQ 定义,它逼你亲手调参、看日志、改代码、验证结论。

1.2 为什么必须用 Java + Eclipse 工程结构?因为你要 debug 状态机跳转

RDT 3.0 的灵魂是四个状态之间的严格转换:WAIT_FOR_CALL_0 → WAIT_FOR_ACK_0 → WAIT_FOR_CALL_1 → WAIT_FOR_ACK_1 → WAIT_FOR_CALL_0。C 语言实现容易把状态藏在 if-else 里,Java 这份代码则用enum RDTState { WAIT_FOR_CALL_0, WAIT_FOR_ACK_0, ... }显式声明,并在Sender.java的send()方法中用switch(state)分支控制行为。这意味着你可以在 Eclipse 里对state == WAIT_FOR_ACK_0打断点,观察「发送后是否真的进入等待 ACK 状态」「收到 ACK 后是否正确切回 WAIT_FOR_CALL_1」。.project文件里还定义了org.eclipse.jdt.core.javabuilder构建器,确保你改完Packet.java的 CRC 计算逻辑后,bin/com/下 class 文件自动更新——这种「改一行代码、debug 一次状态跳转」的闭环,是 Python 脚本或 CMake 工程难以提供的教学穿透力。

1.3 它不是 TCP 替代品,而是 TCP 的「解剖标本」

别被名字误导:TCP-RDT3.0.zip里的TCP_RDT3.0是项目名,不是实现了 TCP。它用 UDP socket 模拟不可靠信道,自己实现序号、校验、超时、重传——这恰恰是理解真实 TCP 的起点。当你用 Wireshark 抓到tcpdump -i lo port 9999(该工程默认监听 UDP 9999 端口),看到的是 raw UDP 包 payload 里明文的SEQ=7 DATA=HelloWorld CHK=0x3A,而不是 TCP header 里的 sequence number。这种「剥离 TCP 协议栈,只留可靠传输内核」的设计,让你能专注验证:CRC-8 算法是否真能检出单比特翻转?Config.ini里corrupt_rate=0.1是否真让 10% 的包校验失败?Log.txt中RECV PKT#7 CORRUPTED!是否触发了预期的无 ACK 行为?这才是协议学习从「纸上谈兵」到「肌肉记忆」的关键跃迁。

2. 从解压到可调试:Eclipse 工程导入、关键类职责拆解与 Config.ini 参数实战指南

2.1 解压即用:四步完成 Eclipse 工程导入(跳过 Maven、Gradle 等干扰项)

这个工程是纯 Java SE 项目,不依赖任何构建工具。解压后直接导入 Eclipse,无需额外配置:

# 1. 解压到工作目录(例如 ~/network-lab/) unzip TCP-RDT3.0.zip -d ~/network-lab/ cd ~/network-lab/TCP_RDT3.0 # 2. 确认关键文件存在(这是工程合法性的物理证据) ls -F .project .classpath src/com/ bin/ Config.ini Log.txt recvData.txt # 3. Eclipse 导入路径:File → Import → General → Existing Projects into Workspace # → Select root directory: ~/network-lab/TCP_RDT3.0 # → 勾选 "TCP_RDT3.0" 项目 → Finish # 4. 编译检查:Project → Properties → Java Build Path → Libraries 标签页 # 应显示 "JRE System Library [JavaSE-1.8]",无红色叉号

提示:如果 Eclipse 提示The project cannot be built until build path errors are resolved,请右键项目 → Properties → Java Build Path → Libraries → RemoveJRE System Library→ Add Library → JRE System Library → Execution environment:JavaSE-1.8。这是因 JDK 版本不匹配导致的常见问题,不是代码缺陷。

2.2 四大核心类职责与调用链:Sender 是大脑,Receiver 是哨兵,Packet 是信使,UDPSocket 是管道

整个协议运行依赖四个 Java 类的协同,它们的职责边界必须清晰:

类名路径核心职责关键方法调试切入点
Sendersrc/com/Sender.java协议发起者:管理发送状态机、维护当前序号、启动超时定时器、处理 ACKsend(),handleAck(),timeoutHandler()在send()开头打断点,观察nextSeqNum如何递增;在timeoutHandler()里看重传逻辑
Receiversrc/com/Receiver.java协议守门人:校验包完整性、按序缓存数据、生成 ACK、处理重复包receive(),checkChecksum(),sendAck()在checkChecksum()内部打断点,手动修改packet.data观察校验失败路径
Packetsrc/com/Packet.java数据载体:序列化 SEQ/DATA/CHK 字段,提供 CRC-8 计算与验证serialize(),deserialize(),computeChecksum()修改computeChecksum()算法(如改成 CRC-16),验证Log.txt中 CHK 值变化
UDPSocketsrc/com/UDPSocket.java通信管道:封装 DatagramSocket,提供 sendTo()/receiveFrom() 接口sendTo(),receiveFrom(),close()在receiveFrom()返回前打断点,模拟网络丢包(注释掉socket.receive())

调用链非常线性:Sender.send()→UDPSocket.sendTo()→ 网络 →UDPSocket.receiveFrom()→Receiver.receive()→Receiver.sendAck()→UDPSocket.sendTo()→Sender.handleAck()。这种扁平结构让你能用 Eclipse 的 Debug → Step Into 逐层下钻,而不是在回调地狱里迷失。

2.3 Config.ini 参数详解:超时、丢包、错误注入——三把钥匙打开协议鲁棒性测试

Config.ini是控制实验行为的总开关,共 6 个参数,每个都直接影响协议表现。不要跳过这一步直接 run:

# Config.ini # ======== 协议基础参数 ======== port=9999 timeout_ms=500 max_retransmit=3 # ======== 错误注入参数(仅用于测试) ======== drop_rate=0.0 corrupt_rate=0.0 delay_ms=0 # ======== 日志与输出 ======== log_file=Log.txt recv_file=recvData.txt
  • timeout_ms=500:最关键参数。它定义 Sender 等待 ACK 的毫秒数。设得太小(如 100ms)会导致频繁误重传;设得太大(如 2000ms)会让吞吐量暴跌。真实网络中,RTT(往返时延)应是 timeout 的 2~4 倍,此处timeout_ms应 ≥ 2× 实际 RTT。
  • drop_rate=0.0:丢包率,0.0 表示 0%,0.1 表示 10%。启用后,UDPSocket.sendTo()内部会以该概率丢弃包。注意:丢包发生在发送端,所以Log.txt仍会记录SENT PKT#X,但recvData.txt不会出现对应数据。
  • corrupt_rate=0.0:数据篡改率。启用后,Packet.serialize()输出的 byte[] 中,会随机翻转一个 bit,导致Receiver.checkChecksum()失败。此时Log.txt会记录RECV PKT#X CORRUPTED!,且 Receiver 不发 ACK。
  • delay_ms=0:人为增加网络延迟。设为 300,则每个包在UDPSocket.sendTo()后 sleep 300ms 再真正发送。这会直接拉长 RTT,考验timeout_ms设置是否合理。

注意:drop_rate和corrupt_rate是互斥的——一个包不会既被丢弃又被篡改。delay_ms对所有包生效,包括 ACK。

2.4 第一次运行:验证环境、观察 Log.txt、确认 recvData.txt 写入成功

导入工程后,右键Sender.java→ Run As → Java Application。几秒后,控制台应输出:

[INFO] Sender started on port 9999 [INFO] Sending packet #0... [INFO] Sending packet #1... ... [INFO] All packets sent. Waiting for final ACK... [INFO] Sender terminated.

同时检查三个文件:

# 查看 Log.txt —— 应有至少 10 行,包含 SENT/RECV/ACK/CORRUPTED 记录 tail -n 10 Log.txt # 示例输出: # [2024-03-15 14:22:03.872] SENT PKT#0 SEQ=0 CHK=0x1F # [2024-03-15 14:22:03.875] RECV PKT#0 SEQ=0 CHK=0x1F # [2024-03-15 14:22:03.876] SEND ACK#0 # 查看 recvData.txt —— 应为明文,内容与发送数据一致 cat recvData.txt # 示例输出: # Hello World! This is RDT 3.0 test data. # 查看 bin/ 目录 —— 确认 class 文件已编译 ls -l bin/com/*.class | head -5

如果recvData.txt为空或Log.txt只有SENT没有RECV,说明 Receiver 未启动或端口冲突。此时需手动启动 Receiver:右键Receiver.java→ Run As → Java Application(先启 Receiver,再启 Sender)。

3. 协议状态机深度解析:WAIT_FOR_CALL_0 到 WAIT_FOR_ACK_1 的四态跳转与超时陷阱

3.1 RDT 3.0 状态机本质:两个序号(0/1)+ 两个等待(CALL/ACK)= 四个原子状态

RDT 3.0 的核心创新是引入「交替位协议(Alternating Bit Protocol)」,用 1 bit 序号(0 或 1)解决停等协议的效率瓶颈。其状态机不是抽象概念,而是Sender.java中RDTStateenum 的四个实例:

// src/com/Sender.java public enum RDTState { WAIT_FOR_CALL_0, // 等待上层应用调用 send() 发送 SEQ=0 的包 WAIT_FOR_ACK_0, // 已发 SEQ=0,等待 ACK#0 WAIT_FOR_CALL_1, // 等待上层应用调用 send() 发送 SEQ=1 的包 WAIT_FOR_ACK_1 // 已发 SEQ=1,等待 ACK#1 }

状态跳转由三个事件驱动:

  • 上层调用send():触发WAIT_FOR_CALL_X → WAIT_FOR_ACK_X
  • 收到 ACK#X:触发WAIT_FOR_ACK_X → WAIT_FOR_CALL_(1-X)
  • 超时发生:触发WAIT_FOR_ACK_X → WAIT_FOR_ACK_X(重传,状态不变)

关键洞察:WAIT_FOR_CALL_X状态下,nextSeqNum必须等于 X;WAIT_FOR_ACK_X状态下,nextSeqNum仍为 X,直到收到 ACK#X 才翻转。这个细节决定了重传时序号绝不会错。

3.2 从代码看状态跳转:Sender.send() 中的 switch-case 是状态机执行引擎

Sender.send()方法是状态机的中枢,其逻辑完全由switch(state)控制:

// src/com/Sender.java public void send(byte[] data) throws IOException { switch (state) { case WAIT_FOR_CALL_0: // 构造 SEQ=0 的包,启动超时定时器,发送 Packet pkt = new Packet(0, data); startTimer(); udpSocket.sendTo(pkt.serialize(), receiverAddress); state = RDTState.WAIT_FOR_ACK_0; // 状态跳转! break; case WAIT_FOR_CALL_1: // 构造 SEQ=1 的包,启动超时定时器,发送 pkt = new Packet(1, data); startTimer(); udpSocket.sendTo(pkt.serialize(), receiverAddress); state = RDTState.WAIT_FOR_ACK_1; // 状态跳转! break; default: // WAIT_FOR_ACK_0 or WAIT_FOR_ACK_1:不允许上层再次 send() throw new IllegalStateException("Cannot send when waiting for ACK"); } }

血泪经验:初学者常在此处犯错——忘记state = RDTState.WAIT_FOR_ACK_X这行赋值,导致状态永远卡在WAIT_FOR_CALL_X,send()被反复调用却无实际发送。Eclipse 调试时,在state = RDTState.WAIT_FOR_ACK_0;这行打条件断点state == WAIT_FOR_CALL_0,即可验证跳转是否发生。

3.3 超时定时器实现:Timer + TimerTask 的精确控制与内存泄漏风险

超时机制不是Thread.sleep(),而是java.util.Timer的异步回调:

// src/com/Sender.java private Timer timer; private TimerTask timeoutTask; private void startTimer() { if (timer != null) timer.cancel(); // 防止重复启动 timer = new Timer(true); // daemon thread timeoutTask = new TimerTask() { @Override public void run() { timeoutHandler(); // 执行重传逻辑 } }; timer.schedule(timeoutTask, timeout_ms); // 在 timeout_ms 毫秒后触发 } private void timeoutHandler() { // 重传当前序号的包(注意:nextSeqNum 未变!) Packet retransmitPkt = new Packet(nextSeqNum, lastData); udpSocket.sendTo(retransmitPkt.serialize(), receiverAddress); // 重置定时器(为下次超时准备) startTimer(); }

避坑点:timer.cancel()必须在startTimer()开头调用,否则多次send()会创建多个 Timer,导致内存泄漏。Timer(true)创建守护线程,避免 JVM 无法退出。timeoutHandler()中startTimer()是关键——它让重传后继续等待 ACK,形成「重传→再等→再重传」循环,直到max_retransmit达到上限。

3.4 ACK 处理逻辑:handleAck() 如何安全地翻转状态与序号

handleAck()方法负责解析 ACK 并推进状态:

// src/com/Sender.java public void handleAck(int ackNum) { if (ackNum == nextSeqNum) { // ACK#X 匹配当前期望的 SEQ=X // 状态翻转:WAIT_FOR_ACK_X → WAIT_FOR_CALL_(1-X) if (state == RDTState.WAIT_FOR_ACK_0) { state = RDTState.WAIT_FOR_CALL_1; nextSeqNum = 1; } else if (state == RDTState.WAIT_FOR_ACK_1) { state = RDTState.WAIT_FOR_CALL_0; nextSeqNum = 0; } // 取消定时器(ACK 到达,无需重传) if (timer != null) { timer.cancel(); timer = null; } System.out.println("[INFO] Received ACK#" + ackNum); } else { // ACK#Y 不匹配当前期望(Y != X),可能是旧 ACK 迟到 System.out.println("[WARN] Unexpected ACK#" + ackNum + ", ignoring"); } }

玄学问题根源:当网络严重抖动,ACK#0迟到到达时,Sender 可能已重传PKT#0并进入WAIT_FOR_ACK_1状态。此时handleAck(0)会因ackNum != nextSeqNum(此时 nextSeqNum=1)而忽略该 ACK,这是协议设计的容错机制,不是 bug。Log.txt中若出现[WARN] Unexpected ACK#0, ignoring,说明网络存在乱序,RDT 3.0 正在按设计工作。

4. 避坑 / 常见问题 / 排查:五条真实踩坑记录,每条附现象、原因、解决

4.1 现象:Log.txt显示SENT PKT#0但recvData.txt为空,Receiver 控制台无输出

原因:Receiver 未启动,或 Sender 与 Receiver 使用不同端口(Config.ini中port值不一致),或防火墙拦截 UDP 9999 端口。
解决:

  1. 确保先运行Receiver.java,再运行Sender.java;
  2. 检查双方Config.ini的port值是否均为9999;
  3. Linux/macOS 执行sudo lsof -i :9999查看端口占用;Windows 执行netstat -ano | findstr :9999;
  4. 临时关闭防火墙测试:sudo ufw disable(Ubuntu)或 Windows Defender 防火墙设置中允许 UDP 9999。

4.2 现象:Log.txt中RECV PKT#X CORRUPTED!频繁出现,但corrupt_rate=0.0

原因:Packet.computeChecksum()计算逻辑与Receiver.checkChecksum()验证逻辑不一致,或serialize()/deserialize()字节序处理错误,导致校验值天然不匹配。
解决:

  1. 在Packet.java的computeChecksum()和Receiver.java的checkChecksum()中分别添加System.out.println("CHK calc: " + chk);;
  2. 对比两处输出值——若不等,检查computeChecksum()是否对data字节数组做了非预期修改(如Arrays.copyOf()未复制完整);
  3. 确认serialize()将seq、data、chk按固定顺序写入 byte[],deserialize()按相同顺序读取。

4.3 现象:Sender 发送PKT#0后卡死,Log.txt无后续SENT PKT#1,timeout_ms设为 500 但未触发重传

原因:startTimer()中timer.schedule(timeoutTask, timeout_ms)的timeout_ms为 0 或负数,导致Timer立即触发timeoutHandler(),但此时lastData为空,重传失败。
解决:

  1. 在startTimer()开头添加校验:if (timeout_ms <= 0) throw new IllegalArgumentException("timeout_ms must be > 0");;
  2. 检查Config.ini中timeout_ms是否被误写为0或-500;
  3. 在timeoutHandler()开头添加if (lastData == null) { System.err.println("[ERROR] lastData is null, cannot retransmit"); return; }。

4.4 现象:启用drop_rate=0.1后,Log.txt显示SENT PKT#0,但RECV PKT#0消失,且 Sender 在WAIT_FOR_ACK_0状态无限等待

原因:drop_rate仅作用于UDPSocket.sendTo(),但 Sender 的超时定时器未被正确重置——重传后startTimer()未被调用,导致第一次超时后无后续重传。
解决:

  1. 检查timeoutHandler()方法末尾是否调用了startTimer();
  2. 确认startTimer()内部timer.cancel()后重新创建了Timer实例;
  3. 在timeoutHandler()中添加日志:System.out.println("[INFO] Retransmitting PKT#" + nextSeqNum);,验证重传是否执行。

4.5 现象:recvData.txt中数据乱序,如PKT#1内容出现在PKT#0之前

原因:Receiver 未按序缓存数据。RDT 3.0 要求 Receiver 只接收SEQ=expectedSeqNum的包,其他包丢弃。但代码中Receiver.receive()可能直接将PKT#1写入recvData.txt,未检查pkt.seq == expectedSeqNum。
解决:

  1. 在Receiver.receive()中添加序号校验:
if (pkt.seq != expectedSeqNum) { System.out.println("[WARN] Out-of-order PKT#" + pkt.seq + ", expected " + expectedSeqNum + ", dropping"); return; // 丢弃乱序包 }
  1. expectedSeqNum初始为 0,每次成功接收并写入后翻转:expectedSeqNum = 1 - expectedSeqNum;
  2. recvData.txt应只在pkt.seq == expectedSeqNum时追加pkt.data。

5. CRC-8 校验实战:手撕算法、注入单比特错误、对比 Log.txt 与 Wireshark 抓包验证

5.1 CRC-8 算法实现:多项式 0x07 的字节查表法与在线计算验证

Packet.java中的computeChecksum()使用 CRC-8-ITU 标准(多项式 x⁸ + x² + x + 1,即 0x07),采用查表法提升性能:

// src/com/Packet.java private static final byte[] CRC8_TABLE = { 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, // ... (共 256 项,此处省略) }; public static byte computeChecksum(byte[] data) { byte crc = 0x00; for (byte b : data) { crc = CRC8_TABLE[crc ^ (b & 0xFF)]; } return crc; }

验证方法:

  1. 取data = "Hello".getBytes(),手动计算 CRC-8:
    • 在线工具输入48 65 6C 6C 6F(Hello 的 hex),选择 CRC-8/ITU,得到0x3F;
    • 运行代码System.out.println(Integer.toHexString(Packet.computeChecksum("Hello".getBytes())));,输出应为3f。
  2. 若不一致,检查CRC8_TABLE是否完整(必须 256 项),或b & 0xFF是否缺失(Java byte 为 signed,需转为 unsigned int)。

5.2 注入单比特错误:用十六进制编辑器篡改 recvData.txt,触发校验失败链路

要验证 CRC-8 是否真能检出单比特错误,不能只靠corrupt_rate:

  1. 正常运行一次,获取recvData.txt(假设内容为HelloWorld);
  2. 用xxd或 HxD 编辑器打开recvData.txt,找到W字符(ASCII 0x57),将其改为V(ASCII 0x56)——即翻转 bit 3;
  3. 将篡改后的recvData.txt重命名为recvData_corrupted.txt;
  4. 修改Receiver.java,在receive()中读取recvData_corrupted.txt作为模拟接收数据(或直接在checkChecksum()前data[5] ^= 0x08翻转 bit);
  5. 运行 Receiver,Log.txt应出现RECV PKT#X CORRUPTED!,且无SEND ACK#X记录。

提示:data[5] ^= 0x08是最直接的单比特翻转(0x08 = 0b00001000),比改字符更精准。

5.3 Wireshark 抓包对照:UDP payload 解析与 CRC 字段定位

Wireshark 是验证协议行为的终极手段。抓包命令:

# Linux/macOS sudo tcpdump -i any -w rdt3.pcap port 9999 # Windows(需 WinPcap/Npcap) # tshark -i "Ethernet" -w rdt3.pcap port 9999

在 Wireshark 中打开rdt3.pcap,找到 UDP 包,展开UDP → Data,右键Data→Export Packet Bytes保存为pkt_raw.bin。用xxd pkt_raw.bin查看:

00000000: 00 48 65 6c 6c 6f 57 6f 72 6c 64 21 3f .HelloWorld!?
  • 00是 SEQ 字段(1 byte);
  • 48 65 6c 6c 6f 57 6f 72 6c 64 21是 DATA("HelloWorld!" 的 ASCII);
  • 3f是 CHK 字段(1 byte)。

对比Log.txt中SENT PKT#0 SEQ=0 CHK=0x3F,确认 CRC 值一致。若chk字段被篡改(如3f→3e),Wireshark 显示的 payload 会变化,Receiver.checkChecksum()必然失败。

5.4 性能瓶颈实测:吞吐量(TPS)与超时阈值的量化关系

RDT 3.0 的吞吐量受timeout_ms严格制约。理论最大 TPS = 1000 / (2 × RTT + processing_time)。实测步骤:

  1. 设Config.ini:timeout_ms=1000,drop_rate=0.0,corrupt_rate=0.0;
  2. 修改Sender.java,在send()循环外记录开始时间long start = System.currentTimeMillis(),在Sender结束时记录long end = System.currentTimeMillis();
  3. 发送 100 个包,计算 TPS = 100 / ((end - start) / 1000.0);
  4. 逐步降低timeout_ms至 200,重测 TPS;
  5. 绘制曲线:X 轴timeout_ms,Y 轴TPS,会发现 TPS 在timeout_ms ≈ 2×RTT时达到峰值,之后下降(因超时重传增多)。

真实数据参考:在本地 loopback(RTT≈1ms),timeout_ms=10时 TPS≈80;timeout_ms=100时 TPS≈50;timeout_ms=1000时 TPS≈10。这印证了「超时不是越大越好」的工程直觉。

6. 进阶技巧:用 Log.txt 时间戳反推网络 RTT、构建自动化测试脚本、将 RDT 3.0 移植到 ESP32

6.1 从 Log.txt 提取 RTT:用 awk 一行命令计算平均往返时延

Log.txt的时间戳精确到毫秒,是反推网络性能的金矿。提取SENT PKT#X与对应RECV PKT#X的时间差:

# 提取所有 PKT#X 的 SENT 和 RECV 时间戳(格式:[YYYY-MM-DD HH:MM:SS.mmm]) grep -E "(SENT PKT#[0-9]+|RECV PKT#[0-9]+)" Log.txt | \ awk '{ # 提取时间字符串(如 [2024-03-15 14:22:03.872]) time_str = substr($1, 2, length($1)-2) # 提取序号和事件类型 if ($2 == "SENT") { seq = $3; event[seq] = "SENT"; time[seq] = time_str } else if ($2 == "RECV") { seq = $3; event[seq] = "RECV"; time[seq] = time_str } } END { # 计算每个序号的 RTT(毫秒) for (i in event) { if (event[i] == "SENT" && event[i] == "RECV") { # 这里需要将 time_str 转为毫秒数,简化版:假设同秒内 split(time[i], t, /[ :.]/) sent_ms = t[4]*3600000 + t[5]*60000 + t[6]*1000 + t[7] # 实际需解析完整时间,此处用 sed 预处理更可靠 } } }' Log.txt

更可靠方案:用 Python 脚本解析:

# parse_rtt.py import re from datetime import datetime log_lines = open('Log.txt').readlines() sent_times = {} recv_times = {} for line in log_lines: # 匹配 [2024-03-15 14:22:03.872] SENT PKT#0 ... match = re.match(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\] (SENT|RECV) PKT#(\d+)', line) if match: timestamp, event, seq = match.groups() dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M <p> <a href="https://download.csdn.net/download/Lovejmy/64126610" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:32:43

Python报错UnboundLocalError深度解析:作用域规则与正确解法

做 Python 开发的人&#xff0c;几乎没人能绕开这行报错&#xff1a;UnboundLocalError: local variable xxx referenced before assignment我第一次遇到它是在写一个计数统计脚本的时候&#xff0c;函数里明明在文件顶部定义了变量&#xff0c;结果一运行就给我甩这个脸子。当…

作者头像 李华
网站建设 2026/10/1 10:32:01

Qi水Yin乐 Win客户端僵尸歌曲清理和个人推荐歌单工具

用 Node.js 给汽水音乐做了个本地辅助服务缘起 用汽水音乐 PC 端的时候&#xff0c;有几个小痛点一直没解决&#xff1a; 推荐流里经常混着试听片段&#xff0c;听着正起劲突然切了 歌单里躺着一些已经下架的僵尸歌曲&#xff0c;点了没反应 想把喜欢的歌缓存到本地&#xff0c…

作者头像 李华
网站建设 2026/10/1 10:31:59

DeepSeek Harness官方正版桌面端发布,还有体验金领

DeepSeek Harness v0.2&#xff1a;当模型越来越强&#xff0c;还要不要这层「外壳」 8月13日 v0.1 开源 → 9月29日 v0.2 桌面版 9月29日&#xff0c;DeepSeek Harness 发布 v0.2 预览版&#xff0c;macOS 和 Windows 桌面安装包同步上线。距离 8月13日以 MIT 协议开源的 v…

作者头像 李华
网站建设 2026/10/1 10:31:57

Python异步编程从入门到实战,一篇就够了

写了个爬虫&#xff0c;抓一百个网页&#xff0c;同步跑要三分钟&#xff0c;其中两分半在等网络响应。CPU闲得发慌&#xff0c;你却在干等。这不是代码写得慢&#xff0c;是模式选错了。同步编程像排队打饭&#xff0c;一个人打完才轮到下一个&#xff1b;异步编程像同时开十个…

作者头像 李华
网站建设 2026/10/1 10:31:55

【仓颉语言入门 · 第21课】

【仓颉语言入门 第21课】cjpm 包管理与多文件项目组织&#xff1a;从单文件练习迈向工程化开发 前 20 课的代码都活在一个单独的 src/main.cj 里&#xff0c;靠 import std.* 使用标准库。但真实项目必然要拆文件、分包、复用别人写的库。本课带你掌握 cjpm 包管理&#xff1a…

作者头像 李华
网站建设 2026/10/1 10:29:46

PandaAI Qube一站式交易策略平台

今天给大家介绍一款PandaAI出品的非常好用的交易策略平台Qube&#xff0c;通过提示词对话就可以直接生成你想要的交易策略&#xff0c;平台担心大家不知道怎么开始&#xff0c;特意在平台的首页【想从哪儿开始&#xff1f;】功能提供很多提示词模版&#xff0c;并且支持主流的A…

作者头像 李华