Qt:Qt网络
目录
Qt网络介绍
UDP Socket
TCP Socket
HTTP Client
总结
Qt网络介绍
和多线程类似,Qt为了支持跨平台,对网络编程的API也进行了重新封装。咱们接下来的重点介绍Qt的网络相关的API的使用。
实际Qt开发中进行网络编程,也不⼀定使用Qt封装的网络API,也有⼀定可能使用的是系统原生API或者其他第三方框架的API。
在进行网络编程之前,需要在项目中的 .pro 文件中添加 network 模块
有时添加之后要手动编译一下项目,使 Qt Creator 能够加载对应模块的头文件
UDP Socket
主要的类有两个:QUdpSocket 和 QNetworkDatagram
QUdpSocket 表示一个 UDP 的 socket 文件
名称 | 类型 | 说明 | 对标原生API |
bind(constQHostAddress&,quint16) | 方法 | 绑定指定的端⼝号 | bind |
receiveDatagram() | 方法 | 返回 读取⼀个UDP数据报 | recvfrom |
writeDatagram(const QNetworkDatagram&) | 方法 | 发送⼀个UDP数据报 | sendto |
readyRead | 信号 | 在收到数据并准备就绪后触发 | 无(类似于IO多路复用的通知机制) |
QNetworkDatagram 表示一个 UDP 数据报
名称 | 类型 | 说明 | 对标API |
QNetworkDatagram(const QByteArray&,const QHostAddress&,quint16) | 构造函数 | 通过,目标IP地址,目标端口号构造⼀个UDP数据报,通常用于发送数据时 | 无 |
data() | 方法 | 获取数据报内部持有的数据 返回QByteArray | 无 |
senderAddress() | 方法 | 获取数据报中包含的对端的IP地址. | 无,recvfrom包含了该功能. |
senderPort() | 方法 | 获取数据报中包含的对端的端⼝号. | 无,recvfrom包含了该功能. |
代码示例:回显服务器
创建界面,包含一个 QListWidget 用来显示消息
widget.hpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
#include <QMessageBox>
#include <QNetworkDatagram>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void processRequest();
QString process(const QString& request);
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
#endif // WIDGET_H
widget.cpp
先连接信号槽,再绑定端口。若顺序反过来,可能会出现端口绑定好之后,立即接收到请求,但此时还没来得及连接信号槽,那么这个请求就有可能错过了
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
setWindowTitle("服务器");
//实例化socket
socket = new QUdpSocket(this);
//连接信号槽
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
//绑定端口
bool ret = socket->bind(QHostAddress::Any, 9090);
if(!ret) {
QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
//读取请求
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
QString request = requestDatagram.data();
//根据请求计算响应
const QString& response = process(request);
//将响应写回客户端
QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
socket->writeDatagram(responseDatagram);
//打印日志
QString log = "[" + requestDatagram.senderAddress().toString() + ":" + \
QString::number(requestDatagram.senderPort()) + "] request:" + \
request + ", response:" + response;
ui->listWidget->addItem(log);
}
//本代码实现的是回显服务器,所以process方法中不包含实质性的业务代码
QString Widget::process(const QString &request) {
return request;
}
代码示例:回显客户端
创建界面,包含一个 QLineEdit、QPushButton、QListWidget
widget.hpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
#include <QMessageBox>
#include <QNetworkDatagram>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void sendRequest();
void recvResponse();
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
const QString SERVER_IP = "127.0.0.1";
const qint16 SERVER_PORT = 9090;
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
setWindowTitle("客户端");
//实例化socket
socket = new QUdpSocket(this);
//连接按钮的信号槽
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::sendRequest);
//连接socket的信号槽
connect(socket, &QUdpSocket::readyRead, this, &Widget::recvResponse);
}
Widget::~Widget()
{
delete ui;
}
void Widget::sendRequest()
{
//获取输入框的内容
const QString& text = ui->lineEdit->text();
//构造请求数据
QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT);
//发送请求
socket->writeDatagram(requestDatagram);
//消息添加到列表框中
ui->listWidget->addItem("客户端:" + text);
//清空输入框
ui->lineEdit->setText("");
}
void Widget::recvResponse()
{
//读取请求
const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
//消息添加入列表
ui->listWidget->addItem(QString("服务器:") + response);
}
TCP Socket
核心类是两个:QTcpServer 和 QTcpSocket
QTcpServer 用于监听端口和获取客户端连接
名称 | 类型 | 说明 | 对标原生API |
listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号并开始监听 | bind和listen |
nextPendingConnection() | 方法 | 从系统中获取一个已经建立好的tcp连接 返回一个TcpSocket,表示这个连接 通过这个socket对象完成与客户端之间的通信 | accept |
newConnection | 信号 | 有新的客户端建立好连接后触发 | 无(类似与IO多路复用中的通知机制) |
QTcpSocket 用于客户端和服务器之间的数据交互
名称 | 类型 | 说明 | 对标原生API |
readAll() | 方法 | 读取当前接收缓冲区中的所有数据. 返回QByteArray对象 | read |
write(constQByteArray&) | 方法 | 把数据写入socket中 | write |
deleteLater | 方法 | 暂时把socket对象标记为无效 Qt会在下个事件循环中析构释放该对 象 | 无(但是类似于"半自动化的垃圾回收") |
readyRead | 信号 | 有数据到达并准备就绪时触发 | 无(但是类似于IO多路复用中的通知机制) |
disconnected | 信号 | 连接断开时触发 | 无(但是类似于IO多路复用中的通知机制) |
QByteArray 用于表示一个字节数组,可以很方便的和 QString 进行相互转换
- 使用 QString 的构造函数即可把 QByteArray 转成 QString
- 使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray
代码示例:回显服务器
创建界面,包含一个 QListWidget,用于显示收到的数据
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMessageBox>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void processConnection();
QString process(const QString&);
private:
Ui::Widget *ui;
QTcpServer* tcpServer;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
setWindowTitle("服务端");
//实例化tcpServer
tcpServer = new QTcpServer(this);
//信号槽处理新连接
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
//监听端口
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if(!ret) {
QMessageBox::critical(nullptr, "服务器启动出错", tcpServer->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
//获取新连接对应的socket
QTcpSocket* socket = tcpServer->nextPendingConnection();
//打印日志
QString log = "[" + socket->peerAddress().toString() + ":" + \
QString::number(socket->peerPort()) + "]客户端上线";
ui->listWidget->addItem(log);
//通过信号槽处理接收到的请求
connect(socket, &QTcpSocket::readyRead, [=]() {
//读取请求
QString request = socket->readAll();
//根据请求处理响应
QString response = process(request);
//将响应写回客户端
socket->write(response.toUtf8());
//打印日志
QString log = QString("[") + socket->peerAddress().toString() \
+ ":" + QString::number(socket->peerPort()) + "] request: " + \
request + ", response: " + response;
ui->listWidget->addItem(log);
});
//通过信号槽处理断开连接的情况
connect(socket, &QTcpSocket::disconnected, this, [=]() {
QString log = QString("[") + socket->peerAddress().toString() \
+ ":" + QString::number(socket->peerPort()) + "] 客⼾端下线";
ui->listWidget->addItem(log);
// 删除 socket
socket->deleteLater();
});
}
QString Widget::process(const QString& request) {
return request;
}
删除socket时最好不要直接使用delete,而是使用deleteLate
因为整个槽函数都是围绕socket进行操作的,务必确保delete是函数中的最后一步。使用deleteLater更加保险,其不会立即销毁socket,而是在下一轮事件循环中再进行销毁操作。槽函数都是在事件循环中执行的,进入到下一轮事件循环,意味着上一轮事件循环肯定结束了(即槽函数肯定执行完成了)。
代码示例:回显客户端
创建界面,包含一个 QLineEdit、QPushButton、QListWidget
先使用谁平布局把QLineEdit和QPushButton放好,并设置这两个控件的垂直方向的sizePolicy为Expanding
再使用垂直布局把QListWidget和上面的水平布局放好
设置垂直布局的layoutStretch为5,1(当然这个尺寸比例根据个人喜好微调)
创建QTcpSocket并实例化
修改widget.h,创建成员
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 新增 QTcpSocket
QTcpSocket* socket;
};
修改widget.cpp,对QTcpSocket进行实例化
- 设置窗口标题
- 实例化socket对象(父元素设为当前控件,会在父元素销毁时被⼀起销毁)
- 和服务器建立连接
- 等待并确认连接是否出错
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(1);
}
修改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);
});
先启动服务器,再启动客户端(可以启动多个),最终执行效果:
由于我们使用信号槽处理同⼀个客户端的多个请求,不涉及到循环,也就不会使客户端之间相互影响了
HTTP Client
进行Qt开发时,和服务器之间的通信很多时候也会用到HTTP协议
- 通过HTTP从服务器获取数据
- 通过HTTP向服务器提交数据
关键类主要是三个:QNetworkAccessManager、QNetworkRequest、QNetworkReply
QNetworkAccessManager 提供了 HTTP 的核心操作
方法 | 说明 |
get(constQNetworkRequest&) | 发起⼀个HTTPGET请求,返回QNetworkReply对象 |
post(constQNetworkRequest&,const QByteArray&) | 发起⼀个HTTPPOST请求,返回QNetworkReply对象 |
QNetworkRequest 表示一个HTTP请求(不含body)
若需要发送一个带有 body 的请求(如post),会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body
方法 | 说明 |
QNetworkRequest(constQUrl&) | 通过URL构造⼀个HTTP请求 |
setHeader(QNetworkRequest::KnownHeaders header,constQVariant&value) | 设置请求头 |
QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值:
取值 | 说明 |
ContentTypeHeader | 描述body的类型 |
ContentLengthHeader | 描述body的长度 |
LocationHeader | ⽤于重定向报文中指定重定向地址.(响应中使用,请求用不到) |
CookieHeader | 设置cookie |
UserAgentHeader | 设置User-Agent |
QNetworkReply表示一个HTTP响应,这个类同时也是QIODevice的子类
方法 | 说明 |
error() | 获取出错状态 |
errorString() | 获取出错原因的文本 |
readAll() | 读取响应body |
header(QNetworkRequest::KnownHeaders header) | 读取响应指定header的值 |
QNetworkReply 还有一个重要的信号 finished 会在客户端收到完整的响应数据之后触发
代码示例:HTTP客户端
创建界面,包含⼀个 QLineEdit,QPushButton和QListWidget
- 先使用水平布局把 QPushButton,QLineEdit 和 sizePolicy 为 QListWidget QPushButton 放好,并设置这两个控件的垂直方向的 Expanding
- 再使用垂直布局把 QListWidget 和上面的水平布局放好
- 设置垂直布局的 layoutStretch 为 5,1 (当然这个尺寸比例根据个人喜好微调)
此处建议使用 QPlainTextEdit 而不是 QTextEdit,主要因为 QTextEdit 要进行富文本解析,如果得到的HTTP响应体积很大,就会导致界面渲染缓慢甚至被卡住。
修改widget.h,创建QNetworkAccessManager属性
class Widget : public QWidget
{
Q_OBJECT
public:
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, 构造 QUrl 对象
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();
});
}
执行程序,观察效果
其他模块
Qt 中还提供了FTP,DNS,SSL等网络相关的组件工具,此处不再⼀⼀展开介绍,有需要的可以自行翻阅官方文档学习相关API的使用。
总结
Qt是一个跨平台的应用程序和用户界面框架,广泛用于开发图形用户界面程序,同时也提供了强大的网络编程能力。Qt的网络编程主要基于其网络模块,即QtNetwork模块。以下是一些关于Qt网络编程的小结:
TCP套接字编程
- 使用QTcpSocket可以创建客户端和服务器端的TCP连接。客户端通过连接到服务器的IP地址和端口来建立连接,而服务器端则监听特定端口等待客户端的连接请求。
UDP套接字编程
- QUdpSocket用于处理无连接的网络通信。它允许发送和接收UDP数据包。UDP不保证数据包的顺序或可靠性,但其开销较小,适用于对实时性要求较高的应用。
HTTP请求
- QNetworkAccessManager是处理HTTP请求的核心类。它支持GET、POST、PUT、DELETE等多种HTTP方法。通过QNetworkRequest可以设置请求的URL、头部信息等,而QNetworkReply用于处理服务器返回的数据。