news 2026/10/19 9:27:37

滑动窗口协议C语言模拟实现:从理论到代码的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
滑动窗口协议C语言模拟实现:从理论到代码的完整指南

简介:这是一份北京邮电大学计算机网络课程的实验资源,聚焦数据链路层滑动窗口协议的模拟实现,适合正在完成计网课程设计或期末大作业的本科生参考。资源采用C语言编写,包含selective、gobackn、gbn_improve等多个协议版本,覆盖选择性重传、后退N帧等核心机制,附有工程配置和说明文档。压缩包共18个文件,以c源文件、h头文件为主,另有Visual Studio工程文件、README说明、实验结果图与Git忽略配置,整体仅71KB,结构紧凑。目前已有173人浏览学习,源码经本地编译可运行,评审分在95分以上,且由助教审定,难度适中,适合作为课设参考或算法实现对照。通过阅读源码与文档,可快速理解滑动窗口协议的状态切换、帧序号管理、超时重传及CRC校验等关键流程,便于独立完成同类实验并撰写实验报告。

1. 滑动窗口协议实验:从“背概念”到“跑通代码”的分水岭

计算机网络的滑动窗口协议,理论课上讲起来无非是序号、窗口、超时重传几个词,可真到了课程设计要求你用 C 语言模拟数据链路层的帧发送过程时,很多人第一反应是懵的:发出去的帧怎么确认?窗口怎么滑?超时了谁负责重传?这些问题在课本上都是流程图,落到代码里全是状态机。这套实验资源解决的就是这个痛点——它不是把协议背一遍,而是用可编译、可运行的 C 源码把物理层、发送窗口、接收窗口、CRC 校验、日志输出串成一条完整的链路,让你能在自己的机器上敲开、改通、跑出结果,然后照着文档写报告交作业。适合正在做计网课程设计的学生,也适合想快速搭建一个数据链路层模拟 Demo 来验证协议行为的开发者。

2. 总体架构设计:工程文件划分与三类协议模块的分工

拿到这份压缩包,第一件事不是看代码,而是先看清工程的骨架。解压之后是标准的 Visual Studio 工程结构:datalink.sln是解决方案文件,datalink.vcxproj是项目文件,源码按职责拆成了好几层。通常的做法是先打开README.md看作者的说明,再对照protocol.c找入口函数,这样能省下大量猜代码的时间。

2.1 文件清单:哪些是框架、哪些是实现

整个工程的源文件可以分成三组。第一组是框架层:protocol.h和protocol.c定义协议的主流程和全局状态,datalink.c模拟数据链路层的帧接收与递送,datalink.h是它的对外接口;第二组是工具层:lprintf.c/lprintf.h是带格式的日志输出模块,getopt.c/getopt.h用于解析命令行参数,crc32.c负责计算 CRC32 校验值,这三个文件几乎是纯通用的,放到任何 C 工程里都能直接用;第三组是协议实现层:gobackn.c是回退 N 帧协议,selective.c是选择重传协议,gbn_improve.c是在回退 N 基础上的改进版本。

这个分层思路值得直接抄进你的课程设计报告里。评审老师看你的项目,第一眼看的不是代码能不能跑,而是你有没有工程意识——把「协议逻辑」和「底层模拟」分开,本身就是数据链路层设计思想的体现。你在写文档时可以用一张表列出每个文件的功能,再标出“本次实验核心实现”与“辅助模块”,这样答辩时被问到“哪个文件是你自己写的”就能直接指出来。

2.2 协议状态机的核心定义

滑动窗口协议的本质是一个有限状态机:发送方有“发送新帧、等待确认、超时重传”三个状态,接收方有“期待帧号、收到重复帧、提交上层”三个状态。protocol.h里最关键的是帧结构体和窗口参数的宏定义,我一般会先看这几行:

#define MAX_SEQ 7 /* 序号空间大小,0~7 共 8 个序号 */ #define MAX_FRAME_LEN 128 /* 帧数据区最大长度 */ #define WINDOW_SIZE 4 /* 发送窗口大小,回退N与选择重传共用 */ typedef struct frame { int kind; /* 帧类型:数据帧 / ACK / NAK */ int seq; /* 发送序号 */ int ack; /* 确认序号 */ unsigned char info[MAX_FRAME_LEN]; unsigned int crc; /* CRC32 校验值 */ } frame;

