简介:本资源是面向计算机网络课程学习者与初学者的TCP可靠性传输原理实践包,聚焦RDT 2.0简化模型,深入解析位错检测、停止-等待机制、序列号管理、超时重传及ACK丢失/重复处理等核心机制,助力理解TCP底层可靠传输的设计逻辑。压缩包共16个文件,含4个Java源码文件(实现发送端/接收端逻辑)、5个class字节码文件(可直接运行验证)、2个txt文本(含接收日志与配置说明)、1个INI配置文件及Eclipse项目相关元数据(.project、.classpath、.prefs等),整体体积仅1.04MB,轻量易部署。已有428人学习下载,适合课堂实验复现、课设开发或协议原理巩固。读者可直接导入Eclipse运行调试,观察数据包收发、校验失败响应、ACK超时触发重传等关键行为,并结合Log.txt与recvData.txt分析协议交互全过程,掌握从理论到代码落地的完整链路。
1. TCP-RDT2.0.zip 不是“TCP协议实现包”,而是本科《计算机网络》课程中一个被反复手撕、调试到凌晨的可靠数据传输教学原型
你解压TCP-RDT2.0.zip,看到rdt_sender.py、rdt_receiver.py、udt_channel.py和几份.txt测试用例——这不是工业级 TCP 栈,也不是 Linux 内核模块,而是一个严格限定在停等协议(Stop-and-Wait)框架下、仅实现 RDT2.0(带校验和 + ACK/NACK 反馈 + 超时重传)语义的 Python 仿真环境。它不处理序列号滚动、滑动窗口、拥塞控制、MSS 分段或 TIME_WAIT 状态;它的存在意义,是让你亲手把“为什么需要 ACK”“NACK 和超时谁先触发”“校验和怎么伪造错误”这些抽象概念,变成能 print 出来、能单步进、能改一行代码就让整个传输链路崩掉的血肉逻辑。适合刚学完《计算机网络:自顶向下方法》第3章、正在啃 Wireshark 抓包截图却始终不明白“为什么这个 ACK 没发出去”的本科生;也适合想快速验证某条协议设计直觉(比如“如果我把 timeout 设成 0.1s 会怎样?”)的嵌入式通信初学者。它跑在本地内存里,不占端口、不建 socket、不碰网卡驱动——所有“网络”行为,都由udt_channel.py里那个可配置丢包率/延迟/乱序的黑匣子模拟。别指望它连上 real server,但你改三行代码,就能复现教科书里那张经典的“RDT2.0 状态机翻车图”。
2. 从零跑通 RDT2.0:四文件结构拆解与最小可执行命令链
RDT2.0 的核心不是“写 TCP”,而是用最简模型暴露可靠传输的底层契约。它刻意剥离了真实 TCP 的复杂性,只保留三个不可妥协的要素:发送方必须等待确认、接收方必须显式反馈正确性、信道不可靠(丢包/比特翻转)必须被检测并重传。这四份 Python 文件构成闭环:
udt_channel.py:虚拟信道——唯一真正“联网”的模块,但它不走物理网卡,只在内存里做概率丢包、随机延迟、注入比特错误;rdt_sender.py:状态机主体——维护当前待确认分组、计时器、重传逻辑;rdt_receiver.py:接收端契约——只接受按序到达且校验正确的分组,对重复/损坏包发 NACK;rdt_tester.py:测试驱动——喂数据、设信道参数、启动收发、比对输出。
提示:所有文件默认使用 Python 3.7+,无需安装额外依赖。
pip install -r requirements.txt在本项目中不存在——这是刻意为之的轻量设计。
2.1 四文件职责与交互时序:一张图看懂谁调谁、何时发什么
RDT2.0 的运行不是“启动两个进程”,而是单进程内协程式调度:rdt_tester.py创建 sender/receiver 实例 → 启动 sender 发送第一个分组 → udt_channel 模拟传输 → receiver 解析 → 发回 ACK/NACK → udt_channel 再次模拟返回 → sender 收到后决定继续发下一个或重传。整个过程没有多线程锁、没有 callback 回调、没有 event loop——全靠while True:+time.sleep()+ 显式状态轮询。这种“反现代”的写法,正是为了让你看清:可靠性的代价,是发送方必须阻塞等待、接收方必须主动响应、信道必须可干预。
关键交互点有三处:
sender.send(data)→ 触发make_pkt()构造含 seqnum、checksum、data 的分组 →udt_send()把它扔进信道;receiver.receive()→ 从信道取包 →is_corrupt()校验 → 若错则udt_send(NACK),若对则udt_send(ACK)并交付上层;sender.wait_for_ack()→ 启动Timer→ 若超时未收到 ACK/NACK,则retransmit();若收到 NACK,则立即重传;若收到 ACK,则nextseqnum = 1 - nextseqnum(翻转 0/1)。
2.2 最小可执行命令:三步启动,亲眼看见“丢包→重传→成功”的完整链路
不要直接双击运行。先打开终端,cd 进解压目录,执行以下三步:
# 第一步:启动测试器,指定信道丢包率 0.3(30%)、基础延迟 0.1s、启用比特错误注入 python rdt_tester.py --loss_rate 0.3 --delay 0.1 --bit_error True # 第二步:观察输出——你会看到类似这样的日志流: # [SENDER] Sending packet with seqnum=0, checksum=0x3a4f... # [CHANNEL] Dropping packet (loss_rate=0.3)... # [SENDER] Timeout! Retransmitting packet with seqnum=0... # [RECEIVER] Received corrupted packet -> sending NACK # [SENDER] Received NACK -> retransmitting... # 第三步:修改参数再试,验证你的直觉: python rdt_tester.py --loss_rate 0.0 --bit_error False # 理想信道,应无重传 python rdt_tester.py --loss_rate 0.0 --bit_error True # 无丢包但有比特错误,应触发 NACK参数说明:
--loss_rate控制udt_channel.py中random.random() < loss_rate的丢包判定;--delay是time.sleep(delay * random.uniform(0.5, 1.5))的基线延迟;--bit_error开启后,udt_channel.corrupt_packet()会以 50% 概率翻转 payload 中一个随机 bit。这些参数不是“配置项”,而是你亲手操控的故障旋钮——调高丢包率,看重传次数飙升;关掉 bit_error,看 NACK 消失;把 delay 设为 0,看 ACK/NACK 返回快得像本地函数调用。
2.3 校验和(Checksum)实现细节:为什么compute_checksum()必须用struct.pack而不是sum()?
RDT2.0 的 checksum 不是 CRC32,也不是 MD5,而是教科书级的 16-bit one's complement sum(RFC 1071)。它的实现藏在rdt_sender.py和rdt_receiver.py的compute_checksum()函数里:
import struct def compute_checksum(packet): # packet: bytes, e.g., b'\x00\x01\xab\xcd...' (seqnum + checksum placeholder + data) checksum = 0 # 每次取2字节,转为无符号短整型(大端) for i in range(0, len(packet), 2): if i + 1 < len(packet): # 将两个字节打包成 >H(大端无符号短整型),加到 checksum word = struct.unpack('>H', packet[i:i+2])[0] checksum += word else: # 奇数字节:最后一个字节左移8位,补0 word = packet[i] << 8 checksum += word # 折叠进位:将高16位加到低16位,直到只剩16位 while checksum >> 16: checksum = (checksum & 0xffff) + (checksum >> 16) # 取反,得到最终 checksum return ~checksum & 0xffff为什么不用
sum(packet)?
因为sum()是字节级简单相加,无法模拟网络设备真实的校验逻辑。RDT2.0 的 checksum 必须满足:任何单比特错误都能以高概率被检测到,且对字节顺序敏感。struct.unpack('>H')强制按网络字节序(大端)解释每两个字节,这和真实 IP/TCP 头部 checksum 计算方式一致。如果你把>H改成<H(小端),或者用int.from_bytes(packet[i:i+2], 'little'),那么 receiver 就永远验不出 sender 的 checksum——这就是一个典型的“协议两端字节序不一致”翻车点,也是你调试时第一眼该盯的。
3. RDT2.0 状态机详解:从WAITING_FOR_ACK到WAITING_FOR_DATA的七种状态跃迁
RDT2.0 的灵魂是它的有限状态机(FSM),而非代码行数。rdt_sender.py里self.state只有两个合法值:WAITING_FOR_ACK和WAITING_FOR_DATA;rdt_receiver.py里self.expected_seqnum只能是0或1。但这两个变量的组合,以及它们在不同事件(timeout / recv_ack / recv_nack / recv_data)下的响应,构成了完整的协议行为。下面这张表,是你调试时贴在显示器边上的速查手册:
| 当前状态(Sender) | 触发事件 | 动作 | 下一状态 | 关键逻辑说明 |
|---|---|---|---|---|
WAITING_FOR_DATA | send(data)被调用 | 构造 pkt,启动 timer,udt_send(pkt) | WAITING_FOR_ACK | 此刻 sender 已无新数据可发,必须等 ACK 才能推进 |
WAITING_FOR_ACK | timer.timeout() | retransmit(),重启 timer | WAITING_FOR_ACK | 超时是重传唯一触发器,NACK 不重启 timer(避免雪崩) |
WAITING_FOR_ACK | recv_ack(0)且nextseqnum==0 | stop_timer(),nextseqnum = 1 | WAITING_FOR_DATA | ACK 匹配当前 seqnum,成功交付,切换到发下一包 |
WAITING_FOR_ACK | recv_ack(1)且nextseqnum==0 | 忽略(重复 ACK) | WAITING_FOR_ACK | RDT2.0 不处理乱序 ACK,只认当前期待的 seqnum |
WAITING_FOR_ACK | recv_nack(0)且nextseqnum==0 | retransmit(),不重启 timer | WAITING_FOR_ACK | NACK 明确告知错误,立刻重传,但 timer 继续跑(防 NACK 丢失) |
WAITING_FOR_ACK | recv_nack(1)且nextseqnum==0 | 忽略(错误 NACK) | WAITING_FOR_ACK | 接收方只对 expected_seqnum 发 NACK,其他 NACK 无效 |
注意:
rdt_receiver.py的状态更隐蔽——它没有显式state变量,而是靠self.expected_seqnum隐式表达。当它收到seqnum == self.expected_seqnum的正确包,就交付、self.expected_seqnum = 1 - self.expected_seqnum、发 ACK;收到seqnum != self.expected_seqnum的包(重复或乱序),就静默丢弃、发 ACK(RDT2.0 要求对重复包也发 ACK,这是和 RDT2.1 的关键区别);收到损坏包,就发 NACK。Receiver 永远不重传、不缓存、不排序——它就是一个纯粹的“校验+反馈”黑盒。
3.1 为什么 RDT2.0 的 receiver 对重复包发 ACK,而不是静默丢弃?
这是教科书故意设置的反直觉陷阱。真实 TCP 的 receiver 对重复 segment 是静默丢弃(因为有 buffer 和 SACK),但 RDT2.0 的 receiver 没有 buffer,也没有 sequence number window,它只认一个expected_seqnum。当 sender 因 timeout 重传了 pkt0,而 receiver 早已正确收到 pkt0 并发过 ACK0,此时重传 pkt0 到达——receiver 发现seqnum==0 == expected_seqnum,但它已交付过该数据,怎么办?RDT2.0 的设计选择是:仍发 ACK0。这样做的目的,是让 sender 知道“我收到了,别再重传了”,从而避免 sender 因没收到 ACK 而无限重传。这看似浪费带宽,却是停等协议下保证终止性的必要设计。你可以注释掉rdt_receiver.py中if seqnum == self.expected_seqnum:分支里的self.udt_send(ACK),然后跑测试——你会看到 sender 在收到第一个 ACK 后,依然不断重传,因为 receiver 对重复包什么都不发,sender 的 timer 永远等不到确认。
3.2 Timer 实现的三个致命细节:为什么threading.Timer是毒药,而time.time()是解药?
RDT2.0 的 timer 不是asyncio.create_task(),也不是signal.alarm(),而是最朴素的time.time()+ 循环轮询。rdt_sender.py中:
class RDT_Sender: def __init__(self): self.timer_start = 0.0 self.timeout_interval = 0.5 # 单位:秒 def start_timer(self): self.timer_start = time.time() def is_timeout(self): return time.time() - self.timer_start > self.timeout_interval def stop_timer(self): self.timer_start = 0.0为什么不用
threading.Timer?
因为threading.Timer会创建新线程,而 RDT2.0 的整个收发循环是单线程阻塞式的(while not self.done:)。如果 sender 启动一个threading.Timer,它在后台触发回调,但回调函数要修改 sender 的state或调用retransmit(),这就引入了竞态条件——主线程可能正在wait_for_ack(),而 timer 线程突然改了state。RDT2.0 故意规避多线程,用is_timeout()在每次循环迭代中检查,确保所有状态变更都在同一上下文发生。这是教学代码的“安全冗余”:宁可牺牲一点性能(每毫秒轮询一次),也要杜绝并发 bug。你若强行换成threading.Timer,十次测试里至少有三次会因state被异步修改而卡死或发错包。
4. 避坑指南:RDT2.0 项目里五个让我凌晨三点删库重来的血泪错误
RDT2.0 看似只有几百行,但每个字符都踩过坑。以下是我在带学生实验、自己复现、甚至投稿课程作业时,反复栽倒又爬起的五条铁律。它们不是“可能出错”,而是只要违反,必然失败,且错误现象极其隐蔽。
4.1 现象:Sender 一直重传,receiver 从不发 ACK/NACK
原因:udt_channel.py中udt_send()函数没有把 packet 存入self.packets_to_receive列表,或者udt_receive()没有从该列表 pop 数据。
解决:检查udt_channel.py的__init__是否初始化了self.packets_to_receive = [];检查udt_send()是否执行self.packets_to_receive.append(packet);检查udt_receive()是否执行return self.packets_to_receive.pop(0) if self.packets_to_receive else None。漏掉任何一个 append 或 pop,信道就变成黑洞。
4.2 现象:Receiver 收到正确包却发 NACK,或收到损坏包却发 ACK
原因:rdt_receiver.py中is_corrupt()函数计算 checksum 时,传入的 packet 是原始字节流,但compute_checksum()期望的是“去掉原 checksum 字段后的 packet”。RDT2.0 的 packet 结构是[seqnum:1byte][checksum:2bytes][data:nbytes],校验时必须把中间 2 字节置 0 再算。
解决:在is_corrupt()中,必须先packet_copy = bytearray(packet)→packet_copy[1:3] = b'\x00\x00'→computed = compute_checksum(bytes(packet_copy))→return computed != received_checksum。如果直接对原始 packet 算 checksum,结果永远错。
4.3 现象:Timeout 时间极短(如 0.01s),但 sender 从未触发重传
原因:time.time()返回浮点秒,但self.timeout_interval = 0.01太小,加上udt_channel的time.sleep(delay)基础延迟(默认 0.1s)远大于 timeout,导致is_timeout()永远为 False。
解决:要么调大timeout_interval(建议 ≥0.2),要么在udt_channel.py中把time.sleep(delay * random.uniform(0.5, 1.5))的delay参数设为 0.001。timeout 必须显著大于信道平均延迟,否则协议退化为“永远等不到 ACK”。
4.4 现象:修改rdt_tester.py中的data字符串长度,程序崩溃或 checksum 错误
原因:compute_checksum()假设 packet 总长度为奇数时,最后一个字节要左移 8 位。但如果data是空字符串"",packet 只有seqnum + checksum共 3 字节,i=2时i+1=3 >= len(packet)=3,进入 else 分支,packet[i]是 checksum 的第一个字节(本该是 0),导致 checksum 计算污染。
解决:在compute_checksum()开头加保护:if len(packet) == 0: return 0;更健壮的做法是,构造 packet 时确保data至少为 1 字节(如b'X'),或在make_pkt()中 padding。RDT2.0 的 packet 结构隐含了最小长度约束,空 data 是非法输入。
4.5 现象:rdt_tester.py运行后无输出,卡住不动
原因:rdt_sender.py的wait_for_ack()循环里,self.udt_receive()返回None(信道无数据),但代码没有time.sleep(0.01)让出 CPU,导致 busy-wait 占满 100% CPU,且 receiver 根本没机会执行udt_send(ACK)。
解决:在wait_for_ack()的 while 循环末尾,加time.sleep(0.01)。所有轮询循环必须 sleep,否则 sender 和 receiver 无法并发执行——这是单线程模拟多角色的唯一协调机制。
5. 进阶技巧:用 RDT2.0 验证真实网络协议原理的三个硬核实验
RDT2.0 的价值不在“跑起来”,而在“改出来”。下面三个实验,每一个都对应一个真实 TCP 协议栈中的核心机制,你只需修改 3~5 行代码,就能在本地复现并理解其本质。
5.1 实验一:验证“超时重传 vs NACK 重传”的响应速度差异
真实 TCP 中,快速重传(Fast Retransmit)靠 3 个重复 ACK 触发,比超时快得多。RDT2.0 没有重复 ACK,但有 NACK。我们可以对比:当 packet 损坏时,NACK 重传是否比 timeout 重传更快?
操作步骤:
- 在
rdt_tester.py中,设置--loss_rate 0.0 --bit_error True(确保只触发 NACK,不丢包); - 在
rdt_sender.py的wait_for_ack()循环中,在if self.recv_nack():分支里加一行print(f"[SENDER] NACK received at {time.time():.3f}"); - 在
timer.timeout()分支里加print(f"[SENDER] Timeout at {time.time():.3f}"); - 运行,观察时间戳差值。
预期结果:NACK 重传发生在udt_receive()返回后立即执行,而 timeout 重传需等待完整timeout_interval。例如,若timeout_interval=0.5,NACK 可能在0.123s触发,timeout 在0.501s触发——相差近 400ms。这证明:显式负面反馈(NACK)比被动等待(timeout)更能压缩重传延迟,这也是为什么现代协议(QUIC、BBR)极力避免纯 timeout 机制。
5.2 实验二:注入“ACK 丢失”故障,观察 sender 如何通过 timeout 恢复
TCP 的 ACK 丢失是常态,RDT2.0 必须能应对。我们手动让 receiver 发的 ACK 消失。
操作步骤:
- 修改
udt_channel.py的udt_send()函数,在if isinstance(packet, bytes):分支内加:if len(packet) == 3 and packet[0] in [0, 1] and packet[1:3] == b'\x00\x00': # 判断是 ACK/NACK(3字节:seqnum+0+0) if random.random() < 0.5: # 50% 概率丢 ACK return # 直接丢弃,不 append 到 packets_to_receive - 运行
python rdt_tester.py --loss_rate 0.0 --bit_error False; - 观察日志:sender 应先发 pkt0 → receiver 发 ACK0 → ACK0 丢失 → sender timeout → 重传 pkt0 → receiver 再次收到 pkt0(重复)→ 发 ACK0(再次)→ sender 收到,推进。
关键洞察:RDT2.0 的可靠性不依赖 ACK 100% 可达,而依赖 sender 的 timeout 机制兜底。ACK 丢失不是错误,而是协议设计的输入条件——这正是 TCP “ACK 不可靠,但重传可靠” 的哲学根基。
5.3 实验三:把 RDT2.0 改造成 RDT2.1,只加两行代码
RDT2.1 的核心改进是:receiver 对重复包发 ACK(而非 NACK),且 sender 对重复 ACK 静默忽略。这解决了 RDT2.0 中“NACK 丢失导致 sender 无法重传”的问题。
改造步骤:
- 在
rdt_receiver.py的receive()函数中,找到if seqnum == self.expected_seqnum:分支,将其改为:if seqnum == self.expected_seqnum: # 正常情况:交付数据,翻转 expected_seqnum,发 ACK self.deliver_data(data) self.expected_seqnum = 1 - self.expected_seqnum self.udt_send(self.make_ack(seqnum)) else: # 重复包:静默丢弃,但发 ACK(不是 NACK!) self.udt_send(self.make_ack(seqnum)) # ← 关键:这里发 ACK,不是 NACK - 在
rdt_sender.py的wait_for_ack()中,删除recv_nack()的所有处理逻辑,只保留recv_ack(); - 运行测试,你会发现:即使 receiver 收到重复 pkt0,它也发 ACK0,sender 收到后忽略(因为
nextseqnum==1),不再重传。
结论:RDT2.1 的鲁棒性提升,来自 receiver 的“宽容”(对重复包发 ACK)和 sender 的“健忘”(忽略非期待 ACK)。这背后是协议设计的权衡:用带宽换确定性——多发几个 ACK,换来 sender 状态机的简化与可靠性提升。
我带过六届网络课,每次讲到 RDT2.0,都会让学生先花一小时把TCP-RDT2.0.zip里的四个文件逐行读完,然后关掉所有文档,只留编辑器和终端,从python rdt_tester.py开始,亲手制造每一个错误、修复每一个 bug。不是为了交作业,而是为了在某天调试真实 Modbus TCP 设备时,看到 Wireshark 里那个孤零零的ACK包丢失,能立刻反应过来:“哦,这不是设备坏了,是它的 RTO 设置太小,或者我的 NACK 没发出去——得去查它的 timeout 机制。” 这种肌肉记忆,比背一百遍三次握手流程都管用。希望帮到你。
本文还有配套的精品资源,点击获取