☰
TCP RDT 2.2 工程详解:累计确认与超时重传的可靠传输实现
2026/10/9 9:30:54 网站建设 项目流程

简介:TCP RDT 2.2实验包面向计算机网络课程学生与协议实现者,围绕可靠数据传输协议设计,重点解决ACK包位错带来的确认失效问题。资源在RDT 2.0基础上融入累计确认、超时重传与纠错编码策略,帮助理解TCP可靠传输机制及实际容错设计。压缩包约1.04MB,共15个文件,包含Java源码与class字节码(协议核心逻辑)、txt数据与日志文件、Eclipse工程配置(.project/.classpath/.prefs)及运行参数文件,可导入开发环境直接运行调试。现有323人学习使用,适合作为教学实验或课程设计参考。借助源码与收发数据记录,使用者可追踪数据段与ACK交互流程,观察超时触发条件,并修改校验策略验证不同场景下的可靠性表现,是深入掌握TCP RDT原理的实用素材。

1. TCP RDT2.2.zip:一份能跑通的可靠传输协议实验工程

如果你正在准备计算机网络课的“可靠数据传输”实验,或者想真正弄懂 TCP 在发送端和接收端之间是怎么保证“不丢、不错、不乱序”的,那这份 TCP_RDT2.2.zip 就是标准答案的工程化版本。它不是一篇纯讲原理的 PDF,而是一个 Eclipse 工程项目,解压后可以直接导入 IDE,跑出发送方、接收方的收发日志和接收数据文件。它模拟的是 TCP 的底层机制:序列号、校验和、ACK 确认、超时重传,重点解决 RDT 2.0 没有考虑的“ACK 包本身出错”问题。适合正在做 RDT 实验、需要交报告或准备面试深挖 TCP 可靠传输机制的人。我第一次拿到这包资源时以为就是个课堂作业,直到跑完日志才发现,它能完整演示累计确认和超时重传在真实网络噪声下的表现。

2. 先读懂 RDT 2.2 在改什么:从 RDT 2.0 的 ACK 脆弱点说起

2.1 RDT 2.0 只防数据不防 ACK,位错时发送方被卡住

RDT(Reliable Data Transfer,可靠数据传输)是 TCP 教学模型,重点不在性能,而在“可信”二字:数据必须按序到达,错误必须被检测,丢失必须被重传。RDT 2.0 版本里已经有了序列号和校验和,发送方给每个数据包编号,接收方收到后校验和验证,发现错误就回一个 NAK(否定确认),正确就回 ACK。听起来已经闭环了,但 RDT 2.0 有一个隐蔽漏洞:它只对数据包做校验,对 ACK 包完全不设防。

网络环境里噪声是双向的,数据包能被干扰,确认包同样会被反转位。假如接收方正确收到了序号为 5 的数据,回复了 ACK 5,但 ACK 在链路上发生了位错,发送方收到的确认内容校验失败,此时发送方陷入一种尴尬状态:它不知道对端是没收到数据,还是没返回确认。RDT 2.0 的简单设计里没有这个分支,只能死等,等不到 ACK 就一直重发同一份数据,且不能发新数据,整个传输被卡住。这个场景在教学里很容易被忽略,因为用本地回环跑代码时,ACK 几乎不会坏。

2.2 RDT 2.2 的三处改动:累计确认、超时重传、校验增强

RDT 2.2 的改进核心就是堵住 ACK 出错留下的窟窿。第一个改动是累计确认。接收方不再对每个数据包单发一个 ACK,而是只发送“最后一个连续收到的数据序号”。比如连续收到 1、2、3 号数据,只回一个 ACK 3,代表“1 到 3 都对了”。这样即使中间某个 ACK 丢失或损坏,后面正确的 ACK 也能覆盖之前的结果,减少确认包的绝对数量,也就降低了位错概率。

