☰
cpp-httplib 流式 API 实战:用 stream::Get 与 open_stream 实现逐块读取、SSE 与反向代理
2026/10/1 7:55:55 网站建设 项目流程
  • 后端
  • 网络

【免费下载链接】cpp-httplib

A C++ header-only HTTP/HTTPS server and client library

项目地址:https://gitcode.com/GitHub_Trending/cp/cpp-httplib
点击查看免费下载

本篇文章以 cpp-httplib 官方文档 README-stream.md 为核心,系统讲解该库新增的Streaming API:基于迭代器的stream::Get()/stream::Result高层接口,以及直接操作 socket 的open_stream()/StreamHandle底层接口。读完本文,你将掌握如何用极低内存代价逐块消费 HTTP 响应体,落地 LLM 流式输出、SSE(Server-Sent Events)、大文件下载与反向代理等真实场景,并理解这些 API 在 httplib.h 中的底层实现原理。

一、什么是 cpp-httplib 流式 API

传统上,Client::Get()会等待整个响应体完整接收并缓冲到内存中,这对小响应和 Keep-Alive 连接复用很友好,但遇到超大文件或无限流式输出(如大模型逐字生成)时,内存与首字节延迟都不可接受。

cpp-httplib 为此提供了流式扩展:数据直接从网络 socket 读取,一次只保留当前一个数据块在内存中,配合迭代器风格的循环逐块处理,实现真正的 socket 级流式(true socket-level streaming)。

流式 API 特别适用于以下几类场景:

  • LLM / AI 流式响应(如 ChatGPT、Claude、Ollama 等以 JSON Lines 逐行输出的接口);
  • Server-Sent Events(SSE)实时推送;
  • 大文件下载及下载进度跟踪;
  • 反向代理实现,把上游响应体原样转发给下游。

使用前请先记住三条重要约束:

  • 无 Keep-Alive:每次stream::Get()都会使用一条专用连接,响应体读完后连接即关闭;需要连接复用时请改用Client::Get()。
  • 只能迭代一次:next()方法只能从头到尾遍历响应体一遍。
  • Result 非线程安全:stream::Get()可以在多个线程同时被调用,但返回的stream::Result只能由单个线程使用。

二、Quick Start:第一个流式客户端

在项目目录中引入头文件后,即可用与Client::Get()几乎相同的写法发起流式请求:

#include "httplib.h" int main() { httplib::Client cli("http://localhost:8080"); // Get streaming response auto result = httplib::stream::Get(cli, "/stream"); if (result) { // Process response body in chunks while (result.next()) { std::cout.write(result.data(), result.size()); } } return 0; }

核心循环是while (result.next()):next()每次从 socket 读入一块数据并返回true,读到流结束返回false;result.data()指向当前块起始位置,result.size()给出当前块字节数。整个过程中内存中只保留一个块,配合std::cout.write()即可把响应原样输出。

需要说明的是,stream::Get只是open_stream("GET", ...)的便捷封装,源码见 httplib.h,默认块大小chunk_size = 8192字节。除了 GET,stream命名空间还提供Post/Put/Patch/Delete/Head/Options等对应方法,均支持传Headers、Params与请求体(见 httplib.h)。

三、API 分层:从高层到低层四层接口

cpp-httplib 针对不同使用诉求提供了四层 API,从上到下抽象层级递减、控制粒度递增:

┌─────────────────────────────────────────────┐ │ SSEClient │ ← SSE-specific, parsed events │ - on_message(), on_event() │ │ - Auto-reconnect, Last-Event-ID │ ├─────────────────────────────────────────────┤ │ stream::Get() / stream::Result │ ← Iterator-based streaming │ - while (result.next()) { ... } │ ├─────────────────────────────────────────────┤ │ open_stream() / StreamHandle │ ← General-purpose streaming │ - handle.read(buf, len) │ ├─────────────────────────────────────────────┤ │ Client::Get() │ ← Traditional, full buffering └─────────────────────────────────────────────┘

选择建议如下:

使用场景推荐 API
需要自动重连的 SSESSEClient(见 README-sse.md)
LLM 流式输出(JSON Lines)stream::Get()
大文件下载stream::Get()或open_stream()
反向代理open_stream()
小响应 + Keep-AliveClient::Get()

