cpp-httplib 客户端实战:获取响应体与使用 ContentReceiver 流式保存大文件
cpp-httplib 客户端实战:获取响应体与使用 ContentReceiver 流式保存大文件
导读
在 cpp-httplib 中,httplib::Client 的 Get() 调用会返回一个封装了 HTTP 响应的 Result 对象,其中 res->body 就是一个可直接使用的 std::string。本篇 cookbook 指南围绕"如何获取响应体、如何把响应保存到文件"展开:既覆盖一次性读入内存的简单用法,也深入讲解借助 ContentReceiver 回调分块写入磁盘、用 ResponseHandler 在下载前检查响应头、以及配合进度回调实现取消下载等实战方案。读完本文,你将能根据响应大小正确选择"内存读取"与"流式落盘"两种模式,并掌握 cpp-httplib 客户端响应处理的核心回调体系。
1. 准备:header-only 客户端的最小用法
cpp-httplib 是 header-only 的库,客户端代码只需要包含根目录下的 httplib.h 即可,无需链接任何库:
#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;
}
}
以本仓库根目录为当前目录编译(header-only 实现,C++11 即可;Unix 系平台通常需要链接 pthread):
g++ -std=c++11 -pthread main.cpp -o client
仓库中的 example/client.cc 给出了同款的最小用法:cli.Get("/hi") 后依次读取 res->status、res->get_header_value("Content-Type") 与 res->body。
2. 把整个响应体作为字符串获取
最简单的方式是直接访问 res->body:
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/hello");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
res->body 的类型是 std::string(见 httplib.h 中 struct Response 的定义),拿到手就可以直接使用:解析 JSON 时可直接传给 nlohmann/json 这类库,返回给上层业务时也无需任何转换。
从源码结构看,Get() 返回的是 class Result(httplib.h),它同时封装了响应与错误信息:
operator bool():当且仅当内部持有有效的Response时返回true;res->status:HTTP 状态码(默认值为 -1,尚未解析出状态行时为该值);res->body、res->headers、res->version、res->reason:响应正文、响应头、HTTP 版本与状态文本;res.error():enum class Error(httplib.h),配合httplib::to_string(res.error())(httplib.h)可得到可读的错误描述。
2.1 区分"连接错误"与"HTTP 错误"
判断 res 本身是否为真,与判断 res->status 是两件独立的事(这一模型在 tour/02-basic-client.md 中有完整示例):
- 连接层错误:服务器不可达(连接被拒、超时、TLS 失败等)时,
res为假,此时应调用res.error()查看具体原因; - HTTP 层错误:服务器正常返回了 404、500 等状态码时,
res为真,但需要检查res->status才能发现业务失败。
推荐的完整检查写法:
auto res = cli.Get("/hello");
if (!res) {
std::cerr << "Connection error: " << httplib::to_string(res.error()) << std::endl;
return 1;
}
if (res->status != 200) {
std::cerr << "HTTP error: " << res->status << std::endl;
return 1;
}
std::cout << res->body << std::endl;
3. 注意:res->body 会把整个响应加载进内存
直接读 res->body 虽然方便,但有一个不可忽视的前提——整个响应体会一次性加载到内存中。对于普通 API 返回的 JSON、HTML 这类小响应完全没问题;但如果用同样的方式去抓取一个 GB 级别的文件,内存会被整份文件占据,既不经济也可能导致 OOM。
Warning: If you fetch a large file with
res->body, it all goes into memory. For large downloads, use aContentReceiveras shown below.
因此,判断依据很简单:响应体积小、后续需要整体处理 → 用 res->body;响应体积大、目标是落盘 → 用 ContentReceiver 流式接收。
4. 使用 ContentReceiver 分块下载并直接写入文件
对于大文件下载,cpp-httplib 提供 ContentReceiver 回调机制:数据到达后按分块交给你的回调,每一块处理完即可丢弃,不必等整个响应体收齐。
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);
});
要点说明:
- 回调签名:
bool(const char *data, size_t data_length),与源码中using ContentReceiver = std::function<bool(const char *data, size_t data_length)>(httplib.h)一致; - 分块落盘:回调被调用多少次取决于传输分块,每个分块直接
ofs.write(data, len)写入,整份文件不需要先在内存里拼完整; - 二进制安全:目标文件务必以
std::ios::binary打开,避免平台相关的换行转换破坏数据; - 回调返回值即"是否继续":返回
false会立即中止本次下载。上面的例子中,一旦ofs.write使流进入失败状态,static_cast<bool>(ofs)为false,下载自动停止,不会继续把数据写进一个已经出错的流。
4.1 Get() 的多个重载与参数组合
从 httplib.h(客户端侧)与 httplib.h(流式客户端侧)可以看到,Get() 提供了一系列重载,可以按需组合:
Get(path):最简形式,响应整体进内存,通过res->body读取;Get(path, ContentReceiver):正文以分块方式交给回调(本节用法);Get(path, ResponseHandler, ContentReceiver):先回调响应头,再分块接收正文(下一节);Get(path, Headers)/Get(path, Params)/Get(path, Params, Headers):携带自定义请求头或查询参数;- 上述任意形式后还可追加
DownloadProgress进度回调(见第 6 节)。
5. 下载前检查响应头:ResponseHandler + ContentReceiver 组合
很多时候我们需要先知道"这个文件有多大、是什么类型"再决定是否下载。cpp-httplib 允许在正文接收之前插入一个 ResponseHandler:
auto res = cli.Get("/large-file",
[](const httplib::Response &res) {
auto len = res.get_header_value("Content-Length");
std::cout << "Size: " << len << std::endl;
return true; // return false to skip the download
},
& {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
关键语义:
- 执行时机:
ResponseHandler在响应头到达之后、正文开始接收之前被调用。源码中using ResponseHandler = std::function<bool(const Response &response)>(httplib.h); - 读取头部:通过
res.get_header_value("Content-Length")(httplib.h)读取指定头;若关心数值比较,还可使用get_header_value_u64(key, def, id)(httplib.h),它返回size_t并支持默认值,更适合做"剩余空间是否够"这类数值判断; - 返回
false跳过下载:ResponseHandler返回false时,本次下载被整体跳过。典型场景是:Content-Length 超过磁盘剩余空间、或者响应 Content-Type 不符合预期时提前放弃,省去无谓的流量消耗。
实际应用时可以先在 ResponseHandler 里完成"长度/类型检查",把是否继续的决定权交给返回值;只有通过检查的响应才会进入 ContentReceiver 逐块落盘。
6. 进阶:边下载边显示进度,并可随时取消
当同时需要"流式写文件"和"进度展示"时,把 ContentReceiver 与 DownloadProgress 一起传入即可:
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/large-file",
[](size_t current, size_t total) {
auto percent = (total > 0) ? (current * 100 / total) : 0;
std::cout << "\rDownloading: " << percent << "% ("
<< current << "/" << total << ")" << std::flush;
return true; // return false to abort
},
& {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
- 回调签名:
DownloadProgress定义为std::function<bool(size_t current, size_t total)>(httplib.h); total的来源:来自响应头Content-Length。若服务器未发送该头(例如 chunked 传输),total可能为 0,此时无法计算百分比,退化为只显示已接收字节数;- 取消下载:进度回调返回
false即可中止传输。UI 场景下常用一个std::atomic<bool>标志位配合"取消按钮",下一次进度 tick 到来时返回!cancelled.load()即可停止,具体示例见 C11. Use the progress callback。
上传方向同理:Post() / Put() 的最后一个参数可传入 UploadProgress,进度语义完全一致。
7. 本节要点速查
| 场景 | 推荐方式 | 内存特征 |
|---|---|---|
| 小响应(API JSON / HTML) | cli.Get(path) 后读 res->body |
整份进内存,方便直接使用 |
| 大文件下载 | cli.Get(path, ContentReceiver) |
分块回调,逐块落盘,内存占用低 |
| 下载前先校验大小/类型 | cli.Get(path, ResponseHandler, ContentReceiver) |
ResponseHandler 返回 false 可整体跳过 |
| 大文件 + 进度/取消 | ContentReceiver + DownloadProgress 同时传入 |
分块处理,进度回调控制中止 |
无论采用哪种形式,都应先判断 res 是否为真(连接层是否成功),再判断 res->status(HTTP 层是否成功),最后按需消费 res->body 或回调数据。
参考与延伸阅读
- 本文关联文档:C01. Get the response body / save to a file
- 进度回调与取消下载的完整示例:C11. Use the progress callback
- 客户端基础用法(查询参数、请求头、POST、错误处理):Basic Client
- 核心实现:
Result与Response定义见 httplib.h 和 httplib.h;Get()全部重载见 httplib.h - 可运行示例:example/client.cc