大家好,我是大华!在现代的Web开发中,实时通信需求越来越普遍。比如在线聊天、实时数据监控、消息推送等场景。
面对这些需求,我们通常有两种选择:SSE和WebSocket。它们都能实现实时通信,但设计理念和适用场景却有很大不同。
什么是 SSE?
SSE(Server-Sent Events)是一种基于 HTTP 的服务器推送技术。它的核心特点是:单向通信,只能由服务器向客户端发送数据。
SSE 的核心特点
- 基于 HTTP 协议:使用标准的 HTTP/1.1 协议
- 单向通信:服务器 → 客户端
- 自动重连:浏览器内置重连机制
- 简单易用:API 设计简洁直观
- 文本传输:主要支持 UTF-8 文本数据
SSE 使用示例
客户端JS代码:
javascript
// 创建 SSE 连接
const eventSource = new EventSource('/api/real-time-data');
// 监听服务器推送的消息
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('收到实时数据:', data);
updateUI(data); // 更新界面
};
// 监听自定义事件类型
eventSource.addEventListener('systemAlert', function(event) {
const alertData = JSON.parse(event.data);
showAlert(alertData.message);
});
// 错误处理 - 自动重连是内置的
eventSource.onerror = function(event) {
console.log('连接异常,正在自动重连...');
};
服务器端代码(Node.js + Express):
javascript
app.get('/api/real-time-data', (req, res) => {
// 设置 SSE 必需的响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// 发送初始连接确认
res.write('data: {"status": "connected"}\n\n');
// 模拟实时数据推送
let count = 0;
const interval = setInterval(() => {
const data = {
id: count++,
timestamp: new Date().toISOString(),
value: Math.random() * 100
};
// SSE 标准格式:data: 开头,两个换行符结尾
res.write(`data: ${JSON.stringify(data)}\n\n`);
// 每10秒发送一次系统状态
if (count % 10 === 0) {
res.write('event: systemAlert\n');
res.write(`data: {"message": "系统运行正常"}\n\n`);
}
}, 1000);
// 客户端断开连接时清理资源
req.on('close', () => {
clearInterval(interval);
console.log('客户端断开连接');
});
});
什么是 WebSocket?
WebSocket 是一种真正的全双工通信协议,允许服务器和客户端之间建立持久连接,进行双向实时通信。
WebSocket 的核心特点
- 独立协议:基于 TCP 的独立协议(ws:// 或 wss://)
- 双向通信:服务器 ↔ 客户端
- 低延迟:建立连接后开销极小
- 数据多样:支持文本和二进制数据
- 手动管理:需要手动处理连接状态
WebSocket 使用示例
客户端JS代码:
javascript
class ChatClient {
constructor() {
this.socket = null;
this.isConnected = false;
}
connect() {
this.socket = new WebSocket('wss://api.example.com/chat');
this.socket.onopen = () => {
this.isConnected = true;
console.log('WebSocket 连接已建立');
this.send({ type: 'join', username: '小明' });
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.socket.onclose = () => {
this.isConnected = false;
console.log('连接已断开');
this.attemptReconnect();
};
this.socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
}
sendMessage(content) {
if (this.isConnected) {
this.send({
type: 'message',
content: content,
timestamp: Date.now()
});
}
}
send(data) {
this.socket.send(JSON.stringify(data));
}
handleMessage(data) {
switch (data.type) {
case 'chat':
this.displayMessage(data);
break;
case 'userJoin':
this.showUserJoin(data.username);
break;
}
}
attemptReconnect() {
setTimeout(() => {
console.log('尝试重新连接...');
this.connect();
}, 3000);
}
}
服务器端代码(Node.js + ws 库):
javascript
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: false
});
// 存储连接的用户
const connectedUsers = new Map();
wss.on('connection', (ws, request) => {
console.log('新的客户端连接');
let currentUser = null;
ws.on('message', (rawData) => {
try {
const data = JSON.parse(rawData);
switch (data.type) {
case 'join':
currentUser = data.username;
connectedUsers.set(ws, currentUser);
// 广播用户加入消息
broadcast({
type: 'userJoin',
username: currentUser,
time: new Date().toISOString()
}, ws);
// 发送欢迎消息
ws.send(JSON.stringify({
type: 'system',
message: `欢迎 ${currentUser} 加入聊天室!`
}));
break;
case 'message':
// 广播聊天消息
broadcast({
type: 'chat',
username: currentUser,
message: data.content,
timestamp: data.timestamp
});
break;
}
} catch (error) {
console.error('消息解析错误:', error);
ws.send(JSON.stringify({
type: 'error',
message: '消息格式错误'
}));
}
});
ws.on('close', () => {
if (currentUser) {
connectedUsers.delete(ws);
// 广播用户离开
broadcast({
type: 'userLeave',
username: currentUser,
time: new Date().toISOString()
});
}
console.log('客户端断开连接');
});
// 心跳检测
const heartbeat = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
ws.on('close', () => {
clearInterval(heartbeat);
});
});
function broadcast(data, excludeWs = null) {
wss.clients.forEach((client) => {
if (client !== excludeWs && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
}
区别对比
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信模式 | 单向(服务器推客户端) | 双向(全双工通信) |
| 协议基础 | HTTP/1.1 | 独立的 WebSocket 协议 |
| 连接建立 | 普通 HTTP 请求 | HTTP 升级握手 |
| 数据格式 | 文本(事件流格式) | 文本和二进制帧 |
| 重连机制 | 浏览器自动处理 | 需要手动实现 |
| 头部开销 | 每次消息带 HTTP 头 | 建立后极小帧头 |
| 兼容性 | 良好(除 IE) | 优秀(IE10+) |
| 开发复杂度 | 简单直观 | 相对复杂 |
适用场景
推荐使用 SSE 的场景
1. 实时数据监控面板 2. 实时消息通知 3. 实时数据流展示
比如:股票价格实时更新、体育比赛比分直播、物流订单状态跟踪和服务器日志实时显示等。
推荐使用 WebSocket 的场景
1. 实时交互应用 2. 实时游戏应用 3. 实时音视频通信
比如:视频会议系统、在线客服聊天和实时协作编辑文档等
如何选择?
选择 SSE:
- 只需要服务器向客户端推送数据
- 希望快速实现、简单维护
- 项目对移动端兼容性要求高
- 数据更新频率适中(秒级)
- 不需要传输二进制数据
选择 WebSocket:
- 需要真正的双向实时通信
- 数据传输频率很高(毫秒级)
- 需要传输二进制数据(如图片、音频)
- 构建实时交互应用(游戏、协作工具)
- 对延迟极其敏感的场景
混合使用策略
在一些复杂应用中,可以同时使用两者:
javascript
class HybridApp {
constructor() {
// 使用 SSE 接收通知和广播消息
this.notificationSource = new EventSource('/api/notifications');
// 使用 WebSocket 进行实时交互
this.interactionSocket = new WebSocket('wss://api.example.com/interact');
this.setupEventHandlers();
}
setupEventHandlers() {
// SSE 处理广播类消息
this.notificationSource.onmessage = (event) => {
this.handleBroadcastMessage(JSON.parse(event.data));
};
// WebSocket 处理交互类消息
this.interactionSocket.onmessage = (event) => {
this.handleInteractionMessage(JSON.parse(event.data));
};
}
}
总结
SSE 的优势在于简单易用、自动重连、与 HTTP 基础设施完美集成,适合服务器向客户端的单向数据推送场景。
WebSocket 的优势在于真正的双向通信、低延迟、支持二进制数据,适合需要高频双向交互的复杂应用。
在实际项目中,可以根据具体的业务需求、性能要求来做出合理的技术选型。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot+Vue3 整合 SSE 实现实时消息推送》