简介:这是一份面向计算机网络课程实验的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/.project | Eclipse 工程配置 | 指定源码路径和版本,让工程能一次构建 |
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; List<byte[]> 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 对两者的处理策略不同,丢包靠超时发现,损坏靠校验和发现。建议从loss=0.1、corrupt=0.05起步,逐步加到loss=0.4、corrupt=0.2,观察日志里重传次数和处理时间的变化。
同一份代码里,概率一般用均匀随机数实现。发数据分组时按loss丢弃,按corrupt随机翻转 payload 里的几个字节。注意翻转字节必须落在payload范围内,不要翻seq,否则会把逻辑错误和信道错误混在一起,排错很难收场。
4.2 从 Log.txt 反推协议状态
一次有错误的运行会留下类似下面的日志:
[TX] send seq=0 len=1400 checksum=a41b [CH] drop seq=0 loss [TX] timeout rto=300 retransmit seq=0 [RX] recv seq=0 checksum=ok deliver [TX] ack seq=0 [RX] recv duplicate seq=0 re-ack=0 [TX] send seq=1 len=1400逐行看,[CH] drop表示信道模拟器丢弃了分组,这是第一次seq=0的发送没有到达接收方。发送方等了 300 毫秒没有 ACK,触发重传。第二次seq=0成功到达并交付,接收方回ack seq=0。但这条 ACK 本身也被信道丢掉了,所以发送方没有收到,于是再次等待超时。这里接收方又收到重复的seq=0,触发re-ack=0,发送方收到重复 ACK 后确认成功,才进入seq=1。
读日志最需要盯的是两件事:一是retransmit次数有没有持续上涨,二是同一个序号在日志里出现几次。如果retransmit比例超过 20%,通常是rto设置太小,或者信道丢包率高于预期。如果协议正常但日志里出现大量duplicate而很少timeout,说明 ACK 丢失较多,但重传逻辑有效。
传输结束后,用下面的命令验证数据是否完整:
md5sum ENCDA.tcp recvData.txt两个文件的 MD5 一致,说明接收端交付结果与发送端原始数据完全相同。这一步是整个实验的最终验收标准。
4.3 性能读数:吞吐量与协议利用率
停等协议的性能上限可以估算。假设 payload 大小是mss,RTT 是往返时间,每个分组发送一次并等待 ACK,理论最大吞吐量近似为:
吞吐量 ≤ mss / rttmss=1400字节、rtt=50ms时,理论极限约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 的窗口可能被接收方通告为很小,比如win=1,这时 TCP 的表现和 RDT 3.0 几乎一样,吞吐量会被 RTT 卡死。遇到这种场景,先别怀疑代码性能,重点看接收方的窗口通告是不是被小缓冲区限制了。这就是从 RDT 3.0 学到的检查习惯:先看确认行为,再看超时,最后才看数据内容。
5.2 用日志定位三类高频故障
RDT 3.0 工程调试里最有用的技能,是建立“现象到机制”的映射,这套映射可以直接迁移到 TCP 长连接和 Modbus TCP 等工业场景。
| 故障现象 | 日志特征 | 定位思路与调参 |
|---|---|---|
| 吞吐量远低于理论值 | 同一 seq 反复出现retransmit | rto是否小于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 把传输层最基础的状态迁移做成了可见、可注入、可量化的实验,排障时能直接复用“先校验、再判序、最后交付”的次序,这套次序对任何可靠传输设计都适用。
本文还有配套的精品资源,点击获取