前置基础:理解 SSE 必备的底层认知
在学习 SSE 之前,需要先明确两个 HTTP 协议的基础特性,以及实时数据交互的核心矛盾。
- HTTP 协议的单向通信模型HTTP 是一种基于 "请求 - 响应" 模式的应用层协议:通信只能由客户端主动发起,服务端被动接收请求后返回对应响应;单次请求对应单次响应,响应结束后连接通常就会释放。服务端无法在没有客户端请求的前提下,主动向客户端发送数据。
- 实时数据更新的需求与早期方案的痛点在行情监控、消息通知、实时日志等场景中,业务需要服务端在数据更新时立刻告知客户端,这与 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. 具象示例
以网站的 "系统公告实时推送" 为例:
- 用户登录网站后,页面自动创建 SSE 连接,向服务端的
/api/notice接口发起请求 - 服务端收到请求后,返回 200 状态码,设置好对应的响应头,保持连接不关闭
- 当管理员发布一条新公告时,服务端直接在已有的 SSE 连接中写入公告内容
- 用户的浏览器收到推送数据,立刻在页面顶部弹出公告提示
- 整个过程用户无需刷新页面,服务端也不需要等待客户端主动请求
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 个步骤:
- 连接建立 :客户端创建 EventSource 对象,向服务端发起 GET 请求,请求头携带
Accept: text/event-stream - 响应握手 :服务端返回 HTTP 200 状态码,响应头包含
Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive,不结束响应体 - 持续推送:服务端有新数据时,按照 SSE 格式向响应流中写入数据,客户端自动触发消息事件接收数据
- 心跳保活:服务端定期发送注释行(冒号开头的内容)作为心跳,防止连接因长时间无数据被中间节点(如网关、防火墙)断开
- 断线重连:如果网络波动导致连接断开,浏览器会自动等待一段时间后重新发起连接请求,重连时会带上上次收到的最后一个事件 ID,服务端可以据此补发中间遗漏的数据
4. 应用场景
通信流程的特性决定了它在以下场景的优势:
- 弱网环境下的实时推送:自动重连机制降低了网络波动的影响
- 长时间持续的数据流:比如持续数小时的监控数据推送,连接开销极低
- 需要断点续传的推送场景:比如行情数据、消息通知,不能因为短暂断网就丢失数据
三、SSE 数据协议规范
1. 定义
SSE 定义了一套纯文本格式的事件流编码规范,服务端必须按照该格式发送数据,客户端才能正确解析出事件类型、数据内容、事件 ID 等信息。
2. 底层原理
SSE 采用纯文本、行分隔的格式设计,核心原因有三点:
- 完全兼容 HTTP 协议,不需要额外的协议解析层
- 文本格式人类可读,调试排查问题简单
- 浏览器可以内置通用的解析逻辑,降低前端使用成本
SSE 的数据以事件为单位,每个事件由多行组成,事件之间用一个空行分隔;每行由字段名和值组成,用冒号分隔。标准字段共有 4 个:data、event、id、retry。
3. 具象示例
(1)最简单的普通消息
plaintext
kotlin
data: 这是一条推送的消息内容
- 只有一个
data字段,客户端会触发默认的message事件 - 末尾的空行是事件结束的标志,必不可少
(2)自定义事件类型的消息
plaintext
makefile
event: notice
data: 您有一条新的系统公告
- 通过
event字段指定事件名称为notice - 客户端可以监听
notice事件单独处理这类消息,实现多事件分类
(3)带事件 ID 与重连时间的消息
plaintext
yaml
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
- 如果实时性要求不高、追求极致兼容:可以用短轮询
- 如果是旧系统改造、不想改太多架构:可以用长轮询过渡
五、常见认知误区
- 误区:SSE 和 WebSocket 是竞争关系,SSE 是落后技术纠正:两者定位完全不同。SSE 专注单向推送,实现更简单、原生支持断线重连与断点续传、可以直接复用 HTTP 网关与鉴权体系;WebSocket 专注双向通信。单向推送场景下 SSE 是更优解,并非技术落后。
- 误区:SSE 只能传输文本,完全不能传二进制数据纠正:标准 SSE 确实只支持文本格式(通常是 JSON 字符串),但可以通过 Base64 编码将二进制数据转为文本传输,只是会增加约 33% 的体积开销。如果有大量二进制传输需求,更适合用 WebSocket。
- 误区:SSE 没有跨域问题纠正:SSE 的 EventSource API 默认受浏览器同源策略限制,需要服务端配置 CORS 响应头才能跨域使用。另外,旧版浏览器的 EventSource 不支持自定义请求头,跨域鉴权需要通过 URL 参数等方式处理。
- 误区:SSE 连接数量没有限制纠正:浏览器对同一个域名的 HTTP 并发连接数有限制(通常是 6 个),SSE 会占用一个长连接名额;如果打开多个同域名页面,SSE 连接数会叠加,达到上限后新的连接会被阻塞。HTTP/2 下这个限制会大幅放宽。
六、落地实操建议
服务端实现建议
- 必须设置正确的响应头:
Content-Type: text/event-stream; charset=utf-8、Cache-Control: no-transform、Connection: keep-alive - 禁用反向代理的缓冲:Nginx 等反向代理默认会缓冲响应体,导致数据不能实时推送,需要配置
proxy_buffering off - 定期发送心跳:间隔建议 30 秒以内,防止连接被中间节点超时断开,心跳用注释行即可,不影响业务
- 支持断点续传:读取请求头的
Last-Event-ID,补发对应 ID 之后的消息,提升可靠性
前端实现建议
- 优先使用原生
EventSourceAPI,不需要引入额外依赖 - 监听
error事件处理连接异常,配合retry机制优化重连体验 - 需要自定义请求头或 POST 方式时,可以用 fetch + ReadableStream 模拟 SSE 解析,突破原生 API 的限制
- 页面隐藏时可以考虑暂停处理非紧急消息,降低性能消耗
架构适配建议
- 小规模业务直接在业务服务中集成 SSE 接口即可
- 大规模推送场景可以搭配消息队列,服务端订阅消息后推给客户端
- 有网关的场景提前确认网关对长连接、流式响应的支持,避免被网关截断连接