关注「索引目录」公众号,获取更多干货。
现在大家都默认使用 WebSocket 来实现实时功能。但大多数人不应该这样做。
事实是: 95%的“实时”应用只需要服务器到客户端的更新。例如:聊天通知、实时仪表盘、股票行情、日志流、人工智能响应。
WebSocket 提供双向通信功能。但双向通信也带来一些弊端:复杂性、资源开销、扩展性挑战以及调试难题。
服务器发送事件 (SSE) 的功能只有一个:将数据从服务器流式传输到客户端。它们在这方面做得非常出色。对于大多数应用程序来说,这已经足够了。
以下是 SSE 应该成为实时功能默认选项的原因,并附有真实的生产数据和客观的权衡取舍。
WebSocket 假设
对话通常是这样的:
开发人员: “我们需要实时更新。”
团队: “使用 WebSocket。”
开发人员: “为什么?”
团队: “因为它们是实时的。”
没有人质疑这一点。WebSocket 已成为所有涉及“实时”或“动态”功能的默认选择。
但令人不安的事实是:双向沟通很少是必要的。
让我们来看看“实时”在生产应用中究竟意味着什么:
无需双向通信的实时功能:
仪表盘:
-
服务器推送指标 -
客户端渲染图表 -
更新单向进行:服务器 → 客户端
通知:
-
服务器发送警报 -
客户展示它们 -
单向
实时直播:
-
服务器传输新内容(推文、帖子、事件) -
客户端向信息流添加内容 -
单向
AI聊天(ChatGPT风格):
-
服务器会在生成令牌时立即将其传输到服务器流中。 -
客户逐字显示 -
响应流程:服务器 → 客户端(用户输入通过单独的 POST 请求)
股票代码:
-
服务器推送价格更新 -
客户端更新界面 -
单程
日志流:
-
服务器尾部日志 -
客户实时显示 -
服务器 → 仅客户端
构建状态/CI/CD:
-
服务器发送进度更新 -
客户展示构建步骤 -
单向
看出规律了吗?这些占了95%的“实时”用例。
真正需要双向通信的实时功能:
多人游戏:
-
玩家发出走法 -
服务器向所有玩家广播 -
持续的双向交通
协作编辑(Google 文档):
-
用户编辑文档 -
服务器协调变更 -
向所有编辑广播 -
高频双向
视频通话/WebRTC信令:
-
同伴发现 -
ICE候选人交换 -
持续谈判
交易平台:
-
用户下单 -
服务器确认 -
市场动态回流 -
两个方向同时
WebSocket 在以下5% 的应用场景中表现出色。
至于其他功能呢?你实际上是在为那些你根本用不到的功能支付 WebSocket 税。
什么是服务器发送事件 (SSE)?
SSE 非常简单:它就是一个始终保持打开状态的 HTTP 连接。每当有新数据时,服务器都会向该连接写入数据。
协议:
GET /api/stream HTTP/1.1
Host: example.com
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"message": "Hello World"}
data: {"message": "Another update"}
data: {"message": "And another"}
就是这样。纯文本通过HTTP传输。无需协议升级。无需握手过程。
客户端代码(JavaScript):
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
eventSource.onerror = (error) => {
console.error('Connection error:', error);
// Browser automatically reconnects
};
服务器端代码(Node.js/Fastify):
fastify.get('/api/stream', (req, reply) => {
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const interval = setInterval(() => {
const data = JSON.stringify({ message: 'Update', timestamp: Date.now() });
reply.raw.write(`data: ${data}\n\n`);
}, 1000);
req.raw.on('close', () => {
clearInterval(interval);
});
});
10 行代码。无需任何库。仅支持 HTTP。
相比之下,WebSockets:
const ws = new WebSocket('ws://example.com/socket');
ws.onopen = () => {
console.log('Connected');
// Now what? Send ping? Subscribe to channels?
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
ws.onerror = (error) => {
console.error('Error:', error);
// Manual reconnection logic required
};
ws.onclose = () => {
console.log('Connection closed');
// Implement exponential backoff, reconnect...
};
// Send heartbeat to keep connection alive
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
已经更复杂了。而且我们还没处理重连、消息排队或协议协商等问题。
SSE 与 WebSocket:真正的比较
让我们来看看实际部署的生产数据。
性能基准测试(Timeplus,2024)
测试设置:每秒 100,000 个事件,10-30 个并发连接
结果:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
结论:性能基本相同。SSE 占用的 CPU 资源略多(可忽略不计),WebSocket 的延迟略低(相差 3 毫秒)。
对于每秒 10 万个事件来说,这种差异无关紧要。
资源利用率(生产,2025 年)
场景:实时仪表盘,10,000 个并发连接
上证综:
-
内存:约20MB(仅连接状态) -
CPU:空闲时 15%,负载时 35% -
网络:标准 HTTP -
缩放:水平(无状态,带背板)
WebSocket:
-
内存:约50MB(连接缓冲区+帧缓冲区) -
CPU:25% 空闲(ping/pong 帧),45% 负载下 -
网络:持久 TCP + WebSocket 协议开销 -
扩展性:需要粘性会话或消息背板
为什么会有这种差异?
SSE 就是 HTTP。没有帧掩码,没有协议协商,也没有 ping/pong 机制。
WebSocket 帧有开销:
[Frame Header (2-14 bytes)] [Payload]
每条消息都会被封装。客户端到服务器的消息会被屏蔽(异或运算会增加 CPU 开销)。
SSE 只写入文本:
data: {...}\n\n
无边框。无遮罩。极低的CPU占用率。
延迟深度解析
问:如果 WebSocket 速度快 3 毫秒,这重要吗?
答:几乎从不。
典型应用延迟预算:
User action: 0ms
↓
Frontend validation: 5ms
↓
Network RTT: 20-100ms (varies by location)
↓
Backend processing: 10-500ms (depends on query)
↓
Database query: 5-50ms
↓
Response render: 10ms
↓
Total: 50-665ms
SSE 和 WebSocket 之间只有 3 毫秒的差别?这简直是小巫见大巫。
当延迟至关重要时:
-
游戏(60 FPS = 每帧 16 毫秒预算) -
交易(微秒至关重要) -
VoIP/视频(对抖动敏感)
对于这些情况?请使用 WebSocket(或基于 UDP 的解决方案,例如 WebRTC、WebTransport)。
对于仪表盘、通知、信息流来说,3毫秒的延迟无关紧要。
为什么SSE在大多数应用中胜出
1. 这只是HTTP协议
SSE 运行在 80/443 端口。无需特殊防火墙规则,也无需代理配置。它在所有支持 HTTP 的地方都能正常工作。
WebSocket 需要协议升级:
GET /socket HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
某些企业防火墙会屏蔽Upgrade请求头。某些反向代理不支持 WebSocket。某些 CDN 存在问题。
SSE 协议运行良好。它基于 HTTP 协议。代理服务器可以识别它。CDN 可以缓存它(并添加正确的头部信息)。负载均衡器可以路由它。
真实案例(来自 Stack Overflow):
“我们部署了 WebSocket,在开发环境中运行完美。但在生产环境中,公司网络屏蔽了 ws:// 协议。我们花了两个星期进行调试。后来切换到 SSE,问题立即得到解决。”
2. 内置自动重连功能
SSE(EventSource API):
const eventSource = new EventSource('/stream');
// That's it. Browser handles reconnection automatically.
连接断开?浏览器等待 3 秒后重试。您无需进行任何操作。
WebSocket:
let ws;
let reconnectAttempts = 0;
const maxReconnectDelay = 30000;
function connect() {
ws = new WebSocket('ws://example.com/socket');
ws.onopen = () => {
reconnectAttempts = 0;
console.log('Connected');
};
ws.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
reconnectAttempts++;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(connect, delay);
};
ws.onerror = (error) => {
console.error('Error:', error);
ws.close();
};
}
connect();
你来编写重连逻辑。指数退避。最大尝试次数。抖动以防止出现群体攻击。
或者您可以使用 http://Socket.IO(会增加 40KB 的软件包大小),它会为您完成此操作。
SSE: 1 行代码。WebSocket
: 20 行以上代码或外部库。
3. HTTP/2 多路复用
还记得当年对SSE“6个连接数限制”的批评吗?
HTTP/1.1:浏览器限制每个域最多建立 6 个连接。
HTTP/2:一个 TCP 连接,通过多路复用实现无限流。
到 2026 年,HTTP/2 将无处不在:
-
Chrome:97% 的请求使用 HTTP/2 -
生产服务器:NGINX、Caddy、Cloudflare 默认都使用 HTTP/2。
通过 HTTP/2 进行 SSE 通信 = 无连接数限制。
你可以通过一个 TCP 连接传输 1000 个 SSE 流。高效、快速、开销低。
4. 支持 curl(调试)
上证综:
curl -N https://api.example.com/stream
data: {"message": "Update 1"}
data: {"message": "Update 2"}
^C
您可以使用 curl 调试 SSE 流。无需特殊工具,无需浏览器,只需 curl 即可。
WebSocket:
# Need wscat or similar
npm install -g wscat
wscat -c ws://example.com/socket
Connected (press CTRL+C to quit)
> {"type": "ping"}
< {"type": "pong"}
需要专用工具。操作起来比较复杂。
5. 对CDN友好
SSE 就是 HTTP。CDN理解 HTTP。
想要缓存 SSE 流?请设置标头:
res.setHeader('Cache-Control', 'public, max-age=1');
Cloudflare、Fastly 和 CloudFront 都能透明地处理 SSE。
WebSocket?大多数 CDN 将其视为特例。有些 CDN 完全不支持 WebSocket。配置起来也比较复杂。
实际生产用例
ChatGPT / OpenAI API (2025)
ChatGPT 如何传输回复:
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4',
messages: [...],
stream: true // Enable streaming
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
const parsed = JSON.parse(data);
console.log(parsed.choices[0].delta.content);
}
}
}
这就是SSE。OpenAI使用服务器发送事件(Server-Sent Events)来实现流式完成。
为什么?因为这是单向的。用户通过 POST 请求发送请求,AI 处理响应。SSE 完美解决了这个问题。
Shopify BFCM 实时地图(2022)
挑战:黑色星期五/网络星期一实时销售可视化。数百万用户同时观看实时销售数据。
解决方案: SSE
建筑学:
-
Flink 处理 Kafka 流(销售事件) -
汇总数据(按地区划分的销售额、热门产品) -
SSE服务器向前端推送更新 -
在 NGINX 负载均衡器后进行水平扩展
结果:
-
4天内处理了3230亿条事件 -
数百万个并发的SSE连接 - 全球延迟小于300毫秒
-
WebSocket 复杂度为零
为什么选择SSE?
“我们只需要服务器到客户端的数据传输。SSE 让我们完全消除了客户端轮询,并充分利用了现有的 HTTP 基础设施。”
http://Split.io 实时功能标志(2025)
应用场景:功能开关平台。客户需要即时更新功能开关状态。
规模:
- 每月发生1万亿起事件
- 平均全局延迟小于 300 毫秒
-
数百万个并发连接
技术: SSE
为什么不用WebSocket?
“标志位在服务器端更改。客户端只需监听。我们不需要双向通信。SSE 让我们能够在 WebSocket 规模下实现 HTTP 的简洁性。”
个人项目:实时日志流
场景:开源日志管理平台。用户可以实时查看到达的日志(类似于tail -f在浏览器中查看)。
要求:
-
超过1000个用户同时观看不同的日志流 -
从日志摄取到浏览器的延迟低于 50 毫秒 -
部署在每月 20 美元的服务器上
实现方式: PostgreSQL LISTEN/NOTIFY + SSE
// Backend: Listen to Postgres
const pgClient = new Client({ connectionString: DB_URL });
await pgClient.connect();
await pgClient.query(`LISTEN logs_${orgId}`);
pgClient.on('notification', (msg) => {
const log = JSON.parse(msg.payload);
// Push to connected SSE clients for this org
sseManager.broadcast(orgId, log);
});
// SSE endpoint
app.get('/api/logs/stream', (req, reply) => {
const { orgId } = req.user;
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // Disable nginx buffering
});
const clientId = generateId();
sseManager.addClient(orgId, clientId, reply.raw);
// Heartbeat every 30s
const heartbeat = setInterval(() => {
reply.raw.write(': heartbeat\n\n');
}, 30000);
req.raw.on('close', () => {
clearInterval(heartbeat);
sseManager.removeClient(orgId, clientId);
});
});
结果:
-
在 4 个虚拟 CPU 服务器上支持 1000 个并发连接 -
p50 延迟:45 毫秒,p95 延迟:120 毫秒 -
CPU 使用率:约 30% -
内存:约500MB -
无需任何 WebSocket 库。协议复杂度为零。
为什么选择SSE?
日志流从服务器端流向客户端。用户不会通过此流发送日志。如果需要上传日志,则需要单独发送 POST 请求。
SSE堪称完美。简单、快速、可扩展。
WebSocket 真正胜出的时候
说实话,WebSocket 并非总是多此一举。
何时使用 WebSocket:
1. 真正的双向通信
双方持续发送信息。
例如:
-
多人游戏(玩家持续输入 + 服务器更新) -
协作编辑(本地编辑 + 远程编辑同时进行) -
VoIP/视频通话信令
2. 低延迟至关重要
延迟要求低于10毫秒。
例如:
-
高频交易 -
FPS游戏(60+ FPS = <16ms预算) -
现场拍卖
3. 二进制数据
发送图像、音频、视频帧。
WebSocket 支持二进制:
ws.send(new Uint8Array([1, 2, 3, 4]));
SSE 仅支持文本格式。您需要对二进制数据进行 Base64 编码(会增加 33% 的开销)。
何时使用 SSE:
1. 服务器 → 仅客户端
数据单向流动。
例如:
-
仪表盘 -
通知 -
实时画面 -
AI 流式响应 -
原木尾矿 -
股票代码 -
构建状态 -
分析
2. 简洁至上
没有重连逻辑,没有帧处理,只有HTTP协议。
3. 适用范围广
企业防火墙、代理服务器、CDN,HTTP 都能正常工作。
4. 调试很重要
curl有效。浏览器开发者工具的网络选项卡清晰地显示了SSE流。
生产陷阱(以及如何解决它们)
1. Nginx 缓冲
问题: Nginx 默认会缓存响应,导致 SSE 事件卡住。
症状:事件以突发形式发生,而不是实时发生。
使固定:
location /api/stream {
proxy_pass http://backend;
proxy_buffering off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
或者设置响应头:
res.setHeader('X-Accel-Buffering', 'no');
2. 负载均衡器粘性会话
问题:后端服务器众多。客户端连接到服务器 A。服务器 A 崩溃。客户端重新连接到服务器 B。消息丢失。
解决方案:使用消息后端(Redis Pub/Sub、RabbitMQ、Kafka)。
建筑学:
Client 1 → LB → Server A ─┐
Client 2 → LB → Server B ─┼→ Redis Pub/Sub
Client 3 → LB → Server C ─┘
事件已发布 → Redis → 所有服务器 → 所有已连接的客户端
实现方式(Node.js + Redis):
const redis = require('redis');
const subscriber = redis.createClient();
const publisher = redis.createClient();
// Subscribe to channel
await subscriber.subscribe('updates');
subscriber.on('message', (channel, message) => {
// Broadcast to all SSE clients connected to THIS server
sseClients.forEach(client => {
client.write(`data: ${message}\n\n`);
});
});
// Publish event (from any server)
publisher.publish('updates', JSON.stringify({ data: 'New event' }));
现在可以横向扩展了。可以添加/移除服务器。客户端不会在意。
3. 心跳
问题:代理服务器会关闭空闲连接(通常为 60-120 秒)。
解决方案:每 30 秒发送一次心跳信号。
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n'); // Comment line, ignored by client
}, 30000);
req.on('close', () => clearInterval(heartbeat));
4. 身份验证
问题: EventSource 不支持自定义标头。
解决方案:
A) 使用查询参数:
const eventSource = new EventSource(`/stream?token=${authToken}`);
B) 使用 Cookie:
// Server sets cookie on login
res.cookie('auth', token, { httpOnly: true });
// EventSource automatically sends cookies
const eventSource = new EventSource('/stream');
C) 使用授权 URL(非标准但有效):
const eventSource = new EventSource(`https://${authToken}@api.example.com/stream`);
5. 并发连接数限制
问题: EventSource 计入浏览器连接限制(HTTP/1.1 上每个域 6 个连接)。
解决方案:
A) 使用 HTTP/2(最佳选择 - 无限流量)
B) 使用子域分片:
const shard = userId % 4;
const eventSource = new EventSource(`https://stream${shard}.example.com/events`);
C)关闭未使用的连接:
eventSource.close(); // When no longer needed
实现模式
模式 1:简单广播
使用场景:向所有客户端发送相同数据(股票行情、新闻推送)
const clients = new Set();
app.get('/stream', (req, reply) => {
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
});
clients.add(reply.raw);
req.raw.on('close', () => {
clients.delete(reply.raw);
});
});
// Broadcast function
function broadcast(data) {
const message = `data: ${JSON.stringify(data)}\n\n`;
clients.forEach(client => client.write(message));
}
// Example: Update every second
setInterval(() => {
broadcast({ timestamp: Date.now(), price: Math.random() * 100 });
}, 1000);
模式 2:按用户划分的流
使用场景:针对不同用户提供不同的数据(通知、个性化推送)
const userClients = new Map(); // userId -> Set of connections
app.get('/stream', (req, reply) => {
const userId = req.user.id;
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
});
if (!userClients.has(userId)) {
userClients.set(userId, new Set());
}
userClients.get(userId).add(reply.raw);
req.raw.on('close', () => {
const clients = userClients.get(userId);
clients.delete(reply.raw);
if (clients.size === 0) {
userClients.delete(userId);
}
});
});
// Send to specific user
function sendToUser(userId, data) {
const clients = userClients.get(userId);
if (!clients) return;
const message = `data: ${JSON.stringify(data)}\n\n`;
clients.forEach(client => client.write(message));
}
模式 3:事件类型
使用场景:同一数据流上的多种事件类型(日志+指标+警报)
app.get('/stream', (req, reply) => {
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
});
// Named events
function sendEvent(eventType, data) {
reply.raw.write(`event: ${eventType}\n`);
reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
}
sendEvent('log', { level: 'info', message: 'App started' });
sendEvent('metric', { cpu: 45, memory: 2048 });
sendEvent('alert', { severity: 'high', message: 'Disk full' });
});
// Client
const eventSource = new EventSource('/stream');
eventSource.addEventListener('log', (e) => {
console.log('Log:', JSON.parse(e.data));
});
eventSource.addEventListener('metric', (e) => {
console.log('Metric:', JSON.parse(e.data));
});
eventSource.addEventListener('alert', (e) => {
console.log('Alert:', JSON.parse(e.data));
});
迁移:WebSocket → SSE
场景:你已经有了 WebSocket,现在想简化它。该怎么做?
第一步:识别沟通模式
审核您的 WebSocket 使用情况:
// What messages does CLIENT send?
ws.send({ type: 'subscribe', channel: 'updates' });
ws.send({ type: 'ping' });
// What messages does SERVER send?
ws.send({ type: 'update', data: {...} });
ws.send({ type: 'pong' });
如果客户端仅发送:
-
订阅/配置(连接开始时) -
Ping/心跳(保持连接)
您可以使用 SSE + HTTP POST。
步骤 2:将客户端→服务器迁移到 HTTP
WebSocket:
ws.send({ type: 'subscribe', channel: 'metrics' });
SSE 同等产品:
// Subscribe via query param or POST
const eventSource = new EventSource('/stream?channel=metrics');
// OR
await fetch('/subscribe', {
method: 'POST',
body: JSON.stringify({ channel: 'metrics' })
});
const eventSource = new EventSource('/stream');
步骤 3:将 WebSocket 服务器替换为 SSE
之前(WebSocket):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
// Handle client message
});
setInterval(() => {
ws.send(JSON.stringify({ data: 'update' }));
}, 1000);
});
之后(SSE):
app.get('/stream', (req, reply) => {
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
});
const interval = setInterval(() => {
reply.raw.write(`data: ${JSON.stringify({ data: 'update' })}\n\n`);
}, 1000);
req.raw.on('close', () => clearInterval(interval));
});
步骤 4:更新客户端
之前(WebSocket):
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = (e) => console.log(JSON.parse(e.data));
之后(SSE):
const es = new EventSource('http://localhost:3000/stream');
es.onmessage = (e) => console.log(JSON.parse(e.data));
结果:代码更简洁,功能不变。
底线
对于 95% 的实时应用而言,SSE 是更好的选择。
为什么:
-
更简单(仅支持HTTP) -
更容易调试(curl 可以正常工作) -
自动重连(内置) -
在所有地方都能用(没有代理问题) -
性能相当(适用于大多数使用场景) -
水平缩放(无状态,带背板)
何时使用 WebSocket:
-
真正的双向(双方频繁发送) -
二进制数据(音频/视频帧) -
需要超低延迟(<10毫秒) -
游戏、协作编辑、VoIP
默认使用 SSE 协议。仅在需要使用 WebSocket 的特定功能时才使用它。
最好的技术并非功能最强大的,而是最简单且能解决你问题的技术。
对于服务器到客户端的流媒体传输,那就是服务器发送事件。
简单。可靠。快速。
有时候,看似平庸的技术才是正确的技术。
关注「索引目录」公众号,获取更多干货。

