socket编程UDP-实现滑动窗口机制与累积确认GBN

在下面博客中,我介绍了利用UDP模拟TCP连接、按数据包发送文件的过程,并附上完整源码

socket编程UDP-文件传输&模拟TCP建立连接脱离连接(进阶篇)_udp socket发送-CSDN博客

下面博客实现了停等机制。

socket编程UDP-实现停等机制(接收确认、超时重传)-CSDN博客

本篇博客,我将在此基础上实现滑动窗口机制,完成客户端发送服务器接收的累积确认(GBN)

目录

一、协议设计

1.思路概览

2.完整实例 

二、核心代码

1.代码实现思路

 2. 核心源码

3.可运行完整源码

三、运行演示

1.正常传输(收到正确序列号)

2.乱序抵达(出现丢包情况,收到偏大/偏小数据包)


一、协议设计

1.思路概览

​ 1)对于发送端(客户端)发送窗口为N>1,每次发送的基准窗口设置为base(即每轮发送的最小序列值)。并且:

  • 允许发出N个未得到确认的分组,连续发送而不等待接收端的回复

  • 如果在定时范围内收到该轮最大的ack(expectedAck=base+N-1),立即更新基准窗口base=expectedAck+1,并且开始下一轮发送。(也就是说,即使定时器时间未到,也立刻开始下一轮发送)

  • 如果在定时范围内没有收到该轮最大的ack,将base更新为这段时间收到的最大ack值+1,即base=max(getAck)+1.并且从base开始进行下一轮发送

2)对于接收端(服务器端)接收窗口大小设置为1,因此每收到一个数据包,都会发送一次ack。为了处理丢包,设置buffer缓冲区,用于存储乱序抵达的包

  •  如果收到期待的数据包(seq==expectedseq,其中expectedseq=last\_ack+1),将数据写入文件,并检查缓冲区中是否存储有后续数据包的内容;如果有,将缓冲区的内容写入文件。依据已经写入文件的数据包序列号来更新expectedseq,并发送ack = expectedSeq - 1。

  •  如果收到的数据包序列号大于期待值(seq>expectedseq),将数据暂存进缓冲区buffer,发送最后一次按序抵达的数据包对应的ack.

  • 如果收到的数据包序列号小于期待值(seq<expectedseq),说明收到了重复的数据包,,只发送最后一次按序抵达的数据包对应的ack

2.完整实例 

 

  • 发送窗口大小设置为4,初始时客户端连续发送序列号为0,1,2,3的四个数据包。

  • 图中的seq=0,seq=1的数据包按序抵达(seq==expectedseq,其中expectedseq=last\_ack+1),服务器端会发送对应的ack=0,ack=1.

  •  图中seq=2的数据包丢失,于是当seq=3的数据包抵达时,服务器端收到了偏大的序号(seq=3>expectedseq=2)。服务器端会暂存seq=3的数据包到缓冲区中,并发送已经收到的最大正确序号(ack=1).

  • 客户端一直期待收获到“ack=3”,等待一段时间没有收到,说明超时。于是基准窗口移动为收到的最大ack+1,即1+1=2,发送序列号为2,3,4,5的四个数据包。

  •  服务器端收到seq=2的数据包后,由于之前暂存了seq=3的数据包,因此ack更新为3,将两个数据包一起写入文件,并发送ack=3.

  • 服务器端再次收到seq=3的数据包,不会重复写入,依旧发送ack=3.

  •  服务器端成功接收四个数据包,客户端收到期待的ack(ack=5),不再等待定时器到时,立刻更新基准窗口为6,开始下一轮发送。

二、核心代码

1.代码实现思路

服务器端为接收方,只需要简单地接收数据包并发送对应ack.

而客户端的实现较为复杂,主要逻辑如下图:

客户端每收到一个ack,就会比较ack与getACK的值,把最大ack赋给getACK.

如果客户端收到了期待的最大ack(即传输过程中没有出现丢包),会立即更新base并开始下一轮发送;

