【Qt】系统相关(七)网络编程讲解,UDP回显服务器的实现
一、网络编程讲解二、UDP的接口介绍三、UDP回显服务器的实现
·
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
Qt系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
前言
【Qt】系统相关(六)QMutex锁,QMutexLocker的使用,QWaitCondition条件变量,QSemaphore信号量的讲解——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【Qt】系统相关(七)网络编程讲解,UDP回显服务器的实现
一、网络编程讲解
- 接下来我们就要学习Qt网络编程了,那么这里小编在之前讲解过Linux网络编程,而Qt网络编程其实也就是对Linux网络编程的封装,也就是说Qt网络编程其实是和我们之前学习的Linux网络编程强相关的,所以各有读者友友,在学习Qt网络编程之前,一定一定要把Linux网络编程学习扎实再来学习 关于Linux网络编程的讲解,详情请点击<——,那么后面小编在进行讲解Qt网络编程的时候,默认大家已经掌握了Linux网络编程的相关知识,并且由于Linux网络编程内容很多,这里小编不便于贴出链接,而是直接拿Linux网络编程的知识进行讲解Qt网络编程了
- 对于网络编程其实是操作系统提供的一组网络编程API,也就是Socket API,对于Linux中也就是Socket编程,Qt网络编程也就是对操作系统提供的网络编程API进行的封装,和之前不同,C++标准库中并没有提供对于系统的网络编程API的封装
- 也就是说C++标准库并没有提供网络库,而在日常编码的过程中,如果使用系统的网络编程在接口调用上进行传参十分的不便,并且在日常开发中,对于网络编程的使用场景有很多,而你C++标准库却又不提供网络库,所以这就很让人恼火
- 更恼火的是在C++新标准中放着网络库不进行提供,反而去提供一些无关紧要,奇奇怪怪的东西,关于这背后的原因,我们也不得而知,当然,小编只是站在现在C++目前最新的C++23标准来看,至于是否C++26,C++29会出网络库,我们同样也不得而知
- 那么我们在进行网络编程的时候,本质是在编写应用层代码,需要传输层提供支持,传输层中最为核心的协议就是UDP和TCP,并且这两个协议的差别还比较大,所以Qt就提供了两套API,而要想使用Qt网络编程的API,就需要现在.pro文件中添加network模块,同样的,之前我们学习过的Qt各种控件,各种内容例如信号槽,窗口,事件等,都是包含在QtCore模块中的,而我们没有手动包含QtCore模块就可以使用Qt各种控件,各种内容的原因是Qt已经帮我们自动在.pro文件中包含了QtCore模块,那么为什么Qt要划分出这些模块呢?
- 因为Qt本身是一个非常庞大,包罗万象的框架,如果把所有的Qt的功能都放在一起,即使我们只写一个简单的hello world,此时生成的可执行程序也会很庞大,而在这个可执行程序中就包含了大量没有使用的功能,所以此时就会有问题了,例如在嵌入式系统上
- 对于嵌入式系统来讲,本身内存,硬盘,CPU资源就紧张,此时再来一个很庞大的可执行程序,那么硬盘中都可能放不下这个很庞大的可执行程序,所以Qt引入了模块化处理,可以使得Qt生成的可执行程序较为轻量化
- 所谓的模块化处理,也就是将不同的功能封装成不同的模块,默认.pro文件中只包含和Qt各种控件,各种内容相关的QtCore模块,默认情况下对于其它额外的模块不会参与编译,此时只包含QtCore模块的代码生成的Qt的可执行程序就比较轻量化,后面如果需要使用其它模块的功能了,那么就在.pro文件中,包含引入对应的模块,把对应模块的功能给编辑加载进来即可,此时代码中就包含几个模块,而Qt是一个非常庞大,包罗万象的框架,一共被划分了大约60个模块,所以仅仅包含几个模块生成的代码被编译形成的可执行程序还是相较于比较轻量化的
- 同样的Qt还提供了静态库的版本和动态库的版本
(1)对于静态库的版本,那么引入的模块都会被编入到最终的可执行程序中
(2)对于动态库的版本,其实是将每一个模块都编成了一个动态库,此时如果我们如果要使用动态库的版本就需要将动态库加载到共享区,在我们引入好我们需要使用的对应的模块之后,引入的模块并不会被编入到对应的可执行程序中,而是根据动态库的特性,在程序运行时,如果需要使用到动态库的代码,那么拿着虚拟地址加上共享库的起始地址就可以找到共享库对应的方法或者变量进行使用
(3)关于动态库和静态库的讲解,如下
……(1)【linux】linux基础IO(七)静态库的制作与使用
……(2)【linux】linux基础IO(八)动态库的制作与使用
……(3)【linux】linux基础IO(九)动态库是如何被加载的
二、UDP的接口介绍
- 在Qt中给UDP协议的使用封装了一套API,Qt对于UDP也封装成了类,分别是QUdpSocket和QNetworkDatagram
(1)QUdpSocket表示一个UDP的socket文件,这样说可能有点抽象,那么小编通俗一点QUdpSocket和UDP的创建,绑定,发送,读取有关
(2)QNetworkDatagram表示一个UDP的数据报,别忘了UDP是面向数据报的,所以Qt中使用QNetworkDatagram类表示UDP的数据报 - 下面我们来认识一下QUdpSocket,如下是相关的接口
(1)bind(const QHostAddress&, quint16),用于给服务器绑定IP地址,端口号
(2)receiveDatagram(),用于读取一个UDP数据报,自然的返回值就是QNetworkDatagram
(3)writeDatagram(const QNetworkDatagram&),用于发送一个UDP数据报
(4)readyRead,这个readyRead是一个信号
……(1)当socket收到请求的时候,QUdpSocket就会触发这个信号,此时就可以在这个readyRead信号绑定的对应的槽函数中完成读取请求的操作了
……(2)那么这里对比于在Linux中默认采用的阻塞式读取数据来讲,一旦Linux中调用了read,recvfrom这样的读取接口,一旦对端客户端没有给我发送请求,那么此时服务器就只能阻塞等待,一旦阻塞等待,就带来了消耗,与其阻塞等待,倒不如去做一下其它的事情,而在Qt中,没有采取这种阻塞式读取的方式,而是巧妙的借助信号槽,就天然的达成了事件驱动这样的一种网络编程的方式 - 下面我们来认识一下QNetworkDatagram,如下是相关接口
(1)QNetworkDatagram(const QByteArray&, const QHostArray&, quint16),构造函数,通过传参QByteArray,目标IP地址,目标端口号port来构造一个UDP数据报,并且UDP数据报的构建通常用于发送数据的时候
(2)data,用于获取UDP数据报内部的有效载荷,也就是用户实际要发送的数据,以QByteArray的形式返回
(3)senderAddress,用于获取数据报中对端的IP地址
(4)senderPort,用于获取数据报中对端的端口号port
三、UDP回显服务器的实现
- 下面我们就来编写一个带有界面的UDP回显服务器,其实这里我们要清楚,一个正经的服务器很少会有图形化界面,因为图形化界面会给服务器的性能带来消耗,造成服务器服务的客户端的数量减少,而服务器的作用就是给客户端进行服务器,所以服务器一般都是命令行的,这里我们为了便于演示现象,所以给服务器带有图形化界面
- 同样的我们也要认识到,虽然Qt用于编写图形化界面程序,但是Qt同样也可以编写控制台程序的,也就是可以编写命令行的服务器,所以接下来我们创建一个项目名为UdpServer,基类为QWidget,派生类为Widget的项目,接下来我们点击ui文件,进入Qt Designer

