Qt网络相关

" 所有生而孤独的人,葆有的天真 "


为了⽀持跨平台, QT对⽹络编程的 API 也进⾏了重新封装。本章会上手一套基于QT的网络通信编写。

UDP Socket

在使用Qt进行网络编程前,需要在Qt项目中的.pro文件里添加对应的网络模块( network ).

cpp 复制代码
QT += core gui network

QUdpsocket 核心API

|-----------------------------------------------------------------------|------|--------------------------------------|----------|
| 名称 | 类型 | 说明 | 原⽣ API |
| bind(const QHostAddress&, quint16) | 方法 | 绑定指定的端⼝号 | bind |
| receiveDatagram() | ⽅法 | 返回 QNetworkDatagram . 读取 ⼀个 UDP 数据报. | recvfrom |
| writeDatagram(const QNetworkDatagram&) | ⽅法 | 发送⼀个 UDP 数据报. | sendto |
| readyRead | 信号 | 在收到数据并准备就绪后触发 | |
| QNetworkDatagram(const QByteArray&, const QHostAddress& , quint16 ) | 构造函数 | | |
| data() | ⽅法 | 获取数据报内部持有的数据. 返回QByteArray | |
| senderAddress() | ⽅法 | 获取对端的 IP 地址. | |
| senderPort() | ⽅法 | 获取对端的端⼝号 | |

基于udp的简单回显程序

· 服务端

🎃 创建界面,包含一个 QListWidget 用于显示消息

🎃 在主类中,创建QUdpSocket成员

🎃 进行初始化

cpp 复制代码
    // 1. 设置窗口标题
    this->setWindowTitle("服务器");

    // 2. 实例化
    socket = new QUdpSocket(this);

    // 3. 连接信号槽, 处理收到的请求
    connect(socket,&QUdpSocket::readyRead,this,&MainWindow::processRequest);

    // 4. 端口bind(ip,port)
    bool ret = socket->bind(QHostAddress::Any,9090);
    if(ret == false){
        QMessageBox::critical(nullptr,"服务器启动错误",socket->errorString());
        return;
    }

一般来说,都是先建立信号与槽的连接,再进行网络端口的绑定。如果顺序反过来,当网络端口进行bind后,客户端就可以发送来消息处理,此时如果没来得及连接信号槽,为这个请求提供的服务就可能失效。

🎃 槽函数实现(实现对端消息的回显功能)

cpp 复制代码
QString MainWindow::process(const QString & req)
{
    return req;
}

void MainWindow::processRequest()
{
    // 当走到这里 说明服务器已经收到对端信息递达的信号 触发的槽函数处理~
    // 1.获取请求
    const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
    // QNetworkDatagram.data() 拿到对端请求的原始数据
    QString request = requestDatagram.data();

    // 2. 计算处理请求
    const QString& response = process(request);

    // 3.把响应写回到客⼾端
    QNetworkDatagram responseDatagram(response.toUtf8(),\
                requestDatagram.senderAddress(),requestDatagram.senderPort());
    socket->writeDatagram(responseDatagram);

    // 4.显示打印日志
    QString log = "[" + requestDatagram.senderAddress().toString() + ":"  +
            QString::number(requestDatagram.senderPort()) + "]" + " " + "req: " + request + " | " + "resp: " + response;
    ui->listWidget->addItem(log);
}

· 客户端

🧧 创建一个界面,包含用户发送消息窗口、发送按钮、输入栏。发送窗口仍然使用QListWidget,依次是pushButton、QLineEdit。

先使⽤⽔平布局( layout) 把两个控件齐整。

进入到控件中的sizePolicy 设置为 "Expanding "。 接着再使用垂直布局,将消息发送窗口与水平布局的两个空间再进行空间管理。当然我们需要在垂直布局中设置比例,否则比较难看~

这样,简易的界面也算完成了。

🧧 初始化IP和端口

在mainwindows.h中定义两个静态变量

cpp 复制代码
static const QString& server_ip = "127.0.0.1";
static const quint16 server_port = 9090;

🧧 消息发送实现(槽函数)
完成QUdpsocket实例化后,我们只需要关注数据报的发送即可。

cpp 复制代码
void MainWindow::on_send_putton_clicked()
{
    // 1. 获取输入的内容
    const QString& text = ui->message_edit->text();
    // 2. 利用text构造数据报
    QNetworkDatagram requestDatagram = \
            QNetworkDatagram(text.toUtf8(),QHostAddress(server_ip),server_port);
    // 3.发送请求
    socket->writeDatagram(requestDatagram);
    // 4.前端回显
    ui->message_screen->addItem("客户端说: " + text);
    // 5.每发完一条消息 清空输入框
    ui->message_edit->clear();
}

🧧 接收客户端回显(槽函数)

cpp 复制代码
    // 接收服务端回显
    connect(socket,&QUdpSocket::readyRead,this,[=]()
    {
        const QNetworkDatagram responseDatagram = socket->receiveDatagram();
        QString resp = responseDatagram.data();
        ui->message_screen->addItem(QString("服务器回显: " + resp));
    });