如果客户端未收到期待的最大ack,会陷入等待,定时器到时后会将base更新为getAck+1,已经读取过的数据包重新发送、未读取的数据包读取后再发送。 

 2. 核心源码

 bool Sender::waitForAck() {
    std::unique_lock<std::mutex> lock(mtx);
    //如果 `ackReceived` 在超时之前变为 `true`,则 `wait\_for` 返回并继续执行后续代码;
    //如果超时后 `ackReceived` 仍为 `false`,则 `wait\_for` 也会返回。
    return cv.wait_for(lock, std::chrono::milliseconds(5*TIMEOUT), [this]() { return ackReceived.load() ; });//超时或者收到所有ack返回
}
 void Sender::receiveAck() {
    Datagram ackPacket(SERVER_PORT,ROUTER_PORT);
    socklen_t len = sizeof(routerAddr);
    while (true) {
        if (recvfrom(sock, reinterpret_cast<char*>(&ackPacket), sizeof(ackPacket), 0, (struct sockaddr*)&routerAddr, &len) > 0) {
            if (ackPacket.flag == 0 && ackPacket.validateChecksum(clientAddr.sin_addr.S_un.S_addr, routerAddr.sin_addr.S_un.S_addr)) {
                if(ackPacket.ack==65535)
                {
                    continue;
                }
                std::lock_guard<std::mutex> lock(mtx);
                std::cout << "收到ack=" << ackPacket.ack << std::endl;
                if(ackPacket.ack>getAck){getAck=ackPacket.ack;}
                if(ackPacket.ack==expectedAck)
                {
                    ackReceived = true;
                    cv.notify_one();
                }   
            }
        }
    }
}

void Sender::sendFile(const std::string& filename) {
      //.......
        base = 0;
        nextseq = 0;
        while (true) {
            //.......
            //1.基于窗口连续发送N条消息
            while (nextseq < base + WINDOW_SIZE && !file.eof()) {
                if(nextseq<3&&AckPacket.flag == 2&&AckPacket.validateChecksum(clientAddr.sin_addr.S_un.S_addr, routerAddr.sin_addr.S_un.S_addr))//2.如果此时又收到了SYN-ACK
                {
                   //...重新发送
                }
                std::unique_lock<std::mutex> lock(mtx);
                ackReceived = false;
                expectedAck = base+WINDOW_SIZE-1;
                Datagram packet;//默认构造,从客户端发往路由器端
                packet.seq = nextseq;
                file.read(packet.data, BUFFER_SIZE);
                packet.dataSize = static_cast<int>(file.gcount());
                packet.flag = 0;

                window[nextseq] = packet;
                //window.emplace(nextseq, packet);
                sendPacket(packet);
                std::cout << "发送数据包.SEQ=" << packet.seq <<",校验码="<< packet.checksum << std::endl;
                nextseq++;
                lock.unlock();
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(TIMEOUT));
            
            // 2.结束条件:如果所有包都被确认,则跳出
            if (base == nextseq && file.eof()) break;
            if (waitForAck()) {
                std::unique_lock<std::mutex> lock(mtx);
                base = base + WINDOW_SIZE;
                std::cout << "base窗口后移为:" << base << ",开始下一轮发送" << std::endl;
            } else {
                std::unique_lock<std::mutex> lock(mtx);
                if(getAck==-1){base=0;}
                else{base =getAck+1;}
                std::cout << "未收到所有ack,base窗口后移为:" << base << ",重新发送此部分" << std::endl;
                for (int i = base; i < nextseq; ++i) {
                    sendPacket(window[i]);
                    std::cout << "发送数据包.SEQ=" << window[i].seq << ",校验码=" << window[i].checksum << std::endl;
                }
            }
        }
        //.......
    }

3.可运行完整源码

已上传github:

https://github.com/yeyeyeyeye-zhang/Computer-Network/tree/main/lab3-2/codes

三、运行演示

在src目录下输入:

 g++ -o cs main.cpp Datagram.cpp Sender.cpp Receiver.cpp -lws2_32

1.正常传输(收到正确序列号)

客户端发送数据包,收到期待ack值,窗口正常后移的情况

