ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RDT2.0教学原型:停等协议与可靠传输原理实践

RDT2.0教学原型:停等协议与可靠传输原理实践 简介本资源是面向计算机网络课程学习者与初学者的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接收端契约——只接受按序到达且校验正确的分组对重复/损坏包发 NACKrdt_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.330%、基础延迟 0.1s、启用比特错误注入 python rdt_tester.py --loss_rate 0.3 --delay 0.1 --bit_error True # 第二步观察输出——你会看到类似这样的日志流 # [SENDER] Sending packet with seqnum0, checksum0x3a4f... # [CHANNEL] Dropping packet (loss_rate0.3)... # [SENDER] Timeout! Retransmitting packet with seqnum0... # [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 ones complement sumRFC 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:i2])[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:i2], 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_DATArdt_receiver.py里self.expected_seqnum只能是0或1。但这两个变量的组合以及它们在不同事件timeout / recv_ack / recv_nack / recv_data下的响应构成了完整的协议行为。下面这张表是你调试时贴在显示器边上的速查手册当前状态Sender触发事件动作下一状态关键逻辑说明WAITING_FOR_DATAsend(data)被调用构造 pkt启动 timerudt_send(pkt)WAITING_FOR_ACK此刻 sender 已无新数据可发必须等 ACK 才能推进WAITING_FOR_ACKtimer.timeout()retransmit()重启 timerWAITING_FOR_ACK超时是重传唯一触发器NACK 不重启 timer避免雪崩WAITING_FOR_ACKrecv_ack(0)且nextseqnum0stop_timer()nextseqnum 1WAITING_FOR_DATAACK 匹配当前 seqnum成功交付切换到发下一包WAITING_FOR_ACKrecv_ack(1)且nextseqnum0忽略重复 ACKWAITING_FOR_ACKRDT2.0 不处理乱序 ACK只认当前期待的 seqnumWAITING_FOR_ACKrecv_nack(0)且nextseqnum0retransmit()不重启 timerWAITING_FOR_ACKNACK 明确告知错误立刻重传但 timer 继续跑防 NACK 丢失WAITING_FOR_ACKrecv_nack(1)且nextseqnum0忽略错误 NACKWAITING_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的包重复或乱序就静默丢弃、发 ACKRDT2.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 发现seqnum0 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 字节i2时i13 len(packet)3进入 else 分支packet[i]是 checksum 的第一个字节本该是 0导致 checksum 计算污染。解决在compute_checksum()开头加保护if len(packet) 0: return 0更健壮的做法是构造 packet 时确保data至少为 1 字节如bX或在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_interval0.5NACK 可能在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/NACK3字节seqnum00 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它也发 ACK0sender 收到后忽略因为nextseqnum1不再重传。结论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 机制。” 这种肌肉记忆比背一百遍三次握手流程都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表