RDT 3.0 状态机与 Java 实现:从停等 ARQ 到 TCP 可靠传输
简介这是一份面向计算机网络课程实验的RDT 3.0可靠数据传输协议学习资料适合高校学生、教师及网络协议初学者用于理解TCP可靠传输的核心机制。压缩包共16个文件包含Java源码与编译后的class文件、工程配置文件、说明文档及数据文件等既可直接运行模拟程序也可依据源码和实验记录进行二次开发与调试。包体约1.04MB结构紧凑便于课堂演示与个人练习。已有442人学习下载。资料围绕停等ARQ协议展开涵盖序号管理、校验和检测、超时重传等关键逻辑并配有Log与recvData等过程记录文件可帮助读者观察丢包、数据篡改等异常场景下的协议行为深入理解TCP如何通过确认与重传保证可靠传输。对于正在完成计算机网络课程设计或准备相关实验报告的学习者这套资源能提供可直接运行的示例、可读性较好的代码注释及工程组织参考节省搭建环境的时间更快掌握RDT 3.0的设计思想与实现细节。1. 一个挂着 TCP 名字的教学包解决了传输层最硬的问题把 RDT3.0 的源码包拖进 IDE第一反应往往是.classpath、Config.ini、ENCDA.tcp这些文件里哪个才是协议本体。项目名写着 TCP-RDT3.0代码里却没有一个真正的 socket 监听这其实是计算机网络实验课里最常见的设定在一条不可靠的信道上用最少的机制模拟出可靠传输。RDT 3.0 解决的问题很具体——不加滑动窗口、不做流量控制只靠序号、校验和、超时重传这三样让一个会丢包、会出错、会重复的信道看起来像一条可靠信道。适合正在学 TCP 三次握手和传输层原理的学生也适合想把“停等 ARQ”从 PPT 变成代码的工程师。这个包真正值钱的地方在于它把理论状态机做成了能跑、能注入错误、能看日志的实验工程。2. RDT 3.0 的停等 ARQ 状态机与工程文件映射2.1 一比特序号如何把网络问题变成逻辑问题RDT 3.0 和 2.x 的本质差别是引入了一个一比特序号。发送方每发一个数据分组给序号0或1收到对应 ACK 后翻转成另一个。这个看起来简单的设计实际上解决了“分组重复”这个最难的问题。在没有序号时接收方无法分辨刚到的分组是新的还是超时重传的旧副本。比如接收方已经交付了第N个分组但 ACK 丢了发送方超时重传第N个接收方如果当作新分组交付数据就重复了。有了一个比特序号接收方只要记住“下一个期望的 seq 是 0 还是 1”发现到来的 seq 不等于期望值就立即丢弃或重新确认不会重复交付。这个设计的边界也很清楚停等协议同一时刻只有一条未确认的分组因此两种状态就够用。如果发送窗口大于 1一比特序号立刻失效必须像 TCP 那样做回绕处理。理解这一点后面看 TCP 的 32 位序号和窗口缩放选项视角会完全不同。2.2 Eclipse 工程里每个文件负责什么拿到工程文件后先看目录再读代码。TCP_RDT3.0根目录下是标准 Eclipse Java 工程.classpath、.project、.settings/org.eclipse.jdt.core.prefs负责编译环境和编码格式src/com是协议源码bin/com是编译产物Log.txt是运行输出recvData.txt是接收方最终落盘的数据文件。文件 / 目录类型在实验中的角色src/com源码RDT 3.0 发送方、接收方、分组封装、信道模拟bin/com编译产物可直接运行的 class 文件.classpath/.projectEclipse 工程配置指定源码路径和版本让工程能一次构建Config.ini配置常见做法是存数据文件路径、端口号、丢包率和超时参数ENCDA.tcp数据文件待传输的载荷文件可能是编码后的业务数据recvData.txt数据文件接收方交付结果用于比对传输前后是否一致Log.txt日志记录发送、接收、丢包、超时、重传事件排错主要依据其中ENCDA.tcp命名有点迷惑它不是协议格式而是一个输入文件。做实验时把文本内容改成二进制内容也能跑关键是接收方落盘的recvData.txt要和原文件逐字节一致。2.3 校验和判断分组损坏的最小成本手段RDT 3.0 要求对每个分组做差错检测。常见作业写法是奇偶校验但实际可用的实现会用 16 位累加和。下面这段是核心计算static short calcChecksum(boolean ack, int seq, byte[] payload) { int sum (ack ? 1 : 0) seq; for (byte b : payload) { sum (b 0xff); while ((sum 0xffff0000) ! 0) { // 进位折叠 sum (sum 0xffff) (sum 16); } } return (short) ~(sum 0xffff); }计算逻辑分三步先把 ACK 标记和序号累加进去再把 payload 每个字节按无符号值累加最后把高 16 位的进位不断折叠回低 16 位按位取反得到校验值。接收方用同样的函数重算如果结果与分组携带的 checksum 不一致说明数据中转错位、翻转或长度变化直接抛掉这个分组。参数上payload长度是实验调优的关键。校园网实验里常用 512 或 1024 字节太大时单个分组更易损坏太小时校验和开销占比升高。注意校验和只能保证“大概率检测出错误”不能定位错误位置这和 TCP 头部的检验和处理思路一致。可以把这个函数理解为“先校验、后交付”的前置闸门。3. Java 发送方与接收方的最小可运行实现3.1 分组结构设计seq、checksum、payload 的边界先定义分组对象这是整个工程的公共约定。下面是我在这个场景下最常用的结构压缩包里的实现未必完全同名但字段逻辑一致public class RDTPacket { final boolean isAck; final int seq; final int ackSeq; final byte[] payload; final short checksum; RDTPacket(boolean isAck, int seq, int ackSeq, byte[] payload, short checksum) { this.isAck isAck; this.seq seq; this.ackSeq ackSeq; this.payload payload; this.checksum checksum; } boolean checksumValid() { short re RdtChecksum.calc( isAck, seq, ackSeq, payload); return re checksum; } }isAck区分数据分组和确认分组。seq是数据分组的序号ackSeq是确认分组回显的序号TCP 里的 ACK 号码也是同样的逻辑。checksum覆盖 seq、ackSeq 和 payload确保确认分组里携带的序号不被篡改。发送方构建分组时把字节流切成固定块每个块封装进一个RDTPacket按序发出去。这里有个容易混淆的点ACK 分组本身也有序号或者回显号。接收方确认的是“我已经正确收到了序号为ackSeq的数据”发送方据此判断可以进入下一个分组。3.2 发送方超时重传不是规定动作是主循环发送方代码的核心是“阻塞在等待 ACK 的主循环”不是简单地发一次就完事。下面这段是最小实现private void rdtSend(byte[] data) throws Exception { int seq 0; Listbyte[] chunks splitByMss(data, mss); // 按 mss 分片 for (byte[] chunk : chunks) { boolean acked false; RDTPacket pkt buildDataPacket(seq, chunk); while (!acked) { channel.send(pkt); // 发送一次 timer.start(rto); // 重启看门狗 while (!timer.isExpired()) { RDTPacket resp channel.tryReceive(); if (resp null) continue; if (resp.isAck resp.checksumValid() resp.ackSeq seq) { acked true; // 匹配到正确 ACK break; } // 损坏的 ACK 或错误序号一律忽略 } if (!acked) { Log.w(timeout seq seq , retransmit); seqStat.retransmitCount; } } seq 1 - seq; // 翻转一比特序号 } }发送主循环关键在两点。第一timer.start(rto)必须放在每次重传之后不能复用旧定时器否则超时判断会失真。第二收到损坏的 ACK 或序号不匹配的 ACK 时不进入acked继续等直到定时器到期。常见错误是在收到错误 ACK 后立刻重传这会导致网络里出现大量无畏的重复分组。参数rto是超时时间单位通常用毫秒。在本地回环实验里RTT 只有几毫秒rto设 10 到 50 毫秒可保证较快反应如果模拟公网延迟到 100 毫秒rto至少要大于2 * rtt否则会频繁超时重传概率虚高吞吐量断崖式下滑。3.3 接收方重复 ACK 的防御是最容易漏的地方接收方看起来简单却是作业里扣分最重的地方。核心代码如下private void rdtReceive() throws Exception { int expectedSeq 0; while (running) { RDTPacket pkt channel.tryReceive(); if (pkt null) continue; if (!pkt.checksumValid()) { Log.w(corrupt seq pkt.seq , discard); continue; // 损坏分组直接丢弃 } if (!pkt.isAck pkt.seq expectedSeq) { deliverToFile(pkt.payload); // 写入 recvData.txt sendAck(expectedSeq); // 发送确认 expectedSeq 1 - expectedSeq; // 翻转期望序号 continue; } if (!pkt.isAck pkt.seq ! expectedSeq) { // 重复分组说明 ACK 丢了发送方超时重传 sendAck(1 - expectedSeq); Log.w(duplicate seq pkt.seq , re-ack (1 - expectedSeq)); } } }接收方校验顺序是先检查checksumValid()再检查序号。如果校验失败直接丢弃不发送任何反馈。如果校验通过但序号不是期望值说明这是重传的旧分组此时要重新发送上一次的 ACK。这个“重复 ACK”动作很重要发送方可能因为 ACK 丢失而超时重新收到这个 ACK 才能继续推进。很多人只做“丢弃重复分组”而不重发 ACK结果是发送方反复超时重传实验日志里出现大量timeout吞吐量低下。正确的行为模式是校验错误丢包序号错误重确认序号正确再交付。4. 错误注入、日志与性能读数4.1 丢包率和损坏率怎么设置才像真实网络RDT 3.0 实验要想有价值必须让信道“坏”起来。把-loss 0.0 -corrupt 0.0跑通只是验证编码正确真正能看出协议能力的是注入错误。常见启动方式是这样java -cp bin com.rdt3.RdtReceiver \ -port 9090 -output recvData.txt java -cp bin com.rdt3.RdtSender \ -host 127.0.0.1 -port 9090 \ -input ENCDA.tcp -loss 0.2 -corrupt 0.05 -rto 300-loss是丢包概率-corrupt是损坏概率-rto是重传超时时间。两个参数分别模拟不同故障丢包模拟网络拥塞和路由丢弃损坏模拟线路噪声或网卡故障。RDT 3.0 对两者的处理策略不同丢包靠超时发现损坏靠校验和发现。建议从loss0.1、corrupt0.05起步逐步加到loss0.4、corrupt0.2观察日志里重传次数和处理时间的变化。同一份代码里概率一般用均匀随机数实现。发数据分组时按loss丢弃按corrupt随机翻转 payload 里的几个字节。注意翻转字节必须落在payload范围内不要翻seq否则会把逻辑错误和信道错误混在一起排错很难收场。4.2 从 Log.txt 反推协议状态一次有错误的运行会留下类似下面的日志[TX] send seq0 len1400 checksuma41b [CH] drop seq0 loss [TX] timeout rto300 retransmit seq0 [RX] recv seq0 checksumok deliver [TX] ack seq0 [RX] recv duplicate seq0 re-ack0 [TX] send seq1 len1400逐行看[CH] drop表示信道模拟器丢弃了分组这是第一次seq0的发送没有到达接收方。发送方等了 300 毫秒没有 ACK触发重传。第二次seq0成功到达并交付接收方回ack seq0。但这条 ACK 本身也被信道丢掉了所以发送方没有收到于是再次等待超时。这里接收方又收到重复的seq0触发re-ack0发送方收到重复 ACK 后确认成功才进入seq1。读日志最需要盯的是两件事一是retransmit次数有没有持续上涨二是同一个序号在日志里出现几次。如果retransmit比例超过 20%通常是rto设置太小或者信道丢包率高于预期。如果协议正常但日志里出现大量duplicate而很少timeout说明 ACK 丢失较多但重传逻辑有效。传输结束后用下面的命令验证数据是否完整md5sum ENCDA.tcp recvData.txt两个文件的 MD5 一致说明接收端交付结果与发送端原始数据完全相同。这一步是整个实验的最终验收标准。4.3 性能读数吞吐量与协议利用率停等协议的性能上限可以估算。假设 payload 大小是mssRTT 是往返时间每个分组发送一次并等待 ACK理论最大吞吐量近似为吞吐量 ≤ mss / rttmss1400字节、rtt50ms时理论极限约1400 / 0.05 28KB/s。实际运行时还要扣除超时等待和重传开销通常只能达到这个值的 60% 到 80%。这也是为什么真实 TCP 不用停等而用滑动窗口加累积确认窗口可以让多个分组同时在途吞吐量按窗口大小倍数增长。做过这个实验再回头看 TCP 的快速重传理解会更深。TCP 收到乱序段后发送重复 ACK连续收到 3 个重复 ACK 就重传而不等超时本质上是在 RDT 3.0 重复 ACK 逻辑上做的时间优化。RDT 3.0 的“等定时器到期再重传”在实际网络里代价太高TCP 的触发器变成了事件驱动。4.4 常见误用把超时当丢包率调节阀很多实验者发现重传太多第一反应是加大rto。rto翻倍后timeout确实变少但吞吐量会同步下降因为在等待一个注定不回来的 ACK 上白白耗时间。正确思路是分类型处理loss高导致的timeout需要降低网络负载或增大重传频率corrupt高导致的checksum丢弃需要检查底层信道质量调大rto只能缓解瞬时抖动。标准做法是设置一个指数退避上限。第一次超时后重传间隔翻倍到某个阈值后不再增加。这样网络持续拥塞时不疯狂冲击链路网络恢复后又能快速收敛。这个机制直接对应 TCP 的指数退避。5. 把 RDT 3.0 的边界用到 TCP 工程调试上5.1 停等还是滑动窗口先看序号推进速度做 TCP 抓包排障时可以用 RDT 3.0 的思路快速判断协议状态。打开抓包文件找同一对 IP 和端口之间的数据段看序列号的推进方式如果发送方发出一个段后必须等 ACK 才发下一个那是停等如果连续出现多个不同序号的段再收到一个 ACK那是滑动窗口。真实业务系统里TCP 的窗口可能被接收方通告为很小比如win1这时 TCP 的表现和 RDT 3.0 几乎一样吞吐量会被 RTT 卡死。遇到这种场景先别怀疑代码性能重点看接收方的窗口通告是不是被小缓冲区限制了。这就是从 RDT 3.0 学到的检查习惯先看确认行为再看超时最后才看数据内容。5.2 用日志定位三类高频故障RDT 3.0 工程调试里最有用的技能是建立“现象到机制”的映射这套映射可以直接迁移到 TCP 长连接和 Modbus TCP 等工业场景。故障现象日志特征定位思路与调参吞吐量远低于理论值同一 seq 反复出现retransmitrto是否小于2*rtt尝试加大mss数据收到了但总是乱序接收端出现duplicate seq检查 ACK 丢失是否过多调整确认策略应用层偶发数据重复交付序号翻转错误检查接收方expectedSeq字段是否在交付后翻转连接长期无进展日志停在同一 seq不再发送检查接收方是否启动端口是否一致5.2.1 超时重传后的重复交付最后一个容易踩的坑在交付环节。接收方收到重复分组时正确动作是重发 ACK绝不能把 payload 再次写入recvData.txt。很多实现把“校验通过”和“序号正确”混在一起判断错误地导致同一个数据块交付两次。RDT 3.0 的验收标准是md5sum一致但即使一致也要进日志确认没有重复写入。这里最稳妥的做法是在写文件前记录文件偏移只有seq expectedSeq时才推进偏移。5.2.2 把实验参数移植到 TCP 排查如果自己维护着一个 TCP 长连接服务处理重传问题时可以用同样的三段式判断先看对端有没有回 ACK再看 ACK 序号是不是最新然后看超时退避有没有触发。把Log.txt换成tcpdump的[TCP Retransmission]和[TCP Dup ACK]标注分析路径完全一致。RDT 3.0 把传输层最基础的状态迁移做成了可见、可注入、可量化的实验排障时能直接复用“先校验、再判序、最后交付”的次序这套次序对任何可靠传输设计都适用。本文还有配套的精品资源点击获取