- 由于我们要实现的是UDP回显服务器,也就是将来自客户端的信息回显到界面上,而消息是有多条的,那么我们可以将每一条消息作为一个item放到QListWidget列表中作为一个元素,所以多条消息就在QListWidget列表上添加多个元素item,以列表形式显示消息正好 关于QListWidget的讲解,详情请点击<——,所以此时我们拖拽左侧红框内的QListWidget列表控件,然后调整成上图界面即可,objectName保持不变

- 由于我们要创建UDP服务器,自然的就要使用QUdpSocket创建服务器,那么我们先包含一下QUdpSocket对应的头文件#include <QUdpSocket>,可是此时小编一进行包含,就提示找不到#include <QUdpSocket>头文件,这是为什么呢?之前包含控件什么的头文件都没有问题

- 其实这是由于我们之前讲解的Qt模块化的设计,包含控件的头文件没有问题,是因为Qt默认在.pro文件中已经引入了和控件相关的模块QtCort了,而这里由于我们要创建UDP服务器,也就意味着要使用Qt网络模块的功能,所以此时就要给.pro文件中引入网络模块相关的network,那么我们在.pro文件的第一行在core gui后面使用空格间隔,然后添加network即可

- 可是小编,此时尽管我们在.pro文件中引入了网络模块相关的network,可是为什么#include <QUdpSocket>头文件还是提示找不到呢?其实这是由于.pro文件还没有被解析,也就意味着此时网络模块的相关功能还没有真正引入,所以我们该如何让Qt解析.pro文件引入网络模块的相关功能呢?

