声明:禁止以任何形式转载本文章。本文章仅供个人学习记录与交流探讨,文章中提供的思路只是一种解决方案,代码也并非完整代码,如有需要,请自行设计协议并完成编程任务。
食用本文章之前,推荐阅读:C++实现流式socket聊天程序
目录
在C++实现流式socket聊天程序中,我们使用TCP协议传输数据,TCP实现的是可靠传输。但对于简单的交互应用和一些对延时敏感的应用来说,TCP需要握手挥手、维护连接状态、差错重传,这些都会增加延时。因此,这些应用通常使用UDP服务,而需要在UDP之上,也就是应用层增加可靠机制,保证数据正常传输。
本文实现了一个简单的基于UDP协议的可靠传输,实现的功能主要有:
我们先来看看如何使用UDP协议发送和接收消息。
以Server服务器为例:
// 加载环境
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建数据报套接字
SOCKET sockServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 绑定ip地址和端口
sockaddr_in addrServer;
memset(&addrServer, 0, sizeof(sockaddr_in));
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8000);
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
bind(sockServer, (SOCKADDR*)&addrServer, sizeof(SOCKADDR))
// 客户端地址
sockaddr_in addrClient;
// 接收和发送消息,注意这里的recvfrom是阻塞函数
int len = sizeof(SOCKADDR);
while(true){
recvfrom(sockSrv, recvBuf, 1024, 0, (SOCKADDR *)&addrClient, &len);
sendto(sockSrv, sendBuf, 1024, 0, (SOCKADDR *)&addrClient, len);
}
// 关闭监听套接字
closesocket(sockServer);
//清理环境
WSACleanup();
客户端也是同样的流程,只是可以不需要绑定ip地址和端口。
下面,我们在这个基本框架之上,尝试实现可靠传输。
首先我们需要为传输的数据设计一个消息头,存储一些重要信息,以便实现后续的功能。需要明确的是,我们需要以二进制形式传输数据,因此必须考虑消息头里的数据类型的位数。在此提供一种设计方案:
struct HeadMsg {
u_short len; // 数据长度,16位
u_short checkSum; // 校验和,16位
unsigned char type; // 消息类型
unsigned char seq; // 序列号,可以表示0-255
};
有了消息头之后,我们每次发送消息时都要设置好消息头,再加上要传输的数据。接下来,我们从最简单的三次握手和四次挥手开始。
在TCP中,三次握手的流程如下:
本文章的设计和TCP中的三次握手一致。当然你也可以设计二次握手,即只需一方发出请求连接就确立;也可以设计四次握手,需要双方分别发送建立连接的请求。
主动握手的代码如下:
HeadMsg h1;
h1.type = SYN;
char *sendBuf = new char[1024];
memset(sendBuf, 0, sizeof(sendBuf));
memcpy(sendBuf, &h1, sizeof(h1));
if (sendto(sockClient, sendBuf, 1024, 0, (SOCKADDR*)&addrServer, sizeof(SOCKADDR)) == -1) {
return false;
}
else {
cout << "Client: [SYN] Seq=0" << endl;
}
其中的memcpy函数在本文章中会经常使用:
// 从b地址开始,把c个字节的数据写入a地址
memcpy(a, b, c);
对于发送的消息,初步只设置了消息头的消息类型,其它的功能我们后续一步步完善。
等待握手的代码如下:
char *recvBuf = new char[1024];
memset(recvBuf, 0, sizeof(recvBuf));
int addrlen = sizeof(SOCKADDR);
HeadMsg h2;
while (true) {
if (recvfrom(sockClient, recvBuf, 1024, 0, (SOCKADDR*)&addrServer, &addrlen) > 0) {
memcpy(&h2, recvBuf, sizeof(h2));
if (h2.type == SYN_ACK) {
cout << "Server: [SYN, ACK] Seq=0 Ack=1" << endl;
break;
}
else {
return false;
}
}
}
大家可以自行完善三次握手的过程。接下来我们介绍四次挥手:
在TCP中,双方都可以先发送挥手,也就是断开连接的请求。你可以设计三次挥手,即把二三次挥手结合在一起发送;也可以设计二次挥手,一方请求断开连接则暴力断开连接;也可以制定哪一方先发送挥手。
本文章中,为了方便,设计发送端先发送挥手。
四次挥手的代码和三次握手类似:
// 挥手
HeadMsg h1;
h1.type = FIN;
char *sendBuf = new char[1024];
memset(sendBuf, 0, sizeof(sendBuf));
memcpy(sendBuf, &h1, sizeof(h1));
if (sendto(sockServer, sendBuf, 1024, 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR)) == -1) {
return false;
}
else {
cout << "Server: [FIN, ACK] Seq=n" endl;
}
// 等待挥手
char *recvBuf = new char[1024];
memset(recvBuf, 0, sizeof(recvBuf));
int addrlen = sizeof(SOCKADDR);
HeadMsg h2;
while (true) {
if (recvfrom(sockServer, recvBuf, 1024, 0, (SOCKADDR*)&addrClient, &addrlen) > 0) {
memcpy(&h2, recvBuf, sizeof(h2));
if (h2.type == ACK) {
cout << "Client: [ACK] Seq=n"<< " Ack=m" << endl;
break;
}
else {
return false;
}
}
}
接下来我们介绍发送和接收消息,也就是本文章的重点部分。
我们需要以二进制方式读文件并将其保存到缓冲区,这部分和协议设计没什么关系,但是由于过去很少接触,还是介绍一下:
// 以二进制方式打开文件
ifstream fin(file.c_str(), ifstream::in | ios::binary);
if (!fin) {
cout << "Error: cannot open file!" << endl;
return false;
}
fin.seekg(0, fin.end); // 指针定位在文件尾
int length = fin.tellg(); // 获取文件大小(字节)
fin.seekg(0, fin.beg); // 指针定位在文件头
char *data = new char[length];
memset(data, 0, sizeof(data));
fin.read(data, length);
fin.close();
接下来明确一下我们发送消息的基本框架:
细心的同学可能发现,如果一直没有收到或收到错误的ACK消息,发送端会一直重新发送信息。因此,大家可以自行设计如何跳出发送消息,例如设置最大重传次数、最大等待时间等。
// 设置信息头
HeadMsg h;
h.seq = curSeq;
h.len = packLen;
h.type = PSH;
char *sendBuf = new char[h.len+sizeof(h)];
memset(sendBuf, 0, sizeof(sendBuf));
memcpy(sendBuf, &h, sizeof(h));
// data存放的是读入的二进制数据,sentLen是已发送的长度,作为分批次发送的偏移量
memcpy(sendBuf + sizeof(h), data + sentLen, h.len);
sentLen += (int)h.len;
// 计算校验和
h.checkSum = checkSumVerify((u_short *)sendBuf, sizeof(h) + h.len);
memcpy(sendBuf, &h, sizeof(h));
// 发送消息
if (sendto(sockServer, sendBuf, h.len + sizeof(h), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR)) == -1) {
cout << "Error: fail to send messages!" << endl;
return false;
}
// 等待接收消息
char *recvBuf = new char[1024];
memset(recvBuf, 0, sizeof(recvBuf));
int addrlen = sizeof(SOCKADDR);
while (true) {
if (recvfrom(sockServer, recvBuf, 1024, 0, (SOCKADDR*)&addrClient, &addrlen) > 0) {
// 收到消息需要验证消息类型、序列号和校验和
else {
// 差错重传并重新计时
}
}
else {
// 超时重传并重新计时
}
}
收到消息后,我们需要检查消息是否正确。
首先,需要检查消息类型是否正确。本文章设计的发送数据时的消息类型为PSH,大家也可以自行设计。
其次,需要检查消息的序列号是否正确。接收端每次发送ACK消息时,序列号都为其最后正确收到的消息的序列号。例如发送端发送一条序列号seq=n的消息,接收端收到后,会发送ACK消息确认,序列号seq=n;若发送端的消息损坏,接收端同样会发送ACK消息,但是这是序列号seq=n-1,也就是最后正确收到的消息是n-1号消息,以此来告诉发送端你的消息损坏了。因此,在发送端,我们只需要正确设置序列号就可以了。本文章的消息头中,序列号有8位,可以表示0-255。在发送端,我们只需要维护一个全局变量seq即可。
// 初始化8位序列号
unsigned char seq = 0;
// 每次成功发送消息后,序列号+1,但是要注意序列号空间有限
seq = (seq + 1) % 256;
// 收到消息时检查序列号
if(h.seq == seq)
最后,需要检查校验和是否正确。校验和是消息头中的冗余字段,用来检测数据报传输过程中出现的差错。校验和的计算方法是:
接收端接收到数据时,需要用同样的方法计算校验和,但是不需要先将校验和清零。如果校验和结果全为0,说明消息正确,否则,说明消息损坏。
实现校验和的具体代码如下:
// 校验和:每16位相加后取反,接收端校验时若结果为全0则为正确消息
u_short checkSumVerify(u_short* msg, int length) {
int count = (length + 1) / 2;
u_short* buf = (u_short*)malloc(length + 1);
memset(buf, 0, length + 1);
memcpy(buf, msg, length);
u_long checkSum = 0;
while (count--) {
checkSum += *buf++;
if (checkSum & 0xffff0000) {
checkSum &= 0xffff;
checkSum++;
}
}
return ~(checkSum & 0xffff);
}
确认重传包括差错重传和超时重传。
差错重传就是在刚刚的差错检测部分,如果发现收到的ACK消息有错,则重新发送数据报,代码和上面一样,不再赘述。
超时重传就是如果超出最大响应时间还没有收到ACK消息,则重新发送数据报。
// 开始计时
clock_t start = clock();
// 发送消息......
// 如果超时
if (clock() - start > maxTime) {
// 重新发送数据报......
// 重新计时
clock_t start = clock();
}
注意,差错重传和超时重传都要重新计时。
虽然本文章实现的是单向传输,有一个发送端和一个接收端,但是其实双方都需要发送和接收消息,有很多代码是类似的。
char *recvBuf = new char[maxSize + sizeof(h1)];
memset(recvBuf, 0, sizeof(recvBuf));
// 等待接收消息
while (true) {
// 收到消息需要验证校验和及序列号
if (recvfrom(sockClient, recvBuf, maxSize+sizeof(h1), 0, (SOCKADDR*)&addrServer, &addrlen) > 0) {
memcpy(&h1, recvBuf, sizeof(h1));
HeadMsg h2;
h2.type = ACK;
char *sendBuf = new char[1024];
memset(sendBuf, 0, sizeof(sendBuf));
if (h1.seq == (lastAck+1)%256 && !checkSumVerify((u_short*)recvBuf, len)) {
lastAck = (lastAck + 1) % 256;
h2.seq = lastAck;
h2.checkSum = checkSumVerify((u_short*)&h2, sizeof(h2));
memcpy(sendBuf, &h2, sizeof(h2));
sendto(sockClient, sendBuf, 1024, 0, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));
memcpy(data + totalLen, recvBuf + sizeof(h1), h1.len);
totalLen += (int)h1.len;
}
else { // 差错重传
h2.seq = lastAck;
h2.checkSum = checkSumVerify((u_short*)&h2, sizeof(h2));
memcpy(sendBuf, &h2, sizeof(h2));
sendto(sockClient, sendBuf, 1024, 0, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));
}
}
}
其中,需要特别注意lastAck的设置,一定是最后接收到的正确消息的序列号。
在上述代码中,我们将接收到的数据存入data缓冲区,总长度为totalLen。所有的数据接收完毕后,我们需要将其写入指定位置。
// 以二进制方式写入文件
ofstream fout(file.c_str(), ofstream::out | ios::binary);
if (!fout) {
cout << "Error: cannot open file!" << endl;
return false;
}
fout.write(data, totalLen);
fout.close();
其实三次握手和四次挥手本质上也是发送和接收消息。现在,大家可以把三次握手和四次挥手的代码更新一下,加上序列号、校验和、确认重传等,使协议更加完整。
至此,我们完成了基于UDP协议的可靠传输。
运行程序,可以看到三次握手的过程,成功建立连接。发送端输出消息提示用户输入需要传输的文件或者断开连接。

