> 技术文档 > QT跨平台应用程序开发框架(11)—— Qt网络编程

QT跨平台应用程序开发框架(11)—— Qt网络编程


目录

一,概述

二,UDP

2.1 API 介绍

2.2 回显服务器

2.3 回显客户端

2.4 演示

三,TCP

3.1 API 介绍

3.2 回显服务器

3.3 回显客户端

3.4 演示 

四,HTTP

4.1 API 介绍

4.2 http客户端


计算机网络可以参考:https://blog.csdn.net/aaqq800520/category_12705783.html

一,概述

支持网络的操作系统也都会一共一组 API(socket API)来供用户使用,但是 C++ 至今还没有提供一套封装了网络编程的 API,被业界很多人吐槽,所以后面我们将使用 Qt 自己封装的网络 API

  • 我们编写的网络程序,需要传输层支持,所以Qt 也就提供了两套 API,分别针对 UDP 和 TCP
  • 使用 Qt 的网络 API,需要先在 .pro 文件添加 network 模块,我们前面介绍的各种控件,都是包含在 QtCore 模块中的,因为这个模块默认添加

关于模块化,下面来简单概括一下:

  • Qt 是一个体量非常大的框架,如果直接把其所有的功能放到一起,那么搞一个程序就会包含大量没有用到的东西,造成浪费
  • 所以Qt 就把这些功能分成一个一个的模块,要用哪个就添加哪个即可,能极大节约成本 

下面直接开始实操 

二,UDP

2.1 API 介绍

主要涉及的类是两个,QUdpSocketQNetworkDatagram

QUdpSocket 表示一个 UDP 的 socket 文件,核心方法如下:

API 类型 说明 对标原生 API bind(const QHostAddress&, quint16) 方法 绑定指定的端口号 bind receiveDatagram() 方法 返回 QNetworkDatagram,是一个 UDP 数据包对象 recvfrom writeDatagram(const QNetworkDatagram&) 方法

发送一个数据包

sendto readyRead 信号

收到数据并准备就绪后触发该信号

  • 我们在 Linux 上是直接通过阻塞式地等待客户端发小
  • Qt 的这个类似 IO 的多路复用机制

QNetworkDatagram 表示一个 UDP 数据报,核心方法如下:

名称 类型 说明 对标原生 API QNetworkDatagram(const QByteArray&, const  QHostAddress&, quint16) 构造函数 通过 QByteArray,添加目标IP、目标端口,构造一个 UDP 数据报,通常用于发送数据 无 data() 方法 获取数据报内部持有的数据,返回 QByteArray 无 senderAddress() 方法 获取数据包中包含的对端的 IP 地址 无,recvfrom 包含了该功能 senderPort() 方法 获取数据报中包含的对端的端口号 无,recvfrom 包含了该功能

2.2 回显服务器

关于回显服务器:

回显服务器是指用于测试和验证网络连接的服务器。它的主要功能是返回客户端发送的数据,以便客户端确认网络连接是否正常。回显服务器一般是一个简单的程序,它接收来自客户端的数据,并将该数据原样返回给客户端。回显服务器的工作原理如下:

  • 当客户端向回显服务器发送数据时,服务器将接收到的数据存储起来,然后将它原样返回给客户端
  • 客户端收到服务器返回的数据后,可以比对原始数据和返回数据是否一致,从而验证网络连接的准确性和稳定性

回显服务器常用于网络测试和诊断,可以帮助用户判断网络是否通畅。例如:

  • 在网络故障排查时,可以使用回显服务器来测试网络连接是否正常
  • 此外,回显服务器还可以用于测试服务器的性能和负载能力,帮助管理员监控网络性能

在实际应用中,回显服务器有多种实现方式:

  • 一种常见的方式是使用简单的套接字编程来实现回显服务器,通过编写程序来进行数据的接收和返回
  • 另一种方式是使用专门的回显服务器软件,这些软件提供了更多的功能和配置选项,能够更好地满足用户的需求

总之,回显服务器是一种用于测试和验证网络连接的服务器,它通过返回客户端发送的数据来确认网络连接的正常性,常用于网络测试、诊断以及性能监控。

