ARTICLE DETAIL

资讯详情

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

GBN滑动窗口协议仿真:用Python从零搭建离散事件模拟器

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