cpp-httplib 实战:HTTP 客户端响应体的两种获取方式(res->body 字符串读取与 ContentReceiver 流式文件下载)

原创2026-10-01 10:14:501,902 阅读
文章标签:后端网络Web框架

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 端口)进行端到端测试。

登录后查看全文
cpp-httplib