C++实现UDP可靠传输(二)

news2024/10/7 14:28:45

声明:禁止以任何形式转载本文章。本文章仅供个人学习记录与交流探讨,文章中提供的思路只是一种解决方案,代码也并非完整代码,如有需要,请自行设计协议并完成编程任务。 

食用本文章之前,推荐阅读:C++实现UDP可靠传输(一)

目录

流量控制

GBN算法

程序实现

发送消息的函数

计时线程

接收ACK消息的线程

拥塞控制

New RENO算法

程序实现

计时线程

接收消息的线程

发送消息的函数

程序测试


在上篇文章中,我们实现了一个简单的基于UDP协议的可靠传输,主要使用rdt3.0协议,完成了差错检测和确认重传功能。但这个协议的设计还存在一些缺陷,例如,流量控制采用停等机制可能造成延时过长,没有设置拥塞控制等。在这篇文章中,我们将在上篇文章的基础上改进:

  • 使用GBN算法,实现基于滑动窗口的流量控制机制
  • 使用New RENO算法,实现拥塞控制

流量控制

在停等机制中,我们只能一次发送一个数据包,等到该数据包被确认后才能发送下一个数据包,这种情况下链路利用率不高,存在性能问题。我们可以对此进行改进,采用流水线机制,也就是在确认未返回之前允许发送多个数据包。

但是,我们不能一直发送数据包,否则接收端的缓冲区可能溢出,也可能中途某个数据包丢失,造成失序。本文章中,主要介绍使用GBN算法实现流量控制的方法。

GBN算法

GBN算法的基本思想如下:

  • 允许发送端发出N个未得到确认的分组
  • 采用累积确认,只确认连续正确接收分组的最大序列号
  • 发送端设置定时器,定时器超时时,重传所有未确认的分组

也就是说,使用GBN算法和上一篇文章中的程序设计不同之处在于:

  • 需要增加序列号空间:其实rdt3.0协议中,只需要两个序列号,但是在上一篇文章中,我们设计的序列号有8 bits,能够表示0-255,因此这一点可以忽略。
  • 累积确认,接收端不缓存失序的数据包。
  • 发送端重传只靠超时,如果接收的ACK消息出错,发送端什么也不做。

发送端的有限状态机如下:

接收端的有限状态机如下:

程序实现

首先来设置一些全局变量,之后我们会看到它们的用途。

int resendCount = 0;		// 重传次数
int base = 2;				// 窗口最左边
unsigned char nextSeq = 0;	// 要发送的下一个序列号
bool resend = false;		// 是否超时重传
bool restart = false;		// 是否重新开始计时
bool wait = false;			// 是否因窗口不够而需等待
long int lenCopy = 0;		// 文件数据偏移量的拷贝,供重传时使用

我们现在采用流水线机制发送消息,因此必须要使用线程来负责计时和接收消息。大家先记住这个需求,我们先写主体部分,也就是发送消息的函数,写完后就明白两个线程应该怎么写了。

发送消息的函数

我们先来看看发送消息的函数相比上一篇文章中有什么变化。

首先当然是窗口的设置。我们需要设置一个固定窗口大小window,窗口的空间就在base到window(不含)之间。

其次是发送消息的部分。发送消息时,我们需要判断这条消息的序列号是否在窗口空间内,如果是则发送,如果不是,则等待。等到什么时候呢?直到收到ACK消息或超时重传。这就需要用到两个线程了。

wait标志位表示是否等待窗口,在接收消息的线程中,如果收到正确的ACK消息,线程会更新窗口大小,这时会将wait设置为false。resend标志位表示是否超时重传,在计时的线程中,如果发现超时,我们需要将resend设置为true。

