SSE vs WebSocket:服务端推送的两种姿势,原理和实现全解析
从实时推送这个需求说起
Web 应用发展到今天,"浏览器主动请求,服务器被动响应"的模型在很多场景下已经不够用。
消息通知、订单状态更新、AI 对话流式输出、实时股价——这些场景都需要服务器主动把数据推送给客户端。实现这个需求,主流方案就两个:SSE 和 WebSocket。
这两个东西经常被放在一起比较,但底子完全不同。选错了,要么杀鸡用牛刀,要么牛刀不够用。这篇把它们的原理和实现细节掰开揉碎讲清楚。
SSE:基于 HTTP 的"单向通道"
SSE 全称 Server-Sent Events,服务端推送事件。名字已经说得很明白了——服务端向客户端推送事件,单向的。
原理一句话
客户端发一个普通的 HTTP 请求,告诉服务器"我不走了,你有数据就发过来"。服务器在同一个 HTTP 连接上,持续不断地把数据以 text/event-stream 格式写回。
客户端实现
浏览器原生支持,用 EventSource 接口就能搞定:
const source = new EventSource('/api/sse/notifications');
// 监听命名事件
source.addEventListener('order-update', (event) => {
const data = JSON.parse(event.data);
updateOrderStatus(data);
});
// 监听未命名事件
source.onmessage = (event) => {
console.log('收到数据:', event.data);
};
// 错误处理
source.onerror = (err) => {
console.error('SSE 连接异常:', err);
// EventSource 会自动重连,不用自己写重试逻辑
};
代码量少得可怜,浏览器自带重连机制。断线了自动重试,重试间隔服务端还能通过 retry 字段控制。
服务端实现
服务端稍微麻烦一点,但也不复杂。以 Spring Boot 为例:
@GetMapping(path = "/api/sse/notifications", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamNotifications() {
SseEmitter emitter = new SseEmitter(0L); // 0L 表示不超时
// 在另一个线程推送数据
executorService.execute(() -> {
try {
while (true) {
Notification notif = notificationQueue.poll(5, TimeUnit.SECONDS);
if (notif != null) {
emitter.send(SseEmitter.event()
.name("order-update")
.data(notif));
} else {
// 发送心跳,保持连接
emitter.send(SseEmitter.event().comment("heartbeat"));
}
}
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
SseEmitter 是 Spring 对 SSE 的封装,核心就是建立连接后一直保持着,有数据就写,没数据就发心跳。
数据格式
SSE 的传输格式是纯文本,一行一个字段:
event: order-update
data: {"orderId": 1001, "status": "shipped", "timestamp": "2026-06-04T15:00:00Z"}
event: order-update
data: {"orderId": 1002, "status": "delivered"}
: 这个是注释,相当于心跳
data: 这是一条没有事件名的消息
每条消息用空行分隔。字段含义很直观:
event:事件名称,客户端用addEventListener监听data:数据内容,可以是任何文本id:事件 ID,用于断线重连时告诉服务端从哪开始retry:重连间隔,毫秒:开头的是注释,通常用作心跳
WebSocket:全双工的"长连接"
WebSocket 跟 SSE 完全不是一路东西。SSE 是在 HTTP 协议上"借道",WebSocket 是通过 HTTP 升级协议后的全新 TCP 通道。
握手机制
WebSocket 不是简单发个请求就完事的,它有个握手流程:
客户端 → 服务端:
GET /ws/chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务端 → 客户端:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
关键在 Upgrade: websocket 和 101 Switching Protocols。握手完成后,协议就从 HTTP 切换到了 WebSocket,之后的数据传输走的是独立的二进制帧协议,不再受 HTTP 的约束。
客户端实现
const ws = new WebSocket('wss://api.example.com/ws/chat');
// 连接建立
ws.onopen = () => {
console.log('WebSocket 已连接');
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};
// 接收消息
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleMessage(msg);
};
// 发送消息
function sendMessage(text) {
ws.send(JSON.stringify({
type: 'message',
content: text,
timestamp: Date.now()
}));
}
// 错误处理
ws.onerror = (err) => {
console.error('WebSocket 错误:', err);
};
// 连接关闭
ws.onclose = (event) => {
console.log('WebSocket 已断开, code:', event.code);
// 注意:WebSocket 不会自动重连,需要自己实现
reconnect();
};
WebSocket 也是浏览器原生支持的,但跟 SSE 一个显著区别是:WebSocket 断线后不会自动重连,需要自己写重连逻辑。
服务端实现
Java 用 Spring WebSocket:
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = extractUserId(session);
sessions.put(userId, session);
broadcast(userId + " 已上线");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
ChatMessage msg = parseMessage(message.getPayload());
// 根据消息类型做不同处理
switch (msg.getType()) {
case "chat" -> sendToUser(msg.getTargetId(), msg);
case "broadcast" -> broadcast(msg);
case "typing" -> sendTypingIndicator(msg);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String userId = extractUserId(session);
sessions.remove(userId);
broadcast(userId + " 已离线");
}
}
WebSocket 是真正的双向通信。客户端能发消息给服务端,服务端也能主动推给客户端。同一个连接上,两边都能随时开口说话。
核心区别一张表
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端 → 客户端(单向) | 双向 |
| 协议基础 | HTTP | WebSocket(基于 TCP) |
| 浏览器支持 | EventSource 原生支持 | WebSocket 原生支持 |
| 自动重连 | 内置 | 需自己实现 |
| 数据传输格式 | 纯文本(强制 UTF-8) | 文本或二进制 |
| 最大连接数限制(同域) | 浏览器限制 6 个 | 无明确限制 |
| 自定义 Headers | 不支持(EventSource 限制) | 支持 |
| 实现复杂度 | 简单 | 中等 |
| 兼容性 | 不支持 IE(Edge 支持) | 几乎所有现代浏览器 |
什么场景用 SSE
AI 对话流式输出。 ChatGPT 那种一个字一个字往外蹦的效果,现在主流方案就是 SSE。数据流向单一(服务器→客户端),实现简单,浏览器自带的 EventSource 天然适合流式消费。
消息通知。 订单状态变更、系统告警、审批通知。都是服务器主动推给客户端,客户端不需要回传数据。SSE 够用,不需要 WebSocket 这种重量级方案。
日志实时展示。 运维面板上 tail 服务器日志。服务端只管吐数据,客户端只管渲染。单向、高吞吐、断线可续传——SSE 的 Last-Event-ID 机制让你重连后从断点继续接收。
数据看板 / 监控面板。 股价行情、服务器指标、在线人数统计。每隔几秒推送一次更新数据,SSE 比轮询优雅,又比 WebSocket 轻量。
// Spring Boot 集成 SSE 实现流式输出(GPT 风格)
@GetMapping(value = "/api/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestParam String prompt) {
SseEmitter emitter = new SseEmitter(60000L);
aiService.streamChat(prompt, new StreamCallback() {
@Override
public void onToken(String token) {
safeSend(() -> emitter.send(token));
}
@Override
public void onComplete() {
emitter.complete();
}
@Override
public void onError(Throwable t) {
emitter.completeWithError(t);
}
});
return emitter;
}
什么场景用 WebSocket
即时通讯 / 聊天。 用户在群里发消息,消息要推给群里的其他人。这是典型的双向通信——每个人既要发也要收。WebSocket 是唯一合理的选择。
多人协作。 在线文档编辑、白板协作、协同编程。用户的操作需要实时广播给协作者,同时接收其他人的变更。WebSocket 的低延迟双向能力在这里不可替代。
实时游戏。 对战类游戏里,玩家的操作指令和服务器的事件推送都是毫秒级的双向交互。WebSocket 的二进制帧支持还能自定义高压缩比的通信协议。
实时表单 / 数据绑定。 一个用户在后台编辑配置,所有打开这个页面的管理员都要实时看到变化。双向同步的场景,WebSocket 更加自然。
一些容易忽略的细节
浏览器同域 SSE 连接数限制。 Chrome 和 Firefox 对同一个域名只允许 6 个 SSE 连接。如果你同时打开多个页面或标签,6 个限制很容易用完。WebSocket 没有这个限制。
WebSocket 的负载均衡。 WebSocket 连接是长连接,会绑定到具体服务器实例。如果后面挂了一组服务器,需要引入 Sticky Session 或者独立的消息中间件做广播。SSE 本质也是长连接,同样面临这个问题,但 SSE 可以降级到轮询。
SSE 不能发自定义请求头。 EventSource API 设计得"太简单"了,导致你没法在初始化时带上 Authorization header。解决方案有两个:一是把 token 放在 URL 参数里(安全隐患自己评估),二是在 Server 端通过 Cookie 鉴权。当然你也可以用 fetch API 手动解析 SSE 流来绕过这个限制。
WebSocket 对网关和防火墙不友好。 某些企业网络会拦截非标准端口的 WebSocket 连接,或者代理不支持 Upgrade 头。SSE 走的是标准 HTTP 端口,通常不会被拦截。
总结
选 SSE 还是选 WebSocket,你只需要回答三个问题:
- 需要双向通信吗? 是 → WebSocket,否 → 继续往下看
- 数据是服务器主动推送吗? 是 → 考虑 SSE,否 → 不用选这两个
- 实现简单优先,还是功能全面优先? 简单优先 → SSE,全面优先 → WebSocket
没有哪个方案能覆盖所有场景。AI 流式输出用 SSE,最合适;在线聊天用 WebSocket,最合适。
把技术的边界搞清楚,你的架构决策就不会跑偏。