天天看點

Qt 封裝HTTP網絡工具類HttpClient

作者:QT教程

一、前言

Qt 使用 QNetworkAccessManager 通路網絡,這裡對其進行了簡單的封裝,通路網絡的代碼可以簡化為:

// [[1]] GET 請求無參數
HttpClient("http://localhost:8080/device").success([](const QString &response) {
	qDebug() << response;
}).get();
           

更多的使用方法請參考 main() 裡的例子。HttpClient 的實作為 HttpClient.h 和 HttpClient.cpp 部分。

二、main.cpp

main() 函數裡展示了 HttpClient 的使用示例。

#include <QApplication>
#include <QNetworkAccessManager>
#include <QDebug>
#include <QFile>
#include "HttpClient.h"

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

// 在代碼塊裡執行網絡通路,是為了測試 HttpClient 對象在被析構後,網絡通路的回調函數仍然能正常執行
{

    // [[1]] GET 請求無參數
    HttpClient("http://localhost:8080/device").success([](const QString &response) {
        qDebug() << response;
    }).get();

    // [[2]] GET 請求有參數,有自定義 header
    HttpClient("http://localhost:8080/device").success([](const QString &response) {
        qDebug() << response;
    }).param("id", "1").param("name", "諸葛亮").header("token", "123AS#D").get();


    // [[3]] POST 請求有參數,有自定義 header
    HttpClient("http://localhost:8080/device").success([](const QString &response) {
        qDebug() << response;
    }).param("id", "2").param("name", "卧龍").header("token", "DER#2J7")
    .header("content-type", "application/x-www-form-urlencoded").post();

    // [[4]] 每建立一個 QNetworkAccessManager 對象都會建立一個線程,當頻繁的通路網絡時,為了節省線程資源,調用 useManager()
    // 使用共享的 QNetworkAccessManager,它不會被 HttpClient 删除。
    // 如果下面的代碼不傳入 QNetworkAccessManager,從任務管理器裡可以看到建立了幾千個線程。
    QNetworkAccessManager *manager = new QNetworkAccessManager();
    for (int i = 0; i < 5000; ++i) {
        HttpClient("http://localhost:8080/device").success([=](const QString &response) {
            qDebug() << response << ", " << i;
        }).manager(manager).get();
    }
}

    return a.exec();
}
           

三、HttpClient.h

#ifndef HTTPCLIENT_H
#define HTTPCLIENT_H

#include <functional>
#include <QMap>
#include <QVariant>
#include <QStringList>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QNetworkAccessManager>

class HttpClientPrivate;

/**
* 對 QNetworkAccessManager 簡單封裝的 HTTP 通路用戶端,簡化 GET、POST、PUT、DELETE、上傳、下載下傳等操作。
* 在執行請求前設定需要的參數和回調函數:
*     1. 調用 header() 設定請求頭
*     2. 調用 param() 設定參數,使用 Form 表單的方式送出請求,GET 請求的 query parameters 也可以用它設定
*     3. 調用 json() 設定 JSON 字元串的 request body,Content-Type 為 application/json,
*        當然也可以不是 JSON 格式,因使用 request body 的情況多數是使用 JSON 格式傳遞複雜對象,故命名為 json
*     4. 調用 success() 注冊請求成功的回調函數
*     5. 調用 fail() 注冊請求失敗的回調函數
*     6. 調用 complete() 注冊請求結束的回調函數
*        success(), fail(), complete() 的回調函數是可選的,根據需要注冊對應的回調函數,也可以一個都不注冊
* 然後根據請求的類型調用 get(), post(), put(), remove(), download(), upload() 執行 HTTP 請求
*
* 預設 HttpClient 會建立一個 QNetworkAccessManager,如果不想使用預設的,調用 manager() 傳入即可。
* 調用 debug(true) 設定為調試模式,輸出調試資訊如 URL、參數等。
*/
class HttpClient {
public:
    HttpClient(const QString &url);
    ~HttpClient();
    void stop2();