// 若下一个序列号在窗口内,发送消息
// 注意base+window>=256的情况
if (((base + window) < 256 && (int)nextSeq < base + window) ||
	((base + window) >= 256 && ((int)nextSeq >= base || (int)nextSeq < (base + window) % 256))) {
	if (sendto(sockSender, sendBuf, h.len + sizeof(h), 0, (SOCKADDR*)&addrReceiver, sizeof(SOCKADDR)) == -1) {
		cout << "Error: fail to send messages!" << endl;
		return false;
	}
	else {
		nextSeq = (nextSeq + 1) % 256;
		sentLen += (int)h.len;
	}
}
// 若下一个序列号不在窗口内,等待ACK
else {
	cout << "Waiting for window!" << endl;
	wait = true;
	while (wait & !resend) {
		Sleep(100);
	}
}

最后是超时重传的部分。记住计时的部分我们会单独写在计时线程中,线程会判断是否超时并设置resend标志位。在发送消息的函数中,如果resend标志位被设置为true,那么我们需要重传所有未被确认的分组,即从base到nextSeq的数据包。

看过上一篇文章的读者应该记得,在发送消息的函数中,我写了一个for循环,从k=0开始,发送packNo个数据包。在这里我设计的超时重传方式是更新k和其它变量,让for循环跳到base所对应的k处,相当于重传。大家也可以采取其它方式,例如直接在这里调用sendto函数。

// 超时重传
if (resend) {
	if (resendCount >= maxResend) {
		cout << "Fail to send messages!" << endl;
		return false;
	}
	if ((nextSeq - 1) < base) {
		k -= ((int)nextSeq + 256 - (int)base + 1);
	}
	else {
		k -= ((int)nextSeq - (int)base + 1);
	}
	nextSeq = base;
	sentLen = lenCopy;
	resend = false;
	resendCount++;
}

明确了发送消息的函数有什么变化后,我们可以来编写计时线程和接收消息的线程。 

计时线程

计时线程比较简单,我们设置了restart标志位,只要在其他的线程中正确设置这个标志位,即计时线程就可以正常运行。我们还设置了resend标志位,这是负责通知主线程应该超时重传了。细心的读者可能发现,我总是通过标志位来统一线程,这是因为我希望线程尽可能保持简单,毕竟线程一旦出错很难debug。

DWORD WINAPI timer(LPVOID lparam) {
	clock_t start = clock();
	while (true) {
		if (restart) {
			start = clock();
			restart = false;
		}
		if (clock() - start > maxTime) {
			resend = true;
			start = clock();
		}
	}
	return 0;
}

接收ACK消息的线程

接收消息的线程只负责一件事,就是接收到正确的ACK消息后更新窗口,所以核心其实只有一句:base = (h2.seq + 1) % 256。但是为了程序能够正常运转,我们还需要正确的设置其他的全局变量,例如重新开始计时、停止等待窗口等。特别需要注意的是,需要更新lenCopy,这个值是base所对应的文件的偏移量,在超时重传的时候会用到。大家也可以采用其它的方法标记,只要超时重传能够从base对应的地方读取文件即可。

DWORD WINAPI recvThread(LPVOID IpParameter) {
	SOCKET sockSender = *(SOCKET*)IpParameter;
	int addrlen = sizeof(SOCKADDR);
	char recvBuf[1024];
	memset(recvBuf, 0, sizeof(recvBuf));
	while (true) {
		if (recvfrom(sockSender, recvBuf, 1024, 0, (SOCKADDR*)&addrClient, &addrlen) > 0) {
			HeadMsg h2;
			memcpy(&h2, recvBuf, sizeof(h2));
			// 收到消息需要验证校验和
			if (h2.type == ACK && !checkSumVerify((u_short*)&h2, sizeof(h2))) {
				lenCopy += (h2.seq - base + 1) * maxSize;
				base = (h2.seq + 1) % 256;
				restart = true;
				resendCount = 0;
				wait = false;
			}
			// 如果报文损坏,什么都不做,重传只靠超时
		}
	}
	return 0;
}

 至此,我们就基本完成了GBN算法实现的流量控制机制。下面我们来看拥塞控制。

拥塞控制

