当前位置: 首页 > news >正文

Qt 系统相关 - 网络

Qt 网络

针对网络编程,都是操作系统提供的一组 API(Socket API),Qt 为了支持跨平台,对网络编程的 API 也进行了重新封装。

但是对于 C++ 标准库中,并没有提供网络编程的 API 的封装!

咱们接下来的课程内容中重点介绍 Qt 的网络相关的 API 的使用。

注意:

  • 实际 Qt 开发中进行网络编程,也不一定使用 Qt 封装的网络 API,也有一定可能使用的是系统原生 API 或者其他第三方框架的 API。


在进行网络编程之前,需要在项目中的 .pro 文件中添加 network 模块。

添加之后要手动编译一下项目,使 Qt Creator 能够加载对应模块的头文件。

之前学过的 Qt 的各种控件,各种内容,都是包含在 QtCore 模块中的,这是默认就添加的!

UDP Socket

核心 API 概览

主要的类有两个:QUdpSocketQNetworkDatagram

QUdpSocket 表示一个 UDP 的 socket 文件。

QUdpSocket 核心 API

名称类型说明对标原生 API
bind方法绑定指定的端口号。bind
receiveDatagram方法返回 QNetworkDatagram,读取一个 UDP 数据报。recvfrom
writeDatagram方法发送一个 UDP 数据报。sendto
readyRead信号在收到数据并准备就绪后触发。无 (类似于 IO 多路复用的通知机制)

readyRead --- 当 socket 收到请求的时候,QUdpSocket 就会触发这个信号,此时就可以在槽函数里完成读取请求的操作了! --- 基于信号槽,就天然达成了“事件驱动”这样的一种网络编程的方式!

QNetworkDatagram 表示一个 UDP 数据报。

QNetworkDatagram 核心 API

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

这些API是Qt网络编程中常用的函数,可以帮助开发者更好地管理和控制网络通信。

回显服务器

1. 创建界面,包含一个 QListWidget 用来显示消息。

  • 创建 QUdpSocket 成员
  • 修改 widget.h
class Widget : public QWidget
{Q_OBJECT
public:Widget(QWidget *parent = nullptr);~Widget();
private:Ui::Widget *ui;QUdpSocket *socket;
};

修改 widget.cpp,完成 socket 后续的初始化

一般来说,要先连接信号槽,再绑定端口。一旦绑定窗口了,就意味着请求就可以被收到了!

如果相反,在完成绑定之后,在连接信号槽之前,有客户端将请求发送过来了,此时就可能读不到这个请求!

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1. 设置窗口标题this->setWindowTitle("服务器");// 2. 实例化 socketsocket = new QUdpSocket(this);// 3. 连接信号槽,处理收到的请求connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);// 4. 绑定端口bool ret = socket->bind(QHostAddress::Any, 9990);if (!ret) {QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString());return;}
}

实现 processRequest,完成处理请求的过程

  • 读取请求并解析

  • 根据请求计算响应

  • 把响应写回到客户端

void Widget::processRequest()
{// 1. 读取请求const QNetworkDatagram& requestDatagram = socket->receiveDatagram();QString request = requestDatagram.data();// 2. 根据请求计算响应const QString response = process(request);// 3. 把响应写回到客户端QNetworkDatagram responseDatagram(response.toUtf8(),requestDatagram.senderAddress(), requestDatagram.senderPort());socket->writeDatagram(responseDatagram);
}

实现 process 函数
由于我们此处是实现回显服务器,所以 process 方法中并没有包含实质性的内容。

QString Widget::process(const QString &request)
{return request;
}

💡 “根据请求处理响应” 是服务器开发中的最核心的步骤。
一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的。


此时,服务器程序编写完毕。
但是直接运行还看不出效果,还需要搭配客户端来使用。


回显客户端

创建界面,包含一个 QLineEditQPushButtonQListWidget

  • 先使用水平布局把 QLineEditQPushButton 放好,并设置这两个控件的垂直方向的 sizePolicyExpanding

  • 再使用垂直布局把 QListWidget 和上面的水平布局放好。

  • 设置垂直布局的 layoutStretch 为 5, 1(当然这个尺寸比例根据个人喜好微调)。

widget.cpp 中,先创建两个全局常量,表示服务器的 IP 和 端口

// 提前定义好服务器的 IP 和 端口
const QString SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9990;
  • 创建 QUdpSocket 成员
  • 修改 widget.h,定义成员
