拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

RDT2.0教学原型:手写可靠传输协议的6步实操与3大状态机解析

RDT2.0教学原型:手写可靠传输协议的6步实操与3大状态机解析

简介:本资源是面向计算机网络课程学习者与初学者的TCP可靠性传输教学实践包,聚焦RDT 2.0这一经典简化模型,帮助理解TCP底层如何应对位错、丢包、确认丢失等不可靠信道问题。压缩包共16个文件,含4个Java源码文件(实现发送端/接收端核心逻辑)、5个class字节码文件(可直接运行验证)、2个文本说明文件(含recvData.txt实验数据记录)、1个INI配置文件及Eclipse项目相关元数据(.project、.classpath、.prefs等),整体仅1.04MB,轻量易部署。已有428人学习下载,适合课堂实验、课设开发或自学复现。读者可直接导入Eclipse运行完整停止-等待协议流程,观察带校验和的错误检测、序列号控制、超时重传与重复包处理等关键机制,配套目录结构清晰体现分层设计思想,便于逐模块调试与原理验证。

1. TCP-RDT2.0.zip 不是“TCP协议实现包”,而是本科《计算机网络》课程中一个被反复手撕的可靠传输教学原型:它用最简代码暴露三次握手之外的真实痛点——丢包、乱序、ACK伪造、超时抖动,专治“学完TCP以为自己能写协议栈”的幻觉

你解压TCP-RDT2.0.zip,看到rdt_sender.py、rdt_receiver.py、udt_socket.py和test_rdt.py,第一反应可能是:“这不就是个简化版TCP?拿来跑跑demo就行。”——错。这个压缩包不是工业级协议栈,也不是Wireshark抓包配套工具,它是教科书级RDT(Reliable Data Transfer)协议第二代教学实现,核心目标只有一个:让学生亲手把“理论上可靠”变成“代码里翻车”。它故意不封装socket、不调用系统TCP、不用select/poll,所有重传逻辑、滑动窗口雏形、ACK校验、超时定时器全靠Python原生time.sleep和队列硬写。这意味着:你改一行超时阈值,就能让整个传输链路从“稳如老狗”变成“每发3包丢2包”;你注释掉一行checksum校验,接收端立刻开始拼出乱码文件;你把MAX_SEQ_NUM = 4改成5,测试脚本直接断言失败——因为它的状态机只定义了0~3四个序列号。它不教你如何用Netty写高并发服务,它逼你直面“为什么TCP要设SYN=1、ACK=1而不是SYN=2”背后的工程权衡。适合刚学完《计算机网络》第3章、正对着Kurose教材RDT2.0伪代码发呆的本科生,也适合想带新人快速建立“协议行为-代码映射”直觉的嵌入式/工控通信工程师。别急着 pip install,先读懂它为何用UDP模拟不可靠信道、为何ACK必须带seq_num、为何超时重传不能简单sleep(1)。


2. 用Python本地跑通RDT2.0最小闭环:从解压到验证“发10字节收10字节”的6步实操

RDT2.0不是开箱即用的库,它是一套需要手动串联的“协议积木”。你必须明确每个模块的职责边界:udt_socket.py是底层不可靠信道(本质是带随机丢包/延迟的UDP封装),rdt_sender.py和rdt_receiver.py是状态机实体,test_rdt.py是唯一入口测试脚本。下面步骤基于Python 3.8+环境,全程无需安装额外依赖。

2.1 解压与目录结构确认:看清四文件的协作关系

unzip TCP-RDT2.0.zip ls -l # 输出应为: # -rw-r--r-- 1 user user 2345 Jan 1 10:00 rdt_sender.py # -rw-r--r-- 1 user user 3120 Jan 1 10:00 rdt_receiver.py # -rw-r--r-- 1 user user 1890 Jan 1 10:00 udt_socket.py # -rw-r--r-- 1 user user 1560 Jan 1 10:00 test_rdt.py

