
简介这是一份关于滑动窗口协议仿真的课程设计报告适用于计算机网络课程的作业参考也适合初学者通过具体实例理解流量控制与拥塞避免机制。压缩包内仅含1个doc文档大小约400KB结构完整、排版规范可直接作为同类课程设计的撰写模板。目前已有635人学习浏览。报告首先阐述了滑动窗口协议的基本原理包括发送窗口、接收窗口的维护及大小差异其次深入比较了1bit滑动窗口、后退N协议、选择重传协议三种典型类型的运行机制、优缺点与适用场景随后详述了使用VC进行编程模拟的整体设计涵盖发送方与接收方的队列模块、主函数实现、帧序号管理、确认与超时处理以及界面要求、调试操作等。读者既能掌握滑动窗口协议的数据传输与错误恢复过程也能获得一个可运行思路清晰的仿真程序架构尤其适合毕业设计或课程汇报前系统梳理相关知识。1. 滑动窗口协议仿真这份课程设计 doc 里哪些内容值得真正跑起来做计算机网络课程设计选滑动窗口协议仿真十个人里有八个最后交的是停等协议改出来的假窗口——界面上画了几个框实际发送还是发一帧等一帧。这份《课程设计报告-滑动窗口协议仿真.doc》不一样的地方在于它用 VC 把发送方和接收方拆成两个独立线程通过 socket 在本地通信数据帧里真实携带序号和确认号丢包、超时、nak 重传都做了仿真。它解决两个问题一是把窗口机制从课本概念变成可运行的逻辑二是让发送窗口、接收窗口、帧序号这些状态变化在 DOS 界面里实时可见。适合正在写课设的你尤其是卡在「不知道怎么写丢包和超时模拟」这类问题上的人。整份文档 29 页原理、需求分析、结构体定义、完整源码、调试说明都在里面最值钱的部分是第五章的源代码——那是可以直接编译跑的完整程序。2. 三种协议从停等走到选择重传窗口尺寸决定效率上限2.1 同一套窗口机制三种协议只差窗口大小滑动窗口协议的核心思想并不复杂任意时刻发送方维持一个连续允许发送的帧序号区间叫发送窗口接收方维持一个连续允许接收的帧序号区间叫接收窗口。发送窗口和接收窗口的上下界不必一致大小也可以不同。不同协议之间的差别归根结底就是窗口尺寸不同1bit 滑动窗口协议发送窗口 1接收窗口 1后退 N 协议发送窗口 1接收窗口 1选择重传协议发送窗口 1接收窗口 1我第一次看这段原理时觉得太抽象直到自己动手写代码才明白窗口其实就是一组允许发送或接收的序号区间代码里用一个队列就能表达。发送窗口内的帧是那些已发送但还没确认的帧接收窗口内的序号对应接收方缓冲区里预留的位置。文档里对这套机制的阐述很直白没有绕弯子。2.2 从 1bit 停等到后退 N以重传代价换信道利用率当发送窗口和接收窗口都为 1滑动窗口协议就退化为停等协议。发送方每发一帧就停下等 ack 回来才发下一帧。因为同一时刻只有一帧在途只用 1 bit 序号就够区分新帧和重发帧。这个协议的优点是实现简单缺点是信道利用率太低——发一帧等一个往返时延链路大部分时间是空的。连续 ARQ 协议的思路是让发送方不等确认连续发多个帧。接收方不必对每个帧单独回 ack而是采用累积确认收到几个帧后对按序到达的最后一个帧发 ack。这就带来了一个新问题——如果发送方发了 5 帧第 3 帧丢了接收方只能对前 2 帧确认。发送方不知道后面 3 帧的下落只能把第 3、4、5 帧全部重传一次这就是后退 NGo-Back-N。文档里点得很清楚后退 N 一方面因连续发送而提高了效率另一方面在重传时必须把已正确传送过的帧也重传一遍信道质量差时连续重传不一定优于停等协议。这里有个容易被忽略的关键点后退 N 的接收窗口是 1意味着接收方只要发现某个帧出错后续即使正确到达的帧也必须丢弃不做缓存。它的重传以「出错帧及其之后所有帧」为粒度窗口越大一次重传的代价越高。2.3 选择重传协议用接收方缓冲区换更少的浪费选择重传协议是三种协议里最「抠」的一种。接收方发现某帧出错后不丢弃后续正确到达的帧而是先把它们收进缓冲区同时要求发送方只重传出错的那一帧。收到重传帧后再把缓冲区里按序排好的帧一并交给上层。代价是接收方必须有足够大的缓冲区。三种协议的取舍可以列成一张表协议发送窗口接收窗口重传粒度接收方缓冲区需求1bit 滑动窗口停等11单帧重传1 个帧后退 N 11出错帧及其后全部帧1 个帧选择重传 1 1仅出错的帧与窗口等量缓存选择重传还有个暗坑序号空间有限时窗口尺寸不能无限大。对 n bit 序号空间通常要求发送窗口 接收窗口 ≤ 2^n且接收窗口 ≤ 发送窗口否则接收方无法区分新帧和重传帧。这份仿真代码里帧序号用 unsigned int 表示窗口小的时候没问题但如果后续你想把发送窗口改成 8 或 16序号位宽和窗口上限必须一起改这一点在后面调试部分我会详细说。3. 先拆数据结构再谈协议帧结构、队列与 socket 通信的落地方式3.1 frame 结构体四种帧类型和一段随机的数据负载文档第四章定义的结构体是整个程序的地基。核心是 frame_head 和 frame 两层结构typedef enum { data 1, ack, nak, tout } frame_kind; // 帧类型 typedef struct frame_head { frame_kind kind; // 帧类型data / ack / nak / tout unsigned int seq; // 序列号 unsigned int ack; // 确认号 unsigned char data[MAX_LENGTH]; // 数据负载 } Head; typedef struct frame { frame_head head; // 帧头 unsigned int size; // 数据大小 } Frame;逻辑说明帧类型枚举把通信内容区分成四类——data 是数据帧ack 是确认帧nak 是否认帧tout 是超时标志。发送方收到 ack 会删掉队头继续发下一帧收到 nak 会重发当前帧。seq 和 ack 都是 unsigned intseq 是发送方给帧编的序号ack 是接收方回执时携带的确认序号。data 数组是实际载荷MAX_LENGTH 是载荷上限size 记录本次实际数据长度。参数说明这里有个设计细节值得注意MAX_LENGTH 是编译期宏直接决定单帧最大负载。文档里 GetFrameFromHost 用rand() % MAX_LENGTH生成长度意味着每次发送的帧大小随机这比固定长度更能模拟真实网络里报文长短不一的情况。如果你在自己的实现里想演示大帧分片或小帧合并改这个宏就可以不需要动结构体。3.2 用链表队列模拟发送缓冲区front 指向待发送帧rear 追加新帧发送方需要一个缓冲区来容纳「已从主机取走但尚未确认」的帧文档用链表队列实现。关键成员是 front 和 rear 两个指针以及一套队列操作函数typedef struct framenode { // 队列节点 frame head_data; struct framenode *next; } Framenode; typedef struct { Framenode *front; // 队头指向待发送的首帧 Framenode *rear; // 队尾 } LinkQueue; void GetFrameFromHost(LinkQueue *q) { if(QueueLen(q) MAXPOOL) { return; // 缓冲池已满不再取帧 } Framenode *p (Framenode *)malloc(sizeof(Framenode)); srand((unsigned)time(NULL)); p-head_data.size rand() % MAX_LENGTH; // 帧大小随机生成 p-next NULL; if(QueueEmpty(q)) q-front q-rear p; // 首帧是待发送的帧 else { q-rear-next p; q-rear p; } GetFrameFromHost(q); // 递归取帧直到缓冲池满 }逻辑说明GetFrameFromHost 负责从「主机」取帧入队——所谓主机这里就是假设有无限多的帧等待发送函数只受 MAXPOOL 上限约束。队列非空时新帧挂在 rear 后边队头始终是下一个要发送的帧。当发送方收到 ack 确认会把队头 DeLine 删掉这样窗口就向前滑动一格。参数说明MAXPOOL 是发送缓冲池上限决定队列最多能缓存多少帧。按文档的设计发送方在循环里反复调用 GetFrameFromHost 和 DeLine队列长度在 MAXPOOL 以内波动。队列长度等于当前在途帧数加待发送帧数这个值在界面里能直接看到是判断窗口是否「滑动」最直观的指标。有一点要提醒GetFrameFromHost 用递归填充队列数据量小没问题但递归深度会随 MAXPOOL 增大真要在生产环境做传输别照抄这个写法。3.3 socket 通信层回环地址上跑两个线程端口 7001发送方和接收方之间的传输通道用 Windows Sockets 实现TCP 协议接收方端口 7001。发送方 connect 失败后用 goto 跳回 Begin 标签重新初始化——这个写法在课设代码里很常见虽然 goto 不优雅但演示场景下「连不上就重试」的逻辑足够直观。Begin: WORD wVersionRequested MAKEWORD(1, 1); // 请求 1.1 版 Winsock WSADATA wsaData; int err WSAStartup(wVersionRequested, wsaData); if (err ! 0) { return; } if (LOBYTE(wsaData.wVersion) ! 1 || HIBYTE(wsaData.wVersion) ! 1) { WSACleanup(); return; } socketClient socket(AF_INET, SOCK_STREAM, 0); // 流式套接字 SOCKADDR_IN clientadd; clientadd.sin_family AF_INET; clientadd.sin_port htons(7001); // 目标端口 7001 // 两台机器跑把地址改成对端实际 IP if (SOCKET_ERROR connect(socketClient, (SOCKADDR*)clientadd, sizeof(SOCKADDR))) { WSACleanup(); goto Begin; // 连接失败重来 }逻辑说明WSAStartup 是使用 Winsock 前的必调函数MAKEWORD(1,1) 请求 1.1 版本LOBYTE 和 HIBYTE 分别取版本号的高低字节做校验。socket 用 AF_INETIPv4 SOCK_STREAMTCPconnect 指向本机 7001 端口。连接建立后双方先互发一段提示信息确认链路通畅再按任意键开始传输仿真。参数说明端口 7001 是硬编码。同一台机器开两个终端窗口跑发送方和接收方走的是回环地址不用改任何配置。如果你要拿到两台机器上跑需要把 clientadd.sin_addr.s_addr 显式设置成接收方机器的局域网 IP同时放行防火墙对 7001 端口入站连接的限制。文档 3.6 节写的「两台机器或一台机器中两个独立的线程」指的就是这两种运行方式。4. 发送方实现队列取帧、5 秒超时与线程收 ack 的主循环4.1 发送主循环从取帧到超时判断的完整流程发送方的主函数逻辑可以浓缩成一段骨架代码文档第五章的源码本质就是这段循环加上错误处理while(1) { GetFrameFromHost(QueueQ); // 1. 从主机取数据帧 memset(packetsend, 0, sizeof(packetsend)); packetsend QueueFront(QueueQ); // 2. 取队头即待发送帧 ret send(socketClient, (char *)packetsend, sizeof(packetsend), 0); if(ret SOCKET_ERROR) { // 3. 发送失败则跳过 printf(发送数据出错\n); continue; } const unsigned long timeOut 5 * 1000; // 4. 超时计时器 5 秒 InitializeCriticalSection(gCS); // 5. 初始化临界区 hThread CreateThread(NULL, 0, ReceiveFun, (LPVOID)packetreceive, 0, NULL); // 6. 等待接收线程返回最多等 5 秒 int r WaitForMultipleObjects(1, hThread, TRUE, timeOut); DeleteCriticalSection(gCS); if(r WSA_WAIT_TIMEOUT) { TerminateThread(hThread, 0); // 7. 超时则杀掉收 ack 线程 } // 8. 随机模拟20% 概率判定为超时不删队头 srand((unsigned)time(NULL)); switch(rand() % 5) { case 0: break; // 超时重传队列头帧 default: DeLine(QueueQ); // 确认成功队头出队 break; } // 9. 运行满 20 秒后询问是否继续 if(GetTickCount() - tick 20 * TIMEOUT) { printf(持续时间 20s. 按 q 退出其他键继续\n); int kbc getch(); if(kbc q || kbc Q) break; } }逻辑说明这个循环展示了发送方五个关键动作。第一步调用 GetFrameFromHost 补充缓冲池第二步取队头帧作为本次要发的包第四到第六步是核心——创建 ReceiveFun 线程去阻塞收 ack主线程用 WaitForMultipleObjects 等它最多 5 秒第七步如果主线程等到 timeout 就强制结束接收线程认定该帧超时第八步是这份仿真的精髓它不是真的靠网络状况决定丢包而是用随机数模拟「这一轮帧是否被确认」20% 概率不删队头相当于超时重传80% 概率删队头相当于收到 ack窗口滑动。第九步控制整个演示的持续时间。参数说明这里三个常量决定仿真行为的走向。timeOut 5 * 1000 是超时阈值单位毫秒收到 ack 前超过 5 秒未返回就重传20 * TIMEOUT 是总演示时长乘出来是 20 秒到点后询问继续还是退出rand() % 5 的 case 0 对应 20% 超时概率想调成 10% 就把取模换成 10想调成 50% 就改成 rand() % 2。这三处是演示效果最敏感的调节点也是答辩时老师最喜欢问的问题——你要能说清楚它们各自对行为的影响。4.2 ReceiveFun 线程与临界区为什么收 ack 要单独开线程发送方不能一边阻塞收 ack 一边继续主流程所以文档用了 CreateThread 开独立线程接收数据配合临界区保证 socket 的 recv 不被多个线程同时调用DWORD WINAPI ReceiveFun(LPVOID pArg) { EnterCriticalSection(gCS); // 进入临界区独占 socket 收数据 frame *packetreceive (frame *)pArg; ret recv(socketClient, (char *)packetreceive, sizeof(*packetreceive), 0); LeaveCriticalSection(gCS); // 离开临界区 return ret; }逻辑说明ReceiveFun 做的事情只有一个——阻塞在 recv 上等待接收方的回应帧。主线程 WaitForMultipleObjects 等它返回5 秒内收到回应说明链路正常主线程继续处理等不到就用 TerminateThread 强制结束。EnterCriticalSection 和 LeaveCriticalSection 保证 recv 与主线程的 socket 读写互斥避免两个线程同时操作同一个 socket 导致数据错乱。参数说明pArg 是接收缓冲区的指针这里指向主函数里定义的 packetreceive 结构体。TerminateThread 这个函数在真实程序里是危险操作因为被强杀线程持有的锁和资源不会自动释放文档用它是课设场景的妥协——主线程等超时后必须立刻重传没有精力做线程的优雅退出。这里你心里要有数演示没问题但不要把这个模式搬进正式的网络程序。再补充一个容易忽略的细节文档里发送方在每次 send 之后都 Sleep(SLEEPMS) 一段时间再创建接收线程。SLEEPMS 是全局的演示节流常量控制终端输出的刷新速度。这个值太小会导致界面刷屏太大则整个仿真看起来像卡死实际调试的时候建议一开始设大一点比如 1000ms观察流程确认逻辑正确后再调小。5. 接收方实现与调试避坑随机丢包、nak 重传和五条实测记录5.1 接收方主流程监听到 ack20% 概率制造错误接收方的主函数先初始化 socket进入监听状态accept 发送方的连接请求然后进入循环处理帧。它的处理逻辑是典型的三种分支判定// 等待接收数据帧 // 校验数据帧假定产生随机结果20% 的概率校验错误或发送方超时 if (rand() % 5 0) { // 20% 概率出错 // 丢弃数据帧并发送否认帧 nak } else { // 判断是否是上一帧的重发 if (帧序号 上一帧序号) { // 丢弃重复帧仍发送确认帧 ack } else { // 保存数据帧到当前接收窗口 // 发送确认帧 ack } } // 送数据帧至主机逻辑说明接收方用随机数模拟校验结果20% 概率判定当前帧出错发送 nak 让发送方重传剩余 80% 概率校验正确此时要区分两种情况——收到的是新帧保存到接收窗口并回 ack收到的是上一帧的重传丢弃但同样回 ack因为发送方可能没收到之前的确认。这种「重复帧也要回 ack」的处理是滑动窗口协议最容易写错的地方漏了它发送方会一直超时重传窗口永远滑不动。参数说明这个 20% 的判断和发送方的rand() % 5相互独立意味着同一轮传输中发送方在「等 ack 超时」和接收方在「校验出错」两个层面各有一层随机性。两处概率叠加后实际丢包率比 20% 略高你演示时看到的偶尔连续重传就是这两层随机共同作用的结果。如果希望单侧控制丢包就把某一侧的随机判断去掉只保留一处的 rand()。接收方回 nak 后发送方收到 nak 会重新发送当前帧——注意发送方主循环里随机删队头的逻辑它并不会显式区分「收到 ack」和「收到 nak」而是统一按概率处理。这是课设仿真「以简代真」的地方真实协议靠帧类型决定分支这里靠随机数决定结果。文档里接收方窗口队列的 DeLine 函数带 curw 参数curw 是当前打开的接收窗口起点配合队列操作把按序到达的帧提交主机。5.2 避坑记录这份代码在实测里最容易踩的五个点以下五条来自我按这份文档编译、运行、改参数过程中的实测记录现象和原因都验证过。坑一发送方卡死在「持续等待确认」按什么都没反应现象程序运行十几秒后终端不再刷新看起来像死循环按 q 也无法退出。原因WaitForMultipleObjects 超时后走的 TerminateThread 强杀接收线程但 recv 所在的 socket 内部状态已被破坏后续循环里再次调用 recv 会一直阻塞。主线程卡在接收上轮不到超时判断。解决超时分支里不要直接 TerminateThread。常见做法是先调用 closesocket 关闭 socket让 recv 立刻返回 SOCKET_ERROR再重新创建 socket 重连或者把接收线程设计成非阻塞循环用标志位通知退出。演示场景下至少要在 TerminateThread 之后加一次 socket 重建而不是接着用旧 socket 继续循环。坑二两台机器联调时接收方一直收不到连接现象发送方提示「连接失败」反复重试也无法建立连接。原因代码里 clientadd 没有显式设置 sin_addr.s_addr默认指向本机回环地址另外发送方机器上的防火墙可能拦截了 7001 端口的出站请求。解决在两台机器跑之前把clientadd.sin_addr.s_addr inet_addr(接收方IP)这句话加上再确认接收方防火墙对 7001 端口设置入站放行。实验室环境经常是 Windows 自带防火墙拦截不是代码问题。同一台机器跑回环地址则什么都不用改。坑三发送方终端报错后直接闪退连错误信息都看不到现象接收方先被关闭发送方在 send 或 recv 时报 SOCKET_ERROR紧接着程序退出没有打印任何提示。原因main 函数末尾的 WSACleanup 和 closesocket 顺序与出错分支不在同一路径。出错分支里 send 失败只 printf 一行 continue但 socket 资源没有清理后续循环再发数据必然继续失败而 recv 出错时直接走到主循环外Windows 在程序退出时自动清理 socket反而把错误信息吞了。解决统一在 send、recv 每个出错点输出带错误码的提示然后调用 closesocket 关闭当前 socket 再选择重连或退出。别让出错分支把 socket 留到下轮循环否则会出现「错误后连续刷屏」的假死现象。坑四把窗口从 1 改成 8 后接收方帧序对不上数据错乱现象按照文档思路把发送窗口改大后界面显示的帧序号跳变接收方认为全部帧都是重复帧拒绝保存。原因序号空间、发送窗口、接收窗口三者没有同步调整。这份仿真代码的 seq 是 unsigned int能表示的空间很大但接收方「是否重传」的判断只拿当前帧序号跟上一帧比较——窗口改成 8 后接收方仍用单帧记忆判断新旧逻辑必然出错。解决把接收窗口也改成数组或环状缓冲按窗口范围判断帧是否落在接收窗口内而不是单帧对比。判断规则建议用「帧序号是否落在当前接收窗口区间」落在窗口下半部分视为重传落在上半部分视为新帧。做这个改造时接收方的队列缓存容量也要同步加大代码里 MAXPOOL 和接收窗口是两个独立参数很容易只改一个。坑五仿真速度忽快忽慢演示时看起来像失控现象数据帧发送速度在终端里肉眼可见地不均匀有时连续快速翻帧有时停顿好几秒。原因三个因素叠加——Sleep(SLEEPMS) 虽然存在但只在部分节点生效printf 往控制台输出本身是阻塞操作输出量大时拖慢循环随机帧大小导致 send 的数据量波动。解决把 Sleep(SLEEPMS) 挪到每轮循环的固定位置只留一处别在发送前后各放一个演示前把 MAX_LENGTH 调小到固定长度减少帧大小波动如果还觉得速度不合预期直接把 SLEEPMS 从 100 调到 200 或 500先慢后快调参而不是改代码逻辑。6. 把仿真改成可配置的验收工具丢包率、窗口与日志的三个小改动课设验收时老师最常问的三句话是「丢包率在哪调窗口能改吗你怎么证明数据传对了」原版代码三个问题的答案分别是——翻代码找 rand() % 5、改宏再重新编译、盯着终端刷屏。运行没问题但演示和答辩的掌控感差一些。我一般拿到这类仿真源码会先做三处小改造把「演示程序」变成「验收工具」。第一处把随机丢包率从散落各处的 rand() 抽成统一宏。发送方侧的超时概率和接收方侧的校验错误概率各自定义成 DROP_RATE_SEND 和 DROP_RATE_RECV再写一个统一的随机判断函数// 头文件里定义统一丢包率两侧可分别设置 #define DROP_RATE_RECV 20 // 接收方校验错误概率单位 % #define DROP_RATE_SEND 20 // 发送方超时概率单位 % // 判断本次传输是否命中丢包 int is_dropped(int rate) { return (rand() % 100) rate; // rate 范围 0~100 }逻辑说明原来rand() % 5 0的写法概率固定且不直观改成 0~99 随机数后宏的值就是百分之多少的丢包率。两侧各自独立设置可以构造出「发送方不丢、接收方丢 20%」这类对照组演示三种协议在不同丢包率下的表现差异。参数说明两个宏一个在发送方主循环的等待分支里用一个在接收方校验分支里用。丢包率调到 0 时整个传输过程不再有重传界面会连续翻帧这是验证基本流程是否正常的手段调到 50 以上重传频繁适合演示超时和 nak 触发逻辑。第二处把终端刷屏输出改成可选的文件日志。打印到控制台和写入文件并不冲突关键是把每个帧的事件写成结构化文本FILE *logfp fopen(sim.log, a); // 追加写日志 fprintf(logfp, [%lu ms] seq%u ack%u kind%d size%u result%s\n, GetTickCount() - tick_start, frame_recv.head.seq, frame_recv.head.ack, frame_recv.head.kind, frame_recv.size, is_dropped(DROP_RATE_RECV) ? DROP : OK);逻辑说明日志里记录发送时刻、帧序号、确认号、帧类型和本次处理结果。验收时不用盯着滚动的终端直接打开 sim.log 按时间段检索就能看到某个帧是第一次到达还是重传到达ack 是确认了哪个序号哪些帧最终没被确认。参数说明tick_start 在程序开始时记录时间差用 GetTickCount() 计算单位毫秒便于和代码里的 5 秒超时、20 秒总时长对应。result 字段标记每次处理的随机判定结果DROP 和 OK 的数量比就是实际仿真丢包率答辩时可以直接拿这个数字说话。第三处小改动是把窗口尺寸做成编译期变量并配套修改接收窗口判断。原版发送窗口本质是 1演示后退 N 协议时说服力不够。把发送窗口改成 WINDOW_SIZE 宏接收方的「重复帧判断」同步改成「窗口区间判断」再在界面里实时显示当前窗口的滑动的起始序号和末尾序号。窗口越大、丢包率越高后退 N 的重传浪费就越明显——这是答辩时最有演示效果的一张画面。从那以后我拿到任何一份带源码的课设 doc都会先做两件事把随机逻辑的入口找出来确认丢包模拟落在哪一侧把所有影响行为的时间常量、窗口大小、概率常量整理成宏定义清单。这两件事做完程序的行为边界基本就清楚了改起来也不会翻车。希望这份滑动窗口协议仿真的拆解和踩坑记录帮到你。本文还有配套的精品资源点击获取