SSE(Server-Sent Events)圣经: 底层原理 + 应用开发 + 技术对比 (图解+秒懂+史上最全)
本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面
在45岁老架构师 尼恩的读者交流群(50+)中,通过 Java+AI双驱架构 帮助很多小伙伴拿到了一线企业如 字节、得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的面试题:
什么是SSE?SSE为何突然爆火?
SSE与WEBSocket 如何选型?
最近有小伙伴在面试希音、滴滴、阿里等,都到了这个的面试题。 小伙伴是按照尼恩的套路是作答的,拿到了 阿里、希音offer 。
小伙伴是二本,而且空挡一年了, 能拿到 阿里、希音offer ,他也觉得尼恩的回答套路 太牛逼了。
这里尼恩把这个 sse的介绍体系, 展示给大家,帮助大家进大厂,拿高薪。
sse现在很火,建议大家收藏起来,多看几遍
一、什么是SSE?SSE为何突然爆火?
你有没有想过,为什么 ChatGPT 的回答能逐字逐句地“流”出来?
这一切的背后,都离不开一项关键技术——SSE(Server-Sent Events)!
1.1、什么是 SSE?
SSE(Server-Sent Events)是一种基于 HTTP 协议的服务器推送技术,允许服务端主动向客户端发送数据流。
SSE 可以被理解为 HTTP 的一个扩展或一种特定用法。它不是一个全新的、独立的协议,而是构建在标准 HTTP/1.1 协议之上的技术。
SSE 就像是服务器打开了一个“单向数据管道”,服务器通过HTTP 扩展 可以持续不断地流向浏览器,无需客户端反复发起请求。
其实很简单的: SSE = HTTP 扩展字段 + Keepalive 长连接。
SSE 提供了一种简单、可靠的方式来实现服务器向客户端的实时数据推送。它非常适合通知、实时数据更新、日志流和类似 ChatGPT 的逐字输出场景。如果你只需要单向通信,SSE 往往是比 WebSocket 更简单、更轻量的选择。
SSE 适用于服务器主动向客户端推送数据的场景,如实时通知、动态更新等。
所以,目前 几乎所有主流浏览器都原生支持SSE。
1.2 SSE (Server-Sent Events) 诞生背景
短轮询、长轮询、Flash 、 WebSocket
在 SSE 技术出现之前,Web 应用要实现服务器向客户端的实时数据推送,主要依赖以下几种技术,但它们都存在明显的缺陷:
1、 短轮询 (Polling):
- 原理:客户端以固定的时间间隔(例如每秒一次)频繁地向服务器发送请求,询问是否有新数据。
- 缺点:大量请求可能是无效的(无新数据),浪费服务器和带宽资源,实时性差。
短轮询 的流程图
2、 长轮询 (Long Polling):
- 原理:使用长连接请求数据。 客户端发送一个请求,服务器会保持这个连接打开(长连接),直到有新数据可用或超时。一旦客户端收到响应,会立即发起下一个请求。
- 缺点:虽然减少了无效请求,但每个连接仍然需要客户端发起,服务器需要维护大量挂起的连接,实现复杂。
长轮询 (Long Polling) 突破:减少无效请求,但服务器需维护挂起连接
3、 基于 Flash 的解决方案:
- 原理:利用 Adobe Flash 插件提供的 Socket 功能实现全双工通信。
- 缺点:依赖浏览器插件,在移动端(如 iPhone)不受支持,且随着技术的发展(Flash 被淘汰)已走向消亡。基于 Flash 方法都非原生支持,效率低下或依赖外部插件。
4、 基于 WebSocket的解决方案:
- 原理:在客户端与服务器之间建立一条全双工的 TCP 长连接,双方可随时互相推送数据。
- 缺点: 需要一次额外的协议升级握手(
Upgrade: websocket
),对 CDN、防火墙、代理服务器的兼容性不如普通 HTTP; 双向通信能力在“服务器→客户端单向推送”场景下显得过度设计,增加心跳、重连、帧解析等复杂度;早期浏览器支持不一(IE ≤ 9 无原生实现),需要 Polyfill 或 Flash 降级方案。
WebSocket:全双工通道 革命性:摆脱HTTP束缚,实现真 实时交互
WebSocket 并不是 Web 领域 的通讯协议,属于复杂度高 二进制通讯协议。
SSE 诞生的核心背景
因此,Web 领域迫切需要一种标准化的、高效的、由浏览器原生支持的服务器到客户端的单向通信机制。这就是 SSE 诞生的核心背景。
核心需求:
- 简单:易于服务器和客户端实现。
- 高效:基于 HTTP/HTTPS,避免不必要的请求开销。
- 标准:成为 W3C 标准,得到浏览器原生支持。
- 自动重连:内置连接失败后自动重试的机制。
SSE:真正的服务器推送
1.3 SSE 发展历程
SSE 的发展是 Web 标准化进程和实时通信需求共同推动的结果。
下图概述了其关键发展节点:
让我们对图中的关键阶段进行详细解读:
1、 诞生背景(2006 年以前)
- Web 早期只有“请求-响应”范式,实时需求(股票、IM、行情)只能靠轮询或长轮询,延迟高、浪费资源。
- Comet(长连接 iframe、jsonp、xhr-streaming 等 Hack 方案)出现,但实现复杂、浏览器兼容性差、占用连接数高。
- 业界急需一种“浏览器原生、基于 HTTP、单向服务器推送”的轻量机制。
2、 概念提出与标准化 (约 2006-2009年)
- SSE 的概念最初作为 HTML5 标准的一部分被提出,由 WHATWG (Web Hypertext Application Technology Working Group) 和 W3C (World Wide Web Consortium) 共同推动。
- 其设计思想是定义一个简单的、基于 HTTP 的协议,允许服务器通过一个长连接持续地向客户端发送文本流。
- 2006 年,Opera 9 在浏览器里率先实现名为 Server-Sent Events 的实验 API,用 DOM 事件把服务器推送的文本块喂给页面。
- 同期 WHATWG HTML5 草案开始收录相关章节,定义了 text/event-stream MIME 类型及“event: / data:”行协议。
- 后来,它从庞大的 HTML5 规范中分离出来,成为了一个独立的 W3C 标准文档。
- 2008 年:SSE 被正式写入 HTML5 草案,随后进入 W3C 标准流程。
3、 浏览器支持与推广 (约 2010-2015年)
- 2011年左右,主流浏览器(如 Firefox、Chrome、Safari、Opera)开始陆续支持 SSE API。 Firefox 6、Chrome 6、Safari 5、Opera 11.5 陆续完成原生实现;IE 系列缺席(直到 Edge 79 才补票)。
- 关键的障碍:Internet Explorer (包括 IE 11) 始终没有支持 SSE API。这在一定程度上限制了其早期的广泛应用,开发者通常需要为此准备降级方案(如回落到长轮询)。
- 随着 Chrome、Firefox 等现代浏览器的市场份额不断上升,以及移动端浏览器对 SSE 的良好支持,SSE 逐渐成为开发实时 Web 应用的可信选择。
- 2014 年 10 月:HTML5 成为 W3C Recommendation,SSE 作为官方子模块锁定最终语法,浏览器阵营格局定型。
4. 正式推荐与成熟 (2015年至2022 )
-
2015-2020 年,WebSocket 与 WebRTC 占据实时通信话题中心,SSE 主要在企业内部仪表盘、日志 tail 等低频场景默默使用。
-
SSE 由于有 “单向文本流 + 自动重连 + 轻量” 特性,所以没有被WebSocket 与 WebRTC 踩死, 使其在 IoT 设备、移动端 WebView 中仍保有一席之地。
-
2015年,W3C 发布了 Server-Sent Events 的正式推荐标准,标志着该技术的成熟和稳定。
-
在此期间,前端生态框架(如 React、Vue.js)和后端语言(如 Node.js、Python、Java)都提供了对 SSE 的良好支持,出现了大量易用的库和示例。
5、 大模型时代的爆发(2022 至今)
-
虽然 WebSocket 提供了全双工通信能力,但 SSE 因其简单的 API、基于 HTTP 带来的良好兼容性(如无需担心代理或防火墙问题)、以及自动重连等特性,在只需要服务器向客户端推送数据的场景中(如新闻推送、实时行情、状态更新、AI 处理进度流式输出等)成为了更简单、更合适的选择。
-
ChatGPT、Claude 等生成式 AI 需要“打字机”式逐 token 输出,SSE 天然契合:
-
基于 HTTP/1.1 无需升级协议,CDN 缓存友好;
-
浏览器 EventSource API 一行代码即可接入;
-
文本流可直接承载 JSON Lines 或 markdown 片段。
-
2022 年底起,OpenAI、Anthropic、Google Bard 均把 text/event-stream 作为官方流式回答协议,社区库(FastAPI SSE-Star、Spring WebFlux、Node sse.js、Go gin-sse)迎来二次繁荣。
1.4、SSE 的主要特点
特性 | 说明 |
---|---|
单向通信 | 仅支持服务器向客户端发送数据 |
基于 HTTP | 无需升级协议或使用额外端口,兼容现有网络设施 |
自动重连 | 浏览器在连接断开后可自动重新建立连接 |
轻量易用 | 浏览器原生支持,API 简洁易懂 |
文本流支持 | 默认支持 UTF-8 文本,二进制数据需编码后传输 |
SSE和WebSocket 都能建立浏览器与服务器的长期通信,但区别很明显:
- SSE 是单向推送 不是双向推送, 而且是http协议的一个扩展协议, 使用简单、自动重连,适合文本类实时推送。
- WebSocket 是双向通信,不是 http协议的一个扩展协议,WebSocket 更灵活,但实现相对复杂。
流程解读:
1、连接初始化:客户端使用特定的 Content-Type: text/event-stream
向服务器发起一个普通的 HTTP GET 请求。服务器确认并保持连接开放。
2、数据推送:服务器通过保持打开的连接,以纯文本格式(遵循 data: ...
、event: ...
等规范)持续发送数据块。每个消息以两个换行符 \n\n
结束。
3、连接容错:如果连接因网络问题中断,SSE 客户端内置的机制会自动尝试重新建立连接,极大地提高了应用的鲁棒性。
4、客户端处理:浏览器端的 EventSource
API 会解析收到的数据流,触发相应的事件(如 onmessage
或自定义事件),让开发者能够处理推送来的数据
SSE 的诞生是 Web 开发对简单、高效、标准化的服务器推送技术需求的直接结果。它有效地替代了笨拙的轮询技术,在与 WebSocket 的竞争中,找到了自身在单向数据流场景下的独特定位。
其发展历程经历了从概念提出、浏览器支持到成为正式标准的完整路径。尽管曾受限于 IE,但在现代浏览器中已成为一项稳定、可靠且被广泛采用的技术。如今,在实时通知、金融仪表盘、实时日志跟踪和大型语言模型(LLM)的流式响应输出等场景中,SSE 都是首选的解决方案。
二:SSE 出来20年才一夜 爆火 ,为什么?
SSE 最近站到聚光灯下,几乎可以说最大的推手就是当前 AI 应用(尤其是 ChatGPT 等大型语言模型)的爆发式增长。
SSE 之所以成为 AI 应用的“标配”,是因为 SSE 与 AI 所需的“打字机” 输出模式 是 天作之合。
2.1 什么是“打字机” 式逐 token 输出?
“打字机”式 逐 token 输出是一种流式传输方式,它模拟了人类打字或思考的过程。
服务器不是等待 LLM 生成整个答案 后一次性发送给 用户,而是 流式输出, 每生成一个“词元”(token,可以粗略理解为一个词或一个字),就立刻发送这个“词元”。
下面举一个例子,对比 一下 传统方式(非流式)和 “打字机” (流式)式 的过程。
传统方式(非流式)过程如下:
1、你提问:“请写一首关于春天的诗”。
2、服务器端的 AI 开始思考、生成,整个过程你需要等待(可能好几秒甚至更久)。
3、AI 生成完整的诗歌:“春风拂面绿意浓,百花争艳映晴空...”。
4、服务器将整首诗作为一个完整的 JSON 对象 { "content": "春风拂面绿意浓,百花争艳映晴空..." }
发送给客户端。
5、客户端一次性收到全部内容并渲染出来。
“打字机”(流式)过程如下:
1、你提问:“请写一首关于春天的诗”。
2、服务器端的 AI 生成第一个 token “春”,立刻通过 SSE 发送 data: “春”
。
3、客户端收到“春”并显示出来。
4、AI 生成第二个 token “风”,立刻发送 data: “风”
。
5、客户端在“春”后面追加“风”,形成“春风”。
6、后续 token “拂”、“面”、“绿”、“意”、“浓”... 依次迅速发送和追加。
7、你看到的效果就是文字一个接一个地“打”在屏幕上,就像有人在远端为你实时打字一样。
“打字机”(流式) 模式的巨大优势:
1、极低的感知延迟:用户几乎在提问后瞬间就能看到第一个字开始输出,无需经历漫长的等待白屏期,体验流畅自然。
2、提供了“正在进行”的反馈:看着文字逐个出现,给人一种模型正在为你“思考”和“创作”的生动感,而不是在“沉默中宕机”。
3、更高效地利用时间:用户可以在前半句还在输出时,就开始阅读和理解,节省了总体的认知时间。
2.2 为什么 SSE 是这种模式的“天作之合”?
这正是 SSE 的设计初衷和核心优势所在,它与 AI 流式输出的需求完美匹配:
1、单向通信的完美匹配:
AI 的文本生成过程本质上是服务器到客户端的单向数据推送。
客户端只需要接收,不需要在生成过程中频繁地发送请求。
SSE 的“服务器推送”模型正是为此而生,而 WebSocket 的双向能力在这里是多余的。
2、基于 HTTP/HTTPS,简单且兼容:
SSE 使用标准的 HTTP 协议,这意味着 SSE 易于实现和调试:任何后端框架和前端语言都能轻松处理。在浏览器中调试时,你可以在“网络”选项卡中直接看到以文本流形式传输的事件,非常直观。
SSE 使用标准的 HTTP 协议,这还意味着 容易绕过网络障碍:公司防火墙和代理通常对 HTTP/HTTPS 放行,而可能会阻拦陌生的 WebSocket 协议。这使得 SSE 的部署兼容性极好。
3、内置的自动重连机制:
网络连接并不完全可靠。
如果用户在接收很长的回答时网络波动,连接中断,SSE 客户端会自动尝试重新连接。
这对于长时间流的应用至关重要,提供了天然的鲁棒性。
4、轻量级的文本协议:
AI 流式输出传输的就是文本(UTF-8编码)。SSE 的协议 data: ...\n\n
就是为传输文本片段而设计的,极其高效和简单。
WebSocket 虽然也能传文本,但其协议设计还考虑了二进制帧、掩码等更复杂的情况,对于纯文本流来说显得有些“重”。
5、原生浏览器 API:
现代浏览器都原生支持 EventSource
API,开发者无需引入额外的第三方库,即可轻松实现接收流式数据,减少了依赖和打包体积。
所以,SSE 站到聚光灯下的原因正是:
AI 应用需要“打字机”式的逐 token 输出体验,而 SSE 作为一种基于 HTTP 的、简单的、单向的服务器推送技术,是实现这种体验最自然、最高效、最可靠的技术选择。
它就像是为这个场景量身定做的工具,没有多余的功能,只有恰到好处的设计。
因此,当 ChatGPT 等应用席卷全球时,其背后默默无闻的 SSE 技术也终于从幕后走到了台前,被广大开发者所重新认识和重视。
三、SSE 的工作原理
3.1 工作机制的流程图
SSE 通过一个持久的 HTTP 连接实现服务器到客户端的单向数据流。
以下是其工作机制的流程图:
关键步骤解析:
1、浏览器发起一个 HTTP 请求,Header 中包含:
Accept: text/event-stream
2、服务器响应类型必须为:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
3、服务器发送事件格式(每个事件以两个换行符结束):
event: message
data: {"time": "2023-10-05T12:00:00", "value": "New update!"}
id: 12345
retry: 5000
\n\n
4、浏览器通过 EventSource
API 接收并处理事件。
5、服务器发送 一个特殊“结束”事件,可以结束传输。
比如,服务器发送一个如 event: end
的消息,可以结束传输。
客户端预先监听这个自定义的 end
事件,一旦收到,就知道传输结束,并可以选择主动关闭 EventSource 连接。
6、若连接中断,浏览器会根据 retry
字段自动重连。
如果没有收到 特殊“结束”事件, 浏览器 可以自动重连。
3.2、SSE 与其他通信方式对比
不同通信技术各有适用场景,我们用表格清晰对比:
技术 | 通信方向 | 基于协议 | 复杂度 | 适用场景 | 浏览器原生支持 |
---|---|---|---|---|---|
轮询(Polling) | 客户端→服务器 | HTTP | 简单 | 数据更新频率低(如定时查邮件) | 全部 |
长轮询(Long Polling) | 客户端→服务器 | HTTP | 中等 | 低频但需实时(如即时消息提醒) | 全部 |
SSE | 服务器→客户端 | HTTP | 简单 | 持续单向推送(如实时日志、股价、chartGPT) | (除IE) |
WebSocket | 双向 | 自定义协议 | 复杂 | 互动性强(如在线游戏、视频聊天) | 全部 |
简单说:
- 只需服务器"说话"选SSE
- 需要双方"对话"选WebSocket
- 偶尔查一次数据选轮询/长轮询
3.3 SSE 的适用场景
1、ChatGPT 式逐字输出( “打字机” 式逐 词元 token输出)
2、实时通知系统
- 新订单提醒
- 用户消息推送
- 审核状态更新
3、实时数据看板
- 股票行情
- 设备监控数据
- 实时日志流
四、sse 客户端 API 详解
SSE的客户端实现非常简单,浏览器原生提供了EventSource
对象来处理与服务器的SSE连接。下面我们详细介绍它的使用方法和核心特性。
4.1、认识 浏览器 EventSource对象
浏览器兼容性检测
在使用SSE前,首先需要确认当前浏览器是否支持EventSource
(除IE/Edge外,几乎所有现代浏览器都支持)。检测方法如下:
// 检查浏览器是否支持SSE
if ('EventSource' in window) {
// 支持SSE,可正常使用
console.log('浏览器支持SSE');
} else {
// 不支持SSE,需降级处理
console.log('浏览器不支持SSE');
}
创建连接
使用EventSource
创建与服务器的连接非常简单,只需传入服务器的SSE接口地址:
// 建立与服务器的SSE连接
// url为服务器提供的SSE接口地址(可同域或跨域)
var source = new EventSource(url);
如果需要跨域请求并携带Cookie,可通过第二个参数配置:
// 跨域请求时,允许携带Cookie
var source = new EventSource(url, {
withCredentials: true // 默认为false,设为true表示跨域请求携带Cookie
});
连接状态(readyState)
EventSource
实例的readyState
属性用于表示当前连接状态,只读且有三个可能值:
值 | 常量对应 | 含义说明 |
---|---|---|
0 | EventSource.CONNECTING | 连接未建立,或断线后正在重连 |
1 | EventSource.OPEN | 连接已建立,可正常接收服务器推送的数据 |
2 | EventSource.CLOSED | 连接已关闭,且不会自动重连 |
可以通过该属性判断当前连接状态,例如:
if (source.readyState === EventSource.OPEN) {
console.log('SSE连接已正常建立');
}
4.2、基本使用方法
EventSource
通过事件机制处理连接过程中的各种状态和接收的数据,核心事件包括open
、message
、error
。
下面用流程图展示SSE客户端的完整使用流程:
连接建立:open事件
当客户端与服务器成功建立SSE连接时,会触发open
事件:
// 方式1:使用onopen属性
source.onopen = function (event) {
console.log('SSE连接已建立');
// 可在此处做连接成功后的初始化操作,如更新UI状态
};
// 方式2:使用addEventListener(推荐,可添加多个回调)
source.addEventListener('open', function (event) {
console.log('SSE连接已建立(监听方式)');
}, false);
接收数据:message事件
当客户端收到服务器推送的数据时,会触发message
事件(默认事件,处理未指定类型的消息):
// 方式1:使用onmessage属性
source.onmessage = function (event) {
// event.data为服务器推送的文本数据
var data = event.data;
console.log('收到数据:', data);
// 可在此处处理数据,如更新页面内容
};
// 方式2:使用addEventListener
source.addEventListener('message', function (event) {
var data = event.data;
console.log('收到数据(监听方式):', data);
}, false);
注意:
event.data
始终是字符串类型,如果服务器发送的是JSON数据,需要用JSON.parse(data)
转换。
连接错误:error事件
当连接发生错误(如网络中断、服务器出错)时,会触发error
事件:
// 方式1:使用onerror属性
source.onerror = function (event) {
// 可根据readyState判断错误类型
if (source.readyState === EventSource.CONNECTING) {
console.log('连接出错,正在尝试重连...');
} else {
console.log('连接已关闭,无法重连');
}
};
// 方式2:使用addEventListener
source.addEventListener('error', function (event) {
// 错误处理逻辑
}, false);
关闭连接:close()方法
如果需要主动关闭SSE连接(关闭后不会自动重连),可调用close()
方法:
// 主动关闭SSE连接
source.close();
console.log('SSE连接已手动关闭');
4.3、自定义事件
默认情况下,服务器推送的消息会触发message
事件。
但实际开发中,我们可能需要区分不同类型的消息(如"新订单通知"和"系统公告"),这时就可以使用自定义事件。
客户端通过addEventListener
监听自定义事件名,例如监听order
事件:
// 监听名为"order"的自定义事件
source.addEventListener('order', function (event) {
var orderData = event.data;
console.log('收到新订单:', orderData);
// 处理订单相关逻辑
}, false);
// 再监听一个名为"notice"的自定义事件
source.addEventListener('notice', function (event) {
var noticeData = event.data;
console.log('收到系统公告:', noticeData);
// 处理公告相关逻辑
}, false);
注意:自定义事件不会触发
message
事件,只会被对应的addEventListener
捕获。上面代码中,浏览器对 SSE 的
foo``notice
事件进行监听。如何实现服务器发送foo``notice
事件,请看下文。
五、SSE服务器实现:数据格式与规则
服务器要实现SSE,核心是按照特定格式向客户端发送数据。
下面详细介绍服务器端的实现规范。
5.1 HTTP 头信息要求
服务器向客户端发送SSE数据时,必须设置以下HTTP响应头,否则客户端无法正确识别为事件流:
Content-Type: text/event-stream // 必须,指定为事件流类型
Cache-Control: no-cache // 必须,禁止缓存,确保数据实时性
Connection: keep-alive // 必须,保持长连接
这三个头信息是SSE的基础,缺少任何一个都可能导致连接失败或数据异常。
5.2 数据传输格式
服务器发送的每条消息(message)由多行组成,每行格式为[字段]: 值\n
(字段名后必须跟冒号和空格,结尾用换行符\n
)。
多条消息之间用\n\n
(两个换行符)分隔。
此外,以:
开头的行是注释(服务器可定期发送注释保持连接)。
基本格式示例
: 这是一条注释(客户端会忽略)\n
data: 这是第一条消息\n\n
data: 这是第二条消息的第一行\n
data: 这是第二条消息的第二行\n\n
注意:换行符必须是
\n
(Unix格式),\r\n
可能导致客户端解析错误。
5.3 核心字段说明
SSE消息支持四个核心字段,分别用于不同场景:
1. data字段:消息内容
data
字段用于携带实际的消息内容,是最常用的字段。
-
单行数据:
data: Hello, SSE!\n\n // 单行数据,以\n\n结束
-
多行数据(适合JSON等复杂结构):
data: {\n // 第一行以\n结束 data: "name": "张三",\n // 第二行以\n结束 data: "age": 20\n // 第三行以\n结束 data: }\n\n // 最后一行以\n\n结束
客户端接收后,
event.data
会自动拼接为完整字符串:{"name": "张三","age": 20}
2. event字段:指定事件类型
event
字段用于指定消息的事件类型,客户端可通过对应事件名监听(即3.3节的自定义事件)。
服务器发送:
event: order\n // 指定事件类型为order
data: 新订单ID:12345\n // 消息内容
\n // 消息结束(\n\n简化为单独一行)
客户端监听:
source.addEventListener('order', function(event) {
console.log(event.data); // 输出:新订单ID:12345
});
3. id字段:消息标识
id
字段用于给消息设置唯一标识,客户端会自动记录最后一条消息的id
(存于source.lastEventId
)。
核心作用:当连接断线重连时,客户端会在请求头中携带Last-Event-ID: [最后收到的id]
,服务器可根据该ID恢复数据传输(避免重复或丢失)。
服务器发送:
id: msg1001\n // 消息标识
data: 这是第1001条消息\n
\n
客户端重连时的请求头:
Last-Event-ID: msg1001 // 自动携带最后收到的id
4. retry字段:重连间隔
retry
字段用于指定客户端断线后的重连间隔(单位:毫秒),默认重连间隔约为3秒。
服务器发送:
retry: 5000\n // 告诉客户端,断线后5秒再重连
data: 重连间隔已设置为5秒\n
\n
5. 服务器保持连接示例
服务器可以定期发送注释行,保持连接活跃:
: 这是保持连接活动的注释行\n
: 服务器时间 2023-10-05T12:00:00\n
5.4 服务器发送流程
服务器发送SSE数据的完整流程如下:
下面是一个包含多种字段的服务器发送示例,模拟一个实时通知系统:
: 服务器开始发送消息(注释)\n
id: 1001\n
event: notice\n
data: 系统将在10分钟后维护\n\n
id: 1002\n
event: order\n
data: {"orderId": "20230501", "status": "paid"}\n\n
retry: 10000\n
id: 1003\n
data: 重连间隔已调整为10秒\n\n
: 这是保持连接活动的注释行\n
: 服务器时间 2023-10-05T12:00:00\n
客户端接收后:
notice
事件会捕获到"系统将在10分钟后维护"order
事件会捕获到订单JSON数据- 重连间隔被设置为10秒
- 最后收到的消息ID是1003(断线重连时会携带)
通过以上规范,服务器就能轻松实现SSE功能,向客户端实时推送数据。
相比WebSocket,SSE的服务器实现更简单,无需处理复杂的协议握手,只需按格式发送文本数据即可。
六、SSE实战案例:用Spring Boot搭建实时通信系统
接下来, 通过一个完整案例 手把手教你用Spring Boot实现SSE功能。
这个案例包含服务端(后端)和客户端(前端)代码, 可以直接运行体验服务器主动推送数据的效果。
6.1、案例整体架构
我们要实现的系统包含三个核心部分:
- 后端服务:基于Spring Boot,提供SSE连接接口、消息广播接口和任务进度推送接口
- 前端页面:一个简单的HTML页面,通过
EventSource
与后端建立SSE连接 - 交互流程:客户端连接后,可接收服务器主动推送的连接状态、广播消息和任务进度
整体架构流程图:
6.2、服务端实现
6.2.1 准备依赖
首先创建Spring Boot项目,在pom.xml
中添加以下依赖(用于web开发和页面渲染):
<dependencies>
<!-- Spring Web:提供SSE相关类和HTTP服务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf:用于渲染前端页面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
这些依赖是基础:spring-boot-starter-web
提供了SSE核心类SseEmitter
,spring-boot-starter-thymeleaf
用于将HTML页面返回给浏览器。
6.2.2 编写SSE核心控制器
创建SseController
,这是服务端处理SSE连接和消息推送的核心类:
package com.example.sse.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class SseController {
// 存储所有活跃的SSE连接(线程安全的列表)
// CopyOnWriteArrayList适合读多写少场景,避免并发问题
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
// 线程池:用于异步发送事件,避免阻塞主线程
private final ExecutorService executor = Executors.newCachedThreadPool();
/**
* 客户端订阅SSE的接口
* 客户端通过访问该接口建立长连接,接收服务器推送的事件
*/
@GetMapping(value = "/sse/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
// 创建SseEmitter实例,设置超时时间为无限(默认30秒会超时,这里设为Long.MAX_VALUE避免自动断开)
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
// 将新连接加入活跃列表(后续推送消息时会遍历这个列表)
emitters.add(emitter);
// 设置连接完成/超时的回调:从活跃列表中移除该连接,释放资源
emitter.onCompletion(() -> emitters.remove(emitter)); // 连接正常关闭时
emitter.onTimeout(() -> emitters.remove(emitter)); // 连接超时关闭时
// 发送初始连接成功消息(给客户端的"欢迎消息")
try {
emitter.send(SseEmitter.event()
.name("CONNECTED") // 事件名称:客户端可通过"CONNECTED"事件监听
.data("You are successfully connected to SSE server!") // 消息内容
.reconnectTime(5000)); // 告诉客户端:如果断开连接,5秒后重连
} catch (IOException e) {
// 发送失败时,标记连接异常结束
emitter.completeWithError(e);
}
return emitter; // 将emitter返回给客户端,保持连接
}
/**
* 广播消息接口:向所有已连接的客户端推送消息
* 可通过浏览器访问 http://localhost:8080/sse/broadcast?message=xxx 触发
*/
@GetMapping("/sse/broadcast")
public String broadcastMessage(@RequestParam String message) {
// 用线程池异步执行广播,避免阻塞当前请求
executor.execute(() -> {
// 遍历所有活跃连接,逐个发送消息
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("BROADCAST") // 事件名称:客户端监听"BROADCAST"事件
.data(message) // 广播的消息内容
.id(String.valueOf(System.currentTimeMillis()))); // 消息ID(用于重连时定位)
} catch (IOException e) {
// 发送失败(可能客户端已断开),从列表中移除并标记连接结束
emitters.remove(emitter);
emitter.completeWithError(e);
}
}
});
return "Broadcast message: " + message; // 给调用者的响应
}
/**
* 模拟长时间任务:向客户端推送实时进度
* 适合文件上传、数据处理等需要实时反馈进度的场景
*/
@GetMapping("/sse/start-task")
public String startTask() {
// 异步执行任务,避免阻塞当前请求
executor.execute(() -> {
try {
// 模拟任务进度:从0%到100%,每次增加10%
for (int i = 0; i <= 100; i += 10) {
Thread.sleep(1000); // 休眠1秒,模拟处理耗时
// 向所有客户端推送当前进度
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("PROGRESS") // 事件名称:客户端监听"PROGRESS"事件
.data(i + "% completed") // 进度数据
.id("task-progress")); // 固定ID,标识这是任务进度消息
} catch (IOException e) {
// 发送失败,移除连接
emitters.remove(emitter);
}
}
// 任务完成时,发送结束消息
if (i == 100) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("COMPLETE") // 事件名称:客户端监听"COMPLETE"事件
.data("Task completed successfully!"));
} catch (IOException e) {
emitters.remove(emitter);
}
}
}
}
} catch (InterruptedException e) {
// 任务被中断时,恢复线程中断状态并退出
Thread.currentThread().interrupt();
break;
}
});
return "Task started!"; // 告诉调用者任务已启动
}
}
核心代码说明:
SseEmitter
:Spring提供的SSE核心类,每个实例对应一个客户端连接emitters
列表:管理所有活跃连接,方便广播消息(类似"客户端注册表")executor
线程池:异步处理消息发送,避免阻塞主线程(如果同步发送,一个客户端卡住会影响所有用户)- 事件发送:通过
emitter.send(SseEmitter.event())
构建消息,可指定事件名、数据、ID和重连时间
6.2.3 编写页面控制器
创建PageController
,用于将前端页面返回给浏览器:
package com.example.sse.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller // 注意这里用@Controller而非@RestController,用于返回页面
public class PageController {
/**
* 访问根路径时,返回SSE客户端页面
*/
@GetMapping("/")
public String index() {
// 返回src/main/resources/templates目录下的sse-client.html
return "sse-client";
}
}
6.3、客户端实现(HTML页面)
在src/main/resources/templates
目录下创建sse-client.html
,这是用户交互的前端页面:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>SSE Client Example</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.message { padding: 10px; margin: 5px 0; border-left: 3px solid #ccc; }
.broadcast { border-color: #4CAF50; background-color: #E8F5E9; } /* 广播消息:绿色边框 */
.progress { border-color: #2196F3; background-color: #E3F2FD; } /* 进度消息:蓝色边框 */
.complete { border-color: #FF9800; background-color: #FFF3E0; } /* 完成消息:橙色边框 */
.error { border-color: #F44336; background-color: #FFEBEE; } /* 错误消息:红色边框 */
button { padding: 10px 15px; margin: 5px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>Server-Sent Events (SSE) Client</h1>
<!-- 操作按钮区 -->
<div>
<button onclick="connectSSE()">Connect to SSE</button> <!-- 建立SSE连接 -->
<button onclick="disconnectSSE()">Disconnect</button> <!-- 断开连接 -->
<button onclick="broadcastMessage()">Send Broadcast</button> <!-- 发送广播 -->
<button onclick="startTask()">Start Task</button> <!-- 启动模拟任务 -->
</div>
<!-- 消息展示区 -->
<div>
<h2>Messages:</h2>
<div id="messages"></div> <!-- 动态显示服务器推送的消息 -->
</div>
</div>
<script>
let eventSource = null; // 用于存储SSE连接实例(全局变量)
/**
* 建立SSE连接
*/
function connectSSE() {
// 避免重复连接
if (eventSource) {
addMessage("Already connected to SSE", "error");
return;
}
// 创建EventSource实例,连接服务端的SSE接口
eventSource = new EventSource('/sse/subscribe');
// 连接成功建立时触发
eventSource.onopen = function(event) {
addMessage("Connection established", "complete");
};
// 监听未指定事件名的消息(对应服务端未设置name的事件)
eventSource.onmessage = function(event) {
addMessage("Message: " + event.data, "message");
};
// 监听服务端发送的"CONNECTED"事件(连接成功通知)
eventSource.addEventListener("CONNECTED", function(event) {
addMessage("Connected: " + event.data, "complete");
});
// 监听服务端发送的"BROADCAST"事件(广播消息)
eventSource.addEventListener("BROADCAST", function(event) {
addMessage("Broadcast: " + event.data, "broadcast");
});
// 监听服务端发送的"PROGRESS"事件(任务进度)
eventSource.addEventListener("PROGRESS", function(event) {
addMessage("Progress: " + event.data, "progress");
});
// 监听服务端发送的"COMPLETE"事件(任务完成)
eventSource.addEventListener("COMPLETE", function(event) {
addMessage("Complete: " + event.data, "complete");
});
// 连接出错时触发(如网络中断)
eventSource.onerror = function(event) {
addMessage("Error occurred", "error");
// 注意:SSE会自动重连,无需手动处理
};
}
/**
* 断开SSE连接
*/
function disconnectSSE() {
if (eventSource) {
eventSource.close(); // 关闭连接
eventSource = null;
addMessage("Disconnected from SSE", "complete");
} else {
addMessage("Not connected to SSE", "error");
}
}
/**
* 发送广播消息(调用服务端的广播接口)
*/
function broadcastMessage() {
const message = prompt("Enter message to broadcast:"); // 弹出输入框
if (message) {
// 调用后端广播接口
fetch('/sse/broadcast?message=' + encodeURIComponent(message))
.then(response => response.text())
.then(data => addMessage("Server: " + data, "message"))
.catch(error => addMessage("Error: " + error, "error"));
}
}
/**
* 启动模拟任务(调用服务端的任务接口)
*/
function startTask() {
fetch('/sse/start-task')
.then(response => response.text())
.then(data => addMessage("Server: " + data, "message"))
.catch(error => addMessage("Error: " + error, "error"));
}
/**
* 在页面上添加消息(辅助函数)
*/
function addMessage(text, className) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'message ' + className; // 添加样式类
// 显示时间和消息内容
messageDiv.textContent = new Date().toLocaleTimeString() + ' - ' + text;
messagesDiv.appendChild(messageDiv);
// 自动滚动到最新消息
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>
客户端核心逻辑:
eventSource
:EventSource
实例,是客户端与服务端SSE连接的"桥梁"- 事件监听:通过
addEventListener
监听服务端定义的事件(CONNECTED
/BROADCAST
等) - 自动重连:当连接断开时,
EventSource
会自动重试(无需手动写重连逻辑) - 交互函数:
connectSSE
/disconnectSSE
等函数对应页面按钮,实现用户操作
6.4、运行与测试
6.4.1 启动步骤
1、 确保Spring Boot项目配置正确(默认端口8080,无需额外配置)
2、 启动Spring Boot应用(运行带有main
方法的启动类)
3、 打开浏览器,访问 http://localhost:8080/
,看到客户端页面
6.4.2 功能测试
1、建立连接:点击"Connect to SSE"按钮,页面会显示"连接成功"的消息(服务端通过CONNECTED
事件推送)
2、发送广播:点击"Send Broadcast"按钮,输入任意消息(如"Hello SSE"),页面会显示广播消息(服务端向所有连接的客户端推送)
3、启动任务:点击"Start Task"按钮,页面会每秒收到一条进度消息(从0%到100%),最后显示"任务完成"
4、断开连接:点击"Disconnect"按钮,连接关闭,不再接收消息
4.3 测试流程图
6.5、 服务端 关键技术点
1、SseEmitter的作用:Spring封装的SSE工具类,简化了"保持连接+发送事件"的实现,无需手动处理HTTP流格式。
2、连接管理:用CopyOnWriteArrayList
存储活跃连接,确保线程安全;通过onCompletion
/onTimeout
回调清理无效连接,避免内存泄漏。
3、异步处理:必须用线程池(ExecutorService
)异步发送消息,否则会阻塞主线程,导致新请求无法处理。
4、事件设计:通过name
区分不同类型的事件(如PROGRESS
/BROADCAST
),客户端按需监听,逻辑更清晰。
5、自动重连:SSE客户端(EventSource
)内置重连机制,网络恢复后会自动重新连接,无需额外代码。
通过这个案例,你可以清晰看到SSE的优势:实现简单(几行代码就能建立实时连接)、无需额外协议(基于HTTP)、自带重连机制。如果你的场景只需要服务器单向推送数据(如实时通知、进度更新),SSE会是比WebSocket更轻量的选择。
七:SSE与WEBSocket 如何选型?
由于平台篇幅限制, 剩下的内容,请参参见原文地址