? ?在 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. 結論
方案 | 優點 | 缺點 |
---|---|---|
順序重試(隊列模式) | 減少服務器壓力、保證請求順序 | 可能導致某些請求等待較久 |
并行重試(多定時器) | 提高吞吐量,適合高并發 | 可能讓服務器短時間內收到大量重試請求 |
?
?