    /**
    * @brief 每建立一個 QNetworkAccessManager 對象都會建立一個線程,當頻繁的通路網絡時,為了節省線程資源,
    *     可以傳入 QNetworkAccessManager 給多個請求共享 (它不會被 HttpClient 删除,使用者需要自己手動删除)。
    *     如果沒有使用 manager() 傳入一個 QNetworkAccessManager,則 HttpClient 會自動的建立一個,并且在網絡通路完成後自動删除它。
    *
    * @param  manager 執行 HTTP 請求的 QNetworkAccessManager 對象
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& manager(QNetworkAccessManager *manager);

    /**
    * @brief  參數 debug 為 true 則使用 debug 模式,請求執行時輸出請求的 URL 和參數等
    *
    * @param  debug 是否啟用調試模式
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& debug(bool debug);

    /**
    * @brief 添加一個請求的參數,可以多次調用添加多個參數
    *
    * @param name  參數的名字
    * @param value 參數的值
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& param(const QString &name, const QVariant &value);

    /**
    * @brief 添加多個請求的參數
    *
    * @param ps QMap 類型的參數,key 為參數名,value 為參數值
    *           可以使用 {{"name", 1}, {"box", 2}} 的方式建立 QMap 對象
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& params(const QMap<QString, QVariant> &ps);

    /**
    * @brief 添加請求的參數 (請求體),使用 Json 格式,例如 "{\"name\": \"Alice\"}"
    *
    * @param json 請求體 (request body) 為 Json 格式的參數字元串
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& json(const QString &json);

    /**
    * @brief 添加請求頭
    *
    * @param name  請求頭的名字
    * @param value 請求頭的值
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& header(const QString &name, const QString &value);

    /**
    * @brief 添加多個請求頭
    *
    * @param nameValues 請求頭的名字和值對
    *                   可以使用 {{"name", 1}, {"box", 2}} 的方式建立 QMap 對象
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& headers(const QMap<QString, QString> nameValues);

    /**
    * @brief 注冊請求成功的回調函數
    *
    * @param successHandler 成功的回調函數,參數為響應的字元串
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& success(std::function<void(const QString &)> successHandler);

    /**
    * @brief 注冊請求失敗的回調函數
    *
    * @param failHandler 失敗的回調函數,參數為失敗原因和 HTTP 狀态碼
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& fail(std::function<void(const QString &, int)> failHandler);

    /**
    * @brief 注冊請求結束的回調函數,不管成功還是失敗請求結束後都會執行
    *
    * @param completeHandler 完成的回調函數,無參數
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& complete(std::function<void()> completeHandler);

    /**
    * @brief 設定請求響應的字元集,預設使用 UTF-8
    *
    * @param cs 字元集
    * @return 傳回 HttpClient 的引用,可以用于鍊式調用
    */
    HttpClient& charset(const QString &cs);

    /**
    * @brief 執行 GET 請求
    */
    void get();

    /**
    * @brief 執行 POST 請求
    */
    void post();

    /**
    * @brief 執行 PUT 請求
    */
    void put();

    /**
    * @brief 執行 DELETE 請求,由于 delete 是 C++ 的運算符,是以用同義詞 remove
    *        注意: Qt 提供的 DELETE 請求是不支援傳遞參數的,
    *        請參考 QNetworkAccessManager::deleteResource(const QNetworkRequest &request)
    */
    void remove();

    /**
    * @brief 使用 GET 進行下載下傳,下載下傳的檔案儲存到 savePath
    *
    * @param savePath 下載下傳的檔案儲存路徑
    */
    void download(const QString &savePath);

    /**
    * @brief 上傳單個檔案
    *        使用 POST 上傳,伺服器端擷取檔案的參數名為 file
    *
    * @param path 要上傳的檔案的路徑
    */
    void upload(const QString &path);

    /**
    * @brief 上傳檔案,檔案的内容已經讀取到 data 中
    *        使用 POST 上傳,伺服器端擷取檔案的參數名為 file
    *
    * @param path 要上傳的檔案的路徑
    */
    void upload(const QByteArray &data);

