1 概述:什么是 Server-Sent Events (SSE)
Server-Sent Events (SSE) 是一种基于 HTTP 协议的服务器推送技术,允许服务器通过持久化单向连接向客户端实时推送数据。与传统的请求-响应模式不同,SSE 采用**“服务器主动推送”**模式,适用于需要从服务器持续获取更新的场景(如实时通知、股票行情、日志流等)。
核心特点:
- 单向通信:仅支持服务器向客户端推送数据,客户端无法主动发送数据
- 基于 HTTP:无需额外协议(如 WebSocket 的握手过程),兼容现有 HTTP 基础设施
- 自动重连:浏览器内置断线重连机制,默认重试间隔为 3 秒,可通过
retry
字段自定义 - 轻量级协议:数据格式简单(文本流),协议开销低,适合传输频繁的小数据包
与传统轮询的区别:
特性 | 传统轮询 | SSE |
---|---|---|
连接方式 | 短连接(频繁建立/关闭) | 长连接(一次建立,持续复用) |
实时性 | 依赖轮询间隔(延迟高) | 实时推送(延迟低) |
服务器负载 | 高(频繁请求) | 低(单连接复用) |
带宽消耗 | 高(重复请求头) | 低(仅传输数据) |
2 工作原理:SSE 的通信机制
2.1 协议基础
SSE 基于 HTTP/1.1 的分块传输编码(Chunked Transfer Encoding) 实现,服务器通过持续发送“数据块”维持连接。客户端使用 EventSource
API 建立连接并监听事件流。
关键 HTTP 头:
Content-Type: text/event-stream # 标识 SSE 流类型
Cache-Control: no-cache # 禁止缓存,确保实时性
Connection: keep-alive # 维持长连接
Access-Control-Allow-Origin: * # 跨域配置(按需设置)
2.2 数据格式规范
SSE 数据流由事件块组成,每个事件块包含以下可选字段(字段间用换行分隔,事件块以空行结束):
data
:事件数据(必填),支持多行(每行以data:
开头)event
:事件类型(可选,默认触发message
事件)id
:事件 ID(可选,用于断线重连时恢复数据)retry
:重连间隔(可选,单位毫秒,覆盖浏览器默认值)
示例事件流:
id: 1001
event: update
data: {"status": "syncing", "progress": 30}
retry: 5000
data: {"status": "syncing", "progress": 60}
data: {"status": "completed"}
2.3 客户端 EventSource API
浏览器通过 EventSource
对象接收 SSE 流,核心方法和事件:
new EventSource(url)
:建立 SSE 连接onmessage
:接收默认事件(无event
字段的事件)addEventListener(eventType, callback)
:监听自定义事件(如update
)onerror
:连接错误/中断时触发close()
:主动关闭连接
3 使用场景:SSE 适合哪些业务场景
SSE 适用于服务器单向推送、数据更新频繁、实时性要求中等的场景,典型案例包括:
3.1 实时通知系统
如社交媒体消息提醒、邮件送达通知等。服务器在事件发生时立即推送通知,客户端无需轮询。
3.2 实时数据监控
如系统日志流、传感器数据、股票行情等。SSE 可高效传输持续产生的时序数据。
3.3 进度跟踪
如文件上传/下载进度、长时间任务(如视频转码)的进度更新。通过 retry
字段控制更新频率,平衡实时性与服务器负载。
3.4 在线协作工具
如文档协同编辑中的光标位置同步、用户在线状态更新(需配合客户端心跳检测)。
4 Next.js 实现示例:基于 TypeScript 的完整实践
4.1 环境准备
确保项目使用 Next.js 14+ 和 TypeScript,通过以下命令创建项目:
npx create-next-app@latest sse-demo --typescript --app
4.2 服务端实现:创建 SSE 端点
在 App Router 中,创建 app/api/sse/route.ts
文件,实现 SSE 数据流:
// app/api/sse/route.ts
import { NextResponse } from 'next/server'
// 定义 SSE 事件类型(TypeScript 类型安全)
interface SSEEvent {
id?: string
event?: string
data: string
retry?: number
}
// 生成模拟数据(如实时进度更新)
async function* generateEvents() {
let progress = 0
while (progress <= 100) {
// 构造 SSE 事件
const event: SSEEvent = {
id: Date.now().toString(),
event: 'progress',
data: JSON.stringify({
progress,
timestamp: new Date().toISOString()
}),
retry: 3000 // 重连间隔 3 秒
}
// 格式化事件为 SSE 协议格式
let eventStr = ''
if (event.id) eventStr += `id: ${event.id}\n`
if (event.event) eventStr += `event: ${event.event}\n`
eventStr += `data: ${event.data}\n`
if (event.retry) eventStr += `retry: ${event.retry}\n`
eventStr += '\n' // 事件块结束标志
yield eventStr
progress += 10
await new Promise(resolve => setTimeout(resolve, 1000)) // 每秒推送一次
}
// 推送完成事件
yield `event: complete\ndata: {"status": "done"}\n\n`
}
export async function GET() {
// 创建可读流(ReadableStream)
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
for await (const event of generateEvents()) {
// 将事件编码为 Uint8Array 并写入流
controller.enqueue(encoder.encode(event))
}
controller.close() // 数据发送完毕,关闭流
}
})
// 返回 SSE 响应
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform', // 禁止缓存和压缩
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // 禁用反向代理缓冲(如 Nginx)
}
})
}
4.3 客户端实现:接收 SSE 事件
创建客户端组件 app/sse-client.tsx
,使用 EventSource
接收并处理 SSE 流:
// app/sse-client.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
// 定义 SSE 事件数据类型
interface ProgressData {
progress: number
timestamp: string
}
interface CompleteData {
status: string
}
export default function SSEClient() {
const [progress, setProgress] = useState<number>(0)
const [status, setStatus] = useState<string>('disconnected')
const eventSourceRef = useRef<EventSource | null>(null)
const reconnectAttemptsRef = useRef<number>(0)
// 建立 SSE 连接
const connect = () => {
setStatus('connecting')
const es = new EventSource('/api/sse')
eventSourceRef.current = es
// 监听进度事件(自定义事件类型 "progress")
es.addEventListener('progress', (event) => {
const data: ProgressData = JSON.parse(event.data)
setProgress(data.progress)
setStatus('connected')
reconnectAttemptsRef.current = 0 // 重置重连计数
})
// 监听完成事件(自定义事件类型 "complete")
es.addEventListener('complete', (event) => {
const data: CompleteData = JSON.parse(event.data)
setStatus(data.status)
es.close() // 完成后主动关闭连接
})
// 监听错误事件(处理断线重连)
es.onerror = () => {
if (es.readyState === EventSource.CLOSED) {
setStatus('reconnecting')
es.close()
// 指数退避重连(1s, 2s, 4s... 最大 30s)
const delay = Math.min(2 ** reconnectAttemptsRef.current * 1000, 30000)
setTimeout(connect, delay)
reconnectAttemptsRef.current += 1
}
}
}
// 组件挂载时建立连接,卸载时关闭连接
useEffect(() => {
connect()
return () => {
eventSourceRef.current?.close()
}
}, [])
return (
<div style={{ padding: '20px' }}>
<h2>SSE 实时进度演示</h2>
<p>状态:{status}</p>
<div style={{
height: '20px',
width: '100%',
backgroundColor: '#eee',
marginTop: '10px'
}}>
<div
style={{
height: '100%',
width: `${progress}%`,
backgroundColor: '#0070f3',
transition: 'width 0.3s ease'
}}
/>
</div>
<p>进度:{progress}%</p>
</div>
)
}
4.4 页面集成
在 app/page.tsx
中引入客户端组件:
// app/page.tsx
import SSEClient from './sse-client'
export default function Home() {
return (
<main>
<h1>Server-Sent Events (SSE) Demo</h1>
<SSEClient />
</main>
)
}
5 优缺点分析:SSE 的适用边界
5.1 优势
- 简单易用:基于 HTTP 协议,无需复杂配置,客户端直接使用
EventSource
API - 低开销:文本协议,无额外握手过程,服务器资源占用低
- 自动重连:浏览器内置重连机制,无需手动实现
- 兼容性:支持 HTTP/2 多路复用(减少连接数限制),可穿透大部分防火墙
5.2 局限性
- 单向通信:仅支持服务器向客户端推送,无法实现双向交互
- 浏览器连接限制:HTTP/1.1 下同一域名最多 6 个并发连接(可通过子域名分流解决)
- 不支持二进制数据:仅能传输文本数据(需手动编码二进制数据为 Base64)
- IE 不支持:需使用 polyfill(如
event-source-polyfill
)兼容老旧浏览器
6 与 WebSocket 对比:如何选择实时技术
维度 | SSE | WebSocket |
---|---|---|
通信方向 | 单向(服务器→客户端) | 双向(全双工) |
协议基础 | HTTP/1.1/2(长连接) | 独立协议(需握手升级) |
数据格式 | 文本流(支持 JSON、纯文本) | 二进制/文本(灵活) |
连接限制 | HTTP/1.1 下 6 个/域名 | 无(单连接全双工) |
重连机制 | 内置(基于 retry 字段) | 需手动实现 |
适用场景 | 实时通知、数据监控、日志流 | 实时聊天、协作编辑、游戏 |
服务器复杂度 | 低(基于现有 HTTP 服务器) | 高(需专门 WebSocket 服务器) |
选型建议:
- 若需单向实时数据(如通知、行情),优先选择 SSE(简单、低开销)
- 若需双向交互(如聊天、游戏),选择 WebSocket
- 若需兼容老旧系统或穿透严格防火墙,SSE 更优(基于 HTTP)
7 注意事项:生产环境中的关键配置
7.1 服务器配置
- 禁用缓冲:确保服务器/反向代理(如 Nginx、Vercel)不缓冲 SSE 流,添加
X-Accel-Buffering: no
头 - 连接超时:设置合理的连接超时时间(如 5 分钟),避免过早断开空闲连接
- CORS 配置:跨域场景下需设置
Access-Control-Allow-Origin
和Access-Control-Allow-Credentials
7.2 客户端最佳实践
- 重连策略:自定义重连逻辑时采用指数退避(如 1s → 2s → 4s,上限 30s),避免服务器过载
- 连接管理:页面隐藏时(如
visibilitychange
事件)关闭连接,显示时重新连接,节省资源 - 错误处理:区分网络错误(可重连)和业务错误(如 401 需重新授权),避免无效重连
7.3 浏览器兼容性
SSE 在主流浏览器中支持良好,但需注意:
- 支持情况:Chrome 6+、Firefox 6+、Safari 5.1+、Edge 79+(Can I Use 数据)
- IE 兼容:需引入 polyfill(如
event-source-polyfill
),并确保服务器支持 CORS
7.4 性能优化
- 心跳检测:定期发送空事件(
data:\n\n
)维持连接,避免被网关断开 - 事件合并:高频更新场景下合并小事件(如每秒合并多次进度更新为一次)
- 连接池管理:服务端使用连接池复用资源,避免大量并发连接耗尽文件描述符
8 总结:SSE 的价值与未来
SSE 作为轻量级实时推送技术,以其简单、低开销、基于 HTTP的特性,在单向实时场景中具有不可替代的优势。结合 Next.js 14+ 的 App Router 和 TypeScript 类型安全,可快速构建可靠的实时应用。
随着 HTTP/3 的普及,SSE 的连接限制和性能问题将进一步改善,成为实时通知、数据监控等场景的首选技术。在实际开发中,需根据业务需求(单向/双向、实时性要求、兼容性)选择 SSE 或 WebSocket,必要时可结合两者构建混合实时系统。