简介:这份cpr-1.3.0预编译库面向使用C++开发网络请求功能的开发者,尤其适合在VS2015环境下希望跳过源码编译与依赖配置、直接集成cpr的项目。cpr以现代C++接口封装libcurl,可用于HTTP请求、会话管理、超时与代理等常见场景。压缩包共27个文件,包含21个头文件、4个lib导入/静态库和2个dll动态库,整体约738KB;Debug与Release两套库文件分别提供cpr.lib、libcurl_imp.lib及libcurl.dll,便于按构建配置直接引用。已有691人学习下载。包内保留完整头文件与对应库,能减少自行编译cpr和curl时的环境配置、链接错误排查成本,适合快速搭建示例工程或在既有项目中接入HTTP客户端能力。
1. 从一次接口联调说起:cpr 1.3.0 把 libcurl 的哪些活儿接了过去
前阵子接一个第三方数据接口,C++ 服务里原本用 libcurl 手写回调,一个 POST 请求光设置项就铺了四十多行,换环境后 SSL 后端和超时行为又各不一样。后来把这块换成 cpr 1.3.0,也就是 C++ Requests 的 1.3.0 版本,同样的请求压缩到十行以内,编译进工程后只多一个链接目标。它做的事很明确:把 libcurl 的 easy 接口、回调、header 解析和错误码包成接近脚本语言 requests 风格的 C++ API,让 Get、Post、Session、Parameters、Header 这些概念直接落在类型上。适合已经用 CMake 管工程、需要稳定调用 HTTP 接口但不想重复造轮子的团队;不适合要求极致裁剪的嵌入式环境,也不适合需要 HTTP/2 服务端推送这类底层控制的场景。下面按“怎么编进来、请求怎么拼、响应怎么判、坑在哪”的顺序拆。
2. 把 cpr 1.3.0 编进工程:CMake 目标、依赖对账与三条集成路线
2.1 三条集成路线的取舍:FetchContent、add_subdirectory 与系统包
cpr 1.3.0 在 CMake 工程里有三种常见接法,选错路线后面排错成本很高。第一种是FetchContent,在配置阶段拉取指定 tag,依赖版本和主工程一起锁死,适合没有内网镜像、又能访问源码仓库的团队;缺点是首次配置慢,离线构建要提前做缓存。第二种是把源码目录放进third_party/cpr,用add_subdirectory引入,适合内网构建机不能出网、但能把源码包一起归档的项目;好处是构建完全可重复,坏处是升级要手动替换目录。第三种是系统包或包管理器已经装好的 cpr,find_package(cpr CONFIG REQUIRED)后链接cpr::cpr,适合公司基础镜像里已经统一了版本的环境;前提是系统里的 cpr 版本、libcurl 版本和 SSL 后端跟主工程一致,否则会出现“编译过了、运行时报 SSL 符号冲突”的翻车。
我一般优先选FetchContent并锁定GIT_TAG 1.3.0,因为 cpr 的 CMake 选项和 libcurl 的查找逻辑在版本之间改过,锁 tag 比锁分支稳。如果构建机完全隔离,就把源码包解压到third_party/cpr-1.3.0,用add_subdirectory,同时在顶层CMakeLists.txt里显式设置CPR_USE_SYSTEM_CURL,避免它又去碰系统 curl。
2.2 一份能跑通的最小 CMakeLists 与编译命令
下面这份配置在 Linux 和 Windows 的 MSVC 下都跑过,关键是关掉测试和示例,减少无关目标;同时把 C++ 标准抬到 17,避免 cpr 依赖的字符串视图和文件系统特性在旧标准下报错。
cmake_minimum_required(VERSION 3.16) project(cpr_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) include(FetchContent) FetchContent_Declare( cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.3.0 ) # 关掉 cpr 自带的测试和示例,避免拉取额外依赖 set(CPR_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(CPR_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) # 让 cpr 使用它自己拉取的 curl,减少系统 curl 版本差异 set(CPR_USE_SYSTEM_CURL OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(cpr) add_executable(cpr_demo main.cpp) target_link_libraries(cpr_demo PRIVATE cpr::cpr)这段配置里,GIT_TAG 1.3.0是版本锚点,不要写成master;CPR_BUILD_TESTS和CPR_BUILD_EXAMPLES关掉后配置时间明显缩短;CPR_USE_SYSTEM_CURL OFF表示让 cpr 走它自己的 curl 依赖,适合主工程没有强制统一 curl 的情况。反过来,如果公司安全规范要求必须用系统 curl,就改成ON,并在顶层加find_package(CURL REQUIRED),否则链接阶段容易缺curl的传递依赖。
配置和编译命令建议分两步走,先只配置,确认依赖拉取和编译器识别没问题,再全量编译:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --parallel 8-S .指定源码目录,-B build指定构建目录,--parallel 8按机器核数调整。如果构建机内存小,把并行数降到 4,cpr 和 curl 一起编译时链接阶段容易吃内存。Windows 下如果用的是多配置生成器,编译命令要加--config Release,否则默认走 Debug,运行库和 Release 库混用会在链接期报LNK2038。
2.3 版本与后端参数:curl 来源、SSL 后端和运行库必须对齐
cpr 1.3.0 本身不实现网络传输,底层还是 libcurl。工程里最容易出问题的地方不是 cpr 的 API,而是“cpr 用的 curl”和“主工程其他库用的 curl”不是同一份。比如主工程已经链接了系统 curl,cpr 又通过FetchContent拉了一份静态 curl,链接器可能把两套符号都放进来,运行期出现curl_easy_init返回空、SSL 握手失败或者证书路径读不到。排查时先看链接行里出现了几次 curl 库,再看cpr的配置输出里CPR_USE_SYSTEM_CURL的值。
SSL 后端也要统一。Linux 上常见 OpenSSL 和系统自带 TLS 库混用,Windows 上常见 Schannel 与 OpenSSL 混用。cpr 提供CPR_FORCE_OPENSSL_BACKEND这类开关,但不要盲目打开,先确认主工程依赖的 OpenSSL 版本和 cpr 拉取的 curl 期望的版本一致。我的血泪经验是:只要出现“证书验证失败但浏览器能访问”,先不要怀疑 cpr 的VerifySsl,先查 curl 的 CA 证书路径和 SSL 后端,八成是后端错配。
运行库一致性同样重要。MSVC 下主工程用/MD,cpr 和 curl 也要用/MD;如果某个第三方静态库用了/MT,链接期就会报运行库不匹配。可以在顶层设置CMAKE_MSVC_RUNTIME_LIBRARY统一,或者用target_compile_options给所有目标加一致选项。不要等到运行期崩溃再回头查,链接器报LNK2038的时候就把运行库对账做完。
3. 请求对象怎么拼:Url、Parameters、Header 与 Session 复用的参数表
3.1 Url、Parameters、Header、Body 的组合规则
cpr 的请求构造是“具名参数”风格,cpr::Get接受Url、Parameters、Header、Timeout等对象,顺序不敏感。第一次用容易把查询参数直接拼在 URL 里,短请求看不出问题,一旦参数里有中文、空格、&就会翻车。cpr::Parameters会做百分号编码,比自己拼字符串稳。下面这段代码演示一个带查询参数、自定义 header 和超时的 GET:
#include <cpr/cpr.h> #include <iostream> int main() { cpr::Response r = cpr::Get( cpr::Url{"https://api.example.com/v1/items"}, cpr::Parameters{{"page", "1"}, {"size", "50"}, {"keyword", "c++ http"}}, cpr::Header{{"Accept", "application/json"}, {"X-Request-Id", "demo-001"}}, cpr::ConnectTimeout{1500}, // 连接阶段超时,单位毫秒 cpr::Timeout{5000} // 整个请求超时,单位毫秒 ); std::cout << "status=" << r.status_code << " error=" << r.error.message << std::endl; if (r.status_code == 200) { std::cout << r.text.substr(0, 200) << std::endl; } return 0; }cpr::Url只放基础地址,不要带?和查询串;cpr::Parameters负责编码,值里有空格和加号也不会把服务端参数截断;cpr::Header的键名按 HTTP 习惯写,cpr 在内部会按不区分大小写处理,但日志里最好保持统一,方便排查;cpr::ConnectTimeout和cpr::Timeout单位都是毫秒,前者只管 TCP/TLS 连接建立,后者管从发请求到收完响应的总时长。两个都设,别只设一个,否则 DNS 慢或者服务端卡住时定位不到阶段。
POST JSON 的写法要注意Content-Type和Body成对出现,只给Body不给 header,服务端可能按表单解析:
cpr::Response r = cpr::Post( cpr::Url{"https://api.example.com/v1/items"}, cpr::Header{{"Content-Type", "application/json"}, {"Accept", "application/json"}}, cpr::Body{R"({"name":"demo","enabled":true})"}, cpr::Timeout{5000} );cpr::Body接std::string,里面是原始字节,不做额外编码;JSON 里的双引号用原始字符串R"(...)"包住,避免转义错。如果服务端要求表单,就换成cpr::Payload{{"key","value"}},它会按application/x-www-form-urlencoded编码。不要把Payload和Body同时塞进一个请求,行为取决于底层 curl 的最后设置,容易出“服务端收到空 body”的怪问题。
3.2 Session 复用与连接池:为什么不要每次 Get 都新建句柄
cpr::Get这种一次性调用写起来快,但每次都会新建一个 curl easy 句柄,DNS 解析、TCP 握手、TLS 握手都要重来。对同一个域名连续请求几十次,耗时差距很明显。cpr::Session把句柄和配置存下来,复用连接,适合“同一个 base URL、同一组 header、频繁调用”的场景。下面是一个最小复用示例:
#include <cpr/cpr.h> int main() { cpr::Session session; session.SetUrl(cpr::Url{"https://api.example.com/v1/items"}); session.SetHeader(cpr::Header{{"Accept", "application/json"}}); session.SetConnectTimeout(cpr::ConnectTimeout{1500}); session.SetTimeout(cpr::Timeout{5000}); for (int page = 1; page <= 5; ++page) { session.SetParameters(cpr::Parameters{{"page", std::to_string(page)}, {"size", "50"}}); cpr::Response r = session.Get(); if (r.status_code != 200) { // 这里只做演示,生产环境要按错误码分类处理 break; } } return 0; }Session的SetUrl只设一次,循环里只换Parameters;SetHeader和超时也只设一次。这样底层 curl 句柄会复用连接,省掉重复握手。注意Session不是线程安全的:一个Session对象不要被多个线程同时调用Get或Post,否则会出现响应串包、header 错乱甚至崩溃。常见做法是每个线程持有自己的Session,或者用线程局部存储;如果只是读配置,可以把 URL、header 复制到新Session里,不要共享同一个对象。
3.3 超时、重定向与 SSL 校验的参数表
参数设错是 cpr 使用中最隐蔽的坑。下面这张表按“参数—单位/取值—适用场景—注意点”整理,照着填能避开大部分低级问题:
| 参数 | 单位/取值 | 适用场景 | 注意点 |
|---|---|---|---|
cpr::Timeout | 毫秒 | 控制整个请求总时长 | 包含重定向和响应体传输时间,不要设得比下游服务超时还长 |
cpr::ConnectTimeout | 毫秒 | 控制连接建立阶段 | DNS 慢、TLS 握手慢时先看这个值 |
cpr::Redirect | 布尔或次数 | 跟随 3xx | 自动重定向可能改变方法或丢 header,涉及鉴权时最好手动处理 |
cpr::VerifySsl | 布尔 | 校验证书 | 生产环境不要关;关闭只用于本地自签证书调试 |
cpr::Authentication | 用户名/密码/模式 | Basic 等鉴权 | 密码不要写进源码,从配置或环境变量读 |
cpr::Cookies | 键值对 | 会话保持 | 跨域名不要复用同一组 cookie |
重定向特别说一下:默认情况下 cpr 不一定跟随 3xx,涉及登录跳转的接口要显式设置cpr::Redirect{true}。但 301/302 对 POST 的处理在不同服务端和 curl 版本下表现不一样,有的会把 POST 改成 GET,有的会把Authorization头丢掉。我的做法是:内部服务之间的接口不依赖自动重定向,遇到 3xx 先把Location拿出来,再决定是否重新发请求;只有纯 GET 的静态资源才开自动跟随。
SSL 校验不要为了“先跑通”关掉。cpr::VerifySsl{false}能让自签证书的请求过去,但上线后忘了改回来,等于把中间人风险带进生产。本地调试可以临时关,提交代码前用 grep 查一遍VerifySsl,确认没有遗留。
4. 响应回来之后:状态码、错误码与二进制落盘的判断逻辑
4.1 cpr::Response 的字段语义:status_code、error、text、header
cpr::Response里最常看的是status_code、error、text、header、elapsed和cookies。status_code是 HTTP 状态码,只有收到完整响应头才有意义;error.code是传输层错误码,连接失败、超时、SSL 握手失败时status_code可能是 0,这时候看error才有用;text是响应体,按字节存进std::string,文本和二进制都能放,但大文件不要指望它;header是响应头集合,键名大小写在不同服务端下不统一,取值时最好做一次大小写无关的比较;elapsed是耗时,单位秒,做性能日志时直接打出来。
一个常见的误判是只看status_code == 200,不看error。网络层失败时status_code为 0,如果代码里写if (r.status_code == 200) { ... } else { 重试 },会把 DNS 解析失败、证书错误也当成可重试,结果重试风暴。正确顺序是先看error.code,再看status_code:
#include <cpr/cpr.h> #include <iostream> void Handle(const cpr::Response& r) { if (r.error.code != cpr::ErrorCode::OK) { std::cerr << "transport error: " << r.error.message << std::endl; return; } if (r.status_code >= 200 && r.status_code < 300) { std::cout << "ok, body size=" << r.text.size() << std::endl; } else if (r.status_code >= 400 && r.status_code < 500) { std::cerr << "client error: " << r.status_code << std::endl; } else if (r.status_code >= 500) { std::cerr << "server error: " << r.status_code << std::endl; } }r.error.code等于cpr::ErrorCode::OK只代表传输层没出错,不代表业务成功;r.status_code在 2xx 之外还要区分 4xx 和 5xx,前者通常不要重试,后者可以按退避策略重试。r.error.message是给人看的,不要拿它做分支判断,分支只认error.code和status_code。
4.2 上传下载与二进制数据:不要用 text 接文件
小文件用r.text接二进制没问题,因为std::string本身能存\0;但大文件会一次性占满内存,而且中间多一次拷贝。常见做法是用cpr::WriteCallback把数据块直接写进文件流,或者用 cpr 提供的下载接口。下面给一个用回调落盘的示例,重点是回调返回true表示继续,返回false会中止传输:
#include <cpr/cpr.h> #include <fstream> int main() { std::ofstream out("download.bin", std::ios::binary); cpr::Response r = cpr::Get( cpr::Url{"https://api.example.com/v1/export"}, cpr::WriteCallback{[&out](std::string data, intptr_t /*userdata*/) { if (!out) { return false; // 写文件失败,中止传输 } out.write(data.data(), static_cast<std::streamsize>(data.size())); return static_cast<bool>(out); }}, cpr::Timeout{30000} ); if (r.error.code != cpr::ErrorCode::OK) { // 回调返回 false 也会落到错误分支,记得清理半截文件 } return 0; }cpr::WriteCallback的 lambda 第一个参数是本次收到的数据块,第二个是用户数据指针;回调里不要做耗时操作,写文件用缓冲流;返回false会触发 curl 中止,r.error.code会变成非 OK。下载失败要清理半截文件,否则下次续传或校验会拿到脏数据。上传文件用cpr::Multipart或cpr::Body接文件内容,前者适合表单上传,后者适合直接把文件字节当请求体;不管哪种,都要在请求头里写对Content-Type,否则服务端可能按文本解析。
4.3 错误分支写法:把 error.code 映射成可重试与不可重试
生产代码里不要在每个调用点写一长串if,把错误分类抽成一个函数,调用点只决定“重试几次、退避多久”。下面这个映射按经验分类,超时和连接失败可重试,DNS 解析失败和 SSL 错误不盲目重试:
#include <cpr/cpr.h> bool IsRetryable(const cpr::Response& r) { if (r.error.code == cpr::ErrorCode::OPERATION_TIMEDOUT) { return true; // 超时,可退避重试 } if (r.error.code == cpr::ErrorCode::COULDNT_CONNECT) { return true; // 连接失败,可能是瞬时网络抖动 } if (r.error.code == cpr::ErrorCode::COULDNT_RESOLVE_HOST) { return false; // DNS 解析失败,先查配置和域名 } if (r.error.code == cpr::ErrorCode::SSL_CONNECT_ERROR) { return false; // 证书或 TLS 版本问题,重试没用 } if (r.error.code == cpr::ErrorCode::OK) { return r.status_code == 429 || r.status_code >= 500; } return false; }OPERATION_TIMEDOUT和COULDNT_CONNECT重试时要加退避,不要原地循环;COULDNT_RESOLVE_HOST先检查 DNS 和域名拼写,重试只会放大故障;SSL_CONNECT_ERROR多半是证书链、SNI 或 TLS 版本问题,重试不会自愈;status_code == 429要看服务端有没有Retry-After,有就按它来,没有再用指数退避。重试次数不要超过三次,超过三次还失败,把请求 ID、URL、错误码打进日志,交给人工排查。
5. 避坑与排查:链接失败、SSL 后端错配与并发串包的五个现场
5.1 编译与链接阶段:三个最容易卡半天的现场
第一个现场是链接期报undefined reference to curl_easy_init。现象是 cpr 的头文件能找到、编译通过,链接时一堆 curl 符号缺失。原因通常是CPR_USE_SYSTEM_CURL开了,但主工程没有把CURL::libcurl传递到最终目标,或者系统里只有 curl 的可执行文件没有开发包。解决方式是先确认find_package(CURL REQUIRED)能找到库,再把CURL::libcurl显式加到target_link_libraries;如果不想碰系统 curl,就把CPR_USE_SYSTEM_CURL关掉,让 cpr 自己拉 curl,但要注意清理旧的构建目录,避免缓存里还留着上一次的查找结果。
第二个现场是程序启动或第一次请求时报 SSL 相关符号冲突。现象是编译链接都过,运行时报undefined symbol: SSL_CTX_new或者证书验证直接失败。原因是 cpr 拉取的 curl 用的 OpenSSL 版本和主工程链接的 OpenSSL 不是同一份,两个版本的符号在动态链接时互相覆盖。解决方式是统一 SSL 后端:要么全用系统 OpenSSL,要么全用 cpr 内置的;切换后必须删掉build目录重新配置,CMake 的缓存不会自动感知后端变化。
第三个现场是 Windows 下 Debug 正常、Release 链接报LNK2038。现象是运行库不匹配,/MT和/MD混用。原因是 cpr、curl 和主工程的CMAKE_MSVC_RUNTIME_LIBRARY不一致。解决方式是在顶层CMakeLists.txt里统一设置运行库,并确保所有第三方目标都继承同一选项;如果某个预编译库只有/MT版本,就把它换成/MD版本,不要靠关闭警告硬链接。
5.2 运行期:超时误判与 Session 跨线程
第四个现场是请求偶发超时,日志里连接已经建立,但迟迟没有响应。现象是cpr::Timeout设了 5 秒,仍然在 5 秒左右失败,且失败率随并发上升。原因常见于只设了总超时,没有设连接超时,DNS 或 TLS 握手阶段耗掉大部分时间;另外Timeout包含重定向和响应体传输,如果响应体很大,5 秒可能不够。解决方式是ConnectTimeout设短、Timeout按业务给足,重定向场景单独评估;排查时把r.elapsed和r.error.message一起打出来,区分是连接阶段还是传输阶段。
第五个现场是并发下偶发响应串包或崩溃。现象是多线程共用一个cpr::Session,A 线程拿到的响应体像是 B 线程请求的,或者程序在Get里崩溃。原因是Session持有 curl 句柄和内部状态,不是线程安全的。解决方式是每个线程一个Session,或者每次请求新建局部Session;如果只是想共享 base URL 和 header,把这些配置存成不可变结构,在每个线程里复制一份再构造Session。不要用全局锁把整个请求串行化,那样连接复用的收益也没了。
提示:出现任何“偶发”问题时,先把请求 ID、线程 ID、
error.code、status_code、elapsed打进一行日志,再谈复现。没有这些字段,排查基本靠猜。
6. 再包一层:把 cpr 1.3.0 收进项目内的 HttpClient 小类
直接在每个业务文件里写cpr::Get不是不行,但项目一大就会出现超时值满天飞、header 各写各的、错误处理复制粘贴。我通常会在 cpr 之上包一个很薄的HttpClient,只做四件事:固定 base URL、统一超时、统一重试、把响应转成业务能用的结构。下面是一个骨架,故意不引入 JSON 库,只返回status_code、body和错误码,业务层自己决定怎么解析:
#include <cpr/cpr.h> #include <string> class HttpClient { public: explicit HttpClient(std::string base_url, int timeout_ms = 5000) : base_url_(std::move(base_url)), timeout_ms_(timeout_ms) {} cpr::Response Get(const std::string& path, const cpr::Parameters& params = {}) { cpr::Session session; session.SetUrl(cpr::Url{base_url_ + path}); session.SetParameters(params); session.SetHeader(cpr::Header{{"Accept", "application/json"}}); session.SetConnectTimeout(cpr::ConnectTimeout{1500}); session.SetTimeout(cpr::Timeout{timeout_ms_}); return session.Get(); } private: std::string base_url_; int timeout_ms_; };HttpClient的构造函数接 base URL 和默认超时,Get里每次新建Session,避免跨线程共享;如果确定单线程高频调用,可以把Session提成成员,但要在注释里写清楚“非线程安全”。重试不要藏在Get里自动做,业务层看到IsRetryable为真再决定,避免下载大文件时重复传输。日志里至少打三样:请求路径、status_code、error.code,出问题时能直接过滤。
验证这套封装能不能用,我一般会写一个不依赖业务的最小请求,指向一个稳定的健康检查接口,连续跑一百次,看连接复用有没有生效、超时分布是否正常、错误码有没有异常跳变。这个习惯是从一次线上事故之后养成的:当时只测了单次请求,没测连续调用,结果Session被跨线程复用,偶发串包查了两天才定位到。从那以后我每次接 cpr 都先写一个最小的循环请求,把并发和超时跑一遍,再往业务里接。希望帮到你。
本文还有配套的精品资源,点击获取