    /**
    * @brief 上傳多個檔案
    *        使用 POST 上傳,伺服器端擷取檔案的參數名為 files
    *
    * @param paths 要上傳的檔案的路徑
    */
    void upload(const QStringList &paths);

private:
    HttpClientPrivate *d;
};

#endif // HTTPCLIENT_H
           

四、HttpClient.cpp

#include "HttpClient.h"

#include <QDebug>
#include <QFile>
#include <QHash>
#include <QUrlQuery>
#include <QHttpPart>
#include <QHttpMultiPart>

/*-----------------------------------------------------------------------------|
|                              HttpClientPrivate                              |
|----------------------------------------------------------------------------*/
/**
* @brief 請求的類型
*
* 注: UPLOAD 不是 HTTP Method,隻是為了上傳時對請求進行特殊處理而定義的
*/
enum class HttpClientRequestMethod {
    GET, POST, PUT, DELETE, UPLOAD
};

/**
* @brief 緩存 HttpClientPrivate 的資料成員,友善在異步 lambda 中使用 = 以值的方式通路。
*/
class HttpClientPrivateCache {
public:
    std::function<void(const QString &)>      successHandler = nullptr;
    std::function<void(const QString &, int)>    failHandler = nullptr;
    std::function<void()>                    completeHandler = nullptr;
    bool debug = false;
    bool internal = false;
    QString charset;
    QNetworkAccessManager* manager = nullptr;
};

/**
* @brief HttpClient 的輔助類,封裝不希望暴露給用戶端的資料和方法,使得 HttpClient 隻暴露必要的 API 給用戶端。
*/
class HttpClientPrivate {
    friend class HttpClient;

    HttpClientPrivate(const QString &url);
    ~HttpClientPrivate();
    void stop1();

    /**
    * @brief 緩存 HttpClientPrivate 的資料成員
    *
    * @return 傳回 HttpClientPrivateCache 緩存對象
    */
    HttpClientPrivateCache cache();

    /**
    * @brief 擷取 Manager,如果傳入了 manager 則傳回此 manager,否則新建立一個 manager,預設會自動建立一個 manager,
    *        使用傳入的 manager 則 interval 被設定為 false,自動建立的 manager 則設定 interval 為 true
    *
    * @return 傳回 QNetworkAccessManager 對象
    */
    QNetworkAccessManager* getManager();

    /**
    * @brief 使用使用者設定的 URL、請求頭、參數等建立 Request
    *
    * @param d      HttpClientPrivate 的對象
    * @param method 請求的類型
    * @return 傳回可用于執行請求的 QNetworkRequest
    */
    static QNetworkRequest createRequest(HttpClientPrivate *d, HttpClientRequestMethod method);

    /**
    * @brief 執行請求的輔助函數
    *
    * @param d      HttpClientPrivate 的對象
    * @param method 請求的類型
    */
    static void executeQuery(HttpClientPrivate *d, HttpClientRequestMethod method);

    /**
    * @brief 上傳檔案或者資料
    *
    * @param d     HttpClientPrivate 的對象
    * @param paths 要上傳的檔案的路徑(path 和 data 不能同時使用)
    * @param data  要上傳的檔案的資料
    */
    static void upload(HttpClientPrivate *d, const QStringList &paths, const QByteArray &data);

    /**
    * @brief 使用 GET 進行下載下傳,下載下傳的檔案儲存到 savePath
    *
    * @param d        HttpClientPrivate 的對象
    * @param savePath 下載下傳的檔案儲存路徑
    */
    static void download(HttpClientPrivate *d, const QString &savePath);

    /**
    * @brief 使用 GET 進行下載下傳,當有資料可讀取時回調 readyRead(), 大多數情況下應該在 readyRead() 裡把資料儲存到檔案
    *
    * @param readyRead 有資料可讀取時的回調 lambda 函數
    */
    static void download(HttpClientPrivate *d, std::function<void(const QByteArray &)> readyRead);

    /**
    * @brief 讀取伺服器響應的資料
    *
    * @param reply   請求的 QNetworkReply 對象
    * @param charset 請求響應的字元集,預設使用 UTF-8
    * @return 傳回伺服器端響應的字元串
    */
    static QString readReply(QNetworkReply *reply, const QString &charset = "UTF-8");

