流式渲染的基础——SSE

40 阅读11分钟

前置基础:理解 SSE 必备的底层认知

在学习 SSE 之前,需要先明确两个 HTTP 协议的基础特性,以及实时数据交互的核心矛盾。

  1. HTTP 协议的单向通信模型HTTP 是一种基于 “请求 - 响应” 模式的应用层协议:通信只能由客户端主动发起,服务端被动接收请求后返回对应响应;单次请求对应单次响应,响应结束后连接通常就会释放。服务端无法在没有客户端请求的前提下,主动向客户端发送数据。
  2. 实时数据更新的需求与早期方案的痛点在行情监控、消息通知、实时日志等场景中,业务需要服务端在数据更新时立刻告知客户端,这与 HTTP 的单向模型天然矛盾。早期行业主要用两种方案模拟实时效果:
  • 短轮询:客户端每隔固定时间向服务端发一次 HTTP 请求,拉取最新数据。优点是实现简单,缺点是大量无效请求,浪费服务器资源和带宽,实时性受轮询间隔限制。
  • 长轮询:客户端发起请求后,服务端如果没有新数据就先不返回响应,一直挂起连接,直到有数据或超时才返回;客户端收到响应后立刻再发起下一次请求。相比短轮询减少了请求次数,但连接频繁重建,依然有额外开销。

SSE 正是为了低成本、标准化地解决 “服务端单向实时推送” 这一问题而诞生的技术。


一、SSE 核心概念

1. 定义

SSE 全称 Server-Sent Events(服务器发送事件) ,是 HTML5 标准中定义的一套基于 HTTP 协议的、服务端向客户端单向推送文本数据的实时通信规范。通俗来说,SSE 允许客户端只发起一次 HTTP 请求,服务端就会保持连接持续打开,后续有新数据时直接通过这条连接推送,全程数据只能从服务端流向客户端。

2. 底层原理

SSE 的核心本质是一个持续不结束的 HTTP 长响应

  • 它没有创造新的传输层协议,完全复用标准 HTTP 协议
  • 客户端通过 Accept: text/event-stream 请求头声明自己期望接收事件流格式的数据
  • 服务端返回响应时,设置 Content-Type: text/event-stream 响应头,标识响应体是持续的事件流
  • 服务端不关闭 TCP 连接,持续向响应体中追加数据,客户端则以流的方式持续读取并解析数据
  • 浏览器内置了 EventSource API(浏览器提供的专门用于接收 SSE 推送的原生接口),自动处理连接的建立、数据解析和断线重连

3. 具象示例

以网站的 “系统公告实时推送” 为例:

  1. 用户登录网站后,页面自动创建 SSE 连接,向服务端的 /api/notice 接口发起请求
  2. 服务端收到请求后,返回 200 状态码,设置好对应的响应头,保持连接不关闭
  3. 当管理员发布一条新公告时,服务端直接在已有的 SSE 连接中写入公告内容
  4. 用户的浏览器收到推送数据,立刻在页面顶部弹出公告提示
  5. 整个过程用户无需刷新页面,服务端也不需要等待客户端主动请求

4. 适用场景总览

SSE 天然适合服务端单向推送、客户端只接收不发送的场景,典型场景包括:

  • 实时消息通知、系统公告
  • 股票、加密货币等行情数据推送
  • 服务器日志、监控指标实时展示
  • 大模型对话的流式响应输出
  • 体育赛事、直播弹幕等实时数据流

二、SSE 完整通信工作流程

1. 定义

SSE 的通信流程,指的是从客户端发起连接建立请求,到连接持续推送数据,再到异常断线、自动重连的完整生命周期过程。

2. 底层原理