来源:回显服务器是什么 • Worktile社区

我们先新建一个基于 QWidget 的项目,名称改为 UdpServer,并创建一个 List Widget用来显示消息:

然后就是添加 Udp 头文件,但是前面说过,需要在 .pro 文件添加 network 才行,如下:

然后就是在 widget.h 文件里添加定义了:

widget.cpp 内容如下:

#include \"widget.h\"#include \"ui_widget.h\"#include#includeWidget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget){ ui->setupUi(this); socket = new QUdpSocket(this); //创建实例,并且也可以挂对象树上,所以用 this 构造 this->setWindowTitle(\"服务器\"); //设置窗口标题 connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest); //我们需要先连接信号槽再绑定端口号,这就好比要开店,得先装修好,如果没装修好就开业,那就完了 bool ret = socket->bind(QHostAddress::Any, 8080); //端口有效范围是1 ~ 65535,是十六位二进制最小和最大值 if(!ret) { QMessageBox::critical(this, \"服务器启动出错\", socket->errorString()); return; } }Widget::~Widget(){ delete ui;}//该函数负责服务器核心逻辑,包括://1,读取请求并解析//2,根据请求计算响应//3,将响应发回客户端//其实就是那一套,基本没变,应该说绝大部分服务器都是这样的void Widget::processRequest() { //1,读取解析请求 const QNetworkDatagram& requestDatagram = socket->receiveDatagram(); QString request = requestDatagram.data(); //返回一个 QByteArray,可以用来赋值和构造 QString(优势) //2,根据请求计算响应(由于是回显服务器,所以不需要计算响应,就是请求本身) const QString& response = process(request); //3,把响应写回客户端 QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort()); //上面有三个参数,第一个表示取出 QS听 内部的字节数组;第二个表示目的IP,第三个表示目的端口 socket->writeDatagram(responseDatagram); //和文件描述符一样,所以是 write 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;}

2.3 回显客户端

我们另外搞一个项目,和上面一样,命名为 UdpClient,然后创建下列控件:

UdpClient 项目的 widget.cpp 内容如下:

#include \"widget.h\"#include \"ui_widget.h\"#include //地址和端口const QString& SERVER_IP = \"127.0.0.1\";const quint16 SERVER_PORT = 8080; //quint16 就是一个 unsigned short,上一个2字节的无符号整数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::processResponse);}Widget::~Widget(){ delete ui;}void Widget::on_pushButton_clicked(){ //1,获取到输入框的内容 const QString& text = ui->lineEdit->text(); //2,构造 UDP 的请求数据 QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT); //将QString类型的 ip 转为合适的类型 //3,发送请求数据 socket->writeDatagram(requestDatagram); //4,把发送的请求也添加到列表框中 ui->listWidget->addItem(\"客户端说:\" + text); //5,把输入框的内容也清空一下 ui->lineEdit->setText(\"\");}void Widget::processResponse() //这个函数来处理收到的响应{ //1,读取到响应数据 const QNetworkDatagram& responseDatagram = socket->receiveDatagram(); QString response = responseDatagram.data(); //能用引用尽量用引用,但是涉及到不同类型转换时,还是要用值 //2,把响应数据显示到界面上 ui->listWidget->addItem(\"服务器说:\" + response);}

2.4 演示

三,TCP

3.1 API 介绍

  • UDP:无连接,不可靠传输,面向数据包,全双工
  • TCP:有连接,可靠传输,面向字节流,全双工

所以,TCP的代码比UDP多出很多

主要涉及两个类,QTcpServerQTcpSocket

QTcpServer 用于监听端口,和获取客户端连接,核心方法如下:

名称 类型 说明 对标原生 API listen(const  QHostAddress&, quint16 port) 方法 绑定指定的地址和端口号,并开始监听 bind 和 listen nextPendingConnection() 方法
  • 从系统中获取到一个已经建立好的 tcp 连接
  • 返回一个 QTcpSocket对象,表示这个客户端的连接
  • 通过这个socket对象完成和客户端之间的通信
accept newConnection 信号 无,但是类似 IO 多路复用的通知机制

