ARTICLE DETAIL

资讯详情

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

VC++实现滑动窗口协议仿真:Go-Back-N与Selective Repeat对比

VC++实现滑动窗口协议仿真:Go-Back-N与Selective Repeat对比 简介本资源是一份面向计算机专业本科生的《滑动窗口协议仿真》课程设计报告聚焦网络协议核心机制的理解与编程实践解决数据传输中拥塞控制、可靠交付与效率平衡等关键问题。文档以VC为开发环境完整呈现协议原理分析、三类典型实现1bit停等、后退N、选择重传对比、发送/接收双端模块化代码设计、仿真过程可视化要求及调试记录适合作为计算机网络课程实验参考与课设答辩支撑材料。资源为单文件Word文档.doc大小400KB内容涵盖引言、窗口机制详解、需求分析、结构体定义、主函数逻辑、源码节选及操作说明共29页结构严谨、步骤清晰、理论与实现紧密结合。目前已有634人学习下载读者可直接获取完整课程设计框架、可运行的模块划分思路、关键算法实现细节及常见问题应对策略。1. 为什么用 VC 在 Windows 上手写滑动窗口协议仿真比调库更练真功夫你刚学完《计算机网络》的可靠传输章节课本上画着那几个方框箭头——发送窗口、接收窗口、ACK、NACK、超时重传——但合上书脑子里还是模糊的窗口到底怎么“滑”序号怎么绕回丢包后是重发一个还是多个超时时间设成 100ms 还是 500ms 才合理这些不是靠背定义能搞懂的得亲手让字节在内存里跑起来、卡住、重发、确认才能把 TCP 的“呼吸感”刻进肌肉记忆。这份《课程设计报告-滑动窗口协议仿真.doc》不是交差文档它是一份可执行的“协议黑匣子解剖指南”。我们不用 Wireshark 抓现网流量也不用 NS-3 搞大型拓扑——就用最原始的 VCVisual C 6.0 或 VS2019 兼容模式在 Windows 桌面环境里从零搭起一个带完整状态机、可调参数、可视化窗口进度的滑动窗口仿真器。它不对接真实网卡但每一帧的生成、发送、模拟丢包、ACK 回传、窗口滑动、重传触发全由你写的 C 代码驱动。做完它你再看 RFC 793 或谢希仁《计算机网络》第 5 章会发现那些“滑动”“累积确认”“选择重传”不再是术语而是你SendBuffer[seq]数组里跳动的true/false是你windowSize min(rwnd, cwnd)计算出的那个整数。适合大三做课程设计、考研复试前刷底层理解、或想甩开 Python 胶水层直面协议逻辑的硬核学习者。2. 用 VC 在 Windows 上搭建滑动窗口仿真框架从 Win32 窗口到协议状态机2.1 创建 Win32 GUI 主窗口不是为了炫酷是为了实时观察窗口状态滑动窗口协议的核心是“状态可见”。光打印printf(Sent seq5)不够你得看到发送窗口的蓝色方块在滚动条上移动看到接收窗口的绿色方块逐个点亮看到丢包时红色叉号砸在某个序号上。VC 的 Win32 API 虽老但轻量、无依赖、直接操作 GDI正适合这种教学级可视化。// main.cpp - Win32 窗口初始化核心片段 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { static HDC hdc; static PAINTSTRUCT ps; switch (msg) { case WM_PAINT: hdc BeginPaint(hwnd, ps); // 绘制发送窗口横向条形图每个序号占 20px 宽 for (int i 0; i MAX_SEQ; i) { RECT rect {10 i*20, 50, 10 i*20 18, 90}; if (sendBuffer[i].valid !sendBuffer[i].acked) { FillRect(hdc, rect, CreateSolidBrush(RGB(100,149,237))); // 未确认蓝色 } else if (sendBuffer[i].acked) { FillRect(hdc, rect, CreateSolidBrush(RGB(50,205,50))); // 已确认绿色 } else { FillRect(hdc, rect, CreateSolidBrush(RGB(220,220,220))); // 空闲灰色 } } EndPaint(hwnd, ps); break; // ... 其他消息处理 } return DefWindowProc(hwnd, msg, wParam, lParam); }提示这里没用 MFC 或 Qt因为 Win32 原生 API 能让你清晰看到“窗口句柄 → 设备上下文 → 绘图函数”的链条。每个FillRect对应一个序号的状态你改一行颜色值就能立刻验证 ACK 是否真的更新了状态——这是调试协议逻辑最直观的“后悔药”。2.2 定义滑动窗口核心数据结构用数组指针模拟真实缓冲区协议仿真的灵魂不在界面而在内存布局。我们不抽象成std::queue而是用固定大小数组模拟发送/接收缓冲区用显式指针管理窗口边界——这才是理解base,nextSeqNum,windowSize的唯一路径。// protocol.h - 协议核心结构体 #define MAX_SEQ 16 // 模拟 4-bit 序号空间0~15便于观察绕回 #define WINDOW_SIZE 4 // 初始发送窗口大小 struct Packet { int seqNum; // 序号0~15 char data[64]; // 模拟数据载荷 bool valid; // 是否已构造有效包 bool acked; // 是否收到 ACK DWORD sendTime; // 发送时间戳用于超时判断 }; class SlidingWindowProtocol { public: Packet sendBuffer[MAX_SEQ]; // 发送缓冲区存储待确认包 bool recvBuffer[MAX_SEQ]; // 接收缓冲区标记是否收到该序号包GSR int base; // 当前窗口左边界最小未确认序号 int nextSeqNum; // 下一个待发送序号 int windowSize; // 当前窗口大小可动态调整 DWORD timeout; // 超时时间ms初始设为 300 SlidingWindowProtocol() : base(0), nextSeqNum(0), windowSize(WINDOW_SIZE), timeout(300) { memset(sendBuffer, 0, sizeof(sendBuffer)); memset(recvBuffer, 0, sizeof(recvBuffer)); } // 关键方法发送逻辑 —— 只有 nextSeqNum 在窗口内才允许发 bool canSend() { return (nextSeqNum - base MAX_SEQ) % MAX_SEQ windowSize; } // 关键方法滑动窗口 —— 收到 ACK 后移动 base void slideWindow(int ackSeq) { while (base ! ackSeq) { sendBuffer[base].acked true; base (base 1) % MAX_SEQ; } } };参数说明MAX_SEQ16刻意设小方便你在界面上一眼看清序号绕回15→0windowSize4初始值后续可通过 UI 滑块实时调整观察不同窗口大小对吞吐的影响canSend()中的(nextSeqNum - base MAX_SEQ) % MAX_SEQ是经典模运算处理序号绕回——这是学生最容易写错的点必须亲手敲一遍slideWindow()不是简单base而是循环直到base ackSeq因为 ACK 是累积的如收到 ACK3意味着 0,1,2,3 全确认。2.3 实现协议事件驱动循环用 Windows Timer 模拟网络时延与丢包真实网络没有“线程 sleep”只有事件驱动。我们用SetTimer()触发周期性检查模拟“时间流逝”——这比Sleep()更贴近协议本质。// main.cpp - 定时器事件处理 #define TIMER_ID_SEND 1 #define TIMER_ID_CHECK 2 void CALLBACK TimerProc(HWND hwnd, UINT msg, UINT_PTR id, DWORD dwTime) { static int step 0; switch (id) { case TIMER_ID_SEND: // 每 200ms 尝试发送一个新包模拟应用层数据到达 if (swp.canSend()) { int seq swp.nextSeqNum; swp.sendBuffer[seq].seqNum seq; swp.sendBuffer[seq].valid true; swp.sendBuffer[seq].sendTime GetTickCount(); swp.nextSeqNum (seq 1) % MAX_SEQ; // 模拟随机丢包丢包率 20% if (rand() % 100 20) { // 不发出去直接标记为“丢包” swp.sendBuffer[seq].acked false; // 保持未确认 // 界面绘制时会显示红色叉号 } else { // 模拟成功发送记录到日志等待 ACK logMessage(Sent packet seq, seq); } } break; case TIMER_ID_CHECK: // 每 50ms 检查超时遍历所有未确认包 for (int i 0; i MAX_SEQ; i) { if (swp.sendBuffer[i].valid !swp.sendBuffer[i].acked) { if (GetTickCount() - swp.sendBuffer[i].sendTime swp.timeout) { logMessage(Timeout! Resending seq, i); // 触发重传重新设置 sendTime不改变 seqNum swp.sendBuffer[i].sendTime GetTickCount(); } } } break; } }关键设计理由TIMER_ID_SEND200ms控制发送节奏避免瞬间塞满窗口TIMER_ID_CHECK50ms高频检查超时确保重传及时——这个频率远高于真实网络但教学仿真需要“加速时间”来快速暴露问题丢包在发送时模拟而非接收时因为滑动窗口协议中丢包发生在“发送→接收”链路接收方根本不知道包丢了重传不生成新序号而是复用原seqNum这是 ARQ 协议的铁律。3. 实现 Go-Back-N 与 Selective Repeat两种策略的代码分叉点在哪3.1 Go-Back-N 的核心收到乱序 ACK 时只滑动窗口不清理中间未确认包Go-Back-N 的“回退”特性体现在slideWindow()和重传逻辑的耦合上。当收到 ACK5但 3、4 还没确认时窗口只滑到 5而 3、4 依然在缓冲区等待——一旦超时它们会被一起重传。// protocol.cpp - Go-Back-N 版本的 ACK 处理 void SlidingWindowProtocol::handleACK(int ackSeq) { // 累积确认ACKn 表示所有 n 的包都收到了 if (ackSeq base ackSeq (base windowSize) % MAX_SEQ) { // 滑动窗口base 移到 ackSeq slideWindow(ackSeq); // 注意不清理 sendBuffer 中 [base, ackSeq) 区间的包 // 它们仍需等待自己的 ACK或被超时重传 } } // 重传逻辑已在 TimerProc 中自动覆盖所有未确认包 // 无需额外判断——这就是 Go-Back-N 的“粗粒度”体现为什么这样设计因为 Go-Back-N 接收方不缓存乱序包所以发送方必须保证窗口内所有包按序确认。slideWindow()只响应最高 ACK其余未确认包留在缓冲区等着被“连坐重传”。这是它和 Selective Repeat 的根本分水岭。3.2 Selective Repeat 的核心接收方缓存乱序包发送方只重传真正丢失的Selective Repeat 要求接收方维护recvBuffer并缓存乱序包发送方则需单独记录每个包的确认状态并只重传明确丢失的。// protocol.cpp - Selective Repeat 版本的 ACK 处理 void SlidingWindowProtocol::handleSACK(int seqNum) { // SACK 是单个序号确认非累积 if (seqNum base seqNum (base windowSize) % MAX_SEQ) { sendBuffer[seqNum].acked true; // 检查是否可以滑动从 base 开始找到第一个未确认的序号 int newBase base; while (newBase ! nextSeqNum sendBuffer[newBase].acked) { newBase (newBase 1) % MAX_SEQ; } base newBase; } } // 重传逻辑需升级只重传 sendBuffer[i].ackedfalse 的包 void SlidingWindowProtocol::checkTimeoutAndResend() { for (int i 0; i MAX_SEQ; i) { if (sendBuffer[i].valid !sendBuffer[i].acked) { if (GetTickCount() - sendBuffer[i].sendTime timeout) { // 只重传这个特定序号 sendBuffer[i].sendTime GetTickCount(); logMessage(SR: Resending only seq, i); } } } }参数对比表两种策略的关键差异特性Go-Back-NSelective Repeat接收方缓存❌ 不缓存乱序包丢弃✅ 缓存乱序包至recvBufferACK 类型累积 ACKACKn 表示 ≤n 全收到独立 ACK每个包单独确认窗口滑动时机收到任一 ACK 即滑动至该 ACK必须从base开始连续确认才滑动重传粒度超时则重传base到nextSeqNum-1所有未确认包超时只重传该序号单个包内存开销小只需发送缓冲区大需发送缓冲区 接收缓冲区吞吐优势场景低丢包率5%高丢包率或高时延如卫星链路血泪经验初学者常把 Selective Repeat 的handleSACK()写成和 Go-Back-N 一样调slideWindow()结果窗口乱滑。记住SR 的滑动不是“收到 ACK 就滑”而是“从 base 开始找到第一个未确认的序号那里就是新 base”。4. 避坑VC 滑动窗口仿真中 4 个必踩的“玄学”陷阱与硬核解法4.1 现象窗口明明该滑动但界面上蓝色方块卡死不动原因base和nextSeqNum的模运算溢出未处理导致(nextSeqNum - base) % MAX_SEQ计算为负数canSend()返回false。例如base14, nextSeqNum11-14-13-13%163C 中负数取模结果为负实际应为3但你的比较逻辑崩了。解决所有模运算必须加MAX_SEQ再取模确保非负int windowOccupied (nextSeqNum - base MAX_SEQ) % MAX_SEQ; // 正确 // 错误写法int windowOccupied (nextSeqNum - base) % MAX_SEQ;4.2 现象丢包率设为 30%但实际几乎不丢或全丢原因rand()未初始化种子每次运行生成相同序列或rand() % 100在MAX_SEQ16时因RAND_MAX不是 100 倍数导致分布不均。解决// 在 WinMain() 开头加 srand((unsigned int)time(NULL)); // 初始化种子 // 丢包判断改用更均匀的方案 if ((rand() * 100) / RAND_MAX lossRate) { // lossRate 是 0~100 的整数 // 丢包 }4.3 现象超时重传后收到旧 ACK窗口错误回退原因未实现序号空间的“回绕感知”。例如窗口在seq14,15,0,1收到ACK1但base是14slideWindow(1)会错误地认为1 14拒绝滑动。解决slideWindow()必须用循环比较而非数值大小void slideWindow(int ackSeq) { // 正确用模距离判断 ACK 是否在窗口内 int dist (ackSeq - base MAX_SEQ) % MAX_SEQ; if (dist windowSize) { // ACK 在当前窗口内 while (base ! ackSeq) { sendBuffer[base].acked true; base (base 1) % MAX_SEQ; } } }4.4 现象VC 6.0 编译报错error C2065: snprintf : undeclared identifier原因VC 6.0 标准库老旧不支持 C99 的snprintf而你用它拼接日志字符串。解决方案 A推荐用_snprintf替代VC 特有char buf[256]; _snprintf(buf, sizeof(buf)-1, Sent seq%d, seq); buf[sizeof(buf)-1] \0; // 确保结尾方案 B改用sprintf_sVS2005或std::stringstream跨平台注意不要用sprintf它不检查缓冲区溢出是安全漏洞。5. 用仿真器验证教科书结论3 个必须亲手跑出来的关键实验5.1 实验一验证“窗口大小 带宽 × 时延积BDP”的吞吐瓶颈教科书说“窗口太小会限制吞吐太大浪费内存”。我们用仿真器量化验证固定链路时延RTT200ms带宽10Mbps→ BDP 10×10^6 bit/s × 0.2s 2×10^6 bits ≈ 250KB假设每包1KB→ 理论最优窗口 250包在仿真器中将WINDOW_SIZE从1逐步调到512记录 10 秒内成功传输的包数即吞吐量预期曲线吞吐量随窗口增大而上升到WINDOW_SIZE250附近达峰值之后持平。若窗口500吞吐不变但内存占用翻倍——这就是 BDP 的实证。你亲手调滑块、看数字跳变比背公式深刻十倍。5.2 实验二对比 Go-Back-N 与 Selective Repeat 在 30% 丢包下的吞吐差异设置lossRate30timeout500ms运行 60 秒Go-Back-N因一次丢包导致窗口内所有包重传吞吐暴跌Selective Repeat只重传丢包其他包正常确认吞吐维持高位关键观察点打开日志Go-Back-N 日志里Resending seq3,4,5,6...连续出现而 SR 日志里Resending only seq3、Resending only seq7孤立出现。这种“连坐 vs 精准”的差异肉眼可见。5.3 实验三调试“糊涂窗口综合症SWS”——小包泛滥的根源故意在发送端制造小数据块如每次只填data[0]A并禁用 Nagle 算法仿真器中设nagleEnabledfalse现象界面上瞬间涌出大量seq0,1,2,3...的小包窗口疯狂滑动又卡住原因应用层频繁调用send()而 TCP 层未合并修复验证在仿真器中加入简易 Nagle 逻辑——缓存小数据等ACK回来或200ms后再发// 伪代码Nagle 算法模拟 if (dataLen MSS !lastACKReceived) { // 缓存不立即发送 pendingData.append(data); } else { // 发送 sendPacket(); }开启后界面上包密度骤降吞吐反而提升——这就是 SWS 的现场教学。6. 把 .doc 报告变成可执行资产从文字描述到可复现代码包的 3 个交付技巧6.1 报告里的“系统架构图”必须对应真实代码文件结构别再画 UML 用例图糊弄了。你的.doc报告里“系统总体结构”章节应该直接截图自项目文件夹SlidingWindowSimulator/ ├── SlidingWindowSimulator.cpp // Win32 主窗口入口 ├── protocol.h / protocol.cpp // 协议核心类含 GBN/SR 两套实现 ├── resource.h // 界面资源 ID按钮、滑块、日志框 ├── res/ // 图标、位图资源 └── README.md // 一行命令启动说明技巧在报告中粘贴这段树状图然后在protocol.cpp文件开头加注释// 【报告图2-1 对应】此文件实现 SlidingWindowProtocol 类 // 包含 base/nextSeqNum/windowSize 三要素及 handleACK() 方法 // 详见报告 3.2 节“Go-Back-N 状态机设计”——让阅卷老师鼠标一点就能跳转到代码证明你真写了不是抄的。6.2 “仿真结果分析”表格必须带原始数据来源报告里“表4-1 不同丢包率下吞吐量对比”不能只写数字。每一行数据后面用小字号标注生成方式丢包率吞吐量包/秒数据来源10%4.2log.txt第 127 行countSuccess(10000ms)30%1.8log.txt第 254 行countSuccess(10000ms)操作在仿真器中加一行统计函数int countSuccess(DWORD durationMs) { DWORD start GetTickCount(); int count 0; while (GetTickCount() - start durationMs) { if (somePacket.acked) count; Sleep(10); // 避免忙等 } return count; }运行时导出log.txt截图其中关键段落附在报告里。数据可溯源结论才可信。6.3 附录放“一键编译脚本”而不是“安装 Visual Studio”VC 环境配置是最大劝退点。别写“请安装 VS2019 并配置 MFC”。直接在项目根目录放build.batecho off echo 正在使用 VC 工具链编译... call C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat x64 cl /c /EHsc /W4 SlidingWindowSimulator.cpp protocol.cpp link SlidingWindowSimulator.obj protocol.obj user32.lib gdi32.lib /OUT:Simulator.exe echo 编译完成运行 Simulator.exe 即可。 pause为什么有效它不依赖 IDE纯命令行任何装了 VS 的 Windows 机器都能跑路径用vcvarsall.bat自动配置避开手动设环境变量的坑/W4开启最高警告级别帮你提前发现uninitialized variable这类协议逻辑致命错误学生交作业时老师双击build.bat就能出 exe不会因环境问题判零分。我带过 7 届网络课程设计凡是把.doc报告和可运行代码包打包提交的学生答辩通过率 100%。因为报告里的每句话都有代码行号、日志截图、编译命令对应——这不是作业是工程师的交付物。希望帮到你。本文还有配套的精品资源点击获取
返回列表