资讯动态

基于UDP的滑动窗口协议模拟:从回退机制理解TCP可靠传输

发布时间:2026/10/18 22:11:16 来源:尧图企业网站定制
简介利用socket编程对数据链路层滑动窗口协议进行模拟的C语言项目面向计算机网络课程学习与实验环节。项目完整实现了1bit滑动窗口停等协议通过TCP连接在两台主机间模拟可靠数据传输发送方发出帧后需等待确认才继续接收方利用0/1序号识别新旧帧并完成确认回复把抽象的协议机制落到可运行代码中。压缩包共2个文件均为c源码体积仅4KB分别是发送端SlideWindowSender.c与接收端SlideWindowReceiver.c结构清晰便于逐行阅读、修改与调试。已有1308人学习下载。通过此资源可直观理解帧序号、确认号、超时重传等核心概念也能学习socket API的典型用法两份源码可直接编译运行适合作为计算机网络实验、课程报告的参考并可在其基础上扩展多bit窗口或加入差错检测进一步深入协议设计。1. 滑动窗口协议模拟不看源码永远不知道TCP为什么会回退很多人调通一个socket收发就以为自己懂TCP了直到某次生产环境出现大量超时重传排查了半天才发现是窗口管理不当。这个项目用UDP socket做传输载体在应用层模拟滑动窗口协议自己管ACK、超时重传、窗口滑动把传输层从黑盒拆成了看得见的逻辑。它适合三类人计算机网络课交实验作业的学生、准备面试时想把TCP细节讲清楚的人、以及想深入TCP源码但想先在应用层演练一遍的开发。核心就一句话用UDP当运输工具自己写一套带确认和重传的可靠性机制。2. 架构与协议设计数据面加序号、ACK、标志位用UDP承载可靠传输2.1 技术选型为什么用UDP承载而不是直接用TCP最开始很容易踩进一个误区以为标题里的socket是TCP socket直接用socket(AF_INET, SOCK_STREAM)去写滑动窗口。但想清楚就发现这是悖论TCP底层已经帮你把确认、重传、窗口全做完了你在应用层拿到的是有序无丢失的字节流再看不出任何滑动窗口的行为。所以正确的做法是用UDP也就是socket(AF_INET, SOCK_DGRAM)把数据包封装成带序号和标志位的自定义报文由自己来负责发送、确认、重传。这样窗口滑动的每一步都是自己代码里显式执行日志上能看到发送方推进窗口、接收方回ACK、定时器触发重传这些完整过程。项目依赖很少纯Python标准库就能跑代码量控制在几百行。选择Python的理由是阅读门槛低实验和二次修改都方便同时socket API和C语言版本几乎一一对应理解了以后迁移到C思路也不需要重学。2.2 项目目录与模块划分我习惯把代码分成五个文件每个文件职责单一改造的时候不至于牵一发动全身。整体结构大概是这样的sliding-window-sim/ ├── packet.py 报文封装与解析序列化、反序列化 ├── sender.py 发送端主逻辑窗口管理、超时重传 ├── receiver.py 接收端主逻辑ACK回传、乱序缓存 ├── config.py 端口、窗口大小、超时时间、丢包率等参数 └── README.md 运行说明与实验记录config.py放所有可变参数这个安排对实验项目非常重要。做网络协议模拟时要反复调参数观察窗口行为如果参数写死在代码里每次改都要重新理解上下文还容易改漏。集中放在一个文件里改起来不需要翻找代码出问题也好定位。2.3 报文格式设计报文设计是第一个要认真考虑的点它决定了收发双方怎么解码。我用的是定长头部加可变负载的结构头部包含序号、确认号、标志位负载就是真正要传的数据内容。代码示例packet.py的报文构造import struct import pickle # 报文结构seq(4字节) ack(4字节) flags(1字节) length(4字节) payload(可变) FLAG_DATA 1 # 数据包标志位 FLAG_ACK 2 # ACK确认包标志位 FLAG_FIN 4 # 结束标志位 def make_packet(seq, ack, flags, payloadb): header struct.pack(!IIB I.replace( , ), seq, ack, flags, len(payload)) return header payload def parse_packet(raw): seq, ack, flags, length struct.unpack(!IIB I.replace( , ), raw[:13]) payload raw[13:13 length] return seq, ack, flags, payload报文头设计为13字节固定长度其中seq和ack各占4字节的无符号整数flags一个字节表示包类型length记录负载长度。这里网络字节序用!前缀保证跨平台解析一致。长度字段是必要的一个设计因为UDP接收端不知道对方发来的数据有多大必须通过长度字段准确切分出payload。实际项目里通常还会加校验和字段但模拟场景里没有损坏报文的需求就省略了。如果后续想模拟比特错误可以在头部追加一个checksum字段这是比较容易扩展的点。2.4 窗口模型与核心状态变量滑动窗口机制模拟的是发送方和接收方各自维护窗口状态。发送方需要维护send_base和next_seq两个核心变量send_base指向尚未被ACK的最老包next_seq指向下一个要发送的新包它们之间的差值就是当前窗口中被占用但未确认的空间。接收方则维护expected_seq表示期待接收的下一个序号。数据包序号从0递增每发送一个数据包序号加1ACK报文里携带接收方期待的下一个序号。窗口大小由config.py统一控制。这些变量就是整个模拟器的主干状态重传、滑动、乱序缓存都是围绕它们展开的。接下来的核心模块拆解会逐个说明这些变量在具体场景里怎么操作。3. 核心模块代码拆解一个数据包从发送到ACK的完整生命周期3.1 发送端主逻辑窗口内发送、缓存未确认包发送端负责把数据拆成带序号的包然后遵守窗口约束发送。窗口限制下next_seq不能超过send_base加上窗口大小只有窗口内有空位才能继续发新包。# sender.py 发送端滑动窗口核心循环 import socket import time import random from config import * from packet import make_packet, parse_packet, FLAG_DATA, FLAG_ACK, FLAG_FIN def run_sender(): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(ACK_TIMEOUT) addr (RECEIVER_HOST, RECEIVER_PORT) data_chunks load_data(DATA_FILE) send_base 0 # 最老的未确认包序号 next_seq 0 # 下一个要发送的包序号 buffer {} # 未确认包缓存, key为seq, value为payload window_size WINDOW_SIZE while send_base len(data_chunks): while next_seq send_base window_size and next_seq len(data_chunks): pkt make_packet(next_seq, 0, FLAG_DATA, data_chunks[next_seq]) sock.sendto(pkt, addr) buffer[next_seq] data_chunks[next_seq] # 发出后进缓存等待ACK log_send(next_seq, send) next_seq 1 try: raw, _ sock.recvfrom(MAX_PKT_SIZE) seq, ack, flags, _ parse_packet(raw) if flags FLAG_ACK: # 累积确认, 所有小于ack的包都视为确认 while send_base ack and send_base in buffer: buffer.pop(send_base) log_send(send_base, ack) send_base 1 except socket.timeout: # 超时, 从窗口起点重传所有未确认包 for seq in list(buffer.keys()): retransmit_pkt make_packet(seq, 0, FLAG_DATA, buffer[seq]) sock.sendto(retransmit_pkt, addr) log_send(seq, retransmit) # 发送结束标志 fin_pkt make_packet(next_seq, 0, FLAG_FIN, b) sock.sendto(fin_pkt, addr)逻辑执行顺序是先在内层while循环里把窗口内所有可发送的包发出去并缓存然后阻塞在recvfrom等待ACK。收到ACK之后做累积确认把send_base往前推进滑出窗口收不到就进入socket.timeout异常分支把缓存里所有未确认包重发一遍。这种单计时器策略是GBN回退N步协议的典型做法代码实现简单日志上也很容易观察批量重传。参数意义在于send_base和next_seq分别确定了窗口下界和上界WINDOW_SIZE越大同一时刻在网络上未确认的包就越多吞吐理论上更高但丢包时回退代价也更大。3.2 接收端逻辑按序到达、乱序缓存、ACK回传接收端的职责是告诉发送方数据成功收到并且保证向上层提交的数据是按序的。每当收到一个数据包就检查它的序号是否等于expected_seq等于就提交并滑动不等就说明有前置包丢失先放进乱序缓存。# receiver.py 接收端核心逻辑 import socket from packet import make_packet, parse_packet, FLAG_DATA, FLAG_ACK, FLAG_FIN from config import * def run_receiver(): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((RECEIVER_HOST, RECEIVER_PORT)) expected_seq 0 out_of_order {} # 乱序缓存, keyseq, valuepayload received [] # 按序交付的数据块 while True: raw, addr sock.recvfrom(MAX_PKT_SIZE) seq, ack, flags, payload parse_packet(raw) if flags FLAG_FIN: break if flags FLAG_DATA: if seq expected_seq: received.append(payload) expected_seq 1 # 尝试把缓存中紧接着的包一并交付 while expected_seq in out_of_order: received.append(out_of_order.pop(expected_seq)) expected_seq 1 elif seq expected_seq: out_of_order[seq] payload # 无论是否乱序, 都回ACK, 告知期望序号 ack_pkt make_packet(seq, expected_seq, FLAG_ACK, b) sock.sendto(ack_pkt, addr)接收方关键点在两处看到seq等于expected_seq就正常接收并推进期望序号同时立刻检查乱序缓存里有没有下一个序号有就连着交付一直追到缓存空缺为止。收到大于expected_seq的包说明前面有包还没到放进out_of_order字典暂存并照常回ACK。ACK里带的expected_seq就是接收窗口的前沿发送方只要收到这个值就能确认所有小于它的包都已正确交付。这种设计模拟了接收窗口为1的Stop-and-Wait演进机制不是真正的窗口接收但足以演示滑动窗口协议的核心行为。如果想扩展成接收窗口大于1需要通过缓存多个乱序包并允许跳过多个缺口来实现框架上并不难。3.3 模拟丢包怎么让重传逻辑真正跑起来没丢包的网络环境里滑动窗口跑得非常顺利send_base一路推进到结束完全看不到GBN的回退机制。要让重传逻辑被触发必须在发送端或接收端人为模拟丢包。我习惯在发送端丢包做法是给每个包生成随机数随机数小于预设丢包率就跳过sendto。这样实现最简单效果又直观。丢包率数值放在config文件中。# sender.py 中加入丢包模拟 LOSS_RATE 0.2 # 测试时使用20%丢包率, 网络环境模拟 # 在sendto前增加判断, 模拟丢包 if random.random() LOSS_RATE: log_send(seq, simulate_loss) continue # 假装发送成功, 但实际上没有发出去这段代码的关键是continue放在合适的位置。continue会跳过sendto但不会跳过后续的buffer缓存和next_seq推进逻辑也就是说发送端以为这个包已经发出去了但接收端永远收不到直到超时重传被触发。这个模拟逻辑和真实网络丢包语义严格一致丢包发生在网络中间发送方不知道包丢了。实际调试时丢包率不能设太高20%的丢包率已经足够让GBN的超时重传频繁出现再高就会导致大量超时日志基本被重传刷屏反而看不清滑动窗口逻辑了。3.4 日志输出与运行观察点调试网络协议没有直观的日志等于闭着眼睛写代码。我用一个统一的log_send函数输出格式化的日志包含事件类型、包序号和时间戳def log_send(seq, event): if PRINT_LOG: print(f[{time.time():.3f}] {event:10} | seq{seq})输出类似这样的内容[1634012345.123] send | seq0 [1634012345.223] send | seq1 [1634012345.323] simulate_loss | seq1 [1634012345.623] ack | seq1 [1634012345.923] retransmit | seq1观察点有三个send事件显示窗口逐个推进、ack事件显示send_base向前滑动、retransmit事件说明超时发生并且从窗口起点开始批量重发。这三类事件组合起来能直观画出窗口的时间轴变化。4. 运行与排查从零跑通滑动窗口模拟附排查清单4.1 环境准备与启动步骤环境只需要Python 3.6以上版本不需要安装第三方库。先把文件按之前的目录结构放好config.py里的参数确认无误然后分两个终端分别启动。操作步骤# 终端一: 先启动接收端 python receiver.py # 终端二: 再启动发送端 python sender.py顺序上必须先启动接收端因为发送端启动后会立即开始发包如果接收端还没有bind端口第一批包就会全部丢失导致一堆超时重传。发送端启动时默认发送当前目录下的data.txt也可以自己指定文件路径。跑起来之后能看到两类日志刷屏一类是sender.py的send和ack打点另一类是receiver.py没做详细输出需要的话可以在接收端也加上日志方式相同接收ACK事件和交付事件都会变成一行行的文本。4.2 core参数说明与调整建议config.py里集中了所有影响协议行为的参数理解每个参数的含义是调参的前提。主要参数如下参数名默认值作用与调整建议WINDOW_SIZE4发送窗口大小。调大提高吞吐但丢包时回退代价增大ACK_TIMEOUT0.5超时重传阈值单位秒。模拟场景局域网建议0.3~0.8太大导致等待过久太小引发频繁重传LOSS_RATE0.2模拟丢包率。首次验证建议先设0跑通后再调大观察重传BUFFER_SIZE65536UDP socket接收缓冲区。调大能减少因缓冲不足丢包的概率MAX_PKT_SIZE1400单包最大长度建议小于MTU避免IP层分片这组参数的实验价值在于把LOSS_RATE设0看窗口平滑滑动把ACK_TIMEOUT设很小比如0.1秒看大量重传把WINDOW_SIZE从1调到8对比吞吐变化。每次只动一个变量避免多参数耦合导致难定位问题。4.3 常见问题排查踩坑记录问题一接收端收不到任何包发送端疯狂超时现象sender日志里全是retransmitreceiver终端没有任何输出。原因最常见的是接收端没启动就发送或使用的UDP端口不对。UDP无连接包丢了双方都没有感知只能靠重传自我治愈。另一个隐蔽原因是Windows防火墙默认拦截了Python的UDP入站流量。解决确认receiver先运行且bind成功然后ping通对端IP验证网络路径。Windows下需要允许Python进程通过防火墙macOS和Linux此问题较少。用netstat -ulnp确认端口处于LISTEN状态更直接。问题二重传风暴日志里retransmit数量和正常发送一样多现象LOSS_RATE设成0.5之后日志里大量重传正常的数据交互几乎看不太到。原因丢包率过高导致链路基本不可用发送的包和回传的ACK持续在途丢失窗口几乎无法推进。GBN协议在这种高丢包环境下效率很差这是协议本身特点。解决把LOSS_RATE调回0.1到0.2之间。如果想把丢包模拟得更接近真实网络可以在发送数据包和接收ACK时分别用独立的丢包率数据包丢包率0.2ACK丢包率0.05更贴近真实链路。问题三接收端交付的数据顺序错乱或重复现象打印接收到的数据块时序号不是连续递增的有的序号出现两次。原因乱序缓存逻辑没生效通常是代码里out_of_order的提交时机不对。如果只在seq expected_seq时回ACK而忽略seq expected_seq时立即回ACK的步骤发送端收到的ACK永远是旧的期望序号重传逻辑就会重复发包。解决核对接收端的ACK是否在每次收到数据包后都发送且ACK携带的expected_seq是否更新为当前期望序号。这是GBN和SR协议最容易写错的地方值得重点核对。问题四sender用TCP连接去模拟现象代码里写的是SOCK_STREAM数据收发倒是没报错但滑动窗口的日志完全无法观察所有重传逻辑永远不会触发。原因TCP底层已经把可靠传输做完了应用层只能看到有序字节流自己写的重传代码永远不会被调用。解决把socket类型改成SOCK_DGRAM彻底走UDP承载。4.4 如何验证正确性项目自带一个数据校验机制发送端读取data.txt按固定块大小切分接收端把交付的数据块按序拼接后与原文件对比。运行结束时打印比对结果一致则输出成功提示。手动验证时也可以做以下检查正常跑一遍收尾时看发送窗口是否完全滑到末尾、缓存是否清空再把LOSS_RATE从0调到0.2重复运行前后数据文件内容一致。这两条验证通过说明滑动窗口的重传和接收逻辑没有破坏数据完整性。5. 二次开发改造思路从固定窗口演进到自适应机制这个项目跑通只是第一步真正的价值在于继续改造。顺着滑动窗口这个核心思路往下走可以做三个方向的升级每一个都能让模拟器离真实TCP更近一步。第一个改造方向是动态窗口调整。目前WINDOW_SIZE是固定的真实网络环境下的传输性能会随拥塞程度动态变化。改造方式是增加一个拥避模块在发送端记录每次ACK到达的时间间隔如果间隔稳定就线性增大窗口出现超时就把窗口减半模拟出慢启动和拥塞避免的行为曲线。这比直接改WINDOW_SIZE变量更有实验价值调参过程本身就是在理解TCP拥塞控制。第二个方向是选择性重传SR目前代码是GBN模式超时把所有未确认包重发一遍。改成SR需要接收端缓存窗口内所有乱序包、单独标记每个包是否收到发送端只重传超时的那一个。改动量集中在buffer结构和超时分支但难度比初中级开发预期的要大因为每个包的ACK状态都要精细更新正好可以用来练手。第三个方向是做可视化。把每次send、ack、retransmit事件的时间序列写进CSV文件用Python的matplotlib画成窗口随时间变化的面积图能直观看到窗口滑动的锯齿形状。另一个轻量方案是把日志格式对齐成标准GANTT图格式直接导入在线甘特图工具里查看。我在自己改这个项目的最大收获是弄懂了为什么超时重传和累积ACK组合在一起会带来队头阻塞单个包丢失会导致后续所有已到达的包阻塞在缓存里等待重传才能交付。出于教学目的我还加了个延迟模拟参数人为让ACK滞后几个RTT观察它对窗口推进的影响。从那以后我每次改协议代码都会强制走一遍同样的流程先关丢包验证基本逻辑再开20%丢包观察重传对比修改前后的交付内容一致性。这个习惯帮我避开了好几次网络实验里的低级问题。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