    /**
    * @brief 請求結束的處理函數
    *
    * @param cache          HttpClientPrivateCache 緩存對象
    * @param reply          QNetworkReply 對象,不能為 NULL
    * @param successMessage 請求成功的消息
    * @param failMessage    請求失敗的消息
    */
    static void handleFinish(HttpClientPrivateCache cache, QNetworkReply *reply, const QString &successMessage, const QString &failMessage);

    /////////////////////////////////////////////////// 成員變量 //////////////////////////////////////////////
    QString   url;                            // 請求的 URL
    QString   json;                           // 請求的參數使用 Json 格式
    QUrlQuery params;                         // 請求的參數使用 Form 格式
    QString   charset = "UTF-8";              // 請求響應的字元集
    QHash<QString, QString> headers;          // 請求頭
    QNetworkAccessManager *manager = nullptr; // 執行 HTTP 請求的 QNetworkAccessManager 對象
    bool useJson = false;                    // 為 true 時請求使用 Json 格式傳遞參數,否則使用 Form 格式傳遞參數
    bool debug = false;                    // 為 true 時輸出請求的 URL 和參數
    bool internal = true;                     // 是否使用自動建立的 manager

    std::function<void(const QString &)>   successHandler = nullptr; // 成功的回調函數,參數為響應的字元串
    std::function<void(const QString &, int)> failHandler = nullptr; // 失敗的回調函數,參數為失敗原因和 HTTP status code
    std::function<void()>                 completeHandler = nullptr; // 結束的回調函數,無參數
};

HttpClientPrivate::HttpClientPrivate(const QString &url) : url(url) { }

HttpClientPrivate::~HttpClientPrivate() {
    manager = nullptr;
    successHandler = nullptr;
    failHandler = nullptr;
    completeHandler = nullptr;
}

void HttpClientPrivate::stop1()
{
    manager->deleteLater();
}

// 緩存 HttpClientPrivate 的資料成員
HttpClientPrivateCache HttpClientPrivate::cache() {
    HttpClientPrivateCache cache;

    cache.successHandler = successHandler;
    cache.failHandler = failHandler;
    cache.completeHandler = completeHandler;
    cache.debug = debug;
    cache.internal = internal;
    cache.charset = charset;
    cache.manager = getManager();

    return cache;
}

// 執行請求的輔助函數
void HttpClientPrivate::executeQuery(HttpClientPrivate *d, HttpClientRequestMethod method) {
    // 1. 緩存需要的變量,在 lambda 中使用 = 捕獲進行值傳遞 (不能使用引用 &,因為 d 已經被析構)
    // 2. 建立請求需要的變量
    // 3. 根據 method 執行不同的請求
    // 4. 請求結束時擷取響應資料,在 handleFinish 中執行回調函數

    // [1] 緩存需要的變量,在 lambda 中使用 = 捕獲進行值傳遞 (不能使用引用 &,因為 d 已經被析構)
    HttpClientPrivateCache cache = d->cache();

    // [2] 建立請求需要的變量
    QNetworkRequest request = HttpClientPrivate::createRequest(d, method);
    QNetworkReply    *reply = nullptr;

    // [3] 根據 method 執行不同的請求
    switch (method) {
    case HttpClientRequestMethod::GET:
        reply = cache.manager->get(request);
        break;
    case HttpClientRequestMethod::POST:
        reply = cache.manager->post(request, d->useJson ? d->json.toUtf8() : d->params.toString(QUrl::FullyEncoded).toUtf8());
        break;
    case HttpClientRequestMethod::PUT:
        reply = cache.manager->put(request, d->useJson ? d->json.toUtf8() : d->params.toString(QUrl::FullyEncoded).toUtf8());
        break;
    case HttpClientRequestMethod::DELETE:
        reply = cache.manager->deleteResource(request);
        break;
    default:
        break;
    }

    // [4] 請求結束時擷取響應資料,在 handleFinish 中執行回調函數
    // 請求結束時一次性讀取所有響應資料
    QObject::connect(reply, &QNetworkReply::finished, [=] {
        QString successMessage = HttpClientPrivate::readReply(reply, cache.charset.toUtf8());
        QString failMessage = reply->errorString();
        HttpClientPrivate::handleFinish(cache, reply, successMessage, failMessage);
    });
}