测试:

双方都能看到对端发送的消息,并能及时回显。

TCP Socket

TCP相对于UDP而言要复杂很多,只要你曾学过网络知识。我们首先来了解了解Tcp Socket中的核心API。

QTcpServer 核心API

|--------------------------------------------|----|--------------------------------------------------------|---------------|
| 名称 | 类型 | 说明 | 对标原⽣ API |
| listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端⼝号, 并开始监听 | bind 和 listen |
| nextPendingConnection() | 方法 | 从系统中获取到⼀个已经建⽴好的 tcp 连接. 返回⼀个 QTcpSocket , 表⽰这个 客⼾端的连接. | accept |
| newConnection() | 信号 | 有新的客⼾端建⽴连接好之后触发 | |
| readAll() | 方法 | 读取当前接收缓冲区中的所有数据. 返回 QByteArray 对象 | read |
| write(const QByteArray& ) | 方法 | 把数据写⼊ socket 中 | write |
| readyRead | 信号 | 有数据到达并准备就绪时触发 | |
| deleteLater() | 方法 | 暂时把 socket 对象标记为⽆效. | |
| disconnected() | 信号 | 连接断开时触发 | |

想对比于udp的核心API,我们会发现没有了于 "Datagram" 相关的任何接口了。这本质是因为TCP是面向字节流而非udp那样的数据报。

基于TCP的简单回显程序

因为都是做回显,那么这里服务器、客户端的前端依旧同udp是一的 ~

· 服务端

🎨 编写QTcpServer

cpp 复制代码
#include <QMainWindow>
#include <QString>
#include <QTcpServer>
#include <QHostAddress>
#include <QMessageBox>
#include <QTcpSocket>


    // 设置窗口信息
    this->setWindowTitle(" 服务器 ");

    // 1.实例化
    tcpserver = new QTcpServer(this);

    // 2.通过信号槽, 处理客⼾端建⽴的新连接.
    connect(tcpserver,&QTcpServer::newConnection,this,&MainWindow::processConnection);

    // 3.监听+bind
    bool ret = tcpserver->listen(QHostAddress::Any,9090);
    if(ret == false){
        QMessageBox::critical(nullptr,"服务器启动失败",tcpserver->errorString());
        exit(-1);
    }

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

cpp 复制代码
void MainWindow::processConnection()
{
    // 1.根据 listen 获取接收到的新连接
    // 注: 这里是 "QTcpSocket"
    QTcpSocket* clientsocket = tcpserver->nextPendingConnection();

    // 更新服务端日志
    QString log = QString("[") + clientsocket->peerAddress().toString() + ":" + \
            QString::number(clientsocket->peerPort()) + "] 客户端上线";
    ui->listWidget->addItem(log);    
}

🎨 完成回显工作(槽函数实现)

可以发现,不管是做udp还是tcp的网络模型服务,我们都舍弃了用循环的方式处理请求。这会导致我们,一旦存在占用资源的连接不及时释放cpu资源,那么别的请求就不会被读取到,直到该请求的任务执行完成。

Qt中的槽机制恰好避免了这样的困境,一旦发出信号,就执行槽函数即可~

cpp 复制代码
    // 信号槽处理 处理收到请求的情况
    connect(clientsocket,&QTcpSocket::readyRead,this,[=]()
    {
        // 字节流 把所有字节都 读上来
        const QString req = clientsocket->readAll();
        // 根据请求 制作响应
        const QString& resp = process(req);
        // 写回客户端
        clientsocket->write(resp.toUtf8());

        // 服务端回显
        QString log = "[" + clientsocket->peerAddress().toString() + ":" + QString::number(clientsocket->peerPort()) \
                + "]" + " " + "req: " + req;
        ui->listWidget->addItem(log);
    });

由于Tcp可靠性的特征,每一次客户端的连接都会被服务端保存着。当客户端断开连接时而服务端并不是释放两者之间用于连接的资源时,就会导致 资源泄漏~~

cpp 复制代码
    // 通过信号槽, 处理断开连接的情况
    connect(clientsocket,&QTcpSocket::disconnected,this,[=]()
    {
        QString log = QString("[") + clientsocket->peerAddress().toString() + ":" + \
                QString::number(clientsocket->peerPort()) + "] 客户端下线";
        ui->listWidget->addItem(log);
        
        // 释放资源
        clientsocket->deleteLater();    // 并不会立即释放
    });

· 客户端

👑 初始化mainwindow.cpp

cpp 复制代码
    this->setWindowTitle("客户端");
    // 1. 实例化socket
    socket = new QTcpSocket(this);
    // 2. 建立连接
    socket->connectToHost("127.0.0.1",9090);
    // 3.等待并确认连接是否出错
    bool ret = socket->waitForConnected();
    if(ret == false){
        QMessageBox::critical(nullptr,"连接失败",socket->errorString());
        exit(-1);
    }

