1. SSE 是什么
SSE,全称 Server-Sent Events,是浏览器和服务端之间的一种服务端单向推送方案。
它的特点是:
- 前端通过
EventSource发起一个 HTTP 请求。 - 后端返回
text/event-stream类型的响应。 - 后端不立即结束响应,而是持续向这个响应流中写入数据。
- 浏览器持续接收服务端推送的事件。
- 通信方向主要是服务端到前端,前端不能通过同一条 SSE 连接向后端发送消息。
前端示例:
ts
const source = new EventSource('/api/sse')
source.onopen = () => {
console.log('SSE connected')
}
source.onmessage = (event) => {
console.log('message:', event.data)
}
source.onerror = () => {
console.log('SSE error')
}
后端响应头通常需要包含:
http
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
SSE 消息格式示例:
txt
event: message
data: {"content":"hello"}
注意:一条 SSE 消息必须以空行结束。也就是 data 行后面需要有两个换行,浏览器才会认为这条事件结束并触发对应回调。
2. SSE 和 WebSocket 的区别
SSE 和 WebSocket 都可以用于实时通信,但适用场景不同。
SSE 更适合:
- 服务端单向推送。
- AI 流式输出。
- 任务进度推送。
- 后台通知。
- 报表生成进度。
- 导入导出进度。
- 系统公告。
- 低频实时状态变更。
WebSocket 更适合:
- 高频双向通信。
- 聊天室。
- 游戏实时同步。
- 协同编辑。
- 客户端和服务端都需要频繁主动发送消息的场景。
- 二进制数据传输。
简单判断标准:
- 如果主要是后端向前端推消息,优先考虑 SSE。
- 如果前后端都需要高频互发消息,优先考虑 WebSocket。
3. SSE 心跳是什么意思
后端每隔 15-30s 发一次心跳,意思是:后端通过同一条 SSE 连接,定时向前端发送一条没有业务含义的小消息。
心跳不是额外建立一个接口,也不是前端轮询,而是后端继续往当前 SSE 响应流里写数据。
推荐心跳格式:
txt
event: heartbeat
data: {"time":1784270000000}
前端监听:
ts
source.addEventListener('heartbeat', () => {
lastActiveTime = Date.now()
})
心跳的作用有两个:
- 告诉浏览器、Nginx、网关、负载均衡器这条连接还活着。
- 让前端知道连接最近还有数据到达,可以判断连接是否假死。
很多业务场景下,服务端可能很久没有真实业务消息。如果 SSE 连接长时间没有任何数据,中间代理可能会认为连接空闲太久,然后主动断开。心跳就是为了避免这种空闲连接被中间层切断。
4. 注释心跳和事件心跳
SSE 有两种常见心跳写法。
第一种是注释心跳:
txt
: ping
这种格式是合法 SSE 注释。它可以让连接产生数据流动,起到保活作用,但通常不会触发前端的 onmessage,也不方便前端更新最后活跃时间。
第二种是事件心跳:
txt
event: heartbeat
data: {}
这种方式前端可以通过 addEventListener('heartbeat') 监听到,适合用来更新 lastActiveTime。
如果前端需要做心跳超时检测,推荐使用事件心跳。
5. 后端重启、报错、崩溃时前端能知道什么
如果后端正常运行,前端可以持续收到业务消息或心跳。
如果后端重启、进程挂掉、容器退出、机器断开,原来的 SSE 连接会中断,前端不会再收到心跳,也不会再收到业务消息。
此时前端通常能感知到连接异常:
ts
source.onerror = () => {
console.log('SSE connection error')
}
但需要注意:onerror 只能说明 SSE 连接异常,不能精确说明异常原因。
onerror 可能由以下原因触发:
- 后端服务重启。
- 后端进程崩溃。
- SSE 接口返回
500、502、503。 - Nginx 或网关断开连接。
- 用户网络断开。
- 浏览器到服务器之间的链路超时。
- 代理层空闲超时。
所以前端可以知道"连接异常了",但不能只靠 onerror 百分百判断"后端重启了"。
6. 常见异常场景分析
6.1 服务端正常,但长时间没有业务数据
这种情况下 SSE 连接可能仍然是活的,前端不会触发 onerror。
如果后端没有心跳,中间代理可能因为长时间没有数据而断开连接。
解决方案:
- 后端定时发送
heartbeat。 - 前端收到心跳后更新最后活跃时间。
- 前端超过一段时间没有收到任何数据时主动重建连接。
6.2 后端业务报错,但 SSE 连接没有断
如果后端只是某个任务执行失败,但 SSE 连接还在,前端不会自动知道这是业务错误。
解决方案:后端通过自定义事件通知前端。
txt
event: app-error
data: {"code":"TASK_FAILED","message":"任务执行失败"}
前端监听:
ts
source.addEventListener('app-error', (event) => {
console.warn('app error:', event.data)
})
6.3 后端优雅重启
如果后端能捕获关闭信号,并且还有机会向客户端发送消息,可以在关闭前发送服务重启事件。
txt
event: server-restarting
data: {"message":"服务即将重启"}
前端监听:
ts
source.addEventListener('server-restarting', () => {
console.warn('server restarting')
})
这种情况下前端可以展示"服务重启中,正在重连"。
6.4 后端突然崩溃或被强制 kill
如果后端进程直接崩溃、容器被强制杀掉、机器断电,后端来不及发送任何 SSE 事件。
前端只能通过以下方式间接感知:
onerror触发。- 长时间没有收到心跳。
- 重连失败。
- 后续重连成功。
6.5 网络抖动或代理断开
网络异常和后端异常对前端来说表现可能很像,都是连接断开或长时间无数据。
解决方案不是精确区分所有原因,而是建立统一的恢复机制:
- 检测连接异常。
- 标记状态为重连中。
- 使用退避策略重连。
- 重连成功后恢复业务。
7. 业界推荐整体方案
可靠的 SSE 方案通常由以下部分组成:
- 后端定时发送心跳。
- 前端维护最后活跃时间。
- 前端做心跳超时检测。
- 前端监听
onerror。 - 前端使用退避重连。
- 后端通过自定义事件传递业务错误。
- 后端优雅重启前发送重启事件。
- 重要消息带
id。 - 后端支持
Last-Event-ID补发。 - Nginx 或网关关闭响应缓冲。
- 页面销毁时前端主动关闭 SSE。
一句话概括:
txt
心跳解决连接是否还活着,onerror 解决连接是否断了,退避重连解决如何恢复,Last-Event-ID 解决消息是否会丢。
8. 前端实现策略
前端需要维护几个状态:
- 当前
EventSource实例。 - 当前连接状态。
- 最后一次收到数据的时间。
- 当前重连次数。
- 心跳检查定时器。
- 重连定时器。
连接状态可以定义为:
ts
type SseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed' | 'error'
核心示例:
ts
type SseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed' | 'error'
let source: EventSource | null = null
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let status: SseStatus = 'closed'
let retryCount = 0
let lastActiveTime = 0
const HEARTBEAT_TIMEOUT = 60 * 1000
const CHECK_INTERVAL = 10 * 1000
function getReconnectDelay() {
const delays = [1000, 2000, 5000, 10000, 30000]
return delays[Math.min(retryCount, delays.length - 1)]
}
function connectSse(url: string) {
closeSse()
status = retryCount === 0 ? 'connecting' : 'reconnecting'
lastActiveTime = Date.now()
source = new EventSource(url)
source.onopen = () => {
status = 'open'
retryCount = 0
lastActiveTime = Date.now()
}
source.onmessage = (event) => {
lastActiveTime = Date.now()
console.log('message:', event.data)
}
source.addEventListener('heartbeat', () => {
lastActiveTime = Date.now()
})
source.addEventListener('app-error', (event) => {
lastActiveTime = Date.now()
console.warn('app error:', event.data)
})
source.addEventListener('server-restarting', (event) => {
lastActiveTime = Date.now()
console.warn('server restarting:', event.data)
})
source.onerror = () => {
status = 'error'
reconnect(url)
}
heartbeatTimer = setInterval(() => {
if (!source) return
const idleTime = Date.now() - lastActiveTime
if (idleTime > HEARTBEAT_TIMEOUT) {
console.warn('SSE heartbeat timeout, reconnecting')
reconnect(url)
}
}, CHECK_INTERVAL)
}
function reconnect(url: string) {
source?.close()
source = null
if (reconnectTimer) return
status = 'reconnecting'
const delay = getReconnectDelay()
retryCount += 1
reconnectTimer = setTimeout(() => {
reconnectTimer = null
connectSse(url)
}, delay)
}
function closeSse() {
source?.close()
source = null
status = 'closed'
if (heartbeatTimer) {
clearInterval(heartbeatTimer)
heartbeatTimer = null
}
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
}
如果后端心跳间隔是 20s,前端超时时间建议设为 60s 或 90s。不要把超时时间设得太短,否则普通网络抖动也会导致频繁重连。
9. 原生 EventSource 的自动重连
原生 EventSource 在连接异常时会自动重连,这是浏览器内置行为。
但实际项目中仍然常常需要手动控制重连,原因包括:
- 需要展示连接状态。
- 需要处理心跳超时导致的假死连接。
- 需要控制重连间隔,避免高频请求。
- 需要在页面销毁时彻底关闭连接。
- 需要在鉴权失效时停止重连并引导用户重新登录。
因此,推荐做法是把 SSE 封装成一个统一的客户端模块或 Vue hook,在其中集中处理连接、心跳、异常、重连、关闭。
10. 退避重连策略
后端异常时,前端不能无限制高频重连,否则会在服务恢复前给后端和网关造成额外压力。
推荐使用退避策略:
ts
const delays = [1000, 2000, 5000, 10000, 30000]
含义:
- 第 1 次失败后,等待
1s重连。 - 第 2 次失败后,等待
2s重连。 - 第 3 次失败后,等待
5s重连。 - 第 4 次失败后,等待
10s重连。 - 后续最多等待
30s。
重连成功后,应将失败次数清零。
如果系统用户量很大,可以在退避时间上增加随机抖动,避免大量客户端同时重连。
示例:
ts
function getReconnectDelay() {
const delays = [1000, 2000, 5000, 10000, 30000]
const baseDelay = delays[Math.min(retryCount, delays.length - 1)]
const jitter = Math.floor(Math.random() * 1000)
return baseDelay + jitter
}
11. Last-Event-ID 和消息补偿
如果业务消息非常重要,不能丢消息,后端需要给每条重要消息加 id。
示例:
txt
id: 1024
event: message
data: {"content":"xxx"}
浏览器在 SSE 断开后重新连接时,会自动带上 Last-Event-ID 请求头。后端可以根据这个 ID 判断客户端最后收到哪条消息,然后补发之后遗漏的消息。
这适合:
- 审批流状态。
- 订单状态。
- 任务步骤进度。
- 重要通知。
- 需要保证前端最终一致的消息流。
如果只是 AI 流式输出,是否需要补偿要看业务要求。如果用户刷新页面后可以重新发起任务或重新查询结果,则可以不做严格补偿。
前端也建议做消息去重,避免重连补发导致重复处理。
12. 鉴权方案
原生 EventSource 不能直接设置自定义请求头,例如不能直接加:
http
Authorization: Bearer token
常见鉴权方案:
- 使用 Cookie 鉴权。
- 在 URL 上携带短期 token。
- 使用
fetch读取流,自行实现 SSE 解析。
推荐优先使用 Cookie 鉴权,原因是原生 EventSource 使用简单,浏览器会自动携带同域 Cookie。
如果必须使用 Authorization 请求头,可以考虑不用原生 EventSource,改用 fetch 加 ReadableStream,但实现复杂度会更高,需要自己处理流解析、重连、事件格式。
13. Nginx 和网关配置
SSE 很怕代理层缓冲。如果 Nginx 开启响应缓冲,后端即使已经写入消息,Nginx 也可能先缓存,不会立即推给浏览器。
推荐配置:
nginx
location /api/sse {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
关键配置说明:
proxy_http_version 1.1:使用 HTTP/1.1 长连接。proxy_set_header Connection "":避免代理错误处理连接头。proxy_buffering off:关闭响应缓冲,保证消息实时到达前端。proxy_cache off:关闭缓存。proxy_read_timeout 3600s:延长代理读取超时时间。
如果后端能设置响应头,也可以加:
http
X-Accel-Buffering: no
用于提示 Nginx 不要缓冲该响应。
14. 后端实现注意事项
后端需要注意:
- 设置正确响应头。
- 写入消息后及时 flush。
- 定时发送心跳。
- 客户端断开后清理连接资源。
- 服务关闭前尽量发送
server-restarting。 - 业务异常用
app-error事件通知。 - 重要消息写入消息存储,支持断线补发。
- 控制单用户连接数,避免重复打开多个连接。
SSE 消息格式可以封装成统一函数:
txt
id: 1024
event: message
data: {"content":"hello"}
如果只有 data,事件会进入前端的 source.onmessage。
如果指定了 event,前端需要用 source.addEventListener(eventName, handler) 监听。
15. 前端页面生命周期
在 Vue 页面中使用 SSE 时,需要在页面卸载时关闭连接。
ts
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
connectSse('/api/sse')
})
onUnmounted(() => {
closeSse()
})
如果不关闭,用户离开页面后连接仍可能存在,导致:
- 多个页面重复连接。
- 重复收到消息。
- 内存泄漏。
- 后端连接数增加。
如果 SSE 是全局通知类能力,可以放在全局 store 或应用入口中统一管理,而不是每个页面单独创建。
16. 状态展示建议
前端可以对用户展示以下状态:
connecting:正在连接。open:已连接。reconnecting:连接中断,正在重连。error:连接异常。closed:连接已关闭。
并不一定所有状态都要强提醒用户。推荐策略:
- 短时间重连中可以弱提示。
- 长时间重连失败再明显提示。
- 关键任务场景需要提示"连接中断,正在恢复"。
- 后端明确发送
server-restarting时提示"服务重启中"。
17. 最终推荐标准
后端标准:
- SSE 响应头正确。
- 每
15-30s发送一次heartbeat。 - 业务异常发送
app-error。 - 优雅重启前发送
server-restarting。 - 重要消息携带
id。 - 支持
Last-Event-ID补偿。 - 写入数据后及时 flush。
- 客户端断开后释放资源。
- Nginx 或网关关闭 buffering。
前端标准:
- 监听
onopen、onmessage、onerror。 - 监听
heartbeat。 - 收到任何消息都更新
lastActiveTime。 - 超过
60-90s没有任何消息时主动重建连接。 - 重连使用退避策略。
- 重连成功后清零失败次数。
- 页面销毁时调用
close()。 - 关键业务消息做去重。
- 鉴权失效时停止重连并走登录流程。
18. 总结
SSE 连接可靠性不能只依赖浏览器的 EventSource 自动重连。
成熟方案应该同时处理四件事:
- 用心跳判断连接是否还活着。
- 用
onerror感知连接是否断开。 - 用退避重连恢复连接。
- 用
Last-Event-ID处理关键消息补偿。
心跳不是为了让后端崩溃后还能继续发送消息,而是为了在后端正常时保持连接活跃,并帮助前端识别连接假死。
当后端真正重启、崩溃或断开时,前端无法继续收到原连接上的消息,只能通过 onerror、心跳超时和重连结果判断连接状态,并在服务恢复后重新建立 SSE 连接。