Qt的网络编程

Qt的网络编程

使用 Qt 的网络编程 API 需要先在 .pro 文件中添加 network 模块。Qt 的模块提供了静态库和动态库两个版本。

不默认包含网络等其他模块,是为了使 Qt 生成的可执行程序更加轻量化。

bash 复制代码
#.pro
QT			+=core gui network
#添加到如上位置

Qt 封装了自己的网络编程接口,将 Qt 的信号和槽的机制运用到了 Qt 的网络编程接口上,这些接口的使用方法与 C 语言原生的套接字编程接口区别较大,尤其是信号和槽的机制使得 Qt 服务器设计可很少使用甚至不使用多线程,也能实现正常的服务器功能。但由于服务器一般都不会做成图形化界面,或者说不会使用 Qt 进行编写,也就无所谓 Qt 的服务器设计了。

1. UDP Socket

Qt 提供了两个类,用 QUdpSocket 来表示一个 UDP 的 socket 文件,用 QNetworkDatagram 来表示一个 UDP 数据报。

QUdpSocket:

声明 类型 说明 对标原生 API
bind(constQHostAddress&, quint16) 方法 绑定指定的端口号。 bind()
QNetworkDatagram receiveDatagram() 方法 读取一个 UDP 数据报。 recvfrom()
writeDatagram(const QNetworkDatagram&) 方法 发送一个 UDP 数据报。 sendto
readyRead 信号 在收到数据并准备就绪后触发。 无

quint16 是 Qt 提供的短整型类型,由于 C++ 并未规定 short int 的大小,于是 Qt 为了解耦自创了自己的短整型类型。

QNetworkDatagram:

声明 类型 说明 对标原生 API
QNetworkDatagram(const QByteArray&,const QHostAddress&,quint16) 构造函数 构造一个 UDP 数据报,通常用于发送数据时使用。 无
QByteArray data() 方法 获取数据报内部持有的数据。 无
senderAddress() 方法 获取数据报中包含的对端 IP 地址。 无
senderPort() 方法 获取数据报中包含的对端端口号。 无

QString 提供了一个 QByteArray 的构造,可以使用 QString 直接赋值。

1.1 UDP回显服务器

注意 Qt 的信号和槽提供了一种类似于多路复用 IO 的机制,当对端发送数据到来,本机准备读取时,就会触发 readyRead 信号并调用相关槽函数(自定义)。

1.1.1 服务器

mainwindow.h:

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QNetworkDatagram>
#include <QUdpSocket>

QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    QUdpSocket* socket;

    void processRequest();
    QString process(const QString& request);

};
#endif // MAINWINDOW_H

mainwindow.cpp:

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    socket = new QUdpSocket(this);

    this->setWindowTitle("服务器");

    connect(socket,&QUdpSocket::readyRead,this,&MainWindow::processRequest);

    bool ret = socket->bind(QHostAddress::Any,6666);
    if(!ret)
    {
        QMessageBox::critical(this,"端口号绑定失败",socket->errorString());
    }
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::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())+"] req: "
                  +request+"resp: "+response;
    ui->listWidget->addItem(log);
}
QString MainWindow::process(const QString& request)
{
    return request;
}
1.1.2 客户端

mainwindow.h:

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QNetworkDatagram>
#include <QUdpSocket>


QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();

private:
    Ui::MainWindow *ui;
    QUdpSocket* socket;
    void processResponse();

};
#endif // MAINWINDOW_H

mainwindow.cpp:

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    socket = new QUdpSocket(this);

    this->setWindowTitle("客户端");

    connect(socket,&QUdpSocket::readyRead,this,&MainWindow::processResponse);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::processResponse()
{

    const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
    QString response = responseDatagram.data();
    ui->listWidget->addItem("服务器:"+response);
}

void MainWindow::on_pushButton_clicked()
{
    if(ui->lineEdit->text()=="")
    {
        return;
    }
    const QString& text = ui->lineEdit->text();
    QNetworkDatagram requestDatagram(text.toUtf8(),QHostAddress("127.0.0.1"),6666);

    socket->writeDatagram(requestDatagram);

    ui->listWidget->addItem("客户端:"+text);
    ui->lineEdit->setText("");
}

2. TCP Socket

Qt 提供了两个类,用 QTcpServer 来监听端口,获取客户端连接。用 QTcpSocket 来实现客户端和服务器之间的数据交换。

QTcpServer:

声明 类型 说明 对标原生 API
listen(constQHostAddress&, quint16 port) 方法 绑定指定的地址和端口号,并开始监听。 bind() 和 listen()
nextPendingConnection() 方法 从系统中获取一个已经建立好的 TCP 连接。返回一个 QTcpSocket 表示这个客户端的连接,通过这个 socket 对象完成和客户端之间的通信。 accept()
newConnection 信号 有新的客户端建立连接好之后触发。 无,类似于多路复用

QTcpSocket

声明 类型 说明 对标原生 API
QBytearray readAll() 方法 读取当前接收缓冲区中的所有数据,返回 QBytearray 对象。 read()
write(const QByteArray&) 方法 把数据写入 socket 中。 write()
deleteLater 方法 暂时把 socket 对象标记为无效。Qt 会在下个事件循环中析构释放该对象。 无
readyRead 信号 有数据到达并准备就绪时就触发。 无
disconnected 信号 连接断开时触发。 无

2.1 TCP回显服务器

