Qt 访问网络

一、前言

Qt 中访问网络使用 QNetworkAccessManager,它的 API 是异步的,这样在访问网络的时候不需要启动一个线程,在线程里执行请求的代码。(但这一点在有时候需要阻塞时就是个麻烦了)

需要注意一点的是,请求响应的对象 QNetworkReply 需要我们自己手动的删除,一般都会在 QNetworkAccessManager::finished 信号的曹函数里使用 reply->deleteLater() 删除,不要直接 delete reply

二、示例一

#include <QDebug>
#include <QApplication>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QNetworkAccessManager *manager = new QNetworkAccessManager();
    QNetworkRequest request(QUrl("http://www.baidu.com"));
    QNetworkReply *reply = manager->get(request);

    int count = 0;

    QObject::connect(reply, &QNetworkReply::readyRead, [&] {
        qDebug() << QString(reply->readAll());
        qDebug() << ++count;
    });
    
    // 请求错误处理
    QObject::connect(reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), [&] {
        qDebug() << reply->errorString();
    });

    // 请求结束时删除 reply 释放内存
    QObject::connect(reply, &QNetworkReply::finished, [&] {
        reply->deleteLater();
    });

    return app.exec();
}

仔细观察上面程序的输出结果,由于返回的数据比较大,readyRead 被调用了多次,而不是一次性就得到了请求的响应数据,这个特点在某些情况下很有用,例如下载 100M 的文件,多次读取肯定是合适的,因为读取后数据就会从 reply 中删除,不会导致占用太多内存。

但是在某些情况下却不太好用,例如读取一个响应 JSON 的数据,一般都不会太大,大的也就几十上百 K,如果一次得不到 JSON 的全部数据,多次读取的情况下想要拼出一个完整的 JSON 字符串不太容易,这时如果能一次性的得到响应的 JSON 数据是不是就很方便了呢?

三、示例二

要一次性读取到响应的数据可以在 QNetworkReply::finished 信号处理中进行,如下:

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QNetworkAccessManager *manager = new QNetworkAccessManager();
    QNetworkRequest request(QUrl("http://www.baidu.com"));
    QNetworkReply *reply = manager->get(request);

    // 请求错误处理
    QObject::connect(reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), [&] {
        qDebug() << reply->errorString();
    });
    
    // 请求结束时一次性读取所有响应数据
    QObject::connect(reply, &QNetworkReply::finished, [&] {
        if (reply->error() == QNetworkReply::NoError) {
            qDebug() << reply->readAll();
        }

        reply->deleteLater();
    });

    return app.exec();
}

四、封装HTTP网络工具类

观察上面的程序,会发现很多代码都是重复的模版代码,例如

  • 创建 QNetworkRequest
  • 获取 QNetworkReply
  • 删除 QNetworkReply
  • 错误处理
  • 响应处理

大量的模版代码可以把它们封装成一个工具类,方便使用,参考下面 main() 函数里的调用,代码一下子看上去就清晰简单了很多。

main.cpp

#include "NetworkUtil.h"

#include <QDebug>
#include <QApplication>
#include <QNetworkAccessManager>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QNetworkAccessManager *manager = new QNetworkAccessManager();

    // 访问 baidu
    NetworkUtil::get(manager, "http://www.baidu.com", [](const QString &response) {
        qDebug() << response;
    });  // 默认的异步方式

    // 访问 163
    NetworkUtil::get(manager, "http://www.163.com", [](const QString &response) {
        qDebug() << response;
    }, NULL, true, "GB2312"); // 同步方式

    return app.exec();
}

NetworkUtil.h

#ifndef NETWORKUTIL_H
#define NETWORKUTIL_H

#include <functional>

class QString;
class QNetworkAccessManager;

class NetworkUtil {
public:
    /**
     * @brief 使用 GET 访问网络
     *
     * @param manager QNetworkAccessManager 对象
     * @param url 需要访问的 URL
     * @param successHandler 访问成功的 Lambda 回调函数
     * @param errorHandler 访问失败的 Lambda 回调函数
     * @param async 默认为异步方式,false 以设置为同步(阻塞式)方式
     * @param encoding 读取响应数据的编码
     */
    static void get(QNetworkAccessManager *manager,
                    const QString &url,
                    std::function<void (const QString &)> successHandler,
                    std::function<void (const QString &)> errorHandler = NULL,
                    const bool &async = true,
                    const char *encoding = "UTF-8");
};

#endif // NETWORKUTIL_H

NetworkUtil.cpp

#include "NetworkUtil.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QTextStream>
#include <QEventLoop>

void NetworkUtil::get(QNetworkAccessManager *manager,
                      const QString &url,
                      std::function<void (const QString &)> successHandler,
                      std::function<void (const QString &)> errorHandler,
                      const bool &async,
                      const char *encoding) {
    QUrl urlx(url);
    QNetworkRequest request(urlx);
    QNetworkReply *reply = manager->get(request);

    // 请求错误处理
    QObject::connect(reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), [=] {
        if (NULL != errorHandler) {
            errorHandler(reply->errorString());
        }
    });

    // 请求结束时一次性读取所有响应数据
    QObject::connect(reply, &QNetworkReply::finished, [=] {
        if (reply->error() == QNetworkReply::NoError) {
            // 读取响应数据
            QTextStream in(reply);
            QString result;

            in.setCodec(encoding);
            while (!in.atEnd()) {
                result += in.readLine();
            }

            successHandler(result);
        }

        reply->deleteLater();
    });

    // 异步,还是同步阻塞
    if(!async) {
        // 使用QEventLoop阻塞
        QEventLoop eventLoop;
        QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);
        eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
    }
}

Qt 的网络操作类是异步(非阻塞的),所以这里使用 QEventLoop 来手动阻塞以实现同步。

这里暂时只介绍了一次性读取时 Get 的封装,后面考虑实现 Get 多次读取的封装,Post 请求的封装等。


参考:

Qt 访问网络

Qt实现同步(阻塞式)http get等网络访问操作


posted @ 2021-04-30 15:46  fengMisaka  阅读(1592)  评论(0编辑  收藏  举报