HTTP 失败重试(重发)方案

发布于:2025-03-24 ⋅ 阅读:(24) ⋅ 点赞:(0)

   在 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,如 500502503)。
  • 速率限制(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:使用队列逐个重试

  • 适用于: 不希望所有请求同时重试,逐个处理失败请求,降低服务器压力。

实现思路:

  1. 维护一个 QQueue<QNetworkRequest> 队列,存储失败的请求
  2. 失败请求会进入队列,并按照 FIFO(先进先出)顺序依次重试。
  3. 使用 QTimer 控制指数退避重试时间依次出队并重试
  4. 如果重试成功,从队列中移除请求,否则增加重试次数,重新排队。
#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. 结论

方案 优点 缺点
顺序重试(队列模式) 减少服务器压力、保证请求顺序 可能导致某些请求等待较久
并行重试(多定时器) 提高吞吐量,适合高并发 可能让服务器短时间内收到大量重试请求

 

 

注:本人也在学习中,如果有错误,请指出!!!


网站公告

今日签到

点亮在社区的每一天
去签到