注意,回显服务器的设计没有考虑到粘包问题,在实际的服务器设计中,应当自定义应用层协议,在报文里写好本次发送的数据包总大小,并使用一个足够大的字节数组作为缓冲区来接收,然后按照协议的方法解析报文。

2.1.1 服务器

mainwindow.h

注意服务器是使用 QTcpServer 类型作为成员变量监听套接字,当接收到新的连接之后,再创建 QTcpSocket 变量作为通信的套接字使用。

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTcpServer>

QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    QTcpServer* tcpServer;
    void processConnection();
    QString process(const QString request);
};
#endif // MAINWINDOW_H

mainwindow.cpp:

每有一个连接来到,服务器就会有一个 QTcpSocket* 对象,随着客户端越来越多,如果不释放就会释放就会导致严重的内存泄漏问题。但是手动 delete 并不可行,因为槽函数都是依赖这个对象来进行操作的,最好使用 Qt 提供的 deleteLater() 接口,它会在下次事件循环时,将对象释放掉。

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QTcpSocket>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->setWindowTitle("服务器");

    tcpServer = new QTcpServer(this);

    connect(tcpServer,&QTcpServer::newConnection,this,&MainWindow::processConnection);

    bool ret = tcpServer->listen(QHostAddress::Any,6666);
    if(!ret)
    {
        QMessageBox::critical(this,"服务器启动失败!",tcpServer->errorString());
        exit(1);
    }
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::processConnection()
{
    QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
    QString log ="[" + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort())+"]客户端上线!";
    ui->listWidget->addItem(log);

    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);
    });

    connect(clientSocket,&QTcpSocket::disconnected,this,[=]()
    {
        QString log = "[" + clientSocket->peerAddress().toString()+":"+QString::number(clientSocket->peerPort())+"]客户端下线!";
        ui->listWidget->addItem(log);
        //手动释放 clientSocket,使用Qt提供的方法,在下一次事件循环时释放
        clientSocket->deleteLater();

    });
}

QString MainWindow::process(const QString request)
{
    return request;
}
2.1.2 客户端

mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTcpSocket>

QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();

private:
    Ui::MainWindow *ui;
    QTcpSocket* socket;
};
#endif // MAINWINDOW_H

mainwindow.cpp

connectToHost() 是在进行三次握手。

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->setWindowTitle("客户端");

    socket = new QTcpSocket(this);
    socket->connectToHost("127.0.0.1",6666);
    connect(socket,&QTcpSocket::readyRead,this,[=]()
    {
        QString response = socket->readAll();
        ui->listWidget->addItem(" 服务器:"+response);
    });

    bool ret=socket->waitForConnected();
    if(!ret)
    {
        QMessageBox::critical(this,"服务器连接出错",socket->errorString());
        exit(1);
    }

}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_clicked()
{
    const QString& text = ui->lineEdit->text();
    if(text=="")
    {
        return;
    }
    socket->write(text.toUtf8());
    ui->listWidget->addItem("客户端:"+text);
    ui->lineEdit->setText("");
}

3. HTTP

Qt 提供了 HTTP 客户端的库,但没有提供服务器的库。Qt 的 HTTP 库并没有像浏览器那样解析 HTML ,进行类似于网页浏览的操作。一般用于通过 HTTP 从服务器获取数据或向服务器提交数据。

Qt 的 API 主要是三个类包含的,QNetworkAccessManager、QNetworkRequest、QNetworkReply 。

QNetworkAccessManager 提供了 HTTP 的核心操作:

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

QNetworkRequest 表示一个 HTTP 请求:

注意 QNetworkRequest 是不包含 body 的,body 需要通过 setHeader() 来包含。

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

QVariant 表示一个类型可变的值,类似于 C 语言中的 void* 。

QNetworkRequest::KnownHeaders 是一个枚举变量,常用取值:

取值 说明
ContentTypeHeader 描述 body 的类型。
ContentLengthHeader 描述 body 的长度。
LocationHeader 用于重定向报文中指定重定向地址。
CookieHeader 设置 cookie。
UserAgentHeader 设置 User-Agent。

QNetworkReply 表示一个 HTTP 响应:

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

注意 QNetworkReply 的方法一般都是非阻塞的,即执行流不会等待方法获取请求或响应完成后再执行剩下的代码,Qt 通过提供了 finished 信号,这个信号会在客户端收到完整的响应数据之后触发。

相关推荐
白帽攻防录8 小时前
SRC 挖洞:Roundcube 预认证 SQL 注入深度复盘,CVE-2026-48842 preg_replace 转义绕过怎么打穿邮件系统
网络·数据库·sql·网络安全·sql注入
左手の明天9 小时前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
青少儿编程课堂9 小时前
后缀自动机解析:本质不同子串与最长重复片段统计
c++·python·算法·bfs·信息学竞赛
朝朝辞暮i9 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
无名猿11 小时前
shared_ptr 完全指南:引用计数、控制块与开销
c++·性能优化·内存管理·标准库·现代c++
要吃这碗饭12 小时前
深入理解 TCP 协议:从三次握手到可靠传输
网络·网络协议·tcp/ip
和裕12 小时前
全纸结构重型纸箱能否满足 1 吨以上设备出口熏蒸豁免要求?通关合规性全解析
大数据·运维·网络·人工智能·算法
呉師傅13 小时前
得力P2500、M2500系列打印机硒鼓加粉及清零方法【纯享版】
运维·网络·windows·电脑
xailorliu13 小时前
SLIC_超像素算法
c++·opencv·目标检测·机器学习·计算机视觉