- 此时我们左下角直接运行项目,此时Qt就会自动解析.pro文件引入网络模块的相关功能,如上,此时#include <QUdpSocket>头文件就找到了,也就意味这此时我们可以正常使用Qt网络相关的功能了
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
#endif // WIDGET_H
- 那么在Widget的.h头文件中,我们声明一个UDP的服务器,所以这里我们声明QUdpSocket*指针类型的私有成员变量socket,同样这也是进行网络编程的常用写法
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QUdpSocket(this);
this->setWindowTitle("服务器");
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
bool ret = socket->bind(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
}

- 在Widget的.cpp源文件中,在Widget的构造函数中,我们就可以给socket创建实例,也就是new一个QUdpSocket的对象给socket,此时我们来看对于QUdpSocket的构造函数中出现了我们熟悉的parent,也就意味着我们可以传入this指针,指定UDP对应socket的父元素为this指针对应的Widget窗口,将socket对象挂接到对象树上,那么在Widget窗口被关闭销毁的时候,就会自动调用delete去释放socket,对象树机制也可以完美的在Qt网络编程中进行使用,所以这里对于QUdpSocket的构造函数我们传入this指针,那么是否我们可以什么都不传呢?
- 可以,也就意味着此时我们不使用Qt的对象树机制,此时对于socket的生命周期就需要我们手动进行管理,那么就要在Widget的析构函数中手动delete这个socket进行析构,其实这里比较推荐的方法还是使用对象树机制,对于QUdpSocket的构造函数我们传入this指针,让Qt的对象树帮我们管理socket的生命周期
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
void processRequest();
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
#endif // WIDGET_H
- 接下来我们就要先连接信号槽了,当socket收到请求报文之后,会触发readyRead信号,那么我们就可以使用connect给这个readyRead信号绑定对应的槽函数processRequest,所以我们在Widget的.h文件中声明一下槽函数processRequest,然后在Widget的.cpp源文件中定义一下对应的槽函数processRequest即可,对于这个槽函数processRequest也就是用于读取报文数据,进行业务逻辑的处理,再将相应报文发送给客户端,那么此时问题来了,有没有可能此时没有请求报文,进而造成这里直接进行读取请求报文阻塞呢?
- 不会,一定不会,此时通过信号槽机制,进行读取的请求报文一定不会被阻塞,因为Qt的机制可以保证当socket收到请求报文之后,会触发readyRead信号,所以此时在对应的槽函数processRequest中直接进行读取报文数据就可以将报文数据读取上来,这也就完美的体现了Qt中基于事件驱动这样的一种网络编程方式
- 所以此时我们使用connect将socket的readyRead信号绑定到对应Widget中的槽函数processRequest上即可,此时如果触发readyRead信号,就会执行绑定的槽函数processRequest,接下来我们使用bind绑定IP地址和端口号port即可
- 那么对于绑定的IP地址,这里我们绑定QHostAddress提供的QHostAddress::And,其实这个QHostAddress::And本质上就是0.0.0.0,我们当前是一个服务器,给服务器绑定了0.0.0.0这个IP地址,由于一个主机上可能有多个网卡,一个网卡对应一个IP地址,所以一个主机上可能有多个IP地址,而每一个IP地址都有可能接收到来自客户端的请求报文,如果服务器仅仅绑定了某一个具体的IP,那么此时服务器仅仅只能收到这一个IP上来自客户端的请求报文,而主机上有多个IP地址呀,那么其它的IP地址上来自客户端的请求报文就接收不到了,所以我服务器想要接收到该如何做呢?
- 给服务器绑定了0.0.0.0这个IP地址即可,此时操作系统可以保证对于这台主机上的所有IP地址收到的来自客户端的请求报文都可以被服务器接收到,那么服务器发送报文的时候,会使用主机上的哪一个IP地址呢?我们不需要关心
- 如果当前服务器的主机上只有一个IP地址,那么系统就会采用这个IP地址作为源IP发送报文,如果当前服务器主机上有多个IP地址,那么由系统自动选择其中的一个IP地址作为源IP发送报文,也就意味着我们不需要关心服务器发送报文的时候会采用主机上的哪个IP地址,系统会帮我们自动选择其中的一个IP地址作为源IP发送报文
- 所以此时那么对于bind绑定的IP地址,这里我们绑定QHostAddress提供的QHostAddress::And,其实这个QHostAddress::And本质上就是0.0.0.0,然后对于bind绑定的端口号port我们随便填写一个9090即可,注意这里随便填写的端口号port的范围要在[1024, 65535]之间,因为端口号是16位比特位,也就是说最大表示的范围是65535,端口号可以从0开始,而系统占用的端口号port是[0, 1023],所以我们填写的端口号port的范围要在[1024, 65535]之间
- 那么观察仔细的读者友友可能会观察,小编是先connect连接了信号槽,再进行了bind绑定了IP地址和端口号port,这是为什么呢,能不能先bind绑定IP地址和端口号port,然后再connect连接信号槽呢?
- 不能,其实一旦bind绑定了IP地址和端口号,也就意味着客户端的请求报文可以被服务器接收到了,而connect连接信号槽是为了当服务器收到客户端的请求的时候可以触发readyRead信号,进而去执行关联的槽函数处理需求的
- 所以一旦先bind绑定IP地址和端口号port,然后再connect连接信号槽,那么在先bind绑定IP地址和端口号port之后,然后在connect连接信号槽之前,这个窗口期,有客户端把请求报文发送过来了,此时就有可能由于信号槽没有完成连接,所以对于服务器对于收到客户端的请求的时候可以触发readyRead信号不会进行处理,进而此时来自客户端的请求报文就无法进行读取了,自然也就无法进行业务逻辑的处理,也就无法给客户端响应了
- 所以先bind绑定IP地址和端口号port,然后再connect连接信号槽本身就是不合理的,对于客户端来讲,你服务器bind绑定IP地址和端口号port,也就意味着你服务器要给我客户端提供服务了,可是我客户端把请求报文发送给你了,你服务器却无法处理我的请求,无法给我响应,所以这本身就是不科学的
- 而先connect连接信号槽,然后再bind绑定IP地址和端口号port是合理的,此时先connect连接信号槽,也就意味着此时服务器先准备好当服务器收到客户端的请求的时候可以触发readyRead信号,进而去执行关联的槽函数处理需求的,然后给客户端发送响应报文,此时由于服务器没有bind绑定,也就意味着此时服务器还没有提供服务,所以自然的客户端界面上发送请求报文之后会显示服务器还没有提供服务
- 接下来我们bind绑定IP地址和端口号port代表着服务器此时可以提供服务了,那么客户端发送请求报文,就可以被服务器收到,此时服务器是socket触发readyRead信号,进而去执行关联的槽函数将客户端的请求报文读取上来,进行业务逻辑的处理,构建响应报文并发送给客户端,没有问题
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QUdpSocket(this);
this->setWindowTitle("服务器");
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
bool ret = socket->bind(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
}
- 而对于bind给服务器绑定IP地址和端口号port来讲,我们知道一个端口号port只能被一个socket绑定,所以此时万一我们随机填写的9090被主机上的其它服务绑定了呢?所以此时服务器绑定端口号port就会失败,此时也就意味着服务器启动出错了,服务器也就无法给客户端提供服务了,所以我们可以看出对于服务器绑定端口号port失败是一个很严重的问题
- 所以我们期望对bind绑定是否失败的结果进行判定,而恰好bind的返回值就是bool类型的变量,所以此时我们使用bool类型的变量ret接收bind的返回值,如果bind的返回值为false,代表bind绑定失败,那么就代表此时端口号port被占用或者其它的因素,那么此时我们就使用QMessageBox::critical弹出一个严重问题的消息对话框
- 接下来传参父元素为Widget窗口对应的this指针,挂接到对象树上,然后继续传参严重问题的消息对话框的标题为服务器启动出错,接下来传参严重问题的消息对话框中显示的文本,此时我们知道bind绑定失败的原因吗?好像不太清楚,对于此时bind绑定失败的原因有可能是端口号port被占用或者IP地址错误或者权限不足或者端口号非法等原因
- 所以对于bind绑定失败的原因,我们确实不太清楚,那么谁清楚呢?系统清楚,系统调用bind失败时候,会设置错误码errno,而在C语言中使用perror就可以将errno对应的错误信息以字符串的形式进行打印,恰好Qt中对于errno机制同样封装成了errorString,这里的Qt中的errnoString的作用就和C语言中的perror类似,所以我们这里就调用errnoString将errno对应的错误信息转换成字符串的形式进行传参
- 那么在弹出严重问题的消息对话框之后,由于此时服务器绑定端口失败,也就意味着此时服务器启动失败了,即此时服务器无法为客户端提供服务了,所以后面的逻辑我们也不用执行了,直接return即可,如果bind的返回值为true走到下面,代表此时服务器bind绑定IP地址和端口号port成功,所以我们就继续执行后面的逻辑即可
- 当然在我们的代码中,Widget的构造函数中好像没有后续的逻辑了,实则不然,后续就要到main.cpp中的main函数中了,show显示服务器的界面,然后exec执行后续的逻辑,服务器基于信号槽,进行事件驱动式的,收到来自客户端的请求报文,触发readyRead信号,执行对应的槽函数进而处理来自客户端的请求,然后进行业务逻辑,构建响应报文并且发送给客户端
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QNetworkDatagram>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QUdpSocket(this);
this->setWindowTitle("服务器");
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
bool ret = socket->bind(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
// 读取来自客户端的请求报文,并且解析请求报文
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
QString request = requestDatagram.data();
// 进行业务逻辑处理,并且构建响应报文
QString response = this->process(request);
QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
// 将响应报文发送给客户端
socket->writeDatagram(responseDatagram);
// 把交互的信息回显到界面上
QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort())
+ "] req: " + request + ", resp: " + response;
ui->listWidget->addItem(log);
}
QString Widget::process(const QString& request)
{
return request;
}
- 接下来我们就该进行socket的readyRead信号对应的槽函数processRequest的编写了了,而这个槽函数是要处理来自客户端的请求的,对于服务器处理客户端的请求,一般都是按照如下三个步骤来进行处理,下面小编逐一进行讲解
(1)读取来自客户端的请求报文,并且解析请求报文
(2)进行业务逻辑处理,并且构建响应报文
(3)将响应报文发送给客户端 - 对于第一步就是读取来自客户端的请求报文,并且解析请求报文,所以此时我们先使用receiveDatagram读取客户端的请求报文,而receiveDatagram的返回值就是客户端的请求报文,所以我们使用QNetworkDatagram类型的变量requestDatagram接收receiveDatagram的返回值,即接收客户端的请求报文,接下来我们就要解析请求报文了,如何解析呢?