// 使用 GET 進行下載下傳,下載下傳的檔案儲存到 savePath
void HttpClientPrivate::download(HttpClientPrivate *d, const QString &savePath) {
    // 1. 打開下載下傳檔案,如果打開檔案出錯,不進行下載下傳
    // 2. 給請求結束的回調函數注入關閉釋放檔案的行為
    // 3. 調用下載下傳的重載函數開始下載下傳
    QFile *file = new QFile(savePath);

    // [1] 打開下載下傳檔案,如果打開檔案出錯,不進行下載下傳
    if (!file->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
        file->close();
        file->deleteLater();

        if (d->debug) {
            qDebug().noquote() << QString("[錯誤] 打開檔案出錯: %1").arg(savePath);
        }

        if (nullptr != d->failHandler) {
            d->failHandler(QString("[錯誤] 打開檔案出錯: %1").arg(savePath), -1);
        }

        return;
    }

    // [2] 給請求結束的回調函數注入關閉釋放檔案的行為
    std::function<void()> userCompleteHandler = d->completeHandler;
    std::function<void()> injectedCompleteHandler = [=]() {
        // 請求結束後釋放檔案對象
        file->flush();
        file->close();
        file->deleteLater();

        // 執行使用者指定的結束回調函數
        if (nullptr != userCompleteHandler) {
            userCompleteHandler();
        }
    };
    d->completeHandler = injectedCompleteHandler;

    // [3] 調用下載下傳的重載函數開始下載下傳
    HttpClientPrivate::download(d, [=](const QByteArray &data) {
        file->write(data);
    });
}

// 使用 GET 進行下載下傳,當有資料可讀取時回調 readyRead(), 大多數情況下應該在 readyRead() 裡把資料儲存到檔案
void HttpClientPrivate::download(HttpClientPrivate *d, std::function<void(const QByteArray &)> readyRead) {
    // 1. 緩存需要的變量,在 lambda 中使用 = 捕獲進行值傳遞 (不能使用引用 &,因為 d 已經被析構)
    // 2. 建立請求需要的變量,執行請求
    // 3. 有資料可讀取時回調 readyRead()
    // 4. 請求結束時擷取響應資料,在 handleFinish 中執行回調函數

    // [1] 緩存需要的變量,在 lambda 中使用 = 捕捉使用 (不能使用引用 &,因為 d 已經被析構)
    HttpClientPrivateCache cache = d->cache();

    // [2] 建立請求需要的變量,執行請求
    QNetworkRequest request = HttpClientPrivate::createRequest(d, HttpClientRequestMethod::GET);
    QNetworkReply    *reply = cache.manager->get(request);

    // [3] 有資料可讀取時回調 readyRead()
    QObject::connect(reply, &QNetworkReply::readyRead, [=] {
        readyRead(reply->readAll());
    });

    // [4] 請求結束時擷取響應資料,在 handleFinish 中執行回調函數
    QObject::connect(reply, &QNetworkReply::finished, [=] {
        QString successMessage = "下載下傳完成"; // 請求結束時一次性讀取所有響應資料
        QString failMessage = reply->errorString();
        HttpClientPrivate::handleFinish(cache, reply, successMessage, failMessage);
    });
}

