- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
本篇文章以 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 |
|---|---|
| 需要自动重连的 SSE | SSEClient(见 README-sse.md) |
| LLM 流式输出(JSON Lines) | stream::Get() |
| 大文件下载 | stream::Get()或open_stream() |
| 反向代理 | open_stream() |
| 小响应 + Keep-Alive | Client::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 成员一览
| 成员 | 类型 | 说明 |
|---|---|---|
response | std::unique_ptr<Response> | 含响应头的 HTTP 响应对象 |
error | Error | 请求失败时的错误码 |
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()实现可以看到它做了完整的事前准备:
- 目标编码与缓冲发送路径保持一致:空
Params时直接用path,否则调用append_query_params()追加查询参数,再经detail::encode_request_target()编码,保证无论走哪个 API,同样的path产生同样的请求行; - 连接准备:在
socket_mutex_保护下检查现有 socket 是否存活(is_socket_alive,SSL 下还会检查对端是否关闭),失效则断开重建,并通过setup_proxy_connection()处理代理; - 所有权转移:
transfer_socket_ownership_to_handle()(httplib.h)把 socket 描述符(及 SSL 会话)从Client移交给StreamHandle,客户端自身的socket_置为INVALID_SOCKET,从此连接生命周期完全由句柄掌控; - 请求写出:先在内存
BufferStream中组装请求行与请求头(write_request_line+check_and_write_headers),校验通过后再一次性写入网络,随后写入请求体(若有),避免被拒绝的头污染线上数据; - 响应解析:读取响应行与响应头,并做与普通路径相同的 framing 检查(HEAD、204、304 可合法携带无体 framing 头,
Content-Length冲突视为Error::Read); - 传输语义识别:根据响应头设置
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() | int | HTTP 状态码 |
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
相关推荐
实时数据推送新范式:cpp-httplib实现Server-Sent Events(SSE)全指南
实时数据推送新范式:cpp httplib实现Server Sent Events SSE 全指南 你是否还在为Web应用的实时数据更新烦恼?传统轮询效率低下,
后端网络cpp-httplib HTTP重定向处理:301、302与最佳实践
cpp httplib HTTP重定向处理:301、302与最佳实践 你是否在C++ HTTP服务开发中遇到过重定向逻辑混乱、客户端兼容性问题或性能瓶颈?作为c
后端网络3行代码搞定!cpp-httplib响应头获取实战指南
3行代码搞定!cpp httplib响应头获取实战指南 你是否还在为C++ HTTP客户端获取响应头信息而烦恼?本文将用最简洁的方式,带你掌握cpp httpl
后端网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考