Введение
Реальные сети ненадёжны: запросы могут «подвисать» из-за проблем на стороне сервера, временной потери связи или перегруженного Wi-Fi. Без явной настройки таймаутов HTTP-запрос через QNetworkAccessManager может ждать ответа неопределённо долго, замораживая логику приложения, которая от него зависит. В статье рассмотрим, как настроить таймауты и реализовать устойчивую логику повторных попыток (retry) с задержкой.
Концепция
С Qt 5.15 у QNetworkRequest появился метод setTransferTimeout(), который ограничивает время на весь обмен данными (от отправки запроса до получения полного ответа). Для более старых версий или более тонкого контроля таймаут реализуют вручную через QTimer, который при срабатывании вызывает reply->abort().
Логика повторных попыток должна учитывать тип ошибки: сетевые сбои (QNetworkReply::NetworkError, TimeoutError) обычно стоит повторять, а ошибки на уровне HTTP, означающие клиентскую проблему (4xx, кроме 429), повторять бессмысленно — повтор того же неверного запроса даст тот же результат. Также важно увеличивать интервал между попытками (экспоненциальная задержка), чтобы не «забивать» и без того нестабильный сервер новыми запросами.
Пример кода: Таймауты и переподключение
#include <QCoreApplication>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTimer>
#include <QDebug>
class ResilientRequest : public QObject
{
Q_OBJECT
public:
explicit ResilientRequest(QNetworkAccessManager *manager, QObject *parent = nullptr)
: QObject(parent), m_manager(manager) {}
void fetch(const QUrl &url, int maxRetries = 3)
{
m_url = url;
m_maxRetries = maxRetries;
m_attempt = 0;
sendRequest();
}
private:
void sendRequest()
{
m_attempt++;
QNetworkRequest request(m_url);
request.setTransferTimeout(5000); // 5 секунд на весь обмен
qDebug() << "Попытка" << m_attempt << "из" << m_maxRetries;
QNetworkReply *reply = m_manager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
onFinished(reply);
});
}
void onFinished(QNetworkReply *reply)
{
reply->deleteLater();
const auto error = reply->error();
if (error == QNetworkReply::NoError) {
qDebug() << "Успех:" << reply->readAll().size() << "байт получено";
return;
}
const bool isRetryable = error == QNetworkReply::TimeoutError
|| error == QNetworkReply::TemporaryNetworkFailureError
|| error == QNetworkReply::HostNotFoundError
|| error == QNetworkReply::RemoteHostClosedError;
if (isRetryable && m_attempt < m_maxRetries) {
const int delayMs = 1000 * (1 << (m_attempt - 1)); // 1с, 2с, 4с...
qWarning() << "Ошибка" << error << "- повтор через" << delayMs << "мс";
QTimer::singleShot(delayMs, this, &ResilientRequest::sendRequest);
} else {
qWarning() << "Запрос окончательно провален:" << reply->errorString();
}
}
QNetworkAccessManager *m_manager;
QUrl m_url;
int m_maxRetries = 0;
int m_attempt = 0;
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QNetworkAccessManager manager;
ResilientRequest request(&manager);
request.fetch(QUrl("https://example.com/api/data"));
return app.exec();
}
#include "main.moc"
Пояснения к коду
Метод fetch() запускает первую попытку, сохраняя URL и счётчик допустимых повторов. В onFinished() происходит ключевая развилка: если ошибка относится к «временным» сетевым проблемам (список isRetryable) и лимит попыток не превышен, запрос повторяется после задержки, которая растёт экспоненциально — 1000 * (1 << (attempt - 1)) даёт последовательность 1с, 2с, 4с, 8с и так далее. Если ошибка не относится к повторяемым (например, ContentNotFoundError для HTTP 404) или лимит попыток исчерпан, запрос завершается с ошибкой окончательно.
setTransferTimeout(5000) гарантирует, что даже «подвисший» запрос без таймаута на уровне сервера не будет ждать ответа дольше 5 секунд.
Подводные камни
- Бесконтрольный retry на ошибках 4xx. Повторение запроса, который сервер отверг как некорректный (400 Bad Request, 401 Unauthorized), не имеет смысла и может только усугубить нагрузку — важно различать классы ошибок.
- Отсутствие «джиттера» в задержке. Если множество клиентов одновременно теряют связь с сервером и одновременно начинают retry по одинаковому расписанию (1с, 2с, 4с), они создают синхронизированные всплески нагрузки при восстановлении сервера («thundering herd»). Добавление случайного смещения к задержке снижает этот эффект.
setTransferTimeout()доступен только с Qt 5.15+. В более старых версиях нужно реализовывать таймаут вручную черезQTimerиreply->abort(), аккуратно отслеживая, чтоabort()сам вызоветfinished()с соответствующей ошибкой.- Накопление «зависших» реплаев при частых ретраях. Если не вызывать
deleteLater()сразу в обработчике (как сделано в примере), при множестве повторов через retry-логику может накопиться много неудалённых объектовQNetworkReply.