目标读者:已经看完前 1--6 期、具备一定 C++ 和 Qt 开发经验,希望在桌面项目中把"调用 HTTP/REST 接口"这一块设计规范、稳定好用的工程师。
开发环境示例:Qt 5.12 / 5.15 + Qt Creator + CMake(文中工程均以 CMake 为例,可自行改为 qmake)。
目录
[一、问题背景:从「能发一个 GET」到「能撑住线上场景」](#一、问题背景:从「能发一个 GET」到「能撑住线上场景」)
[1. UI 卡顿:网络一慢,整个界面跟着「休克」](#1. UI 卡顿:网络一慢,整个界面跟着「休克」)
[2. QNetworkAccessManager 到处 new,一会儿就「不知道谁在管网络」](#2. QNetworkAccessManager 到处 new,一会儿就「不知道谁在管网络」)
[3. 没有统一的超时和错误处理:接口挂住,全应用跟着「半死不活」](#3. 没有统一的超时和错误处理:接口挂住,全应用跟着「半死不活」)
[4. HTTPS / 证书问题:开发阶段一把 ignoreSslErrors(),上线埋雷](#4. HTTPS / 证书问题:开发阶段一把 ignoreSslErrors(),上线埋雷)
[5. 请求一多,就分不清「哪个响应对应哪个请求」](#5. 请求一多,就分不清「哪个响应对应哪个请求」)
[二、核心知识点:把 QNetworkAccessManager 真正「用对」](#二、核心知识点:把 QNetworkAccessManager 真正「用对」)
[1. Manager:少而精,长寿命](#1. Manager:少而精,长寿命)
[2. QNetworkRequest:配置一次,少填坑](#2. QNetworkRequest:配置一次,少填坑)
[3. QNetworkReply:记得收尾和错误分流](#3. QNetworkReply:记得收尾和错误分流)
[4. 超时管理:QNetworkReply 自身没有「超时」的概念](#4. 超时管理:QNetworkReply 自身没有「超时」的概念)
[5. HTTPS 与 SSL 错误:别一刀切全部 ignore](#5. HTTPS 与 SSL 错误:别一刀切全部 ignore)
[6. 请求队列与并发控制:让 QNetworkAccessManager「不乱」](#6. 请求队列与并发控制:让 QNetworkAccessManager「不乱」)
[三、完整工程实战:GitHub REST API 小客户端](#三、完整工程实战:GitHub REST API 小客户端)
[1. 工程结构](#1. 工程结构)
[2. CMakeLists.txt(可直接使用)](#2. CMakeLists.txt(可直接使用))
[3. RequestInfo:封装一次 HTTP 调用的参数与结果](#3. RequestInfo:封装一次 HTTP 调用的参数与结果)
[4. NetworkManager:统一管理 QNetworkAccessManager](#4. NetworkManager:统一管理 QNetworkAccessManager)
[5. ApiClient:面向业务的 REST 客户端](#5. ApiClient:面向业务的 REST 客户端)
[6. MainWindow:一个简单的 GitHub 客户端界面](#6. MainWindow:一个简单的 GitHub 客户端界面)
[7. main.cpp:程序入口](#7. main.cpp:程序入口)
[四、实战中的坑与优化:结合这个 Demo 再看几条经验](#四、实战中的坑与优化:结合这个 Demo 再看几条经验)
[1. 把「请求发起」和「结果处理」分开](#1. 把「请求发起」和「结果处理」分开)
[2. 不要在 UI 层直接处理 QNetworkReply](#2. 不要在 UI 层直接处理 QNetworkReply)
[3. 超时不是「可选项」,而是「必需项」](#3. 超时不是「可选项」,而是「必需项」)
[4. 日志与调试](#4. 日志与调试)
[五、小结:项目中可落地的 Qt 网络访问实践清单](#五、小结:项目中可落地的 Qt 网络访问实践清单)
一、问题背景:从「能发一个 GET」到「能撑住线上场景」
很多人第一次在 Qt 里发 HTTP 请求,大概都是下面这种写法:
cpp
QNetworkAccessManager mgr;
QNetworkRequest req(QUrl("https://api.example.com/data"));
QNetworkReply *reply = mgr.get(req);
QByteArray data = reply->readAll(); // 甚至直接这样同步用
能不能跑?能。但一旦放进真实项目,就会暴露出一堆问题。
1. UI 卡顿:网络一慢,整个界面跟着「休克」
常见现象:
- 点一个「同步」按钮后,窗口直接假死几十秒;
- 鼠标拖动窗口边框不跟手,甚至无法关闭窗口;
- 明明只是请求个接口,用户感觉像程序崩溃了一样。
本质上还是把网络 I/O 写成了「同步逻辑」:
cpp
// 各种 waitFor... / 循环 + processEvents 之类的同步逻辑
reply->waitForReadyRead(); // 或者 while(!reply->isFinished()) { ... }
这在 Demo 里或内网接口上看不出问题,一到弱网或偶发抖动的环境,体验就会崩。
2. QNetworkAccessManager 到处 new,一会儿就「不知道谁在管网络」
我见过不少类似的代码:
cpp
void SomeDialog::doRequest()
{
auto *mgr = new QNetworkAccessManager; // 没有 parent
connect(mgr, &QNetworkAccessManager::finished, this, &SomeDialog::handleReply);
mgr->get(QNetworkRequest(QUrl("https://...")));
}
跑起来之后:
- 请求确实发出去了,也有响应;
- 但
mgr一直没人 delete,进程内存慢慢爬; - 多个模块各自 new 一个 Manager,连接复用不好、Cookie 管理混乱。
QNetworkAccessManager 本身设计的思路是「少量长寿命实例」,而不是每发一次请求就 new 一次。
3. 没有统一的超时和错误处理:接口挂住,全应用跟着「半死不活」
典型写法是:
- 只连了
finished(),没处理errorOccurred(); - 没有专门的超时逻辑,只能等网络栈自己放弃(可能是几十秒甚至更久);
- 业务代码里只知道「没回调」,很难区分是超时、DNS 失败,还是 TLS 出问题。
一旦后台某个接口出现问题,前台就会出现长时间「转圈圈」,用户完全不知道发生了什么。
4. HTTPS / 证书问题:开发阶段一把 ignoreSslErrors(),上线埋雷
不少人遇到自签名证书 / 企业代理抓 HTTPS 时,第一反应是:
cpp
connect(reply, &QNetworkReply::sslErrors,
this, [](const QList<QSslError> &errors){
Q_UNUSED(errors);
reply->ignoreSslErrors();
});
开发阶段看起来很「爽」,什么都不挡。但真实发布时:
- 很可能绕过了本该生效的安全校验;
- 遇到环境里 TLS 配置不一致时,又会变成「某些机器老是报错」的玄学问题。
5. 请求一多,就分不清「哪个响应对应哪个请求」
例如:
cpp
connect(manager, &QNetworkAccessManager::finished,
this, &MainWindow::onReplyFinished);
在槽函数里,只能通过 qobject_cast<QNetworkReply*>(sender()) 判断是谁,但:
- 如果你同时请求了「用户信息、仓库列表、Issue 列表」等多个接口;
- 不做额外封装,槽函数就会变成一个大号 switch-case,极不利于维护。
这一期,我们的目标不是「教你发一个 GET/POST」,而是搭一整套可以直接迁入项目的网络访问小框架,解决:
- QNetworkAccessManager 的生命周期和复用;
- 请求的封装、队列和并发控制;
- 超时与错误处理;
- 简单的 REST API 封装(以 GitHub API 为例);
- 在 UI 中清晰展示请求状态和错误信息。
二、核心知识点:把 QNetworkAccessManager 真正「用对」
1. Manager:少而精,长寿命
建议:整个应用中,按「业务域」划分 QNetworkAccessManager,而不是「一个请求一个 Manager」。
典型做法:
- 应用层提供一个 NetworkManager 单例;
- 内部持有一个或少量 QNetworkAccessManager;
- 所有请求都通过这个单例调度,方便统一配置(代理、Cookie、SSL 策略等)。
cpp
class NetworkManager : public QObject
{
Q_OBJECT
public:
static NetworkManager *instance();
QNetworkAccessManager *manager() { return &m_mgr; }
private:
explicit NetworkManager(QObject *parent = nullptr);
QNetworkAccessManager m_mgr;
};
这样:
- Manager 的生命周期和应用一致;
- 可以很容易在一个地方挂统一的日志 / 代理 / 身份认证逻辑。
2. QNetworkRequest:配置一次,少填坑
常用要点:
- URL :
setUrl(QUrl("https://api.github.com/user")) - 通用头部:User-Agent、Accept、Authorization 等;
- 缓存策略 :
setAttribute(QNetworkRequest::CacheLoadControlAttribute, ...) - 超时提示:可以通过 Attribute 或自定义属性配合 QTimer 使用。
示例:
cpp
QNetworkRequest req(QUrl("https://api.github.com/user/repos"));
req.setHeader(QNetworkRequest::UserAgentHeader, "QtRestDemo/1.0");
req.setRawHeader("Accept", "application/json");
// 自定义属性(超时毫秒)
req.setAttribute(QNetworkRequest::User, 15000);
3. QNetworkReply:记得收尾和错误分流
核心模式(推荐写法):
cpp
QNetworkReply *reply = mgr->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply]{
reply->deleteLater(); // 无论成功失败,最后一定要释放
if (reply->error() != QNetworkReply::NoError) {
// 统一错误处理
handleNetworkError(reply);
return;
}
QByteArray body = reply->readAll();
// 解析 JSON / 文本
});
特别注意:
- 不要在
finished之前读数据(除非你明确知道在readyRead阶段就拿数据); - 成功与失败都要有日志,方便排查;
- 如果你准备封装成一个「请求对象」,最好在里面统一管理
deleteLater()。
4. 超时管理:QNetworkReply 自身没有「超时」的概念
典型的模式是给每个请求配一个 QTimer:
cpp
QNetworkReply *reply = mgr->get(req);
QTimer *timer = new QTimer(reply); // 以 reply 作为 parent,方便自动销毁
timer->setSingleShot(true);
timer->setInterval(timeoutMs);
connect(timer, &QTimer::timeout, reply, [reply]{
if (reply->isRunning()) {
reply->abort(); // 触发 error() 和 finished()
}
});
connect(reply, &QNetworkReply::finished, timer, &QTimer::stop);
timer->start();
这样:
- Timeout 的资源和 reply 绑定,不用担心泄漏;
abort()后会触发 error 回调,那里可以统一判断 TimeoutError。
5. HTTPS 与 SSL 错误:别一刀切全部 ignore
对于自己控制的测试环境,可以针对性地忽略某些 SSL 错误;而在正式环境中,应该以「尽量不忽略」为原则。
基础处理模式:
cpp
connect(reply, &QNetworkReply::sslErrors,
this, [this, reply](const QList<QSslError> &errors){
bool canIgnore = true;
for (const QSslError &e : errors) {
// 对可以接受的错误单独判断(比如某些测试环境)
if (e.error() == QSslError::SelfSignedCertificate) {
continue;
}
canIgnore = false;
qWarning() << "SSL error:" << e.errorString();
}
if (canIgnore) {
reply->ignoreSslErrors();
} else {
reply->abort();
}
});
6. 请求队列与并发控制:让 QNetworkAccessManager「不乱」
QNetworkAccessManager 本身有连接复用与并发控制,但在业务层我们还是希望:
- 控制「同时在飞」的请求数量(例如最多 4 个);
- 超出部分进队列,按 FIFO 或优先级执行;
- 提供取消当前所有请求的能力。
实现思路:
- 自己维护一个
QQueue<RequestInfo*>; - 用
QSemaphore控制并发; - 每个 RequestInfo 封装 URL、方法、头、body、超时和回调。
三、完整工程实战:GitHub REST API 小客户端
下面给出一个可以直接在 Qt Creator 中编译运行的 Demo 工程,名字叫 RestApiClient,主要功能:
- 使用 GitHub API 获取当前用户信息;
- 拉取用户的仓库列表;
- 查看选中仓库的 Issues 列表;
- 支持请求队列、基本错误处理和简单 UI 展示。
说明:为了方便演示,这里使用 GitHub 的公开 API,你需要自己准备一个 Personal Access Token(只勾选最小权限即可),填进界面中的 Token 输入框。
1. 工程结构
cpp
RestApiClient/
├── CMakeLists.txt
├── include/
│ ├── networkmanager.h
│ ├── requestinfo.h
│ ├── apiclient.h
│ └── mainwindow.h
└── src/
├── main.cpp
├── networkmanager.cpp
├── requestinfo.cpp
├── apiclient.cpp
└── mainwindow.cpp
为避免一次性太复杂,我这里不再写独立队列类,而是把关键能力收敛在 ApiClient + NetworkManager 里,方便你先看懂整体流程,再按需扩展。
2. CMakeLists.txt(可直接使用)
QtCreator可以自动生成。
3. RequestInfo:封装一次 HTTP 调用的参数与结果
include/requestinfo.h
cpp
#pragma once
#include <QObject>
#include <QUrl>
#include <QVariantMap>
#include <QNetworkReply>
/**
* @brief RequestInfo 封装一次 HTTP 请求的元数据与结果
*/
class RequestInfo : public QObject
{
Q_OBJECT
public:
enum Method {
Get,
Post,
Put,
Delete
};
Q_ENUM(Method)
explicit RequestInfo(const QUrl &url,
Method method,
const QVariantMap &headers = {},
const QByteArray &body = {},
QObject *parent = nullptr);
// 便于 ApiClient 访问的字段(这里只做演示,不做过度封装)
QUrl url;
Method method;
QVariantMap headers;
QByteArray body;
// 结果
QByteArray responseData;
int statusCode = 0;
QNetworkReply::NetworkError error = QNetworkReply::NoError;
QString errorString;
// 配置
int timeoutMs = 30000;
};
src/requestinfo.cpp
cpp
#include "requestinfo.h"
RequestInfo::RequestInfo(const QUrl &u,
Method m,
const QVariantMap &h,
const QByteArray &b,
QObject *parent)
: QObject(parent),
url(u),
method(m),
headers(h),
body(b)
{
}
4. NetworkManager:统一管理 QNetworkAccessManager
include/networkmanager.h
cpp
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
#include <QPointer>
class RequestInfo;
/**
* @brief NetworkManager 全局网络管理器
*
* - 持有一个 QNetworkAccessManager 实例
* - 提供发送请求的统一入口
*/
class NetworkManager : public QObject
{
Q_OBJECT
public:
static NetworkManager *instance();
// 发送请求(异步),完成后发射 finished(RequestInfo*)
void send(RequestInfo *info);
signals:
void finished(RequestInfo *info);
private:
explicit NetworkManager(QObject *parent = nullptr);
static QPointer<NetworkManager> s_instance;
QNetworkAccessManager m_mgr;
};
src/networkmanager.cpp
cpp
#include "networkmanager.h"
#include "requestinfo.h"
#include <QNetworkRequest>
#include <QTimer>
#include <QDebug>
QPointer<NetworkManager> NetworkManager::s_instance = nullptr;
NetworkManager *NetworkManager::instance()
{
if (!s_instance) {
s_instance = new NetworkManager();
}
return s_instance;
}
NetworkManager::NetworkManager(QObject *parent)
: QObject(parent)
{
// 可以在这里设置代理、网络配置等
qDebug() << "NetworkManager created";
}
void NetworkManager::send(RequestInfo *info)
{
if (!info) return;
QNetworkRequest req(info->url);
// 设置常用头部
req.setHeader(QNetworkRequest::UserAgentHeader, "QtRestApiDemo/1.0");
req.setRawHeader("Accept", "application/json");
// 追加自定义头
for (auto it = info->headers.constBegin(); it != info->headers.constEnd(); ++it) {
req.setRawHeader(it.key().toUtf8(), it.value().toByteArray());
}
// 发起请求
QNetworkReply *reply = nullptr;
switch (info->method) {
case RequestInfo::Get:
reply = m_mgr.get(req);
break;
case RequestInfo::Post:
reply = m_mgr.post(req, info->body);
break;
case RequestInfo::Put:
reply = m_mgr.put(req, info->body);
break;
case RequestInfo::Delete:
reply = m_mgr.deleteResource(req);
break;
}
if (!reply) {
info->error = QNetworkReply::UnknownNetworkError;
info->errorString = "无法创建网络请求";
emit finished(info);
return;
}
// 超时处理
int timeoutMs = info->timeoutMs > 0 ? info->timeoutMs : 30000;
QTimer *timer = new QTimer(reply);
timer->setSingleShot(true);
timer->setInterval(timeoutMs);
QObject::connect(timer, &QTimer::timeout, reply, [reply](){
if (reply->isRunning()) {
qWarning() << "Request timeout, aborting" << reply->url();
reply->abort();
}
});
timer->start();
// finished
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, info, timer](){
timer->stop();
timer->deleteLater();
info->error = reply->error();
info->errorString = reply->errorString();
info->responseData = reply->readAll();
info->statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
emit finished(info);
});
// sslErrors(示例中只打印)
QObject::connect(reply, &QNetworkReply::sslErrors, this,
[reply](const QList<QSslError> &errors){
for (const QSslError &e : errors) {
qWarning() << "SSL error:" << e.errorString();
}
// 根据需要决定是否 ignore,这里演示不忽略
Q_UNUSED(reply);
});
}
5. ApiClient:面向业务的 REST 客户端
include/apiclient.h
cpp
#pragma once
#include <QObject>
#include <functional>
#include <QVariantMap>
#include <QVariantList>
class RequestInfo;
/**
* @brief ApiClient 简单 GitHub API 客户端
*
* 只演示几个典型接口:
* - GET /user
* - GET /user/repos
* - GET /repos/{owner}/{repo}/issues
*/
class ApiClient : public QObject
{
Q_OBJECT
public:
explicit ApiClient(QObject *parent = nullptr);
void setToken(const QString &token);
// 回调签名:result, success, message
using ApiCallback = std::function<void(const QVariant &result, bool ok, const QString &msg)>;
void getCurrentUser(const ApiCallback &cb);
void getUserRepos(const ApiCallback &cb);
void getRepoIssues(const QString &owner,
const QString &repo,
const ApiCallback &cb);
private slots:
void onRequestFinished(RequestInfo *info);
private:
RequestInfo *createRequest(const QString &path,
const QString &method = "GET",
const QVariantMap &body = {});
QVariant parseJson(const QByteArray &data, bool &ok, QString &msg);
private:
QString m_baseUrl = "https://api.github.com";
QString m_token;
QHash<RequestInfo*, ApiCallback> m_callbacks;
};
src/apiclient.cpp
cpp
#include "apiclient.h"
#include "networkmanager.h"
#include "requestinfo.h"
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
ApiClient::ApiClient(QObject *parent)
: QObject(parent)
{
connect(NetworkManager::instance(), &NetworkManager::finished,
this, &ApiClient::onRequestFinished);
}
void ApiClient::setToken(const QString &token)
{
m_token = token;
}
RequestInfo *ApiClient::createRequest(const QString &path,
const QString &method,
const QVariantMap &body)
{
QUrl url(m_baseUrl + path);
RequestInfo::Method m = RequestInfo::Get;
if (method.compare("POST", Qt::CaseInsensitive) == 0) {
m = RequestInfo::Post;
} else if (method.compare("PUT", Qt::CaseInsensitive) == 0) {
m = RequestInfo::Put;
} else if (method.compare("DELETE", Qt::CaseInsensitive) == 0) {
m = RequestInfo::Delete;
}
QVariantMap headers;
headers["Accept"] = "application/vnd.github+json";
if (!m_token.isEmpty()) {
headers["Authorization"] = "token " + m_token;
}
QByteArray bodyBytes;
if (!body.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromVariant(body);
bodyBytes = doc.toJson(QJsonDocument::Compact);
headers["Content-Type"] = "application/json";
}
auto *info = new RequestInfo(url, m, headers, bodyBytes, this);
info->timeoutMs = 20000; // 20 秒超时
return info;
}
void ApiClient::getCurrentUser(const ApiCallback &cb)
{
RequestInfo *info = createRequest("/user", "GET");
m_callbacks.insert(info, cb);
NetworkManager::instance()->send(info);
}
void ApiClient::getUserRepos(const ApiCallback &cb)
{
// GitHub 支持 /user/repos 获取当前用户所有仓库
RequestInfo *info = createRequest("/user/repos", "GET");
m_callbacks.insert(info, cb);
NetworkManager::instance()->send(info);
}
void ApiClient::getRepoIssues(const QString &owner,
const QString &repo,
const ApiCallback &cb)
{
QString path = QString("/repos/%1/%2/issues").arg(owner, repo);
RequestInfo *info = createRequest(path, "GET");
m_callbacks.insert(info, cb);
NetworkManager::instance()->send(info);
}
QVariant ApiClient::parseJson(const QByteArray &data, bool &ok, QString &msg)
{
ok = false;
msg.clear();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
msg = QString("JSON 解析错误: %1").arg(err.errorString());
return {};
}
if (doc.isObject()) {
ok = true;
return doc.object().toVariantMap();
} else if (doc.isArray()) {
ok = true;
return doc.array().toVariantList();
} else {
msg = "未知 JSON 根类型";
return {};
}
}
void ApiClient::onRequestFinished(RequestInfo *info)
{
if (!m_callbacks.contains(info)) {
// 不是这个客户端发起的请求
return;
}
ApiCallback cb = m_callbacks.take(info);
bool ok = false;
QString msg;
if (info->error != QNetworkReply::NoError) {
msg = QString("网络错误(%1): %2")
.arg(static_cast<int>(info->error))
.arg(info->errorString);
} else if (info->statusCode >= 400) {
msg = QString("HTTP 错误 %1").arg(info->statusCode);
} else {
QVariant parsed = parseJson(info->responseData, ok, msg);
if (cb) {
cb(parsed, ok, msg);
}
info->deleteLater();
return;
}
// 走到这里说明有错误
if (cb) {
cb(QVariant(), false, msg);
}
info->deleteLater();
}
6. MainWindow:一个简单的 GitHub 客户端界面
include/mainwindow.h
cpp
#pragma once
#include <QMainWindow>
class QLineEdit;
class QPushButton;
class QLabel;
class QListWidget;
class QComboBox;
class QTextEdit;
class QProgressBar;
class ApiClient;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private slots:
void onLoginClicked();
void onFetchReposClicked();
void onFetchIssuesClicked();
void onClearIssuesClicked();
private:
void setupUi();
void setLoggedIn(bool loggedIn);
void logStatus(const QString &text);
private:
ApiClient *m_client = nullptr;
QLineEdit *m_tokenEdit = nullptr;
QPushButton *m_loginBtn = nullptr;
QLabel *m_userLabel = nullptr;
QPushButton *m_fetchReposBtn = nullptr;
QListWidget *m_repoList = nullptr;
QComboBox *m_repoCombo = nullptr;
QPushButton *m_fetchIssuesBtn = nullptr;
QPushButton *m_clearIssuesBtn = nullptr;
QTextEdit *m_issuesText = nullptr;
QProgressBar *m_progress = nullptr;
QLabel *m_statusLabel = nullptr;
QString m_currentUser;
};
src/mainwindow.cpp
cpp
#include "mainwindow.h"
#include "apiclient.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QListWidget>
#include <QComboBox>
#include <QTextEdit>
#include <QProgressBar>
#include <QMessageBox>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle(tr("Qt5 GitHub REST API 示例"));
resize(960, 640);
setupUi();
m_client = new ApiClient(this);
setLoggedIn(false);
}
void MainWindow::setupUi()
{
QWidget *central = new QWidget(this);
QVBoxLayout *mainLayout = new QVBoxLayout(central);
// 登录区
auto *loginGroup = new QGroupBox(tr("GitHub 登录"), this);
auto *loginLayout = new QHBoxLayout(loginGroup);
auto *tokenLabel = new QLabel(tr("Token:"), this);
m_tokenEdit = new QLineEdit(this);
m_tokenEdit->setEchoMode(QLineEdit::Password);
m_tokenEdit->setPlaceholderText(tr("请输入 GitHub Personal Access Token"));
m_loginBtn = new QPushButton(tr("登录"), this);
loginLayout->addWidget(tokenLabel);
loginLayout->addWidget(m_tokenEdit, 1);
loginLayout->addWidget(m_loginBtn);
loginGroup->setLayout(loginLayout);
mainLayout->addWidget(loginGroup);
// 用户信息
auto *userLayout = new QHBoxLayout();
m_userLabel = new QLabel(tr("未登录"), this);
m_userLabel->setStyleSheet("font-weight:bold;");
userLayout->addWidget(m_userLabel);
userLayout->addStretch();
mainLayout->addLayout(userLayout);
// 控制按钮
auto *ctrlLayout = new QHBoxLayout();
m_fetchReposBtn = new QPushButton(tr("获取仓库列表"), this);
m_fetchIssuesBtn = new QPushButton(tr("获取 Issues"), this);
m_clearIssuesBtn = new QPushButton(tr("清空 Issues"), this);
ctrlLayout->addWidget(m_fetchReposBtn);
ctrlLayout->addWidget(m_fetchIssuesBtn);
ctrlLayout->addWidget(m_clearIssuesBtn);
ctrlLayout->addStretch();
mainLayout->addLayout(ctrlLayout);
// 主内容
auto *contentLayout = new QHBoxLayout();
// 左侧:仓库列表
auto *leftLayout = new QVBoxLayout();
auto *repoListLabel = new QLabel(tr("仓库列表:"), this);
m_repoList = new QListWidget(this);
leftLayout->addWidget(repoListLabel);
leftLayout->addWidget(m_repoList);
contentLayout->addLayout(leftLayout, 1);
// 右侧:选择仓库 + Issues
auto *rightLayout = new QVBoxLayout();
auto *repoComboLabel = new QLabel(tr("当前仓库:"), this);
m_repoCombo = new QComboBox(this);
m_issuesText = new QTextEdit(this);
m_issuesText->setReadOnly(true);
rightLayout->addWidget(repoComboLabel);
rightLayout->addWidget(m_repoCombo);
rightLayout->addWidget(m_issuesText, 1);
contentLayout->addLayout(rightLayout, 2);
mainLayout->addLayout(contentLayout, 1);
// 状态栏
auto *bottomLayout = new QHBoxLayout();
m_progress = new QProgressBar(this);
m_progress->setRange(0, 0);
m_progress->setVisible(false);
m_statusLabel = new QLabel(tr("就绪"), this);
bottomLayout->addWidget(m_progress);
bottomLayout->addWidget(m_statusLabel);
bottomLayout->addStretch();
mainLayout->addLayout(bottomLayout);
setCentralWidget(central);
// 连接信号槽
connect(m_loginBtn, &QPushButton::clicked,
this, &MainWindow::onLoginClicked);
connect(m_fetchReposBtn, &QPushButton::clicked,
this, &MainWindow::onFetchReposClicked);
connect(m_fetchIssuesBtn, &QPushButton::clicked,
this, &MainWindow::onFetchIssuesClicked);
connect(m_clearIssuesBtn, &QPushButton::clicked,
this, &MainWindow::onClearIssuesClicked);
}
void MainWindow::setLoggedIn(bool loggedIn)
{
m_tokenEdit->setEnabled(!loggedIn);
m_loginBtn->setEnabled(!loggedIn);
m_fetchReposBtn->setEnabled(loggedIn);
m_fetchIssuesBtn->setEnabled(loggedIn && !m_repoCombo->currentText().isEmpty());
if (!loggedIn) {
m_userLabel->setText(tr("未登录"));
m_currentUser.clear();
m_repoList->clear();
m_repoCombo->clear();
m_issuesText->clear();
}
}
void MainWindow::logStatus(const QString &text)
{
m_statusLabel->setText(text);
qDebug() << text;
}
void MainWindow::onLoginClicked()
{
QString token = m_tokenEdit->text().trimmed();
if (token.isEmpty()) {
QMessageBox::warning(this, tr("提示"), tr("请先输入 Token"));
return;
}
m_client->setToken(token);
m_progress->setVisible(true);
logStatus(tr("正在验证 Token..."));
m_client->getCurrentUser([this](const QVariant &result, bool ok, const QString &msg){
m_progress->setVisible(false);
if (!ok) {
QMessageBox::warning(this, tr("登录失败"), msg);
setLoggedIn(false);
return;
}
QVariantMap obj = result.toMap();
m_currentUser = obj.value("login").toString();
if (m_currentUser.isEmpty()) {
QMessageBox::warning(this, tr("登录失败"), tr("未获取到 login 字段"));
setLoggedIn(false);
return;
}
m_userLabel->setText(tr("已登录:%1").arg(m_currentUser));
setLoggedIn(true);
logStatus(tr("登录成功,欢迎 %1").arg(m_currentUser));
});
}
void MainWindow::onFetchReposClicked()
{
if (m_currentUser.isEmpty()) {
QMessageBox::warning(this, tr("提示"), tr("请先登录"));
return;
}
m_progress->setVisible(true);
m_repoList->clear();
m_repoCombo->clear();
m_issuesText->clear();
logStatus(tr("正在获取仓库列表..."));
m_client->getUserRepos([this](const QVariant &result, bool ok, const QString &msg){
m_progress->setVisible(false);
if (!ok) {
QMessageBox::warning(this, tr("获取仓库失败"), msg);
logStatus(tr("获取仓库失败:%1").arg(msg));
return;
}
QVariantList list = result.toList();
for (const QVariant &v : list) {
QVariantMap repo = v.toMap();
QString name = repo.value("name").toString();
if (!name.isEmpty()) {
m_repoList->addItem(name);
m_repoCombo->addItem(name);
}
}
logStatus(tr("仓库获取完成,共 %1 个").arg(list.size()));
});
}
void MainWindow::onFetchIssuesClicked()
{
if (m_currentUser.isEmpty()) {
QMessageBox::warning(this, tr("提示"), tr("请先登录"));
return;
}
if (m_repoCombo->currentText().isEmpty()) {
QMessageBox::warning(this, tr("提示"), tr("请先选择一个仓库"));
return;
}
QString repoName = m_repoCombo->currentText();
m_progress->setVisible(true);
m_issuesText->clear();
logStatus(tr("正在获取 %1/%2 的 Issues...").arg(m_currentUser, repoName));
m_client->getRepoIssues(m_currentUser, repoName,
[this, repoName](const QVariant &result, bool ok, const QString &msg){
m_progress->setVisible(false);
if (!ok) {
QMessageBox::warning(this, tr("获取 Issues 失败"), msg);
logStatus(tr("获取 Issues 失败:%1").arg(msg));
return;
}
QVariantList issues = result.toList();
QString html;
for (const QVariant &v : issues) {
QVariantMap issue = v.toMap();
int number = issue.value("number").toInt();
QString title = issue.value("title").toString();
QString state = issue.value("state").toString();
html += QString("<h3>#%1 [%2] %3</h3>")
.arg(number)
.arg(state)
.arg(title);
html += "<hr/>";
}
if (html.isEmpty()) {
html = tr("没有获取到 Issue。");
}
m_issuesText->setHtml(html);
logStatus(tr("仓库 %1 的 Issues 获取完成,共 %2 个")
.arg(repoName)
.arg(issues.size()));
});
}
void MainWindow::onClearIssuesClicked()
{
m_issuesText->clear();
logStatus(tr("Issues 已清空"));
}
7. main.cpp:程序入口
cpp
#include "mainwindow.h"
#include <QApplication>
#include <QSslSocket>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QApplication::setApplicationName("RestApiClient");
QApplication::setOrganizationName("QtAdvancedSeries");
a.setStyle("Fusion");
// 设置 OpenSSL 路径
QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath());
qDebug() << "SSL支持:" << QSslSocket::supportsSsl();
qDebug() << "SSL库版本:" << QSslSocket::sslLibraryVersionString();
qDebug() << "SSL构建版本:" << QSslSocket::sslLibraryBuildVersionString();
MainWindow w;
w.show();
return a.exec();
}
四、实战中的坑与优化:结合这个 Demo 再看几条经验
1. 把「请求发起」和「结果处理」分开
在这个 Demo 里:
NetworkManager::send()只负责:组装 QNetworkRequest、创建 QNetworkReply、处理超时、收集结果;ApiClient负责:构建 RequestInfo、解析 JSON、把结果转成 QVariantMap / QVariantList,再以 callback 的方式抛给 UI。
这种分层的好处是:如果将来你要换成别的 HTTP 库(例如 libcurl、Boost.Beast),只需要改 NetworkManager 和 RequestInfo,不动 UI 和业务层。
2. 不要在 UI 层直接处理 QNetworkReply
UI 层只关心「成功还是失败、结果是什么」,不关心:
- 返回头里有什么;
- TLS 是什么版本;
- 是否重试、是否走代理等。
这些都应该在网络层或 ApiClient 层统一封装。
这样 UI 层的函数签名就很干净:
cpp
void MainWindow::onFetchReposClicked()
{
m_client->getUserRepos([this](const QVariant &result, bool ok, const QString &msg){
// ...
});
}
3. 超时不是「可选项」,而是「必需项」
所有实际项目中用到的 HTTP 请求,都应该有明确的超时逻辑:
- 网络层统一设置默认超时;
- 某些接口可以单独提高 / 降低;
- 超时后给出明确提示,而不是让用户干等。
4. 日志与调试
建议在 NetworkManager 里增加:
- 请求 URL、方法、头部与 body 的调试日志;
- 响应状态码、错误码、错误字符串;
- 必要时可以把 body 也打印到单独日志里(注意脱敏)。
这样在出现线上问题时,只需远程看一眼日志,就能大致判断是「网络问题、服务端问题还是客户端逻辑问题」。
五、小结:项目中可落地的 Qt 网络访问实践清单
结合本期内容,把关键点整理成一份「可以直接写进团队开发规范」的条目:
-
统一入口
- 全局只维护一个或少量 QNetworkAccessManager,由 NetworkManager 封装;
- 所有 HTTP 请求都通过统一入口发起,方便挂公共逻辑。
-
请求封装
- 用 RequestInfo(或类似结构)记录 URL、方法、头部、body、超时等信息;
- 网络层返回时只负责填充 error/statusCode/body 等基础字段。
-
响应解析
- 专门的 ApiClient 层负责把
QByteArray→QJsonDocument→ QVariantMap/VariantList; - ApiClient 只暴露「业务相关」的函数,例如
getUserRepos()、getRepoIssues()。
- 专门的 ApiClient 层负责把
-
错误与超时处理
- 超时一律由网络层配合 QTimer 实现;
- 所有请求都必须在失败时给出明确的错误信息(包括错误码和简要提示)。
-
线程与 UI 安全
- 不在子线程直接访问 QWidget;
- 全部网络回调通过 Qt 的信号槽在主线程处理,然后再更新 UI。
-
安全性
- HTTPS 和 SSL 错误要按类型区分处理,不要无脑
ignoreSslErrors(); - 访问令牌、密码等敏感信息不要写死在代码中,而是从配置或 UI 注入。
- HTTPS 和 SSL 错误要按类型区分处理,不要无脑
如果你把这一套 Demo 跑通,再按上面的规范梳理一下自己项目中的网络访问代码,通常可以明显减少:
- 难以复现的网络崩溃;
- 无法解释的卡顿;
- 到处散落的「一次性」网络逻辑。
到这里,Qt5 进阶专题从基础机制(前 1--5 期)走到文件(第 6 期)和网络(第 7 期),已经基本覆盖了绝大多数桌面应用的「底座能力」。
后续再配合数据库、架构与性能优化,你就可以比较自信地搭建起一个完整的 Qt 桌面应用。