class Widget : public QWidget
{Q_OBJECT
public:Widget(QWidget *parent = nullptr);~Widget();
private:Ui::Widget *ui;QUdpSocket *socket;
};

修改 widget.cpp,初始化 socket

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1. 设置窗口名字this->setWindowTitle("客户端");// 2. 实例化 socketsocket = new QUdpSocket(this);
}

给发送按钮 slot 函数,实现发送请求

void Widget::on_pushButton_clicked()
{// 1. 获取到输入框的内容const QString text = ui->lineEdit->text();// 2. 构造请求数据QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP),SERVER_PORT);// 3. 发送请求socket->writeDatagram(requestDatagram);// 4. 信息添加到列表框中ui->listWidget->addItem("客户端说:" + text);// 5. 清空输入框ui->lineEdit->setText("");
}

再次修改 Widget 的构造函数,通过信号槽,来处理服务器的响应。

connect(socket, &QUdpSocket::readyRead, this, [=]() {const QNetworkDatagram responseDatagram = socket->receiveDatagram();QString response = responseDatagram.data();ui->listWidget->addItem(QString("服务器说:") + response);
});

最终执行效果
启动多个客户端都可以正常工作。

TCP Socket

核心 API 概览

核心类是两个: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 多路复用中的通知机制)

QByteArray 用于表示一个字节数组。可以很方便的和 QString 进行相互转换。

例如:

  • 使用 QString 的构造函数即可把 QByteArray 转换成 QString。

  • 使用 QString 的 toUtf8 函数即可把 QString 转换成 QByteArray

回显服务器

创建界面。包含一个 QListWidget,用于显示收到的数据。

创建 QTcpServer 并初始化
修改 widget.h,添加 QTcpServer 指针成员。

class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private:Ui::Widget *ui;// 创建 QTcpServerQTcpServer* tcpServer;
};

修改 widget.cpp,实例化 QTcpServer 并进行后续初始化操作。

  • 设置窗口标题

  • 实例化 TCP server(父元素设为当前控件,会在父元素销毁时被一起销毁)。

  • 通过信号槽,处理客户端建立的新连接。

  • 监听端口

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);this->setWindowTitle("服务器端");// 2. 实例化 TCP servertcpServer = new QTcpServer(this);// 3. 通过信号槽,处理客户端建立的新连接。connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);// 4. 监听端口bool ret = tcpServer->listen(QHostAddress::Any, 9090);if (!ret) {QMessageBox::critical(nullptr, "服务器启动失败!", tcpServer->errorString());exit(0);}
}

继续修改 widget.cpp,实现处理连接的具体方法 processConnection

  • 获取到新的连接对应的 socket。

  • 通过信号槽,处理收到请求的情况

  • 通过信号槽,处理断开连接的情况。

void Widget::processConnection()
{// 1. 获取到新的连接对应的 socket.QTcpSocket* clientSocket = tcpServer->nextPendingConnection();QString log = QString("[") + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线!";ui->listWidget->addItem(log);// 2. 通过信号槽,处理收到请求的情况.connect(clientSocket, &QTcpSocket::readyRead, this, [=]() {QString request = clientSocket->readAll();QString requestStr = process(request);QString response = requestStr;clientSocket->write(response.toUtf8());QString log = QString("[") + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort()) + "] req: "+ request + ", resp: " + response;ui->listWidget->addItem(log);});// 3. 通过信号槽,处理断开连接的情况connect(clientSocket, &QTcpSocket::disconnected, this, [=]() {QString log = QString("[") + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort()) + "] 客户端下线!";ui->listWidget->addItem(log);// 删除 clientSocketclientSocket->deleteLater();});
}

实现 process 方法,实现根据请求处理响应。
由于我们此处是实现回显服务器,所以 process 方法中并没有包含实质性的内容。

QString Widget::process(const QString &request)
{return request;
}

回显客户端

创建界面。包含一个 QLineEditQPushButtonQListWidget

  • 先使用水平布局把 QLineEditQPushButton 放好,并设置这两个控件的垂直方向的 sizePolicyExpanding

  • 再使用垂直布局把 QListWidget 和上面的水平布局放好。

  • 设置垂直布局的 layoutStretch 为 5, 1(当然这个尺寸比例根据个人喜好微调)。

创建 QTcpSocket 并实例化
修改 widget.h,创建成员。

class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private:Ui::Widget *ui;// 新增 QTcpSocketQTcpSocket* socket;
};

修改 widget.cpp,对 QTcpSocket 进行实例化。

