在 Qt 网络开发中,使用 QNetworkAccessManager
进行 HTTP 请求时,可能会遇到网络超时、服务器错误等情况。为了提高请求的可靠性,可以实现 HTTP 失败重试(重发) 机制。下面介绍几种常见的 失败重发方案:
单请求
1. 基本重试策略
适用于: 短暂的网络抖动、服务器瞬时不可用等情况。
实现思路:
- 监听
QNetworkReply
的finished()
信号,检查error()
是否为失败状态。 - 如果失败,等待一段时间后重新发送请求。
- 设定 最大重试次数,避免无限循环重试。
-
#include <QCoreApplication> #include <QNetworkAccessManager> #include <QNetworkRequest> #include <QNetworkReply> #include <QTimer> #include <QDebug>class HttpRetryHandler : public QObject {Q_OBJECT public:HttpRetryHandler(QObject *parent = nullptr): QObject(parent), networkManager(new QNetworkAccessManager(this)), retryCount(0) {connect(networkManager, &QNetworkAccessManager::finished, this, &HttpRetryHandler::onReplyFinished);}void sendRequest() {QNetworkRequest request(QUrl("https://example.com/api"));request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");reply = networkManager->get(request);connect(reply, &QNetworkReply::finished, this, &HttpRetryHandler::onReplyFinished);}private slots:void onReplyFinished() {if (reply->error() == QNetworkReply::NoError) {qDebug() << "Request successful:" << reply->readAll();reply->deleteLater();} else {qDebug() << "Request failed:" << reply->errorString();if (retryCount < maxRetries) {retryCount++;qDebug() << "Retrying..." << retryCount;QTimer::singleShot(retryInterval, this, &HttpRetryHandler::sendRequest);} else {qDebug() << "Max retries reached. Giving up.";}}}private:QNetworkAccessManager *networkManager;QNetworkReply *reply;int retryCount;const int maxRetries = 3; // 最大重试次数const int retryInterval = 2000; // 失败后等待 2 秒再重试 };int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);HttpRetryHandler handler;handler.sendRequest();return a.exec(); }#include "main.moc"
关键点
- 设置最大重试次数 (
maxRetries
),避免无限重试。 - 使用
QTimer::singleShot()
延迟重试,避免立即重试导致服务器压力增大。
2. 指数退避(Exponential Backoff)重试
适用于: API 速率限制、网络负载较高时的自动恢复。
实现思路:
- 每次重试时,等待时间 指数增长(如
2^retryCount
)。 - 可以设置一个 最大等待时间,避免等待过长。
- 适用于 API 限流(如 HTTP 429 Too Many Requests)或 服务器负载高的情况。
void retryWithBackoff() {if (retryCount < maxRetries) {int delay = qMin(initialRetryInterval * (1 << retryCount), maxRetryInterval);qDebug() << "Retrying in" << delay << "ms...";QTimer::singleShot(delay, this, &HttpRetryHandler::sendRequest);retryCount++;} else {qDebug() << "Max retries reached. Stopping.";}
}
关键点
- 指数增长等待时间:
delay = initialRetryInterval * (1 << retryCount)
- 避免等待过长:使用
qMin()
限制最大重试间隔。
3. 仅对特定 HTTP 状态码重试
适用于:
- 服务器错误(HTTP 5xx,如
500
、502
、503
)。 - 速率限制(HTTP
429 Too Many Requests
)。
void onReplyFinished() {int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();if (reply->error() == QNetworkReply::NoError) {qDebug() << "Success:" << reply->readAll();} else if (statusCode == 500 || statusCode == 502 || statusCode == 503 || statusCode == 429) {qDebug() << "Server error, retrying...";retryWithBackoff();} else {qDebug() << "Request failed permanently:" << reply->errorString();}
}
关键点
- 避免对所有错误重试,只对 5xx/429 进行重试,减少无效请求。
4. 结合 QNetworkReply::redirected()
处理重定向
适用于: 遇到 301/302/307 重定向 时自动跟随新 URL。
void onReplyFinished() {QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);if (!redirectTarget.isNull()) {QUrl newUrl = redirectTarget.toUrl();qDebug() << "Redirected to:" << newUrl;request.setUrl(newUrl);sendRequest();return;}
}
关键点
- 检查
RedirectionTargetAttribute
并重新发起请求。
总结
方案 | 适用情况 | 优缺点 |
---|---|---|
基本重试 | 网络波动、短暂的服务器异常 | 简单有效,但可能导致过多请求 |
指数退避 | API 限流、服务器负载高 | 避免频繁请求,适应性更强 |
特定状态码重试 | 5xx/429 错误 | 只在必要时重试,减少无效请求 |
自动重定向 | 301/302/307 响应 | 处理 URL 变更,防止访问失败 |
在实际开发中,可以 结合多种策略:
- 对
5xx/429
进行指数退避重试。 - 对
301/302
自动重定向。 - 对不可恢复错误(如
403/404
)直接放弃。
多请求
如果有多个失败请求需要重试,可以使用 队列管理 机制来处理所有失败的请求,而不是单独重试每个请求。这可以确保 多个请求按顺序重试,并且不会让服务器负担过重。或者可以让请求并行。
方案 1:使用队列逐个重试
- 适用于: 不希望所有请求同时重试,逐个处理失败请求,降低服务器压力。
实现思路:
- 维护一个
QQueue<QNetworkRequest>
队列,存储失败的请求。 - 失败请求会进入队列,并按照 FIFO(先进先出)顺序依次重试。
- 使用
QTimer
控制指数退避重试时间,依次出队并重试。 - 如果重试成功,从队列中移除请求,否则增加重试次数,重新排队。
#include <QCoreApplication>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTimer>
#include <QQueue>
#include <QDebug>
#include <QRandomGenerator>class HttpRetryHandler : public QObject {Q_OBJECT
public:HttpRetryHandler(QObject *parent = nullptr): QObject(parent), networkManager(new QNetworkAccessManager(this)) {connect(networkManager, &QNetworkAccessManager::finished, this, &HttpRetryHandler::onReplyFinished);}void sendRequest(const QUrl &url) {QNetworkRequest request(url);request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");QNetworkReply *reply = networkManager->get(request);requestMap.insert(reply, {request, 0}); // 记录请求和当前重试次数connect(reply, &QNetworkReply::finished, this, &HttpRetryHandler::onReplyFinished);}private slots:void onReplyFinished() {QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());if (!reply) return;int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();auto it = requestMap.find(reply);if (reply->error() == QNetworkReply::NoError) {qDebug() << "Request successful: " << reply->readAll();requestMap.remove(reply);} else if (statusCode == 500 || statusCode == 502 || statusCode == 503 || statusCode == 429) {// 服务器错误或限流,加入重试队列qDebug() << "Server error" << statusCode << ", adding request to retry queue...";retryQueue.enqueue(it.value());processRetryQueue(); // 处理队列} else {qDebug() << "Request failed permanently: " << reply->errorString();}reply->deleteLater();}void processRetryQueue() {if (retryQueue.isEmpty() || retrying) return;retrying = true;RequestData requestData = retryQueue.dequeue();if (requestData.retryCount < maxRetries) {int delay = qMin(initialRetryInterval * (1 << requestData.retryCount) + getJitter(), maxRetryInterval);qDebug() << "Retrying request in" << delay << "ms...";QTimer::singleShot(delay, this, [=]() {sendRequest(requestData.request.url());retrying = false;processRetryQueue(); // 继续处理下一个请求});requestData.retryCount++;} else {qDebug() << "Max retries reached for request: " << requestData.request.url();retrying = false;}}private:struct RequestData {QNetworkRequest request;int retryCount;};QNetworkAccessManager *networkManager;QMap<QNetworkReply *, RequestData> requestMap;QQueue<RequestData> retryQueue;bool retrying = false;const int maxRetries = 5; // 最大重试次数const int initialRetryInterval = 1000; // 初始重试间隔 1 秒const int maxRetryInterval = 16000; // 最大重试间隔 16 秒int getJitter() { return QRandomGenerator::global()->bounded(500); } // 额外随机延迟 0~500ms
};int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);HttpRetryHandler handler;// 发送多个请求handler.sendRequest(QUrl("https://example.com/api1"));handler.sendRequest(QUrl("https://example.com/api2"));handler.sendRequest(QUrl("https://example.com/api3"));return a.exec();
}#include "main.moc"
关键点
(1) 多个请求失败后的处理
- 使用
QMap<QNetworkReply *, RequestData>
记录请求信息 QQueue<RequestData>
存储失败的请求processRetryQueue()
依次从队列取出请求并重试
(2) 防止所有请求同时重试
- 使用
bool retrying
确保一次只处理一个重试请求 - 指数退避 (
2^retryCount
) 并增加随机抖动
**(3) 重试失败的请求
- 如果达到
maxRetries
,则放弃重试 - 否则等待
delay
时间后重试
方案 2:并行重试多个请求
如果你不想让请求 顺序重试,而是 并行重试多个请求,可以 使用多个 QTimer 并行调度,同时让多个请求进行指数退避重试。
void processRetryQueue() {while (!retryQueue.isEmpty()) {RequestData requestData = retryQueue.dequeue();if (requestData.retryCount < maxRetries) {int delay = qMin(initialRetryInterval * (1 << requestData.retryCount) + getJitter(), maxRetryInterval);qDebug() << "Retrying request to " << requestData.request.url() << " in " << delay << "ms...";QTimer::singleShot(delay, this, [=]() {sendRequest(requestData.request.url());});requestData.retryCount++;} else {qDebug() << "Max retries reached for request: " << requestData.request.url();}}
}
区别:
- 多个请求可以同时重试(不会等待上一个重试完成)。
- 所有请求仍然使用指数退避时间控制频率。
5. 结论
方案 | 优点 | 缺点 |
---|---|---|
顺序重试(队列模式) | 减少服务器压力、保证请求顺序 | 可能导致某些请求等待较久 |
并行重试(多定时器) | 提高吞吐量,适合高并发 | 可能让服务器短时间内收到大量重试请求 |