news 2026/10/19 9:15:17

滑动窗口协议课程设计:C语言实现停等、GBN与SR协议模拟

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
滑动窗口协议课程设计:C语言实现停等、GBN与SR协议模拟

简介:面向北京邮电大学“计算机网络”课程设计与期末大作业场景,压缩包完整模拟数据链路层的滑动窗口协议,适合正在完成计网实验、需要参考可靠传输机制实现思路的本科生。核心内容包括选择性重传、回退N帧等经典协议算法的C语言源码,以及可运行的工程配置、README说明与实验效果图。源码均经本地编译通过,评审成绩95分以上,项目难度适中,内容由助教审定,能满足课程设计或期末大作业的参考需求。压缩包共18个文件,以C语言源文件(8个.c)和头文件(4个.h)为主,辅以工程方案、项目配置、截图与文档,整体约71KB,目录紧凑便于检索。已有173人学习下载,对需要快速理解滑动窗口协议编码实现与报告撰写的同学具有直接参考价值。

1. 滑动窗口协议课程设计:从源码到能吃透的每一步

做计网实验时最常遇到的情况是——教材把滑动窗口画得明明白白,但真要自己写一个能跑、能演示、还能答辩的程序,写着写着就卡在“发出去的帧到底什么时候该丢、ACK 怎么模拟、超时怎么触发”这类具体问题上。这份资源就是冲着这个痛点来的:一个用 C 语言完整实现的数据链路层滑动窗口协议模拟项目,覆盖停等、回退 N(GBN)和选择性重传(SR)三种策略,自带文档说明和可编译的工程文件。适合正在做计网课程设计、准备期末大作业或者想把手头 GBN 代码改造成 SR 的人,拿来当骨架比自己从零写省掉不少玄学调试时间。整份源码是本地编译过能直接跑的,评审分能到 95 分以上,不是说它代码量多华丽,而是该有的协议行为都模拟到位了。

2. 工程结构:源码里到底藏了哪些文件,先摸清楚再动手

拿到压缩包别急着点开 .vcxproj 就编译。我习惯先把文件清单过一遍,搞清楚每个模块扮演什么角色,后面改代码才不抓瞎。这份资源里的文件命名很直白,基本上一看文件名就知道它管哪块。

2.1 解压后的文件清单与职责划分

整个工程是一个传统的 Windows VC++ 控制台项目,结构大致如下:

文件职责
protocol.h / protocol.c协议层接口与调度逻辑,定义帧结构、状态枚举和各协议入口
datalink.h / datalink.c模拟物理链路,负责发送、接收、丢包、延迟和错误注入
crc32.cCRC32 校验算法,用来模拟帧在链路上传输后的检错过程
gbn.c / gobackn.c回退 N 帧协议实现,go back N 的核心逻辑都在这里
selective.c选择性重传 SR 协议实现,包含缓存窗口和选择确认
gbn_improve.cGBN 的改进版本,通常用来对比优化效果
lprintf.h / lprintf.c日志输出模块,格式化打印发送/接收/重传事件
getopt.h / getopt.c命令行参数解析,方便运行时指定窗口大小、帧数量等
datalink.sln / datalink.vcxprojVisual Studio 工程文件
README.md编译方式与运行说明

这个文件布局有一点值得学:它把链路模拟、协议策略、日志输出拆成三个独立层次。链路层(datalink)只管链路特性,比如丢包率、误码率、传播延迟;协议层(protocol + gbn/selective)只关心状态机和帧收发;日志层单独抽出 lprintf,这样调试时不用在代码里到处插 printf,所有输出都走统一函数。

2.2 链路模拟的核心机制:丢包和错误是怎么注入的

数据链路层模拟程序要演示滑动窗口,前提是链路必须不完美——如果发送一个就成功一个,那滑动窗口的优势根本看不出来。我翻了一下 datalink.c 的实现思路,它把一个“虚拟信道”抽象成三个参数:

  • 丢包率:每发送一帧,有固定概率直接在信道上被丢弃,接收方根本收不到;
  • 误码率:帧到达接收方但 CRC32 校验失败,接收方会丢弃该帧并认为它丢了;
  • 传播延迟:帧和 ACK 在链路上的往返时间,用定时器模拟。