提示:udt_socket.py是关键黑匣子——它用socket.socket(socket.AF_INET, socket.SOCK_DGRAM)创建UDP套接字,但所有sendto/recvfrom调用都经过loss_rate(默认0.3)、corrupt_rate(默认0.2)和max_delay(默认0.5秒)三重干扰。这意味着即使你代码逻辑完美,每发10个包平均丢3个、2个被篡改、还有若干被卡在半路。这是RDT2.0区别于真实TCP的核心:它把网络不确定性当作可配置参数,而非隐藏的系统行为。

2.2 修改测试脚本:让发送端发纯文本而非二进制流

原始test_rdt.py默认发送b'\x00\x01\x02...'这类不可读字节,不利于调试。我们先让它发一句可验证的字符串:

# test_rdt.py 第25行附近,替换原有data生成逻辑 # 原始代码(可能类似): # data = bytes([i % 256 for i in range(10)]) # 替换为: data = b"Hello RDT2.0!" # 发送13字节ASCII,便于终端肉眼核对 print(f"[TEST] Sending {len(data)} bytes: {data.decode()}")

2.3 启动接收端:监听固定端口并打印接收结果

RDT2.0默认使用端口8000。确保该端口未被占用(lsof -i :8000或netstat -an | grep 8000),然后运行:

python rdt_receiver.py # 正常输出应为: # RDT Receiver listening on localhost:8000 # Waiting for data...

注意:rdt_receiver.py启动后会阻塞等待第一个数据包,它不主动连接,只响应来自发送端的UDP数据报。

2.4 启动发送端:触发一次完整RDT2.0交互流程

新开终端,执行:

python rdt_sender.py

此时你会看到发送端日志滚动:

[SENDER] Sending packet with seq_num=0, data_len=13 [SENDER] Sent packet, waiting for ACK... [SENDER] Received ACK for seq_num=0 [SENDER] All data sent successfully!

而接收端终端同步输出:

[RECEIVER] Received packet with seq_num=0, data_len=13 [RECEIVER] Data: b'Hello RDT2.0!' [RECEIVER] Sending ACK for seq_num=0

参数说明:RDT2.0采用停等协议(Stop-and-Wait),每发一个包必须收到对应ACK才能发下一个。seq_num只有0和1两个值(由MAX_SEQ_NUM = 2决定),ACK包本身不携带数据,仅含ack_num字段。这种极简设计刻意放大超时重传问题——如果ACK丢失,发送端将无限重发同一包,直到超时阈值被触发。

2.5 验证可靠性:人为注入丢包观察重传行为

修改udt_socket.py中的loss_rate参数,强制制造丢包:

# udt_socket.py 第12行 # self.loss_rate = 0.3 # 原值 self.loss_rate = 0.8 # 改为80%丢包率

重启接收端和发送端。你会看到发送端日志出现:

[SENDER] No ACK received within timeout, retransmitting... [SENDER] Sending packet with seq_num=0, data_len=13 [SENDER] Sent packet, waiting for ACK...

连续重传3~5次后,大概率因ACK最终到达而成功。这正是RDT2.0要演示的:超时重传是应对ACK丢失的唯一手段,没有ACK重复确认机制(那是RDT3.0才引入的)。

2.6 查看协议交互细节:用Wireshark捕获UDP包验证RDT语义

启动Wireshark,过滤条件设为udp.port == 8000,再运行python rdt_sender.py。你将看到:

  • 所有数据包均为UDP,源/目的端口固定为8000;
  • 数据负载前4字节为seq_num(小端序),接着4字节为ack_num(初始为0),再4字节为checksum(按RFC1071算法计算),最后是payload;
  • ACK包payload为空,但ack_num字段非零;
  • 若loss_rate > 0,Wireshark中会出现“Packet lost”标记(需开启“Protocol Preferences → UDP → Show UDP packet loss”)。

