彻底搞懂 SSE 协议:从底层原理到实战实现(附完整代码)
目录
四、从零实现 SSE:极简示例(Node.js + 原生 JS)
前言
在实时通信场景中,我们常听到 WebSocket,但很多人忽略了另一个轻量、易用的方案 ——SSE(Server-Sent Events)。相比于 WebSocket 的双向通信,SSE 专注于服务端向客户端单向实时推送,基于 HTTP 原生协议实现,无需复杂的协议升级和额外依赖,是系统通知、实时监控、日志推送等场景的最优解。
本文将从底层原理到代码实现,一步步拆解 SSE 协议,带你彻底搞懂它的工作机制、协议规范和落地方式,新手也能跟着实操。
一、SSE 是什么?先理清定位
1. 核心定义
SSE(Server-Sent Events)是 HTML5 标准化的服务器向客户端单向实时推送数据的通信协议,基于 HTTP/HTTPS 协议实现,无需建立独立的 TCP 连接,专门解决 “HTTP 协议下服务端无法主动向客户端推送数据” 的问题。
2. SSE vs WebSocket:选对场景更重要
很多人会混淆两者,其实它们是互补而非替代关系,核心区别如下:
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务端→客户端) | 双向(服务端↔客户端) |
| 协议基础 | 基于 HTTP/HTTPS,复用现有栈 | 基于 TCP,需 HTTP 握手升级协议 |
| 重连机制 | 浏览器内置自动重连 | 需手动实现重连逻辑 |
| 开发复杂度 | 极低(原生 API + 简单协议) | 较高(需处理握手、心跳) |
| 部署成本 | 兼容所有 HTTP 网关 / 代理 | 需代理支持 WebSocket 升级 |
| 适用场景 | 实时通知、监控、日志推送 | 在线聊天、弹幕、实时协作 |
简单说:单向推送选 SSE,双向交互选 WebSocket。
二、SSE 的底层原理:HTTP 长连接 + 流式响应
SSE 的 “实时性” 和 “单向推送” 特性,本质是对 HTTP 协议的巧妙利用,核心原理可拆解为 3 点:
1. 核心:HTTP 长连接的流式响应
普通 HTTP 请求是 “请求 - 响应” 模式:客户端发请求→服务端一次性返回所有数据→连接关闭;SSE 则是 “请求 - 持续响应” 模式:
- 客户端发起一次 GET 请求后,服务端不立即关闭连接,保持HTTP 长连接;
- 服务端通过这个长连接,分块、实时输出数据(流式响应);
- 浏览器实时监听响应流,每收到一块数据就立即解析,无需等待连接关闭。
2. 协议保障:浏览器原生解析
浏览器内置EventSource对象,专门用于处理 SSE 连接:
- 自动识别
text/event-stream类型的响应,无需手动解析流数据; - 内置自动重连、断点续传逻辑,无需开发者重复造轮子;
- 按 SSE 协议格式解析消息,触发对应事件(如自定义事件、默认消息事件)。
3. 稳定性保障:心跳 + 重连 + 断点续传
为了保证长连接的稳定性,SSE 协议内置了 3 个关键机制:
- 心跳保活:服务端定期发送注释消息(以
:开头),防止长连接因 “空闲” 被网关 / 浏览器断开; - 自动重连:连接断开后,浏览器自动发起重连(默认 3 秒间隔,可自定义);
- 断点续传:浏览器记录最后接收的消息 ID,重连时通过
Last-Event-ID请求头告知服务端,服务端可补发未推送的消息。
三、SSE 协议规范:必须遵守的 “规则”
要实现 SSE,必须严格遵循协议规范,核心是响应头和消息格式,这是浏览器能正确解析的前提。
1. 核心响应头
服务端返回的响应头必须包含以下字段,否则浏览器无法识别为 SSE 连接:
# 核心:标识SSE响应类型
Content-Type: text/event-stream; charset=UTF-8
# 禁止缓存,保证消息实时性
Cache-Control: no-cache
# 保持长连接
Connection: keep-alive
# Nginx代理时必须加:禁用缓冲区,避免消息被缓存
X-Accel-Buffering: no
2. 消息格式:固定结构 + 空行结束
SSE 的消息由 “字段行 + 空行” 组成,空行是消息结束的唯一标识,核心字段有 4 个:
| 字段 | 作用 |
|---|---|
data |
消息体核心内容,支持多行(每行以data:开头,最终拼接为完整消息) |
id |
消息唯一标识,用于断点续传(浏览器记录为Last-Event-ID) |
event |
自定义事件类型,客户端可按需监听(默认值为message) |
retry |
浏览器自动重连的间隔(毫秒),覆盖默认值 |
常见消息示例
# 示例1:默认事件+单行数据
data: 这是一条普通消息
id: 1001
# 示例2:自定义事件+多行JSON数据
event: notice
id: 1002
data: {
data: "title": "系统通知",
data: "content": "订单支付成功"
data: }
# 示例3:注释消息(心跳保活,浏览器忽略)
: heartbeat-2026-01-29 16:00:00
# 示例4:自定义重连间隔
retry: 5000
data: 重连间隔设置为5秒
四、从零实现 SSE:极简示例(Node.js + 原生 JS)
为了让你直观理解,先实现一个最基础的 SSE 示例 —— 服务端每秒推送当前时间,客户端实时接收展示。
1. 服务端(Node.js):流式推送数据
新建server.js,无需任何框架,纯 Node.js 实现:
const http = require('http');
// 创建HTTP服务器
const server = http.createServer((req, res) => {
// 仅处理/SSE路径的请求
if (req.url === '/sse') {
// 1. 设置SSE核心响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=UTF-8',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*' // 解决跨域
});
// 2. 定时推送数据(每秒1次)
let msgId = 1;
const timer = setInterval(() => {
const now = new Date().toLocaleString();
// 按SSE协议拼接消息
const sseMsg = `event: time
id: ${msgId}
data: 当前时间:${now}
`; // 空行必须加,标识消息结束
// 3. 流式输出消息到客户端
res.write(sseMsg);
msgId++;
}, 1000);
// 4. 连接关闭时清理定时器,避免内存泄漏
req.on('close', () => {
clearInterval(timer);
res.end();
});
} else {
res.end('请访问 /sse 建立SSE连接');
}
});
// 启动服务,监听3000端口
server.listen(3000, () => {
console.log('SSE服务已启动:http://localhost:3000');
});
2. 客户端(HTML + 原生 JS):实时接收
新建index.html,直接打开即可:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SSE极简示例</title>
</head>
<body>
<h3>SSE实时接收时间戳:</h3>
<div id="time-box" style="width: 600px; height: 300px; border: 1px solid #ccc; padding: 10px; overflow-y: auto;"></div>
<script>
// 1. 建立SSE连接
const eventSource = new EventSource('http://localhost:3000/sse');
// 2. 监听自定义事件(time)
eventSource.addEventListener('time', (e) => {
const timeBox = document.getElementById('time-box');
// 3. 实时展示消息
timeBox.innerHTML += `<p>消息ID:${e.lastEventId} | ${e.data}</p>`;
// 滚动到底部
timeBox.scrollTop = timeBox.scrollHeight;
});
// 连接成功回调
eventSource.onopen = () => {
console.log('SSE连接已建立');
};
// 连接异常回调(自动重连)
eventSource.onerror = (e) => {
console.log('连接异常,正在重连...', e);
};
// 页面关闭时主动关闭连接
window.onbeforeunload = () => {
eventSource.close();
};
</script>
</body>
</html>
3. 运行效果
- 执行
node server.js启动服务; - 打开
index.html,页面会每秒刷新服务端推送的时间戳; - 关闭服务端再重启,客户端会自动重连,继续接收数据。
五、Spring Boot 实战 SSE:生产级实现
如果你的技术栈是 Java/Spring Boot,这里提供生产级的 SSE 实现,包含连接管理、广播、心跳保活等核心功能。
1. 基础依赖
仅需 Spring Web 核心依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. SSE 连接管理器(核心)
用于管理所有客户端连接,避免内存泄漏:
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
public class SseEmitterManager {
// 存储客户端连接:key=客户端ID,value=SSE连接
private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
// 心跳线程池
private ScheduledExecutorService heartbeatExecutor;
// 心跳间隔(30秒)
private static final long HEARTBEAT_INTERVAL = 30L;
/**
* 添加客户端连接
*/
public SseEmitter addEmitter(String clientId) {
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30分钟超时
// 连接关闭/异常时移除
emitter.onCompletion(() -> emitterMap.remove(clientId));
emitter.onError(e -> emitterMap.remove(clientId));
emitter.onTimeout(() -> emitterMap.remove(clientId));
emitterMap.put(clientId, emitter);
return emitter;
}
/**
* 向单个客户端推送消息
*/
public void sendToClient(String clientId, String event, String id, Object data) {
SseEmitter emitter = emitterMap.get(clientId);
if (emitter == null) return;
try {
emitter.send(SseEmitter.event().name(event).id(id).data(data));
} catch (Exception e) {
emitterMap.remove(clientId);
}
}
/**
* 广播消息(推送给所有客户端)
*/
public void broadcast(String event, String id, Object data) {
emitterMap.forEach((clientId, emitter) -> sendToClient(clientId, event, id, data));
}
/**
* 启动心跳保活任务
*/
@PostConstruct
public void init() {
heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
heartbeatExecutor.scheduleAtFixedRate(() -> {
// 发送注释消息作为心跳
emitterMap.forEach((clientId, emitter) -> {
try {
emitter.send(": heartbeat");
} catch (Exception e) {
emitterMap.remove(clientId);
}
});
}, 0, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
}
/**
* 销毁资源
*/
@PreDestroy
public void destroy() {
if (heartbeatExecutor != null) heartbeatExecutor.shutdown();
emitterMap.clear();
}
}
3. 控制层接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.UUID;
@RestController
@RequestMapping("/api/sse")
@CrossOrigin(origins = "*") // 跨域配置
public class SseController {
@Autowired
private SseEmitterManager sseEmitterManager;
/**
* 建立SSE连接
*/
@GetMapping("/connect")
public SseEmitter connect(@RequestParam String clientId) {
return sseEmitterManager.addEmitter(clientId);
}
/**
* 向单个客户端推送消息
*/
@PostMapping("/send")
public void sendToClient(
@RequestParam String clientId,
@RequestParam String event,
@RequestBody Object data
) {
String msgId = UUID.randomUUID().toString();
sseEmitterManager.sendToClient(clientId, event, msgId, data);
}
/**
* 广播消息
*/
@PostMapping("/broadcast")
public void broadcast(@RequestParam String event, @RequestBody Object data) {
String msgId = UUID.randomUUID().toString();
sseEmitterManager.broadcast(event, msgId, data);
}
}
4. 前端测试
复用第四部分的index.html,仅需修改 SSE 连接地址为:
const eventSource = new EventSource('http://localhost:8080/api/sse/connect?clientId=user1');
六、生产环境注意事项
1. Nginx 代理配置(关键)
若通过 Nginx 反向代理,需禁用缓冲区,否则消息会被缓存:
server {
listen 80;
server_name your-domain.com;
location /api/sse/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
proxy_set_header Host $host;
proxy_cache off;
proxy_buffering off; # 禁用缓冲区
proxy_read_timeout 3600s; # 超时时间大于心跳间隔
}
}
2. 连接数优化
浏览器默认单域名仅支持 6 个 SSE 连接,解决方案:
- 多域名部署(如sse1.xxx.com、sse2.xxx.com);
- 前端使用共享 Worker,多个页面共用一个 SSE 连接。
3. 异常处理
- 服务端:推送失败立即移除无效连接,设置超时时间;
- 客户端:监听
onerror事件,手动补充重连逻辑(配合浏览器自动重连)。
七、常见问题与避坑
- 消息延迟 / 不实时:大概率是 Nginx 开启了缓冲区,需添加
proxy_buffering off; - 连接频繁断开:未设置心跳保活,服务端需定期发送注释消息;
- 重连后数据丢失:未使用
id字段和Last-Event-ID实现断点续传; - IE 浏览器不支持:IE 无
EventSourceAPI,需使用 polyfill 库(如eventsource-polyfill)。
总结
SSE 协议的核心是基于 HTTP 长连接的流式响应,以 “轻量、易用、适配现有 HTTP 生态” 为核心优势,是单向实时推送场景的最佳选择:
- 原理层面:通过 HTTP 长连接 + 流式响应实现实时性,浏览器原生
EventSource简化开发; - 协议层面:严格的响应头和消息格式是正确解析的前提,空行是消息结束的关键;
- 实现层面:简单场景可直接用原生 API,生产环境需做好连接管理、心跳保活和异常处理。
如果你的业务场景是系统通知、实时监控、日志推送等单向推送需求,SSE 比 WebSocket 更简单、更易维护;如果需要双向交互,再考虑 WebSocket 即可。
更多推荐

所有评论(0)