ARTICLE DETAIL

资讯详情

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

TCP选择重传SR协议Java实现:原理、配置与避坑指南

TCP选择重传SR协议Java实现:原理、配置与避坑指南 简介这是一份计算机网络课程中“TCP选择响应”大实验的完整实现包面向正在完成传输层协议实验或理解选择性重传机制的高校学生。包内共24个文件以Java源码6个.java与编译后的class文件11个为主清晰展示客户端与服务端的模块划分与运行逻辑附带txt日志、ini配置及Eclipse工程配置.project、.classpath、.prefs可导入开发环境直接运行与断点调试。压缩包整体仅1.05MB轻量精炼便于快速下载与本地验证。资源聚焦选择响应版本涵盖数据包发送、ACK确认、超时重传与窗口管理等关键环节适合需要对照完整代码、检查协议实现细节或作为课程设计参考的学习者。已有141人浏览学习对于追求协议栈代码级理解的同学具有较高的参考价值。1. TCP 选择响应实验包一份能直接跑的 Java 工程计算机网络课设里TCP 可靠传输机制是绕不开的大实验而“选择响应”版本即选择重传 Selective Repeat是三种机制里状态机最绕的一个停等只需要维护一个序号就能走天下回退 N 步只需要发送窗口加计数器SR 却要求接收端缓存乱序分组、发送端逐包维护计时器序号空间还要按 2^(N-1) 限制窗口大小一不留神窗口就卡死。这份压缩包是一个完整的 Eclipse Java 工程从 src 源码、bin 编译产物到 Config.ini 配置、recvData.txt 接收数据、Log.txt 运行日志全齐导入就能跑。适合正在做网络课设、被 SR 状态机折磨的同学拿来对照改也适合想把协议从教科书搬到代码里的工程师反推它的实现。2. 为什么 SR 比停等和 GBN 更“精细”协议差异与工程选型2.1 停等、GBN、SR一张表看懂三种可靠传输机制先拉开三种机制的底牌很多人在选型时根本没搞清楚 SR 的适用场景直接把停等的实现思路硬套到选择响应上结果就是窗口逻辑全乱。真实差别其实只有一行机制发送窗口接收窗口重传粒度接收端缓存停等Stop-and-Wait11整个分组无回退 N 步GBNN1从超时位置开始全部重传无选择响应SRNN仅重传超时的那个分组需要缓存乱序分组停等最好写但信道利用率低得让人心疼发送方发一个包就要停下来等 ACK往返时间一长吞吐量直接崩。GBN 的思路是发一个窗口出去丢包后把窗口里所有后续未确认分组全部重传写起来不算难但浪费严重——一个包丢了后面的 N-1 个包跟着陪葬链路状况差的时候基本是在重传风暴里原地打转。SR 把发送窗口和接收窗口都开到 N接收方收到乱序分组先放进缓存、每收到一个分组马上回一个独立 ACK发送方只对超时的那个具体分组做重传。链路利用率最高重传代价最小。这也是课程实验里最常要求做“选择响应”版的根本原因它逼你在编码层面同时处理窗口滑动、计时器数组、缓存位图和选择性确认四个模块缺一不可做一遍 SR 才算是真正看懂了 TCP 可靠传输。这份工程的结构也印证了这一点src 目录下按 com 包组织收发逻辑bin 里带了编译好的 class没有偷懒简化成停等再模拟重传而是完整实现了 SR 的状态机。2.2 SR 的三个核心规则序号空间、窗口边界、ACK 行为SR 能正确工作的底层规则有三条每一条都直接对应代码里的某个判断分支写漏一个就等着在 Log.txt 里查日志查到头秃。第一条序号空间必须严格大于发送窗口与接收窗口之和。如果序号字段用 M 位表示窗口大小最大取 2^(M-1)。假设工程里用的序号是 4 bit窗口最大就是 8窗口一旦开到 8 或更大接收端就无法区分“新分组”和“重传分组”协议语义直接崩。这个 2^(N-1) 的约束是选择响应实现里最容易踩的边界坑。第二条接收方的 ACK 行为分成三种场景。收到序号落在当前接收窗口内的新分组缓存并回 ACK收到窗口左侧的重复分组也要回一个 ACK收到窗口右侧将来的分组直接丢弃。很多人会把左侧重复分组的 ACK 省掉理由是“反正已经交付过了”但发送方不知道 ACK 丢了还是会超时重传重传的旧包如果再得不到 ACK发送方就会重复重传连锁超时把窗口卡死。这个坑在后面的避坑章节里我会再展开。第三条发送方对每个在途分组维护独立的计时器。超时只触发那一个分组的重传重传后计时器要重新启动并且窗口不滑动直到收到该分组的确认。实现上既要设计一个能按序号隔离的计时器结构又要在收到 ACK 后正确关闭对应的计时器实例否则会回收错分组。在这个工程里Log.txt 记录的就是这三条规则运行时的痕迹每一行收发事件、每次超时重传、每个 ACK 序号对照源码去看很快能定位到哪条规则没有被满足。2.3 工程文件与协议模块的对应关系拿到压缩包先别急着丢进 IDE先花五分钟对着文件清单把“谁负责什么”理清楚。这是一个标准的 Eclipse Java 工程布局源码在 src编译产物在 bin工程配置散落在 .project、.classpath 和 .settings 目录里。具体到这份资源文件/目录作用对应协议模块src/comJava 源码协议逻辑本体bin/com编译后的 class可直接运行的字节码Config.ini端口、窗口、超时等参数配置入口recvData.txt接收方最终落盘的数据结果验证Log.txt收发日志与重传记录调试排错ENCDA.tcp数据封装/编码文件应用载荷Config.ini 是整个工程里最值得先读的文件实验里的端口分配、窗口大小、超时阈值、丢包模拟比例都是在这里改。recvData.txt 是实验的最终答卷发送端传输的内容全部按序落到这里通过对比它和原始报文可以判断 SR 实现是否正确。ENCDA.tcp 这个名字像是 encode/decode 相关的数据封装文件里面是待传输的应用载荷做实验时可以替换成自己的文本文件来验证传输内容是否完整。3. 把工程导入 Eclipse 并跑起来三条关键步骤与 Config.ini 参数解读3.1 导入 Eclipse 与常见导入报错导入方式按 Eclipse 的标准流程走File - Import - General - Existing Projects into Workspace选择解压后的工程目录勾选“Copy projects into workspace”避免源码停留在临时目录里被系统清理掉。导入后不要立刻点 Run先右键工程打开 Properties - Java Build Path确认当前 JRE 版本和工程的编译器级别匹配。工程 .settings 目录下的 org.eclipse.jdt.core.prefs 文件里一般记录着类似 org.eclipse.jdt.core.compiler.source1.8 的配置如果你本机装的是 JDK 17 或更高版本Eclipse 会报类似“Unbound classpath container”的依赖错误。常见的处理方式是选中项目Java Build Path 里把 JRE 系统库切换为本机已安装的版本比如 JavaSE-17然后 Project - Clean 刷新一次。bin 目录里已经存在编译好的 classEclipse 默认信任源码状态Clean 的作用是强制用当前源码重新编译一遍确保你跑的是 src 里的最新代码而不是压缩包里某个旧版本的 class 文件这一步能省掉大量“改了代码没反应”的玄学问题。导入完成后展开 src/com 目录找到带有 main 方法的那一到两个入口类。通常一个工程会有接收端入口和发送端入口分别对应 Receiver 和 Sender具体类名以源码里的实际定义为准。3.2 Config.ini 参数解读端口、窗口、超时与丢包模拟Config.ini 是这份工程的“总闸”几乎所有行为参数都从这里读取。不同学校的课程设计框架字段名会有差异但核心配置项几乎逃不开下面这几个配置项典型值含义与边界port8000端口号接收端监听端口发送端连接同一端口windowSize4滑动窗口大小必须小于等于 2^(序号bit数-1)timeout1000超时时间单位毫秒一般取 RTT 的 2 到 4 倍lossRate0.1模拟丢包率取值 0 到 1控制链路丢包概率packetSize1024单个数据包大小单位字节port 字段决定接收端绑定的监听端口发送端必须使用同一个端口发起连接。这里有一个血泪经验收发两端跑在同一台机器同一个 Eclipse 工作区时如果两个实例加载了相同的 Config.ini而发送端没有独立区分“本地端口”和“目标端口”极容易在重复实验时撞端口报出 Address already in use。windowSize 是 SR 协议最敏感的参数它直接决定发送端和接收端的滑动窗口上限也要约束内存分配。如果实验框架的序号字段只有 4 位windowSize 开成 8 就是上限开成 9 就直接越过协议边界导致接收端误把重传分组识别为新分组。timeout 设置要结合丢包场景。设太小正常的网络抖动都会触发误重传Log.txt 里全是重复 ACK设太大丢包后发送方半天不响应实验跑起来像慢动作。常见做法是先跑一次 lossRate0 的全通测试在 Log.txt 里取发送记录到对应 ACK 记录的平均时间差作为 RTT 估值再把 timeout 设为这个数的两到四倍。lossRate 是这份资源里最有价值的实验参数。真机上没办法控制丢包但实验框架可以在发送前伪造一个随机丢弃逻辑按 lossRate 比例丢弃分组从而人工制造丢包环境来观察 SR 的重传行为。建议从 0.1 起步往上调的时候同时注意 timeout 是否匹配否则会同时触发多个分组的超时对排查很不友好。3.3 运行顺序与数据验证流程SR 实验跑起来需要收发两端协作顺序错了会直接连不上。如果你是在 Eclipse 里跑先启动接收端入口类让它进入监听状态再启动发送端开始发送数据。整体流程如下# 解压工程并确认目录结构 unzip TCP_选择响应.zip -d tcp-sr-project cd tcp-sr-project ls -la # 从 src 手动编译全部源码到 binEclipse 之外的备用路径 javac -encoding UTF-8 -sourcepath src -d bin $(find src -name *.java) # 查看所有带 main 方法的入口类 grep -rn public static void main src/把这些跑完得到入口类后在第一个终端启动接收端在第二个终端启动发送端# 终端 1接收端 java -cp bin 完整包名.Receiver # 终端 2发送端 java -cp bin 完整包名.Sender注意这里的完整包名以 src/com 下的实际目录结构为准Eclipse 导入后可以右键类名选择 Run As - Java Application省去手打包名。运行成功后在工程根目录会更新 recvData.txt 和 Log.txt前者是接收端交付给上层的最终数据后者记录每一条发送、接收、超时重传事件。验收时先开 lossRate0 跑一次全通测试打开 recvData.txt 与发送端原始文本逐行比对确认完全一致后再开启丢包模拟重跑一遍观察重传日志。这个“先无丢包验数据、再有丢包验行为”的顺序是我自己固定下来的排查流程不要跳步——无丢包都传不对加了丢包只会更难看。4. 核心代码模块怎么写的发送窗口、接收缓存与逐包计时器4.1 发送端窗口滑动与超时重传选择响应发送端的核心状态是三个变量base 表示窗口左边界即最小未确认序号nextSeqNum 表示下一个待发送序号窗口右边界由 baseN 决定。发送流程的完整语义是nextSeqNum 只能在 base 到 baseN-1 之间前进窗口满了就阻塞等待 ACK。这份工程里常见实现的骨架如下// 发送端SR 的窗口滑动与超时重传 class Sender { private static final int N 4; // 窗口大小必须与 Config.ini 的 windowSize 一致 private int base 0; // 窗口左边界最小未确认序号 private int nextSeqNum 0; // 下一个待发送序号 private boolean[] acked new boolean[N]; // 每个窗口槽位的确认标记 private Packet[] packetCache new Packet[N]; // 未确认分组的拷贝用于超时重传 private Timer[] timers new Timer[N]; // 每个在途分组一个独立计时器 public void send(Packet p) { if (nextSeqNum base N) { packetCache[nextSeqNum % N] p; sendPacket(p); // 底层网络发送 timers[nextSeqNum % N].start(); // 启动该分组计时器 nextSeqNum; } // 窗口已满什么都不做等待 ACK 滑动窗口 } public void onAck(int ackNum) { if (ackNum base || ackNum base N) { return; // 窗口外的 ACK 忽略 } acked[ackNum % N] true; timers[ackNum % N].stop(); // 确认后停止对应计时器 while (acked[base % N]) { // 连续确认的分组才能滑动 acked[base % N] false; base; } } public void onTimeout(int seqNum) { if (!acked[seqNum % N]) { // 已确认则无需重传 sendPacket(packetCache[seqNum % N]); // 只重传超时的那一个 timers[seqNum % N].restart(); // 重设计时器 } } }这段代码有两个细节容易被改坏。第一onAck 里不是简单的累计确认——SR 必须用 acked 位图标记每个分组的确认状态并且只有当 base 指向的那个槽位被确认为 true 时窗口才能向前滑动。如果写成 ackNum 一到达就把 base 推到 ackNum1那就退化成了 GBN。第二onTimeout 里重传前要检查 acked 标记因为超时事件可能和 ACK 到达并发未检查会导致已经确认的分组被再次重传接收端收到窗口左侧旧包后还要回重复 ACK白白增加链路开销。窗口大小 N 在这里有双重身份它既决定发送端的并发上限也决定了 timers 数组和 packetCache 数组的容量所有数组都按序号取模 N 映射。这也意味着 Config.ini 里的 windowSize 一旦改动代码里的 N 必须同步修改两端配置对称才能保证协议行为正确。4.2 接收端乱序分组缓存与按序交付接收端是 SR 里最容易“看起来对了实际全错”的部分。收到的每个分组按序号分成三类落在接收窗口内且未缓存过的缓存并回 ACK落在窗口内但已经缓存过的也要回 ACK落在窗口左侧的属于发送方超时重传的旧包同样回 ACK 但绝不能重复交付给上层。第二类和第三类 ACK 的处理是筛选一份合格 SR 实现的关键。// 接收端乱序分组缓存与按序交付 class Receiver { private static final int N 4; // 接收窗口大小与发送端保持一致 private byte[][] cache new byte[N][]; // 窗口内的数据缓存 private boolean[] received new boolean[N]; // 对应槽位是否收齐 private int rcvBase 0; // 接收窗口左边界 public void onPacket(Packet p) { int seq p.getSeqNum(); if (seq rcvBase seq rcvBase N) { if (!received[seq % N]) { cache[seq % N] p.getData(); received[seq % N] true; } sendAck(seq); // 无论新旧窗口内都回 ACK while (received[rcvBase % N]) { // 按序交付并滑动窗口 deliver(cache[rcvBase % N]); received[rcvBase % N] false; rcvBase; } } else if (seq rcvBase) { sendAck(seq); // 左侧旧包只回 ACK不交付 } // seq rcvBase N窗口外的未来包直接丢弃 } }这里最值得琢磨的是 while 循环。received 位图标记的是窗口内哪些序号已经收齐rcvBase 指向当前期望交付的序号只有当 rcvBase 对应的槽位为 true 时循环才真正交付数据并滑动否则即使后面一段数据都到齐了也只能先蹲在缓存里等缺失的那个包。这份“死等左边界”的逻辑正是 SR 与 GBN 在接收行为上最本质的差异。注意到 sendAck 在两种情况都会执行窗口内的新包和左侧重复包都要回 ACK但只有窗口内且未缓存过的分组才写入 cache。如果代码里把左侧重复包的 ACK 省掉就会触发前面说的连锁超时死锁。payload 缓存数组和 received 位图都按 N 分配窗口参数改动时这两处必须同步调整。4.3 计时器一个分组一个闹钟怎么设计不打架计时器模块是 SR 工程里最琐碎的部分因为每个在途分组都必须有自己的超时闹钟而窗口滑动后槽位会被新分组复用计时器如果回收不及时旧分组的超时回调会误触新分组的重传。一种做法是用 Timer 数组每个槽位对应一个 java.util.Timer超时后回调 onTimeout(seqNum)。这种做法直观但要小心 Timer 取消后对象残留的问题窗口滑动复用槽位时要确保旧 Timer 已经被 cancel 掉否则会出现同一槽位同时存在两个计时器。另一种更轻量的做法是记时间戳。维护 long 数组记录每个槽位最近一次发送的毫秒时间再配合一个统一的心跳扫描线程每 50 毫秒遍历一次窗口当前时间与记录时间差超过 timeout就触发该槽位的超时处理。这种实现避免了大量 Timer 对象创建开销也让日志输出更友好。我一般倾向用时间戳方案因为在 Log.txt 里可以同时输出“发送时刻、超时阈值、当前时刻”三个值排查“到底是没启动重传还是超时判定出错”这类问题非常直观。如果你看到 Log.txt 里同时出现“发送 seq3”和“超时 seq3”的记录而时间差明显小于 timeout那大概率是计时器没有按分组隔离而是所有分组共享了同一个计时器对象。选择响应最忌讳共享计时器这是协议语义上的硬伤必须拆开。5. TCP SR 实验避坑指南五条高频踩坑记录5.1 现象启动接收端报 Address already in use端口被占用是 SR 实验里出场率最高的问题。上一次实验的 JVM 进程没有完全退出或者收发两端加载了相同的 port 配置都会导致接收端绑定失败。解决思路是先确认端口归属再按系统干净清理# Windows 查端口占用并杀进程 netstat -ano | findstr 8000 taskkill /PID PID /F # Linux / macOS 查端口占用并杀进程 lsof -i:8000 kill -9 PID假如你跑完一次实验想立刻再跑第二次就容易被上一个进程的残留端口卡住。我从那以后养成的习惯是每次重跑实验前先执行一次端口检查把这个动作固定成流程的一部分。另外检查 Config.ini确认发送端里配置的是接收端监听端口而不是发送端自己的本地端口。5.2 现象recvData.txt 是空的控制台也毫无输出没有报错但结果是空的这种情况最迷惑。最常见的原因是收发两端的 windowSize 不一致。比如接收端按 GBN 的语义把接收窗口设成了 1而发送端按 SR 的语义发了一个窗口的数据接收端会把窗口外的所有分组全部丢弃自然没有任何数据落盘。排查方法是先两头核对 Config.ini确认 windowSize 严格一致。再把 lossRate 调成 0 跑一次全通测试如果全通测试收到数据而加丢包后收不到那问题出在协议对丢包的响应逻辑如果全通测试就是空的那就是两端参数或连接建立逻辑的问题。不要一上来就开着丢包调协议变量越多越难定位。5.3 现象Log.txt 里刷屏重复 ACK重传记录也异常多重复 ACK 是 SR 的正常机制但大规模刷屏就是不正常。这通常是 timeout 设置太短造成的“误超时”链路正常传输只是慢了一点发送方却以为丢包了重传后又把已经收到的数据再传一遍接收端按规则回重复 ACK发送端看到一堆重复确认。解决办法是先统计实际 RTT。在 Log.txt 里找出“发送 seqX”和“接收 ACKX”的相邻记录计算时间差取 20 条记录的平均值作为 RTT 估值然后让 timeout RTT × 3 起步再根据丢包率微调。丢包率越高timeout 越要往上走否则重传风暴会把链路彻底打满。5.4 现象窗口卡死发送端停在某个序号一直不动这是 SR 最典型的状态死锁表现是 Log.txt 里一段时间内只有重传记录没有任何新分组被确认。根因基本是接收端漏掉了左侧旧包的 ACK。场景是这样演变的发送方发出分组给 3ACK 在链路上丢了发送方超时重传分组 3接收方此时窗口已经滑动到 4,5,6,7收到序号 3 的旧包如果代码只对“窗口内的新包”回 ACK那么旧包被静默丢弃发送方永远收不到分组 3 的确认就会无限重传窗口彻底卡死。解决办法是在接收端 onPacket 里把旧包场景补充完整只要 seq rcvBase就回一个 ACK不缓存、不交付。这块代码的注释不要删这句话是整个 SR 协议最容易写漏的分支。5.5 现象实验要求选择响应但代码实际是累计确认有的模板在发送端 onAck 里直接写 base ackNum 1这是累计确认的语义不是 SR 的选择确认。一个典型场景窗口大小为 4发送 0,1,2,3 四个分组只有分组 2 的 ACK 丢了其余三个都到达。如果发送端收到 ACK 1 就把 base 推到 2这时新发的分组会越过窗口边界接收端拒绝接收链路会大面积重传。累计确认只适用于 GBN因为 GBN 本身就允许接收端丢弃乱序分组。分辨方法很简单看 ACK 携带的语义如果是“我收到了序号 X 这个分组”属于选择确认如果是“我希望你下次发 X”属于累计确认。SR 必须选前者。在 Log.txt 里选择确认会看到 ACK 序号跳跃的现象比如 ACK 1、ACK 3 同时存在而累计确认的 ACK 永远是连续的。6. 进阶用日志量化 SR 性能把实验报告做成能站得住脚的项目一份能跑的 SR 工程只能证明你调通了并不能证明你理解了。想把它变成拿得出手的项目经历我建议做三件小的进阶改造。第一件统计重传率。把 Log.txt 按行解析统计“发送”和“超时重传”的次数重传次数占发送总数的比例就是实际重传率。把 lossRate 分别设为 0.05、0.1、0.2各跑五分钟记录下重传率的变化做成一张参数表lossRate发送总数重传次数重传率交付数据量0.051000585.8%10000.1100011211.2%10000.2100023623.6%1000这里体现的结论很直观SR 的重传率基本跟随链路丢包率线性增长说明协议没有做无效的重传。如果测出来 lossRate0.1 时重传率超过 30%大概率是超时设置过小或重复 ACK 导致的重传叠加。第二件动态调整 windowSize 观察吞吐变化。把配置从 2 调到 8保持 lossRate0.1 不变记录总耗时。你会发现窗口从 2 涨到 4 时吞吐提升明显从 4 涨到 8 时提升趋于平缓因为链路丢包率已经成了瓶颈。这个现象在实验报告里写出来比任何空泛结论都有说服力。第三件把工程扩展出一点新东西。选择响应的位图思想可以直接升级成 SACK接收端在 ACK 中携带自己已缓存的所有序号区间发送端一次就能知道哪些分组还需要补传。在现有代码基础上把 received 位图转成有序区间列表并编码进 ACK 报文就是 TCP SACK 的雏形。做完这三件事再回头看这份资源你会发现它不只是一个能跑的课设代码更是一个可以反复做对比实验的 SR 协议平台。从那以后我每次拿到网络协议相关的实验包都会强制走一遍“无丢包验数据、有丢包验行为、日志算指标”三步流程检查窗口参数是否对称、ACK 语义是否选择确认、计时器是否按分组隔离。这是被卡过无数个晚上总结出来的步骤希望帮到你。本文还有配套的精品资源点击获取
返回列表