QTcpSocket 用于客户端和服务器之间的数据交互,提供的核心方法如下:

名称 类型 说明 对标原生 API readAll() 方法 读取当前接收缓冲区的所有数据,返回一个 QByteArray 对象 read write(const QByteArray&) 方法 把数据写入 socket 中 write deleteLater 方法

暂时把socket对象标记为无效

Qt 会在下个事件循环中析构释放该对 象

无,但是类似于半自动化的垃圾回收 readyRead 信号 有数据到达并准备就绪时触发 无,但是类似 IO 多路复用的通知机制 disconnected 信号 连接断开时触发 无,但是类似 IO 多路复用的通知机制

3.2 回显服务器

和 UDP 一样,兴建一个命名为 TcpServer 的项目,创建一个 ListWidget 控件用来显示 log

先在 por 文件里加上netowrk,然后在 .h 文件里声明函数等:

下面是 widget.cpp 的内容:

#include \"widget.h\"#include \"ui_widget.h\"#include #include Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget){ ui->setupUi(this); //1,修改窗口标题. this->setWindowTitle(\"服务器\"); //2,创建 QTcpServer 的实例 tcpServer = new QTcpServer(this); //3,指定如何处理连接. connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection); //4,绑定并监听端口号,需要把前面初始化啊全完成才能开始监听端口,要开店得先装修好 bool ret = tcpServer->listen(QHostAddress::Any, 8080); //表示愿意接收任何 ip,端口为8080 if (!ret) { QMessageBox::critical(this, \"服务器启动失败!\", tcpServer->errorString()); //弹出错误对话框 exit(1); }}Widget::~Widget(){ delete ui;}void Widget::processConnection(){ //1,通过 tcpServer 拿到一个 socket 对象, 用来和客户端进行通信. QTcpSocket* clientSocket = tcpServer->nextPendingConnection(); //每个客户端都有一个对象,所以可能有多个,所以当断开连接时必须要释放 QString log = \"[\" + clientSocket->peerAddress().toString() + \":\" + QString::number(clientSocket->peerPort()) + \"] 客户端成功连接\"; ui->listWidget->addItem(log); //2,处理客户端发来的请求 connect(clientSocket, &QTcpSocket::readyRead, this, [=]() //使用 lamdba 表达式 { QString request = clientSocket->readAll(); //读取请求内容,返回QByteArray,转成 QString const QString& response = process(request); //处理请求,构建响应,返回要发回的内容 clientSocket->write(response.toUtf8()); //将响应发回客户端 QString log = \"[\" + clientSocket->peerAddress().toString() + \":\" + QString::number(clientSocket->peerPort()) + \"] \" + \" req: \" + request + \", resp: \" + response; ui->listWidget->addItem(log); //打印日志 //上面的处理办法比较简陋,因为一个完整的请求可能是分成多端字节组进行传输,简单来说就是可能每一次收到的内容都不完整(粘包问题) //但是作为回显服务器已经足够,更好的做法是将每次收到的数据包都放到一个大的缓冲区中,并提前约定好应用层协议的格式,再进行更细致的解析 //该步骤在主页的 http服务器 项目里已经实现过,这里就从简了 }); //3,处理断开连接的情况. connect(clientSocket, &QTcpSocket::disconnected, this, [=]() { QString log = \"[\" + clientSocket->peerAddress().toString() + \":\" + QString::number(clientSocket->peerPort()) + \"] 客户端断开连接\"; ui->listWidget->addItem(log); //打印断开连接日志 //手动释放 clientSocket // delete clientSocket; //直接使用 delete 不好,有很多限制,比如要保证 delete 是最后一步,并且不能被 return 或抛异常等操作跳过, clientSocket->deleteLater(); //Qt 提供的,不立马销毁,告诉 Qt 在下一轮事件循环再销毁 //槽函数都是在事件循环执行的,进入到下一轮事件循环,表示上一轮循环中要做的事已经全部做完了,也表示槽函数已经结束了 });}//回显服务器.QString Widget::process(const QString request){ return request;}

3.3 回显客户端

我们再新建一个 QWidget 的项目,命名为 TcpClient,并创建和 UdpClient 一样的控件