SSE 的流程基于 HTTP 长连接机制实现,核心依赖三个基础能力:

  • HTTP 长连接保活:通过 HTTP/1.1 的 Keep-Alive 机制保持 TCP 连接不释放,避免每次推送都重建连接
  • 流式数据传输:利用 HTTP 分块传输编码(Chunked Transfer Encoding,允许响应体分成多个块陆续发送),让响应体可以分多次写入、持续接收
  • 内置断线重连:浏览器的 EventSource API 内置了重连逻辑,连接异常断开后会自动尝试重新建立连接,还支持通过事件 ID 实现断点续传

3. 具象示例

一次完整的 SSE 通信分为 5 个步骤:

  1. 连接建立:客户端创建 EventSource 对象,向服务端发起 GET 请求,请求头携带 Accept: text/event-stream
  2. 响应握手:服务端返回 HTTP 200 状态码,响应头包含 Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive,不结束响应体
  3. 持续推送:服务端有新数据时,按照 SSE 格式向响应流中写入数据,客户端自动触发消息事件接收数据
  4. 心跳保活:服务端定期发送注释行(冒号开头的内容)作为心跳,防止连接因长时间无数据被中间节点(如网关、防火墙)断开
  5. 断线重连:如果网络波动导致连接断开,浏览器会自动等待一段时间后重新发起连接请求,重连时会带上上次收到的最后一个事件 ID,服务端可以据此补发中间遗漏的数据

4. 应用场景

通信流程的特性决定了它在以下场景的优势:

  • 弱网环境下的实时推送:自动重连机制降低了网络波动的影响
  • 长时间持续的数据流:比如持续数小时的监控数据推送,连接开销极低
  • 需要断点续传的推送场景:比如行情数据、消息通知,不能因为短暂断网就丢失数据

三、SSE 数据协议规范

1. 定义

SSE 定义了一套纯文本格式的事件流编码规范,服务端必须按照该格式发送数据,客户端才能正确解析出事件类型、数据内容、事件 ID 等信息。

2. 底层原理

SSE 采用纯文本、行分隔的格式设计,核心原因有三点:

  • 完全兼容 HTTP 协议,不需要额外的协议解析层
  • 文本格式人类可读,调试排查问题简单
  • 浏览器可以内置通用的解析逻辑,降低前端使用成本

SSE 的数据以事件为单位,每个事件由多行组成,事件之间用一个空行分隔;每行由字段名和值组成,用冒号分隔。标准字段共有 4 个:dataeventidretry

3. 具象示例

(1)最简单的普通消息

plaintext

data: 这是一条推送的消息内容
  • 只有一个 data 字段,客户端会触发默认的 message 事件
  • 末尾的空行是事件结束的标志,必不可少
(2)自定义事件类型的消息

plaintext

event: notice
data: 您有一条新的系统公告
  • 通过 event 字段指定事件名称为 notice
  • 客户端可以监听 notice 事件单独处理这类消息,实现多事件分类
(3)带事件 ID 与重连时间的消息

plaintext

id: 10086
retry: 5000
data: 带ID的消息,断线后可以从这里续传
  • id 字段设置事件 ID,浏览器会记录这个 ID,重连时通过 Last-Event-ID 请求头发送给服务端
  • retry 字段设置断线重连的等待时间,单位是毫秒,这里表示断线后 5 秒再尝试重连
(4)心跳注释

plaintext

: 这是心跳注释,不会触发消息事件
  • 以冒号开头的行是注释,客户端会忽略,通常用作心跳保活

4. 应用场景

不同的格式字段对应不同的业务需求:

  • 简单推送场景:只用 data 字段即可,比如实时日志
  • 多类型消息场景:搭配 event 字段做事件分类,比如同时推送通知、行情、公告
  • 高可靠推送场景:搭配 id 字段实现断点续传,确保消息不丢失
  • 弱网适配场景:通过 retry 字段调整重连频率,平衡实时性与服务器压力

四、SSE 与同类实时技术的选型对比

1. 定义

实时通信领域有多种主流方案,SSE 定位是 “轻量化的服务端单向推送方案”,与短轮询、长轮询、WebSocket 等方案在协议、能力、成本上有明确差异。

2. 底层原理