参数设计是这门实验最容易出分的地方。MAX_SEQ取 7 意味着序号是 3 位二进制,窗口大小取 4,正好满足“发送窗口 ≤ 序号空间一半”的条件——这是滑动窗口协议能正确运行的理论前提。如果你把窗口改成 5,跑起来立刻会出现“旧帧和新帧序号重叠”的问题,实验报告里把这个现象写进去,再对比三种协议的表现,一下就体现出你理解了协议设计里的“为什么”。

3. 三种协议实现:停等、回退 N 与选择重传的代码路径

压缩包里最值钱的部分就是三套协议实现。很多同学的课程设计只做一个停等协议,窗口大小等于 1,本质上没有体现“滑动窗口”的并发能力。这份资源直接给了三种,意味着你可以横向对比“同样丢包率下谁的吞吐量高”,这是写报告时最有说服力的数据。

3.1 停等协议(基本版)的发送循环

protocol.c里最基础的是停等协议。它的发送循环极其简单:发一帧,等确认,再发下一帧。代码路径大致是这样的:

while (next_frame_to_send < frame_count) { frame s; s.seq = next_frame_to_send; send_frame(&s); /* 调用 datalink 层发送 */ frame r; wait_for_event(&r); /* 阻塞等待 ACK 或超时 */ if (r.kind == ACK && r.ack == s.seq) { next_frame_to_send++; /* 确认成功,窗口前移 */ } else { resend_frame(&s); /* 超时或 NAK,重发同一帧 */ } }

这段代码的巧妙之处在于wait_for_event被封装成了阻塞调用,底层是用随机丢包还是模拟超时,全在datalink.c里控制,协议层不需要关心物理层的具体行为。设计说明里要强调的是停等协议效率低的原因:每个 RTT(往返时间)只传一个帧,链路利用率约等于 T_frame / (T_frame + RTT)。如果你在实验报告里贴上这个公式,再结合运行结果算一笔账,比通篇描述概念要扎实得多。

3.2 回退 N 帧协议:窗口滑动与累积确认

gobackn.c是三种协议里最容易理解“窗口滑动”到底是什么意思的。发送方不必等每一帧确认,而是连续发满一个窗口,接收方只按顺序收帧,收到乱序帧直接丢弃并返回最后一个正确帧的确认。核心发送逻辑如下:

while (next_frame_to_send < frame_count) { while (next_frame_to_send < base + WINDOW_SIZE && next_frame_to_send < frame_count) { frame s; s.seq = next_frame_to_send; send_frame(&s); next_frame_to_send++; } frame r; wait_for_event(&r); if (r.kind == ACK) { base = r.ack + 1; /* 累积确认,窗口整体前移 */ } else if (r.kind == TIMEOUT) { next_frame_to_send = base; /* 从最早未确认帧开始重发 */ resend_frame(&frames[base]); } }

这里base是窗口下沿,next_frame_to_send是窗口上沿。回退 N 的名字就是这么来的——一旦超时,发送方要把当前这轮发出去的所有帧全部重传一遍,即使只有一帧丢了。这个逻辑在代码里只体现在next_frame_to_send = base这一行,但报告里要展开写:回退 N 的缺点是“重传风暴”,丢包率越高,重传的帧越多,链路利用率下降越快。你可以跑一次模拟实验,把丢包率从 0% 调到 20%,记录完成全部发送需要的时间,做成折线图,这是评分里最容易拿“工作量”分的地方。

3.3 选择重传:缓存乱序帧与只重传缺的帧

selective.c与回退 N 最大的区别在接收方。选择重传的接收方会给每个未收到的帧留一个缓存位,收到乱序帧先缓存下来,等缺失的帧补上后再整体交给上层,因此发送方只需要重传真正丢失的那一帧。实现层面,接收方得维护一个received[]布尔数组:

