应手动实现带指数退避的重试逻辑:仅对临时性错误(如curle_couldnt_resolve_host、http 429/500-504)重试,禁用客户端错误重试;使用base_delay_ms×(2^attempt)+jitter,显式设超时,隔离每请求重试状态,避免全局变量共享。

用 libcurl 实现带指数退避的重试逻辑
直接在 libcurl 的每次请求后判断返回码和网络错误,手动控制重试次数与等待时间。C++ 标准库不提供 HTTP 客户端,libcurl 是最常用、可控性最强的选择。
关键不是“封装一个类”,而是把重试决策从网络调用中解耦出来:失败时先检查 curl_easy_getinfo(..., CURLINFO_RESPONSE_CODE, &code) 和 curl_easy_perform() 返回值(比如 CURLE_COULDNT_CONNECT 或 CURLE_OPERATION_TIMEDOUT),再决定是否重试。
- 只对临时性错误重试:如
CURLE_COULDNT_RESOLVE_HOST、CURLE_OPERATION_TIMEDOUT、CURLE_COULDNT_CONNECT、HTTP 状态码 429 / 500 / 502 / 503 / 504 - 绝对不重试:400 / 401 / 403 / 404 / 405 等客户端错误,或
CURLE_URL_MALFORMAT - 指数退避公式建议用
base_delay_ms * (2 ^ attempt) + jitter,jitter 用随机毫秒(比如 ±100ms)避免雪崩 -
CURLOPT_TIMEOUT_MS和CURLOPT_CONNECTTIMEOUT_MS必须显式设置,否则默认无超时,重试可能卡死
为什么不用 boost::beast 自建重试?
boost::beast 提供底层 HTTP 构造能力,但不内置重试策略 —— 你得自己管理连接生命周期、响应解析、错误分类、定时器调度,工作量远超 libcurl 方案。
典型坑点:
立即学习“C++免费学习笔记(深入)”;
- 异步模式下,
deadline_timer和tcp_stream生命周期容易错配,一次重试失败可能泄漏 socket 或 timer - HTTP/1.1 连接复用需手动处理
Connection: keep-alive和Content-Length,否则重试时可能读到上一次的残留 body - 没有内置 DNS 缓存,高频重试 + 域名解析失败会放大问题,而
libcurl默认启用CURLOPT_DNS_CACHE_TIMEOUT
std::chrono 控制退避时间时的精度陷阱
别直接用 std::this_thread::sleep_for(std::chrono::milliseconds(delay)) 做退避等待 —— 操作系统调度延迟 + 时钟精度会导致实际休眠远长于预期(尤其在 Windows 上误差常达 15ms 起步)。
- 对 100ms 以内的退避,改用
std::this_thread::yield()+ 忙等(仅限极短时间,慎用) - 更稳妥做法:用
libcurl的CURLOPT_TIMEOUT_MS配合单次请求内完成重试,或用epoll/IOCP类机制做非阻塞等待(但复杂度陡增) - 如果必须 sleep,至少用
std::chrono::steady_clock计算起始时间,主动校准下次 delay,避免误差累积
重试上下文必须隔离 per-request
每个请求的重试计数、退避基数、错误历史不能共享。常见错误是把 attempt_count 设成全局或静态变量,导致并发请求互相干扰。
正确做法是把重试状态封装进请求对象(比如 struct HttpRequest { int attempt = 0; int max_attempts = 3; ... };),或用 lambda 捕获局部变量传给回调。
- 多线程环境下,
libcurl的 easy handle 不可跨线程复用,每个请求应独占一个CURL* - 若用连接池,确保重试时新建 handle,而不是复用已失败的 handle(它内部状态可能已损坏)
- 日志里务必打上 request_id 或 URL 片段,否则重试日志无法对应到具体失败链路
真正难的不是写个 for 循环加 sleep,而是区分哪些错误值得重试、什么时候该放弃、以及如何不让重试本身变成 DoS 自己的服务。这些边界条件比算法本身更容易出问题。










