资讯动态

cpr 1.3.0 实战:C++ HTTP 请求编译、链接与避坑

发布时间:2026/10/29 23:20:46 来源:尧图企业网站定制
简介cpr-1.3.0 是 C Requests 的预编译库文件包基于 libcurl 封装面向需要在 C 项目中快速集成 HTTP 请求能力的开发者尤其适合使用 VS2015 构建、希望跳过 libcurl 与 cpr 自行编译配置环节的工程场景。资源包共 27 个文件以 21 个 .h 头文件、4 个 .lib 导入库和 2 个 .dll 动态库为主整体约 738KB头文件覆盖请求构造、响应处理、会话保持、认证、超时、代理与 SSL 选项等常用能力lib 与 dll 按 Debug/Release 分目录提供便于在调试和发布环境间直接切换。已有 691 人学习下载说明其在 C HTTP 客户端选型中具有一定参考价值。对于不想在依赖编译上耗费时间的读者可直接将对应版本接入工程省去库文件生成与链接配置的重复工作把精力放在业务请求封装、接口联调与问题排查上该包编译于 2019 年 2 月 14 日适合需要稳定旧版工具链兼容性的项目参考使用。1. 手上只有 C 标准库却要发 HTTP 请求第一次搜 cpr 1.3.0 的人场景基本一样手头是个 C 工程要调一个返回 JSON 的第三方接口翻遍标准库发现连个 HTTP 客户端都没有翻出 libcurl 又被curl_easy_setopt那一长串 CURLOPT 劝退。cpr 全称 C Requests做的就是把这层 C 接口包成接近 Python requests 的写法——cpr::Get(cpr::Url{...})一行拿到status_code和text错误、超时、请求头都用值对象传参不用记句柄生命周期。这份 1.3.0 的库文件适合两类人一类是需要在离线环境里把依赖编进产物的嵌入式或客户端同学另一类是写测试工具、爬接口、做联调小工具的后端。不适合追求 HTTP/2 多路复用、异步协程的高并发网关场景那属于另一个量级的东西。下面按「怎么编出来 → 怎么调对 → 哪些地方会翻车」的顺序拆。2. 把 cpr 1.3.0 编出来CMake 选项与产物取舍2.1 先看清目录结构再决定链谁解开压缩包之后别急着 cmake先看三件事头文件在哪、CMake 有没有带find_package的配置文件、以及 curl 是自带还是外链。cpr 的源码布局很规整include/cpr/下是一堆单职责头文件url.h、header.h、session.h、response.hcpr/目录是实现test/是它自己的测试用例。tar -xzf cpr-1.3.0.tar.gz cd cpr-1.3.0 ls # CMakeLists.txt cpr/ include/ test/ LICENSE README.md find include -name *.h | head -20 # include/cpr/api.h include/cpr/session.h include/cpr/response.h ...这一步的意义在于知道session.h存在你后面遇到「怎么复用连接」就有地方翻知道include/cpr/是公共头你就不会把cpr/里的内部实现目录加进 include path那会导致符号重复定义。接下来看它暴露了哪些编译开关。不同小版本开关改名是常事最靠谱的办法是让 CMake 自己列出来而不是照抄某篇博客。mkdir -p build cd build cmake -LH .. 2/dev/null | grep -iE cpr|curl|ssl|test # CPR_BUILD_TESTS:BOOLON # CPR_ENABLE_SSL:BOOLON # CPR_USE_SYSTEM_CURL:BOOLOFF # BUILD_SHARED_LIBS:BOOLOFF注意如果grep出来一个都没有说明这个版本的开关没加CPR_前缀把过滤词换成curl|ssl|test再找一遍。名字对不上时硬传-DCMake 只会默默忽略最后你以为关掉了测试其实没关。2.2 Linux 下的一次完整构建我的习惯是优先用系统里的 curl而不是让 cpr 把 curl 源码一起编进去。原因是自带 curl 会把一堆静态符号塞进libcpr.a后面换 SSL 后端或者打补丁都得整体重编用系统 curl 的话libcpr.a里 curl 符号是未定义的由你在最终链接时决定链哪个边界清楚得多。sudo apt-get install -y libcurl4-openssl-dev libssl-dev zlib1g-dev mkdir -p build cd build cmake .. \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX/opt/cpr-1.3.0 \ -DCPR_BUILD_TESTSOFF \ -DCPR_ENABLE_SSLON \ -DCPR_USE_SYSTEM_CURLON \ -DBUILD_SHARED_LIBSOFF make -j$(nproc) make install ls /opt/cpr-1.3.0/lib /opt/cpr-1.3.0/include参数逐个说清楚CMAKE_BUILD_TYPEReleasecpr 大量用到std::shared_ptr和返回值优化Debug 下性能差距在小工具里不明显但一旦你拿它压测就会看出来。CMAKE_INSTALL_PREFIX装到独立前缀避免污染/usr/local。以后要并存 1.3.0 和更新版本时这个前缀就是你的后悔药。CPR_BUILD_TESTSOFF必须关。默认是开的而它的测试会依赖外部网络和额外的测试框架离线机器上十有八九卡在 configure 或者 build 的测试目标上。CPR_ENABLE_SSLON调 HTTPS 接口必须开关掉之后https://的 URL 会直接返回传输层错误。CPR_USE_SYSTEM_CURLON如上换来链接期可控性。BUILD_SHARED_LIBSOFF静态库产物是libcpr.a分发时不用带 so。2.3 业务工程怎么链接顺序和依赖别写错装完之后写业务代码最省事的方式是不折腾find_package直接把前缀目录写进 CMake。很多人在这一步失败不是因为路径错而是库的书写顺序错——静态库是单向解析的-lcpr必须写在-lcurl前面。cmake_minimum_required(VERSION 3.10) project(cpr_demo CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(demo main.cpp) target_include_directories(demo PRIVATE /opt/cpr-1.3.0/include) # 顺序cpr 在前它依赖的 curl / ssl / crypto / z 在后 target_link_libraries(demo PRIVATE /opt/cpr-1.3.0/lib/libcpr.a curl ssl crypto z pthread )如果不用 CMake手写一行 g 等价于g -stdc11 main.cpp -o demo \ -I/opt/cpr-1.3.0/include \ -L/opt/cpr-1.3.0/lib -lcpr \ -lcurl -lssl -lcrypto -lz -lpthreadWindows 上差别主要在依赖集合。用 MSVC 时如果 curl 是静态链的你还要补系统库缺一个就是满屏LNK2019平台额外需要链接的库常见缺失症状Linuxpthread、ssl、crypto、zundefined reference to pthread_createWindows / MSVCws2_32、crypt32、wldap32unresolved external symbol __imp_WSAStartupmacOS-framework Security、-framework CoreFoundation链接 curl 时报 Security 符号缺失提示Windows 上如果同时存在 Debug 和 Release 两套 curl一定要让 cpr 和 curl 的运行时配置一致混用 CRT 会在第一次请求返回时崩在析构里现象特别像「库有 bug」其实是运行库不匹配。3. 从一次 Get 到会话复用参数对象的语义3.1 一个请求的生命周期全在 Response 里cpr 的设计是「传值即配置」所有参数都是临时对象你不需要手动释放任何东西。真正的信息都收在cpr::Response里写业务代码时必须先看error.code再看status_code——这个顺序搞反是后面所有玄学问题的根源。#include cpr/cpr.h #include iostream int main() { cpr::Response r cpr::Get( cpr::Url{http://127.0.0.1:8080/api/items}, cpr::Parameters{{page, 1}, {size, 20}}, cpr::Header{{Accept, application/json}, {Authorization, Bearer token}}, cpr::Timeout{3000} // 单位是毫秒 ); // 第一层传输层。连不上、DNS 失败、超时都落在这里 if (r.error.code ! cpr::ErrorCode::OK) { std::cerr transport: r.error.message \n; return 1; } // 第二层应用层。此时 status_code 一定有值 std::cout status r.status_code elapsed r.elapsed s url r.url \n; std::cout r.text.substr(0, 200) \n; return 0; }几个字段的实际含义值得说清status_code为 0 表示请求根本没走完不能拿去和 200 比较elapsed是秒浮点不是毫秒日志里乘 1000 再打url是最终请求的 URL如果有重定向它和你传进去的可能不一样排查签名类接口时务必核对它。cpr::Parameters会自动做 URL 编码吗只做一部分——它会把键值拼成kvkv挂到 query 上但对非 ASCII 字节的处理依赖你传进去的内容本身。中文参数的问题第 5 章单独讲。3.2 Session连接复用的正确姿势一次性的cpr::Get每次都会新建 curl handle短连接场景无所谓循环调用几千次就会看到大量 TIME_WAIT。要复用就上cpr::Session它持有一个 handle同一个 Session 上的连续请求会走 keep-alive。#include cpr/cpr.h cpr::Session session; session.SetUrl(cpr::Url{http://127.0.0.1:8080/api/items}); session.SetHeader(cpr::Header{{Authorization, Bearer token}}); session.SetTimeout(cpr::Timeout{3000}); for (int page 1; page 3; page) { session.SetParameters(cpr::Parameters{{page, std::to_string(page)}, {size, 20}}); cpr::Response r session.Get(); if (r.error.code ! cpr::ErrorCode::OK) { continue; // 单个分页失败不中断整体 } // 处理 r.text }关键点在于SetParameters是覆盖而不是追加所以循环里每次重设是安全的不会越拼越长。另外Session不可拷贝想放到容器里要用std::unique_ptr或者直接按值移动。注意复用能不能真的省下握手取决于服务端有没有返回Connection: close。有些网关默认短连接这种情况下 Session 复用只是省了 handle 创建的开销TCP 还是每次新建别指望它能把 RT 降一半。3.3 POST 的三种体选错了服务端直接 415cpr 里 POST 的请求体有三条路语义完全不同选错就是 400/415cpr::Body{{\k\:\v\}}原始字节Content-Type 要自己设。调 JSON 接口就走这条。cpr::Payload{{k, v}}自动拼成kvkv并设application/x-www-form-urlencoded对应传统表单提交。cpr::Multipart{...}multipart/form-data上传文件用。#include cpr/cpr.h // 1) JSON cpr::Response r1 cpr::Post( cpr::Url{http://127.0.0.1:8080/api/items}, cpr::Header{{Content-Type, application/json}}, cpr::Body{R({name:demo,count:3})} ); // 2) 表单 cpr::Response r2 cpr::Post( cpr::Url{http://127.0.0.1:8080/login}, cpr::Payload{{user, demo}, {pass, secret}} ); // 3) 文件上传 cpr::Response r3 cpr::Post( cpr::Url{http://127.0.0.1:8080/upload}, cpr::Multipart{{bucket, logs}, {file, cpr::File{/tmp/a.log}}} );第一段里如果不写Content-Typecpr 不会替你猜服务端拿到的是text/plain或者干脆没有返回 415 时先去抓包看请求头。第三段的Multipart在 1.3.0 附近的小版本上接口命名有过调整编译不过就先确认grep -rn class Multipart\|struct Multipart\|MultipartForm include/cpr/看到什么名字就用什么名字别硬套别处的示例代码。4. 超时、错误码与重试把 cpr::Error 当唯一真相4.1 两段超时的区别以及 1.3.0 的边界cpr::Timeout对应的是整个传输过程的总时长从发起到收完最后一个字节单位毫秒。它不是连接超时。一个典型误判是接口在服务端卡 30 秒才返回你把Timeout设成 5000结果请求 5 秒就断了你以为网络有问题其实是自己设的。// 总时长 5 秒服务端慢查询超过 5 秒就会被本端掐断 cpr::Response r cpr::Get( cpr::Url{http://127.0.0.1:8080/api/slow}, cpr::Timeout{5000} );想单独控制「连不上」这一段先确认这个版本有没有暴露独立参数grep -rn ConnectTimeout\|CONNECTTIMEOUT include/cpr/有就用没有的话常见做法是退回 libcurl 层面去设CURLOPT_CONNECTTIMEOUT_MS或者干脆把总超时压到一个能接受的值靠失败重试兜住。别指望一个Timeout同时管两件事那会让线上排障变成黑匣子。4.2 不要穷举 ErrorCode只判 OKcpr::ErrorCode是个枚举不同小版本之间枚举项增删是常态。你写switch覆盖了七八个分支换一个版本编译就报缺 case。我的做法是只判不等于OK剩下的靠error.message打日志cpr::Response r cpr::Get(cpr::Url{http://127.0.0.1:8080/ping}, cpr::Timeout{2000}); if (r.error.code ! cpr::ErrorCode::OK) { // message 里是底层 curl 的描述够定位了 std::cerr fail: r.error.message \n; } else if (r.status_code 0) { // 极少见但一旦出现说明传输层状态异常不能当成功处理 std::cerr no status line\n; }status_code 0这个判断看着多余实际很有用在连接被中间层中断、响应头没解析完整的情况下可能出现error.code是 OK 但status_code为 0 的边界直接拿去做 200判断会走进一个「既不成功也不报错」的分支日志里什么都看不到。4.3 一个能直接抄的重试封装重试不是无脑套循环。要区分两类失败传输层失败连不上、超时值得重试5xx 值得重试而 4xx 重试一万次也是错的——参数错了就是错了。另外 POST 重试要考虑幂等下单、扣款这类接口重试会重复写入。#include cpr/cpr.h #include chrono #include thread #include iostream cpr::Response GetWithRetry(cpr::Session session, int max_attempts 3) { cpr::Response r; for (int attempt 1; attempt max_attempts; attempt) { r session.Get(); bool transport_failed (r.error.code ! cpr::ErrorCode::OK); bool server_error (r.status_code 500); if (!transport_failed !server_error) { return r; // 正常返回4xx 也直接返回 } if (attempt max_attempts) { break; // 最后一次不再睡直接交回调用方 } // 线性退避200ms / 400ms / 600ms避免同时重试打爆对端 std::this_thread::sleep_for( std::chrono::milliseconds(200 * attempt)); } return r; }这段的价值在三个细节一是 4xx 直接返回把判断权交回业务层二是最后一次尝试后不再 sleep否则整个调用会白白多等一个退避周期三是退避用线性而非固定间隔多个客户端同时失败时不会形成整齐的重试脉冲。生产环境里如果对端扛不住把200 * attempt换成指数加随机抖动即可接口不用改。5. 踩坑记录五个让 cpr 1.3.0 翻车的现场5.1 链接期找不到 curl_easy_init现象是编译通过、链接报错一屏undefined reference to curl_easy_init而你的 CMakeLists 里明明写了-lcurl。原因是静态库的解析顺序。libcpr.a里引用了 curl 的符号链接器从左到右扫描如果-lcurl出现在-lcpr之前扫到 curl 的时候还没有任何东西需要它就被丢弃了后面 cpr 再要就找不到了。解决办法很简单把顺序调成-lcpr -lcurl依赖方永远在被依赖方左边。用 CMake 的话更省心直接写target_link_libraries(demo PRIVATE cpr curl)让 CMake 去排。顺手确认一下 curl 的开发包真的装了pkg-config --libs libcurl能出结果才算数。5.2 返回体是一串乱码JSON 解析直接抛异常现象是r.text开头是\x1f\x8b这种不可见字符拿去做 JSON 解析必失败但用浏览器或 curl 命令行请求同一个地址却完全正常。原因是服务端返回了 gzip 压缩内容而你的请求里带了Accept-Encoding: gzip有些中间层会替你加cpr 1.3.0 这一侧没有做透明解压压缩后的原始字节就原样躺在text里了。解决办法有两条选一条即可一是在请求头里显式声明cpr::Header{{Accept-Encoding, identity}}让服务端不要压缩二是自己拿到原始字节后用 zlib 解一遍。前者简单代价是流量变大后者省流量但多一层依赖。判断是否踩了这个坑看r.header[Content-Encoding]有没有值一眼就知道。5.3 中文参数变成问号或者服务端说签名不对现象是 URL 里带中文搜索词服务端收到的却是????或者签名校验怎么都对不上。原因是 query 里的非 ASCII 字节没有做百分号编码直接拼进了 URL。cpr 的Parameters会拼kv但对多字节字符的处理取决于版本指望它全自动是不现实的。解决办法是在传参前自己编码一次只对 value 做别把一起编了#include string #include cstdio std::string UrlEncode(const std::string s) { std::string out; char buf[4]; for (unsigned char c : s) { // 保留非保留字符其余一律百分号编码 if (isalnum(c) || c - || c _ || c . || c ~) { out static_castchar(c); } else { std::snprintf(buf, sizeof(buf), %%%02X, c); out buf; } } return out; }调用时cpr::Parameters{{q, UrlEncode(中文关键词)}}。注意编码后的字符串里不会再有和所以不会被二次拆分。做完这一步再去看签名基本就对了。5.4 多线程里共用一个 Session偶发崩溃现象是单线程跑一万次没问题一上线程池就随机段错误栈回溯指向 curl 内部复现概率跟机器核数有关。原因是cpr::Session底层持有一个 curl handle而 curl 的单个 easy handle 不是线程安全的两个线程同时调Get()会同时操作同一块状态。解决办法是「一个线程一个 Session」或者干脆用无状态的cpr::Get让每次请求自己建 handle。如果一定要共享就在外面套互斥锁但那样串行化了失去了并发的意义。更常见的做法是用线程局部存储#include cpr/cpr.h cpr::Session ThreadSession() { thread_local cpr::Session s; // 每个线程独立一份 s.SetTimeout(cpr::Timeout{3000}); return s; }thread_local的析构在 TLS 里执行要注意别在静态对象析构阶段还去用它程序退出前把线程 join 干净。5.5 登录成功第二个请求还是 401现象是用cpr::Post登录拿到了 200紧接着请求用户信息却返回 401服务端说没带 Cookie。原因是 cpr 不会跨请求保存 Cookie。每次调用都是独立的除非你显式把上一次响应里的 Cookie 传下去。解决办法是从第一次响应里取出来塞进后续请求cpr::Response login cpr::Post( cpr::Url{http://127.0.0.1:8080/login}, cpr::Payload{{user, demo}, {pass, secret}} ); // login.cookies 是 cpr::Cookies可直接复用 cpr::Response me cpr::Get( cpr::Url{http://127.0.0.1:8080/api/me}, login.cookies );如果服务端把会话放在响应头而不是Set-Cookie里那就手动从login.header[X-Token]取再通过cpr::Header带上。用Session的话把SetCookies设一次后续请求会一直带着。6. 再包一层把 cpr 关进一个可替换的薄壳里直接用 cpr 的 API 写业务写到第三个接口就会发现到处都是if (r.error.code ! cpr::ErrorCode::OK)。我的做法是在 cpr 外面套一个二十行的薄壳只暴露三个东西结果结构体、一个Request函数、一个配置结构体。这样以后换 HTTP 库改一个文件就够。#include cpr/cpr.h #include string struct HttpResult { bool ok false; // 业务可用性不是传输层 int status 0; std::string body; std::string error; // 非空即为失败原因 }; HttpResult Request(const std::string url, const cpr::Header headers, int timeout_ms 3000) { HttpResult out; cpr::Response r cpr::Get(cpr::Url{url}, headers, cpr::Timeout{timeout_ms}); if (r.error.code ! cpr::ErrorCode::OK) { out.error r.error.message; return out; } out.status r.status_code; out.body r.text; out.ok (r.status_code 200 r.status_code 300); if (!out.ok) { out.error http std::to_string(r.status_code); } return out; }壳里只做四件事判传输层、判状态码、把 body 拷出来、把错误转成人能读的字符串。业务层拿到的永远是HttpResult不再碰cpr::任何类型编译器会帮你检查有没有漏判。超时值按场景分档给别全用一个数调用场景建议 Timeout说明同机房内部接口10002000 ms超过说明下游有问题宁可快速失败跨网络第三方查询30005000 ms留出 TLS 握手和首包时间文件或大报文下载30000 ms 以上总时长型超时按体积估验证这套东西有没有接对我用三步先用cpr::Get打一个确定返回 200 的地址确认链路通再故意把 URL 端口改错确认error.code分支被走到、日志有输出最后把服务端人为拖到超过超时阈值确认失败路径返回的是超时而不是空 body。这三步跑完这套封装基本就不会在半夜给你制造惊喜了。从那以后我每次引入一个新的 HTTP 依赖都会先把「传输层失败」和「应用层失败」分开写成两行判断再谈业务逻辑——这个习惯救过我很多次。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