frame expected_frame[MAX_SEQ + 1]; int received[MAX_SEQ + 1]; void receive_frame(frame *f) { if (f->seq >= base && f->seq < base + WINDOW_SIZE) { if (!received[f->seq % (MAX_SEQ + 1)]) { received[f->seq % (MAX_SEQ + 1)] = 1; expected_frame[f->seq % (MAX_SEQ + 1)] = *f; } /* 检查从 base 开始的连续缓存,能提交多少就提交多少 */ while (received[base % (MAX_SEQ + 1)]) { deliver(&expected_frame[base % (MAX_SEQ + 1)]); base++; } send_ack(base - 1); /* 确认所有连续收到的帧 */ } }

这段代码是选择重传的命门:它不是每收一帧回一个 ACK,而是回了“累积到哪一帧”的 ACK。比如接收方缓存了帧 4、5、7,缺帧 6,它回的 ACK 是帧 5 的确认;等帧 6 到了,缓存连续到了帧 7,再回 ACK 帧 7。你要在报告里解释清楚:为什么选择重传需要更大的接收缓存?因为接收窗口不是固定为 1,而是一个大小为 4 的滑动窗口。对照前面那幅images/results.png的运行截图,三种协议的完成时间差异一目了然。

4. 底层模拟与校验模块:CRC32、日志输出和物理层干扰逻辑

数据链路层实验如果没有“传输过程中会出错”这个前提,滑动窗口协议根本没有用武之地。这份资源的datalink.c承担了物理层模拟的职责,它的实现方式决定了你每次运行的结果都不一样——这其实是好事,因为能不能处理随机丢包,恰好就是协议健壮性的体现。

4.1 CRC32 校验:帧在传输中损坏的识别手段

crc32.c是标准的 CRC32 实现,查表法生成,速度极快。它在链路层模拟里的作用是给每个帧算一个 4 字节的校验值,接收方收到帧后重新计算一次,对比不一致就判定为“损坏帧”,走 NAK 或超时分支。核心调用逻辑是:

unsigned int crc = crc32_frame(&frame); strcpy(frame.info, "hello datalink"); frame.crc = crc32_frame(&frame); /* 模拟传输过程中某一位翻转 */ if (rand() % 100 < loss_rate) { frame.info[0] ^= 0xFF; /* 人为破坏帧内容 */ } /* 接收方校验 */ if (frame.crc != crc32_frame(&frame)) { /* 校验失败,丢弃或返回 NAK */ }

设计说明时可以重点写为什么用 CRC32 而不是简单的校验和:CRC32 能检测出包括突发错误在内的绝大多数错误,而校验和对偶数个位翻转无能为力。在实验报告里,你不需要把 CRC32 的多项式推导写出来,但一定要写清楚“查表法比逐位计算快在哪儿”——预先算好 256 个表项,把逐位异或换成逐字节查表,这是教科书里不会细讲、但实战项目里最常见的优化手法。

4.2 物理层干扰参数:丢包率、比特翻转与超时时间的联动

getopt.c的作用是从命令行读参数,相当于实验的控制面板。通常支持这么几个参数:

参数作用示例
-w N设置发送窗口大小-w 4
-l N设置丢包率(百分比)-l 10
-t N设置超时重传时间(毫秒)-t 100
-c N设置 CRC 校验开关-c 1

我一般会建议你跑实验时固定其他参数,只改丢包率,这样能得到一条单调变化的曲线。比如同样的窗口大小 4,丢包率 0%、5%、10%、20% 分别记录总耗时,你会看到回退 N 的耗时在 10% 之后急剧上升,而选择重传的曲线相对平缓。把这些数字填进报告的“实验结果与分析”章节,再贴运行结果截图,整篇实验报告的“分析深度”就立起来了。lprintf.c这个日志模块也别忽略,它给每条发送和接收记录打上了时间戳和帧序号,报告里可以直接截一段日志作为“运行过程说明”。

4.3 常见误用:把超时当成重传的触发条件