其中SSEClient(命名空间sse,实现于 httplib.h 起的SSEMessage/SSEClient)会把流按 SSE 规范解析成带event/data/id字段的消息对象,并内置自动重连与Last-Event-ID续传逻辑,是 SSE 场景下的最高层选择。

四、低层 API 参考:StreamHandle 与 open_stream

StreamHandle是流式 API 的底层句柄,接管 socket 连接的所有权,数据直接从网络读取。声明位于 httplib.h。

// Open a stream (takes ownership of socket) httplib::Client cli("http://localhost:8080"); auto handle = cli.open_stream("GET", "/path"); // Check validity if (handle.is_valid()) { // Access response headers immediately int status = handle.response->status; auto content_type = handle.response->get_header_value("Content-Type"); // Read body incrementally char buf[4096]; ssize_t n; while ((n = handle.read(buf, sizeof(buf))) > 0) { process(buf, n); } }

注意:使用open_stream()时连接专用于流式传输,不支持 Keep-Alive;需要连接复用的场景请改用client.Get()。

StreamHandle 成员一览

成员类型说明
responsestd::unique_ptr<Response>含响应头的 HTTP 响应对象
errorError请求失败时的错误码
is_valid()bool响应有效时返回 true
read(buf, len)ssize_t直接从 socket 读取最多len字节
get_read_error()Error获取最近一次读错误
has_read_error()bool检查是否发生读错误

open_stream 的底层实现细节

从 httplib.h 的ClientImpl::open_stream()实现可以看到它做了完整的事前准备:

  1. 目标编码与缓冲发送路径保持一致:空Params时直接用path,否则调用append_query_params()追加查询参数,再经detail::encode_request_target()编码,保证无论走哪个 API,同样的path产生同样的请求行;
  2. 连接准备:在socket_mutex_保护下检查现有 socket 是否存活(is_socket_alive,SSL 下还会检查对端是否关闭),失效则断开重建,并通过setup_proxy_connection()处理代理;
  3. 所有权转移:transfer_socket_ownership_to_handle()(httplib.h)把 socket 描述符(及 SSL 会话)从Client移交给StreamHandle,客户端自身的socket_置为INVALID_SOCKET,从此连接生命周期完全由句柄掌控;
  4. 请求写出:先在内存BufferStream中组装请求行与请求头(write_request_line+check_and_write_headers),校验通过后再一次性写入网络,随后写入请求体(若有),避免被拒绝的头污染线上数据;
  5. 响应解析:读取响应行与响应头,并做与普通路径相同的 framing 检查(HEAD、204、304 可合法携带无体 framing 头,Content-Length冲突视为Error::Read);
  6. 传输语义识别:根据响应头设置BodyReader的content_length/chunked标志,is_chunked_transfer_encoding()判定分块传输,并对Content-Encoding创建对应的解压器(见下节)。

StreamHandle::read()在存在解压器时走read_with_decompression()路径(httplib.h),否则直接调用detail::read_body_content()按 Content-Length / chunked 语义读取;当分块流读完后,还会解析 trailer(parse_trailers_if_needed(),httplib.h)。测试 test/test.cc 中的StreamHandleTest验证了is_valid()对response与error的组合判定逻辑。

五、高层 API 参考:stream::Get() 与 stream::Result

stream::Result是对StreamHandle的迭代器式封装,声明于 httplib.h,实现于 httplib.h,使用起来更符合直觉:

#include "httplib.h" httplib::Client cli("http://localhost:8080"); cli.set_follow_location(true); // ... // Simple GET auto result = httplib::stream::Get(cli, "/path"); // GET with custom headers httplib::Headers headers = {{"Authorization", "Bearer token"}}; auto result = httplib::stream::Get(cli, "/path", headers); // Process the response if (result) { while (result.next()) { process(result.data(), result.size()); } } // Or read entire body at once auto result2 = httplib::stream::Get(cli, "/path"); if (result2) { std::string body = result2.read_all(); }

stream::Result 成员一览

成员类型说明
operator bool()bool响应有效时返回 true
is_valid()bool与operator bool()等价
status()intHTTP 状态码
headers()const Headers&响应头
get_header_value(key, def)std::string获取响应头值(可带默认值)
has_header(key)bool检查响应头是否存在
next()bool读取下一块数据,读完返回 false
data()const char*当前块数据指针
size()size_t当前块大小
read_all()std::string把剩余响应体全部读入字符串
error()Error获取连接/请求错误
read_error()Error获取最近一次读错误
has_read_error()bool检查是否发生读错误

