C++ HTTP客户端库cpr:现代化网络请求的实践指南

1. 项目概述:为什么我们需要一个现代的C++ HTTP客户端库?

如果你用C++写过需要和网络打交道的程序,比如从某个API拉取数据、上传文件到服务器,或者实现一个简单的爬虫,那你大概率经历过一个痛苦的阶段:要么是吭哧吭哧地用系统底层的socket API,处理一堆连接、超时、编码的琐事;要么是去集成一个庞大而古老的库,比如libcurl,然后被它那C风格的回调函数和复杂的选项配置搞得头晕眼花。我自己在早期项目里就没少踩这些坑,直到后来发现了cpr这个库,才感觉在C++里做HTTP请求终于能像在Python里用requests那样优雅和直接了。

cpr,全称是“C++ Requests”,它的设计哲学非常明确:为C++开发者提供一个简单、直观、现代化且功能强大的HTTP客户端库。它的API设计深受Python的requests库启发,所以如果你熟悉requests,那么上手cpr几乎没有任何障碍。它的核心价值在于,把开发者从繁琐的底层网络细节和复杂的配置中解放出来,让你能更专注于业务逻辑本身。无论是快速原型开发,还是需要高性能、高可靠性的生产环境,cpr都能很好地胜任。

这个库适合所有层次的C++开发者。对于新手,它友好的API能让你在几分钟内就发出第一个HTTP请求,快速获得正反馈。对于有经验的开发者,它提供的异步支持、连接池、代理、Cookie管理等高级特性,足以应对复杂的网络交互场景。接下来,我就结合自己多年的使用经验,带你从零开始,彻底掌握这个能极大提升你开发效率的工具。

2. cpr的核心优势与设计哲学

2.1 告别libcurl的“旧时代”:cpr带来的改变

在cpr出现之前,libcurl几乎是C/C++领域进行HTTP通信的事实标准。libcurl非常强大,但也非常“C”。这意味着你需要处理大量的回调函数、手动管理内存(设置CURLOPT_WRITEFUNCTIONCURLOPT_WRITEDATA)、以及面对一长串令人望而生畏的CURLOPT_选项。代码的可读性和可维护性常常因此大打折扣。