👑 给按钮增加点击的 slot 函数, 实现发送请求给服务器

cpp 复制代码
void MainWindow::on_send_clicked()
{
    // 获得输入的内容 并输出在界面上
    const QString& text = ui->edit->text();
    ui->edit->clear();
    ui->messageScreen->addItem("已发送: " + text);

    // 真正的发送消息
    socket->write(text.toUtf8());
}

👑 通过信号槽, 处理收到的服务器的响应

cpp 复制代码
    connect(socket,&QTcpSocket::readyRead,this,[=]()
    {
       QString resp = socket->readAll();
       qDebug() << resp;
       ui->messageScreen->addItem("服务端回显: " + resp);
    });

测试:

不管是响应还是,当客户端断开连接时,我们都能够完成对应的功能。

HTTP

进⾏ Qt 开发时, 和服务器之间的通信很多时候也会⽤到 HTTP 协议。我们大概需要以下几个步骤:

• 通过 HTTP 从服务器获取数据.
• 通过 HTTP 向服务器提交数据.

核心API

关键类有三个 QNetworkAccessManager、QNetworkRequest、QNetworkReply .
🏀 QNetworkAccessManager 提供了 HTTP 的核⼼操作.

|-----------------------------------------------------|------------------------------------------|
| ⽅法 | 说明 |
| get(const QNetworkRequest& ) | 发起⼀个 HTTP GET 请求. 返回 QNetworkReply 对象. |
| post(const QNetworkRequest& , const QByteArray& ) | 发起⼀个 HTTP POST 请求. 返回 QNetworkReply 对 象. |

🏀 QNetworkRequest 表⽰⼀个 HTTP 请求.
如果需要发送⼀个带有 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 | 设置请求头. |

🏀 QNetworkReply 表⽰⼀个 HTTP 响应:

|----------------------------------------------|------------|
| ⽅法 | 说明 |
| error() | 获取出错状态. |
| errorString() | 获取出错原因的⽂本. |
| readAll() | 读取响应 body |
| header(QNetworkRequest::KnownHeaders header) | 设置 cookie |

构建一个Http客户端

因为我们只需要构建一个模拟的HTTP请求。服务端则不需要我们进行什么编写~~

QPlainTextEdit vs QTextEdit

QTextEdit会进⾏富 ⽂本解析, 如果得到的 HTTP 响应体积很⼤, 就会导致界⾯渲染缓慢甚⾄被卡住。

🏐 修改 mainwindow.h, 创建 QNetworkAccessManager 属性

cpp 复制代码
#include <QMainWindow>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>

QNetworkAccessManager* manager;

🏐 创建实例并初始化

cpp 复制代码
    this->setWindowTitle(" Http请求发起器 ");
    
    // 1. 实例初始化
    manager = new QNetworkAccessManager(this);

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

cpp 复制代码
void MainWindow::on_pushButton_clicked()
{
    // 1. 根据输入框里url,构造Qurl
    QUrl url(ui->lineEdit->text());
    // 2. 构造http响应
    QNetworkRequest req(url);
    // 3. 以什么方法访问
    QNetworkReply* resp = manager->get(req);

    // 通过信号槽来处理响应
    connect(resp,&QNetworkReply::finished,this,[=]()
    {
        if(resp->error() == QNetworkReply::NoError){
            QString html(resp->readAll());
            ui->plainTextEdit->setPlainText(html);
        }
        else{
            ui->plainTextEdit->setPlainText(resp->errorString());
        }
        resp->deleteLater();
    });
}

测试:


本篇到此结束,感谢你的阅读。

祝你好运,向阳而生~

相关推荐
kirito学长-Java1 小时前
Java牙科诊所管理系统web医院病例挂号预约平台springboot/ssm代码编写
java·开发语言·spring boot
Evand J3 小时前
课题推荐——基于自适应滤波技术的多传感器融合在无人机组合导航中的应用研究
开发语言·算法·matlab·无人机
csucoderlee3 小时前
Go语言指针的解引用和间接引用
开发语言·后端·golang
minos.cpp4 小时前
MacBook Pro(M1芯片)Qt环境配置
c++·ide·qt·macos·qt6.3
一丝晨光4 小时前
如何构建ObjC语言编译环境?构建无比简洁的clang编译ObjC环境?Windows搭建Swift语言编译环境?
linux·c语言·开发语言·windows·macos·objective-c·clang
喜欢猪猪4 小时前
基于 Java 开发的 MongoDB 企业级应用全解析
java·开发语言·mongodb
一丝晨光6 小时前
为什么会有函数调用参数带标签的写法?Swift函数调用的参数传递需要加前缀是否是冗余?函数调用?函数参数?
java·开发语言·c++·ios·c#·objective-c·swift
雾间云6 小时前
QT简单实现验证码(字符)
开发语言·qt
QQ27437851096 小时前
基于python热门歌曲采集分析系统
开发语言·python