资讯动态

滑动窗口协议C语言模拟实现:GBN与选择性重传实战

发布时间:2026/10/18 4:44:49 来源:尧图企业网站定制
简介面向北京邮电大学计算机网络实验课程的滑动窗口协议模拟项目适合正在完成计网期末大作业或课程设计的本科生参考。资源以C语言实现完整覆盖数据链路层滑动窗口核心机制包括选择性重传、回退N帧及其改进版本并集成CRC32校验、命令行参数解析等底层模块协议头文件与各算法源文件分离模块化程度高便于逐段阅读和二次修改。压缩包共18个文件以8个C源文件和4个头文件为主体附带Visual Studio工程文件、项目说明文档、生成效果图及README整体仅71KB结构清晰直接编译即可运行。源码经过本地编译验证评审分达95分以上难度适中内容经助教审定既适合作为期末答辩的完整方案也可作为同类实验的排错对照。目前已有173人学习值得需要高质量计网实验参考的同学下载使用。1. 滑动窗口协议模拟实验从教材图到可运行的 C 代码滑动窗口协议在计网教材里画得清清楚楚发送方维护窗口、接收方反馈 ACK、超时重传逻辑好像就三句话。但一旦落到 C 语言实现问题就从「协议对不对」变成「定时器为什么没触发」「CRC 明明算了为什么对不上」「窗口滑动到底该往前挪几格」。这份资源就是某邮电类高校计网实验的完整答案一套用 C 写的滑动窗口协议模拟器支持 Go-Back-N 和 Selective Repeat 两种经典协议外加一个改进版 GBN含工程文件、源码和运行说明源码本地编译可运行评审分在 95 以上。适合正在做计网课程设计、期末大作业的同学也适合想彻底搞懂窗口机制底层逻辑的开发者。2. 整体架构拆解一个模拟器如何把三层协议跑起来2.1 目录结构与模块职责拿到压缩包解压后整个工程的文件划分非常清晰典型的「公共模块 协议实现」两层结构。先看一份文件清单文件职责protocol.h / protocol.c帧格式定义、协议类型枚举、公共状态机datalink.h / datalink.c模拟数据链路层发送、接收、丢包注入、计时器gobackn.cGo-Back-N 协议实现selective.cSelective Repeat 协议实现gbn_improve.c改进版 GBN快重传 动态超时crc32.cCRC32 校验算法lprintf.h / lprintf.c带时间戳的日志输出模块getopt.h / getopt.c命令行参数解析Windows 环境移植datalink.sln / datalink.vcxprojVisual Studio 工程文件README.md编译与运行说明这个划分值得借鉴的地方在于把 CRC 校验、日志、参数解析这些「和协议无关」的东西全部抽成独立模块gobackn.c、selective.c、gbn_improve.c 里只写协议逻辑。这样同一套模拟链路可以反复切换协议不需要复制粘贴。对于要交课程设计的同学这种结构在答辩时也特别好讲——模块边界清清楚楚老师一眼就能看出你没把代码堆在一个文件里。2.2 模拟链路层是怎么工作的datalink.c 在这套代码里扮演的角色是「虚拟信道」。它不对接真实的网卡而是用随机数生成器模拟丢包、乱序和延迟。核心思路是上层协议调用send_frame()发送帧datalink 内部先判断「这一帧要不要丢」根据传入的丢包率生成随机数决定是直接丢弃还是转发给接收方。// datalink.c 核心发送逻辑简化结构 int send_frame(frame_t *frame, double loss_rate) { if (rand() % 100 loss_rate * 100) { lprintf(DROP seq%d, simulate packet loss\n, frame-seq); return 0; // 模拟丢包返回 0 表示发送失败 } // 转发到接收端记录日志 lprintf(SEND seq%d, len%d\n, frame-seq, frame-len); deliver_to_receiver(frame); return 1; }这里loss_rate是 0~1 之间的浮点数比如传 0.1 就代表 10% 的丢包率。rand() % 100 loss_rate * 100这个判断方式简单直接但注意它只模拟了随机丢包没模拟突发丢包——真实网络里连续丢包的概率更高这部分在改进版里可以自己扩展后面第 6 章会讲。2.3 公共基础设施日志、CRC、命令行参数这套代码最容易被忽略但最有工程价值的是三个辅助模块。lprintf.c 是一个加强版 printf输出每条日志都带相对时间戳用来追溯「第几毫秒发了哪一帧、超时在哪一刻触发」非常有用。跑一次模拟下来整个协议交互过程就是一条清晰的时间线对写实验报告、截图说明问题特别好使。getopt.c 是 Linux 下命令行解析库的 Windows 移植版。因为原工程跑在 Visual Studio 里而 VS 没有自带 getopt作者就把这个文件一并放进来了。启动时通过参数指定协议类型、窗口大小、丢包率不用改代码就能切换场景例如datalink.exe -p gbn -w 4 -l 0.1-p指定协议gbn / sr / improve-w指定窗口大小-l指定丢包率。比在代码里改宏定义再重新编译要高效得多这对我来说是个很重要的加分项——课程设计的演示环节现场调参比现场改代码体面多了。3. 核心代码逐段拆解帧结构、CRC32 与窗口状态机3.1 帧结构定义字节对齐是个大坑所有协议都建立在帧结构之上。protocol.h 里定义的帧格式决定了整个模拟器的数据流形态。一个完整的数据帧至少需要承载帧类型、序号、确认号、数据长度、数据内容、校验值。// protocol.h 帧结构与协议类型定义 typedef enum { FRAME_DATA 0, FRAME_ACK 1, FRAME_NACK 2 } frame_type_t; #pragma pack(push, 1) // 取消字节对齐避免结构体出现填充字节 typedef struct { frame_type_t type; // 帧类型数据帧 / ACK / NACK unsigned int seq; // 发送序号 unsigned int ack; // 确认号捎带确认时使用 unsigned short len; // 有效数据长度 unsigned char payload[256]; // 数据载荷 unsigned int crc; // CRC32 校验值 } frame_t; #pragma pack(pop)#pragma pack(push, 1)这行值得单独拿出来说。C 语言结构体默认会对齐到 4 字节边界如果去掉这个指令frame_t 里会插入填充字节发送端和接收端计算出来的帧大小不一致CRC 永远对不上。我第一次写类似的模拟器时就栽在这里打印结构体大小和实际收发字节数差了十几个字节查了半天才发现是字节对齐问题。这类模拟实验的数据帧是直接在「内存拷贝」层面模拟信道传输的结构体怎么定义收发双方就必须按同样的内存布局解释。3.2 CRC32 校验与常见翻车点数据帧经过模拟信道可能被「篡改」——注意这和丢包不是一回事。有的实验要求模拟比特翻转让接收方发现校验错误并丢弃帧。crc32.c 用的是标准查表法初始化一个 256 项的 CRC 表之后每字节查表计算性能好且实现稳定。// crc32.c 查表法核心逻辑 static unsigned int crc_table[256]; void crc32_init() { for (unsigned int i 0; i 256; i) { unsigned int c i; for (int k 0; k 8; k) { if (c 1) c 0xEDB88320 ^ (c 1); // 标准 CRC32 多项式 else c 1; } crc_table[i] c; } } unsigned int crc32_calc(unsigned char *buf, unsigned int len) { unsigned int crc 0xFFFFFFFF; for (unsigned int i 0; i len; i) { crc crc_table[(crc ^ buf[i]) 0xFF] ^ (crc 8); } return crc ^ 0xFFFFFFFF; }接收方拿到帧后重新计算 CRC和帧尾携带的 crc 字段比对不一致就丢弃。这里有个参数细节CRC 的计算范围必须包含「帧类型 序号 确认号 长度 载荷」但绝不能包含 crc 字段本身。很多同学会在计算时用sizeof(frame_t)把整个结构体算进去结果自己把校验值也算进了校验范围永远对不上。正确做法是单独定义计算长度或者计算出 crc 后再填充字段。3.3 滑动窗口的状态机发送方与接收方的同步窗口协议的核心是两组变量发送方的 base窗口下沿、next_seq下一个待发序号接收方的 expected_seq期望收到的序号。窗口滑动的本质是这些整数变量的推进。看发送方的核心结构// gobackn.c 发送窗口基础结构 #define MAX_SEQ 32 // 序号空间窗口最大不能超过 16 typedef struct { unsigned int base; // 窗口下沿最早未确认的帧 unsigned int next_seq; // 下一个要发送的新帧 unsigned int window_size; // 窗口大小 frame_t *buffer[MAX_SEQ]; // 已发送未确认的帧缓存 } sender_t; // 尝试发送新帧 void send_new_frame(sender_t *snd) { // 窗口满next_seq 不能超过 base window_size if (snd-next_seq - snd-base snd-window_size) { lprintf(WINDOW FULL, stop sending\n); return; } frame_t *f create_data_frame(snd-next_seq); snd-buffer[snd-next_seq % MAX_SEQ] f; send_frame(f, current_loss_rate); snd-next_seq; }next_seq - base表示当前「在途未确认」的帧数。当这个差值等于 window_size 时说明窗口已经填满发送方必须停下来等 ACK。这个判断逻辑是所有滑动窗口协议的命门——如果这里写成或时边界出错窗口会多滑一格或少滑一格表现出的症状是「协议跑一会儿就卡死」或者「序号错乱」。3.4 定时器与超时重传Go-Back-N 只需要一个定时器——因为它要么全部重传要么等待累积 ACK 推进窗口。模拟器里这个定时器的实现不会真的去调用系统时钟中断而是在主循环里不断检查「当前时间 - 发送 base 帧的时间」超过超时阈值就触发重传。// 主循环中的超时检查 while (running) { handle_received_frames(); // 处理收到的 ACK / 数据帧 // 检查 base 帧是否超时 double elapsed get_elapsed_time(); if (elapsed - send_time[snd.base % MAX_SEQ] timeout_ms) { lprintf(TIMEOUT base%d, retransmit window\n, snd.base); retransmit_window(snd); // 重传 base 到 next_seq-1 的所有帧 } }retransmit_window是 GBN 的「笨重」所在重传窗口里的所有帧即使接收方已经收到了其中的绝大部分。这也是 GBN 在丢包率升高时吞吐率断崖式下跌的原因——窗口越大一次超时重传的代价越高。理解了这个函数后面看 selective.c 的改进就顺理成章了。4. 三种协议实现对比GBN、选择性重传与改进版4.1 gobackn.c累积确认与整体重传Go-Back-N 的接收方逻辑极其简单只接受顺序到达的帧一旦收到失序帧直接丢弃并重新发送对最后一个有序帧的 ACK。发送方收到 ACK 时执行「累积确认」——不管 ACK 确认的是哪一帧base 直接跳到 ack 1因为 GBN 规定 ACK n 代表「n 及之前的所有帧都成功收到」。// gobackn.c 收到 ACK 后的窗口推进 void handle_ack(sender_t *snd, unsigned int ack_seq) { // 累积确认ack_seq 之前的帧全部确认 if (ack_seq snd-base) { snd-base ack_seq 1; lprintf(ACK %d, base move to %d\n, ack_seq, snd-base); } // 如果 base 追上 next_seq说明全部确认停止计时器 if (snd-base snd-next_seq) { stop_timer(); } }这里ack_seq snd-base的条件还需要加一个序号空间的限制因为当序号回绕从 31 回到 0时简单的大小比较会失效。原工程里的序号空间设计为 MAX_SEQ32窗口最大 16保证「窗口内序号不重复」这是教材里「序号空间 ≥ 2 倍窗口大小」的工程落地。4.2 selective.c选择重传的乱序缓存Selective Repeat 的接收方要做的核心事情就是「乱序缓存」。它必须把提前到达的帧先存起来等缺的帧到齐后再一起向上层交付。// selective.c 接收缓冲区 typedef struct { int received[MAX_SEQ]; // 位图标记哪些序号已收到 frame_t *frames[MAX_SEQ]; // 缓存帧 unsigned int expected; // 期望的连续序号起点 } receiver_t; // 收到一帧的处理逻辑 void handle_data_frame(receiver_t *rcv, frame_t *f) { if (f-seq rcv-expected || rcv-received[f-seq % MAX_SEQ]) { lprintf(DUP seq%d, discard\n, f-seq); // 重复帧或过期帧 send_ack(f-seq); // 仍然回复 ACK防止发送方超时 return; } rcv-frames[f-seq % MAX_SEQ] copy_frame(f); rcv-received[f-seq % MAX_SEQ] 1; // 尝试连续交付 while (rcv-received[rcv-expected % MAX_SEQ]) { deliver_to_upper_layer(rcv-frames[rcv-expected % MAX_SEQ]); rcv-received[rcv-expected % MAX_SEQ] 0; rcv-expected; } send_ack(f-seq); // 对每一帧单独确认 }SR 的 ACK 不是累积的每一帧都要单独确认接收窗口是「缓存 位图」的组合。注意处理重复帧时也要回 ACK——这算一个经典的边界问题如果接收方收到了已经交付过的帧就静默丢弃发送方的超时重传会一直在同一个帧上打转因为发送方以为 ACK 丢了才重传。4.3 gbn_improve.c改进版到底改了哪里改进版在代码结构上基于 gobackn.c但加入了两个关键机制快重传和动态超时。所谓快重传就是发送方连续收到 3 个重复 ACK 时不等定时器超时立即重传对应帧。这在真实 TCP 里是标配手段放在课程设计里属于「超出教材但完全合理」的加分项。// gbn_improve.c 快重传判断 void handle_ack_improve(sender_t *snd, unsigned int ack_seq) { if (ack_seq snd-base) { snd-dup_ack_count; if (snd-dup_ack_count 3) { lprintf(FAST RETRANSMIT base%d\n, snd-base); retransmit_frame(snd-base); // 只重传 base 一帧不是整个窗口 snd-dup_ack_count 0; } } else if (ack_seq snd-base) { snd-base ack_seq 1; snd-dup_ack_count 0; // 窗口推进重置计数 } }动态超时的时间不是固定写死的而是统计最近几次 RTT发送到收到 ACK 的往返时间取均值再加上一定余量。固定超时的问题很明显网络一抖动就超时误判频繁重传反而加剧拥塞。改进版维护一个 RTT 样本数组每次收到 ACK 时更新估算值。4.4 三种协议对比与选型参考对比维度Go-Back-NSelective Repeat改进版 GBN接收方缓存无失序帧直接丢弃有缓存失序帧无确认方式累积确认逐帧确认累积确认 重复 ACK 快重传重传粒度整个窗口只重传缺失帧单帧快重传/ 窗口超时实现复杂度低高中丢包率高时吞吐率急剧下降保持稳定下降但优于 GBN放在计网实验的场景下我建议至少把 GBN 和 SR 两个版本都跑通并对比输出日志——这本身就可以作为实验报告的核心分析内容。改进版适合学有余力想冲高分的人答辩时你能说清楚「快重传为什么能减少超时等待」就已经超过大部分同学了。5. 编译运行与避坑指南5.1 从解压到看到结果的完整步骤这套工程是用 Visual Studio 构建的拿到手上最稳妥的做法是直接用 VS 打开datalink.sln编译运行。具体过程解压到本地注意路径不要带中文双击 .sln等待 VS 加载完成确认解决方案配置是 Debug x86部分 Windows 环境可能需要 x64取决于你的系统直接 CtrlB 编译无报错后 CtrlF5 运行。运行后程序会进入命令行交互界面按 README 里的参数格式输入协议类型、窗口大小、丢包率即可。一组值得复现的经典对比实验是这样的datalink.exe -p gbn -w 4 -l 0.1 datalink.exe -p sr -w 4 -l 0.3第一组用 10% 丢包率跑 GBN窗口 4第二组用 30% 丢包率跑 SR窗口 4。对比两组日志里重传的次数和最终完成传输的时间就能直观看到选择性重传在高丢包率下的优势。每个帧的发送、确认、丢弃和超时事件都会通过 lprintf 打印到屏幕上跑完可以在输出窗口看到完整的时序。5.2 避坑记录四个真实踩过的坑坑一编译报错 error C4996fopen 被认为不安全。现象是 VS 把fopen、strcpy这类函数标记为不安全直接报错中断编译。原因是 VS 默认启用安全检查而工程里用了标准 C 库函数。解决方式有两种在项目属性 → C/C → 预处理器里加_CRT_SECURE_NO_WARNINGS或者把报错函数替换成fopen_s等带_s后缀的安全版本。我一般选第一种改代码容易引入新问题。坑二程序跑起来后所有帧都丢失输出全是 DROP。现象是发送方发一帧丢一帧接收方一个 ACK 都回不来。原因是丢包率参数传的是整数而非小数比如传了 50 而不是 0.5。datalink.c 里rand() % 100 loss_rate * 100中 50 * 100 5000判断永远成立。解决确认丢包率参数始终是 0~1 之间的小数。这属于典型的参数取值范围与代码实现不一致导致的翻车。坑三CRC 校验总是失败。现象是接收方打印CRC ERROR但发送方明明计算过。原因有两个方向一是结构体字节对齐导致收发双方的帧大小不一致二是计算 CRC 时把 crc 字段本身算进去了。解决检查是否用了#pragma pack(push, 1)以及计算长度是否为offsetof(frame_t, crc)而不是sizeof(frame_t)。这是所有类似模拟器里最容易遇到的玄学问题。坑四窗口大小超过序号空间的一半时死锁。现象是程序在某处卡住不再发送任何帧也没有超时发生。原因是序号空间 MAX_SEQ32窗口最大只能到 16。这是协议设计的基本约束——窗口超过序号空间一半时接收方无法区分「新帧」和「重传的旧帧」。解决-w参数控制范围不要超过 16。提示如果用命令行参数跑完看不到完整输出把控制台窗口的「快速编辑模式」关掉否则程序运行时鼠标误点到控制台窗口会暂停执行。6. 二次开发把模拟协议挂到真实 UDP 链路上拿到这份代码最典型的二次开发方向是把协议栈里的send_frame()和deliver_to_receiver()两个函数从「虚拟信道」替换成真实的 UDP socket 收发让滑动窗口协议跑在真实网络上。这个改造既能用于课程设计的进阶展示也能应用到比如局域网文件传输的小工具上。// 改造思路把 send_frame 指向 socket 发送 int send_frame_udp(frame_t *frame, double loss_rate) { int sock get_udp_socket(); sendto(sock, (char *)frame, sizeof(frame_t), 0, remote_addr, sizeof(remote_addr)); // 注意真实网络自带的丢包率和模拟参数 loss_rate 无关 return 1; }改造时要注意几个关键差异真实网络里不需要手动模拟丢包但需要处理网络的乱序到达loss_rate参数可以保留但应该从模拟链路中移除接收端recvfrom后必须重新做 CRC 校验因为 UDP 只保证包完整性不保证内容不被篡改虽然概率极低。改造完成后用本机 127.0.0.1 回环地址做自测再通过设置防火墙丢包规则来验证三种协议在真实网络下的行为差异。一个小技巧是保留原有的 lprintf 日志体系在发送和接收两条路径上各自打上时间戳。跑一次真实网络传输后用日志对比「发送序号」和「确认序号」的推进速度能直观地看出窗口利用率是否达到了预期。从那以后我每次拿到协议代码都会先跑一遍丢包率递增的实验再决定是否信任它的窗口和缓存边界处理——这个习惯帮我绕开过不少看起来像随机 bug、实际上却是边界条件写错的问题。希望这份拆解能让你在做计网实验时少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