先在 pro 文件里添加network,下面是 .h 的内容:

下面是 widget.cpp 的内容:

#include \"widget.h\"#include \"ui_widget.h\"#include Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget){ ui->setupUi(this); //1,设置窗口标题 this->setWindowTitle(\"客户端\"); //2,创建 socket 对象的实例 socket = new QTcpSocket(this); //3,和服务器建立连接 (这个不是立马连接,因为有三次握手,此处只是发起连接请求,三次握手交给 tcp做的) socket->connectToHost(\"127.0.0.1\", 8080); //4,连接信号槽, 处理响应 connect(socket, &QTcpSocket::readyRead, this, [=]() { QString response = socket->readAll(); //读取响应内容 ui->listWidget->addItem(\"服务器说: \" + response); //打印内容 }); //5,等待连接建立的结果,确认是否连接成功 bool ret = socket->waitForConnected(); if (!ret) { QMessageBox::critical(this, \"连接服务器出错\", socket->errorString()); exit(1); }}Widget::~Widget(){ delete ui;}void Widget::on_pushButton_clicked(){ const QString& text = ui->lineEdit->text(); //获取输入框内容 socket->write(text.toUtf8()); //将内容发给服务器 ui->listWidget->addItem(\"客户端说: \" + text); //显示发送的内容 ui->lineEdit->setText(\"\"); //清空输入框}

3.4 演示 

当服务器未启动直接启动客户端时,会弹出窗口:

四,HTTP

4.1 API 介绍

主要涉及的类有三个:QNetworkAccessManagerQNetworkRequestQNetworkReply

QNetworkAccessManager 提供了 Http 的两个核心操作,如下:

方法 说明 get(const QNetworkRequest&) 发起一个 HTTP GET 请求,返回 QNetworkReply 对象 post(const QNetworkRequest&, const QByteArray&) 发起一个 HTTP POST 请求,返回 QNetworkReply 对象

 QNetworkRequest 这个类表示一个 Http 请求报头,不含请求正文,方法如下:

方法 说明 QNetworkRequest(const QUrl&) 通过 URL 构造 HTTP 请求 setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value) 设置请求头

QNetworkRequest::KnownHeaders 是一个描述请求正文的枚举类型,常用取值如下:

取值 说明 ContentTypeHeader 描述正文类型 ContentLengthHeader 描述正文长度 LocationHeader 用于重定向,响应报文中使用 CookieHeader 设置 cookie UserAgentHeader 设置 User-Agent

QNetworkReply 表示一个 http 响应,常用方法如下:

方法 说明 error() 获取错误状态 errorString() 获取出错原因 readAll() 读取响应文本 header(QNetworkRequest::Known) 读取响应指定的header的值

 此外,QNetworkReply 还有一个重要的信号 finished 会在客户端收到完整的响应数据后触发

4.2 http客户端

注意,Qt 只提供了 http客户端,而没有提供 http服务器的库,所以服务器直接用 www.baidu.com 或者其它网站即可

首先也是和上面的 Udp 和 Tcp 一样,新建一个项目,添加相同控件,头文件如下: 

下面是 widget.cpp 的内容

#include \"widget.h\"#include \"ui_widget.h\"#include Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget){ ui->setupUi(this); this->setWindowTitle(\"客户端\"); manager = new QNetworkAccessManager(this);}Widget::~Widget(){ delete ui;}void Widget::on_pushButton_clicked(){ //1,获取到输入框中的 url QUrl url(ui->lineEdit->text()); //2,构造一个 HTTP 请求对象 QNetworkRequest request(url); //3,发送请求 QNetworkReply* response = manager->get(request); //4,通过信号槽, 来处理响应 connect(response, &QNetworkReply::finished, this, [=]() { if (response->error() == QNetworkReply::NoError) // 正确获取响应 { QString html = response->readAll(); ui->plainTextEdit->setPlainText(html); //不用 QTextEdit,因为其会对 html 进行解析,就不知原始的 html 代码了 } else ui->plainTextEdit->setPlainText(response->errorString()); //响应错误 // 释放response response->deleteLater(); });}