为了防止主机发送的数据过多或过快,造成网络中的路由器(或其他设备)无法及时处理,从而引入时延或丢弃,我们在发送消息时,还需要进行拥塞控制。TCP协议采用基于窗口的方法,通过拥塞窗口的增大或减小控制发送速率。本文章中,我们也借鉴这样的做法,使用New RENO算法实现拥塞控制。

New RENO算法

先来看看RENO算法的原理。文字描述不太清楚,大家还是看有限状态机:

RENO算法定义了三个阶段:

  • 慢启动阶段:初始拥塞窗口的大小cwnd=1,每个RTT,cwnd翻倍(指数增长),但收到重复ACK消息时cwnd不变。
  • 拥塞避免阶段:每个RTT,cwnd增1(线性增长),但收到重复ACK消息时cwnd不变。
  • 快速恢复阶段:每个RTT,cwnd翻倍(指数增长)。

这三个阶段之间的关系如下:

  • 连接建立时,进入慢启动阶段(可以理解为刚开始只发送一个报文,试探一下网络的情况)。
  • 拥塞窗口达到阈值时,慢启动阶段结束,进入拥塞避免阶段。
  • 收到三次重复ACK消息时,进入快速恢复阶段。
  • 在快速恢复阶段,收到新的ACK消息时,进入拥塞避免阶段。
  • 超时的情况下,进入慢启动阶段。

其中,在快速恢复阶段,收到新的ACK消息时,New RENO算法和RENO算法的不同之处在于,它会检查这个New ACK是否为已经发送的最后一个数据包的序列号,也就是说是否所有发送的数据包都被确认,如果不是,将会继续保持快速恢复,而不会进入拥塞避免。这样程序就不会频繁地在快速恢复和拥塞避免两个阶段反复横跳,造成阈值多次折半的问题。

另外,需要注意的一点是,流量控制和拥塞控制都是基于窗口的机制,二者在窗口大小的计算上是独立的。流量控制的窗口大小取决于接收端的缓冲区,只是我们上面为了简单将其设置为固定大小;拥塞控制的窗口大小是基于网络的情况。而实际发送窗口取决于接收通告窗口和拥塞控制窗口中的较小值

程序实现

有了之前的程序,实现New RENO算法只要在此基础上稍加修改即可。

设置全局变量如下:

const int rwnd = 10;		// 接收通告窗口大小,固定值
double cwnd = 1;			// 拥塞窗口大小
int ssthresh = 8;			// 阈值
unsigned char lastAck = 0;	// 上一个ACK序列号
int dupAck = 0;				// 重复收到的ACK次数
int renoState = 0;			// RENO状态机的状态,0为慢启动,1为拥塞控制,2为快速恢复

计时线程

计时线程需要加上超时后阈值和拥塞控制窗口大小的变化情况,以及更新RENO状态机的状态。

if (clock() - start > maxTime) {
    // 超时后阈值减半,窗口大小为1
    ssthresh = cwnd / 2;
    cwnd = 1;
    dupAck = 0;
    // 超时则进入慢启动阶段
    if (renoState != 0)
        renoState = 0;
}

接收消息的线程

接收到正确的ACK消息后,需要加上RENO状态机的三个状态的处理(参照有限状态机)。

// 慢启动
if (renoState == 0) {
    // 判断是否为new ACK,是的话更新窗口空间
    if (h2.seq == lastAck) {
        dupAck++;
    }
    else {
        dupAck = 0;
        lastAck = h2.seq;
        cwnd++;
    }
    // 窗口大小超过阈值,进入拥塞避免阶段
    if (cwnd >= ssthresh)
        renoState = 1;
    // 重复ACK超过3次,进入快速恢复阶段
    if (dupAck == 3) {
        ssthresh = cwnd / 2;
        cwnd = ssthresh + 3;
        renoState = 2;
    }
}
// 拥塞避免
else if (renoState == 1) {
    if (h2.seq == lastAck) {
        dupAck++;
    }
    else {
        dupAck = 0;
        lastAck = h2.seq;
        cwnd += 1.0 / cwnd;
    }
    // 重复ACK超过3次,进入快速恢复阶段
    if (dupAck == 3) {
        ssthresh = cwnd / 2;
        cwnd = ssthresh + 3;
        renoState = 2;
    }
}
// 快速恢复
else {
    if (h2.seq == lastAck) {
        cwnd++;
    }
    // 接收到new ACK,需要判断是否发送的消息都被接收
    // 如果不是,保持快速恢复;如果是,进入拥塞避免阶段
    else {
        if (h2.seq == (nextSeq - 1 + 256) % 256) {
            renoState = 1;
            lastAck = h2.seq;
        }
    }
}

