滑动窗口协议C语言实现:计网实验答辩级源码解析
简介北京邮电大学计算机网络课程模拟数据链路层滑动窗口协议的实验源码与文档包面向计网课程学生、准备期末大作业或课程设计的高校开发者资源围绕数据链路层的可靠传输核心演示滑动窗口、回退N帧、选择性重传等机制适合有一定C语言基础、希望对照实现链路层协议的读者。压缩包共18个文件以C源码和头文件为主体覆盖选择性重传、回退N帧、改进GBN等不同协议版本同时包含CRC32校验、参数解析、链路层封装实现附带Visual Studio工程、README说明与结果截图整体仅71KB目录划分明确便于按需查阅。源码均已在本地编译通过评审分达95分以上难度适中且经助教老师审定读者可直接运行观察运行截图也可在此基础上改造窗口大小、帧格式或校验策略。目前已有173人学习适合快速上手北邮计网实验要求作为C语言网络编程与协议设计的参考模板。1. 计网实验里的滑动窗口协议一份能拿去答辩的 C 语言实现长什么样在计网实验里“模拟数据链路层的滑动窗口协议”是典型的期末大作业题目看着简单动手就崩。很多人第一反应是写个脚本模拟丢包重传但课程设计最终要求的是协议状态机的正确性而不是黑盒应用。这份资源提供的是 C 语言实现源码能在本地编译运行评审分达到 95 分以上覆盖停等、回退 N、选择重传三种模式并附文档说明。适合谁用正在做计网实验或期末大作业的学生以及想把滑动窗口状态机一次捋清楚的从业者。下面按方案落地的顺序把这套源码的设计思路、关键实现和验收方法一次讲清楚。2. 需求分析与技术选型三种协议模式要覆盖到什么程度2.1 从题目要求里先拆出三个硬指标一般这类课设题目不会直接说“实现滑动窗口”而是给几个隐含条件。第一个硬指标是模拟数据链路层的帧收发不能靠 TCP socket 绕过去第二个是至少实现两种协议并能切换最好能对比停等、回退 N 和选择重传第三个是运行过程要能观察窗口滑动、超时重传和 CRC 校验不是只在控制台打印“发送成功”。很多同学拿到题目就找轮子找到能运行的源码先庆祝结果答辩被问“协议模式怎么切换”就愣住因为代码里没有策略入口协议逻辑全写在 main 函数里。这套资源的 protocol.c 承担的就是策略入口selective.c、gobackn.c、gbn_improve.c 各实现一种策略protocol.h 暴露统一接口。从文件结构能看出它不是堆三个 main而是把公共链路层逻辑抽出来了这样设计才支撑得住三个硬指标。2.2 为什么用 C 而不是 Python 模拟一轮课设做下来我最想强调的选型结论是滑动窗口实验用 C 语言写比用 Python 写脚本更容易拿高分。评分点集中在“协议是否正确、代码是否分层、参数是否可配置”C 的结构体、指针和位操作能把帧格式、缓冲区、状态机直接映射到代码里老师查代码时一眼能看到链路层的影子。Python 写起来确实快但列表和字典会掩盖缓冲区管理和字节序细节答辩时老师挑个细节追问就容易露馅。更重要的是这里选择的是进程内模拟信道而不是真实 socket。链路层实验只关心序号、确认、窗口和超时这些逻辑不需要经过真实网卡真实网络会引入丢包、乱序、延迟抖动结果不可复现。进程内模拟把丢包率设为参数固定随机种子后同一参数跑出来的结果完全一致截图和日志能对上答辩自然有说服力。2.3 命令行参数和验收演示的对应关系getopt.c 和 getopt.h 的存在说明这套代码支持命令行参数运行。通常支持的参数包括协议模式、窗口大小、丢包率和日志级别。参数作用验收演示怎么用-p/--protocol切换 stop_and_wait、gobackn、selective、gbn_improve现场切换三种协议对比重传策略-w/--window设置发送窗口大小停等协议固定为 1改窗口大小看窗口滑动差异-l/--loss_rate模拟信道丢包率0~100设 0、10、30 跑三组观察重传-v/--verbose是否输出详细日志演示时开截图时留关键日志这个参数设计不只是在演示时好看它直接对应评审表里的“可配置性”。老师现场要“把丢包率调到 30 再看看”如果你要改代码重编译才能做到当场就败了。用 getopt 解析还有个好处参数错误时有 usage 提示配合 README 里的说明助教不需要看代码就知道怎么启动。我一般会检查 main 函数开头是否对参数做了校验例如窗口大小不能为 0、丢包率必须在 0 到 100 之间这些边界值提前挡掉演示时才不会出现除零或数组越界。2.4 源码分层从文件依赖关系读代码拿到源码不要从第一行开始读。先看 protocol.h 和 datalink.h 的接口声明再看 protocol.c 的调用关系最后才看 selective.c 和 gobackn.c。datalink.c 是主入口负责初始化信道、解析参数、循环驱动发送protocol.c 是协议分发各协议文件负责窗口状态机crc32.c 和 lprintf.c 是工具层。用工程的话说这是“主流程—策略层—工具层”三层划分。原资源里有 datalink.vcxproj 和 datalink.sln说明在 VS 里可以直接打开编译Linux 下用 gcc 编这些 C 文件时记得把 getopt.c 一起编因为不同平台 getopt 的行为有差异使用项目自带的 getopt 能避免环境不一致。README.md 一般会写清楚编译命令和参数示例我在拿到资源后第一件事是照 README 跑一遍默认参数确认能复现 results.png 里的输出再开始读代码。2.5 窗口大小与序号空间的设计边界在滑动窗口协议里序号空间必须足够区分“新帧”和“重传的旧帧”通常要求序号空间大于最大发送窗口的两倍。这套资源里 frame_t 的 seq 字段是 uint8_t序号范围 0~255实验中的窗口大小一般取 4 或 8完全够用。但代码里必须对 seq 取模否则窗口设到 16 后接收端会分不清新帧和重传帧。selective.c 里接收缓存数组按窗口大小分配索引不能直接用 seq 当数组下标应该先判断 seq 是否落在接收窗口内再对缓存大小取模后写入。这个边界我重点检查过因为很多自己写的代码窗口小的时候正常窗口一调大就偶发越界。对应到实际工程里序号位数、窗口大小、缓冲区大小三者要一起设计不是随便选一个窗口大小就能跑。2.6 链路层模拟和 TCP 重传的区别还有一个容易混的点滑动窗口协议不是 TCP。TCP 的滑动窗口按字节流编号有拥塞控制、RTT 估算和快速重传这个实验的窗口按帧序号编号只解决可靠传输不解决拥塞。所以把 TCP 的重传经验直接套到链路层代码里会出问题比如 TCP 收到三个重复 ACK 会快速重传GBN 收到重复 ACK 只是确认接收端还缺哪一帧不应该触发重传。链路层协议里超时重传的触发条件就是超时器不在 ACK 数量上做文章。代码里 start_timer 和 stop_timer 的粒度是“序号”而不是“连接”这也是和 TCP 最直观的区别。理解这一点答辩时被问到“超时时间怎么定”才不会拿 RTT 估算公式硬套链路层实验里通常就是固定超时值或者按模拟信道的延迟加一个余量。2.7 协议实现里的四个状态变量在阅读 selective.c 和 gobackn.c 之前建议先记住四个状态变量base发送窗口底部、next_seq下一个待发序号、expect_seq接收端期望的下一帧序号、window_size发送窗口上限。在代码里它们的命名可能各不相同但角色是一样的。datalink.c 负责初始化这些变量protocol.c 可能在切换协议时重置它们。我见过一种低年级写法把 base 和 next_seq 定义成全局变量三种协议到处改结果切模式后状态残留。正确做法是每种协议一个上下文结构体切换协议时独立初始化。这个资源如果按文件分工看selective.c 和 gobackn.c 内部应该各自维护上下文不共享全局窗口变量。答辩被问“为什么 GBN 切到 SR 不会串状态”回答“各自有独立 ctx”就比解释“我直接改了全局变量”稳得多。3. 总体设计与核心实现从帧结构到窗口状态机的完整流程3.1 帧结构先定义清楚链路上传什么链路层协议的第一步永远是帧结构。这套源码的 protocol.h 里定义了 frame_t核心字段包括帧类型 type、序号 seq、确认号 ack、长度 len、CRC 校验值 crc 和载荷 payload。为什么 seq 和 ack 都占 8 bit因为模拟实验的帧序号空间不需要很大8 bit 覆盖 256 个序号而且能让帧结构紧凑更接近真实链路层设计。typedef struct frame_t { uint8_t type; /* 0-DATA, 1-ACK, 2-NAK */ uint8_t seq; /* 帧序号按窗口空间取模 */ uint8_t ack; /* 期望收到的下一帧序号 */ uint16_t len; /* payload 长度 */ uint32_t crc; /* 整帧 CRC32 校验值 */ char payload[256]; } frame_t;设计说明ACK 帧不需要载荷len 可以传 0接收端只解析 type 和 ackDATA 帧的 crc 覆盖的是前面所有字节包括 type、seq、ack、len 和 payload不能只对 payload 算 CRC否则帧头部被篡改时校验不出问题。发送端填充帧时seq 要用发送序号对序号空间取模ack 字段填的是“期望收到的下一帧序号”不是“刚刚收到的序号”这两个字段的语义最容易写反。3.2 CRC32查表法和逐位法的取舍crc32.c 实现的是标准 CRC32。这里给一个可以直接跑通的逐位版函数逻辑是标准多项式 0xEDB88320 的反射形式uint32_t crc32(const uint8_t *data, size_t len) { uint32_t crc 0xFFFFFFFF; for (size_t i 0; i len; i) { crc ^ data[i]; for (int j 0; j 8; j) { if (crc 1) crc (crc 1) ^ 0xEDB88320; else crc 1; } } return crc ^ 0xFFFFFFFF; }参数说明初值 0xFFFFFFFF 和末尾异或 0xFFFFFFFF 是标准 CRC32 的两个关键点少一个都会导致和常见校验工具结果不一致。逐位版看起来简洁但每字节要做 8 次循环如果要模拟大量帧性能会拖后腿。所以 crc32.c 里通常用查表法初始化时生成 256 个表项运行时每字节一次查表加三次异或。查表法代码看着长但效率高几倍这是工程里更常见的做法。需要注意查表法生成表时用的多项式方向和逐位法要一致否则两边算出来对不上。3.3 发送窗口状态机累计确认和超时重传滑动窗口协议的核心不在一帧怎么发而在窗口何时向前滑动。发送端有三个关键变量base窗口底部、next_seq下一个待发序号、window_size窗口上限。收到 ACK 之后如果 ack 大于 base说明 base 到 ack-1 的所有帧都被确认base 就可以推进。这个“累计确认”是回退 N 协议最重要的语义也是唯一能和选择重传区分开的行为。void sender_on_ack(sender_t *s, uint8_t ack) { if (ack s-base 1 ack s-next_seq) { s-base ack; log_window(slide, s-base, s-next_seq); } }参数说明在回退 N 中ack 字段表示“期望收到的下一帧序号”所以 ack base 1 表示序号为 base 的那一帧已经收到窗口底部前移一格。收到重复 ACK 时 ack 不会大于当前 base直接忽略。这比“每收到一个 ACK 就推进一格”的做法更贴近真实协议也避免乱序 ACK 导致窗口反复回退。超时处理要区分 GBN 和 SRGBN 超时后重传 base 到 next_seq-1 的所有帧SR 只重传超时的那一帧。这里的重传范围差异直接决定了两种协议的信道利用率差异。GBN 实现简单但窗口越大浪费越大SR 需要接收端缓存乱序帧还要处理 NAK代码复杂度上了一个台阶。gbn_improve.c 通常就是在 GBN 框架上做“只重传丢失帧”的优化相当于在实现复杂度和信道效率之间取折中。3.4 接收端逻辑GBN 不缓存SR 才缓存接收端的实现比发送端更容易写错。GBN 的接收窗口大小固定为 1只接受 seq expect_seq 的帧如果收到乱序帧正确的做法是丢弃并重新发送一个 ACK告诉发送端我还期待哪一帧。很多实现把 GBN 写成了 SR收到乱序帧先缓存等前面的帧补齐后再一起上送虽然没有明显 bug但已经不是回退 N 协议了。答辩时老师会问“你这个缓存区是干什么的”如果 GBN 也回答“缓存乱序帧”基本就暴露了概念不清。选择重传的接收端则必须有窗口缓存数组大小至少等于发送窗口的两倍索引用 seq 对缓存大小取模。收到帧后先判断 seq 是否落在当前接收窗口内不在就丢弃在窗口内就放入缓存并检查能否把连续的数据段上送。上送后推进 expect_seq同时发送 ACK。这段逻辑在每个协议实现里单独维护不放进公共的 datalink.c目的就是让三种策略互不干扰。int sr_receive(sr_ctx_t *ctx, frame_t *f) { int idx f-seq % ctx-buf_size; if (!in_receive_window(ctx, f-seq)) return 0; ctx-cache[idx] *f; /* 尝试把从 expect_seq 开始的连续帧上送 */ while (ctx-cache[ctx-expect_seq % ctx-buf_size].seq ctx-expect_seq) { deliver(ctx, ctx-cache[ctx-expect_seq % ctx-buf_size]); ctx-expect_seq; } send_ack(ctx, ctx-expect_seq); return 1; }这段代码里的 in_receive_window 检查必须放在取模之前否则越界访问可能发生在窗口判断之前。上送循环用 expect_seq 对缓存大小取模作为下标保证缓冲区是循环使用的。这里体现了 SR 和 GBN 最大的不同接收端有自己的窗口和缓存发送端不能假设“没收到 ACK 就一定丢帧”。3.5 模拟信道丢包率怎么控制才能演示不翻车datalink.c 里的模拟信道负责在发送端和接收端之间注入丢包。最简单的方式是 rand() % 100 loss_rate 就丢弃。但注意 rand() 默认随机种子是固定的想要结果可复现可以用固定种子想要每次不同可以用当前时间做种子。课设演示建议用固定种子第一次跑出重传事件后记下参数答辩时按相同参数重跑结果完全一样不会出现“刚刚还能跑现在怎么不重传了”的翻车现场。int channel_send(channel_t *ch, frame_t *f) { if (rand() % 100 ch-loss_rate) { lprintf(LOSS seq%d\n, f-seq); return 0; } lprintf(TX seq%d type%d\n, f-seq, f-type); deliver_frame(ch, f); return 1; }这里丢包率是百分制loss_rate 设为 10 时大约每 10 帧丢 1 帧。日志里 LOSS 行要保留它是说明“这帧没到达接收端”的直接证据如果只打印 TX 不打印 LOSS看起来就像发送端根本没发这帧。窗口滑动和超时重传都依赖日志来证明所以 lprintf 的输出格式值得在 README 里专门说明。3.6 三种协议如何共用同一个主流程很多自己写的滑动窗口代码最大的问题是三种协议各写各的主函数switch 里复制粘贴。这套资源把公共流程抽成 datalink.c 里的统一调度。主循环大致是先处理到达的 ACK 或数据帧再检查定时器最后填满发送窗口。协议策略层只实现三个回调init、handle_frame、timeout。这样 GBN 和 SR 的差异都被封装在各自文件里datalink.c 不用知道窗口细节。static void run_protocol(protocol_ctx_t *ctx, channel_t *ch) { while (!ctx-done) { handle_channel_events(ctx, ch); /* 接收 ACK 或数据帧 */ update_timers(ctx); /* 检查超时触发重传 */ fill_window(ctx, ch); /* 发送窗口内新帧 */ } }三种协议的差异集中在 handle_channel_events 和 fill_window 内部GBN 的 fill_window 在超时后重传整个窗口SR 的重传由定时器单独触发gbn_improve 则在损失一帧时只补那一帧。主循环不变后续加新协议只需要新增文件并实现回调。答辩被问“能不能加一个新协议”时能回答“新增一个文件实现三个函数”的同学和现场改代码的同学分数是完全不同的。3.7 对照实验同一丢包率下三种协议的重传次数差异演示环节最有说服力的不是单跑一个协议而是同一丢包率下三种协议的横向对比。设丢包率 10%、窗口 8、发送 100 帧停等协议几乎每个丢包都要停等一个 RTT回退 N 会把窗口内后续帧全部重传选择重传只重传丢失帧。把三组日志里的重传次数整理成表格比口说“SR 效率高”直观得多。协议丢包率 10% 的重传行为日志特征停等每次丢包后等待超时重发同一帧同一 seq 重复 TXGBN超时后重传窗口内所有帧连续几帧重复 TXSR超时只重传丢失的那一帧单个 seq 重复 TX且伴随缓存滑动日志这个对照表可以直接作为实验报告“结果分析”章节的三行结论。gbn_improve 的对比还可以加一列它比 GBN 少重传窗口内已知收到的帧但不需要 SR 那样的接收缓存介于两者之间。答辩时能背出这个差异比会默写定义更能让老师相信你调过代码。4. 常见问题与避坑滑动窗口实验最容易翻车的 5 个点这一章写给已经能编译运行、但一改参数就翻车的同学。以下五条是我在这个项目里见到最多的现场按概率排序。每条我都会给一个快速定位方式先看日志有没有某种特征再对照原因最后改代码。比直接告诉你“某个函数应该怎么写”更实用因为很多同学的代码经过自己改换后行号已经和原始版本对不上了只有从现象反推才能定位。4.1 窗口只往前挪一格吞吐量上不去现象日志里 ACK 一直在增加发送端也确实收到了但发送窗口的 base 始终慢半拍窗口利用率只有一半。用 4 窗口跑 100 帧重传次数看起来很少但总用时比预期长很多。原因ACK 被处理成“单个确认”而不是“累计确认”。典型的错误写法是收到一个 ACK 就把 base 加 1或者用位图记录“第 n 帧收到”结果丢失中间某一帧时后面的 ACK 都推不动窗口。滑动窗口协议的 ACK 字段是“期望收到的下一帧序号”它天然携带了累计信息ack 4 代表 0~3 都到了。解决统一走 sender_on_ack 那套逻辑先判断 ack 是否在 [base1, next_seq] 范围内再直接把 base 更新为 ack。范围外的 ACK 全部忽略。改完以后可以设丢包率 0 跑一遍观察日志里 base 是不是按 1、2、3 连续推进。定位方式也很简单打开日志数一数收到 ACK 后 base 是否连续增加如果 ACK 和 base 总是差 1多半是确认语义没统一。4.2 GBN 模式没有超时重传日志乱序帧却全收到了现象选择 GBN 时把丢包率调到 20%运行完发现日志里都是 RX超时重传一次都没发生最后统计结果里重传次数却是 0。评审老师一看就怀疑协议没写对。原因接收端把乱序帧缓存起来了。GBN 的接收窗口大小固定为 1只接受 seq expect_seq 的帧乱序帧应该立刻丢弃并重发 ACK让发送端感知到缺失从而触发超时重传。如果接收端像 SR 一样缓存乱序帧GBN 发送端就一直收不到超时信号逻辑上悄悄变成了别的协议。解决在 GBN 的 receive_frame 里先判断 seq 是否等于 expect_seq不等就直接丢弃并发送 ACK(expect_seq)不要触碰任何缓存数组。日志里应该能看到 DISCARD seq... 的记录这样才有论据说“接收端按 GBN 语义处理了乱序帧”。定位方式把丢包率调到 20%跑完看有没有重传日志如果有 RX 却没有重传接收端大概率缓存了乱序帧。4.3 CRC32 工具算的和程序算的对不上现象帧在信道里没有丢包但接收端每次都说校验失败。把同一个缓冲区拿去网上工具算也能算出另一个结果两边永远对不上。原因标准 CRC32 的初值是 0xFFFFFFFF算完后再异或 0xFFFFFFFF。常见抄代码只把算法主体抄了没带上 init 和 final xor或者查表法的表项生成方向不对用的多项式是 0x04C11DB7 而不是反射的 0xEDB88320。还有一个隐蔽点发送端给 crc 字段赋值后再对整帧做 CRC接收端如果对包含 crc 字段的整帧再做一次 CRC这种“余数法”的判定结果和“先算后比较”只能二选一不要混用。解决先用字符串 123456789 验证 crc32 函数正确结果应该是 0xCBF43926。对上了再检查发送端计算 CRC 时是否覆盖了 type、seq、ack、len、payload 全部字段接收端校验范围要完全一致。定位方式单独写个十行测试函数把缓冲区打印出来对比两边实际参与 CRC 的字节数一眼就能看出差异。4.4 窗口从 4 调到 16 后偶发越界现象窗口大小 4 一切正常改成 16 后开始偶发崩溃或者日志里出现一些不应该存在的帧序号。把缓存数组长度加大后又不崩了但不知道为什么要加大。原因SR 接收缓存数组通常按窗口大小分配而帧序号是循环使用的。如果直接用 seq 当数组下标seq 增长到超过数组长度就会越界。这是典型的小窗口能跑、大窗口现原形的 bug。解决访问缓存前先判断 seq 是否落在当前接收窗口落在外面的直接丢弃落在窗口内的用 seq % buf_size 作为数组下标。窗口大小、序号空间、缓冲区大小三者的关系在设计阶段就要定好别等越界了才想起来取模。定位方式窗口调到 16打开调试器在缓存赋值处打断点观察 seq 是不是已经越过数组上界。4.5 演示日志堆成黑匣子答不出丢没丢包现象答辩演示时终端疯狂滚动截一张能说明重传过程的图都难。助教问“你这帧到底丢没丢”只能指着屏幕说“刚刚好像有一行 LOSS 闪过去了”。原因所有输出都走裸 printf没有时间戳没有 TX/RX/LOSS 标记也没有日志级别控制。printf 的输出在管道重定向时还可能受缓冲影响和实际事件顺序不一致进一步增加了解释成本。解决统一用 lprintf 输出每条日志带毫秒时间戳、方向标记和帧类型。演示前用相同随机种子跑通一次日志重定向到文本截图保留“LOSS → TX 重传 → ACK”这样连贯的三行比十屏滚动日志有说服力得多。定位方式日志重定向到文本文件后搜索 LOSS 行是否和 TX 行顺序一致顺序乱了就是缓冲问题顺序没乱但肉眼找不到就是日志格式缺标记。5. 验收与答辩把 95 分的评审点拆成一张自查表交之前我会按这张表走一遍每一行都对应评审老师可能翻的代码位置。自查项怎么做常见扣分点源码编译VS 打开 datalink.sln 直接生成或 gcc 编全部 C 文件答辩现场说“在我电脑上能跑”参数切换用 -p、-w、-l 三个参数分别跑三种协议协议模式写在宏里改一个值要重编译帧格式完整protocol.h 里 type、seq、ack、crc 都有只传结构体不做 CRC窗口滑动可见日志能看到 base 从 0 推进到末尾只有发送统计没有窗口变化协议语义正确GBN 丢弃乱序帧SR 缓存乱序帧把 GBN 写成 SR 的缓存逻辑结果可复现固定随机种子同一参数结果一致每次运行重传次数都不同演示顺序我习惯固定成三遍停等协议丢包率 0证明基本收发没问题GBN 丢包率 10%展示窗口回退和批量重传SR 同丢包率展示只重传单帧。三组日志放在三张截图里老师问差异时直接指着重传数量回答。答辩被问到代码时按“状态变量变化 函数调用链 异常情况分支”三点讲不要只背函数名。这套资源里自带文档说明和 results.png直接按这个流程补截图就能形成一份完整验收材料。最后说一个我的习惯从那以后我每次碰协议模拟实验都强制自己在写代码前先用表格把 sequence 推演一遍再动手。这个习惯帮我少踩了很多坑希望也能帮到你。本文还有配套的精品资源点击获取