cpp-httplib 实战:HTTP 客户端响应体的两种获取方式(res->body 字符串读取与 ContentReceiver 流式文件下载)
cpp-httplib 实战:HTTP 客户端响应体的两种获取方式(res->body 字符串读取与 ContentReceiver 流式文件下载)
本篇文章聚焦 cpp-httplib(仓库根目录下 httplib.h 这一单头文件 C++ HTTP/HTTPS 客户端/服务器库)的客户端侧核心操作:如何获取 GET 请求的响应体。你将掌握两条完整路径——用 res->body 直接拿到 std::string 形式的完整响应体,以及用 ContentReceiver 回调把数据按块(chunk)直接写入文件、避免大文件撑爆内存,并学会用 ResponseHandler 在下载前检查 Content-Length 等响应头、用回调返回 false 中止下载。全文代码可直接编译运行,并附源码级原理与测试用例佐证。
场景与前置条件
本文对应官方文档 C01. レスポンスボディを取得する / ファイルに保存する(英文版见 C01. Get the Response Body / Save to a File),属于 cpp-httplib cookbook 系列的第一篇,主题是“客户端如何消费响应体”。
cpp-httplib 是 header-only 库,只需在源码中包含仓库根目录的 httplib.h 即可,纯 HTTP 场景无需链接额外库;使用 HTTPS 时需配合 OpenSSL 等后端(见 cookbook 中 t01-tls-backends.md 的说明)。后续所有示例都围绕同一个目标服务:
GET /hello:返回一个小型文本响应体;GET /large-file:返回一个体积较大的二进制文件。
方式一:作为字符串整体获取(res->body)
最直接的做法是用 Client::Get 的简单重载,让库把完整响应体读入 Response::body:
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/hello");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
return 0;
}
res->body 的类型与语义
从源码结构看,httplib.h 中 Response 结构体直接声明了:
struct Response {
std::string version;
int status = -1;
std::string reason;
Headers headers;
Headers trailers;
std::string body; // 响应体本体
std::string location; // 重定向地址
...
};
因此 res->body 就是一个普通 std::string,可以原样用于字符串拼接、JSON 解析、正则匹配或直接打印,无需任何额外转换。返回的 res 是 Result 类型:当请求在传输层面失败(连接被拒、超时、TLS 握手失败等)时,Result 为空,if (res) 判断即会失败;请求成功但服务器返回非 2xx 时,res 非空但 status 会携带实际状态码。因此文档中的 if (res && res->status == 200) 是“传输成功 + 业务状态码为 200”的双重保险写法,测试代码 test/test.cc 中同样采用 EXPECT_EQ(StatusCode::OK_200, res->status) 的判断模式。
大文件的内存风险(重要警告)
Warning: 使用
res->body时,整个响应体会被一次性读入内存。如果下载的是大文件,内存占用将与文件大小成正比,可能耗尽进程内存。
这正是原文档强调的核心注意事项:小响应体(API JSON、短文本、错误消息)用 res->body 简洁高效;一旦目标是体积未知或明显较大的文件,就必须切换到下文的方式二。
方式二:用 ContentReceiver 流式接收并保存到文件
Client::Get 提供了带 ContentReceiver 回调的重载。数据到达时不会被整体缓存在 res->body,而是按网络缓冲区块(chunk)交给你的回调,你可以把每一块直接写入 std::ofstream:
#include "httplib.h"
#include <fstream>
#include <iostream>
int main() {
httplib::Client cli("http://localhost:8080");
std::ofstream ofs("output.bin", std::ios::binary);
if (!ofs) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto res = cli.Get("/large-file",
& {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
if (!res) {
std::cerr << "Download failed" << std::endl;
return 1;
}
return 0;
}
要点说明:
- 打开文件使用
std::ios::binary:避免 Windows 平台把\n转义为\r\n,破坏二进制内容; - 回调签名固定为
bool(const char *data, size_t data_length),与 httplib.h 中的类型别名完全一致:using ContentReceiver = std::function<bool(const char *data, size_t data_length)>; - 写文件后返回
static_cast<bool>(ofs):ofstream的operator bool反映流状态。写入成功(磁盘可用、缓冲正常)返回true继续接收;一旦写入失败(如磁盘写满),返回false,下载立即中止,天然形成“写入失败即停止”的防护逻辑。
这种方式的优势正是原文档强调的:任意时刻内存中只保留一个数据块,而不是整个文件,非常适合大文件下载、备份导出、镜像拉取等场景。
回调返回 false 即中止下载
无论 ContentReceiver 还是 ResponseHandler,回调返回 false 都会被库解释为“停止本次响应处理”:
- 对
ContentReceiver而言,返回false会中止剩余 body 的接收,连接按失败处理,最终res携带错误状态; - 对
ResponseHandler而言,返回false会在收到头之后直接跳过 body 下载。
上面的文件示例借助这一机制把磁盘写入失败自动转换为下载中断;你也可以在回调里检查累计长度、校验和或其他自定义条件,主动 return false 提前结束。
进阶组合:ResponseHandler 下载前检查响应头
有时你希望在真正开始下载前确认 Content-Length、Content-Type 等响应头,甚至据此决定是否跳过下载(例如磁盘剩余空间不足)。此时把 ResponseHandler 与 ContentReceiver 一起传给 Get 即可:
#include "httplib.h"
#include <fstream>
#include <iostream>
int main() {
httplib::Client cli("http://localhost:8080");
std::ofstream ofs("output.bin", std::ios::binary);
if (!ofs) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto res = cli.Get("/large-file",
[](const httplib::Response &response) { // ResponseHandler
auto len = response.get_header_value("Content-Length");
std::cout << "Size: " << len << std::endl;
return true; // 返回 false 则跳过下载
},
& { // ContentReceiver
ofs.write(data, len);
return static_cast<bool>(ofs);
});
return res ? 0 : 1;
}
关键机制(原文档明确说明):
- 调用时机:
ResponseHandler在响应头接收完毕之后、body 开始接收之前被调用; - 判定能力:你可以读取
status(响应状态码)、headers(完整头部)以及通过Response::get_header_value(key)获取任意头字段,例如上面的Content-Length; - 跳过下载:处理函数返回
false时,库会跳过 body 的接收,下载本身被取消——这为“先看大小再决定要不要下”提供了原子化的实现手段。
从源码结构看,ResponseHandler 的类型别名定义在 httplib.h:
using ResponseHandler = std::function<bool(const Response &response)>;
Client::Get 相应的重载签名为:
Result Get(const std::string &path,
ResponseHandler response_handler,
ContentReceiver content_receiver,
DownloadProgress progress = nullptr);
见 httplib.h。同系列还提供携带 Headers、Params 的多个 Get 重载(httplib.h),例如 Get(path, headers, response_handler, content_receiver, progress),可用于需要自定义请求头(认证、Range 等)的下载场景。
源码级原理:库内部如何处理 ContentReceiver
了解底层实现有助于你在实际项目中做出正确取舍。
回调的包装与调用链
在客户端实现中,用户提供的 ContentReceiver 会被包装成带偏移量与总长度的内部回调再执行,见 httplib.h:
if (content_receiver) {
req.content_receiver =
content_receiver {
return content_receiver(data, data_length);
};
}
也就是说,库内部实际使用的是 ContentReceiverWithProgress(httplib.h):
using ContentReceiverWithProgress = std::function<bool(
const char *data, size_t data_length, size_t offset, size_t total_length)>;
用户层的 ContentReceiver 只是它的简化投影。内部回调被挂到请求对象上,响应体到达时由底层的 read_content 系列函数逐块驱动(相关实现可追溯 httplib.h 起的 prepare_content_receiver 与 read_content)。这解释了为什么选择 ContentReceiver 重载后 res->body 会保持为空——body 已经被“路由”到回调,而不是聚合进 Response::body。
解压与安全防护
当响应带 Content-Encoding(gzip / brotli / zstd)且客户端启用了对应压缩支持时,库会在投递给你的回调之前完成透明解压,prepare_content_receiver 会先创建解压器,再把解压后的字节流转给接收回调(httplib.h)。这意味着你的 ContentReceiver 拿到的永远是“解压后的最终字节”,可以放心写盘,无需自己处理压缩编码。
同时,解压路径内置了 zip-bomb 防护:prepare_content_receiver 会累计已解压字节数,一旦超过 payload_max_length 上限立即返回失败并标记 exceed_payload_max_length(httplib.h)。若服务端声明的编码无法解码,库会返回 415 Unsupported Media Type 而不是静默继续(httplib.h)。
测试用例佐证
仓库的单元测试为这两种模式提供了完整背书:
- test/test.cc
ChunkedEncodingTest::WithContentReceiver:用ContentReceiver接收 chunked 编码的/chunked响应,把每一块body.append(data, data_length)进std::string,最后与从本地图片读出的完整内容断言相等——证明了回调逐块交付且字节无缺失; - test/test.cc
ChunkedEncodingTest::WithResponseHandlerAndContentReceiver:ResponseHandler(断言status == 200)+ContentReceiver组合下载,验证了两个回调协同工作的完整链路; - 此外 test/test.cc 起的
GzipWithContentReceiver、test/test.cc 起的ZstdWithContentReceiver等用例验证了压缩响应经ContentReceiver透明解压的正确性。
实用建议与后续阅读
- 小响应体(JSON、短文本):用
res->body,代码最简洁; - 大文件/未知大小:用
ContentReceiver直接写文件,内存恒定; - 下载前决策:
ResponseHandler + ContentReceiver组合,先看Content-Length、Content-Type,可返回false跳过; - 磁盘写入失败自动中止:
return static_cast<bool>(ofs);一行搞定; - 展示下载进度:本文示例略过了
DownloadProgress参数(Get的最后可选参数,签名见 httplib.h),其用法详见官方 cookbook 的 C11. 進捗コールバックを使う(进度回调)(英文版 C11. Use the progress callback)。
仓库还提供了可运行的多文件示例供对照学习:example/client.cc、example/simplecli.cc、example/server_and_client.cc;若想验证上述代码,可配合 example/simplesvr.cc 起一个本地 HTTP 服务(监听 8080 端口)进行端到端测试。