设置窗口标题

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1. 设置窗口标题.this->setWindowTitle("客户端");// 2. 实例化 socket 对象.socket = new QTcpSocket(this);// 3. 和服务器建立连接.socket->connectToHost("127.0.0.1", 9090);// 4. 等待并确认连接是否出错.if (!socket->waitForConnected()) {QMessageBox::critical(nullptr, "连接服务器出错!", socket->errorString());exit(0);}
}

修改 widget.cpp,给按钮增加点击的 slot 函数,实现发送请求给服务器。

void Widget::on_pushButton_clicked()
{// 获取输入框的内容const QString text = ui->lineEdit->text();// 清空输入框内容ui->lineEdit->setText("");// 把消息显示到界面上ui->listWidget->addItem(QString("客户端说:") + text);// 发送消息给服务器socket->write(text.toUtf8());
}

修改 widget.cpp 中的 Widget 构造函数,通过信号槽,处理收到的服务器的响应。

// 处理服务器返回的响应.
connect(socket, &QTcpSocket::readyRead, this, [=]() {QString response = socket->readAll();qDebug() << response;ui->listWidget->addItem(QString("服务器说:") + response);
});

先启动服务器,再启动客户端(可以启动多个),最终执行效果:

之前在学习 Linux 时写的 TCP 的回显服务器的时候,遇到了一个问题~~ 多个客户端同时访问的时候,就只有一个生效~~

后来引入了多线程,每个客户端安排一个单独的线程,问题才得到改善~~

之前写的那个程序,之所以出现上述问题,和 TCP,和多线程 都没啥关系…… 从来没有说法,说 TCP 服务器必须使用多线程编写!!!

之前存在这个问题的本质原因,是写双重循环,里层循环没有及时结束,导致外层循环不能快速的第二次调用到 accept,导致第二个客户端无法进行处理了。

引入多线程,本质上就是把双重循环,化简成两个独立的循环。


注意:也是思考点:

服务器通常需要不断地监听客户端的连接请求和数据传输,这在代码中通常表现为一个持续运行的循环(while循环)。然而,在现代的事件驱动编程模型中,这种持续的轮询或循环检查不再是必须的,因为操作系统和编程框架提供了更为高效的事件处理机制。

在代码中,使用了Qt框架来实现服务器。Qt是一个跨平台的应用程序和用户界面框架,它提供了事件驱动的编程模型。在这种模型中,服务器不需要显式地编写一个while循环来不断检查是否有新的连接或数据。相反,Qt框架会在后台处理这些事件,并在适当的时候触发相应的信号(signals)。

在代码中,QTcpServer对象负责监听端口并接受新的连接。当有新的客户端连接时,QTcpServer会发出listen信号,然后通过processConnection槽函数(slot)来处理新的连接。这是通过连接(connect)QTcpServer::listen信号到Widget::processConnection槽函数实现的:

connect(tcpServer, &QTcpServer::listen, this, &Widget::processConnection);

当新的数据到达时,QTcpSocket对象会发出readyRead信号,然后通过一个lambda表达式来读取数据并处理请求。这是通过连接QTcpSocket::readyRead信号到一个lambda表达式实现的:

connect(clientSocket, &QTcpSocket::readyRead, this, [=](){QString request = clientSocket->readAll();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);
});

同样,当客户端断开连接时,QTcpSocket会发出disconnected信号,然后通过一个lambda表达式来处理断开连接的事件:

connect(clientSocket, &QTcpSocket::disconnected, this, [=](){QString log = "[" + clientSocket->peerAddress().toString() + ":" +QString::number(clientSocket->peerPort()) + "] 客户端下线";ui->listWidget->addItem(log);clientSocket->deleteLater();//下一轮事件进行delete!
});

这种事件驱动的模型使得服务器能够高效地处理多个客户端的连接和数据,而不需要显式地编写一个while循环。Qt框架会自动管理这些事件,并在适当的时候调用相应的处理函数。

HTTP Client

  • HTTP 使用场景比 TCP/UDP 更多一些。
  • Qt 中提供了 HTTP 客户端相关支持。
  • HTTP 协议本质上是基于 TCP 协议实现的,实现一个 HTTP 客户端 / 服务器,本质上是基于 TCP socket 进行封装。
  • 需要注意的是,Qt 只是提供了 HTTP 客户端,而没有提供 HTTP 服务器的库。

进行 Qt 开发时,和服务器之间的通信很多时候也会用到 HTTP 协议。

  • 通过 HTTP 从服务器获取数据。

  • 通过 HTTP 向服务器提交数据。