服务器端收到期待序列号的数据包,正常发送对应ack的情况

2.乱序抵达(出现丢包情况,收到偏大/偏小数据包)

客户端发送时出现丢包现象,丢失seq=493的数据包,收到的ack值为492.定时器超时后,base窗口后移为493,发送从493到496的数据包。

 服务器端未收到seq=493的数据包,之后再收到seq=494,495的数据包都仅发送ack=492.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/938558.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux 网络流量控制 - 实现概述

摘要 Linux 提供了一整套丰富的流量控制(traffic control)功能。本文档概述了相应的内核代码设计&#xff0c;描述了其结构&#xff0c;并通过描述一种新的排队策略来说明新元素的添加。 1 引言 最近的Linux内核提供了多种流量控制功能。Alexey Kuznetsov&#xff08;kuznet…

学习日志024--opencv中处理轮廓的函数

目录 前言​​​​​​​ 一、 梯度处理的sobel算子函数 功能 参数 返回值 代码演示 二、梯度处理拉普拉斯算子 功能 参数 返回值 代码演示 三、Canny算子 功能 参数 返回值 代码演示 四、findContours函数与drawContours函数 功能 参数 返回值 代码演示 …

.net core在linux导出excel,System.Drawing.Common is not supported on this platform

使用框架 .NET7 导出组件 Aspose.Cells for .NET 5.3.1 asp.net core mvc 如果使用Aspose.Cells导出excel时&#xff0c;报错 &#xff1a; System.Drawing.Common is not supported on this platform 平台特定实现&#xff1a; 对于Windows平台&#xff0c;System.Drawing.C…

AI视频配音技术创新应用与商业机遇

随着人工智能技术的飞速发展&#xff0c;AI视频配音技术已经成为内容创作者和营销人员的新宠。这项技术不仅能够提升视频内容的吸引力&#xff0c;还能为特定行业带来创新的解决方案。本文将探讨AI视频配音技术的应用场景&#xff0c;并讨论如何合法合规地利用这一技术。 AI视频…

【数字花园】个人知识库网站搭建:①netlify免费搭建数字花园

目录 [[数字花园]]的构建原理包括三个步骤&#xff1a;五个部署方案教程相关教程使用的平台 步骤信息管理 这里记录的自己搭建数字花园&#xff08;在线个人知识库&#xff09;的经历&#xff0c;首先尝试的是网上普遍使用的方法&#xff0c;也就是本篇文章介绍的。 后面会继续…

如何解决samba服务器共享文件夹不能粘贴文件

sudo vim /etc/samba/smb.conf在samba的配置文件中增加一个选项 writable yes重启Samba服务以使更改生效&#xff1a; sudo service smbd restart

NX系列-使用 `nmcli` 命令创建 Wi-Fi 热点并设置固定 IP 地址

使用 nmcli 命令创建 Wi-Fi 热点并设置固定 IP 地址 一、前言 在一些场景下&#xff0c;我们需要将计算机或嵌入式设备&#xff08;例如 NVIDIA Orin NX&#xff09;转换为 Wi-Fi 热点&#xff0c;以便其他设备&#xff08;如手机、笔记本等&#xff09;能够连接并使用该设备…

【Prometheus】Prometheus的样本

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

前端学习一

一 进程与线程 线程是进程执行的最小单位&#xff0c;进程是系统分配任务的最小单位。 一个进程可执行最少一个线程。线程分为子线程和主线程。 主线程关闭则子线程关闭。 二 浏览器进程 浏览器是多进程多线程应用。 进程包括&#xff1a; 浏览器进程 负责程序交互渲染…

鸿蒙开发-ArkTS 创建自定义组件

在 ArkTS 中创建自定义组件是一个相对简单但功能强大的过程。以下是如何在 ArkTS 中创建和使用自定义组件的详细步骤&#xff1a; 一、定义自定义组件 使用Component注解&#xff1a;为了注册一个组件&#xff0c;使其能够在其他文件中被引用&#xff0c;你需要使用Component…

