简介:cpr 1.3.0 预编译库面向需要在 C++ 项目中快速接入 HTTP 请求能力的开发者,尤其适合不想自行处理 curl 依赖与 VS 编译链、希望直接用于调试或发布构建的人群。压缩包共 27 个文件,包含 21 个头文件、4 个 lib 库和 2 个 dll 动态库,整体约 738KB;头文件覆盖会话、响应、鉴权、Cookie、超时、代理、重定向、SSL 选项、multipart 等常用接口,lib 与 dll 按 Debug/Release 提供 cpr 及 libcurl 的链接与运行所需文件。资源基于 VS2015 于 2019 年 2 月编译,可直接用于对应工具链的项目集成,省去自行编译 cpr 与 libcurl 的时间。已有 691 人学习或下载,适合拿来快速验证网络请求、封装客户端模块,或作为学习 cpr 接口组织方式的参考。
1. cpr 1.3.0 不是“又一个 HTTP 库”:它把 libcurl 的样板代码压成三行
写 C++ 调 HTTP 接口,绕不开 libcurl。curl_easy_init、curl_easy_setopt一串CURLOPT_*、手动管curl_slist、回调里拼字符串、curl_easy_cleanup——一个最简单的 GET 认真写下来六十行起步,还得记住哪个setopt传的是指针、哪个是长整型。cpr 1.3.0 就是冲着这段重复劳动来的:C++ Requests,头文件里#include <cpr/cpr.h>,一个cpr::Get(cpr::Url{"..."})拿到cpr::Response,状态码、正文、响应头、耗时全在结构体里。它是 libcurl 的薄封装,不是另起炉灶的协议栈,所以你原来对 curl 的理解、代理配置、CA 证书路径这些经验全部能平移过来。这份 1.3.0 的库文件适合两类人:一类是要在已有 C++ 工程里补几个 REST 调用、不想引入重型网络框架的;另一类是老项目锁死了 C++11 和旧编译器,新版本的 cpr 上不去,只能回退到 1.3.0 这一档。下面按“怎么编、怎么写、哪里翻车”的顺序拆。
2. 编译 cpr 1.3.0:CMake 选项、libcurl 依赖与三种集成姿势
拿到源码包第一件事不是cmake ..,而是先看清楚它怎么对待 libcurl。1.3.0 这一代的默认策略是把 curl 当第三方依赖一起编进来,走子模块或者内置目录;如果你的包是从压缩包解出来的,子模块目录大概率是空的,configure阶段就会报找不到 CURL 的 CMake 文件。这是这份资源最常见的第一个坎,跟代码质量没关系,纯粹是打包方式导致的。
2.1 先把依赖和工具链点一遍,别在 configure 阶段耗半小时
编译前确认三样东西:CMake 3.5 以上(1.3.0 的cmake_minimum_required卡得不高,但太低会缺target_include_directories的一些行为)、支持 C++11 的编译器、libcurl 的开发包。Linux 上开发包和运行时是分开的两个包,只装了运行时的机器上curl-config --cflags能跑,但头文件curl/curl.h找不到,CMake 一样会失败。Windows 上则要提前决定用哪套 curl:自己编的静态库、vcpkg 装的、还是系统里现成的 DLL 导入库,这三者的运行库选项(/MD 还是 /MT)不一样,混用会在链接期炸出一堆LNK2038。
# Linux 下确认依赖是否齐 curl-config --version # 看 libcurl 版本,7.40 以上基本都能跑 curl-config --cflags # 头文件路径 curl-config --libs # 链接参数,能看出是静态还是动态 pkg-config --modversion openssl # 要跑 https 就需要 cmake --version # 至少 3.5,建议 3.10+这三条命令的输出要留意两点。一是curl-config --libs里如果出现-lcurl但没带-lssl -lcrypto,说明系统 curl 是动态链接 OpenSSL 的,cpr 自己不需要再链 SSL;反过来如果 curl 是静态库,那 OpenSSL、zlib、甚至-lpthread -ldl都得由最终可执行文件补上。二是--version输出的 Features 行,SSL、IPv6、AsynchDNS这些标志决定了你后续能不能用 https、域名解析会不会阻塞,出问题时这是第一手证据。
2.2 三个关键选项:测试开关、系统 curl、SSL
1.3.0 的 CMakeLists 里有一堆option(),实际影响编译成败的主要是三个。第一个是CPR_BUILD_TESTS,它默认是开的,开了就会去找 googletest 并编译一大堆测试用例;如果你只是想拿库,这个选项除了拖慢编译、增加一次失败概率之外没有任何收益。第二个是CPR_FORCE_USE_SYSTEM_CURL(部分打包版本里叫CPR_USE_SYSTEM_CURL),打开后不再走内置 curl,直接用find_package(CURL)找到的系统库,离线环境和内网构建必须开。第三个是CPR_ENABLE_SSL,默认开,关掉之后 https 请求会在运行期直接失败,除非你确定只访问内网 http 服务,否则别动它。
# 推荐的一套构建命令(Linux / macOS) mkdir -p build && cd build cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCPR_BUILD_TESTS=OFF \ -DCPR_FORCE_USE_SYSTEM_CURL=ON \ -DCPR_ENABLE_SSL=ON \ -DCMAKE_INSTALL_PREFIX=/opt/cpr-1.3.0 cmake --build . --parallel 4 cmake --install .CMAKE_BUILD_TYPE=Release不只是优化等级的事,它同时决定了最终产物的运行库选择,Windows 上 Release 默认走 /MD,Debug 走 /MDd,这一步选错后面链接主工程时就得返工。CPR_BUILD_TESTS=OFF省掉的是 gtest 依赖和几十分钟编译时间,对产物没有任何影响。CPR_FORCE_USE_SYSTEM_CURL=ON是离线构建的关键,代价是版本受系统限制,如果系统 curl 低于 7.40,某些选项会静默不生效——这一点后面排查章节还会提。CMAKE_INSTALL_PREFIX建议指到一个独立目录而不是/usr/local,1.3.0 的头文件和系统里可能存在的其他版本 cpr 混在一起时,find_package找到哪个完全看运气,独立目录能让你用CMAKE_PREFIX_PATH精确指定。
编译产物通常是一个静态库(libcpr.a或cpr.lib)加一套头文件,头文件入口是cpr/cpr.h,它把cpr/api.h、cpr/response.h、cpr/session.h这些聚合进来,使用方只需要 include 这一个。如果构建时开了BUILD_SHARED_LIBS=ON,拿到的是动态库,此时 Windows 上导出符号的问题会冒出来,1.3.0 这一代对__declspec(dllexport)的处理不算完善,我的建议是能用静态库就别折腾动态库。
2.3 三种集成姿势,按工程规模选
第一种是add_subdirectory,把 cpr 源码目录整个拷进你的工程树,主 CMakeLists 里加一行add_subdirectory(third_party/cpr),然后target_link_libraries(app PRIVATE cpr)。好处是版本跟着代码走,换机器不用预装,坏处是每次全量编译都会带上 cpr 的几十个 cpp,且它的 CMakeLists 会往全局作用域里塞一些变量,偶尔和你自己的配置打架。
cmake_minimum_required(VERSION 3.10) project(http_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 姿势一:源码内嵌 set(CPR_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(CPR_FORCE_USE_SYSTEM_CURL ON CACHE BOOL "" FORCE) add_subdirectory(third_party/cpr) add_executable(http_demo main.cpp) target_link_libraries(http_demo PRIVATE cpr)注意set(... CACHE BOOL "" FORCE)这三行必须写在add_subdirectory之前,cpr 的option()一旦把缓存变量建好,后面再改就不生效了,这是 CMake 缓存机制的经典坑。
第二种是装到独立前缀再用find_package,适合多个工程共享一份编译产物。1.3.0 安装时会生成cprConfig.cmake一类的配置文件,主工程写find_package(cpr REQUIRED)加target_link_libraries(app PRIVATE cpr),配置时用-DCMAKE_PREFIX_PATH=/opt/cpr-1.3.0指过去。这种姿势的隐患是配置文件里记录的依赖路径是绝对路径,把编译产物拷到另一台机器上,curl 的路径对不上就会报错,跨机器分发时不如第一种稳。
第三种最粗暴:只把include/cpr和cpr/*.cpp(不含 tests、test 相关文件)拖进自己的工程,手工加进源文件列表,再自己链 curl。适合老项目、非 CMake 构建系统、或者要把 cpr 塞进一个已有 Makefile 的场景。代价是升级时要手工合并,且容易漏文件——cpr/目录下有几个文件名字很像(session.cpp、session.h、session_impl.cpp),漏一个就是链接期一堆 undefined reference。
3. 发第一个请求:Get、Post、Session 复用与超时参数怎么落
cpr 的 API 设计明显借鉴了 Python Requests 的思路:用花括号初始化列表传可选参数,底层再翻译成curl_easy_setopt。理解这一层映射关系,你就能预判哪些参数有效、哪些是“写了但不生效”。这一章按 GET、POST、Session 三块展开,每一块都给能直接跑的代码,并说明参数改动会带来什么后果。
3.1 Get 请求的四个参数位:Url、Parameters、Header、Timeout
最小可用调用只有 Url 一个参数,但工程里几乎不会这么写。URL 上的查询串用cpr::Parameters传,它会做百分号编码,中文和特殊字符不用自己处理;请求头用cpr::Header,它本质是个std::map,重复键会被覆盖;超时用cpr::Timeout,单位是毫秒。1.3.0 这一代的Timeout直接吃整数毫秒,后面版本改成了std::chrono的时长类型,从老代码往新版本迁移时这一行必然要改。
#include <cpr/cpr.h> #include <iostream> int main() { // 查询参数会被自动 URL 编码,中文不需要手动转义 cpr::Response r = cpr::Get( cpr::Url{"http://127.0.0.1:8080/api/items"}, cpr::Parameters{{"page", "1"}, {"size", "20"}, {"kw", "温度传感器"}}, cpr::Header{{"Accept", "application/json"}, {"User-Agent", "demo/1.0"}}, cpr::Timeout{3000} // 毫秒,这一版是整数 ); if (r.error.code != cpr::ErrorCode::OK) { std::cerr << "transport error: " << r.error.message << std::endl; return 1; } std::cout << "status=" << r.status_code << " elapsed=" << r.elapsed << "s" << " bytes=" << r.text.size() << std::endl; return 0; }这段代码里值得单独说的是cpr::Timeout{3000}。cpr 默认不设超时,等价于 libcurl 的“一直等”,在对方服务假死、或者是 DNS 解析卡住的情况下,这个调用会挂到你怀疑人生。3000 毫秒对局域网接口够用,公网接口建议 5000 到 8000。另一个细节是r.text.size()而不是strlen(r.text.c_str()),原因在第四章讲二进制响应时还会展开。
cpr::Header传std::map意味着键是有序的,同名头只能出现一次。需要发多个同名头(比如多个Set-Cookie之外的场景)时这一版做不到,只能退回直接用 libcurl 的curl_slist。
3.2 Post 的三种 body 形态,选错了服务端直接 415
POST 的坑集中在 Content-Type 上。cpr 提供两条路径:cpr::Payload走multipart/form-data,适合表单和文件;cpr::Body配cpr::Header里的 Content-Type,走原始字节流,适合 JSON。很多 415 Unsupported Media Type 就是这么来的——用了 Payload 却以为自己在发 JSON。
// 形态一:JSON 提交,必须显式声明 Content-Type std::string json = R"({"device":"sensor-01","value":23.5})"; cpr::Response r1 = cpr::Post( cpr::Url{"http://127.0.0.1:8080/api/reading"}, cpr::Body{json}, cpr::Header{{"Content-Type", "application/json; charset=utf-8"}}, cpr::Timeout{5000} ); // 形态二:表单提交,Payload 自动生成 boundary cpr::Response r2 = cpr::Post( cpr::Url{"http://127.0.0.1:8080/api/login"}, cpr::Payload{{"user", "demo"}, {"pass", "123456"}}, cpr::Timeout{5000} ); // 形态三:Basic 认证,两参数版本默认走 BASIC cpr::Response r3 = cpr::Get( cpr::Url{"http://127.0.0.1:8080/api/private"}, cpr::Authentication{"demo", "123456"}, cpr::Timeout{5000} );cpr::Body传的是std::string,序列化 JSON 的活它不干,自己拼或者用现成的库。charset=utf-8建议带上,某些服务端框架在没有 charset 时会按 latin-1 解析,中文变乱码——表现是接口返回 200 但字段全错,比直接报错更难查。cpr::Payload的 boundary 是 libcurl 自动生成的,不要自己往 Header 里塞 Content-Type,塞了反而会破坏 boundary 匹配。
值得注意的是 1.3.0 的cpr::Authentication只有用户名密码两个参数,认证模式默认 BASIC。要用 Digest 或者 NTLM,这一版的接口没有直接暴露,得靠cpr::Session的底层设置或退回 libcurl。
3.3 Session:连接复用不是优化,是长跑服务的必需品
短脚本用cpr::Get一次性调用没问题,但如果你在循环里对同一个域名发几千次请求,每次cpr::Get都会走一遍curl_easy_init→ 建连 → TLS 握手 →curl_easy_cleanup。TLS 握手在跨地域链路上能占到端到端延迟的一半以上,这时候cpr::Session的价值就出来了:一个 Session 持有一个 curl handle,复用 TCP 连接和 TLS 会话。
#include <cpr/cpr.h> #include <vector> #include <thread> // 每个线程持有自己的 Session,绝不跨线程共享 void worker(int id, const std::vector<std::string>& ids) { cpr::Session session; session.SetUrl(cpr::Url{"http://127.0.0.1:8080/api/items"}); session.SetHeader(cpr::Header{{"Accept", "application/json"}}); session.SetTimeout(cpr::Timeout{3000}); for (const auto& item : ids) { session.SetParameters(cpr::Parameters{{"id", item}}); cpr::Response r = session.Get(); // 复用底层连接 if (r.error.code == cpr::ErrorCode::OK && r.status_code == 200) { // 处理 r.text } } } int main() { std::vector<std::string> ids{"a1", "a2", "a3"}; std::vector<std::thread> pool; for (int i = 0; i < 4; ++i) pool.emplace_back(worker, i, ids); for (auto& t : pool) t.join(); return 0; }SetParameters每次循环覆盖前一组的查询参数,Session 上的设置是“最后写入生效”,不会累积,这点和很多人直觉相反。并发部分的写法是每个线程 new 一个 Session,不要做成全局单例加锁——Session 内部的 curl handle 不是线程安全的,共享会导致偶发的段错误或者响应串包,而且这种 bug 在低并发压测下根本复现不出来,属于典型的血泪经验。
如果你的 1.3.0 打包版本里 Session 的某些 setter 名字对不上(不同打包方对上游的裁剪程度不一样),直接退回cpr::Get加参数列表也能跑通,只是失去连接复用,功能上不受影响。
4. 响应对象与错误处理:status_code、elapsed、error 三个字段怎么读
cpr::Response看着简单,实际分层的语义很容易被误读:status_code是 HTTP 层的结果,error.code是传输层的结果,两者互斥且优先级不同。搞混了就会出现“明明返回 404 却当成网络故障重试三次”这种逻辑错误。这一章把字段语义、错误分层、二进制与编码三件事讲透。
4.1 哪些字段能直接信,哪些必须先判空
status_code是整数,HTTP 请求成功到达服务端并拿到响应头之后才有意义;如果 TCP 都没连上,这个值是 0。text是响应正文,注意它是原始字节,不是“文本”,服务端返回 gzip 压缩体而你没让 curl 解压时,text里就是压缩后的二进制。header是响应头,键的大小写取决于服务端,cpr 这一版没做统一小写化,取Content-Type时最好把两种写法都试一遍。elapsed是耗时,1.3.0 里是double类型的秒数,包含 DNS、建连、传输全过程;新版本换成std::chrono::duration之后打印方式要跟着改,直接从老代码抄会编译不过。url是最终请求地址,跟随重定向后这里可能和传进去的不一样,做日志时用它比用入参准。
最容易踩的是header取值的写法。r.header["content-type"]在服务端返回Content-Type时拿到的是空串,因为std::map的键是严格匹配的。稳妥的写法是先find再取值,或者用r.header.count(...)判断。
// 安全的响应头读取方式 auto it = r.header.find("Content-Type"); if (it == r.header.end()) it = r.header.find("content-type"); if (it != r.header.end()) { std::cout << "content-type: " << it->second << std::endl; }4.2 错误分两层:传输失败和 HTTP 失败不是一回事
判断请求是否“成功”,必须先看传输层再看 HTTP 层,顺序反了会漏掉错误。
cpr::Response r = cpr::Get(cpr::Url{"https://127.0.0.1:8443/api/health"}, cpr::Timeout{2000}); if (r.error.code != cpr::ErrorCode::OK) { // 传输层失败:连不上、TLS 握手失败、超时、DNS 解析失败 std::cerr << "code=" << static_cast<int>(r.error.code) << " msg=" << r.error.message << std::endl; } else if (r.status_code >= 200 && r.status_code < 300) { // 业务成功 } else if (r.status_code >= 400 && r.status_code < 500) { // 客户端错误,重试没意义,检查参数和鉴权 } else if (r.status_code >= 500) { // 服务端错误,可以带退避重试 }这个分层结构的意义在于决定“要不要重试”。error.code != OK里既有可以重试的(连接超时、DNS 临时失败),也有绝对不能重试的(证书校验失败、URL 格式错误),单看error.message字符串匹配不可靠,因为不同 libcurl 版本的文案不一样。1.3.0 的cpr::ErrorCode枚举覆盖了常见场景,用枚举值做分支比字符串匹配稳。
还有一类隐蔽问题:libcurl 在收到 4xx/5xx 时默认不认为是错误,error.code仍然是OK,所以只判error.code会把所有 HTTP 错误当成成功。这个设计跟底层一致,但对上层业务不友好,必须在封装层补上status_code的判断。
4.3 二进制响应和中文编码:两个必须动手验证的点
r.text是std::string,能装任意字节,但只要你用c_str()或strlen去碰它,遇到\0就截断。下载图片、PDF、protobuf 时这是高频翻车点,表现是文件大小比预期小、内容损坏。
// 正确:按长度写文件,不依赖 C 字符串 std::ofstream out("download.bin", std::ios::binary); out.write(r.text.data(), static_cast<std::streamsize>(r.text.size())); out.close(); // 错误示范:遇到 0x00 就断,文件必然不完整 // out << r.text.c_str();中文乱码是另一类问题,根源在字节流本身没有编码信息,全靠 Content-Type 或约定。服务端返回application/json不带 charset 时,cpr 原样给你字节,控制台打印乱码不代表数据错了,先确认终端的代码页(Windows 控制台默认 GBK),再把字节按 UTF-8 解码验证。判断方法很简单:r.text.size()和预期字符数对不上时是编码问题,能对上但显示乱就是终端问题。1.3.0 不提供任何编码转换,全交给调用方。
5. 踩坑排查:六条从链接失败到证书报错的现场记录
这一章是我在不同机器、不同工程里反复遇到的六种故障,每条按“现象—原因—解决”写。这些坑的共同点是报错信息指向的位置和真实原因往往差了一层,只看第一行报错会修错方向。
5.1 现象:configure 阶段报找不到 CURLConfig.cmake;原因:子模块没初始化或走了内置 curl;解决:强制用系统 curl
在离线机器上解压源码包,cmake ..直接失败,日志里一行Could NOT find CURL。翻源码目录发现cpr/curl或者third_party/curl是空文件夹——压缩包没法带子模块内容。解决是两步:先装系统 curl 开发包,然后-DCPR_FORCE_USE_SYSTEM_CURL=ON重新 configure。如果老一点的打包版本里这个选项名不生效,用cmake -LAH .. | grep -i curl把实际选项名打出来再传。装完系统包还是找不到时,问题多半在 CMake 的搜索路径,手动加-DCURL_INCLUDE_DIR=/usr/include/x86_64-linux-gnu -DCURL_LIBRARY=/usr/lib/x86_64-linux-gnu/libcurl.so指过去。
5.2 现象:链接期一串 undefined reference tocurl_easy_init;原因:静态库链接顺序或依赖没传递;解决:让 CMake 管依赖,别手写链接串
用 Makefile 手工链接时最容易出这个,cpr 的静态库排在 curl 前面,链接器扫一遍就过去了,符号没人填。CMake 场景下如果还出,通常是find_package(CURL)用的是动态库而 cpr 编的是静态,或者反过来,导致-lcurl没被正确带进最终链接命令行。解决思路是别自己写-lcpr -lcurl,用target_link_libraries(app PRIVATE cpr)让 cpr 的 target 把自己的依赖一起传递出来;实在要手写,顺序是main.o -lcpr -lcurl -lssl -lcrypto -lz -lpthread -ldl,被依赖的放后面。
5.3 现象:运行时报 SSL peer certificate or SSH remote key was not OK(curl code 60);原因:CA 证书路径缺失或用了自签证书;解决:指定 CA 包,测试环境才关校验
静态编译的 curl 常常不带默认 CA 路径,运行时找不到/etc/ssl/certs/ca-certificates.crt就报 60。先确认系统里 CA 包存在,然后通过环境变量CURL_CA_BUNDLE或者在代码里用 cpr 的 CA 配置项指过去。自签证书的内网服务只能两条路:把自签 CA 加到信任链,或者在测试代码里cpr::VerifySsl{false}。后者千万别进生产——关掉校验之后,中间人攻击和证书过期都感知不到,这个后悔药不好吃。
5.4 现象:请求“卡住”几分钟不返回,超时参数像没生效;原因:默认无超时,且 Timeout 只覆盖部分阶段;解决:显式设置并加低速判据
不设cpr::Timeout时,cpr 不给你兜底。设了之后仍然可能卡,原因在于 libcurl 的超时是分阶段的(连接超时、总超时),cpr 这一版映射的是总的传输超时,对“服务器慢慢吐字节”这种低速场景约束有限。解决是在应用层加一个整体耗时上限,或者用 libcurl 的低速限制参数(低于某个速率持续多少秒就断开)。我的习惯是:业务侧必须有一个墙钟超时,cpr 的 Timeout 设成墙钟的 1.5 倍作为兜底,两层都设,任何一层失效另一层能救。
5.5 现象:Windows 下LNK2038: mismatch detected for 'RuntimeLibrary';原因:cpr 与主工程的运行库选项不一致;解决:统一 /MD 或 /MT
这个错在混用预编译库时几乎必现。cpr 用 Release 编出来是 /MD,主工程 Debug 是 /MDd,链接器直接拒绝。解决办法只有统一:要么主工程和 cpr 都用同一套配置编,要么在 cpr 的 CMake 里显式设置CMAKE_MSVC_RUNTIME_LIBRARY跟主工程对齐。还有一种是主工程用了 /MT(静态 CRT),而 cpr 依赖的 curl 是 /MD 编的,这种改不动就得重编 curl,工作量不小,提前规划比事后救火便宜。
5.6 现象:压测时偶发崩溃或响应串包;原因:多线程共享同一个 Session;解决:线程内独占
单线程跑一万次没问题,四个线程跑一百次就崩,或者 A 请求拿到了 B 的响应体。这是 curl handle 跨线程使用的典型症状,且复现概率跟机器负载正相关,本地很难稳定重现。解决就是线程内各建一个 Session,用完析构。如果确实需要全局共享配置(比如统一的 base header),共享不可变的配置结构体,Session 本身不共享。
6. 把 cpr 包成一层薄封装:重试、超时、降级与三个验证动作
裸调cpr::Get的代码散在业务里,迟早会退化成每个调用点各写一套超时和错误处理。我的做法是在 cpr 之上压一层五十行左右的封装,把所有“策略”集中到一个函数里,业务侧只关心“拿到 JSON 还是拿到错误”。这层封装要解决三件事:区分可重试错误、给每次尝试独立的超时预算、把错误信息结构化到能打日志的程度。
#include <cpr/cpr.h> #include <chrono> #include <thread> #include <string> struct HttpResult { bool ok = false; int status = 0; std::string body; std::string errmsg; }; // 只对传输层错误和 5xx 重试,4xx 直接返回 HttpResult GetJson(const std::string& url, int timeout_ms = 3000, int retries = 2) { HttpResult out; for (int attempt = 0; attempt <= retries; ++attempt) { cpr::Response r = cpr::Get( cpr::Url{url}, cpr::Header{{"Accept", "application/json"}}, cpr::Timeout{timeout_ms}); if (r.error.code == cpr::ErrorCode::OK) { out.status = static_cast<int>(r.status_code); out.body = r.text; out.ok = (r.status_code >= 200 && r.status_code < 300); if (out.ok || r.status_code < 500) { if (!out.ok) out.errmsg = "http " + std::to_string(r.status_code); return out; // 4xx 不重试 } out.errmsg = "server error " + std::to_string(r.status_code); } else { out.errmsg = r.error.message; // 传输层错误,可重试 } if (attempt < retries) { std::this_thread::sleep_for( std::chrono::milliseconds(200 * (1 << attempt))); // 200/400ms 退避 } } return out; }这段代码的三个参数各有讲究。timeout_ms是单次尝试的预算,不是总预算,三次尝试最坏耗时接近三倍,业务侧如果对总耗时有要求,得在外面再套一层计时。retries给 2 意味着最多三次尝试,超过这个数在同步调用里会明显拖慢上游,真要长重试应该扔到异步队列。退避用200 * (1 << attempt)是指数退避的简化版,服务端刚重启时立刻重试通常还是失败,延后一点命中率会明显上升。返回值里保留status和errmsg两个字段,是为了让调用方能区分“对方返回了 500”和“根本没连上”,这两种情况在告警策略上完全不同。
验证这层封装,我固定跑三个动作。一是起一个本地服务,让它按顺序返回 500、500、200,确认重试生效且最终成功,同时数一下日志里应该有三次请求记录。二是把服务停掉再调,确认走的是传输层错误分支、退避时间符合预期、总耗时在timeout_ms * 3 + 600ms以内。三是发一个 404,确认只请求一次、没有多余重试。这三个用例覆盖了重试逻辑最容易写错的三种边界,写完就测,比上线后靠日志猜便宜得多。
早些年我在一个采集程序里图省事,直接裸调cpr::Get不设超时,结果对方接口某天开始只建连不回包,线程池被全部占满,整个服务假死,排查了两个小时才定位到是超时没设。从那以后我每次引入一个新的 HTTP 封装,都会强制先跑一遍“断网、慢响应、500 错误”这三个用例,再写业务代码。上面这套封装和验证步骤,照抄改改就能用,希望帮到你。
本文还有配套的精品资源,点击获取