发送消息的函数

在发送消息的函数中,window应该代表实际发送窗口的大小,而这取决于接收通告窗口和拥塞控制窗口中的较小值。本文章将rwnd设置为固定窗口大小,读者可以将其设置为可变的,由接收端发送来的消息确定,这就需要在消息头中加上相关的字段表示rwnd。

// 实际发送窗口取决于接收通告窗口和拥塞控制窗口中的较小值
int window = 0;
if (rwnd < cwnd)
    window = rwnd;
else
    window = cwnd;

程序测试

运行程序,可以看到三次握手的过程,成功建立连接。发送端输出消息提示用户输入需要传输的文件或者断开连接。

我们随便选择一个命令,发送端很快开始发送文件,并输出相关信息。从左到右依次为,发送的数据长度(字节)、消息类型、序列号、校验和、窗口的情况(base为窗口左端,tail为窗口右端)、CWND(拥塞控制的窗口大小)、拥塞控制的阈值。注意,接收通告窗口大小固定为10,方便测试并观察。

可以看到一开始拥塞控制处于慢启动阶段,每次收到ACK消息窗口都增加1。直到cwnd> ssthresh,程序进入拥塞避免阶段,窗口大小增长减慢。

直到cwnd=9时,实际的窗口大小还是base+cwnd。cwnd>10后,实际的窗口大小取接收通告窗口和拥塞控制窗口中的较小值,即接收通告窗口10,此后不论cwnd如何增长,实际的窗口大小都为10。

突然,观察到cwnd=11, ssthresh=8,显然这不是拥塞避免阶段了,发生了什么?

查看接收端,原来刚刚接收端接收到了损坏的数据包(通过写程序人为破坏),因此丢弃了序列号为49的数据包,并且一直在等待它。而发送端还不知道,依然在流水线地发送后面的数据包,所以接收端将后面来的数据包都丢弃了,并返回ACK=48的消息。发送端一直收到重复的ACK消息,重复ACK次数为3时,发送端会进入快速恢复阶段,ssthresh=cwnd/2, cwnd=ssthresh+3,和我们在发送端观察到的一致。

可以看到这时候窗口为49-59(不含),发送端发送完序列号为58的数据包后,开始等待窗口。超时后,发送端重传所有未被确认的数据包,也就是从序列号49到序列号58。这时,拥塞控制从快速恢复阶段进入慢启动阶段,阈值减半,cwnd=1。

查看接收端,发送端重传的数据报被接收端正常收到。

最后,所有数据包都发送完毕并被成功确认后,发送端输出提示消息,并输出传输时间和吞吐率。

查看接收端,同样也输出了相关信息。接收消息完毕后,成功写入文件。

发送端断开连接,可以看到四次挥手的过程,成功断开连接。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RKMEDIA--VENC/VDEC使用

前面两篇已经介绍了VI和VO的使用&#xff0c;本章节来介绍rkmedia且也是瑞芯微平台重点部分&#xff1a;编解码。 目录 一、简介 二、编解码能力 三、编码 编码初始化&#xff1a; 在初始化VENC时需要注意几点&#xff1a; venc编码帧率控制&#xff1a; 编码添加osd位图…

软件设计师考试重点1 计算机组成与体系结构

软件设计师考试重点1 计算机组成与体系结构一、 数据的表示1. 进制转换2. 原码/反码/补码/移码3. 数值表示范围4. 浮点数的运算二、运算器与控制器1. 计算机结构2. 计算机五大组成部分&#xff1a;3. CPU组成部分&#xff1a;4. 控制器组成部分&#xff1a;5. 运算器组成部分&a…