// 上傳檔案或者資料的實作
void HttpClientPrivate::upload(HttpClientPrivate *d, const QStringList &paths, const QByteArray &data) {
    // 1. 緩存需要的變量,在 lambda 中使用 = 捕獲進行值傳遞 (不能使用引用 &,因為 d 已經被析構)
    // 2. 建立 Form 表單的參數 Text Part
    // 3. 建立上傳的 File Part
    //    3.1 使用檔案建立 File Part
    //    3.2 使用資料建立 File Part
    // 4. 建立請求需要的變量,執行請求
    // 5. 請求結束時釋放 multiPart 和打開的檔案,擷取響應資料,在 handleFinish 中執行回調函數

    // [1] 緩存需要的變量,在 lambda 中使用 = 捕捉使用 (不能使用引用 &,因為 d 已經被析構)
    HttpClientPrivateCache cache = d->cache();

    // [2] 建立 Form 表單的參數 Text Part
    QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
    QList<QPair<QString, QString> > paramItems = d->params.queryItems();
    for (int i = 0; i < paramItems.size(); ++i) {
        QString name = paramItems.at(i).first;
        QString value = paramItems.at(i).second;

        QHttpPart textPart;
        textPart.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"%1\"").arg(name));
        textPart.setBody(value.toUtf8());
        multiPart->append(textPart);
    }

    if (paths.size() > 0) {
        // [3.1] 使用檔案建立 File Part
        QString inputName = paths.size() == 1 ? "file" : "files"; // 一個檔案時為 file,多個檔案時為 files

        for (const QString &path : paths) {
            // path 為空時,不上傳檔案
            if (path.isEmpty()) {
                continue;
            }

            // We cannot delete the file now, so delete it with the multiPart
            QFile *file = new QFile(path, multiPart);

            // 如果檔案打開失敗,則釋放資源傳回,終止上傳
            if (!file->open(QIODevice::ReadOnly)) {
                QString failMessage = QString("打開檔案失敗[%2]: %1").arg(path).arg(file->errorString());

                if (cache.debug) {
                    qDebug().noquote() << failMessage;
                }

                if (nullptr != cache.failHandler) {
                    cache.failHandler(failMessage, -1);
                }

                multiPart->deleteLater();
                return;
            }

            // 單個檔案時,name 為伺服器端擷取檔案的參數名,為 file
            // 多個檔案時,name 為伺服器端擷取檔案的參數名,為 files
            // 注意: 伺服器是 Java 的則用 form-data
            // 注意: 伺服器是 PHP  的則用 multipart/form-data
            QString disposition = QString("form-data; name=\"%1\"; filename=\"%2\"").arg(inputName).arg(file->fileName());
            QHttpPart filePart;
            filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(disposition));
            filePart.setBodyDevice(file);
            multiPart->append(filePart);
        }
    }
    else {
        // [3.2] 使用資料建立 File Part
        QString   disposition = QString("form-data; name=\"file\"; filename=\"no-name\"");
        QHttpPart dataPart;
        dataPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(disposition));
        dataPart.setBody(data);
        multiPart->append(dataPart);
    }

    // [4] 建立請求需要的變量,執行請求
    QNetworkRequest request = HttpClientPrivate::createRequest(d, HttpClientRequestMethod::UPLOAD);
    QNetworkReply    *reply = cache.manager->post(request, multiPart);

    // [5] 請求結束時釋放 multiPart 和檔案,擷取響應資料,在 handleFinish 中執行回調函數
    QObject::connect(reply, &QNetworkReply::finished, [=] {
        multiPart->deleteLater(); // 釋放資源: multiPart + file

        QString successMessage = HttpClientPrivate::readReply(reply, cache.charset); // 請求結束時一次性讀取所有響應資料
        QString failMessage = reply->errorString();
        HttpClientPrivate::handleFinish(cache, reply, successMessage, failMessage);
    });
}

// 擷取 Manager,如果傳入了 manager 則傳回此 manager,否則新建立一個 manager,預設會自動建立一個 manager
QNetworkAccessManager* HttpClientPrivate::getManager() {
    return internal ? new QNetworkAccessManager() : manager;
}

