🔴 WebSocket vs SSE vs 长轮询
📖 定义
实时通信是使服务器和客户端之间能够立即交换数据的技术。WebSocket提供双向实时通信,SSE(Server-Sent Events)只从服务器向客户端推送数据,长轮询(Long Polling)是使用HTTP 的实时通信方式。各有优缺点,需要根据使用场景进行选择。
🎯 用比喻理解
电话 vs 广播 vs 信使
普通HTTP = 邮寄
你: "你好吗?" (寄信)
↓ (几天后)
朋友: "嗨!" (回信)
↓ (几天后)
你: "最近好吗?" (再寄信)
- 慢
- 每次重新连接
- 无法实时
长轮询 = 电话(等待通话)
你: "有事就告诉我!" (打电话等待)
↓ (等待一段时间...)
朋友: "现在有话要说!" (响应)
↓ (通话结束)
你: "再有事就告诉我!" (再打电话)
- 基于HTTP
- 保持连接 → 响应 → 重连
- 效率低
SSE = 广播
朋友: "大家好!" (开始广播)
"今天天气..." (持续发送)
"下一条新闻..." (持续发送)
你: (只听)
- 服务器 → 客户端(单向)
- 保持连接
- 简单
WebSocket = 视频通话
你: "你好!" (立即发送)
朋友: "见到你很高兴!" (立即响应)
你: "在做什么?" (立即发送)
朋友: "正在编程!" (立即响应)
- 双向实时
- 保持连接
- 快速且高效
餐厅点餐
普通HTTP = 自助服务
1. 去柜台点餐
2. 等待
3. 取餐回到座位
4. 再去柜台点饮料
5. 再等待
长轮询 = 按铃等待
1. 按铃等待服务员(保持连接)
2. 服务员来了 → "请问您要点什么"
3. 点完再按铃
4. 再等待...
SSE = 厨房显示器
厨房: "1号客人,您的餐已好!"
厨房: "2号客人,正在准备!"
厨房: "3号客人,马上好!"
你: (只听)
WebSocket = 桌边服务
你: "请给我水"
服务员: "好的,马上来"
你: "泡菜也要"
服务员: "马上给您"
服务员: "您的餐来了"
你: "谢谢"
- 自由对话
- 快速响应
⚙️ 工作原理
1. 普通HTTP vs 实时通信
========== 普通HTTP(请求-响应) ==========
客户端 服务器
│ │
│ 1. 连接(Request) │
│────────────────────────>│
│ │
│ │ 处理中...
│ │
│ 2. 响应(Response) │
│<──────────────────── ────│
│ │
│ 连接结束 │
╳ ╳
│ 3. 重新连接 │
│────────────────────────>│
│ │
特点:
- 只有客户端请求时才响应
- 每次重新连接
- 服务器无法主动发送
- 无法实时
========== 实时通信 ==========
客户端 服务器
│ │
│ 连接 │
│<───────────────────────>│
│ │
│ 保持双向通信 │
│<───────────────────────>│
│ │
│ 数据交换 │
│<───────────────────────>│
│ │
特点:
- 保持连接
- 服务器可以主动发送
- 可以实时
2. 长轮询(Long Polling)
客户端 服务器
│ │
│ 1. 请求(有新数据吗?) │
│──────────────────────────────────>│
│ │
│ 保持连接(等待中...) │
│ │
│ │ 没有数据...
│ │ 继续等待...
│ │
│ │ 新数据产生!
│ │
│ 2. 响应(有数据) │
│<──────────────────────────────────│
│ │
│ 连接结束 │
╳ ╳
│ │
│ 3. 立即重连 │
│──────────────────────────────────>│
│ │
│ 再次等待... │
流程:
1. 客户端 → 服务器: "有新数据吗?"
2. 服务器: 等待数据产生
3. 数据产生 → 响应
4. 连接结束
5. 立即重连(重复)
优点:
✅ 基于HTTP(利用现有基础设施)
✅ 没有防火墙问题
✅ 实现简单
缺点:
❌ 效率低(持续重连)
❌ 服务器负担大
❌ 请求头开销大
3. Server-Sent Events (SSE)
客户端 服务器
│ │
│ 1. 连接请求 │
│──────────────────────────────────>│
│ │
│ 2. 保持连接(开始流) │
│<══════════════════════════════════│
│ │
│ 3. 数据推送 │
│<────────────────────────────────── │
│ │
│ 4. 再次数据推送 │
│<──────────────────────────────────│
│ │
│ 连接保持中... │
│<══════════════════════════════════│
特点:
- 服务器 → 客户端(单向)
- 保持连接
- 基于HTTP
- 自动重连
优点:
✅ 简单(浏览器内置)
✅ 自动重连
✅ 可通过事件ID恢复
✅ HTTP/2下高效
缺点:
❌ 单向(服务器 → 客户端)
❌ 不支持二进制数据
❌ IE不支持
4. WebSocket
客户端 服务器
│ │
│ 1. HTTP Upgrade请求 │
│──────────────────────────────────>│
│ │
│ 2. Upgrade确认 │
│<──────────────────────────────────│
│ │
│ 切换到WebSocket协议 │
│<══════════════════════════════════>│
│ │
│ 3. 发送数据 │
│──────────────────────────────────>│
│ │
│ 4. 接收数据 │
│<──────────────────────────────────│
│ │
│ 5. 发送数据 │
│──────────────────────────────────>│
│ │
│ 双向通信持续... │
│<══════════════════════════════════>│
特点:
- 双向实时通信
- 独立协议(ws://, wss://)
- 保持连接
- 低延迟
优点:
✅ 真正的实时
✅ 双向通信
✅ 低开销
✅ 支持二进制
缺点:
❌ 复杂
❌ 可能有代理/防火墙问题
❌ 服务器负担(保持连接)
💡 实例
长轮询示例
// ========== 服务器(Express.js) ==========
const express = require('express');
const app = express();
let messages = [];
let waitingClients = [];
// 添加消息(从其他地方调用)
function addMessage(message) {
messages.push(message);
// 立即响应等待中的客户端
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}
// 长轮询端点
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;
// 如果有新消息立即响应
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}
// 否则添加到等待列表(最多30秒)
waitingClients.push(res);
// 30秒超时
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // 空响应
}
});
});
app.listen(3000);
// ========== 客户端 ==========
let lastMessageId = 0;
async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();
// 处理新消息
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('新消息:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}
// 立即重连
await longPolling();
} catch (error) {
console.error('错误:', error);
// 3秒后重试
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}
// 开始
longPolling();
// ========== 问题 ==========
/*
1. 持续重连(效率低)
2. 网络使用量大
3. 服务器负担大
4. 电池消耗(移动端)
*/
Server-Sent Events (SSE) 示例
// ========== 服务器(Express.js) ==========
const express = require('express');
const app = express();
app.use(express.static('public'));
// SSE端点
app.get('/events', (req, res) => {
// 设置SSE头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// CORS(如果需要)
res.setHeader('Access-Control-Allow-Origin', '*');
// 连接确认消息
res.write('data: Connected\n\n');
// 客户端ID
const clientId = Date.now();
console.log(`客户端 ${clientId} 已连接`);
// 每5秒发送时间
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: '你好!'
};
// 以SSE格式发送
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// 客户端断开连接处理
req.on('close', () => {
console.log(`客户端 ${clientId} 已断开`);
clearInterval(intervalId);
res.end();
});
});
// 事件发布API
const clients = [];
app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 保存客户端
clients.push(res);
req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});
// 向所有客户端发送消息
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;
clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});
res.json({ success: true });
});
app.listen(3000);
// ========== 客户端(HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<script>
// SSE连接
const eventSource = new EventSource('/events');
// 接收消息
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到数据:', data);
const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};
// 连接开始
eventSource.onopen = () => {
console.log('SSE已连接');
};
// 错误处理
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE连接已关闭');
}
};
// 连接关闭(离开页面时)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/
// ========== 高级功能 ==========
// 1. 指定事件类型
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// 多种类型的事件
setInterval(() => {
// 普通消息
res.write(`event: message\ndata: Hello\n\n`);
// 通知
res.write(`event: notification\ndata: New notification!\n\n`);
// 更新
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});
// 客户端按事件类型处理
/*
eventSource.addEventListener('message', (e) => {
console.log('消息:', e.data);
});
eventSource.addEventListener('notification', (e) => {
console.log('通知:', e.data);
});
eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('更新:', data);
});
*/
// 2. 事件ID(重连时从中断处继续接收)
let eventId = 0;
app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('最后的事件ID:', lastEventId);
// 只发送lastEventId之后的事件
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});
// 3. 设置重试时间
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// 5秒后重连
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});