cpp-httplib 客户端实战:获取响应体与使用 ContentReceiver 流式保存大文件

原创2026-10-01 22:58:361,282 阅读
文章标签:后端网络

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 a ContentReceiver as 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 或回调数据。

参考与延伸阅读

登录后查看全文
cpp-httplib