// 使用使用者設定的 URL、請求頭、參數等建立 Request
QNetworkRequest HttpClientPrivate::createRequest(HttpClientPrivate *d, HttpClientRequestMethod method) {
    // 1. 如果是 GET 請求,并且參數不為空,則編碼請求的參數,放到 URL 後面
    // 2. 調試時輸出網址和參數
    // 3. 設定 Content-Type
    // 4. 添加請求頭到 request 中

    bool get = method == HttpClientRequestMethod::GET;
    bool upload = method == HttpClientRequestMethod::UPLOAD;
    bool withForm = !get && !upload && !d->useJson; // PUT、POST 或者 DELETE 請求,且 useJson 為 false
    bool withJson = !get && !upload &&  d->useJson; // PUT、POST 或者 DELETE 請求,且 useJson 為 true

                                                    // [1] 如果是 GET 請求,并且參數不為空,則編碼請求的參數,放到 URL 後面
    if (get && !d->params.isEmpty()) {
        d->url += "?" + d->params.toString(QUrl::FullyEncoded);
    }

    // [2] 調試時輸出網址和參數
    if (d->debug) {
        qDebug().noquote() << "[網址]" << d->url;

        if (withJson) {
            qDebug().noquote() << "[參數]" << d->json;
        }
        else if (withForm || upload) {
            QList<QPair<QString, QString> > paramItems = d->params.queryItems();
            QString buffer; // 避免多次調用 qDebug() 輸入調試資訊,每次 qDebug() 都有可能輸出行号等

                            // 按鍵值對的方式輸出參數
            for (int i = 0; i < paramItems.size(); ++i) {
                QString name = paramItems.at(i).first;
                QString value = paramItems.at(i).second;

                if (0 == i) {
                    buffer += QString("[參數] %1=%2\n").arg(name).arg(value);
                }
                else {
                    buffer += QString("       %1=%2\n").arg(name).arg(value);
                }
            }

            if (!buffer.isEmpty()) {
                qDebug().noquote() << buffer;
            }
        }
    }

    // [3] 設定 Content-Type
    // 如果是 POST 請求,useJson 為 true 時添加 Json 的請求頭,useJson 為 false 時添加 Form 的請求頭
    if (withForm) {
        d->headers["Content-Type"] = "application/x-www-form-urlencoded";
    }
    else if (withJson) {
        d->headers["Content-Type"] = "application/json; charset=utf-8";
    }

    // [4] 添加請求頭到 request 中
    QNetworkRequest request(QUrl(d->url));
    for (auto i = d->headers.cbegin(); i != d->headers.cend(); ++i) {
        request.setRawHeader(i.key().toUtf8(), i.value().toUtf8());
    }

    return request;
}

// 讀取伺服器響應的資料
QString HttpClientPrivate::readReply(QNetworkReply *reply, const QString &charset) {
    QTextStream in(reply);
    QString result;
    in.setCodec(charset.toUtf8());

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

    return result;
}

// 請求結束的處理函數
void HttpClientPrivate::handleFinish(HttpClientPrivateCache cache, QNetworkReply *reply, const QString &successMessage, const QString &failMessage) {
    // 1. 執行請求成功的回調函數
    // 2. 執行請求失敗的回調函數
    // 3. 執行請求結束的回調函數
    // 4. 釋放 reply 和 manager 對象

    if (reply->error() == QNetworkReply::NoError) {
        if (cache.debug) {
            qDebug().noquote() << QString("[結束] 成功: %1").arg(successMessage);
        }

        // [1] 執行請求成功的回調函數
        if (nullptr != cache.successHandler) {
            cache.successHandler(successMessage);
        }
    }
    else {
        if (cache.debug) {
            qDebug().noquote() << QString("[結束] 失敗: %1").arg(failMessage);
        }

        // [2] 執行請求失敗的回調函數
        if (nullptr != cache.failHandler) {
            cache.failHandler(failMessage, reply->error());
        }
    }

    // [3] 執行請求結束的回調函數
    if (nullptr != cache.completeHandler) {
        cache.completeHandler();
    }

    // [4] 釋放 reply 和 manager 對象
    if (nullptr != reply) {
        reply->deleteLater();
    }

    if (cache.internal && nullptr != cache.manager) {
        cache.manager->deleteLater();
    }
}

/*-----------------------------------------------------------------------------|
|                                 HttpClient                                  |
|----------------------------------------------------------------------------*/

// 注意: 在異步請求中 HttpClient 的 HttpClientPrivate 成員變量 d 已經被析構,是以需要先緩存相關變量為棧對象,使用 = 以值的方式通路
HttpClient::HttpClient(const QString &url) : d(new HttpClientPrivate(url)) { }