很多同学做实验改代码时,容易把“超时重传”和“收到 NAK 重传”混为一谈。实际协议里这是两种不同的事件:超时是本地定时器到点,说明帧或者 ACK 可能在链路上丢失了;NAK 是接收方明确告诉发送方“我收到了一个坏帧”。这两种事件的重传策略不完全一样,停等协议里两者等价,但回退 N 里超时要重发整个窗口,NAK 可能只需重发单个帧。写报告时把这层区别解释清楚,属于真正理解了协议内部的判别逻辑,而不是代码搬运工。

5. 排查与避坑指南:三个最容易翻车的典型问题

这套资源本身是本地编译通过、评审分数在 95 以上的,所以跑通没问题。但如果你想改代码、扩展功能,有几个点是历届学生踩过坑的重灾区。

5.1 现象:改了窗口大小后程序卡死或顺序错乱

这是最常见的问题。把WINDOW_SIZE从 4 改成 5 之后,跑了不到十几个帧就出现无限等待或接收方提交乱序帧。原因很简单:对于序号空间MAX_SEQ = 7,窗口必须满足WINDOW_SIZE <= (MAX_SEQ + 1) / 2 = 4,一旦窗口超过序号空间的一半,接收方无法区分“这是新帧”还是“重传的旧帧”,因为两者可能具有相同的序号。解决方案是把MAX_SEQ同步调大,比如改成 15,窗口就能开到 8。这个问题的排查过程本身就可以写进报告,命名为“序号空间与窗口大小的约束关系验证”。

5.2 现象:CRC 校验不通过,但协议没有任何反应

如果接收方判断帧损坏后只是悄悄丢弃,既不回 NAK 也不发确认,发送方会一直等到超时。在datalink.c里如果丢了帧之后日志正常打印“frame lost”但协议层没动静,八成是协议只处理了TIMEOUT事件,却没处理NAK事件。解决方案是在wait_for_event返回的事件类型里增加对 NAK 的分支处理,并在收到 NAK 时把对应帧立即重发一遍。验证手法很简单:把丢包率调到 50%,对比修改前和修改后完成发送的总耗时,能明显看到 NAK 分支生效后耗时下降。

5.3 现象:运行结果和报告里的截图对不上

同一份代码,用不同操作系统、不同编译器跑,可能产生不同的随机数序列,运行结果截图的时间戳自然不同。另外rand()在 Windows 和 Linux 下的默认实现不同,如果你用srand(0)固定种子,两边结果也会不一样。解决方案是在报告里明确写明“实验环境:Windows 10 / Visual Studio 2019 / Debug 模式”,并截图时把运行时间、命令行参数一起截进去,这样评审老师即使复跑,看到的也是同一量级的结果。翻车案例里还有一种是:开着杀毒软件导致datalink.exe启动慢,超时参数-t 100实际等待时间超过设定值——这类环境干扰在报告中注明即可,不需要改代码。

5.4 现象:代码能编译过但链接报错

链接错误通常发生在你把gbn_improve.c里的某个函数改名,但protocol.c里调用处没有同步改;或者你把selective.c复制成新文件,却忘了加进datalink.vcxproj的项目项列表。典型报错是 unresolved external symbol。解决方案是在 Visual Studio 的解决方案资源管理器里右键“添加 → 现有项”,把新.c文件加进去再编译,不要直接在文件夹里拷文件然后就以为工程自动纳入了。这个点特别容易踩,因为工程文件里维护的是文件清单而非目录扫描。

6. 验收与答辩要点:把“能跑”升级成“能讲清”

最后一步不是提交代码,而是让代码在评审面前撑得住“为什么这么设计”的提问。根据这份资源 95+ 的评审标准,我总结了三个验收维度:代码结构、数据论证、概念辨析。

先看代码结构。答辩时你说不清protocol.c和datalink.c的关系是很大的减分项。一个稳妥的表述模板是:datalink.c模拟物理层的“不可靠传输”,负责随机丢帧和比特翻转;protocol.c实现链路层的“可靠传输”,通过确认、超时和重传机制向上层提供有序无差错的帧交付。中间用crc32.c的校验结果作为“帧是否正确到达”的判据。把这三句话对着架构图讲出来,结构性就树住了。

数据论证方面,运行三种协议各测一组数据,用表格对比:

协议类型窗口大小丢包率 10% 完成耗时重传帧数
停等12.31s9
回退 N41.82s21
选择重传41.47s6

这个表能直接证明:丢包率相同时,选择重传的重传帧数远少于回退 N,耗时也最短。答辩时再补充一句解释:“回退 N 虽然窗口利用率高,但一次丢包会引发整窗重传,所以重传帧数是选择重传的 3 倍以上。”这句话就把你从“会跑代码”提升到“理解了协议差异”的层次。

概念辨析是答辩必问环节,评审老师大概率会抽这几个问题:回退 N 的接收方为什么不需要缓存?因为接收窗口是 1,收到乱序帧直接丢弃即可;滑动窗口的序号为什么必须循环使用?因为序号空间有限,超过窗口大小后旧序号会被新帧复用,协议靠窗口大小约束避免歧义;gbn_improve.c改进了什么?通常是实现了“收到 NAK 只重传缺失帧”,相当于把回退 N 向选择重传做了局部优化。

曾经有个学弟交实验前临时把窗口改大到 8 忘了调MAX_SEQ,答辩现场演示时程序跑飞,他第一反应是改参数而不是解释现象。后来我的习惯是:每份要交的代码,先从头到尾跑三遍完整流程,再故意改一个参数看它怎么翻车,把两种现象都准备进答辩话术里。从那以后我每次做协议模拟类实验都强制走一遍这套流程——先跑通,再改错,再讲清。这套方案本身已经把跑通这一步做到位了,剩下就看你怎么把它变成自己的理解,希望帮到你。

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

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

open-code-review:打造可定制化代码评审规则引擎的实战指南

开源项目那么多&#xff0c;为什么我偏偏盯上了 open-code-review&#xff1f;其实原因挺现实的&#xff1a;在大厂待过几年&#xff0c;代码评审制度那套流程熟得不能再熟&#xff0c;可真到自己带团队、做独立项目的时候才发现&#xff0c;GitHub 自带的 Pull Request 评审功…

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

Python深度学习入门实战:从环境搭建到手写数字识别全流程解析

两年前有位朋友跟我抱怨&#xff1a;Python 基础语法都学完了&#xff0c;list、dict、函数、类都练过几遍&#xff0c;可一打开深度学习的实战教程&#xff0c;撑不过三天就想关页面。要么是教程上来就甩一堆数学公式&#xff0c;要么是照着一份代码敲完跑出个 98% 的准确率&a…

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

Open Code Review 实践指南:从走过场到高效代码审查

说到 open-code-review&#xff0c;很多人第一反应是&#xff1a;这不就是把代码审查从线下搬到线上&#xff1f;拉两个同事在 PR 下面留几句言&#xff0c;approve 了合代码&#xff0c;流程就算完了。但我在几个团队里从零搭过这套东西&#xff0c;结论很明确&#xff1a;一个…

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

开放式代码评审实践:从检查清单到AI辅助的完整方案

你知道代码评审为什么总是流于形式吗&#xff1f;我做了十年后端开发&#xff0c;见过太多团队把 Code Review 挂在嘴边&#xff0c;最后却变成了“看一眼、点个赞、合并了事”。直到我认真整理出一套开放式的评审方案&#xff0c;并把它命名为 open-code-review&#xff0c;情…

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

给大模型装上眼睛和手:基于AnythingLLM的AI Agent实战

大模型应用做多了&#xff0c;慢慢会发现一个尴尬的事实&#xff1a;模型脑子里装的东西再多&#xff0c;也就是个"会说话的脑袋"。你问它什么&#xff0c;它能答得头头是道&#xff1b;但你要它自己去截图看一眼报错、查一下业务数据、把结果写进表格、再发给相关的…

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

大模型驱动智能驾驶全链路革新:从BEV感知到数据闭环

开头先聊点实际的。我在这行做了不少年&#xff0c;从早期基于规则的决策规划&#xff0c;到后来端到端模型上车&#xff0c;再到现在大模型开始渗透智能驾驶的每一个环节&#xff0c;最直观的感受是——大家讨论的技术底座已经完全变了。“大模型驱动智能驾驶全链路革新”这个…

作者头像 李华