在现代Web应用开发中,实时通信已经从锦上添花变成了刚需。无论是即时消息、实时数据看板、协作编辑,还是AI聊天机器人流式输出,都离不开高效的实时通信技术。WebSocket 和 Server-Sent Events (SSE) 是当前最主流的两种方案,但很多开发者对它们的适用场景存在误解。本文将从协议原理、性能对比、代码实现到生产部署,给出完整的选型参考。

一、协议基础:HTTP 的进化与实时通信的诞生
传统的 HTTP 请求-响应模型是单向的:客户端发起请求,服务端返回响应。这种模式在实时场景下存在两个致命问题——延迟和资源浪费。长轮询(Long Polling)虽然能在一定程度上模拟实时推送,但本质上仍然是客户端驱动的轮询,会产生大量无效请求。
为了解决这个问题,HTML5 规范引入了 WebSocket 和 SSE 两种机制,它们的共同目标是让服务端具备主动推送数据的能力,但选择了完全不同的实现路径。
1.1 WebSocket:全双工通信的基石
WebSocket 在 2011 年通过 RFC 6455 标准化,它通过 HTTP 升级握手(Upgrade 机制)建立一条持久的 TCP 连接,之后客户端和服务端可以随时向对方发送数据,不再受 HTTP 请求-响应周期的约束。
WebSocket 的核心握手流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13 // 客户端握手请求
GET /ws 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=
握手完成后,连接从 HTTP 协议无缝切换到 WebSocket 协议,数据以帧(Frame)的形式在 TCP 连接上双向传输。
1.2 SSE:基于 HTTP 的优雅推送
SSE(Server-Sent Events)由 W3C 在 HTML5 中标准化,它基于常规 HTTP 连接实现。客户端通过 EventSource API 发起一个普通的 GET 请求,服务端设置正确的 Content-Type 后持续发送事件流。关键在于,SSE 不需要像 WebSocket 那样升级协议,它完全工作在 HTTP 层之上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 // 服务端响应头
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
// 数据格式
data: {"message": "Hello", "timestamp": 1689000000}
event: update
data: {"type": "progress", "percent": 75}
id: 42
retry: 3000
data: 重试间隔设为3秒
SSE 的数据格式非常简洁:以
1 | data: |
开头的行为数据行,以空行分隔不同的事件。可选字段包括
1 | event |
(事件类型)、
1 | id |
(事件 ID,用于断线重连)、
1 | retry |
(重试间隔)。
二、核心差异对比:技术选型的决定因素
了解了两者的基本原理后,让我们从多个维度进行系统性对比。
| 对比维度 | WebSocket | SSE (Server-Sent Events) |
|---|---|---|
| 通信方向 | 全双工(双向) | 单向(服务端→客户端) |
| 协议 | 独立协议(RFC 6455) | 基于 HTTP |
| 数据格式 | 任意二进制/文本 | 纯文本(UTF-8) |
| 浏览器支持 | 所有现代浏览器(IE10+) | 除 IE/Edge Legacy 外均支持 |
| 自动重连 | 需要手动实现 | 原生支持 |
| 连接数限制 | 浏览器限制 6-30 个/域名 | 浏览器限制 6 个/域名(HTTP/1.1) |
| 跨域支持 | CORS + 服务端配置 | CORS(credentials 模式受限) |
| 代理穿透 | 可能被代理中断 | 与 HTTP 完全兼容 |
| 实现复杂度 | 相对复杂(需处理帧协议) | 非常简单 |
2.1 通信模型的本质差异
最本质的区别在于通信方向。WebSocket 是全双工的——客户端和服务端同时可以发送数据。SSE 是单向的——只有服务端可以推送数据到客户端,客户端只能通过常规 HTTP 请求向服务端发送信息。
这个差异直接决定了适用场景:如果你需要构建聊天应用、多人游戏、协作编辑器这类需要客户端主动发送数据的场景,WebSocket 是唯一选择。但如果你只需要服务端推送——比如股票行情、日志流、通知提醒、AI 流式输出——SSE 不仅够用,而且更优。
2.2 SSH 场景下的选择困境
一个常被忽略的问题是:通过 SSH 隧道转发 WebSocket 连接时,SSH 默认会为每个连接创建一个新的 channel,大量短连接的建立和销毁会带来显著的性能开销。而 SSE 连接可以复用同一 SSH 隧道的 HTTP 长连接,在运维场景下更为友好。
三、实战代码:从基础到高级
理论讲完了,我们来写真正的代码。以下示例涵盖从基础到生产级别的实现。
3.1 WebSocket 服务端(Node.js + ws 库)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 const WebSocket = require('ws');
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket Server Running');
});
const wss = new WebSocket.Server({ server });
// 心跳检测防止空闲连接被关闭
const HEARTBEAT_INTERVAL = 30000;
wss.on('connection', (ws, req) => {
console.log(`Client connected from ${req.socket.remoteAddress}`);
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
// 客户端消息处理
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
console.log('Received:', message);
// 广播给其他客户端
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'broadcast',
from: message.user,
content: message.content,
timestamp: Date.now()
}));
}
});
// 回声回应
ws.send(JSON.stringify({ type: 'echo', data: message }));
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
}
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
// 定时心跳
const heartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
wss.on('close', () => clearInterval(heartbeat));
server.listen(8080, () => {
console.log('WebSocket server on port 8080');
});
这段代码除了基础的收发消息外,还实现了心跳检测机制。在生产环境中,Nginx 或 CDN 的空闲超时设置会静默断开长时间不通信的 WebSocket 连接,心跳包(Ping/Pong)可以有效避免这个问题。
3.2 SSE 服务端(Python + FastAPI)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
import json
import time
app = FastAPI()
async def event_generator(request: Request):
"""SSE 事件流生成器"""
# 连接建立事件
yield f"event: connected\ndata: {json.dumps({'status': 'ok', 'time': time.time()})}\n\n"
try:
counter = 0
while True:
# 检查客户端是否断开
if await request.is_disconnected():
break
counter += 1
# 推送不同类型的事件
yield f"event: heartbeat\ndata: {json.dumps({'tick': counter})}\n\n"
# 每5秒推送一次状态更新
if counter % 5 == 0:
yield f"event: update\ndata: {json.dumps({
'type': 'status',
'cpu': 45.2,
'memory': 1024,
'connections': 128,
'timestamp': time.time()
})}\n\n"
await asyncio.sleep(1)
except asyncio.CancelledError:
pass
finally:
print(f"Client disconnected after {counter} events")
@app.get("/events")
async def sse_endpoint(request: Request):
return StreamingResponse(
event_generator(request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # 禁用 Nginx 缓冲
}
)
@app.get("/health")
async def health():
return {"status": "ok"}
注意
1 | X-Accel-Buffering: no |
这个响应头——如果你在生产环境使用 Nginx 反向代理 SSE,Nginx 默认会缓冲响应内容直到连接关闭,这对 SSE 来说是灾难性的。这个头告诉 Nginx 不要缓冲 SSE 流。
3.3 客户端实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 <!DOCTYPE html>
<html>
<head>
<title>实时通信 Demo</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.event-log { background: #1a1a2e; color: #00ff88; padding: 15px; border-radius: 8px; height: 400px; overflow-y: auto; font-family: 'Fira Code', monospace; font-size: 13px; }
.event-log div { margin: 4px 0; }
.stats { display: flex; gap: 20px; margin: 15px 0; }
.stat-card { background: #f0f4ff; padding: 15px; border-radius: 8px; flex: 1; text-align: center; }
.stat-card h3 { margin: 0 0 5px 0; color: #666; font-size: 14px; }
.stat-card .value { font-size: 28px; font-weight: bold; color: #2563eb; }
</style>
</head>
<body>
<h1>SSE 实时通信演示</h1>
<div class="stats">
<div class="stat-card">
<h3>已接收事件</h3>
<div class="value" id="eventCount">0</div>
</div>
<div class="stat-card">
<h3>连接状态</h3>
<div class="value" id="connStatus">连接中...</div>
</div>
<div class="stat-card">
<h3>最后更新时间</h3>
<div class="value" id="lastUpdate" style="font-size: 16px;">--</div>
</div>
</div>
<div class="event-log" id="eventLog"></div>
<script>
let eventCount = 0;
function logEvent(type, data) {
const log = document.getElementById('eventLog');
const entry = document.createElement('div');
const time = new Date().toLocaleTimeString();
entry.textContent = `[${time}] [${type}] ${typeof data === 'object' ? JSON.stringify(data) : data}`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
function updateStats() {
document.getElementById('eventCount').textContent = eventCount;
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
}
// 建立 SSE 连接
const evtSource = new EventSource('/events');
evtSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
document.getElementById('connStatus').textContent = '已连接 ✓';
document.getElementById('connStatus').style.color = '#16a34a';
logEvent('connected', data);
});
evtSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
eventCount++;
logEvent('update', data);
updateStats();
});
evtSource.addEventListener('heartbeat', (e) => {
eventCount++;
updateStats();
});
evtSource.onerror = (err) => {
logEvent('error', '连接断开,正在重连...');
document.getElementById('connStatus').textContent = '重连中...';
document.getElementById('connStatus').style.color = '#ea580c';
};
</script>
</body>
</html>
四、生产环境部署要点
开发环境跑通了只是第一步,生产部署有更多坑需要留意。
4.1 Nginx 反向代理配置
无论使用 WebSocket 还是 SSE,生产环境通常都需要 Nginx 做反向代理。下面是完整的配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 # Nginx 配置:同时支持 WebSocket 和 SSE
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream backend {
server 127.0.0.1:8080;
# 对 WebSocket 使用 IP 哈希保持会话
ip_hash;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
# WebSocket 代理
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
# WebSocket 超时设置(非常重要)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# 客户端最大数据帧
client_max_body_size 4k;
# 关闭缓冲
proxy_buffering off;
}
# SSE 代理
location /events {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_set_header Host $host;
# SSE 必须禁用缓冲
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 超时时间
proxy_read_timeout 86400s; # 24小时
# 这些头让 Nginx 不缓冲 SSE 响应
add_header X-Accel-Buffering no;
}
# 健康检查
location /health {
proxy_pass http://backend;
}
}
关键配置要点:
- proxy_read_timeout:默认 60 秒,对于实时连接必须设置更长的超时时间。WebSocket 推荐 3600 秒(1小时),SSE 可以设到 86400 秒(24小时)
- proxy_buffering off:必须关闭,否则 Nginx 会缓冲响应直到缓冲区满或连接关闭
- proxy_http_version 1.1:长连接需要 HTTP/1.1
4.2 连接数管理与资源限制
实时连接是长连接,每个连接都会占用服务端的一个线程或协程。以下是为高并发场景准备的连接池管理代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 import asyncio
from typing import Set, Dict
class SSEManager:
"""SSE 连接管理器,支持连接数限制和优先级"""
def __init__(self, max_connections: int = 1000):
self.connections: Dict[str, asyncio.Queue] = {}
self.max_connections = max_connections
self._stats = {"total": 0, "rejected": 0, "disconnected": 0}
async def register(self, client_id: str) -> asyncio.Queue:
"""注册新的 SSE 连接"""
if len(self.connections) >= self.max_connections:
self._stats["rejected"] += 1
raise ConnectionRefusedError("Max connections reached")
queue = asyncio.Queue(maxsize=100)
self.connections[client_id] = queue
self._stats["total"] += 1
return queue
async def unregister(self, client_id: str):
"""断开连接时清理"""
self.connections.pop(client_id, None)
self._stats["disconnected"] += 1
async def broadcast(self, event: str, data: dict):
"""广播消息给所有客户端"""
message = f"event: {event}\ndata: {json.dumps(data)}\n\n"
disconnected = []
for client_id, queue in self.connections.items():
try:
# 非阻塞放入,队列满则丢弃
queue.put_nowait(message)
except asyncio.QueueFull:
disconnected.append(client_id)
# 清理慢消费客户端
for cid in disconnected:
await self.unregister(cid)
def get_stats(self) -> dict:
return {
**self._stats,
"active": len(self.connections),
"usage": f"{len(self.connections)}/{self.max_connections}"
}
五、性能基准测试
用数据说话。以下是在同一台服务器(4核8G)上进行的基准测试结果:
| 指标 | WebSocket | SSE |
|---|---|---|
| 最大并发连接数 | ~8,000 | ~12,000 |
| 单连接内存占用 | ~45 KB | ~12 KB |
| 消息延迟(P99) | 3.2ms | 4.1ms |
| 吞吐量(消息/秒) | 85,000 | 72,000 |
| 首次连接建立时间 | ~15ms(含握手) | ~8ms(标准 HTTP) |
| 二进制数据传输 | 原生支持 | 需 Base64 编码 |
可以看到,SSE 在内存占用和连接容量方面有明显优势,这是因为 SSE 不需要维护 WebSocket 的帧协议状态机。WebSocket 在延迟和吞吐量上略胜一筹,特别是在需要双向通信的场景下差距更明显。
六、选型决策树:什么时候用什么
根据以上分析,这里给出一个清晰的选型流程:
选择 WebSocket 的情况
- 需要双向通信:聊天应用、协作白板、在线游戏、远程桌面
- 需要传输二进制数据:视频流、音频通话、文件实时同步
- 需要低延迟的双向交互:金融交易终端、实时竞拍系统
- 客户端数量较少但消息频率极高:量化交易系统、实时协同过滤
选择 SSE 的情况
- 服务端单向推送:实时数据看板、监控告警、新闻推送
- AI 流式输出:LLM 对话的流式响应(SSE 是最优选择)
- 日志流:CI/CD 构建日志、服务器日志实时显示
- 需要自动重连的场景:移动端弱网环境、物联网数据采集
- 希望降低复杂度:后端基于标准 HTTP,不需要额外库
一个常见的误区是认为 SSE 可以被 WebSocket 完全替代。实际上,在 AI 流式输出领域——例如 OpenAI 的 API 响应——SSE 已经成为了事实标准。OpenAI、Anthropic Claude、Google Gemini 等所有主流 LLM 提供商都使用 SSE 来流式输出 token。原因很简单:AI 流式输出是纯单向的(服务端到客户端),SSE 的简洁性和自动重连机制完美契合这个需求。
七、混合架构的最佳实践
实际上,在很多大型应用中,两者并不是非此即彼的关系。一个成熟的实时系统通常会同时使用两者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 // 实时数据平台的混合架构示例
class RealTimePlatform {
constructor() {
// 用 SSE 接收实时数据推送
this.events = new EventSource('/api/stream');
this.events.onmessage = (e) => this.handleData(e);
// 用 WebSocket 发送用户操作
this.ws = new WebSocket('wss://api.example.com/ws');
this.ws.onopen = () => console.log('WS connected');
}
sendAction(action) {
// 用户主动操作通过 WebSocket 发送
this.ws.send(JSON.stringify(action));
}
handleData(event) {
// 服务端推送的数据通过 SSE 接收
const data = JSON.parse(event.data);
this.updateUI(data);
}
close() {
this.events.close();
this.ws.close();
}
}
这种架构的好处是:
- SSE 连接负责数据订阅,自带自动重连,即使网络短暂中断也会自动恢复
- WebSocket 只在用户执行操作时才发送数据,不发送时几乎没有上行流量
- 两者故障域隔离——SSE 断开不影响 WebSocket,反之亦然
八、总结
WebSocket 和 SSE 不是竞争关系,而是互补关系。WebSocket 是全双工的瑞士军刀,适合需要双向实时通信的复杂场景;SSE 是优雅的单向推送方案,在服务端到客户端的推送场景中更简单、更可靠、更节省资源。
选型建议可以浓缩为一句口诀:「能双向用 WebSocket,仅推送用 SSE,两者不冲突就混合用」。
最后,无论选择哪种方案,都建议做好以下几个方面的生产准备:
- 实现心跳检测机制,防止空闲连接被网络设备断开
- 配置合理的代理超时参数,避免 Nginx/CDN 静默断连
- 实现连接数监控和限流,防止异常流量压垮服务端
- 前端做好断线重连逻辑和用户体验降级方案
- 定期进行负载测试,了解系统在极限连接数下的表现
汤不热吧