不同方案的核心差异在协议层与通信方向:

  • 轮询方案:完全基于普通 HTTP 请求,通过频繁请求模拟实时,本质还是客户端主动拉取
  • SSE:基于 HTTP 长连接,单向推送,服务端主动发,客户端只接收
  • WebSocket:基于独立的 WebSocket 协议,握手后是全双工通信(双方可以同时互相发送数据),客户端和服务端都可以主动发数据

3. 具象对比

表格

技术方案通信方向协议基础实时性服务器资源消耗实现复杂度浏览器兼容性
短轮询客户端单向拉取HTTP低(受轮询间隔限制)高(大量无效请求)极低完全兼容
长轮询客户端单向拉取HTTP中(连接频繁重建)完全兼容
SSE服务端单向推送HTTP低(长连接持续)极低(浏览器内置 API)现代浏览器全支持
WebSocket双向全双工通信WebSocket 协议低(长连接持续)现代浏览器全支持

4. 选型应用场景

  • 如果只是服务端给客户端推数据,没有客户端频繁发消息的需求:优先选 SSE,开发成本最低,复用 HTTP 基础设施
  • 如果需要双向实时通信(比如聊天、协同编辑):选 WebSocket
  • 如果实时性要求不高、追求极致兼容:可以用短轮询
  • 如果是旧系统改造、不想改太多架构:可以用长轮询过渡

五、常见认知误区

  1. 误区:SSE 和 WebSocket 是竞争关系,SSE 是落后技术纠正:两者定位完全不同。SSE 专注单向推送,实现更简单、原生支持断线重连与断点续传、可以直接复用 HTTP 网关与鉴权体系;WebSocket 专注双向通信。单向推送场景下 SSE 是更优解,并非技术落后。
  2. 误区:SSE 只能传输文本,完全不能传二进制数据纠正:标准 SSE 确实只支持文本格式(通常是 JSON 字符串),但可以通过 Base64 编码将二进制数据转为文本传输,只是会增加约 33% 的体积开销。如果有大量二进制传输需求,更适合用 WebSocket。
  3. 误区:SSE 没有跨域问题纠正:SSE 的 EventSource API 默认受浏览器同源策略限制,需要服务端配置 CORS 响应头才能跨域使用。另外,旧版浏览器的 EventSource 不支持自定义请求头,跨域鉴权需要通过 URL 参数等方式处理。
  4. 误区:SSE 连接数量没有限制纠正:浏览器对同一个域名的 HTTP 并发连接数有限制(通常是 6 个),SSE 会占用一个长连接名额;如果打开多个同域名页面,SSE 连接数会叠加,达到上限后新的连接会被阻塞。HTTP/2 下这个限制会大幅放宽。

六、落地实操建议

服务端实现建议

  • 必须设置正确的响应头:Content-Type: text/event-stream; charset=utf-8Cache-Control: no-transformConnection: keep-alive
  • 禁用反向代理的缓冲:Nginx 等反向代理默认会缓冲响应体,导致数据不能实时推送,需要配置 proxy_buffering off
  • 定期发送心跳:间隔建议 30 秒以内,防止连接被中间节点超时断开,心跳用注释行即可,不影响业务
  • 支持断点续传:读取请求头的 Last-Event-ID,补发对应 ID 之后的消息,提升可靠性

前端实现建议

  • 优先使用原生 EventSource API,不需要引入额外依赖
  • 监听 error 事件处理连接异常,配合 retry 机制优化重连体验
  • 需要自定义请求头或 POST 方式时,可以用 fetch + ReadableStream 模拟 SSE 解析,突破原生 API 的限制
  • 页面隐藏时可以考虑暂停处理非紧急消息,降低性能消耗

架构适配建议

  • 小规模业务直接在业务服务中集成 SSE 接口即可
  • 大规模推送场景可以搭配消息队列,服务端订阅消息后推给客户端
  • 有网关的场景提前确认网关对长连接、流式响应的支持,避免被网关截断连接