第二个改动是超时重传。发送方维护一个计时器,发出数据后如果超过 RTT(往返时间)的一定倍数还没收到对应确认,就主动重发。这一步把“死等确认”变成了“主动重试”。改动后的状态机里,发送方从单一状态拆成了两个状态:正常发送数据和等待确认超时。第三个改动是给 ACK 加上校验与纠错编码。常见实现是给 ACK 也附上校验和或 CRC,接收方生成确认时计算校验码,发送方收到后先校验,校验失败就直接丢弃并等待超时,不再把错误 ACK 当真。

2.3 从源码定位这些机制:在 src/com 里找到收发与控制逻辑

解压 TCP_RDT2.2.zip 后,源码主要在src/com目录下,多数课堂实现会拆成三个类:发送方类(完成分组、校验、超时重传、状态切换)、接收方类(完成校验、去重、累计确认生成与发送)、以及一个传输服务类(负责模拟底层不可靠信道,可能加入随机丢包和噪声)。如果你拿到的是这种结构,优先打开发送方的发送方法,看它有没有if (timer expired)或startTimer()这类调用,那是超时重传的关键入口。再看接收方构造 ACK 的代码,确认序号是不是取的最后接收到的连续序号,而不是当前收到的包的序号。

一个需要注意的细节是:RDT 2.2 的标准状态机里仍然只有两个状态(等待 0 号 / 等待 1 号,也就是交替比特协议对序号的简化),但在带累计确认的版本里,序号字段可能被扩展到多位,能区分更大的窗口。这份资源里具体实现是哪一种,打开源文件看序号变量的位宽就能判明。如果序号是 1 bit,说明它属于教学简化版;如果序号是 4 bit 或以上,那更接近真实 TCP 的累计确认思路。

3. 把压缩包变成可运行实验:Eclipse 导入与三个关键配置

3.1 工程结构速览:src/com、bin、Config.ini 各自负责什么

工程解压后是一套标准的 JDT Eclipse 项目,顶层有几个明显文件:.project定义工程项目元数据,.classpath指明源码目录和输出目录,.settings/org.eclipse.jdt.core.prefs存的是编译级别等 Java 编译器偏好,Config.ini是运行参数入口。src/com放所有 Java 源代码,bin是编译输出目录,项目已经帮你编译好了 class 文件。Log.txt和recvData.txt是运行时产出的日志文件,其中recvData.txt是接收方最终还原出的数据内容,Log.txt记录每个事件的时间戳和动作。

理解这个结构对排错很重要。很多学生喜欢直接运行 bin 里的 class,但一旦改了源码,bin 里的旧 class 不会自动更新,导致跑的还是旧逻辑。正确做法是让 Eclipse 重新编译整个工程,再以 src 下的主类作为启动入口。.classpath里的内容不用手动改,只要用 Eclipse 的“导入现有项目”功能,它会自动识别。

3.2 导入 Eclipse 的步骤与 .classpath 处理

我通常按下面这套流程操作,基本不会遇到起不来的情况。

# 在 terminal 里解压,注意保留目录结构 unzip TCP_RDT2.2.zip -d ~/workspace cd ~/workspace/TCP_RDT2.2 # 检查关键文件是否存在,缺了这些 Eclipse 无法识别工程 ls .project .classpath src bin
// 如果 .classpath 里 src 路径和实际目录不一致,导入后会有红叉 // 常见写法如下,type="src" 必须指向实际源码目录 <?xml version="1.0" encoding="UTF-8"?> <classpath> <classpathentry kind="src" path="src"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="output" path="bin"/> </classpath>

第一步解压时注意不要把TCP_RDT2.2再套一层目录,否则.project文件会躲在TCP_RDT2.2/TCP_RDT2.2/下面,Eclipse 导入时找不到根。我在第一次弄的时候就翻过车,导入后工程名对不上,源码也不显示。命令里的unzip -d会保留压缩包内顶层目录,如果解压后发现多嵌套了一层,直接mv挪平即可。.classpath里kind="src"的path字段要与实际源码目录严格一致,大小写也不能错,Linux 环境下这点尤其敏感。