- 其实就是获取请求报文中的有效载荷的内容,那么我们可以调用data进行获取,如上我们可以看出data的返回值是一个QByteArray类型的变量,这里其实我们可以使用QString类型的变量request进行接收,因为在前文中,小编讲解过QString类型的构造函数可以支持使用QByteArray类型的对象进行构造,所以这里此时我们使用QString类型的变量接收data为QByteArray类型的变量即可,所以此时我们就拿到了用户发来的请求报文中的有效载荷,这个有效载荷也就是要交给应用层来进行处理的,也就是要进行如下的第二步
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
void processRequest();
QString process(const QString& request);
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
#endif // WIDGET_H
- 对于第二步,也就是要进行业务逻辑处理,并且构建响应报文,对于第二步也就是整个服务器的核心的业务逻辑,一般我们都封装成一个函数,所以这里在Widget的.h头文件中,我们声明一个私有成员函数process,参数为const QString&的request,返回值为QString类型,对于这里的返回值就是要返回响应
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QNetworkDatagram>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QUdpSocket(this);
this->setWindowTitle("服务器");
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
bool ret = socket->bind(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
// 读取来自客户端的请求报文,并且解析请求报文
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
QString request = requestDatagram.data();
// 进行业务逻辑处理,并且构建响应报文
QString response = this->process(request);
QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
// 将响应报文发送给客户端
socket->writeDatagram(responseDatagram);
// 把交互的信息回显到界面上
QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort())
+ "] req: " + request + ", resp: " + response;
ui->listWidget->addItem(log);
}
QString Widget::process(const QString& request)
{
return request;
}
- 那么此时我们就可以在Widget的.cpp源文件中,定义这个业务处理函数process,在process中,由于当前我们实现的是UDP回显服务器,所以请求和响应一样,这里我们直接将请求作为响应直接返回即可,虽然我们这里的业务处理函数process编写的十分简单,但是我们要认识到,对于一个成熟的商业服务器来讲,这里的请求到响应的业务逻辑,可能是非常复杂的