HttpClient::~HttpClient() {
    delete d;
}

void HttpClient::stop2()
{
    d->stop1();
}

// 傳入 QNetworkAccessManager 給多個請求共享
HttpClient& HttpClient::manager(QNetworkAccessManager *manager) {
    d->manager = manager;
    d->internal = (nullptr == manager);

    return *this;
}

// 傳入 debug 為 true 則使用 debug 模式,請求執行時輸出請求的 URL 和參數等
HttpClient& HttpClient::debug(bool debug) {
    d->debug = debug;

    return *this;
}

// 添加一個請求的參數,可以多次調用添加多個參數
HttpClient& HttpClient::param(const QString &name, const QVariant &value) {
    d->params.addQueryItem(name, value.toString());

    return *this;
}

// 添加多個請求的參數
HttpClient& HttpClient::params(const QMap<QString, QVariant> &ps) {
    for (auto iter = ps.cbegin(); iter != ps.cend(); ++iter) {
        d->params.addQueryItem(iter.key(), iter.value().toString());
    }

    return *this;
}

// 添加請求的參數 (請求體),使用 Json 格式,例如 "{\"name\": \"Alice\"}"
HttpClient& HttpClient::json(const QString &json) {
    d->json = json;
    d->useJson = true;

    return *this;
}

// 添加請求頭
HttpClient& HttpClient::header(const QString &name, const QString &value) {
    d->headers[name] = value;

    return *this;
}

// 添加多個請求頭
HttpClient& HttpClient::headers(const QMap<QString, QString> nameValues) {
    for (auto i = nameValues.cbegin(); i != nameValues.cend(); ++i) {
        d->headers[i.key()] = i.value();
    }

    return *this;
}

// 注冊請求成功的回調函數
HttpClient& HttpClient::success(std::function<void(const QString &)> successHandler) {
    d->successHandler = successHandler;

    return *this;
}

// 注冊請求失敗的回調函數
HttpClient& HttpClient::fail(std::function<void(const QString &, int)> failHandler) {
    d->failHandler = failHandler;

    return *this;
}

// 注冊請求結束的回調函數,不管成功還是失敗都會執行
HttpClient& HttpClient::complete(std::function<void()> completeHandler) {
    d->completeHandler = completeHandler;

    return *this;
}

// 設定請求響應的編碼
HttpClient& HttpClient::charset(const QString &cs) {
    d->charset = cs;

    return *this;
}

// 執行 GET 請求
void HttpClient::get() {
    HttpClientPrivate::executeQuery(d, HttpClientRequestMethod::GET);
}

// 執行 POST 請求
void HttpClient::post() {
    HttpClientPrivate::executeQuery(d, HttpClientRequestMethod::POST);
}

// 執行 PUT 請求
void HttpClient::put() {
    HttpClientPrivate::executeQuery(d, HttpClientRequestMethod::PUT);
}

// 執行 DELETE 請求
void HttpClient::remove() {
    HttpClientPrivate::executeQuery(d, HttpClientRequestMethod::DELETE);
}

// 使用 GET 進行下載下傳,下載下傳的檔案儲存到 savePath
void HttpClient::download(const QString &savePath) {
    HttpClientPrivate::download(d, savePath);
}

// 上傳檔案
void HttpClient::upload(const QString &path) {
    QStringList paths = { path };
    HttpClientPrivate::upload(d, paths, QByteArray());
}

// 上傳檔案,檔案的内容以及讀取到 data 中
void HttpClient::upload(const QByteArray &data) {
    HttpClientPrivate::upload(d, QStringList(), data);
}

// 上傳多個檔案
void HttpClient::upload(const QStringList &paths) {
    HttpClientPrivate::upload(d, paths, QByteArray());
}
           

參考:

Qt 通路網絡的 HttpClient

Qt 通路網絡的 HttpClient(封裝QNetworkAccessManager,且有服務端)

點選領取Qt學習資料+視訊教程~Qt開發(視訊教程+文檔+代碼+項目實戰)

繼續閱讀