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 主要是三个类包含的,QNetworkAccessManagerQNetworkRequestQNetworkReply

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 信号,这个信号会在客户端收到完整的响应数据之后触发。

相关推荐
腾讯安全应急响应中心36 分钟前
命悬生死线:当游戏遭遇DDoS围剿,如何用AI破局?
网络·人工智能·游戏·ddos
wen__xvn1 小时前
Codeforces Round 1014 (Div. 2)2092A - Kamilka and the Sheep c++
开发语言·c++·算法
梁下轻语的秋缘1 小时前
每日c/c++题 备战蓝桥杯(全排列问题)
c++·算法·蓝桥杯·深度优先
虾球xz1 小时前
游戏引擎学习第194天
c++·学习·游戏引擎
AredRabbit1 小时前
vector<int> 的用法
c++·算法·vector
Quz1 小时前
QML输入控件:Dial联动、音频均衡器的实现 (3)
qt
残诗1 小时前
ar头显和眼镜图像特效处理
网络·网络协议
m0_687399842 小时前
build cinecert/asdcplib to .so or .a
c++·ubuntu·dci
大草原的小灰灰2 小时前
C/C++回调函数实现与std::function和std::bind介绍
c语言·c++