导入这一步,打开 Eclipse 后选择File -> Import -> General -> Existing Projects into Workspace,然后在Select root directory里选择解压出来的TCP_RDT2.2目录。不要勾选Copy projects into workspace,除非你想把工程复制到默认工作区。导入后如果有红叉,先打开 Problems 视图看错误,最常见的两种:JRE 版本不匹配(org.eclipse.jdt.launching.JRE_CONTAINER指向的编译级别超出本机 JDK),以及 bin 目录被占用。第一种情况去Project Properties -> Java Compiler里把Compiler compliance level调到本机 JDK 支持的版本,比如 1.8 或 11。

3.3 Config.ini 里的参数怎么改:端口、丢包率、超时时间

Config.ini 是这份资源的运行参数入口,典型内容长这样(参数名可能随实现略有差异,但意思一致):

# 发送方监听/连接端口 sender.port=5521 # 接收方端口 receiver.port=5522 # 模拟信道丢包率,0.0-1.0 loss.rate=0.05 # ACK 位错率,用于触发 ACK 损坏场景 ack.corrupt.rate=0.10 # 超时重传时间,单位毫秒 timeout.ms=500 # 待发送数据文件路径 input.file=data.txt # 接收方还原输出文件 output.file=recvData.txt

loss.rate是数据包丢包率,ack.corrupt.rate是 ACK 位错率,这两个值决定你的实验能不能复现“ACK 出错”这个核心场景。我把ack.corrupt.rate设成 0 的时候,日志里几乎看不到超时重传;设成 0.2 之后,重传事件立刻密集起来。timeout.ms的值也很关键,它代表发送方等待确认的最大时间,一般取 RTT 的几倍。如果是本地回环模拟,RTT 只有几毫秒,timeout.ms设成 500 就够;如果你把发送方和接收方跑在两台机器上,就要根据实际 ping 值调整,否则要么频繁超时重传,要么真的丢包了却迟迟不重传。

另一个常被忽略的是input.file路径。默认的data.txt如果不在工程根目录,发送方读文件时会直接抛FileNotFoundException。我的习惯是把它指向绝对路径,或者保证数据文件就在工作目录下。output.file=recvData.txt对应压缩包里已有的 recvData.txt,实验跑完看这个文件是否与输入一致,就能判断协议是否可靠。

4. 跑通一次实验:启动、日志、判读 ACK 是否出错

4.1 先跑默认配置,看 Log.txt 和 recvData.txt 会出现什么

启动顺序上有讲究:接收方要先启动,发送方后启动,否则发送方发出的第一个数据包没有人接收,直接触发超时重传,日志看起来会误导你。Eclipse 里可以创建两个运行配置(Run Configurations),一个指定接收方主类,一个指定发送方主类,先启动接收方,再启动发送方。

跑完默认配置后,Log.txt里会出现两类核心记录:一类是发送方的“Send packet [seq=1, checksum=0x1A2B]”,另一类是接收方的“Deliver data [seq=1] to app, send ACK 1”。如果配置里ack.corrupt.rate=0,你会看到严格交替的发送-确认序列。把ack.corrupt.rate调高到 0.3 后,日志里会出现“ACK corrupted, discard”或类似提示,紧接着出现“Timeout for seq=X, resend packet X”。

同时观察recvData.txt的内容,它应该和发送方的输入数据完全一致。不一致时先别怀疑协议实现,多半是你改了丢包率却没把超时时间同步调大,导致接收方还没收到包,发送方就重传了另一个轮次的数据,序号交错后把重复数据又写入文件。RDT 2.2 的正确实现会对重复包做丢弃,不会重复写入,但如果写成“先写入再校验序号”,就可能把重复数据也追加到输出文件。

4.2 模拟 ACK 位错:注入噪声的常见做法

课堂教学里很少会真的去改网卡或引入电磁噪声,最常见的做法是在传输层模拟。一种是在发送方收到 ACK 后、解析校验和之前,加一个随机翻转逻辑:

// 模拟 ACK 位错:按概率翻转 ACK 内容 if (Math.random() < ackCorruptRate) { byte[] corruptedAck = ackPacket.getData(); int flipIndex = ThreadLocalRandom.current().nextInt(corruptedAck.length); corruptedAck[flipIndex] ^= 0x01; // 只翻转一个位 ackPacket.setData(corruptedAck); }

这段代码的作用是:在模拟信道层,每次 ACK 通过时按ackCorruptRate的概率随机挑一个字节翻转最低位。这样校验和一定会不匹配,发送方就能识别出 ACK 损坏。注意ThreadLocalRandom只适合单线程模拟,若你的实现里发送和接收在不同线程,记得用同步或每线程单独取随机数,否则在并发下可能出现概率分布偏移,重传频率忽高忽低。更稳妥的做法是把随机数种子显式设置,比如new Random(42),这样每次实验产生的位错模式一致,便于线上答辩时复现结果。

4.3 对比观察累计确认与超时重传的日志特征

跑完一组实验后,我一般会把Log.txt按发送方和接收方拆成两份对比看。发送方日志关注事件类型和序号,接收方日志关注收到的序号和发出的确认。累计确认生效时,接收方日志里应该能看到类似“Receive packet seq=3, out of order, waiting seq=2”的记录,意思是它还没收到 2 号,所以不更新确认,或者回一个重复的 ACK 1。这时发送方的重传应该发生在超时之后,而不是立即重传,否则说明实现里把“收到重复 ACK”当作重传触发条件了,那是 RDT 3.0 快速重传的思路,RDT 2.2 里不应该出现。

超时重传的日志特征则是:发送方日志里出现“Timeout triggered”事件,然后重发的序号和之前发送过的某个序号相同。对比时间戳,如果重传间隔都严格等于你设置的timeout.ms,说明没有随机加扰动。真实 TCP 的超时重传带有退避和抖动,RDT 2.2 实验里可以直接看固定超时是否生效,这也是教学要求里最容易打分的一个点。

5. 避坑与常见问题:这五个坑我几乎每个都踩过

5.1 现象一:启动发送方时报 Address already in use

原因:上一次实验没正常关闭进程,接收方的 Socket 还占着端口,或者发送方把自己绑到了接收方占用的同一个端口。解决:先jps或ps -ef | grep java找到残留进程,kill掉。然后检查 Config.ini 里 sender.port 和 receiver.port 是否相同,两个端口不能混用。我因为偷懒直接把两个 port 都写成 5521,导致程序永远发不出去。

5.2 现象二:日志里全是重传,数据一个都到不了对端

原因:timeout.ms设得太小。我把超时时间设成 50ms,本地回环的 RTT 虽然只有几毫秒,但发送方处理日志、写盘的时候线程调度有波动,导致很多包其实到了接收方,ACK 也在路上,发送方就超时重发了。结果接收方不断收到重复包,又把重复 ACK 发回去,形成恶性循环。解决:把超时时间设为 RTT 的 5 到 10 倍,或直接用 ping RTT 乘 2 再乘一个冗余系数。实验环境里一般 500ms 以上就很少误重传。

5.3 现象三:接收方收到包但 always 校验失败,所有数据被丢弃

原因:发送方和接收方用的校验和算法不一致。常见实现里有的是累加和,有的是 CRC16,有的把校验和字段本身也算进校验值,有的不算。我遇到的一次翻车是:发送方用 Java 的Checksum.update()算 CRC,接收方手写了一个异或校验,两边结果永远对不上。解决:找到checksum()方法的实现,确认两个方向调用的是同一个方法,且校验和字段在计算前要清零。顺便说一句,如果代码里同时有 CRC 和奇偶校验两个选项,默认选 CRC,因为位错更可能被检测出来。

5.4 现象四:程序跑完了但 Log.txt 和 recvData.txt 是空文件