- 所以此时使用QString类型的response接收业务处理函数process返回的响应即可,接下来我们就可以开始构建响应报文了,此时使用QNetworkDatagram数据报类型创建responseDatagram作为响应报文,那么我们对于第一个参数需要传入有效载荷的内容,这里我们来看,如上第一个参数是QByteArray类型,可是我们实际给客户端发送的响应内容是QString类型的,此时我们可以使用toUtf8将QString类型取出内部的字节数组转换为QByteArray类型进行传参
- 接下来对于第二个参数我们需要传入客户端的IP地址,我们手头有吗?有的,就在当初接收的客户端的请求报文中包含,使用senderAddress即可取出请求报文中客户端的IP地址传参给第二个参数,同理使用senderPort即可取出请求报文中客户端的端口号port传参给第三个参数
- 那么对于第三步,将响应报文发送给客户端,此时我们调用writeDatagram将我们构建好的响应报文传参发送给客户端即可
- 到了第三步其实对于一般的服务器流程就已经结束了,但是我们的流程还没有结束,因为我们实现的服务器是UDP回显服务器,还要把交互的信息回显到界面上,所以此时我们就构建QString类型的log,进行字符串拼接,将客户端的IP地址),端口号,请求对应的字符串内容,响应对应的字符串内容都进行拼接构建log即可


- 这里我们需要关注我们使用senderAddress取出的IP地址是QHostAddress类型的,而这里进行字符串拼接需要的类型是QString类型的,所以我们需要将QHostAddress类型转换为QString类型的,如何转化呢?我们可以使用QHostAddress类提供的方法toString将QHostAddress类型转换为QString类型的进行字符串的拼接

- 同样的,使用senderPort取出的端口号Port是int类型的,而这里进行字符串拼接需要的类型是QString类型的,所以我们需要将int类型转换为QString类型的,如何转换?小编在前文讲解过QString中提供了静态函数QString::number可以将int类型转换为QString类型进行字符串的拼接
- 此时要添加到QListWidget列表上的元素log已经准备好了,所以我们调用addItem将和客户端交互的元素log添加到界面上的列表中进行回显即可,此时我们的UDP回显服务器就实现完成了,此时我们还没有办法进行测试,需要等下一篇小编实现UDP客户端之后再来进行统一的测试
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)