简介:cpr-1.3.0 是面向 C++ 开发者的预编译 HTTP 客户端库文件包,基于 C++ Requests 项目封装 libcurl,适合需要在 VS2015 环境中快速接入网络请求、又不想从源码编译依赖的开发者。包内共 27 个文件,以 21 个头文件和 4 个 lib、2 个 dll 为主,头文件对应会话、响应、认证、超时、代理、重定向、SSL 选项等接口,lib 与 dll 分别提供 Debug/Release 链接库和 libcurl 动态库,压缩包约 738KB,解压后可直接加入工程引用。资源省去自行编译 cpr 与 libcurl 的时间,便于快速验证 GET/POST、参数传递、Cookie、超时与代理等常见调用,也适合作为课程实验或小工具项目的 HTTP 通信基础。已有 691 人学习,对希望减少环境配置、专注业务代码的 C++ 学习者较有参考价值。
1. 从一段两百行的 curl 胶水代码说起:cpr 1.3.0 到底替你封装了什么
接手一个 C++ 边缘服务时,最怕看到业务代码里散着几十处curl_easy_setopt:超时、header、回调、内存释放全挤在一起,改一个字段要翻三处。cpr 1.3.0(C++ Requests)就是冲这个场景来的——它把 libcurl 的 C 状态机包成 RAII 风格的 C++ 对象,Url、Parameters、Header、Body、Timeout都是可组合的请求选项,Response直接给出status_code、text、header和error。它不试图替代重型网络框架,而是让一个 C++11 工程在半小时内拥有接近脚本语言的 HTTP 调用体验。适合谁?手里已有 libcurl、又不想在业务层直接碰CURLOPT_*的团队;需要 HTTPS、表单、JSON、文件下载,但不想引入完整 RPC 栈的维护者。不适合谁?需要 HTTP/2 精细控制、连接池跨线程共享、或者必须零依赖的单文件场景——那些情况直接写 libcurl 反而更透明。
2. 把 cpr 1.3.0 编进工程:CMake 集成、依赖链与四个构建开关
2.1 目录结构与依赖链:cpr、libcurl、SSL 后端、zlib
拿到 cpr 1.3.0 的源码包,先别急着cmake ..。解开后看顶层:include/cpr是对外头文件,cpr目录是源文件,test是测试,根目录的CMakeLists.txt决定它怎么找依赖。cpr 本身不实现 socket 和 TLS,它把活交给 libcurl;所以真正要盯的是三件事:libcurl 能不能被 CMake 找到、SSL 后端是哪一个、zlib 要不要一起链。Linux 上常见组合是系统 libcurl + OpenSSL;Windows 上如果自己编 libcurl,要注意 debug/release 运行库一致,否则会在链接期报一堆LNK2038。我一般先跑一遍依赖探测,把 libcurl 的版本和 SSL 后端打印出来,再决定用源码内嵌还是系统包。
cmake_minimum_required(VERSION 3.14) project(cpr_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 把 cpr 1.3.0 放在 third_party/cpr,直接作为子目录编进来 add_subdirectory(third_party/cpr) add_executable(cpr_demo src/main.cpp) target_link_libraries(cpr_demo PRIVATE cpr)上面这段的关键不是add_subdirectory本身,而是target_link_libraries里写的是cpr这个 target,不是手写-lcpr。cpr 的 CMake target 会把 include 路径和 libcurl 的传递依赖一起带出来;手写库名很容易漏掉curl、ssl、crypto、z这一串。编译时先关测试,省时间:
cmake -S . -B build \ -DCPR_BUILD_TESTS=OFF \ -DCPR_ENABLE_SSL=ON \ -DCMAKE_BUILD_TYPE=Debug cmake --build build -jCPR_BUILD_TESTS=OFF是常规操作,cpr 的测试会拉一些额外依赖,业务工程没必要编。CPR_ENABLE_SSL=ON控制是否编 HTTPS 支持;如果这版 CMakeLists 里选项名有差异,传了最多得到一个 unused variable 警告,不会把构建弄挂,但最好先grep -n "option(" third_party/cpr/CMakeLists.txt确认一遍。CMAKE_BUILD_TYPE=Debug建议在接入期打开,链接失败时能看到完整符号。
2.2 两种接入方式:add_subdirectory 与 find_package
源码内嵌适合“我要固定 1.3.0 这个版本、不希望构建机预装 cpr”的场景。把整个目录放进third_party/cpr,主工程只依赖一个相对路径,换机器不用改环境。缺点是主工程配置阶段会多编一个库,CI 缓存没做好的话每次全量构建会多几十秒。另一种方式是先安装 cpr,再用find_package:
find_package(cpr REQUIRED) target_link_libraries(cpr_demo PRIVATE cpr::cpr)cpr::cpr是安装后导出的命名空间 target。如果find_package报cprConfig.cmake找不到,先确认安装前缀有没有进CMAKE_PREFIX_PATH:
cmake -S . -B build \ -DCMAKE_PREFIX_PATH=/opt/cpr-1.3.0 \ -DCMAKE_BUILD_TYPE=Release这里有个选择逻辑:团队里如果有人负责基础镜像,find_package更干净;如果 cpr 只是这个项目的一个小依赖,add_subdirectory更省事。我通常在新项目里先用add_subdirectory跑通,等接口稳定了再切成安装包,避免一开始就在“找包”上耗时间。注意,两种方式不要混用:既add_subdirectory又find_package,容易出现两个 cpr target 同名,链接行里出现重复符号。
2.3 静态链接与动态链接:传递依赖和运行期库路径
cpr 1.3.0 默认跟着BUILD_SHARED_LIBS走。静态链接的好处是部署简单,坏处是 libcurl 的依赖会全部压进最终产物;动态链接的好处是多个进程共享库,坏处是运行期要找得到.so或.dll。我一般这样切:
# 动态库构建,安装到 dist 目录 cmake -S . -B build-shared \ -DBUILD_SHARED_LIBS=ON \ -DCMAKE_INSTALL_PREFIX=./dist \ -DCPR_BUILD_TESTS=OFF cmake --build build-shared -j cmake --install build-shared编完之后用ldd看一眼最终可执行文件依赖了哪些库:
ldd build-shared/cpr_demo | grep -E 'cpr|curl|ssl|crypto'如果libcpr.so显示not found,不是链接错了,是运行期搜索路径没配。Linux 下可以在 CMake 里设INSTALL_RPATH,或者把dist/lib加进LD_LIBRARY_PATH;Windows 下把cpr.dll和libcurl.dll放到 exe 同目录。这里常见的翻车点是:编译机上有 libcurl,运行容器里没有,程序启动直接报error while loading shared libraries。所以动态链接方案一定要在目标环境里跑一次ldd,别只在开发机验证。
提示:如果链接期报
undefined reference to curl_easy_init,先看链接行里有没有-lcurl。用cmake --build build --verbose把完整链接命令打出来,比猜快得多。
3. 第一个可复现请求:GET/POST、参数编码与响应对象拆解
3.1 最小 GET:Url、Parameters 与 Response 字段
先写一个能跑的最小 GET,不要一上来就封工具类。下面这段请求https://example.com/search,带两个查询参数,超时 3 秒:
#include <cpr/cpr.h> #include <iostream> int main() { cpr::Response r = cpr::Get( cpr::Url{"https://example.com/search"}, cpr::Parameters{{"q", "c++ http"}, {"page", "1"}}, cpr::Header{{"User-Agent", "cpr-demo/1.0"}}, cpr::Timeout{3000} ); if (r.error.code != cpr::ErrorCode::OK) { std::cerr << "request failed: " << r.error.message << "\n"; return 1; } std::cout << "status=" << r.status_code << "\n"; std::cout << "elapsed_ms=" << r.elapsed.count() << "\n"; std::cout << "body_size=" << r.text.size() << "\n"; std::cout << "content_type=" << r.header["Content-Type"] << "\n"; return 0; }cpr::Parameters会自动做 URL 编码,c++ http里的空格和加号不用自己处理。cpr::Timeout{3000}的单位是毫秒,不是秒,这里写 3000 就是 3 秒。r.error.code比status_code更早判断:DNS 失败、连接超时、SSL 握手失败时status_code是 0,只有error里有原因。r.elapsed是总耗时,单位毫秒,打日志时很有用。r.header按响应头键名取,如果服务端返回的键名大小写和预期不一致,先遍历打印一遍,别硬编码。
| 字段 | 类型 | 用途 |
|---|---|---|
status_code | int | HTTP 状态码,连接失败时为 0 |
text | std::string | 响应体,小响应直接读 |
header | cpr::Header | 响应头,按 key 取 |
url | cpr::Url | 最终 URL,重定向后看这个 |
elapsed | 时间量 | 总耗时,单位毫秒 |
error | cpr::Error | 错误码和消息,先判它 |
3.2 POST 的两种 body:Payload 表单与 Body + Content-Type
POST 最容易出错的是“到底该用 Payload 还是 Body”。cpr::Payload走application/x-www-form-urlencoded,key 和 value 会自动编码;cpr::Body是原样发送,Content-Type 必须自己设。两者别混:
// 表单提交:Content-Type 自动为 application/x-www-form-urlencoded cpr::Response form = cpr::Post( cpr::Url{"https://example.com/login"}, cpr::Payload{{"user", "demo"}, {"password", "p@ss word"}} ); // JSON 提交:Body 原样发送,必须显式设 Content-Type cpr::Response json = cpr::Post( cpr::Url{"https://example.com/api/items"}, cpr::Header{{"Content-Type", "application/json"}}, cpr::Body{R"({"name":"测试项","count":3})"} );Payload里的空格会被编码成+或%20,服务端按表单解析时能正确还原。Body里的中文要求源文件是 UTF-8,且 JSON 字符串本身要合法;如果拼字符串时漏了转义引号,服务端会返回 400,但 cpr 不会替你校验 JSON。一个实用习惯:把 JSON body 单独存成const std::string,发之前打印长度和前 200 字节,比出问题后抓瞎强。
3.3 超时、重定向与 Header 的显式设置
cpr 的请求选项可以叠加,但默认值不要猜。重定向是否跟随、SSL 是否校验、连接超时多少,最好在代码里写死:
cpr::Response r = cpr::Get( cpr::Url{"https://example.com/redirect"}, cpr::Timeout{3000}, // 整个请求最多 3 秒 cpr::ConnectTimeout{800}, // 连接阶段最多 800 毫秒 cpr::Redirect{true}, // 显式跟随 3xx cpr::VerifySsl{true} // 生产环境保持开启 );Timeout覆盖从发起到收完响应的总时间,ConnectTimeout只覆盖 TCP 和 TLS 握手。两个都设,才能区分“连不上”和“连上了但服务端慢”。Redirect{true}显式写出来,避免升级版本后默认行为变化导致 302 被当成失败。VerifySsl{false}只在本地自签证书调试时用,提交前记得改回来;生产环境关掉校验等于把中间人攻击的门打开。Header 里不要手动设Content-Length,让 libcurl 自己算,手设容易和实际 body 长度不一致。
4. Session 复用与并发边界:三个必须显式设置的参数
4.1 Session 的生命周期:一个线程一个 Session
cpr::Session不是线程安全的,它内部持有一个 libcurl easy handle,handle 上有连接缓存、cookie、认证状态。多线程共享一个 Session,轻则响应串包,重则段错误。正确做法是每个线程一个 Session,线程内循环复用:
#include <cpr/cpr.h> #include <thread> #include <vector> #include <iostream> void worker(int id, const std::string& url, int rounds) { cpr::Session session; // 每个线程独立 Session session.SetUrl(cpr::Url{url}); session.SetTimeout(cpr::Timeout{2000}); session.SetConnectTimeout(cpr::ConnectTimeout{500}); session.SetHeader(cpr::Header{{"Connection", "keep-alive"}}); for (int i = 0; i < rounds; ++i) { cpr::Response r = session.Get(); // 复用同一个 easy handle if (r.error.code != cpr::ErrorCode::OK) { std::cerr << "worker " << id << " error: " << r.error.message << "\n"; continue; } // 业务处理 } } int main() { const std::string url = "http://127.0.0.1:8080/health"; std::vector<std::thread> threads; for (int i = 0; i < 8; ++i) { threads.emplace_back(worker, i, url, 500); } for (auto& t : threads) t.join(); return 0; }这段代码的重点不是线程数,而是session在for循环外面。放在循环里面,每轮都新建 easy handle,连接复用直接归零。SetHeader里的Connection: keep-alive在 HTTP/1.1 里是默认行为,显式写是为了让中间层别随手关连接。SetTimeout和SetConnectTimeout也是每个 Session 设一次,不要每轮重复设。
4.2 连接复用、DNS 缓存与超时参数
cpr 1.3.0 没有把 libcurl 的所有连接池参数都暴露出来,但“同一个 Session 连续请求会复用连接”这一点是成立的。复用的收益在短请求上最明显:省掉 TCP 握手和 TLS 握手,单次延迟能从几十毫秒降到几毫秒。DNS 缓存也挂在 easy handle 上,Session 活得越久,重复解析越少。所以我的习惯是:一个线程一个长生命周期 Session,请求参数用SetUrl、SetParameters、SetBody逐次覆盖,不要每次cpr::Get新建临时对象。
但长生命周期带来两个边界。第一,cookie 和认证状态会累积;如果业务需要无状态请求,每轮开始前session.SetCookies(cpr::Cookies{})清一下,或者干脆用短 Session。第二,连接被服务端关掉后,下一次请求会重连,这是正常现象,不要因为偶发一次延迟升高就怀疑复用失效。超时参数要按业务分:健康检查 500 毫秒连接超时、2 秒总超时;文件下载 5 秒连接超时、60 秒总超时。别一套超时打天下。
4.3 并发验证:用本地服务观察连接数
复用有没有生效,不看日志看连接数。本地起一个 HTTP 服务监听 8080,跑上面的多线程程序,同时在另一个终端执行:
# 观察 8080 端口上 ESTABLISHED 的连接数 ss -tan state established '( dport = :8080 )' | tail -n +2 | wc -l如果每轮请求都新建连接,这个数字会随请求量快速上涨;复用生效时,8 个线程大致稳定在 8 到 16 条连接,具体取决于服务端 keep-alive 超时。这个数字不是绝对指标,但能快速判断“连接到底有没有复用”。另一个观察点是r.elapsed:第一轮通常偏慢,后面几轮明显下降,说明 TLS 和连接建立被省掉了。
注意:线程数不是越多越好。cpr 每个 Session 占一个连接,连接数超过服务端 keep-alive 上限后,反而会出现大量 TIME_WAIT。先压 8 个线程看延迟分布,再决定要不要加到 32。
5. 编译与运行期常见问题排查:五个血泪坑
5.1 头文件找不到:include 路径少了一层
现象:编译报fatal error: cpr/cpr.h: No such file or directory。原因通常不是没下载源码,而是只把third_party/cpr加进了 include 路径,而实际头文件在third_party/cpr/include/cpr/cpr.h,少了一层include。另一个常见原因是主工程手动写了target_include_directories,但漏了cpr子目录。解决方式很简单:用 target 链接,别手写路径。
add_subdirectory(third_party/cpr) target_link_libraries(cpr_demo PRIVATE cpr) # 不要写成 target_include_directories(cpr_demo PRIVATE third_party/cpr)如果必须手动加,写${CMAKE_SOURCE_DIR}/third_party/cpr/include,不是${CMAKE_SOURCE_DIR}/third_party/cpr。编之前用find third_party/cpr -name cpr.h确认文件位置,比反复改 CMake 快。
5.2 链接报 curl_easy_init 未定义:传递依赖断了
现象:编译通过,链接报undefined reference to curl_easy_init或undefined reference to SSL_CTX_new。原因有两个:一是只链接了cpr静态库,没有把 libcurl 放进链接行;二是系统里存在多个 libcurl,CMake 找到了旧版本,符号对不上。先看完整链接命令:
cmake --build build --verbose如果链接行里只有libcpr.a没有-lcurl,用 target 方式链接 cpr,让 CMake 带出传递依赖。如果已经有-lcurl还报错,检查curl-config --version和 CMake 找到的路径是否一致。兜底方案是显式找 CURL 并链接:
find_package(CURL REQUIRED) target_link_libraries(cpr_demo PRIVATE cpr CURL::libcurl)Windows 下还要注意 debug/release 不能混:debug 版 cpr 链 release 版 libcurl,会在运行期崩在堆上。
5.3 SSL 证书校验失败:CA 路径和自签证书是两回事
现象:请求 HTTPS 地址返回error: SSL peer certificate or SSH remote key was not OK,或者problem with the SSL CA cert。原因通常是运行环境缺少 CA 证书包,比如精简容器里没装ca-certificates;或者CURL_CA_BUNDLE环境变量指向了一个不存在的文件;再或者服务端用的是自签证书。先确认系统证书路径:
ls -l /etc/ssl/certs/ca-certificates.crt如果文件不存在,装 CA 证书包;如果存在还报错,检查CURL_CA_BUNDLE有没有被设错。本地调试自签服务时,可以临时cpr::VerifySsl{false},但不要把这个选项带进生产配置。更稳妥的做法是把自签 CA 加到系统信任链,而不是关校验。
5.4 POST 中文乱码:Payload 和 Body 用混了
现象:服务端收到name=%E4%B8%AD%E6%96%87但解码后乱码,或者 JSON 解析直接失败。原因有三个常见组合:表单请求用了Body却没设Content-Type: application/x-www-form-urlencoded;用了Payload又手动 URL 编码了一次,造成双重编码;JSON body 里的中文源文件不是 UTF-8。解决方式是按内容类型选 API:表单用Payload,JSON 用Body+Content-Type: application/json,不要在Payload外面再套一层编码函数。
// 正确:表单交给 Payload 编码 cpr::Post(cpr::Url{url}, cpr::Payload{{"name", "中文"}}); // 正确:JSON 自己保证 body 合法,Header 显式声明 cpr::Post(cpr::Url{url}, cpr::Header{{"Content-Type", "application/json"}}, cpr::Body{R"({"name":"中文"})"});排查时打印 body 的十六进制前几十字节,看%是不是出现了两次,比看日志字符串准。
5.5 多线程偶发崩溃:Session 被共享了
现象:压力一上来就偶发segmentation fault,或者 A 请求收到 B 的响应。原因几乎都是多个线程共用一个cpr::Session。Session 内部的 easy handle 有连接状态和缓冲区,libcurl 明确要求同一个 handle 不能并发使用。解决方式是每个线程独立构造 Session,线程内复用;需要共享的只是 URL、Header 这些配置值,不是 Session 对象本身。
// 错误:全局 Session 被多个线程同时 Get // cpr::Session g_session; // 正确:线程函数内部构造 void worker() { cpr::Session session; session.SetUrl(cpr::Url{url}); // ... }如果确实需要跨线程传递配置,封一个struct RequestConfig,每个线程用自己的 Session 去Set,不要复制 Session。这个坑一次就能记住,代价通常是半夜被报警叫起来。
6. 进阶:用 cpr 1.3.0 做下载落盘与校验的验证脚本
大文件下载不要用r.text接,整包进内存,几百兆就能把进程打爆。cpr 1.3.0 可以用WriteCallback流式写文件,边收边落盘:
#include <cpr/cpr.h> #include <fstream> #include <iostream> int main() { const std::string url = "https://example.com/files/demo.bin"; const std::string part = "demo.bin.part"; std::ofstream out(part, std::ios::binary); if (!out) { std::cerr << "open output failed\n"; return 1; } cpr::Response r = cpr::Get( cpr::Url{url}, cpr::WriteCallback{ [](std::string data, intptr_t userdata) { auto* os = reinterpret_cast<std::ofstream*>(userdata); os->write(data.data(), static_cast<std::streamsize>(data.size())); return os->good(); }, reinterpret_cast<intptr_t>(&out) }, cpr::Timeout{30000}, cpr::ConnectTimeout{3000} ); out.close(); if (r.error.code != cpr::ErrorCode::OK || r.status_code != 200) { std::cerr << "download failed: " << r.error.message << "\n"; return 2; } std::cout << "content-length=" << r.header["Content-Length"] << "\n"; return 0; }WriteCallback每收到一块数据就调一次,userdata用来传文件流指针。回调返回false会中断传输,所以写完要检查os->good()。下载到.part临时文件,成功后再改名,避免半截文件被下游当成完整文件读走:
stat -c '%s' demo.bin.part # 和响应头里的 Content-Length 对比,一致再改名 mv demo.bin.part demo.bin校验这一步别省。Content-Length对不上,说明连接中途断了或者服务端返回了错误页;如果服务端还给了ETag,把它记进日志,下次请求带If-None-Match可以省流量。重试只对连接超时和 5xx 做,4xx 重试没有意义;重试前把.part删掉,别在旧文件上追加。我现在的习惯是:任何 cpr 1.3.0 的下载逻辑,先跑一遍小文件确认回调路径,再换大文件看内存曲线;每次接入新工程,先把add_subdirectory、每线程一个 Session、超时单位这三件事写进模板,再谈业务。希望帮到你。
本文还有配套的精品资源,点击获取