- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
Server-Sent Events(SSE)是一种服务器向客户端单向推送事件的轻量协议:连接保持打开,服务器可在任意时刻主动下发数据,且完全基于 HTTP 实现,比 WebSocket 更轻。cpp-httplib 并没有提供 SSE 专用 API,但借助Response::set_chunked_content_provider()与text/event-stream响应类型即可完整实现。本文将围绕 docs-src/pages/ja/cookbook/e01-sse-server.md 的实践路径,结合仓库源码(httplib.h)深入讲解 SSE 服务器的三种写法、连接断开检测、心跳保活、线程池容量规划,以及仓库内真实可运行示例 example/ssesvr.cc 的完整实现。
SSE 与 cpp-httplib 的实现路径
SSE 协议的核心行为是:客户端(浏览器EventSource或其他 SSE 客户端)发起普通 HTTP 请求,服务器以Content-Type: text/event-stream返回响应,随后持续向同一连接写入事件帧,直到连接关闭。事件帧的格式由 SSE 规范定义,最基本的形式是:
data: <内容>\n\n其中\n\n表示一个事件的结束。由于连接是持久的、响应体没有固定长度,SSE 天然适合 HTTP 分块传输编码(chunked transfer encoding)——这正是 cpp-httplib 中set_chunked_content_provider()所提供的机制。
从源码结构看,cpp-httplib 的 SSE 支持完全建立在通用流式响应能力之上,并没有专门的SSE类或接口。其核心入口是 Response::set_chunked_content_provider:
void set_chunked_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser = nullptr);其实现(httplib.h#L11733-L11743)做了三件事:
- 通过
set_header("Content-Type", content_type)设置响应头(传入text/event-stream即完成 SSE 必需的 MIME 类型声明); - 将
content_length_置为 0,表示响应体没有预知长度,走分块传输; - 置
is_chunked_content_provider_ = true,使服务器在发送阶段进入分块写入路径。
基本 SSE 服务器
这是最简形式的 SSE 端点,来自原文档:
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) { res.set_chunked_content_provider( "text/event-stream", [](size_t offset, httplib::DataSink &sink) { std::string message = "data: hello\n\n"; sink.write(message.data(), message.size()); std::this_thread::sleep_for(std::chrono::seconds(1)); return true; }); });这里有三个关键点:
- Content-Type 必须是
text/event-stream:客户端(如浏览器EventSource)只有收到该 MIME 类型才会按 SSE 事件流解析响应; - 消息按
data: <内容>\n\n格式书写:\n\n(即两个换行)是单个事件的结束标记,客户端收到后才触发onmessage; - 每次调用
sink.write()都会立即将数据送达客户端:无需等响应结束,这正是流式响应的意义。
只要连接存活,provider 回调(lambda)会被服务器反复调用。其调用循环位于服务器发送阶段的 write_content_chunked:服务器在一个while (data_available && !is_shutting_down())循环中反复调用content_provider(offset, 0, data_sink);回调内每写一次数据,sink.write就把内容编码为<十六进制长度>\r\n<payload>\r\n的分块帧写入 socket(httplib.h#L8823-L8847);回调返回true则继续下一轮,返回false则服务器以Error::Canceled终止该响应。上述示例中sleep_for(1秒)使/events每秒推送一次hello。
持续发送事件的完整示例
下面这个示例把服务器当前时刻每秒推送一次,并在客户端断开时优雅收尾:
svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) { res.set_chunked_content_provider( "text/event-stream", &req { if (req.is_connection_closed()) { sink.done(); return true; } auto now = std::chrono::system_clock::now(); auto t = std::chrono::system_clock::to_time_t(now); std::string msg = "data: " + std::string(std::ctime(&t)) + "\n"; sink.write(msg.data(), msg.size()); std::this_thread::sleep_for(std::chrono::seconds(1)); return true; }); });这段代码体现了 SSE 服务器两个必备能力:
1. 客户端断开检测
req.is_connection_closed()是Request上可调用的函数对象,在服务器每次处理请求时被注入(httplib.h#L14501-L14505):
req.is_connection_closed = [sock]() { return !detail::is_socket_alive(sock); };其底层是detail::is_socket_alive(),即通过 socket 存活探测判断对端是否已经关闭。检测到断开后应立即调用sink.done()结束响应体——done()在分块协议中会写入终止块0\r\n(httplib.h#L8895、httplib.h#L8875-L8876),随后回调返回true让发送循环自然退出,避免对已断开的 socket 反复写入。
关于断开的更多处理手法可参考 S16. 客户端断开的检测。
2. 按需调度推送内容
provider 回调在服务器线程中串行执行,因此可以在回调内自行控制发送节奏(如sleep_for),也可以像仓库示例那样用条件变量阻塞等待业务事件(见下文“仓库完整示例”)。
用注释行实现心跳保活
SSE 规范规定:以冒号:开头的行是注释行,客户端会忽略其内容,但它仍是合法的连接数据。利用这一点可以定期发送“心跳”帧,维持连接活跃,防止代理服务器或负载均衡器因为长时间无通信而掐断空闲连接:
// 30秒ごとにハートビート if (tick_count % 30 == 0) { std::string ping = ": ping\n\n"; sink.write(ping.data(), ping.size()); }(上例中tick_count为回调内维护的循环计数变量,此处示意每 30 轮发送一次心跳。)
心跳不携带业务数据,但能确保连接上有持续的网络流量,从而让中间设备认为连接仍在使用中。这在生产环境部署 SSE(尤其是经反向代理或云负载均衡)时几乎是必需品。
SSE 与线程池:并发容量规划
SSE 连接是长连接,一个连接在其存续期间会独占一个工作线程。若同时在线客户端数量可观,默认线程池可能不够用。cpp-httplib 的做法是让每个连接在请求处理阶段占用一个 worker,因此 SSE 并发上限直接受线程池容量约束。
原文档给出的方案是开启线程池动态扩容:
svr.new_task_queue = [] { return new httplib::ThreadPool(8, 128); };ThreadPool的构造签名(httplib.h#L2003-L2005):
explicit ThreadPool( size_t n, size_t max_n = 0, size_t mqr = 0, time_t idle_timeout_sec = CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT);各参数含义(结合 httplib.h#L11861-L11873 的实现):
n:基础(常驻)线程数,构造时立即创建;max_n:最大线程数;设为 0 时max_thread_count_就等于n,即不扩容;大于n时线程池可按需动态创建额外线程,用于应对突发的长连接;mqr:请求队列最大长度(max_queued_requests_),超过后新请求会被拒绝;idle_timeout_sec:动态线程的空闲超时,默认值由编译期宏CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT定义,默认为 3 秒(httplib.h#L194-L195)。
svr.new_task_queue是服务器用于创建工作队列的工厂回调(httplib.h#L2322),服务器启动时调用它以构造TaskQueue(httplib.h#L13906)。因此ThreadPool(8, 128)的含义是:常驻 8 个线程,负载高时最多扩展到 128 个线程,每个 SSE 客户端独享其一。更细致的配置方法参见 S21. 多线程数量的设置。
仓库完整示例:事件分发式 SSE 服务器
仓库中的 example/ssesvr.cc 提供了一个更贴近真实业务的 SSE 服务器实现,可编译运行作为对照(编译方式见 example/Makefile)。其设计要点:
- 定义
EventDispatcher类,内部用std::mutex+std::condition_variable实现“事件广播”:send_event()写入消息并notify_all(),每个 SSE 连接的回调通过wait_event()阻塞等待新事件; - 注册两个事件通道
/event1、/event2,均以set_chunked_content_provider("text/event-stream", ...)提供流式响应(example/ssesvr.cc#L77-L91); wait_event()使用cv_.wait_for(..., 5秒)超时等待,避免客户端断开后回调永久挂起(example/ssesvr.cc#L17-L29)——这对应了文档中“断开后调用sink.done()结束”的场景;- 根路径
/返回一段内嵌 HTML,通过浏览器new EventSource("event1")建立两条 SSE 连接实时打印消息(example/ssesvr.cc#L46-L66); - 后台线程每秒向
EventDispatcher推送一条编号事件(example/ssesvr.cc#L93-L103)。
该示例展示了一种与“回调内主动 sleep 定时发送”互补的模式:由业务线程驱动事件,SSE 回调只负责把最新事件写进连接,适合把数据库变更、日志、告警等真实事件实时推给前端。
进阶注意点
多行数据的格式:SSE 规范规定,
data:后的内容若包含换行,必须按行拆成多条data:行发送。例如内容line1\nline2应写成:data: line1 data: line2客户端收到后会将同一事件内的多个
data:行用换行符拼接,作为event.data。因此构造消息时不要直接把含\n的裸文本塞进单条data:行,否则事件会被提前截断。命名事件:如果需要为不同类型的事件指定
event:名称字段,参见 E02. SSE 事件名的区分。客户端接收:SSE 客户端侧的实现与接收方式参见 E04. 在客户端接收 SSE。
连接关闭与资源释放:
set_chunked_content_provider的第三个参数resource_releaser可在响应结束时回调,适合释放回调捕获的动态资源;Response析构时也会调用它(httplib.h#L1876-L1880)。压缩交互:分块写入路径会依据协商结果对每个块做压缩后再发送(httplib.h#L13477-L13487)。对 SSE 这种实时流,建议关闭压缩或评估延迟影响,因为压缩会引入缓冲。
小结
在 cpp-httplib 中实现 SSE 服务器无需任何专用 API,只需三步:用set_chunked_content_provider("text/event-stream", ...)开启流式响应、按data: ...\n\n规范组织事件帧、在回调内通过sink.write()持续推送并用req.is_connection_closed()+sink.done()处理断开。再配合注释行心跳与动态线程池(ThreadPool(8, 128)),即可支撑经代理的生产环境长连接场景。仓库的 example/ssesvr.cc 提供了完整可运行的参考实现,可作为开发起点。
- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
相关推荐
用 cpp-httplib 实现 SSE 服务器:基于 `text/event-stream` 的 HTTP 长连接实时推送实战
用 cpp httplib 实现 SSE 服务器:基于 text/event stream 的 HTTP 长连接实时推送实战 Server Sent Event
后端网络cpp-httplib 服务端处理 SSE 重连:Last-Event-ID、retry 间隔与事件缓冲实践指南
cpp httplib 服务端处理 SSE 重连:Last Event ID、retry 间隔与事件缓冲实践指南 导读 Server Sent Events(S
后端网络cpp-httplib 实现 SSE 命名事件:`event:`、`id:`、JSON 与多行 `data:` 的完整实践
cpp httplib 实现 SSE 命名事件: event: 、 id: 、JSON 与多行 data: 的完整实践 导读 本文围绕 docs src/pag
后端网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考