cpr在底层实际上封装了libcurl,但它做了一次漂亮的“面向对象”和“现代化”的改造。它把libcurl那些过程式的、基于回调的接口,包装成了基于RAII(资源获取即初始化)和异常安全(可选)的C++风格API。举个例子,在libcurl里,下载一个文件并保存到内存,你需要写一个回调函数来拼接数据块;而在cpr里,你只需要一行代码:cpr::Response r = cpr::Get(cpr::Url{“https://api.example.com/data”});,响应内容就已经在r.text里了。

这种设计哲学的核心是“约定优于配置”和“显式优于隐式”。库为你设定了合理的默认行为(比如自动处理重定向、设置常见的User-Agent),同时通过清晰的命名和链式调用来设置参数,让代码意图一目了然。

2.2 关键特性一览:不止于GET和POST

很多人初看cpr,觉得它就是个发GET和POST请求的库。这其实低估了它的能力。经过多个版本的迭代,cpr已经成为一个功能相当全面的HTTP客户端。以下是它的一些核心特性,我将在后续章节详细展开:

  1. 全面的HTTP方法支持: 基础的GET, POST, PUT, DELETE, HEAD, OPTIONS自然不在话下。
  2. 灵活的请求参数设置
    • URL参数: 通过cpr::Parameters以键值对形式方便地构建查询字符串。
    • 请求体: 支持多种格式,包括纯文本(cpr::Body)、表单数据(cpr::Payloadcpr::Multipart)、JSON字符串。
    • 请求头: 通过cpr::Header轻松设置任何自定义头部。
  3. 认证与代理: 支持Basic Auth、Bearer Token认证,以及HTTP/HTTPS/SOCKS5代理。
  4. 超时与重试: 可以设置连接超时、请求超时,并内置了简单的重试机制。
  5. Cookie管理: 自动处理Cookie的发送和存储,也支持手动管理Cookie Jar。
  6. 文件上传与下载: 通过cpr::Multipart支持文件上传;下载大文件时,可以使用回调函数流式处理,避免内存爆掉。
  7. 异步请求支持: 这是cpr的杀手锏之一。它提供了基于std::future的异步接口,让你能轻松实现非阻塞的并发HTTP请求,极大提升程序性能。
  8. SSL/TLS支持: 底层依赖libcurl的SSL后端,支持HTTPS请求,可以指定CA证书路径或跳过证书验证(仅用于测试!)。
  9. 连接池: 对于需要频繁向同一主机发起请求的场景,连接池可以复用TCP连接,减少握手开销,提升性能。

这些特性使得cpr不仅能用于简单的REST API调用,也能处理像模拟登录、爬虫、文件传输服务等更复杂的网络任务。

3. 从零开始:cpr的安装与项目配置

3.1 跨平台安装指南

cpr是一个纯头文件库吗?不完全是。它依赖libcurl,所以你需要先确保系统里安装了libcurl的开发文件。它的安装方式非常灵活,适合各种构建系统。

在Linux (Ubuntu/Debian) 上:

# 1. 安装依赖库libcurl sudo apt-get update sudo apt-get install libcurl4-openssl-dev # 2. 安装cpr。推荐使用包管理器vcpkg或从源码构建。 # 方法A: 使用vcpkg (推荐,方便管理) git clone https://github.com/Microsoft/vcpkg.git cd vcpkg ./bootstrap-vcpkg.sh # Linux ./vcpkg integrate install ./vcpkg install cpr # 方法B: 从源码安装 git clone https://github.com/libcpr/cpr.git cd cpr mkdir build && cd build cmake .. -DCPR_USE_SYSTEM_CURL=ON # 使用系统已安装的curl make sudo make install

在macOS 上:

# 使用Homebrew是最简单的方式 brew install cpr # 或者,同样可以使用vcpkg或源码编译。

在Windows 上:Windows上的配置稍复杂,因为缺乏统一的包管理器。我强烈推荐使用vcpkg,它能自动处理依赖和编译。

  1. 安装vcpkg (如果尚未安装):
    git clone https://github.com/Microsoft/vcpkg.git cd vcpkg .\bootstrap-vcpkg.bat .\vcpkg integrate install
  2. 使用vcpkg安装cpr:
    .\vcpkg install cpr:x64-windows # 安装64位版本 # 或者 .\vcpkg install cpr:x86-windows 安装32位版本
    这会自动下载并编译cpr及其依赖(libcurl)。

3.2 CMake项目集成(现代C++项目标配)

现在绝大多数C++项目都用CMake管理,cpr对此有很好的支持。假设你的项目结构如下:

my_project/ ├── CMakeLists.txt ├── src/ │ └── main.cpp └── ...

如果你用系统包管理器或源码安装了cpr:在你的CMakeLists.txt中,使用find_package来定位cpr。

cmake_minimum_required(VERSION 3.16) project(my_http_client) set(CMAKE_CXX_STANDARD 17) # cpr需要C++11及以上,推荐17 # 查找cpr库 find_package(cpr REQUIRED) add_executable(${PROJECT_NAME} src/main.cpp) # 将cpr::cpr链接到你的目标 target_link_libraries(${PROJECT_NAME} PRIVATE cpr::cpr)

如果你使用vcpkg:在CMake配置时,需要指定vcpkg的工具链文件。

# 在项目根目录(my_project/)下执行 cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path_to_vcpkg]/scripts/buildsystems/vcpkg.cmake cmake --build build

你的CMakeLists.txt写法同上,find_package命令会被vcpkg自动处理。

注意:一个常见的坑是,在Windows上使用Visual Studio,如果你通过其他方式安装了libcurl(比如自己编译),可能会遇到链接错误,提示找不到__imp_curl_easy_init之类的符号。这通常是因为编译cpr时使用的libcurl库和你项目链接的库不匹配(Debug/Release、动态库/静态库)。最稳妥的方案就是让cpr和你的项目使用同一份libcurl,这也是为什么vcpkg方案最省心——它保证了依赖的一致性。

3.3 验证安装:你的第一个cpr程序

创建一个简单的main.cpp文件来测试安装是否成功:

#include <iostream> #include <cpr/cpr.h> // 包含cpr头文件 int main() { // 发起一个简单的GET请求到公共测试API cpr::Response r = cpr::Get(cpr::Url{"https://httpbin.org/get"}); // 打印HTTP状态码 std::cout << "Status code: " << r.status_code << std::endl; // 应该输出 200 // 打印响应体(JSON文本) std::cout << "Response text:\n" << r.text << std::endl; // 检查请求是否成功(状态码2xx) if (r.status_code == 200) { std::cout << "Request succeeded!" << std::endl; } else { std::cout << "Request failed!" << std::endl; } return 0; }

编译并运行这个程序(记得链接cpr库)。如果看到返回的JSON数据和状态码200,那么恭喜你,cpr环境已经配置成功!

4. cpr API深度解析与实战应用

4.1 同步请求:基础但不可或缺

同步请求是阻塞式的,调用cpr::Get等函数后,程序会等待直到收到HTTP响应或超时。它逻辑简单,适用于顺序执行、快速脚本或对并发要求不高的场景。

4.1.1 GET请求与参数传递GET请求通常用于获取资源,参数附在URL后面。

#include <cpr/cpr.h> #include <iostream> int main() { // 方法1:直接将参数拼接在URL里(不推荐,需要自己处理URL编码) // auto r = cpr::Get(cpr::Url{"https://httpbin.org/get?name=foo&value=1"}); // 方法2:使用cpr::Parameters,库会自动处理URL编码 auto r = cpr::Get(cpr::Url{"https://httpbin.org/get"}, cpr::Parameters{{"name", "foo"}, {"value", "1"}, {"city", "New York"}}); std::cout << r.url << std::endl; // 输出完整的URL std::cout << r.text << std::endl; return 0; }

cpr::Parameters是一个std::initializer_list<std::pair<std::string, std::string>>,用起来非常直观。它会自动将特殊字符(如空格、中文)进行百分比编码。

4.1.2 POST请求与多种数据格式POST请求用于提交数据,cpr支持多种数据格式。

  • 发送表单数据 (application/x-www-form-urlencoded):这是网页表单默认的提交格式。

    auto r = cpr::Post(cpr::Url{"https://httpbin.org/post"}, cpr::Payload{{"username", "myuser"}, {"password", "mypass"}}); // cpr::Payload 的用法和 cpr::Parameters 类似,但数据放在请求体中。
  • 发送JSON数据 (application/json):这是现代API最常用的格式。

    #include <nlohmann/json.hpp> // 推荐使用nlohmann/json库来处理JSON using json = nlohmann::json; json data; data["title"] = "Hello"; data["body"] = "World"; data["userId"] = 1; auto r = cpr::Post(cpr::Url{"https://jsonplaceholder.typicode.com/posts"}, cpr::Header{{"Content-Type", "application/json"}}, cpr::Body{data.dump()}); // 将json对象序列化为字符串 std::cout << r.text << std::endl;

    实操心得:在实际项目中,我强烈建议使用像nlohmann/json这样的库来构建和解析JSON,而不是手动拼接字符串。它能避免很多转义错误,并且代码更清晰。记得设置正确的Content-Type头,否则服务器可能无法正确解析。

  • 发送Multipart表单数据 (multipart/form-data):常用于文件上传。

    // 上传一个文本字段和一个文件 auto r = cpr::Post(cpr::Url{"https://httpbin.org/post"}, cpr::Multipart{{"description", "This is a test file"}, {"file", cpr::File{"../test.jpg"}}});

    cpr::File构造函数接受文件路径。cpr会读取文件内容并将其作为multipart的一部分发送。

4.1.3 处理响应cpr::Response对象包含了服务器返回的所有信息:

  • status_code: HTTP状态码(200, 404, 500等)。
  • text: 响应体,以std::string形式存储。对于文本内容(HTML, JSON, XML)很方便。
  • header: 响应头,是一个std::map<std::string, std::string>
  • url: 最终请求的URL(考虑重定向后)。
  • elapsed: 请求耗时。
  • cookies: 服务器设置的Cookies。
  • error: 如果请求过程中发生错误(如网络断开),这里会包含错误信息。
auto r = cpr::Get(cpr::Url{"https://api.github.com"}); if (r.error) { std::cerr << "Request error: " << r.error.message << std::endl; } else { std::cout << "Status: " << r.status_code << std::endl; std::cout << "Content-Type: " << r.header["content-type"] << std::endl; // 解析JSON响应 // auto json_response = nlohmann::json::parse(r.text); }

4.2 异步请求:提升性能的利器

当你的应用需要同时发起多个网络请求,或者不想因为一个慢速的网络调用而阻塞整个线程时,异步请求就是最佳选择。cpr的异步API返回一个std::future<cpr::Response>

4.2.1 基本异步调用

#include <cpr/cpr.h> #include <future> #include <vector> #include <iostream> int main() { // 发起一个异步GET请求 std::future<cpr::Response> future_response = cpr::GetAsync(cpr::Url{"https://httpbin.org/delay/2"}); // 这个端点会延迟2秒响应 std::cout << "Request sent, doing other work..." << std::endl; // ... 这里可以执行其他不依赖响应的任务 ... // 当需要结果时,调用future.get(),这会阻塞直到响应就绪 cpr::Response r = future_response.get(); std::cout << "Async request completed with status: " << r.status_code << std::endl; return 0; }

4.2.2 实现并发请求异步的真正威力在于并发。我们可以同时发起多个请求,然后一起等待它们完成。

std::vector<std::string> urls = { "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", "https://httpbin.org/delay/3" }; std::vector<std::future<cpr::Response>> responses; for (const auto& url : urls) { responses.push_back(cpr::GetAsync(cpr::Url{url})); } std::cout << "All requests fired. Waiting..." << std::endl; // 遍历future向量,获取结果。总耗时约等于最慢的那个请求(~3秒),而非顺序执行的6秒。 for (auto& future_resp : responses) { cpr::Response r = future_resp.get(); // 按完成顺序获取,不一定是发起顺序 std::cout << "URL: " << r.url << " -> Status: " << r.status_code << ", Time: " << r.elapsed << "ms\n"; }

注意事项:std::future::get()方法只能调用一次,调用后future就变为无效。如果你需要在多个地方使用结果,需要保存cpr::Response对象。另外,大量并发请求可能会对目标服务器造成压力,也可能耗尽本地文件描述符,在生产环境中需要合理控制并发度,并考虑使用连接池。

4.3 高级配置:应对复杂网络环境

真实的网络环境很少有一帆风顺的,cpr提供了丰富的选项来应对各种情况。

4.3.1 超时与重试网络不稳定时,超时设置是必须的。

// 设置连接超时(3秒)和请求总超时(10秒) auto r = cpr::Get(cpr::Url{"https://httpbin.org/delay/5"}, cpr::Timeout{10000}, // 总超时10秒 cpr::ConnectTimeout{3000}); // 连接超时3秒 // 启用重试机制:最多重试3次,每次重试间隔逐渐增加(指数退避) cpr::Session session; session.SetUrl(cpr::Url{"https://unstable-api.example.com"}); session.SetTimeout(cpr::Timeout{5000}); session.SetRetry(cpr::RetryWithDelay(3, std::chrono::milliseconds(100))); // 重试3次,基础间隔100ms auto response = session.Get();

cpr::Session对象提供了更精细的控制,适合需要复用配置的复杂请求场景。

4.3.2 代理与认证在公司内网或需要特定网络路由时,代理和认证是关键。

// 使用HTTP代理 auto r1 = cpr::Get(cpr::Url{"https://httpbin.org/ip"}, cpr::Proxies{{"http", "http://10.10.1.10:8080"}, {"https", "http://10.10.1.10:8080"}}); // 注意https代理的协议 // 使用代理并携带认证信息 auto r2 = cpr::Get(cpr::Url{"https://httpbin.org/ip"}, cpr::Proxies{{"https", "http://proxy.com:3128"}}, cpr::ProxyAuthentication{{"http", cpr::EncodedAuthentication{"user", "pass"}}}); // API端点认证 (Bearer Token / Basic Auth) // Bearer Token (常用于JWT) auto r3 = cpr::Get(cpr::Url{"https://api.secure.com/data"}, cpr::Bearer{"your_jwt_token_here"}); // Basic Auth auto r4 = cpr::Get(cpr::Url{"https://httpbin.org/basic-auth/user/passwd"}, cpr::Authentication{"user", "passwd"}); // cpr会自动进行Base64编码

4.3.3 Cookie管理与会话保持cpr可以自动处理Cookie,模拟浏览器会话。

cpr::Session session; session.SetUrl(cpr::Url{"https://httpbin.org/cookies/set?sessionid=abc123"}); session.SetCookies(cpr::Cookies{{"preference", "chocolate"}}); // 设置初始Cookie auto r1 = session.Get(); // 这次请求会发送我们设置的Cookie,并接收服务器设置的Cookie std::cout << "Response1: " << r1.text << std::endl; // 同一个session对象发起第二次请求,会自动携带上次请求收到的Cookie(sessionid) session.SetUrl(cpr::Url{"https://httpbin.org/cookies"}); auto r2 = session.Get(); std::cout << "Response2: " << r2.text << std::endl; // 会显示 {"cookies": {"sessionid": "abc123", "preference": "chocolate"}}

使用cpr::Session对象是管理有状态会话(如登录后的一系列操作)的最佳实践。

5. 性能优化与最佳实践

5.1 连接池:复用连接以提升效率

HTTP/1.1默认支持持久连接(Keep-Alive),但如果你每次请求都创建一个新的cpr::Session或直接调用cpr::Get,底层可能会频繁地建立和断开TCP连接,造成不必要的开销。对于需要向同一主机发起大量请求的场景,启用连接池能带来显著的性能提升。

cpr的连接池功能是通过cpr::Session和libcurl的底层特性共同实现的。核心在于复用同一个cpr::Session对象

cpr::Session session; session.SetUrl(cpr::Url{"https://api.example.com"}); // 为这个session设置一些通用参数,如超时、头部等 session.SetTimeout(cpr::Timeout{5000}); session.SetHeader(cpr::Header{{"User-Agent", "MyApp/1.0"}}); // 第一次请求,会建立TCP连接 auto resp1 = session.Get(); // 紧接着的第二次请求,可能会复用已有的连接(如果服务器支持Keep-Alive) session.SetParameters(cpr::Parameters{{"page", "2"}}); auto resp2 = session.Get();

libcurl内部会为每个CURL*句柄(对应一个cpr::Session)维护一个连接缓存。当使用同一个session对象向同一个主机(相同协议、主机名、端口)发起请求时,它会尝试复用现有的空闲连接。

性能对比心得:在一个内部监控项目中,我需要每分钟轮询几十个服务的健康端点。最初我为每个请求创建临时对象,CPU和网络TIME_WAIT状态连接数都很高。改为使用一个全局的cpr::Session对象池后,平均响应时间下降了约40%,服务器负载也明显降低。对于高频请求,连接池是必选项。

5.2 流式下载与上传:处理大文件

默认情况下,cpr会将整个响应体读入内存的std::string中。对于下载一个大文件(比如几百MB的电影),这会导致内存瞬间暴涨。cpr提供了写入回调(WriteCallback)来支持流式处理。

#include <fstream> #include <cpr/cpr.h> // 自定义一个写入回调,将数据直接写入文件 size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { std::ofstream* file = static_cast<std::ofstream*>(userdata); size_t totalSize = size * nmemb; file->write(ptr, totalSize); return totalSize; // 必须返回实际处理的字节数,否则libcurl会认为出错 } int main() { std::ofstream outfile("large_file.zip", std::ios::binary); if (!outfile.is_open()) { std::cerr << "Failed to open file for writing.\n"; return 1; } cpr::Session session; session.SetUrl(cpr::Url{"https://example.com/large_file.zip"}); session.SetWriteCallback(cpr::WriteCallback(writeCallback, &outfile)); // 设置回调函数和用户数据(文件流指针) // 也可以设置进度回调来显示下载进度 // session.SetProgressCallback(cpr::ProgressCallback([](...){ return true; })); auto r = session.Get(); outfile.close(); if (r.status_code == 200) { std::cout << "File downloaded successfully.\n"; } else { std::cerr << "Download failed with status: " << r.status_code << std::endl; } return 0; }

同理,你也可以使用ReadCallback来实现流式上传大文件,避免一次性将整个文件读入内存。

5.3 错误处理与日志调试

健壮的程序必须妥善处理错误。cpr的错误主要来自两个方面:网络/系统错误和HTTP协议错误。

cpr::Response r = cpr::Get(cpr::Url{"https://non-existent-domain-xyz.com"}); // 1. 检查底层网络错误 if (r.error.code != cpr::ErrorCode::OK) { std::cerr << "Network error: " << r.error.message << std::endl; // 可能的原因:域名无法解析、连接被拒绝、超时等 return; } // 2. 检查HTTP状态码 if (r.status_code >= 400) { std::cerr << "HTTP error: " << r.status_code << std::endl; std::cerr << "Response body: " << r.text << std::endl; // 服务器可能返回了错误详情 // 处理不同的状态码:404 Not Found, 500 Internal Server Error, 403 Forbidden等 switch(r.status_code) { case 404: /* 处理未找到 */ break; case 429: /* 处理请求过多,需要降速 */ break; case 500: /* 处理服务器内部错误 */ break; default: break; } } // 3. 启用详细日志(调试时非常有用) // 设置环境变量 CPR_LOG_LEVEL=debug 或者在你的代码中设置(如果cpr编译时启用了日志) // 日志会输出到stderr,包含详细的HTTP请求和响应信息,有助于排查问题。

对于调试复杂的请求,我经常使用cpr::Session配合cpr::Verbose选项,或者使用像Postmancurl命令先验证API本身是否工作正常,再在代码中复现。有时候问题不在代码,而在服务器配置或网络策略上。

6. 常见问题排查与实战技巧

即使对库很熟悉,在实际开发中还是会遇到各种稀奇古怪的问题。这里我总结了一份“避坑指南”。

6.1 编译与链接问题速查表

问题现象可能原因解决方案
编译错误:undefined reference tocpr::Get(...)`没有正确链接cpr库确保CMake的target_link_libraries包含了cpr::cpr。检查库路径。
链接错误:undefined reference to curl_easy_...没有链接libcurl,或链接的版本不匹配cpr依赖libcurl。确保安装了libcurl-dev(el)包,并且CMake能找到它。在Windows上,使用vcpkg可避免此问题。
运行时崩溃(Windows MSVC)Debug/Release库混用确保你的项目构建配置(Debug/Release)与链接的cpr/libcurl库的配置一致。vcpkg默认安装Release版,如果你在Debug模式下开发,需要安装cpr:x64-windows-static-md或指定三重态。
SSL certificate problem: unable to get local issuer certificatelibcurl找不到CA证书包来验证HTTPS证书Linux/macOS:安装ca-certificates包。Windows:将cURL的CA证书包(curl-ca-bundle.crt)放在合适位置,或通过cpr::SslOptions设置路径,或(仅测试环境)使用cpr::VerifySsl(false)跳过验证(不安全!)。

6.2 运行时典型问题与解决

问题1:请求非常慢,或者偶尔超时。

  • 排查思路:
    1. DNS解析:可能是DNS服务器慢或不稳定。尝试使用cpr::Resolve指定IP,或检查系统的DNS配置。
    2. 连接池:是否频繁创建销毁Session?复用Session对象。
    3. 超时设置:默认超时可能很长。根据场景合理设置cpr::Timeoutcpr::ConnectTimeout
    4. 服务器限流:检查是否返回429状态码。需要在代码中实现退避重试逻辑。
    5. 代理问题:如果身处公司网络,可能需要配置代理,但代理本身可能成为瓶颈。

问题2:发送中文或特殊字符出现乱码。

  • 原因与解决:这通常是字符编码问题。cpr的textstd::string,存储的是原始字节。
    1. URL参数:使用cpr::Parameters,cpr会自动进行URL编码。
    2. 请求体/响应体:确保你和服务器对文本编码的认知一致(如UTF-8)。对于响应,你可能需要将std::string转换为std::wstring或使用如iconv这样的库进行转码。如果响应是JSON,使用JSON库(如nlohmann/json)解析,它通常能处理好编码。

问题3:异步请求似乎没有真正并发。

  • 原因:cpr::GetAsync只是将任务提交到libcurl的异步队列,但默认情况下,所有的异步请求共享同一个libcurl的“多句柄”(multi handle),并在发起get()的线程中执行。如果你在单线程中顺序调用future.get(),它们仍然是顺序完成的。
  • 解决方案:要实现真正的并行,你需要将std::future对象放到不同的线程中去等待(get())。或者,更高级的做法是,为不同的任务组使用不同的cpr::AsyncSession(如果cpr版本支持),或者结合像libuvBoost.Asio这样的事件循环库来驱动libcurl的多句柄。

问题4:内存泄漏报告。

  • 排查:cpr本身基于RAII,一般不会泄漏。问题可能出在:
    1. 回调函数异常:自定义的WriteCallback/ReadCallback如果抛出异常,且未被libcurl捕获,可能导致资源未释放。确保回调函数是异常安全的。
    2. 全局对象析构顺序:如果在全局或静态对象中使用cpr,程序退出时,这些对象析构时若cpr底层libcurl的全局资源已释放,可能导致问题。尽量避免在全局范围内进行网络操作。

6.3 一个综合实战案例:构建一个简单的API客户端

假设我们要为一个虚构的天气服务API编写客户端。该API需要API Key,返回JSON数据。

#include <cpr/cpr.h> #include <nlohmann/json.hpp> #include <iostream> #include <string> class WeatherClient { private: std::string api_key_; std::string base_url_ = "https://api.weatherapp.com/v1"; cpr::Session session_; // 复用Session public: explicit WeatherClient(const std::string& api_key) : api_key_(api_key) { // 配置Session的通用参数 session_.SetHeader(cpr::Header{{"User-Agent", "MyWeatherApp/1.0"}, {"Accept", "application/json"}}); session_.SetTimeout(cpr::Timeout{10000}); session_.SetConnectTimeout(cpr::Timeout{3000}); } std::optional<nlohmann::json> getCurrentWeather(const std::string& city) { session_.SetUrl(cpr::Url{base_url_ + "/current.json"}); session_.SetParameters(cpr::Parameters{{"key", api_key_}, {"q", city}, {"aqi", "no"}}); // 重置可能之前设置的Body等,确保是GET请求 session_.SetBody(cpr::Body{""}); try { auto response = session_.Get(); if (response.error) { std::cerr << "Network error: " << response.error.message << std::endl; return std::nullopt; } if (response.status_code != 200) { std::cerr << "API error: " << response.status_code << ", body: " << response.text.substr(0, 200) << std::endl; return std::nullopt; } return nlohmann::json::parse(response.text); } catch (const nlohmann::json::parse_error& e) { std::cerr << "Failed to parse JSON: " << e.what() << std::endl; return std::nullopt; } } // 可以添加更多方法,如getForecast等 }; int main() { WeatherClient client("your_api_key_here"); auto weather_data = client.getCurrentWeather("London"); if (weather_data) { std::cout << "Temperature in London: " << (*weather_data)["current"]["temp_c"].get<double>() << "°C\n"; } return 0; }

这个案例展示了如何封装一个健壮的、可复用的API客户端,包含了错误处理、JSON解析和Session复用等最佳实践。