原因:输出路径设置不对。Eclipse 的运行工作目录(working directory)默认是工程根目录,但你如果直接双击 class 文件用 javaw 运行,工作目录可能是 class 所在目录bin/,导致写的相对路径落在 bin 下。解决:在运行配置的 Arguments 页面把 Working directory 显式设为${workspace_loc:TCP_RDT2.2},或者把 Config.ini 里的文件路径改成绝对路径。另外注意,有些实现里recvData.txt是追加写不是覆盖写,跑多次实验前要先手动删掉旧文件。

5.5 现象五:发送方收到了 ACK,但序号总是对不上

原因:把“累计确认”和“当前确认”搞混了。接收方收到 3 号包,如果 1、2 号都已经确认过,它应该回 ACK 3;但如果 2 号还没到,它只能回 ACK 1(最后一个连续的序号),或者回重复的 ACK 1。发送方的状态机必须基于“最后连续确认序号”,而不是“最近收到的确认序号”。如果代码里用最后一个 ACK 的值直接决定窗口滑动,就会算出错位。解决:检查发送方更新确认号的地方,是否判断了ackNum > lastAckNum才滑动窗口,这也是累计确认与逐包确认最关键的区别之一。

6. 验证与扩展:用统计重传率验证改进,再把它改造成自适应 RTT

把基础实验跑通之后,我建议你做两件更有价值的事:一是用数据量化 RDT 2.2 相比 RDT 2.0 的效果,二是把它扩展成带自适应 RTT 的版本。前者用来写实验报告,后者用来应付面试中“你怎么优化 TCP”的追问。

验证改进最直接的方式是统计重传率。在发送方代码里加一个计数器,每次超时重传时retransmitCount++,每次发送新包时newPacketCount++,实验结束后打印重传率 = retransmitCount / (newPacketCount + retransmitCount)。分别把ack.corrupt.rate设为 0、0.1、0.3、0.5 各跑一次,你会看到重传率随 ACK 位错率上升,但接收数据始终完整。这就是 RDT 2.2 的核心价值:代价是重传,收益是可靠。如果把 RDT 2.0 的代码(没有超时重传机制)跑同样的 ACK 位错率,会出现发送方永远卡死、接收数据不完整的现象,对比曲线可以用来论证超时重传的必要性。

扩展的方向我推荐做自适应超时。固定 500ms 在本地回环没问题,但一旦网络延迟波动,固定超时就显得笨拙。真实 TCP 用加权移动平均来估计 RTT:EstimatedRTT = 0.875 * EstimatedRTT + 0.125 * SampleRTT,超时时间通常设为EstimatedRTT + 4 * DevRTT。你可以参考这个思路,在发送方记录每次接收 ACK 的时间戳,计算当前样本 RTT,然后迭代更新超时阈值。核心修改点如下:

public long updateTimeout(long sampleRtt) { // 第一次测量时直接初始化 if (estimatedRtt == 0) { estimatedRtt = sampleRtt; devRtt = sampleRtt / 2; } else { // 平滑因子取 0.125,即 RFC 6298 中的 alpha estimatedRtt = (long) (0.875 * estimatedRtt + 0.125 * sampleRtt); devRtt = (long) (0.75 * devRtt + 0.25 * Math.abs(sampleRtt - estimatedRtt)); } // 超时时间等于期望值加四倍偏差 timeoutMs = estimatedRtt + 4 * devRtt; return timeoutMs; }

这段代码参考了 RFC 6298 的思路,参数选的 0.125 和 0.25 是标准建议。改完后再跑丢包率高的场景,你会发现重传率比固定超时低了不少,而且日志里超时事件的时间间隔不再恒定,是跟随网络波动在变化。从那以后我每次验证协议改进,都强制自己先跑一组固定参数做基准,再改一行代码、复测一组数据,绝不直接改多参数交叉验证——否则出了 bug 根本定位不到是哪个改动引入的。这个习惯帮我少走了很多弯路,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询