探索Starship:一款用Rust打造的高性能终端

在终端的世界里&#xff0c;效率和美观往往并行不悖。今天&#xff0c;我们要介绍的是一款名为Starship的终端工具&#xff0c;它以其轻量级、高颜值和强大的自定义功能&#xff0c;赢得了众多开发者的青睐。 安装 任选一种方式进行安装 Windows &#x1fa9f; # scoop scoo…

[Unity] Text文本首行缩进两个字符

Text文本首行缩进两个字符的方法比较简单。通过代码把"\u3000\u3000"加到文本字符串前面即可。 比如&#xff1a; 效果&#xff1a; 代码&#xff1a; TMPtext1.text "\u3000\u3000" "选择动作类型&#xff1a;";

基于stm32的多旋翼无人机(Multi-rotor UAV based on stm32)

由于一直在调试本项目&#xff0c;好久没有发文章&#xff0c;最近本项目的PID调试初见成效&#xff01;开始正文前首先感谢各位粉丝的支持&#xff0c;以及对本项目技术上支持的老师以及师兄&#xff0c;谢谢你们&#xff01; 对应源码及文件&#xff1a;源码及文件下载 基于…

海量数据-Vastbase G100数据库安装

海量数据-Vastbase G100数据库安装 文章目录 海量数据-Vastbase G100数据库安装前期准备防火墙配置方案一&#xff1a;关闭防火墙方案二&#xff1a;开放数据库端口 SELINUX配置时间同步IPC参数配置 单机安装设置主机名创建数据库安装用户和目录(可选)修改资源限制 字符安装&am…

故障013:易忘的NULL表达式

故障013&#xff1a;易忘的NULL表达式 一、问题引入二、探索之路2.1 数据准备2.2 回顾NULL表达式2.3 重现问题2.3.1 分析原因2.3.2 如何化解预期&#xff1f; 三、知识总结 一、问题引入 某单位开发人员理直气壮抛出一张截图&#xff0c;以红色醒目地标记问题&#xff0c;好似…

Ubuntu22.04安装docker desktop遇到的bug

1. 确认已启用 KVM 虚拟化 如果加载了模块&#xff0c;输出应该如下图。说明 Intel CPU 的 KVM 模块已开启。 否则在VMware开启宿主机虚拟化功能&#xff1a; 2. 下一步操作&#xff1a; Ubuntu | Docker Docs 3. 启动Docker桌面后发现账户登陆不上去&#xff1a; Sign in | …

JVM(Java虚拟机)的虚拟机栈

JVM&#xff08;Java虚拟机&#xff09;的虚拟机栈是Java程序运行时的重要组件&#xff0c;以下是对其的详细解析&#xff1a; 一、概念与功能 概念&#xff1a;虚拟机栈也称为Java栈&#xff0c;是JVM为每个线程分配的一个私有的内存区域。每个线程在创建时都会创建一个虚拟…

集成自然语言理解服务,让应用 “听得懂人话”

如今&#xff0c;应用程序智能化已成趋势&#xff0c;开发者想要实现智能化&#xff0c;那么首先需要赋予应用理解自然语言的能力&#xff0c;使其能够准确地听懂人话&#xff0c;进而响应用户需求&#xff0c;并提供一系列智能化服务。比如用户语音控制应用程序帮忙订票&#…

Springboot3.x配置类(Configuration)和单元测试

配置类在Spring Boot框架中扮演着关键角色&#xff0c;它使开发者能够利用Java代码定义Bean、设定属性及调整其他Spring相关设置&#xff0c;取代了早期版本中依赖的XML配置文件。 集中化管理&#xff1a;借助Configuration注解&#xff0c;Spring Boot让用户能在一个或几个配…

fabric.js

目录 一、在canvas上画简单的图形 二、在canvas上用路径(Path)画不规则图形 三、在canvas上插入图片并设置旋转属性(angle) 四、让元素动起来(animate) 五、图像过滤器(filters)让图片多姿多彩 六、颜色模式(Color)和相互转换(toRgb、toHex) 七、对图形的渐变填充(Gradi…