如何构建一个自动化油田注水站监控系统?

一、应用背景 目前&#xff0c;在大部分的钻井平台中&#xff0c;维护油田注水站的工作状态主要通过人工方式进行&#xff0c;这种方式不仅作业效率低且对工人的经验有着较高要求。此外&#xff0c;油田注水站的工作环境恶劣&#xff0c;为了能够有效地掌握各个设备的工作状态…

【测试沉思录】20. 如何做好测试需求分析?

作者&#xff1a;刘亚茹 编辑&#xff1a;毕小烦 我们都知道测试用例是软件测试中保障质量的必要手段&#xff0c;而测试需求作为用例编写的主要依据却往往被很多人忽视。到底什么是测试需求&#xff1f;又如何做好测试需求分析呢&#xff1f;本文带你了解一下。 1. 测试需求到…

Java项目如何导出数据为 PDF 文件?

文章目录Java项目如何导出数据为 PDF 文件&#xff1f;一、代码结构如下二、代码说明1、添加依赖 pom.xml3、添加字体4、PDF 导出工具类三、效果图结语Java项目如何导出数据为 PDF 文件&#xff1f; 一个小需求&#xff0c;需要将页面上的数据导出为PDF&#xff0c;正常情况下…

11月业务安全月报 | 台湾2300万人信息泄露;黑客两分钟即可破解安卓锁屏;乌克兰“IT军团”入侵俄罗斯中央银行

导语&#xff1a;随着数字化的深入普及&#xff0c;业务愈加开放互联。企业的关键数据、用户信息、基础设施、运营过程等均处于边界模糊且日益开放的环境中&#xff0c;涉及利益流和高附加值的业务面临多样的安全隐患&#xff0c;随时可能遭遇损失&#xff0c;进而影响企业运营…

Android 8.0网络DNS

1 Linux DNS规范 Linux上并没有一个单独的方法可以完成DNS查询工作&#xff1b;没有一个有这样的明确接口的核心系统调用system call。不过&#xff0c;glibc (nss)的getaddrinfo (3), gethostbyname (3)等相关API (RFC3493)提供了DNS查询功能。 1&#xff09;不支持nscd&#…

关于使用STM32CubeMx配置串口出现的一些问题

一、使用CubeMX配置好串口中断的工程&#xff0c;打开工程添加相关代码后串口没有数据输出或者看不见数据但串口助手的RX会一直增加的问题。 参考&#xff1a;这里 大概意思就是时钟的原因&#xff0c;需要把stm32f4xx_hal_conf.h文件中的时钟改一下&#xff0c;将25MHz改为8…

Ansys Zemax | 使用 OpticStudio 进行闪光激光雷达系统建模(中)

在消费类电子产品领域&#xff0c;工程师可利用激光雷达实现众多功能&#xff0c;如面部识别和3D映射等。尽管激光雷达系统的应用非常广泛而且截然不同&#xff0c;但是 “闪光激光雷达” 解决方案通常都适用于在使用固态光学元件的目标场景中生成可检测的点阵列。凭借具有针对…

我要涨知识——TypeScript 经典高频面试题(二)

又是一个年底来了&#xff0c;好大一批人可能又准备跑路了&#xff0c;翻了翻掘金和 CSDN 发现好多大佬都有大厂 Offer &#xff0c;看着看着我心动了&#xff01; 话不多说&#xff0c;赶紧开干&#xff0c;给自己整了一个前端面试小助手——微信小程序内搜索 “WEB学习学习加…

jar启动指定JDK/JRE 安装路径教程

前言 因为疫情在家办公的缘故&#xff0c;有个老项目&#xff0c;需要改个接口&#xff0c;然后需要前端联调&#xff0c;因为外网服务器没有多余的空间了&#xff0c;想着把jar给前端让前端开发人员&#xff0c;在自己的本机启动后端服务&#xff0c;进行接口联调&#xff0c;…

