计算机网络实验:从RDT到Reno拥塞控制的日志分析方法
2026/9/18 19:31:41 网站建设 项目流程

简介:这份文档是关于TCP协议迭代开发的计算机网络实验报告,适合正在学习传输层协议、需要完成网络实验或大作业的本科生。报告围绕RDT 2.0、RDT 2.2、RDT 3.0逐步展开,分别说明位错检测、ACK校验和超时重发机制,再延伸到选择响应协议与Reno拥塞控制中的慢开始、拥塞避免、快恢复和乘法减小。内容结合代码与LOG文件分析实现效果,并记录真实遇到的关键困难——如无法直接从日志看出慢开始与拥塞避免阶段,于是采用每轮传输结束后输出当前发送包序号、接收包序号和cwnd值的方式,动态观察拥塞窗口变化。压缩包仅含1个doc文件,总大小945KB,虽内容精炼但涵盖了从基础错误检测到复杂拥塞控制的完整链路,并附有对迭代开发优缺点的反思和实验系统改进建议。目前已有291人学习下载,对于想把握RDT演进脉络、排查拥塞控制状态、提升实验报告撰写深度的读者,有直接借鉴意义。

1. 在一份计算机网络实验报告里,先把传输层状态调到可见再动手

拿到这份实验报告时,多数人第一反应是把 RDT 2.0 到 3.0 的状态转移图背下来,再照着写代码。真正交上去才发现,代码跑通了,log 文件里却看不出任何阶段边界,老师要求的"结合代码和 LOG 文件分析解决效果"成了硬伤。这份报告把位错、ACK 错、发送端发错、选择响应和 Reno 拥塞控制放在同一条迭代链上,每一步都要对应一个可观测的协议状态。与其依赖对可靠传输机制的模糊记忆,不如先设计三个输出字段:包序号、校验结果、当前拥塞窗口,后面所有实验现象都能迅速归位。适合正在做计网大作业、期末复习和准备 408 的人,配合谢希仁《计算机网络》第八版传输层章节来读,效果更直接。

2. RDT 2.0/2.2/3.0 的代码逻辑:位错、ACK 错与发错包的判定点

2.1 接收端校验流程:校验和与序号绑在一起算

RDT 2.0 的信道假设是数据可能翻转、ACK 不会坏,所以接收端只需对收到的数据包重算校验和。RDT 2.2 把 ACK 包也丢进不可靠信道后,发送端必须对收到的 ACK 做相同校验,否则一个翻转的 ACK 会触发错误发送,接收端又没有手段分辨这是一个新包还是重传包。因此 2.2 的实质不是多一次校验,而是把序号纳入了校验范围,让接收端能识别重复包。

接收端骨架如下,把 2.0 到 2.2 的边界写在注释里,是我在做这个实验时保留的最小结构:

def rdt_rcv(pkt, expected_seq): # 校验和覆盖 seq 和 payload,避免数据位错时序号恰好翻成合法值 if checksum(pkt.seq + pkt.payload) != pkt.checksum: return NAK(pkt.seq) # RDT 2.0:检测到位错就要求重发 if pkt.seq != expected_seq: # 校验正确但序号不是期望的值,说明是重复包或发错包 return ACK(pkt.seq) # RDT 2.2:丢弃数据但仍确认,让发送端推进 deliver_to_upper(pkt.payload) return ACK(pkt.seq)

在 RDT 2.0 场景里,expected_seq可以不引入,因为停等协议下前一个包未确认前不可能发下一个。到 2.2 就要把expected_seq用上,否则旧包重传会被当成新数据交付给上层。NAK在真实 TCP 中并不存在,TCP 用"不确认期望序号"表达同样的语义,实验课保留NAK主要是为了把错误和重传两条路径分开观测,log 里查NAK计数比查ACK缺失直观得多。

2.2 发送端三种等待状态:从等 ACK 到等定时器

发送端的差异看下表最清楚,这也是我写实验报告时 log 字段设计的依据:

版本信道假设触发重发的条件log 至少记录
RDT 2.0数据可能位错收到 NAK收到包的校验结果
RDT 2.2数据、ACK 都可能位错等待定时器,ACK 校验失败当没收到ACK 校验失败次数
RDT 3.0数据可能位错或丢失定时器超时超时瞬间的包序号

RDT 2.0 的发送端很省事,rdt_send发出一个包后阻塞,等待接收端返回 NAK 或 ACK。到 2.2 有个容易写错的地方:收到校验失败的 ACK 时不能立刻重发,因为数据包可能已经安全到达,只是 ACK 回程坏了。立刻重发会让接收端收到重复包,虽然靠重复包丢弃机制能兜底,但网络里多出一圈无意义流量。正确做法是丢弃这个坏 ACK,让定时器超时后再重发,RDT 3.0 正是把这条链路补完整。

RDT 3.0 的典型故障是"发送端发错",比如定时器到期前收到旧 ACK,发送窗口推进停不下来,实际上线路里已经有一个包丢失。我在代码里用一个标志位timer_running来消除这类问题:

def rdt_send(data): sndpkt = make_pkt(seq, data) udt_send(sndpkt) if not timer_running: start_timer(rto) # 之后等待三类事件:ACK 正常、ACK 校验失败、超时

timer_running保证一条连接上同时只有一个定时器在跑。停等协议虽然简单,但很容易写成每次发送都重置定时器,把超时时间无限拉长,重发效率很低。验证方法是看 log 里相邻两行超时记录的时间差,若第一次 500ms、第二次 1000ms,基本就是定时器被反复重置了。

2.3 选择响应协议与 RDT 3.0 的重传粒度差异

报告中提及的选择响应协议,接收端"对每一个校验和正确的接收包都进行应答"。这句话点出了和 RDT 3.0 的本质差异:RDT 3.0 是停等,一个包要等到 ACK 才发下一个;选择响应允许发送窗口里存在多个未确认包,接收端缓存乱序到达的包。实验里常见的问题是拿 RDT 3.0 的代码直接改成选择响应,却依然只在收到 ACK 后按顺序推进发送队列,结果窗口开了,乱序缓存永远用不上,性能退化回停等。

这里要做两件事。第一,接收端维护一个窗口哈希表,按seq下标存放正确到达的包;第二,发送端每收到一个正确 ACK 就滑动窗口,不需要等待最小的未确认号。注意"对每个校验正确的包都应答"和 RDT 2.2 的"对重复包也应答"在字面上很像,但语义不同:选择响应确认窗口内各包,是为了不重传已正确的包;RDT 2.2 确认重复包是为了推进发送端状态并清掉定时器。前者必须逐包确认,后者要防 ACK 风暴。

3. RTT 估计与超时定时器:把重传从"猜测"变成可调参数

3.1 为什么 RDT 3.0 不能继续用固定超时

RDT 3.0 引入定时器后,第一个问题不是怎么写定时器,而是超时时间定多长。固定 500ms 的写法在本地回环演示没问题,但实验网多跳时 RTT 抖动很容易超过这个值,包没有丢却反复超时重传。反过来定 2s,链路真的丢包时恢复又太慢。这两类现象都会在 log 里出现,区别在于前者能看到大量重复包,后者能看到长时间的空洞。

计算机网络里的标准处理是维持两个状态量:平滑后的往返时间srtt和往返偏差rttvar,超时值取srtt + 4 * rttvar。实验报告虽然没有单独列这一节,但 Reno 实验要观察丢包后的恢复速度,这一步不能省。我的实现放在发送端接收 ACK 的函数里:

def update_rto(sample_rtt, sndpkt): # 重传过的包不计入 RTT,避免确认歧义 if sndpkt.retransmitted: return rto if srtt is None: srtt = sample_rtt rttvar = sample_rtt / 2 else: rttvar = 0.75 * rttvar + 0.25 * abs(srtt - sample_rtt) srtt = 0.875 * srtt + 0.125 * sample_rtt rto = max(200, srtt + 4 * rttvar) return rto