核心 API

关键类主要是三个:QNetworkAccessManager,QNetworkRequest,QNetworkReply。

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

QNetworkRequest 表示一个 HTTP 请求(不含 body)。

如果需要发送一个带有 body 的请求(比如 post),会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body。

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

其中 QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值:

取值说明
ContentTypeHeader描述 body 的类型。
ContentLengthHeader描述 body 的长度。
LocationHeader用于重定向报文中指定重定向地址(响应中使用,请求用不到)
CookieHeader设置 cookie
UserAgentHeader设置 UserAgent

QNetworkReply 表示一个 HTTP 响应。这个类同时也是 QIODevice 的子类。

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

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

代码示例

给服务器发送一个 GET 请求。

  1. 创建界面。包含一个 QLineEditQPushButton

  • 先使用水平布局把 QLineEditQPushButton 放好,并设置这两个控件的垂直方向的 sizePolicyExpanding

  • 再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好。(QPlainTextEditreadOnly 设置为 true)

  • 设置垂直布局的 layoutStretch 为 5, 1(当然这个尺寸比例根据个人喜好微调)。

💡 此处建议使用 QPlainTextEdit 而不是 QTextEdit,主要因为 QTextEdit 要进行富文本解析,如果得到的 HTTP 响应体积很大,就会导致界面渲染缓慢甚至被卡住。

修改 widget.h,创建 QNetworkAccessManager 属性

class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();private:Ui::Widget *ui;// 新增属性QNetworkAccessManager* manager;
};

修改 widget.cpp,创建实例

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 实例化属性manager = new QNetworkAccessManager(this);
}

编写按钮的 slot 函数,实现发送 HTTP 请求功能

void Widget::on_pushButton_clicked()
{// 1. 获取到输入框中的 url,构造 Url 对象QUrl url(ui->lineEdit->text());// 2. 构造 HTTP 请求对象QNetworkRequest request(url);// 3. 发送 GET 请求QNetworkReply* response = manager->get(request);// 4. 通过信号槽来处理响应connect(response, &QNetworkReply::finished, this, [=]() {if (response->error() == QNetworkReply::NoError) {// 响应正确QString html(response->readAll());ui->plainTextEdit->setPlainText(html);qDebug() << html;} else {// 响应出错ui->plainTextEdit->setPlainText(response->errorString());}response->deleteLater();});
}

执行程序,观察效果

发送 POST 请求代码也是类似。使用 manager->post() 即可。此处不再演示。


其他模块

Qt 中还提供了 FTP, DNS, SSL 等网络相关的组件工具。此处不再一一展开介绍。有需要的同学可以自行翻阅官方文档学习相关 API 的使用。

http://www.dtcms.com/a/395520.html

相关文章:

  • Java中List转换成Map的两种方式
  • 嵌入式 - RAM10
  • Qwen新开源tongyi-DeepResearch:核心优势
  • Java Stream API性能优化实践指南
  • Qt配置序列化与反序列化实战:QSettings的深度应用指南
  • MySQL下载时出现“starting the server”或“initializing错误”的原因以及解决方案
  • MySQL 数据库核心知识点详解
  • 让机器人边思考边行动!新一代具身智能EO-1:统一架构突破VLA瓶颈
  • 数据库笔试选择题:题组1
  • 一款相机是只有桶形畸变 和 枕形畸变的一种,还是两个都有?
  • 德克西尔井盖异动传感器:城市安全的隐形守护者
  • HTML基本标签一
  • BGP高防服务器具体是指什么
  • 打工人日报#20250922
  • Django视图与路由
  • 在thinkphp8的模板文件中 如何调用公共服务类函数
  • Nextcloud增加模块内嵌网页
  • Ubuntu18.04 MySQL5.7.42 内存升高导致OOM MySQL重启解决办法
  • html调起exe程序
  • C#中的Task怎么理解,理解异步编程的核心
  • fastApi框架开发一个web端仓库管理系统
  • mosquitto求医之路(3):Docker安装也不好使
  • 字节 TRAE:AI 原生 Coding Agent 的工程化架构与实战落地
  • 保姆级教程:windows和linux双系统的电脑如何无副作用,安全删除linux
  • SSM宠物领养平台16e63(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 大前端系统课教程(视频教程)
  • Bulutistan:融合本地与云端,借 Azure Arc 开启创新之旅
  • 北极象沉浸式翻译 - 沉浸式翻译 | 免费翻译 | PDF翻译
  • C++编码
  • WKT、WKB和GeoJson