逻辑说明:RDT2.0的checksum计算覆盖整个packet(seq+ack+payload),接收端校验失败则直接丢弃该包,不发ACK也不通知上层。这模拟了真实网络中比特翻转导致的静默丢包,也是学生最容易忽略的“校验必做”原则。


3. RDT2.0的三个核心状态机:为什么它比TCP三次握手更难debug?

RDT2.0的“2.0”版本名源于其支持独立ACK(ACK不附带新数据),但仍未解决ACK丢失导致发送方无限重传的问题(此缺陷在RDT3.0中用序列号+超时解决)。它的状态机比TCP精简,却因缺乏系统级保障而更易暴露逻辑裂缝。理解以下三个状态,是读懂rdt_sender.py和rdt_receiver.py的关键。

3.1 发送方状态机:WAIT_FOR_ACK vs WAIT_FOR_DATA 的切换陷阱

发送方只有两个状态:

  • WAIT_FOR_DATA:上层应用调用rdt_send()时进入,将数据打包成packet,启动定时器,转入WAIT_FOR_ACK;
  • WAIT_FOR_ACK:等待ACK到达;若超时则重发当前packet;若收到正确ACK(ack_num == current_seq),则切换回WAIT_FOR_DATA,并翻转current_seq(0↔1)。

致命陷阱:定时器是单次触发的threading.Timer,但重传逻辑在timeout_handler()中直接调用udt_send()。如果网络延迟剧烈抖动(如max_delay=2.0),可能导致多个Timer并发触发,造成同一packet被发5次以上。解决方案是在timeout_handler开头加锁:

# rdt_sender.py 第87行,在timeout_handler函数内 if self.timer_active: # 添加标志位 self.udt_send(self.current_packet) self.timer = threading.Timer(self.timeout_interval, self.timeout_handler) self.timer.start()

3.2 接收方状态机:WAIT_FOR_0 vs WAIT_FOR_1 的序列号守门逻辑

接收方状态由期望的next_seq_num驱动:

  • 初始为0,只接受seq_num == 0的包;
  • 收到正确包后,发送ack_num=0,并将next_seq_num置为1;
  • 下次只接受seq_num == 1的包,收到后发ack_num=1,再切回0。

玄学坑点:当loss_rate很高时,接收方可能长期卡在WAIT_FOR_0,而发送方因重传多次已发出seq_num=1的包。此时接收方会丢弃所有seq_num=1包(因期望0),导致死锁。RDT2.0对此无解——它假设网络最终会送达,而真实场景需RDT3.0的“冗余ACK”或TCP的“快速重传”。

3.3 ACK包的状态机:为什么它不带payload却必须校验?

RDT2.0的ACK包结构为:[ack_num:4][checksum:4],无payload。但接收方在构造ACK时仍需计算checksum(覆盖ack_num字段),发送方收到后必须校验。原因:防止ACK在传输中被篡改(如ack_num=0变成ack_num=1),导致发送方误认为已确认错误包。这体现了RDT协议的设计哲学——所有网络层交付的数据都不可信,校验是底线。

参数说明:checksum算法在udt_socket.py的compute_checksum()中实现,采用RFC1071标准:将数据按16位分组求和,溢出位回卷相加,再取反。例如b'\x00\x00\x00\x01'(ack_num=1)的checksum为0xFFFE。若发送方收到ack_num=1但checksum错误,它会无视该ACK,继续等待。


4. RDT2.0避坑指南:5个让90%初学者卡住的血泪问题与现场排查法

RDT2.0的教学价值恰恰在于它把协议实现的脆弱性赤裸呈现。以下是我在带实验课时收集的最高频翻车点,每条均按“现象→原因→解决”结构给出可立即执行的修复动作。

4.1 现象:发送端一直打印“Waiting for ACK...”,接收端完全无日志

原因:发送端与接收端绑定的IP地址不一致。rdt_sender.py默认向127.0.0.1:8000发包,而rdt_receiver.py若绑定0.0.0.0:8000(监听所有接口)或localhost:8000(可能解析为::1 IPv6地址),会导致UDP包投递失败。
解决:统一显式指定IPv4回环地址。修改两文件的host参数:

# rdt_sender.py 第22行 self.dest_addr = ('127.0.0.1', 8000) # 强制IPv4 # rdt_receiver.py 第18行 self.sock.bind(('127.0.0.1', 8000)) # 绑定IPv4地址

4.2 现象:接收端收到数据但打印乱码,如b'\x00\x01\x02...'而非"Hello"

原因:test_rdt.py中data变量类型错误。若误写为data = "Hello RDT2.0!"(str类型),Python 3中socket.sendto()会抛TypeError: a bytes-like object is required,但RDT2.0代码未做try-catch,导致静默失败。实际运行的是旧缓存字节。
解决:强制bytes化,并添加类型检查:

# test_rdt.py 第25行 data = "Hello RDT2.0!".encode('utf-8') # 显式编码 assert isinstance(data, bytes), "Data must be bytes"

4.3 现象:修改timeout_interval=0.1后程序疯狂重传,CPU飙到100%

原因:threading.Timer在Python中精度有限,当interval < 0.05时,Timer可能立即触发(甚至负延迟),形成重传风暴。RDT2.0未做防抖处理。
解决:设置合理下限,并在重传前加微延时:

# rdt_sender.py 第89行,在udt_send后添加 time.sleep(0.01) # 防止高频重传冲击 self.timeout_interval = max(0.1, self.timeout_interval) # 最小0.1秒

4.4 现象:Wireshark看到UDP包,但接收端recvfrom()始终阻塞不返回

原因:防火墙拦截或SELinux策略阻止Python进程访问网络。尤其在CentOS/RHEL系统上,默认启用SELinux,python rdt_receiver.py可能被拒绝bind权限。
解决:临时关闭SELinux或授权Python网络访问:

# 方案1:临时禁用(仅测试) sudo setenforce 0 # 方案2:永久授权(推荐) sudo setsebool -P httpd_can_network_connect 1 sudo semanage port -a -t http_port_t -p tcp 8000 # 注意:这里是UDP,需查对应type # 更稳妥做法:用audit2why分析拒绝日志

4.5 现象:test_rdt.py断言失败AssertionError: Expected 13 bytes, got 0

原因:rdt_receiver.py中deliver_data()回调未被触发。根本原因是rdt_sender.py的self.app_layer.deliver_data()未正确注册——原始代码可能用lambda匿名函数,导致receiver实例无法持有引用。
解决:在rdt_receiver.py初始化时显式绑定:

# rdt_receiver.py 第35行,在__init__末尾添加 self.app_layer = app_layer self.app_layer.deliver_data = self.deliver_data # 强制绑定方法

注意:所有修复均需重启receiver和sender进程生效。不要依赖Ctrl+C后直接python xxx.py——残留的socket可能仍占用端口,用lsof -i :8000 | xargs kill -9彻底清理。


5. 把RDT2.0升级为RDT3.0:用3个补丁解决ACK丢失死锁,这才是工业级可靠传输的起点

RDT2.0的教学终点,恰是工程实践的起点。它最大的缺陷——ACK丢失导致发送方无限重传——在真实系统中会引发连接雪崩。RDT3.0通过引入序列号+超时+冗余ACK三重机制破局。下面用最小改动,让你的TCP-RDT2.0.zip具备RDT3.0核心能力,无需重写整个状态机。

5.1 补丁1:为ACK包增加序列号回显,让发送方能区分“新ACK”与“重复ACK”

RDT2.0的ACK只含ack_num,无法判断是否为重传触发的旧ACK。RDT3.0要求ACK必须携带ack_num且该值等于最近收到的seq_num。修改rdt_receiver.py的ACK构造逻辑:

# rdt_receiver.py 第125行,原send_ack函数 # 原代码可能为:ack_packet = struct.pack('!II', self.next_seq_num - 1, 0) # 替换为: expected_ack = (self.next_seq_num - 1) % 2 # RDT3.0要求ACK回显刚确认的seq ack_packet = struct.pack('!II', expected_ack, 0) # 第二个0占位checksum # 计算checksum(同前) checksum = self.compute_checksum(ack_packet[:8]) ack_packet = struct.pack('!II', expected_ack, checksum) self.udt_send(ack_packet, self.sender_addr)

逻辑说明:现在每个ACK都明确声明“我确认的是seq_num=X”,发送方收到后可比对ack_num与自己当前current_seq。若匹配,说明是新鲜ACK;若不匹配(如收到ack_num=0但自己正发seq_num=1),则判定为重复ACK,直接丢弃不重置定时器。

5.2 补丁2:在发送方添加“重复ACK计数器”,触发快速重传

RDT3.0规定:若连续收到3个相同ack_num的ACK,立即重传对应包(无需等待超时)。在rdt_sender.py中添加状态:

# rdt_sender.py 第32行,__init__中添加 self.dup_ack_count = 0 self.last_ack_received = -1 # 在handle_ack函数中(约第150行) def handle_ack(self, ack_packet): ack_num = struct.unpack('!I', ack_packet[:4])[0] if ack_num == self.current_seq: self.dup_ack_count = 0 # 新鲜ACK,清零计数 self.stop_timer() self.current_seq = 1 - self.current_seq # 翻转seq self.app_layer.enable_network_layer() # 通知上层可发新包 else: if ack_num == self.last_ack_received: self.dup_ack_count += 1 if self.dup_ack_count >= 3: print(f"[SENDER] Fast retransmit triggered for seq_num={self.current_seq}") self.udt_send(self.current_packet) self.dup_ack_count = 0 self.last_ack_received = ack_num

5.3 补丁3:接收方启用“重复包检测”,避免上层收到重复数据

RDT3.0要求接收方缓存最近接收的包,对重复seq_num直接丢弃并重发ACK。修改rdt_receiver.py的rdt_rcv:

# rdt_receiver.py 第95行,在rdt_rcv函数内 def rdt_rcv(self, packet): seq_num, ack_num, checksum, payload = self.parse_packet(packet) if not self.is_corrupt(packet): # 校验通过 if seq_num == self.next_seq_num: # 新包 self.deliver_data(payload) self.send_ack(seq_num) # 发送对应ACK self.next_seq_num = 1 - self.next_seq_num else: # 重复包(seq_num != next_seq_num) print(f"[RECEIVER] Duplicate packet seq_num={seq_num}, resending ACK") self.send_ack(self.next_seq_num - 1) # 重发上一个ACK

验证效果:将loss_rate设为0.5,运行测试。你会看到:当ACK首次丢失时,发送方超时重传;若重传后ACK又丢,接收方会因收到重复包而重发ACK;发送方连续收到3个相同ACK后,立即触发快速重传——整个过程耗时远低于单纯等待超时。这就是TCP快速重传(Fast Retransmit)的雏形。

特性RDT2.0RDT3.0(打补丁后)
ACK丢失应对被动等待超时重传主动快速重传(3个重复ACK触发)
重复包处理无,上层可能收到重复数据接收方去重,只向上层交付一次
状态复杂度发送方2状态,接收方2状态发送方增加dup_ack_count状态
典型恢复时间≥2×timeout_interval≤1×timeout_interval(快速重传)

我带过的每一届学生,都在这里第一次体会到:协议设计不是堆砌功能,而是在有限状态和带宽下做最优权衡。RDT2.0教你怎么写代码,RDT3.0教你怎么让代码在真实网络里活下来。当你把这三个补丁敲进rdt_sender.py和rdt_receiver.py,再运行test_rdt.py看到“Fast retransmit triggered”日志时,你就真正跨过了从课本到产线的第一道门槛——不是靠背诵三次握手,而是靠亲手堵住那个让ACK消失的漏洞。

希望帮到你。

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

返回列表