实现细节上,next()(httplib.h)在句柄无效或已结束时直接返回false,否则确保内部缓冲区不小于chunk_size_后调用handle_.read();n > 0则更新current_size_并返回true,读到 0 或负值时置finished_ = true并返回false。read_all()不过是反复next()并把每块append到字符串——这解释了"只能迭代一次"的约束:一旦读完,finished_即被置位。

此外stream::Result是**仅移动(move-only)**类型:拷贝构造与拷贝赋值被删除,移动构造/赋值可用(httplib.h),因此把它放入 lambda 捕获列表或返回时需使用std::move。

六、实战示例

示例 1:SSE(Server-Sent Events)客户端

用stream::Get()逐块读取并实时输出事件流,每读一块立即flush:

#include "httplib.h" #include <iostream> int main() { httplib::Client cli("http://localhost:1234"); auto result = httplib::stream::Get(cli, "/events"); if (!result) { return 1; } while (result.next()) { std::cout.write(result.data(), result.size()); std::cout.flush(); } return 0; }

需要自动重连、事件解析与Last-Event-ID续传的完整 SSE 客户端,可参考仓库示例 example/ssecli-stream.cc:它在主循环中不断调用httplib::stream::Get(cli, path, headers),连接失败或读错误后按retry_ms间隔重连,并通过parse_sse_line()按 SSE 规范把缓冲内容切分成event/data/id/retry字段,遇到空行即完成一个事件;对 204/404/401/403 等永久性错误则直接退出不再重连。它还会校验Content-Type是否为text/event-stream,并在流结束后用result.read_error()区分"正常结束"与"连接中断"。

示例 2:LLM 流式响应

以本地 Ollama 服务的/api/generate为例,逐块接收模型生成内容,并在读取结束后检查错误:

#include "httplib.h" #include <iostream> int main() { httplib::Client cli("http://localhost:11434"); // Ollama auto result = httplib::stream::Get(cli, "/api/generate"); if (result && result.status() == 200) { while (result.next()) { std::cout.write(result.data(), result.size()); std::cout.flush(); } } // Check for connection errors if (result.read_error() != httplib::Error::Success) { std::cerr << "Connection lost\n"; } return 0; }

result.status()在响应头到达后即可获得(无需等待整个响应体),配合read_error()可区分"流正常结束"与"中途断开"两种收尾形态。

示例 3:大文件下载与进度显示

边读边写磁盘,并用累计字节数输出进度:

#include "httplib.h" #include <fstream> #include <iostream> int main() { httplib::Client cli("http://example.com"); auto result = httplib::stream::Get(cli, "/large-file.zip"); if (!result || result.status() != 200) { std::cerr << "Download failed\n"; return 1; } std::ofstream file("download.zip", std::ios::binary); size_t total = 0; while (result.next()) { file.write(result.data(), result.size()); total += result.size(); std::cout << "\rDownloaded: " << (total / 1024) << " KB" << std::flush; } std::cout << "\nComplete!\n"; return 0; }

由于每个块处理完即被覆盖,即使文件达数 GB,内存占用也稳定在一个块(默认 8 KB)附近。仓库测试 test/test.cc 中的PostLarge用例即用stream::Post读取 100 KB 响应并断言累计字节数精确等于100 * 1024,验证了块拼接的完整性。

示例 4:反向代理流式转发

服务端 handler 中打开到上游的流,把状态码、响应头与响应体原样转发给下游客户端:

#include "httplib.h" httplib::Server svr; svr.Get("/proxy/(.*)", [](const httplib::Request& req, httplib::Response& res) { httplib::Client upstream("http://backend:8080"); auto handle = upstream.open_stream("/" + req.matches[1].str()); if (!handle.is_valid()) { res.status = 502; return; } res.status = handle.response->status; res.set_chunked_content_provider( handle.response->get_header_value("Content-Type"), handle = std::move(handle) mutable { char buf[8192]; auto n = handle.read(buf, sizeof(buf)); if (n > 0) { sink.write(buf, static_cast<size_t>(n)); return true; } sink.done(); return true; } ); }); svr.listen("0.0.0.0", 3000);

关键在于handle = std::move(handle):把StreamHandle移动进set_chunked_content_provider的 lambda,使 socket 生命周期覆盖整个响应发送过程;每次回调从上游读一块、经sink.write()写到下游,直到read()返回非正值时调用sink.done()收尾。这正是反向代理场景"读到什么转发什么"的典型实现。