权重取 0.875、0.125 和 0.25 是 RFC 6298 的推荐值,老版本实现会用 0.9、0.1 和 0.2,实验结果差别不大。retransmitted标志很关键,如果重传包的 ACK 回来也算一次 RTT 采样,srtt就会抬高,之后超时时间越变越长。验证这个坑的方法:故意制造一次超时,然后观察后续 RTO 是否保持不变;如果连续增大,说明重传包混入了 RTT 计算。

3.2 定时器与重传包序号的对齐方式

日志里最能说明问题的是超时记录那几行。我一般把发送端事件分成三个级别记录:INFO 记录每个 ACK 的推进,WARN 记录校验失败的 ACK,ERROR 记录超时重传。这样排查问题时不会在几千行 INFO 里捞异常。一份可用的 log 至少要有以下字段:

字段含义判断依据
seq本次发送的数据包序号同一seq出现两次说明发生重传
ack当前确认号大于发送窗起点说明窗口推进
rto当前超时值连续丢包时观察是否按指数退避
src/dst收发端标识排查是否选中了错误链路

接收端如果发现seq跳变后又收到旧序号,应该把乱序和重复分成两种事件记录,而不是统一打一条DATA。这个细分的价值在做 Reno 时尤其明显:三个重复 ACK 可能是真丢包,也可能是乱序引起,log 里没有区分标识,就无法解释为什么进入快恢复的频率特别高。

3.3 超时退避的指数边界

RDT 3.0 和 Reno 里都涉及退避,但语境不同。RDT 3.0 的超时退避是二元指数退避,每次超时执行rto = min(rto * 2, 上限);Reno 的乘法减小是cwnd = max(cwnd / 2, 2),减的是发送速率而不是定时器。报告里"以上出错均能在超时之后重发"这句话,对应的就是二元退避不能无限增长,必须设上限。很多大作业卡在"超时后重发永远不成功",就是因为上限设得太大,测试时间根本等不到第二次重传。

我把退避上限作为配置项暴露在代码里,默认 1000ms,这样在局域网实验里可以让重传快速发生,又不被测试环境误判为无响应。真实 TCP 的 RTO 下限是 1 秒,那是面向跨公网场景设计,本地实验没必要照搬。

4. Reno 拥塞控制实验:从轮次日志里读 cwnd 和 ssthresh 的变化

4.1 慢启动与拥塞避免的边界判定

报告里写得最真实的一点:无法单独从 log 文件直接看出慢启动、拥塞避免等过程。原因很简单,协议代码只记录收发事件,它不会告诉你此刻处于哪个阶段;阶段是人和状态变量推断出来的。我的做法是每轮传输结束后用 Logger 输出一行汇总:

def end_of_round(snd_wnd, rcv_ack, cwnd, ssthresh): logger.info("round=%d snd=%d ack=%d cwnd=%d ssthresh=%d", round_no, snd_wnd.high, rcv_ack, cwnd, ssthresh)

这里的round指一个发送周期,我的实现中把它定义为从某个序列号开始、到这轮 ACK 推进到的位置为止。snd_wnd.high是本轮能发送的最大序号,rcv_ack是已确认序号,两者差值代表在途数据量。cwnd是端到端观测的核心变量:慢启动期间每一轮翻倍,ssthresh不变;拥塞避免开始后cwnd近似线性增长,但每一轮只加一个MSS。如果 log 只有包事件,这两者的差异会被淹没。

一个值得注意的点:慢启动不是到ssthresh才一刀切。Reno 的窗口增长与收到 ACK 的个数挂钩,所以每一轮实际增量由cwnd个 ACK 驱动。日志里观察到的现象是 cwnd 每轮从 1 变 2、4、8,直到超过ssthresh后变成 9、10、11 这类线性步进。

4.2 快重传和快恢复的触发条件

Reno 实验里最容易错的是把三个重复 ACK 当作普通乱序处理。正常乱序可能收到一两个重复 ACK,三个及以上就要进快重传:立即重传丢失包,把ssthresh减半,cwnd置为减半后的值加三个报文段。随后进入快恢复,每收到一个重复 ACK,cwnd临时加一个 MSS,收到新 ACK 后才退出到拥塞避免。这组动作在实现里的核心判断是:

if ack == last_ack: dup_ack_count += 1 if dup_ack_count >= 3: ssthresh = max(cwnd // 2, 2) # 乘法减小 cwnd = ssthresh + 3 # 快恢复起点,补偿已出发的重复 ACK retransmit(last_unacked_seq) # 快重传,不等超时 else: dup_ack_count = 0 # 收到新 ACK 后退出快恢复,回到拥塞避免的线性增长

dup_ack_count是连续相同确认号的计数,一收到更高的确认号就要清零。我见过同学的实现把它设为全局变量,但忘了在新 ACK 时清零,结果一个旧计数把后续连接带进快恢复。这里快恢复退出后的窗口更新,Reno 标准实现是让cwnd贴近ssthresh后线性增加,实际写法通常会维护一个cwnd累加器,避免每 ACK 都做一次浮点运算。

4.3 乱序包对 Reno 判定的干扰

把选择响应与 Reno 结合时,会出现一个有趣的矛盾:选择响应用窗口缓存乱序包,Reno 却把三个重复 ACK 当作丢包信号。假如网络只是乱序而没有丢包,接收端已经缓存了后面的包,发送端仍可能因重复 ACK 进入快恢复,白白降速。

处理这个问题不需要改 Reno 判定逻辑,而是调整接收端回 ACK 的策略:只要收到乱序包就立即回一个重复 ACK,让发送端尽早知道存在空洞,而不是等第三个重复 ACK 才行动。在真实 TCP 场景里还可以启用选择性确认选项,把接收端缓存的位图带回发送端,但基础实验通常不要求。日志里判断是否误判的方法比较简单:快恢复发生的时间点前后,如果后续包序号是连续的,说明线路没有真丢包,属于乱序触发;此时看ssthresh是否被无故减半,就能定位是接收端策略还是发送端判断条件的问题。

5. 报文级日志与校验观察:把传输层输出还原成协议语义

5.1 从报文观察 CRC/校验和是否生效

网上经常有人问计算机网络中的 CRC 校验如何通过报文观察,实验课里最直接的方式是人为翻转 payload 的一位,然后看接收端日志里checksum mismatch计数是否加一。注意 CRC 属于链路层,传输层的校验和并不采用多项式除法,但实验环境里两者经常混用。观察时要看校验字段覆盖的范围:传输层校验和必须包含伪首部字段,否则地址变化不会暴露出来。一个报文里如果有seqpayloadchecksum三个字段,接收端输出的重算值和包内值不同,就能定位是哪一位翻转:先固定seq重算一次,再固定payload重算一次,和包里值比较后就能把错误精确到序号部分还是数据部分。

5.2 用逐轮 cwnd 变化验证拥塞状态

一个人工验证 Reno 状态的技巧:把轮次日志里每行cwndssthresh整理成两列,手动描一个时间序列。cwnd 呈倍数步进的地方是慢启动,变线性步进的地方是拥塞避免,突然跌一半又恢复的地方是乘法减小加快恢复。这个方法可用于回答考试里"画 TCP 拥塞窗口随时间变化曲线"这类题,本质上和网桥转发表题目有共性:网桥通过源地址学习建立转发表,发送方通过 ACK 学习网络容量,动态表项都靠观察到达事件而不是预设值。理解这一点,比背实验现象更能应对期末复习中的变式题。网桥转发表的老化机制也拿来类比:表项长期未用会删除,TCP 的确认信息在一段时间内没有推进就要重发,两者都是基于超时和重复事件做决策。

5.3 把 log 分析映射到课本与 408 考点

回到报告最初的问题:log 文件和代码怎么结合着看。我建议按三个层次查看:代码层看状态分支,log 层看事件序列,汇总层看cwndssthresh。期末复习时,把谢希仁第八版里"可靠传输的实现"和"TCP 的流量控制"两章翻出来,对照 RDT 2.0/2.2/3.0 与 Reno 各自属于哪个层次;408 里常考的超时重传时间选择、拥塞窗口演化、快重传前提,都能在这套 log 上找到对应输出。把这些阶段的日志分别另存一份,即可直接用于平时作业与考前刷题,不需要改一行代码。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询