这三个参数构成整个模拟环境的“干扰源”。窗口协议的作用就是在这种丢包环境下,保证数据最终正确交付。源码里通过一个全局配置结构体传入这些参数,你可以在运行时用命令行参数覆盖默认值。

2.3 协议的层次结构:发送方和接收方如何解耦

看 protocol.h 里的接口能发现一个关键设计:发送和接收两个方向分开处理。发送方维护自己的发送窗口和定时器,接收方维护接收窗口和缓存区。它们之间通过两个回调函数互相通知——一个是“帧从物理层到达”,另一个是“定时器超时”。这种设计让 GBN 和 SR 共用同一套链路接口,只需替换回调函数内部的策略,就能拿到不同协议版本。

我一般会先跑一遍原始工程,确认默认参数下 GBN 和 SR 都能跑通,然后才开始改。第一次运行建议从短帧序列开始,比如 10 帧、窗口 4,这样输出日志短,便于对照协议行为。

# 假设工程编译为 datalink.exe,常见运行方式如下 ./datalink --protocol=gobackn --frames=10 --window=4 --loss=0.1

逻辑说明:--protocol指定走哪个协议的实现,gobackn对应回退 N 帧;--frames是本次模拟的数据帧总数;--window是发送窗口大小;--loss是链路丢包率。参数解析由 getopt 模块完成,如果你在 Linux 环境下用 gcc 编译,需要保留 getopt.c 一起编译,因为 Windows 和 Linux 的 getopt 实现细节不同。

参数说明:丢包率 0.1 表示平均 10 帧里丢 1 帧,这是最容易看出 GBN 回退效果的区间。如果你想对比极端情况,可以设 0.3,这时候 GBN 的重传风暴会非常明显,SR 的优势也一眼可见。

3. 三种协议的核心实现:从停等到选择性重传的推进逻辑

这个工程最核心的价值在于同一套框架下实现了三种不同复杂度的协议,可以直接对比它们对同一丢包环境的反应。我看源码时的阅读顺序建议是:先读最简单的停等实现,再读 GBN,最后读 SR,因为后两者的代码大量复用前者的基础结构。

3.1 停等协议:滑动窗口的特例起点

停等协议在源码里其实没有单独文件,它是 GBN 在窗口等于 1 时的退化形态。但 protocol.c 里的调度逻辑会先判断窗口大小,如果等于 1 就进入停等模式。这一点很多新手会忽略——窗口设成 1 跑出来的日志,行为上等价于停等协议,不需要另写一份代码。

发送方的核心逻辑可以抽象成以下流程:

void sender_wait_ack() { while (next_frame_to_send < total_frames) { send_frame(next_frame_to_send); // 发送当前帧 start_timer(next_frame_to_send); // 启动该帧定时器 wait_for_event(); // 阻塞等待 ACK 或超时 if (event == TIMEOUT) { resend_frame(next_frame_to_send); // 超时则重发同一帧 } else if (event == ACK_RECEIVED) { stop_timer(next_frame_to_send); next_frame_to_send++; // 收到 ACK 才滑动窗口 } } }

逻辑说明:这是停等协议最原始的执行循环:发送一个帧,等它的 ACK,超时重发,收到 ACK 才推进。可以看到窗口滑动依赖 ACK 到达,这在窗口大于 1 时会成为性能瓶颈,于是就有了下面的 GBN。

参数说明:这个wait_for_event是模拟事件驱动,在真实网络里对应中断或 select 系统调用。工程里用回调和状态标记代替了真实阻塞,方便在单线程程序里模拟多帧并发。

3.2 GBN 回退 N 帧:连续发送与累计确认的矛盾

GBN 的实现集中在 gbn.c 和 gobackn.c 两个文件里,gobackn.c 是基础实现,gbn.c 可能是封装入口。它的核心思路是:发送方不等每个 ACK,而是在窗口内连续发帧;接收方只按序接收,一旦出现失序帧就丢弃后续所有帧,并重复发送最后一个有序 ACK。

GBN 发送方的关键代码逻辑可以这样理解:

void gbn_sender() { while (base <= total_frames) { // 填满发送窗口,尽量把窗口内的帧都发出去 while (next_seq < base + window_size && next_seq <= total_frames) { send_frame(next_seq); if (base == next_seq) start_timer(base); next_seq++; } wait_for_event(); if (event == TIMEOUT) { // 重新发送从 base 开始的所有帧 resend_from(base); start_timer(base); } else if (event == ACK_RECEIVED && ack_num > base) { // 累计确认:收到的 ACK 表示该序号之前都已被正确接收 base = ack_num; stop_timer(base); } } }

逻辑说明:这里最重要的一行是base = ack_num。GBN 使用累计确认,收到 ACK 序号 n 意味着序号小于 n 的所有帧都被接收方收到了,所以发送窗口直接滑到 n 之后。如果某个帧超时,发送方就要回退到超时帧的序号重新发送,后面所有帧即使当初已经发出去了也要作废重发——这就是名字里“回退 N”的来源。

参数说明:窗口大小与超时时间是需要一起调的。窗口设得过大而超时设得过短,程序很容易进入频繁重传的循环,日志里会看到大量重复发送的帧,这时候别怀疑代码有 bug,十有八九是超时阈值不合理。

3.3 SR 选择性重传:缓存乱序帧与独立确认的代价

SR 模块在 selective.c 里,和 GBN 最大区别在于接收方有一个缓存窗口。GBN 接收方收到乱序帧直接丢弃,SR 接收方把乱序帧先缓存起来,等缺失帧补齐后按序向上层交付。发送方也只需要重传超时的那个帧,不需要回退重发后续帧。

SR 接收方的缓存逻辑示意:

void sr_receiver(frame f) { if (f.seq >= recv_base && f.seq < recv_base + window_size) { if (f.seq == recv_base) { deliver_frame(f.seq); // 按序帧直接交付 recv_base++; // 检查缓存区里是否有连续帧可以跟着交付 while (buf[recv_base].valid) { deliver_frame(recv_base); recv_base++; } } else { buf[f.seq] = f; // 乱序帧先存缓存 send_ack(f.seq); // 对每个正确收到的帧单独确认 } } }

逻辑说明:这个buf[f.seq] = f是 SR 的核心。接收窗口内的乱序帧不再被丢弃,而是占据一个缓存槽。确认信号也是针对单个序号的,不是累计确认,所以发送方知道哪些帧真正到达了。代价是接收方需要维护一个窗口大小的缓冲区,且每个 ACK 都要携带明确的序号。

参数说明:SR 的窗口大小有个数学约束,一般要求发送窗口与接收窗口之和不超过序号空间大小,否则无法区分新旧帧。这份源码默认用的是固定序号范围,如果你把窗口调得过大(比如序号空间 16 但窗口设为 12),运行时会看到收发的帧序号出现歧义,日志会变得混乱。遇到这种情况先检查这个约束,别急着改代码。

对比这三者可以看出一个结论:停等是 GBN 窗口为 1 的特例,GBN 是 SR 不设接收缓存的退化形态。从算法演进角度来看,这份源码本身就是一份很好的“协议演进”教材,这也是它评审分高的原因——不是花哨,而是层次清晰、几个对比实现的边界关系讲得明白。

4. 编译与运行避坑:我从这份源码里踩过的五个实际坑

任何课程设计源码都不可能零配置跑通所有环境,这份资源也不例外。我把在本地编译运行过程中遇到过的典型问题和排查思路整理出来,按照“现象→原因→解决”的顺序写,方便你对照排查。

4.1 现象:Visual Studio 打开 .sln 后编译报错,提示找不到某些头文件

原因:压缩包解压时如果直接双击打开 zip 再拖出文件,部分嵌套目录结构没有保留,导致 protocol.h、getopt.h 等相对引用失效。

解决:把整个压缩包完整解压到英文路径下,比如D:\lab\sliding-window\,不要用中文目录。然后打开 datalink.sln,在解决方案资源管理器里检查所有 .c 文件是否都出现在项目里,缺哪个就右键“添加→现有项”补上。这个坑 80% 是解压层级不对,不是代码问题。

4.2 现象:编译能过,但运行后没有任何输出,或者日志卡住不结束

原因:getopt 解析参数失败时,程序进入等待输入状态,或者默认的帧数量很大、丢包率很高,程序在长时间模拟中。另一个常见原因是控制台窗口缓冲太小,大量输出被折叠,看起来像“没有输出”。

解决:先试着用最小参数跑:--frames=5 --window=2 --loss=0。如果无丢包情况下能干净地跑完 5 帧,说明框架没问题。卡住的话检查是不是--frames参数没生效,默认值可能非常大;日志不显示的话把控制台缓冲区高度调大,或者重定向到文件里看:./datalink > run.log 2>&1。

4.3 现象:把丢包率设到 0.3 之后,GBN 重传次数爆炸,日志显得乱七八糟

原因:这不是 bug,是 GBN 在高速重传下的真实行为。丢包率越高,回退范围越大,冗余重传指数级增长。很多人第一次看到 GBN 高丢包日志会以为程序死循环了。

解决:先确认程序是否在有限步骤内结束。如果长时间跑不完,把--frames减小到 20 以下观察。想对比 SR 优势,就在同一丢包率下分别跑 GBN 和 SR,数一下总发送帧数。SR 的总发送次数会明显少于 GBN——这个数字能直接写进课程设计报告作为实验数据。

4.4 现象:修改了协议参数后重编译,发现改动不生效,行为还是老样子

原因:最常见的是没有重新生成工程,只重新编译了单个文件;另一个是 Visual Studio 的增量编译缓存,把 .obj 文件当作已是最新,没有重新编译修改过的 .c。

解决:手动“清理解决方案”再“重新生成解决方案”,或者直接删掉 Debug/Release 目录下的所有 .obj 和 .pch 文件再编译。如果你在 Linux 下用 gcc,记得把新加的 .c 文件写进编译命令,比如gcc -o datalink datalink.c protocol.c gbn.c selective.c gobackn.c crc32.c lprintf.c getopt.c -lm,漏了一个文件可能程序永远走的是旧逻辑。

4.5 现象:程序在 Windows 下输出中文乱码,某些日志格式错乱

原因:README 里的源码可能按 UTF-8 保存,而 Visual Studio 默认控制台用 GBK 编码读取,导致中文字符串显示为乱码。

解决:有两种办法,一是把涉及中文输出的 .c 文件用 VS 的“文件→高级保存选项”转存为简体中文(GB2312)编码,再重新编译;二是直接改成英文日志输出,把lprintf里的格式字符串换成英文。第二种更省事,而且答辩演示时英文日志反而显得更规范。

5. 进阶玩法:把模拟程序改成可视化对比工具,验收时更出彩

前四章都在讲怎么跑通和理解这份源码,最后说一下怎么让它从“能跑的课程设计”变成“答辩时能讲出深度的实验工具”。我自己的习惯是拿到任何协议模拟源码,第一步先不急着改逻辑,而是先加一个统一的统计出口,把所有协议跑在同一组链路参数下,生成可对比的输出。

具体做法是在主循环里增加一个统计结构体,记录发送总帧数、重传帧数、ACK 总数、超时次数、最终吞吐量这几个关键指标,然后把协议实现里所有的发送和重传动作都通过一个宏来计数:

#define TX_LOG(seq, reason) do { \ tx_count++; \ if (reason != REASON_FIRST) retx_count++; \ printf("TX seq=%d reason=%s\n", seq, reason_str[reason]); \ } while (0)

逻辑说明:tx_count统计所有发送动作,retx_count区分首传和重传。reason用枚举值标记这次发送是首次发送还是超时重发、抑或是 GBN 回退后的批量重传。这样每个协议跑完后,你只需要比较tx_count和retx_count就能定量说明 GBN 和 SR 在丢包环境下的效率差。

参数说明:这个宏加在 datalink.c 的 send 接口附近最合适,因为所有协议最终都会调到统一的物理层发送函数,钩子放在这里不会漏掉任何发送路径。如果你想把 GBN 的三种重传原因也区分开,可以扩展枚举值:超时重传、收到重复 ACK 后回退、接收方 NACK 触发回退。

再加一个可视化手段:把lprintf的日志输出到文件后,用简单的脚本从日志里提取每条记录的序号和时间戳,画成一张发送/确认时序图。这段脚本不需要很复杂,用 Python 读日志、按帧号分组、标记 ACK 到达点,就能出一张清晰的时序图。答辩时放这张图,比贴几百行源码更能说明你对协议行为真的理解到位了。

这份源码我前后在不同环境里编译过几次,最大的感受是:它的价值不全在“能跑通”,而在于把三种协议的差异放在同一个模拟链路里做对比,这比单独写任何一个协议都要容易讲出深度。从那以后我每次做协议相关的模拟,都强制要求自己先加统计出口、再做功能调试,先保证能度量,再说优化。这个习惯帮我避过不少“感觉跑对了但说不出哪里对”的尴尬情况。希望这份拆解能让你少走几步弯路,把时间花在真正理解协议行为上,而不是浪费在解决环境问题里。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:14:49

番茄成熟度检测数据集实战:VOC+YOLO格式与训练避坑指南

搞目标检测的人应该都有同感&#xff1a;模型结构能抄、训练trick能背&#xff0c;唯独数据这件事&#xff0c;真不是想有就能立刻到手的。尤其农业场景&#xff0c;你得蹲大棚、等果实长到不同成熟度、还要凑光照合适的天气去采集&#xff0c;难度和成本都不低。所以看到“智慧…

作者头像 李华
网站建设 2026/10/19 9:13:32

EasyCode实战:从IDEA插件到自定义模板,彻底告别重复CRUD代码

做新项目最怕什么&#xff1f;我现在的答案不是业务逻辑复杂&#xff0c;而是大量结构完全一致的 CRUD 代码要在十几个文件里反复手敲。一个表建完之后&#xff0c;实体、Mapper、Service、ServiceImpl、Controller、XML 哪个字段改了就是一串文件联动着改&#xff0c;改完还得…

作者头像 李华
网站建设 2026/10/19 9:13:26

从零搭建CRM:数据模型、权限与跟进机制的实战复盘

DeskcommCRM 并不是一个商业产品&#xff0c;它是我给一家销售型小团队从零搭起来的内部客户关系管理系统。刚开始立项的时候&#xff0c;老板的想法很简单&#xff1a;把销售手里的客户资料从 Excel 和微信聊天记录里救出来&#xff0c;放到一个所有人都能看得见、又不会互相乱…

作者头像 李华
网站建设 2026/10/19 9:13:21

YashanDB数据库安全加固:认证、权限、加密、审计与高可用

数据库安全这件事&#xff0c;说大不大&#xff0c;说小不小。很多团队在业务上线之初&#xff0c;优先盯着功能和性能&#xff0c;把安全当成“后期再补”的选项&#xff1b;等到数据泄露、误删、越权这些事故真发生了&#xff0c;才意识到数据库的安全防线往往就是业务的生命…

作者头像 李华
网站建设 2026/10/19 9:12:22

OpenClaw:从对话AI到数字员工,打通自主执行的最后一公里

最近和一个做运维的朋友聊天&#xff0c;他吐槽了一个特别真实的场景&#xff1a;公司里已经接入了各种对话式AI助手&#xff0c;大家日常查资料、问问题都靠它&#xff0c;但真要让它把一张报表整理好、把几十个工单处理完、把周报写好发到群里&#xff0c;还是得自己动手。AI…

作者头像 李华
网站建设 2026/10/19 9:12:12

RDP暴力破解实战:Hydra使用中的5个常见错误与解决思路

1. 一次RDP爆破测试引发的思考先说说我为什么想写这篇文章。前阵子帮某公司做内网渗透测试&#xff0c;目标是一台对外开放的Windows Server。常规扫描发现3389端口开着&#xff0c;管理员账号还是默认的Administrator&#xff0c;口令强度也一般。按流程做口令爆破测试&#x…

作者头像 李华