七、与既有 API 的对比

特性Client::Get()open_stream()stream::Get()
响应头可用时机完整接收后立即可用立即可用
响应体读取方式一次性整体缓冲直接从 socket 读取迭代器式读取
内存占用整个响应体在内存极小(可控)极小(可控)
Keep-Alive 支持✅ 支持❌ 不支持❌ 不支持
压缩处理自动处理自动处理自动处理
最适合场景小响应、连接复用底层流式控制便捷流式读取

八、流式 API 的特性清单

  • 真正的 socket 级流式:数据直接从网络 socket 读取,不经整包缓冲;
  • 低内存占用:任意时刻内存中只有当前一个数据块;
  • 压缩支持:gzip、brotli、zstd 自动解压;
  • 分块传输:完整支持 chunked transfer encoding,含 trailer 解析;
  • SSL/TLS 支持:HTTPS 连接同样可用。

关于压缩与解压,从源码看:open_stream()读取响应头中的Content-Encoding后调用detail::create_decompressor()创建解压器(httplib.h);若是已知编码但当前构建未启用对应后端(如未开启 brotli),返回Error::UnsupportedContentEncoding,未知编码则原样透传。解压路径read_with_decompression()内部使用 8192 字节的压缩缓冲(kDecompressionBufferSize)分块喂给解压器,并受payload_max_length(即客户端的set_payload_max_length)约束,防止解压炸弹。分块传输侧,detail::ChunkedDecoder(httplib.h)按 RFC 9112 解析 chunk-size、chunk-ext 与 trailer,并对畸形分块返回读取错误。

测试 test/test.cc 覆盖了 open_stream 对 gzip 解压、未知编码、默认请求头(Host / User-Agent / Accept-Encoding)、POST 的 Content-Type 行为、大响应与分块响应读取等场景;test/test.cc 则验证了 brotli 压缩内容经open_stream自动解压后内容一致。StreamApiTestfixture(test/test.cc)为所有stream::*测试统一起了一个本地 HTTP 服务,覆盖 Get/Post/Put/Patch 的基本读写、查询参数、自定义头与 404 状态码等断言,是理解 API 行为最直观的参照。

九、Keep-Alive 行为与选型提醒

流式 API(stream::Get()/open_stream())在流的整个生命周期内接管 socket 所有权,这意味着:

  • 流式连接不支持 Keep-Alive;
  • StreamHandle析构时 socket 随即关闭(StreamHandle的connection_与socket_stream_均由其独占持有,见 httplib.h);
  • 需要连接复用的场景请使用标准client.Get()API。
// Use for streaming (no Keep-Alive) auto result = httplib::stream::Get(cli, "/large-stream"); while (result.next()) { /* ... */ } // Use for Keep-Alive connections auto res = cli.Get("/api/data"); // Connection can be reused

选型时把握一条主线:小响应、高频短请求选Client::Get()享受连接复用;大响应、实时流、逐块处理选stream::Get();需要直接控制 socket 读取粒度、做代理转发选open_stream();纯 SSE 且有重连诉求直接选SSEClient。

十、相关资源

  • 流式 API 是 cpp-httplib 近期新增能力,需求源头对应上游 issue #2269(原始功能请求);本仓库中的落地实现集中在 httplib.h 的stream命名空间与ClientImpl::open_stream。
  • SSE 高层客户端:文档见 README-sse.md,实现位于 httplib.h。
  • 带自动重连的流式 SSE 客户端示例:example/ssecli-stream.cc。
  • 流式 API 的完整测试:test/test.cc(open_stream()测试)与 test/test.cc(stream::*测试)。

编译运行本仓库示例时,直接#include "httplib.h"即可(header-only,无需链接额外库);example/目录下提供Makefile与各示例源码,可参照构建。

  • 后端
  • 网络

【免费下载链接】cpp-httplib

A C++ header-only HTTP/HTTPS server and client library

项目地址:https://gitcode.com/GitHub_Trending/cp/cpp-httplib
点击查看免费下载
上一篇:Liquidsoap与FFmpeg集成指南:解锁高级媒体处理与HLS流媒体能力
下一篇:CrowdSec 威胁情报共享协议比较:TAXII vs STIX vs MISP

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询