简介:这是一份计算机网络课程设计报告,主题为滑动窗口协议仿真,面向计算机科学与技术、网络工程等专业学生,适合在学习数据链路层协议、网络编程仿真或完成同类课程作业时参考。报告从引言、基本原理、需求分析到详细设计与调试操作说明,完整覆盖了滑动窗口协议的窗口机制,并对1bit滑动窗口、后退N协议、选择重传协议三种典型类型进行对比分析;同时基于VC++开发环境,围绕sender队列、sender主函数、receiver队列、receiver主函数等模块给出设计思路与源代码,可帮助读者理解端到端数据传送、帧序号管理、超时重传、流量控制等关键实现。资源为单个doc文档,压缩包约400KB,内含课程设计任务书、分工进度、核心代码、调试说明等完整结构。目前已有635人学习下载,对需要撰写网络协议仿真报告或动手实现滑动窗口算法的学生具有直接参考价值。
1. 滑动窗口协议仿真:为什么值得自己做一遍而不是交给仿真工具
很多人拿到课程设计标题“滑动窗口协议仿真”,第一反应是先找能现成跑出波形图的工具,结果装好软件发现协议还是黑盒。我自己写过一轮GBN(Go-Back-N)的离散事件仿真,最大的体会是:滑动窗口协议仿真的核心价值,不是证明协议“能工作”,而是把窗口推进、超时重传、丢包扰动这三件事变成看得见的数字和曲线。做一遍仿真,比把RFC背三遍都管用。这篇文章面向要交课程设计报告、以及想搞懂滑动窗口重传协议行为边界的从业者,用Python从零搭一个最小可运行的滑窗仿真器,把实验参数怎么设、结果怎么读、坑在哪一次讲透。
2. 先想清楚再动手:滑动窗口重传协议的三种形态与链路抽象
2.1 停等、GBN、SR:三条窗口推进路线的本质差异
滑动窗口重传协议按发送窗口和接收窗口的大小,分三种典型形态。
停等协议是窗口等于1的方案——发送方发一个包,等确认到了再发下一个。确认丢了就超时重传。它实现最简单,但要等一个RTT才能再占用链路,利用率在高时延高带宽场景下非常难看,现在基本只在极简单的链路层会出现。
Go-Back-N(GBN)把发送窗口开到大于1,发送方可以连续把窗口里的包发出去,接收方仍然只收按序来的包,乱序的直接丢弃;接收方每收到一个有序帧回一个累计ACK,发送方超时后把窗口内所有未确认的帧全部重发。GBN是教科书里的经典内容,也是课程设计的主角。
选择性重传(Selective Repeat,SR)更进一步,接收方有接收缓存来容纳乱序帧,发送方只重传真正超时的单个包,窗口推进更细,代价是接收方要按序号管理缓存,ACK语义也复杂不少。
这里有一个容易搞混的区分维度:发送窗口决定“能有多少包在路上”,接收窗口决定“能容忍多少乱序”。GBN的接收窗口是1,所以乱序帧不能收,即使它其实已经到了;SR的接收窗口大于1,乱序可以暂存,只重传丢失的那一个包。
| 协议 | 发送窗口 | 接收窗口 | ACK语义 | 重传粒度 | 适用场景 |
|---|---|---|---|---|---|
| 停等 | 1 | 1 | 逐包确认 | 单个包 | 链路时延小、带宽要求低 |
| GBN | >1 | 1 | 累计确认 | 整个窗口 | 网络较可靠、时延带宽积较大 |
| SR | >1 | >1 | 逐包或选择性确认 | 单个包 | 高误码、长时延链路(卫星) |
课程设计为什么优先选GBN?理由很简单:GBN的重传行为最“热闹”,重传整个窗口,吞吐率对丢包率最敏感,画出来的曲线对比最明显,写报告时故事线最完整。SR代码量翻倍,收益在课程设计的篇幅里往往体现不出来。
2.2 为什么课程设计更适合自己写离散事件仿真,而不是直接上NS-3
NS-3这类网络仿真器功能很强,无线、TCP、队列调度都有现成模块。但对一个滑动窗口协议的课程设计来说,我一般不建议第一版就上NS-3。协议逻辑本质是一个状态机,发送方、接收方各自维护窗口状态,几百行代码就能把每个状态转移看清楚;而仿真器把链路、队列、丢包原因全部黑匣子化了,答辩时被问“丢包率到底是作用在数据帧还是ACK帧上”“超时计时器是每个包一个还是全局一个”,说不清楚。
离散事件仿真适合这种场景:系统状态的变化发生在离散时间点(包到达、ACK返回、计时器超时),而不是连续物理过程。滑动窗口协议天然是离散事件系统。用Python写一个简单的时隙驱动仿真器,事件用“延迟队列”模拟,逻辑上比完全事件驱动更加直白,调试也更友好。
真要用NS-3,通常是在你已经有一个验证过行为正确的最小实现之后,当作二次验证或出图工具,而不是当作第一版的学习工具。
2.3 链路模型:丢包、时延、序号空间怎么抽象
要在仿真里模拟真实链路,最少需要三个要素:丢包、时延、序号空间。
丢包分两类:数据帧丢失和ACK帧丢失。前者导致发送方超时重传,后者导致发送方误以为数据帧丢了,也会触发超时重传,但接收方实际上没有漏收包。仿真里一定要分开配置data_loss和ack_loss两个参数,否则报告里说不清楚吞吐率下降的归因。
时延参数delay表示一个帧从发出到到达之间的时隙数。时隙是仿真的时间单位,可以理解为“发送方每时隙最多发送一个数据帧”,这样链路的带宽就被隐含约束了。delay=2表示帧要经过两个时隙才到达对端,RTT大约是4个时隙(数据去程2加上ACK回程2)。
序号空间指协议里序号能取几个不同值,通常取2的幂。GBN有个经典限制:发送窗口大小W需要满足 2W <= seq_mod,否则接收方无法区分新旧帧。一般课程设计里让seq_mod=16、窗口取4,W*2=8<16,安全余量很大。
乱序在纯单链路时隙模型里不太容易出现,因为一条链路不会自行超车。如果要做乱序仿真,可以给每个帧随机额外延迟,或者构造多路径场景,这里先不展开。
3. 纯Python搭一个GBN滑窗仿真器:核心代码与参数逐个拆解
3.1 链路与发送方:Link类和GBNSender类
先写链路类,管丢包和延迟。
import random # 模拟有损链路:数据帧和ACK帧都可能有丢包 class Link: def __init__(self, data_loss=0.0, ack_loss=0.0, delay=2, seed=42): self.data_loss = data_loss # 数据帧丢包率 self.ack_loss = ack_loss # ACK帧丢包率 self.delay = delay # 单向传播延迟(时隙数) self.rng = random.Random(seed) # 固定随机种子,结果可复现 self.data_in_flight = [] # 已发出未到达的数据帧 self.ack_in_flight = [] # 已发出未到达的ACK帧 def send_data(self, seq, now): """发送方调用:投递数据帧,成功返回 'ok',被丢弃返回 'lost'""" if self.rng.random() < self.data_loss: return 'lost' self.data_in_flight.append((now + self.delay, seq)) return 'ok' def send_ack(self, ack, now): """接收方调用:回传ACK,成功返回 'ok',被丢弃返回 'lost'""" if self.rng.random() < self.ack_loss: return 'lost' self.ack_in_flight.append((now + self.delay, ack)) return 'ok' def tick(self, now): """当前时隙到期的帧全部交付,返回(数据帧列表, ACK列表)""" arrived_data = [seq for t, seq in self.data_in_flight if t <= now] arrived_ack = [ack for t, ack in self.ack_in_flight if t <= now] self.data_in_flight = [(t, s) for t, s in self.data_in_flight if t > now] self.ack_in_flight = [(t, a) for t, a in self.ack_in_flight if t > now] return arrived_data, arrived_ack逻辑说明:send_data和send_ack都在帧上打一个“到达时刻=now+delay”的标签,tick按期交付,这样延迟天然模拟了在途帧。随机种子由Link统一持有,数据帧与ACK帧共享同一个随机序列但分别抽样,用不同参数控制概率。
然后是发送方状态机。
# Go-Back-N 发送方:维护 base 和 next_seq 两个指针 class GBNSender: def __init__(self, total, window=4, timeout=8): self.total = total # 要发送的总包数 self.window = window # 发送窗口大小 self.timeout = timeout # 超时计时器阈值(时隙数) self.base = 0 # 最早未确认的序号 self.next_seq = 0 # 下一个新包的序号 self.timer = 0 # 基准计时器剩余时隙 self.sent = 0 # 总发送次数(含重传) self.retrans = 0 # 重传包计数 def all_done(self): return self.base >= self.total def window_full(self): return self.next_seq - self.base >= self.window def run_slot(self, link, now): """每个时隙驱动一次发送与重传逻辑""" # 1) 计时器递减,触发超时重传 if self.timer > 0: self.timer -= 1 if self.timer == 0 and self.base < self.next_seq: for seq in range(self.base, self.next_seq): link.send_data(seq, now) self.sent += 1 self.retrans += 1 self.timer = self.timeout # 2) 窗口未满且还有新包,连续发送 while not self.window_full() and self.next_seq < self.total: link.send_data(self.next_seq, now) self.sent += 1 if self.timer == 0: self.timer = self.timeout # 窗口空闲时才启动计时器 self.next_seq += 1 def on_ack(self, ack): """收到累计ACK:确认序号 ack 及之前的所有帧""" if ack >= self.base: self.base = ack + 1 if not self.all_done(): self.timer = self.timeout逻辑说明:GBN只维护一个基准计时器,这是它与SR的关键差异。计时器在两种情况下重置或启动:发送了新包且原先没有计时器在跑;收到累计ACK后base推进。窗口未满时连续发新包,都靠这一个timer兜底,哪个包超时就把base到next_seq-1之间所有包重发一遍。
参数说明:total决定仿真规模,window大表示链路利用率高,但窗口受序号空间限制;timeout单位是时隙,设置过大让单包丢失后白白空等,过小会导致没等到ACK就重发,后面的实验部分专门讲。
3.2 接收方与主循环:把ACK回传和整个仿真串起来
# Go-Back-N 接收方:只收按序帧,乱序帧直接丢弃 class GBNReceiver: def __init__(self): self.expected = 0 def on_data(self, seq, link, now): if seq == self.expected: self.expected += 1 link.send_ack(seq, now) # 按序到达,回ACK else: # 乱序帧丢弃,不回ACK,等发送方超时重传整个窗口 pass接收方这里故意不做重复ACK,是为了让逻辑最简单。乱序帧在GBN里的处理是“到了也白到”,直接丢;发送方只依赖超时机制恢复。后面扩展SR时,才需要把乱序帧缓存下来并按需回选择性ACK。
def run_gbn(total=12, window=4, timeout=8, data_loss=0.2, ack_loss=0.0, delay=2, seed=42, max_slots=500): link = Link(data_loss, ack_loss, delay, seed) sender = GBNSender(total, window, timeout) receiver = GBNReceiver() now = 0 while now < max_slots and not sender.all_done(): arrived_data, arrived_ack = link.tick(now) # 交付到期帧 for seq in arrived_data: receiver.on_data(seq, link, now) # 接收方处理数据帧 for ack in arrived_ack: sender.on_ack(ack) # 发送方处理ACK sender.run_slot(link, now) # 发送/重传逻辑 now += 1 if not sender.all_done(): print("[警告] 达到 max_slots 上限,仿真未完成,请检查参数") print(f"完成: total={total} slots={now} 发送总数={sender.sent} " f"重传数={sender.retrans} 重传率={sender.retrans / sender.sent:.2f}") return sender.sent, sender.retrans, now主循环逻辑说明:每个时隙先交付到期帧,再让接收方回ACK,再让发送方处理ACK,最后驱动发送方发送和重传。这个顺序很重要:ACK回传到发送方要在同一时隙内先于run_slot执行,否则已经到达的ACK还要晚一个时隙才生效,会把合法延迟额外放大一个时隙,统计结果整体偏悲观。
这里的简化假设是total不超过序号空间,因此序号不会回绕,代码里直接用整数比较就够。这个假设能让初版跑通,真正的序号回绕处理放到避坑章节单独讲。
运行验证:把total设为12、window=4、timeout=8、data_loss=0.2,固定seed,跑完会看到完成时隙数、发送总数和重传率。如果把data_loss改到0.6,大概率会看到max_slots超时警告,这正是后面要分析的现象。
3.3 参数说明与最小可运行实验
仿真器有六个参数,它们的物理含义和调节顺序是:
data_loss(数据丢包率):影响最直接。从0开始逐步加到0.5,能看到发送总数和重传率单调上升。 ack_loss(ACK丢包率):模拟反向链路丢包。GBN对ACK丢包比较敏感,因为累计ACK只有最新的有效,旧ACK被覆盖了。 timeout(超时时限):以时隙为单位。经验初始值是delay2+2左右,即一个RTT加一点余量。 window(发送窗口):受 2window <= seq_mod 限制。窗口越大,单次超时重传的成本越高。 delay(单向时延):RTT=2*delay。 seed(随机种子):实验必须固定,否则结果不可复现。
第一次跑通后,我习惯先跑一个对照实验:data_loss=0、ack_loss=0、window=4,确认发送总数正好等于total,重传数为0。这一步能快速验证链路和状态机本身没有把帧弄丢。
4. 跑实验并读结果:四组参数对照与吞吐率曲线
4.1 实验矩阵:固定窗口改丢包率是最直观的对照
课程设计最稳妥的实验设计,是固定窗口和超时,只扫描丢包率。为什么这样设计?丢包率是链路条件,窗口是你的协议参数,先把外部条件的影响摸清楚,再回来调协议参数,报告的分析逻辑才顺。
推荐做四组实验(每组固定seed为0到4跑5次取平均):
| 实验编号 | total | window | timeout | delay | data_loss | ack_loss |
|---|---|---|---|---|---|---|
| E1 | 100 | 4 | 6 | 2 | 0.0 | 0.0 |
| E2 | 100 | 4 | 6 | 2 | 0.1 | 0.0 |
| E3 | 100 | 4 | 6 | 2 | 0.2 | 0.0 |
| E4 | 100 | 4 | 6 | 2 | 0.4 | 0.0 |
这里total=100意味着序号空间要取不小于100且大于2*window的值,比如128。测量指标有四个:完成时隙数slots、发送总数sent、重传数retrans、重传率=retrans/sent,最后用“有效吞吐率=total/slots”把结果统一到一个指标上。
把这四组结果整理成表,slots和retrans是原始读数,重传率和吞吐率是分析指标。报告里呈现的顺序也是先原始数据、再归一化指标,别一上来就画拟合曲线,否则评审会问你要原始记录。
4.2 结果怎么读:GBN的吞吐率为什么有台阶式下跌
先看E1(无丢包):重传数为0,完成时隙数由发送方连续点火和传播时延决定。此时吞吐率只受窗口和RTT限制,窗口越大越接近“一直能发”。
再看E2到E4:data_loss=0.1时,重传率开始抬头但还能接受;data_loss=0.2时,重传率会明显高于0.2,原因是GBN一超时就要重传整个窗口,一个包丢,后面已发但未确认的包全部陪葬,这些陪葬包本来不用重发。
data_loss继续往上到0.4以后,仿真的耗时会爆炸式增长。重传率可能接近0.6甚至更高,吞吐率掉到一个很低的水平。这就是GBN的悬崖点——超过某个丢包率,链路吞吐会劣化到接近停等协议的效率。这也是“仿真发散”这个词在协议仿真里的含义:数值指标发散,不是链路真的炸了,而是该协议在那组参数下接近不可用。
读结果时有一个正确姿势:把重传率和丢包率画在同一个坐标系里。如果重传率曲线在data_loss=0.1附近出现明显拐点,说明你实现的确实是GBN而不是SR——SR的重传率几乎等于丢包率,不会有这个拐点。
4.3 timeout怎么调:扫描法找平台期,不要拍脑袋
timeout是新手最容易随手设的参数。设小了,ACK还在路上就触发超时,白白重传;设大了,一个包丢了之后链路空等好几个RTT,吞吐掉得厉害。
常见做法是做一个参数扫描:固定data_loss=0.2、window=4、delay=2,让timeout从4扫描到16,记录每个值下的完成时隙数。你会得到一条先下降后上升的曲线,最低点附近就是该用的值。
经验公式一般是 timeout = 2 * delay + 2,对应一个RTT(4个时隙)再加两个时隙的处理余量。更高时延或更忙的链路要往上加,但没必要加过头,因为加大的每一格都是丢包发生后纯等的成本。
要避开最常见的“不调参”误区:直接把timeout设成100跑完整组实验,结果和换一组实验没区别,报告里也讲不出所以然。timeout不是一个随便赋值的参数,它是协议在丢包和时延之间做权衡的旋钮。跑一遍扫描曲线,把曲线截图放进报告,这本身就是一份实验素材。
5. 滑动窗口仿真避坑指南:五个绕不开的问题与排查解法
5.1 序号回绕:窗口滑过模数边界之后,ACK判断全乱
现象:仿真跑了一段时间后,发送方明明收到了某个ACK,窗口却不推进,甚至base算出来比next_seq还大,程序最后卡在max_slots超时退出。
原因:简化代码假设序号不循环。当total大于序号空间,或者窗口滑动接近边界后,普通整数比较ack >= self.base失效。比如序号空间模16,ACK序号2既可能是“第二个包”也可能是“第18个包”,用整数比较会误判为非法确认。
解决:要么把total限制在序号空间内,像第3章的示例那样;要么把比较改成环形窗口判断,用模运算计算(ack - base) % seq_mod是否落在窗口内。课程设计里最好在报告单独写一节“序号回绕与窗口约束”,把 2W <= seq_mod 的推导写出来,这是加分项。
5.2 GBN计时器:每包一个计时器会让GBN变成“假SR”
现象:代码里给每个发出但未确认的包都单独维护一个计时器,结果发现重传数量大幅下降,吞吐曲线和SR差不多,和你教材上GBN的曲线对不上。
原因:GBN的定义是单基准计时器,超时后重传整个窗口。每包计时器本质是把重传粒度变成单包,行为接近SR,但接收方又没有SR的缓存逻辑,于是出现“重传了单个包、接收方却丢弃乱序帧”的怪异组合。
解决:回到单计时器模型。记住一句话:GBN只有一个timer,它代表“最早未确认的那个包已经等了多久”。收到任意合法的累计ACK都会重置它,窗口内任何包丢,整个窗口陪跑重传。
5.3 高丢包率下仿真“发散”:不是Bug,是链路容量到头了
现象:data_loss=0.6时,run_gbn一直运行到max_slots=500还没完成,打印警告退出。
原因:这不是死循环,是高丢包率下GBN的有效吞吐趋近于零。每次超时重传整个窗口,而窗口内的包大概率继续丢,形成“发了丢、丢了发”的循环。数学上链路仍有正概率成功,但期望完成时间指数增长,仿真跑完不现实。
解决:把实验的丢包率控制在0到0.4之间;超过0.4想继续观察,就换用选择性重传代码再试。写报告时把0.4以上的行为描述为“协议在极端链路条件下的退化”,比硬跑等结果更专业。
5.4 ACK丢失的账不要记到数据丢包头上
现象:ack_loss=0.2时,发送方重传率明显上升,报告里把重传率上升直接归因于数据丢包,分析做反了。
原因:ACK丢失不会让接收方漏收数据,但会让发送方超时。超时后重传的包接收方早就收过了,按序检查发现是重复帧直接丢弃,发送方白忙一场。
解决:实验里把ack_loss设成0先测数据丢包的影响,再单独开一组ack_loss不为0的对照,两组分开分析。当年我自己在这个坑里绕了两天,最后打日志才发现重传的包接收方早就收过了。
5.5 随机种子:结果“玄学”变动,翻车就翻在忘了固定seed
现象:同一组参数跑两次,重传率差出一大截,还以为是代码有并发问题。
原因:Link每次实例化都重新生成随机序列,不传seed时每次从系统时间取样,结果完全不可复现。
解决:实验矩阵里把seed作为参数显式传入,固定跑seed=0到4取平均,并把seed写进报告的表里。另一个经验是:调参时改一行跑一次,逐步对比;只要seed没变,任何一个参数的调节结果都能回头复核。
6. 让报告从“能跑”变成“能答辩”:窗口轨迹图与正确性验证
6.1 把窗口推进过程导出成阶梯图
答辩时只给一张吞吐率表格太干。我一般会在仿真的每个时隙把sender.base和sender.next_seq记录成CSV,再用Matplotlib画窗口轨迹阶梯图。
import csv import matplotlib.pyplot as plt # 在主循环的每个时隙末尾,追加一行: # writer.writerow([now, sender.base, sender.next_seq]) with open('window_trace.csv', 'r') as f: rows = list(csv.reader(f))[1:] slot = [int(r[0]) for r in rows] base = [int(r[1]) for r in rows] next_seq = [int(r[2]) for r in rows] plt.step(slot, base, where='post', label='base') plt.step(slot, next_seq, where='post', label='next_seq') plt.xlabel('slot') plt.ylabel('seq') plt.legend() plt.savefig('window_trace.png', dpi=150)这张图能直接看出三件事:窗口多大、有没有满窗停发、超时重传发生时base回退形成的台阶。评审问“窗口什么时候卡住”,你指图回答,比念代码有说服力。
6.2 正确性验证:用理论预期给仿真结果“对表”
仿真结果不是自己说对就对,要和理论可预期行为对表。至少要做两个检查:无损链路下,完成时隙数应该和“第一个包发出到最后一个ACK返回”的时延算出来的理论值对得上,且retrans必须是0;有损链路下,重传率随丢包率的增长曲线要落在“GBN重传率高于丢包率”的区域内,而不是等于或低于丢包率,否则说明你实现的可能变成了SR却挂了个GBN的名字。
我后来养成一个小习惯:每改一个参数,先跑一遍data_loss=0的对照组,只有对照组指标没变化,才能确认这次改动没有破坏正常路径。确认正确性之后再做参数扫描,扫描结果才敢往报告里放。
写课程设计报告最怕的是代码跑出结果但讲不清为什么。把上述调试习惯写进去,评审问“怎么验证你的仿真器是对的”,你至少有三层回答:无损对照、重传率与丢包率的量级关系、窗口轨迹图和理论窗口推进一致。做到这一步,这个滑动窗口协议仿真课程设计的基本盘就算稳了。希望帮到你。
本文还有配套的精品资源,点击获取