streptavidin-PEG-6-FAM 链霉亲和素-聚乙二醇-6-羧甲基荧光素

产品名称&#xff1a;链霉亲和素-聚乙二醇-6-羧甲基荧光素 英文名称&#xff1a;streptavidin-PEG-6-FAM 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体&#xff0c;取决于分子量 PEG分子量可选&#xff1a;350、550、75…

虚拟内存系统【如何支持巨大的虚拟地址空间】

如何支持巨大的虚拟地址空间&#x1f4d6;1. 为什么要支持巨大的虚拟地址空间&#x1f4d6;2. 交换空间&#x1f4d6;3. 存在位&#x1f4d6;4. 页错误&#x1f4d6;5. 为了处理页错误&#xff0c;操作系统大致做了什么&#xff1f;&#x1f4d6;6. 页面换出&#x1f4d6;7. 当…

IDEA 中使用 SparkSQL 远程连接 Hive

文章目录第一步&#xff0c;环境配置第二步&#xff0c;IDEA 配置第三步&#xff0c;授权第四步&#xff0c;连接测试扩展——华为云/阿里云集群报错第一步&#xff0c;环境配置 首先&#xff0c;你要确保你的集群可以正常运行。 我们在 Windows 电脑中安装 Hadoop&#xff0…

【新知实验室--音视频通话】腾讯云TRTC-实时音视频---多人会议视频通话SDK基础搭建

&#x1f996;我是Sam9029&#xff0c;一个前端 Sam9029的CSDN博客主页:Sam9029的博客_CSDN博客-JS学习,CSS学习,Vue-2领域博主 &#x1f431;‍&#x1f409;&#x1f431;‍&#x1f409;恭喜你&#xff0c;若此文你认为写的不错&#xff0c;不要吝啬你的赞扬&#xff0c;求收…

基于STM32单片机的篮球计时记分器proteus仿真原理图PCB

功能&#xff1a; 0.本系统采用STC89C52作为单片机 1.LCD1602液晶实时显示比赛剩余时间&#xff0c;球队分数 2.默认计时器为4节&#xff0c;每节10分钟&#xff0c;每节比赛结束&#xff0c;蜂鸣器报警 3.按键功能介绍: 1’键——加1分 4’键——减1分 2’键——加2分 5’键—…

基于java+springmvc+mybatis+vue+mysql的水果食品果蔬生鲜商城销售系统

项目介绍 网络购物作为一种全新的销售方式赢得了越来越多销售者的青睐&#xff0c;近年来销售额更是以连年翻番的惊人速度成倍增长&#xff0c;网络购物已经形成了自身特有的网络销售市场和全新的网络营销模式&#xff0c;也使网络营销渠道应运而生&#xff0c;同时&#xff0…

SRM供应商平台哪些好用?

SRM系统是管理供应商的系统&#xff0c;旨在与供应商建立高效的协同关系&#xff0c;是一种围绕采购业务的双赢管理模式。 但市面上SRM系统供应商层出不穷&#xff0c;很多企业在数字化转型中不知如何挑选合适自己企业的SRM系统&#xff0c;或是选错了系统&#xff0c;效率提升…

成功的项目管理工具拥有的五大功能

如果您决定投资一种新的项目管理工具&#xff0c;那么您需要确保购买的是最适合自己的。在为您的企业寻找最佳项目管理软件时&#xff0c;不要被所有的花里胡哨分心&#xff0c;因为某些东西看起来很漂亮并不意味着它有实质。不要错过对团队成功至关重要的关键功能&#xff0c;…

【论文精读7】MVSNet系列论文详解-PVA-MVSNet

PVA-MVSNet论文名为&#xff1a;Pyramid Multi-view Stereo Net with Self-adaptive View Aggregation&#xff0c;主要是用了一个自适应的聚合模块来在构建代价体时不用均匀的方差、而是让不同的特征体具有一定的选择注意力权重来对最终的代价体做贡献&#xff0c;同时对于多尺…