我们随便选择一个数据,发送端很快开始发送文件,并输出相关信息。从左到右依次为,发送的数据长度(字节)、消息类型、序列号、校验和。最后输出发送的数据总长度、传输时间和吞吐率。

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

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

如果加上我们人为设置的丢包、损坏和延时,测试如下:

可以看到接收端在很努力地搞破坏,而我们的发送端不辞辛劳地重传。

本文章成功实现了基于UDP协议的可靠传输。但这个协议的设计还存在一些缺陷,例如,流量控制采用停等机制可能造成延时过长,没有设置拥塞控制等。后续将对这两部分进行改进,拟采用基于滑动窗口的流量控制机制,实现RENO算法的拥塞控制。
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复
在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定
一段时间以来,我一直在使用open_uri下拉ftp路径作为数据源,但突然发现我几乎连续不断地收到“530抱歉,允许的最大客户端数(95)已经连接。”我不确定我的代码是否有问题,或者是否是其他人在访问服务器,不幸的是,我无法真正确定谁有问题。本质上,我正在读取FTPURI:defself.read_uri(uri)beginuri=open(uri).readuri=="Error"?nil:urirescueOpenURI::HTTPErrornilendend我猜我需要在这里添加一些额外的错误处理代码...我想确保我采取一切预防措施来关闭所有连接,这样我的连接就不是问题所在,但是我
我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项
我查看了Stripedocumentationonerrors,但我仍然无法正确处理/重定向这些错误。基本上无论发生什么,我都希望他们返回到edit操作(通过edit_profile_path)并向他们显示一条消息(无论成功与否)。我在edit操作上有一个表单,它可以POST到update操作。使用有效的信用卡可以正常工作(费用在Stripe仪表板中)。我正在使用Stripe.js。classExtrasController5000,#amountincents:currency=>"usd",:card=>token,:description=>current_user.email)