简介这是一份面向计算机网络课程教学的实验资源聚焦TCP可靠传输的简化模型RDT 2.0帮助学习者理解在易产生位错的信道上如何通过校验和或循环冗余校验检测错误并借助停止-等待协议、序列号标记、超时重传及确认丢失处理来保证可靠传输。压缩包内含16个文件以Java源码4个.java和编译后的class文件5个.class为核心配合.project、.classpath、.settings等Eclipse工程配置以及Log.txt、recvData.txt等运行记录和ENCDA.tcp实验数据文件整体仅1.04MB结构简洁适合直接导入IDE进行调试。已有428人学习下载。通过阅读源码与模拟运行可以清晰对照RDT 2.0在设计上的每一步处理例如接收端如何识别重复数据、发送方如何在超时后重传并能结合实验记录排查测试中的问题非常适合作为《计算机网络》课程设计或实验报告的参考也可为后续深入理解TCP的流量控制与拥塞控制机制奠定基础。1. TCP-RDT2.0.zip不是又一个 TCP 教程而是一份能跑起来的可靠性协议实现如果你的收藏夹里已经吃灰了几十个 TCP 三次握手图解但真正想动手改一个协议栈、模拟一次丢包重传时却连从哪下手都不知道那 TCP-RDT2.0.zip 这个压缩包值得你花一个晚上认真拆开。它不是论文也不是PPT而是一个可直接运行的可靠数据传输Reliable Data Transfer协议实现包。RDT 2.0 在计算机网络课程里是经典的教学模型负责解决“网络底层不可靠应用层怎么保证数据不丢、不错、不乱序”的问题。把这份代码跑起来你就能亲手看到校验和怎么拦截损坏包、ACK/NAK 怎么驱动重传、序列号怎么防重复而这些机制正是 TCP 协议栈的雏形。新手拿它入门网络编程再合适不过熟手则可以拿它做协议改动的试验田。2. 先搞懂 RDT2.0 在模拟什么从三次握手到校验和重传可靠性是怎么一层层长出来的2.1 RDT 的版本演进为什么上来就是 2.0而不是 1.0 或 3.0RDTReliable Data Transfer是计算机网络中用来抽象“如何在一根不可靠的信道上实现可靠传输”的系列协议模型。不同版本代表不同的可靠性能力RDT 1.0 假设信道完全可靠只做发送和接收相当于理想化的本地管道RDT 2.0 开始面对现实引入位损坏bit corruption的处理机制也就是校验和加确认反馈RDT 2.1 和 2.2 进一步解决 ACK/NAK 本身损坏、重复分组等问题RDT 3.0 则加上超时重传应对丢包。所以你拿到的这个 TCP-RDT2.0.zip核心是让你理解“当信道会篡改数据时协议如何通过反馈与重传自愈”。课程里通常要求用有限状态机FSM来设计发送方和接收方的状态转换而这份代码多半是把 FSM 翻译成了具体的 socket 收发逻辑。理解这一点很重要你运行的不是一个完整的 TCP 协议栈而是一个 TCP 核心机制的教学切片——它模拟的是 TCP 保证可靠性的最小闭环。2.2 三件套校验和、序列号、ACK/NAK 是怎么协同的RDT2.0 接收方主要看三样东西校验和是否匹配、序列号是否是期望值、分组类型是数据还是确认。发送方则围绕“发一条、等反馈、再发下一条”的停等模式工作。校验和的计算通常采用 Internet Checksum 算法把数据按 16 位切分累加溢出回卷最后取反码。接收方拿到分组后重算校验和如果不匹配就回 NAK 要求重发。序列号在这套机制里只取 0 和 1 两个值因为停等协议同一时刻只有一条在途分组0/1 足够区分新旧。TCP 真正的三次握手里序列号是随机的 32 位值靠 SYN、ACK 标志位完成同步但思想源头和 RDT2.0 的 0/1 序列号一致让接收方有能力识别“这条数据是我没见过的还是重复的”。ACK/NAK 的设计也有讲究。NAK 是负面确认告诉发送方“你发的东西坏了重来”。但在真实 TCP 里没有 NAK只有 ACK——接收方通过“期待的序列号”间接表达“你该重发哪一段”。RDT2.3 和 TCP 都选择了只靠 ACK 超时的方案因为 NAK 本身也可能损坏或丢失而 ACK 带期望序列号的设计更健壮。你在跑这份代码时可以留意它用的是 NAK 还是纯 ACK 模式这决定了代码后续能不能平滑地演进成 RDT3.0。2.3 RDT2.0 与真实 TCP 的差距停等和滑动窗口停等协议的效率瓶颈非常明显发送方发一个包就等着网络往返时间RTT越长信道利用率越低。假设 RTT 是 100ms带宽是 10Mbps一个 1KB 的分组发完要等 100ms 才能发下一个实际吞吐只有 80Kbps 左右利用率不到 1%。TCP 解决这个问题用的是滑动窗口和流水线不用等 ACK可以连续发多个包窗口大小由接收方通告和拥塞控制共同决定。所以 TCP-RDT2.0 的价值定位很清晰它帮你把“可靠传输的最小单元”拆到能看见每一个位校验和状态转换然后你再去看 TCP 的滑动窗口、拥塞避免、快速重传这类进阶机制会容易得多。如果直接上手看 TCP 协议栈代码大概率会被状态机、定时器矩阵和拥塞控制算法淹没翻车是常态。3. 从 zip 到跑通解压、环境准备与最小启动命令3.1 解压与目录结构识别拿到 TCP-RDT2.0.zip先别急着双击解压到桌面了事。我一般会建一个专门的实验目录保持路径无中文无空格否则后续抓包、写脚本时会出现各种莫名其妙的路径问题。用命令行解压可以避开 Windows 资源管理器对 zip 伪加密等异常结构的兼容问题特别是当压缩包附带权限位时。mkdir -p ~/lab/tcp-rdt2 cd ~/lab/tcp-rdt2 unzip ../TCP-RDT2.0.zip -d . ls -la解压后用ls看目录结构。常见的教学包会包含sender.py、receiver.py、rdt_utils.py或packet.py这样的模块划分。如果看到 Makefile 或 requirements.txt说明作者准备了构建和依赖清单。若没有也不用慌这类协议模拟项目一般只依赖 Python 标准库的socket、struct、hashlib不会引入第三方重量级依赖。这里的unzip是 Linux/macOS 自带命令。Windows 用户可以在 Git Bash 或 WSL 里执行也可以直接用 PowerShell 的Expand-Archive -Path TCP-RDT2.0.zip -DestinationPath .。核心目的是把文件完整释放确保目录里没有缺失的模块文件。如果解压过程提示 CRC 错误说明 zip 文件本身损坏或下载不完整需要重新获取压缩包这不是代码问题。3.2 运行模式确认谁先启动谁后启动协议模拟程序通常分发送端和接收端两个进程。先启动接收端它会绑定一个端口并监听再启动发送端向指定 IP 和端口发起连接。这和我们平时用 TCP 客户端去连服务端的顺序一致也是 tcp 三次握手的实际发生过程服务端 listen客户端 connect内核自动完成 SYN、SYN-ACK、ACK 交换。# 终端 1启动接收端 python3 receiver.py --port 8888 --loss 0.1 # 终端 2启动发送端 python3 sender.py --host 127.0.0.1 --port 8888 --file ./test.txt常见做法是接收端先监听发送端再发起连接。如果你把顺序反了发送端会直接抛ConnectionRefusedError。这不算 bug而是 TCP 本身的连接模型决定的客户端必须在服务端已经 listen 的前提下才能完成握手。另外注意命令行里的--loss参数它控制模拟丢包率是 RDT3.0 超时重传的关键开关。RDT2.0 不处理丢包理论上不需要这个参数但如果这份代码已经做了扩展丢包率就是测试重传逻辑的核心旋钮。3.3 参数说明端口、文件路径和丢包率怎么配--port接收端监听的端口号范围 1024 到 65535。建议选 8000 以上的端口避开系统服务常用的 8080、3306 等省得和本地其他服务撞车。若看到Address already in use说明端口被占用换一个或用lsof -i:8888查占用进程。--file发送端读取并传送的文件路径。初次实验建议准备一个几百字节的文本文件输出日志短容易和抓包结果对上号。传送大文件时要注意接收端写入路径是否有写权限。--loss模拟丢包率0 到 1 之间的小数。0.1 表示约 10% 的分组会被丢弃。设置成 0 时 RDT2.0 一切正常传输速度极快设置成 0.3 时如果代码没有超时机制程序会直接卡死这就是 RDT2.0 和 RDT3.0 的分水岭。跑通第一遍之后建议你修改--loss从 0 到 0.1 再到 0.3观察发送端日志里重传次数、接收端日志里校验和失败次数的变化。这份代码能不能处理丢包跑一次就现原形。4. 核心代码怎么读校验和、停等逻辑与粘包边界4.1 校验和计算的实现与边界坑把rdt_utils.py或对应模块打开你会看到一个计算校验和的函数。它通常长这样import struct def checksum(data: bytes) - int: if len(data) % 2 1: data b\x00 # 奇数长度补一个零字节保证能按 16 位对齐 total 0 for i in range(0, len(data), 2): # 每两个字节拼成一个 16 位整数大端序读取 word struct.unpack(!H, data[i:i2])[0] total word # 溢出回卷超过 16 位的进位加回低位 total (total 0xffff) (total 16) return (~total) 0xffff # 取反码作为最终校验值这段代码实现的是 RFC 1071 描述的 Internet Checksum和 TCP/IP 头部的校验和算法同源。struct.unpack(!H, ...)按网络字节序读取 16 位无符号整数!代表大端序这是网络协议的标准字节序。发送方算完校验和之后会把它连同序列号和数据一起封装成分组发出去接收方收到后重算校验和如果结果不为 0注意是接收方把收到的校验和字段也参与计算结果为 0 才正确就判定分组损坏回 NAK。边界坑集中在两个地方一是数据长度奇数时必须补零否则最后一字节无法凑成 16 位二是溢出回卷的时机必须在每次累加后立即回卷不能在循环结束后统一处理否则 32 位溢出会丢失进位。这些细节如果写错发送方自己算的校验和跟接收方算的对不上会出现“明明没损坏却一直回 NAK”的翻车现场。4.2 停等发送与 ACK/NAK 的解析逻辑发送方主循环通常是一个while循环从文件读一块数据打包发送等接收方的确认根据确认类型决定继续发下一条还是重发当前这条。伪代码层面的结构大致如下while not file_finished: data file.read(CHUNK_SIZE) pkt make_pkt(seqseq_num, datadata, checksumchecksum(data)) udt_send(pkt) rcv_pkt rdt_rcv() # 阻塞等待接收方反馈 ack extract_ack(rcv_pkt) # 解析确认分组 if corrupt(rcv_pkt) or ack (1 - seq_num): # 校验失败或确认号不匹配重发当前分组不要推进 seq udt_send(pkt) elif ack seq_num: seq_num 1 - seq_num # 确认正确序列号翻转 0/1 continue算法逻辑的要点在于发送方只有在收到和自己当前序列号一致的 ACK 时才推进序列号ACK 不匹配或者校验失败就原地重传。这看起来简单但它是所有可靠传输协议的基石——你永远不该假设网络会给你想要的响应一切进展都以确认到达为唯一依据。所谓 RDT2.0 的“停等”就体现在这里发一条等一个反馈过了才发下一条。参数CHUNK_SIZE是每次从文件读出的字节数也是单个分组携带的数据载荷大小。教学实现里常见的是 1024 或 4096。这个值决定了两点一是分组的最大尺寸二是文件传输需要多少轮停等。调大CHUNK_SIZE能减少轮次但单个分组在模拟丢包时损失更大调小则更贴近 TCP MSS 的语义。动手实验时建议试试 512 和 4096 两种值对比传输耗时直观感受停等协议对分组大小的敏感度。4.3 接收方写入文件可能遇到的粘包与半包边界接收方的核心逻辑是解析分组头重算校验和核对序列号然后把数据写入输出文件。如果校验失败或序列号重复回 NAK 或回一个重复 ACK。def receiver_loop(sock, out_file, expected_seq0): while True: pkt, addr sock.recvfrom(MAX_PKT_SIZE) seq, checksum_val, data unpack_pkt(pkt) if checksum(data) ! checksum_val: # 分组损坏要求重传 send_ack(addr, ack1 - expected_seq) elif seq ! expected_seq: # 序列号不匹配说明是重复分组回 ACK 但不写数据 send_ack(addr, ackexpected_seq) else: out_file.write(data) expected_seq 1 - expected_seq send_ack(addr, ackseq)关键逻辑是“损坏分组不写文件、重复分组不重复写”。这两个分支缺一不可少了损坏处理坏数据会落盘少了重复处理文件大小会膨胀。这里最隐蔽的坑是recvfrom的缓冲区大小。如果MAX_PKT_SIZE设得比发送方的实际分组小UDP 会截断数据接收方解析时直接报错如果设得过大又不影响正确性只是内存浪费。检查收发双方的MAX_PKT_SIZE是否一致是连接异常时首先该做的事情。TCP 编程里的粘包问题在 UDP 模拟里不太容易复现但如果你把这份代码改造成 TCP 版用TCP_NODELAY关闭 Nagle 算法后连续 send就会遇到多条数据黏在一起到达接收方不知道每条消息的边界。解决思路通常是在每条消息前加上 4 字节长度头或者用固定分隔符。RDT2.0 的 UDP 实现天然规避了这个问题因为recvfrom每次恰好取回一个完整数据报。5. 避坑与排查六次让人翻车的本地实验记录5.1 发送端报ConnectionRefusedError现象发送端启动后立刻退出报错说目标端口没有进程监听。原因接收端还没启动或者接收端已经崩溃。TCP 三次握手根本无法开始因为服务端没有在 listen 状态。常见顺序错误是在没启动接收端的情况下直接运行发送端。解决先确认接收端在跑netstat -an | grep 8888能看到LISTEN状态的端口。如果确认接收端在监听仍然报错检查发送端的--host参数是不是写成了局域网 IP 或主机名。本机实验一律用127.0.0.1别让 DNS 解析和防火墙插一脚。5.2 接收端报Address already in use现象第二次运行接收端时bind 失败提示端口已被占用。原因上一次运行没有正常退出socket 处于 TIME_WAIT 状态或者有别的进程抢先占用了端口。TIME_WAIT 是 TCP 四次挥手后的必经状态连接主动关闭方要等 2MSL 才能释放端口这是协议设计的防御机制不是程序 bug。解决临时换端口最省事或者用SO_REUSEADDR选项。在代码里sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)可以允许立即复用端口。命令行层面可以查占用进程lsof -i:8888后 kill 掉残留进程。这个坑在写任何 TCP/UDP 服务时都会遇到属于必踩项目。5.3 传大文件时程序卡死日志停在某条重传现象传几百字节的小文件毫无问题传几 MB 的文件时发送端卡住不动接收端日志也不再推进。卡住的位置固定在某一个序列号上。原因这份代码如果停留在 RDT2.0 水平就完全没有超时重传机制。信道只要丢一个分组发送方永远等不到 NAK接收方也永远收不到后续数据双方进入死锁。丢包率越高卡死概率越大。解决这是 RDT3.0 该干的活——发送端在发出分组后启动一个定时器超时未收到确认就重传。RDT3.0 的伪代码如下import threading def start_timer(timeout0.5): t threading.Timer(timeout, timeout_handler) t.daemon True t.start() return t def timeout_handler(): # 超时未确认重发当前分组并重置计时器 udt_send(current_pkt) start_timer()注意超时值必须大于 RTT否则没等确认到达就盲目重发产生大量重复分组把信道打满反而拖慢传输。0.5 秒是本地环回loopback的合理估计实际局域网或跨机实验需要根据 ping 延迟调整。5.4 收到文件但校验不一致二进制内容对不上现象接收端显示文件传输完成但diff源文件和接收文件发现多处不一致或者文件大小刚好差几个字节。原因多半是接收方写入数据时没有正确处理分块边界。比如某些数据块因为校验失败被丢掉了但接收方没回 NAK发送方以为已经送达或者是文件写入模式用了文本模式在 Windows 上把\n转成了\r\n等于数据被偷偷改写了。解决文件写入必须用二进制模式open(received.bin, wb)读取源文件同样用rb。校验环节如果发现损坏必须回 NAK 并丢弃当前分组绝不能把校验失败的数据写进文件。检查日志中是否存在“checksum mismatch”但文件仍被标记为完成的情况如果存在说明接收方的状态机有漏分支。5.5 抓包能看到数据但接收端收不到现象用 Wireshark 在 loopback 接口抓包能看到发送端发出的数据包在网络上出现但接收端应用层始终没有处理。原因最常见的是防火墙拦截了环回接口以外的流量或者接收端 socket 绑定的 IP 不是预期的那个。如果发送端连接的是192.168.x.x而接收端 bind 的是127.0.0.1内核会直接丢弃到达的数据因为 IP 层根本不知道该交给哪个 socket。解决接收端用bind((0.0.0.0, port))监听所有接口发送端统一用127.0.0.1连接。这样无论数据走哪个回环地址都能被接收。另一个隐含坑是 Wireshark 抓 loopback 需要安装 Npcap 并勾选 loopback adapterWindows 上常见抓不到回环包。5.6 zip 文件解压后缺少某个模块文件现象import rdt_utils直接报ModuleNotFoundError或者运行receiver.py提示找不到packet.py。原因压缩包解压不完整常见于 Windows 资源管理器对 zip 内文件名的编码兼容问题尤其是压缩包内包含中文文件名或特殊字符时。另一个可能是解压工具把文件路径截断了导致模块文件嵌在深层目录里没被看到。解决用命令行工具重新解压并检查目录结构。确保python3 receiver.py的执行目录和模块文件在同一个目录下。如果模块文件在子目录里可以把自己的脚本放到子目录执行或者用sys.path.insert(0, ./subdir)把模块路径加进来。这个坑和 TCP 本身无关但每个拿到 zip 包的人都会遇到值得留个心眼。6. 把 RDT2.0 往真实 TCP 靠用抓包与协议改造验证你的理解当 RDT2.0 的代码已经能稳定跑完小文件和大文件传输后接下来值得做的不是急着学新协议而是把“可靠传输最小闭环”和真实 TCP 的对应关系在 Wireshark 里对照着看一遍。启动接收端监听 8888 端口发送端传一个文件同时用 Wireshark 抓取 loopback 流量。把过滤条件设为tcp.port 8888你看到的将是完整的 tcp 三次握手和四次挥手SYN、SYN-ACK、ACK 建立连接FIN、ACK、FIN、ACK 挥手结束中间夹杂着数据段和对应的 ACK 段。对照 RDT2.0 的代码你能在抓包里找到它对应的行为——校验和失败时的 NAK 重传在 TCP 里表现为快速重传UDP 模拟的丢包在 TCP 里对应的是超时重传0/1 序列号在 TCP 里变成了 32 位的绝对序列号。我通常会做这样一个对照实验来验收自己是否真的理解了这份代码给receiver.py加一行日志打印每个分组的序列号、校验和计算结果、写入文件的字节数发送端同样打印每个分组的序列号、是否收到 ACK、重传次数。跑完一个文件后手动核对收发两条日志是不是一一对应的。如果发送端重传了 5 次接收端日志里就应该能看到 5 次校验失败或序列号不匹配的事件。这种端到端的日志验证比任何单元测试都更能证明协议状态机是正确的。进一步的改造实验有两个方向值得投入。第一个方向是给接收端加上批量 ACK 能力接收方不再对每个分组单独回 ACK而是每收到两个分组才回一条确认确认号表示期望的下一个序列号。这会让发送端多等一个分组的时间但同时减少 ACK 分组数量观察吞吐变化能帮助你理解 TCP 延迟确认delayed ACK机制。第二个方向是引入超时重传并把停等改成流水线维护一个发送窗口允许连续发送 N 个分组而不等待 ACK再为每个分组管理独立的超时计时器。这个改造工程量大但做完之后你再回头读 TCP 的拥塞控制会顺畅得多。最后想说一个习惯问题这类协议代码的调试不要靠print猜养成先开 Wireshark 再复现问题的习惯。你看到的每一个重传、每一次重复 ACK背后都有协议状态机的一次具体状态迁移。理顺状态迁移比记住任何参数都管用。希望这篇拆解能帮你少踩几个坑把 TCP-RDT2.0.zip 里这份代码真正变成自己的东西。本文还有配套的精品资源点击获取