streaming-to-sse-consumer · ZH · 2026-10-27

把聚合站的流接到浏览器 SSE 消费端:一把 key 上的心跳、背压与提前关闭

本文介绍如何将聚合站的流式补全接入浏览器 SSE 消费端,重点讲解心跳保活、客户端断连处理以及断连后的计费逻辑,帮助开发者构建稳定且成本可控的实时应用。

为什么需要关注流式补全的浏览器消费端

聚合站提供的流式补全接口通常基于 HTTP 分块传输或 SSE(Server-Sent Events)。在浏览器端,你可以使用 EventSource 或 fetch 的 ReadableStream 来消费这些数据。然而,直接连接会面临网络波动、代理超时、客户端意外关闭等问题。正确处理心跳、背压和提前关闭,不仅能提升用户体验,还能避免不必要的计费。

两种浏览器消费方式

EventSource

EventSource 是浏览器原生支持的 SSE 客户端,自动处理重连和事件解析。但它只支持 GET 请求,且无法自定义请求头,因此不适合需要传递 API key 或复杂参数的场景。通常你需要在同源后端做一层代理,由后端携带 key 请求聚合站,再将流式响应转发给前端。

fetch + ReadableStream

使用 fetch 获取响应体,通过 response.body.getReader() 逐块读取。这种方式灵活,支持 POST 和自定义头,适合直接与聚合站通信(需注意 CORS 配置)。但你需要手动解析 SSE 格式,并处理连接中断和重连逻辑。

心跳保活机制

流式连接可能因中间代理或负载均衡器的空闲超时而断开。为了保持连接活跃,聚合站或你的代理层应定期发送注释行(如 : heartbeat)或空事件。浏览器端无需特殊处理,但需确保读取循环不会因长时间无数据而阻塞。

如果你自行实现代理,建议设置合理的超时时间,并在无数据时发送心跳。客户端应监听 error 事件(EventSource)或捕获读取异常(fetch),并实施指数退避重连。

背压处理

当客户端消费速度慢于服务端生产速度时,会产生背压。在浏览器中,ReadableStream 默认提供背压支持:如果未调用 reader.read(),流会将压力传递回网络层。但 SSE 是单向的,服务端无法感知客户端消费速度,因此背压主要体现在内存缓冲上。

为避免内存溢出,建议:

  • 及时处理每个数据块,避免在内存中累积大量未处理的事件。
  • 对于高频数据,考虑使用 TextDecoderStream 进行流式解码,并逐行解析。
  • 在 UI 渲染时使用防抖或虚拟化,减少主线程压力。

客户端断连处理

用户可能关闭标签页、刷新页面或网络中断。浏览器端应监听 beforeunload 或 pagehide 事件,主动关闭连接(EventSource 调用 close(),fetch 调用 reader.cancel())。这能释放资源,并通知服务端停止生成。

在代理层,需要检测客户端断开(如 req.on('close')),并中止对聚合站的请求。聚合站通常会在客户端断开后停止计费,但具体取决于实现。

断连后哪些仍计费

计费基于实际生成的 token 数。如果客户端在流式传输完成前断开,聚合站可能已经生成并发送了部分 token。这部分 token 是否计费取决于聚合站的策略:通常,已生成的 token 会计费,而未生成的部分不计费。因此,及时关闭连接可以减少不必要的费用。

建议在客户端实现超时机制,避免长时间挂起的连接。同时,在代理层记录请求和断开时间,便于对账。

实践建议

  • 使用 fetch + ReadableStream 获得更大控制权,但需自行处理 SSE 解析和重连。
  • 为连接设置合理的超时和心跳间隔,避免中间设备断开。
  • 在客户端和代理层均实现断连检测,及时释放资源。
  • 监控 token 消耗,结合业务逻辑优化流式传输的启停。

通过以上方法,你可以构建稳定、高效的流式补全浏览器应用,并有效控制成本。