WebSocket 详解与 SSE 对比
b1babo
2026年4月18日
2026年4月18日
WebSocket 详解与 SSE 对比
目录
WebSocket 基础
什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它于 2011 年被 IETF 标准化为 RFC 6455。
核心特点
| 特性 | 说明 |
|---|---|
| 全双工通信 | 客户端和服务器可以同时发送消息 |
| 持久连接 | 建立连接后保持打开状态 |
| 低开销 | 数据包头部小,减少传输开销 |
| 实时性 | 消息可以立即推送,无需轮询 |
| 跨域支持 | 支持跨域通信 |
基本原理
┌─────────────────────────────────────────────────────────────┐
│ WebSocket 握手过程 │
└─────────────────────────────────────────────────────────────┘
客户端 服务器
│ │
│ ───────── HTTP 握手请求 ──────────────> │
│ GET /ws HTTP/1.1 │
│ Host: server.com │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== │
│ Sec-WebSocket-Version: 13 │
│ │
│ <──────── HTTP 握手响应 ───────────────── │
│ HTTP/1.1 101 Switching Protocols │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= │
│ │
│ ═════════ WebSocket 连接建立 ══════════ │
│ │
│ <────────── 二进制帧消息 ────────────── │
│ Frame 1: "Hello" │
│ │
│ ─────────── 二进制帧消息 ─────────────> │
│ Frame 2: "Hi there" │
│ │
│ <────────── 二进制帧消息 ────────────── │
│ Frame 3: "How are you?" │
│ │
│ ─────────── 二进制帧消息 ─────────────> │
│ Frame 4: {"status": "ok"} │
│ │
│ ═════════════ 持续双向通信 ═════════════ │WebSocket 详解
协议握手
客户端请求
服务器响应
握手关键点:
- 状态码
101表示协议切换 Sec-WebSocket-Accept是对客户端 Key 的确认- 握手完成后,连接从 HTTP 切换到 WebSocket 协议
数据帧格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4bits) |A| (7bits) | (16/64 bits) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+字段说明:
| 字段 | 位 | 说明 |
|---|---|---|
| FIN | 1 | 是否为最后一帧 |
| RSV1-3 | 3 | 保留位,通常为0 |
| Opcode | 4 | 操作码(0x1=文本, 0x2=二进制, 0x8=关闭) |
| MASK | 1 | 是否使用掩码(客户端必须为1) |
| Payload Length | 7/7+16/7+64 | 载荷长度 |
| Masking Key | 32 | 掩码密钥(客户端发送时必需) |
| Payload Data | 可变 | 实际数据 |
Opcode 类型
| Opcode | 名称 | 说明 |
|---|---|---|
| 0x0 | Continuation | 继续帧 |
| 0x1 | Text | UTF-8 文本帧 |
| 0x2 | Binary | 二进制帧 |
| 0x8 | Close | 关闭连接 |
| 0x9 | Ping | 心跳检测 |
| 0xA | Pong | 心跳响应 |
心跳机制
// 客户端发送 Ping
ws.ping();
// 服务器自动回复 Pong
// 或手动处理
ws.on('pong', () => {
console.log('Received pong');
});
// 服务端实现(Node.js)
const interval = setInterval(() => {
ws.ping();
}, 30000);
ws.on('pong', () => {
clearInterval(interval);
});连接状态
switch (ws.readyState) {
case WebSocket.CONNECTING: // 0 - 正在连接
console.log('Connecting...');
break;
case WebSocket.OPEN: // 1 - 连接已打开
console.log('Connected');
break;
case WebSocket.CLOSING: // 2 - 正在关闭
console.log('Closing...');
break;
case WebSocket.CLOSED: // 3 - 连接已关闭
console.log('Closed');
break;
}WebSocket vs SSE 完整对比
核心差异
┌─────────────────────────────────────────────────────────────────────────┐
│ WebSocket vs SSE 对比 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ WebSocket │ SSE │
│ ───────────────────────── │ ───────────────────────── │
│ │
│ ████████████ │ ██████████ │
│ ██ ██ ← │ ██ ██ ← │
│ ██ SERVER ██ ← 双向 │ ██ SERVER ██ ← 单向 │
│ ██ ██ → │ ██ ██ │
│ ████████████ → │ ████████████ │
│ │
│ • 全双工通信 │ • 单向通信(服务器→客户端) │
│ • 二进制帧格式 │ • 文本格式 │
│ • 需要协议升级 │ • 基于HTTP │
│ • 手动实现重连 │ • 自动重连 │
│ │
└─────────────────────────────────────────────────────────────────────────┘详细对比表
| 对比维度 | WebSocket | SSE |
|---|---|---|
| 通信方向 | 双向(全双工) | 单向(服务器→客户端) |
| 协议基础 | HTTP + WebSocket协议 | 纯HTTP |
| 连接方式 | 协议升级(101状态码) | 持久HTTP连接 |
| 数据格式 | 二进制帧 | 文本(UTF-8) |
| 浏览器API | WebSocket | EventSource |
| 重连机制 | 需手动实现 | 内置自动重连 |
| 二进制数据 | 原生支持 | 需Base64编码 |
| 断线续传 | 不支持 | 支持(通过Last-Event-ID) |
| 实现复杂度 | 中等 | 简单 |
| 服务器负载 | 较高(保持双向连接) | 较低 |
| 防火墙/代理 | 可能有兼容性问题 | 兼容性更好 |
| 适用场景 | 聊天、游戏、协作 | 推送、流式输出 |
| 性能开销 | 较低(帧头部小) | 稍高(HTTP头) |
性能对比
连接建立
WebSocket:
1. TCP 三次握手
2. HTTP 请求
3. 101 响应
= 约 2-3 RTT
SSE:
1. TCP 三次握手
2. HTTP 请求
3. 200 响应
= 约 2 RTT
差异:WebSocket 多一次协议升级往返数据传输
WebSocket 帧:
+-----+------+-----+
| HEAD| MASK | DATA |
+-----+------+-----+
2-14字节 4字节 变长
总开销:6-18字节
SSE 数据:
+-------+----------+
| data: | content |\n\n
+-------+----------+
6字节 变长 2字节
总开销:8+字节
结论:WebSocket 帧开销更小,适合高频小消息兼容性对比
| 环境 | WebSocket | SSE |
|---|---|---|
| 现代浏览器 | ✅ 完全支持 | ✅ 完全支持 |
| IE浏览器 | IE10+ | 不支持 |
| 移动浏览器 | ✅ 支持 | ✅ 支持 |
| Node.js | ✅ 原生支持 | ✅ 可实现 |
| Python | ✅ 多库支持 | ✅ 可实现 |
| 代理服务器 | 需配置 | 一般兼容 |
| 防火墙 | 可能被阻断 | 通常通过 |
使用场景分析
WebSocket 最佳场景
1. 实时聊天应用
// 特点:双向实时通信
const ws = new WebSocket('wss://chat.example.com');
ws.send(JSON.stringify({
type: 'message',
content: 'Hello',
userId: '123'
}));
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
displayMessage(msg);
};为什么选 WebSocket:
- 用户需要实时接收消息
- 用户需要实时发送消息
- 需要知道用户在线状态
- 需要双向确认(已读回执)
2. 多人在线游戏
// 游戏状态同步
ws.send(JSON.stringify({
type: 'move',
x: player.x,
y: player.y,
timestamp: Date.now()
}));
// 实时接收其他玩家位置
ws.onmessage = (event) => {
const gameData = JSON.parse(event.data);
updateGameState(gameData);
};为什么选 WebSocket:
- 低延迟要求(毫秒级)
- 高频率数据交换(每秒多次)
- 双向通信(操作同步)
- 二进制数据支持(游戏状态)
3. 实时协作编辑
// 操作同步(类似Google Docs)
ws.send(JSON.stringify({
type: 'edit',
position: {line: 5, ch: 10},
content: 'added text',
version: documentVersion
}));
// 接收其他人的编辑
ws.onmessage = (event) => {
const edit = JSON.parse(event.data);
applyEdit(edit);
};为什么选 WebSocket:
- 多人同时编辑
- 操作需要立即同步
- 冲突解决需要协商
- 需要版本控制机制
4. 实时交易系统
// 买卖订单
ws.send(JSON.stringify({
type: 'order',
action: 'buy',
price: 100.50,
quantity: 10
}));
// 实时行情
ws.onmessage = (event) => {
const quote = JSON.parse(event.data);
updatePrice(quote);
};为什么选 WebSocket:
- 价格实时更新
- 订单需要即时确认
- 延迟直接影响收益
- 需要可靠的双向通信
SSE 最佳场景
1. 大模型流式输出
async def stream_llm_response():
async for token in llm.generate(prompt):
yield f"data: {json.dumps({'token': token})}\n\n"
yield "data: [DONE]\n\n"为什么选 SSE:
- 单向数据流(服务器→客户端)
- 不需要客户端频繁发送数据
- 实现简单,代码清晰
- 与HTTP生态兼容
2. 服务器推送通知
const eventSource = new EventSource('/notifications');
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
};为什么选 SSE:
- 只需要推送通知
- 客户端不需要回复
- 自动重连机制
- 实现比WebSocket简单
3. 实时数据更新(股票、比分)
const scores = new EventSource('/live-scores');
scores.onmessage = (event) => {
const score = JSON.parse(event.data);
updateScoreBoard(score);
};为什么选 SSE:
- 单向数据推送
- 不需要客户端确认
- 可以缓存在代理服务器
- 自动重连保证可靠性
4. 日志流、监控数据
const logs = new EventSource('/logs/stream');
logs.addEventListener('error', (e) => {
console.error('Log error:', e.data);
});
logs.addEventListener('warning', (e) => {
console.warn('Log warning:', e.data);
});为什么选 SSE:
- 大量单向日志数据
- 支持事件类型分类
- 可以断线续传
- 文本格式便于阅读
场景选择决策树
┌─────────────────────────────────────────────────────────────────┐
│ 选择 WebSocket 或 SSE │
└─────────────────────────────────────────────────────────────────┘
需要客户端频繁发送数据?
│
├─ 是 → 使用 WebSocket
│ (聊天、游戏、协作编辑)
│
└─ 否 → 服务器单向推送?
│
├─ 是 → 使用 SSE
│ (通知、日志、流式输出)
│
└─ 否 → 考虑其他方案
(轮询、长轮询)
数据传输频率?
│
├─ 高频(每秒多次)→ WebSocket
│ (游戏、实时交易)
│
└─ 低频(每秒几次)→ SSE 或 轮询
(通知、更新)
需要二进制数据?
│
├─ 是 → WebSocket
│
└─ 否 → SSE 或 WebSocket 均可
浏览器兼容性要求?
│
├─ 需要支持旧IE → WebSocket(IE10+)
│
└─ 现代浏览器 → SSE 更简单代码实现对比
FastAPI 实现
WebSocket 实现
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
import json
app = FastAPI()
@app.get("/")
async def get_client():
return HTMLResponse("""
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
</head>
<body>
<h1>WebSocket 聊天室</h1>
<input id="message" type="text" placeholder="输入消息">
<button onclick="send()">发送</button>
<div id="output"></div>
<script>
const ws = new WebSocket('ws://localhost:8000/ws');
const output = document.getElementById('output');
ws.onopen = () => {
output.innerHTML += '<p>连接已建立</p>';
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
output.innerHTML += `<p>${data.type}: ${data.content}</p>`;
};
ws.onerror = (error) => {
output.innerHTML += '<p>连接错误</p>';
};
ws.onclose = () => {
output.innerHTML += '<p>连接已关闭</p>';
};
function send() {
const input = document.getElementById('message');
ws.send(JSON.stringify({
type: 'message',
content: input.value
}));
input.value = '';
}
</script>
</body>
</html>
""")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
# 接收客户端消息
data = await websocket.receive_text()
message = json.loads(data)
# 处理消息
response = {
"type": "echo",
"content": f"收到: {message['content']}"
}
# 发送响应
await websocket.send_json(response)
except Exception as e:
print(f"Error: {e}")
finally:
await websocket.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)SSE 实现
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, HTMLResponse
import asyncio
import json
app = FastAPI()
@app.get("/")
async def get_client():
return HTMLResponse("""
<!DOCTYPE html>
<html>
<head>
<title>SSE Events</title>
</head>
<body>
<h1>SSE 事件流</h1>
<button onclick="startStream()">开始接收</button>
<button onclick="stopStream()">停止接收</button>
<div id="output"></div>
<script>
let eventSource = null;
function startStream() {
eventSource = new EventSource('/stream');
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
stopStream();
return;
}
const data = JSON.parse(event.data);
const output = document.getElementById('output');
output.innerHTML += `<p>${data.content}</p>`;
};
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
};
}
function stopStream() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
}
</script>
</body>
</html>
""")
async def generate_events():
"""生成SSE事件流"""
messages = [
"欢迎使用 SSE",
"这是第一条消息",
"这是第二条消息",
"这是第三条消息",
"流式输出结束"
]
for i, msg in enumerate(messages):
data = {
"id": i,
"content": msg,
"timestamp": asyncio.get_event_loop().time()
}
yield f"data: {json.dumps(data)}\n\n"
await asyncio.sleep(1)
# 发送结束标记
yield "data: [DONE]\n\n"
@app.get("/stream")
async def stream_events():
return StreamingResponse(
generate_events(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)Python 客户端对比
WebSocket 客户端
import asyncio
import websockets
import json
async def websocket_client():
uri = "ws://localhost:8000/ws"
async with websockets.connect(uri) as websocket:
# 发送消息
message = {"type": "message", "content": "Hello Server"}
await websocket.send(json.dumps(message))
# 接收响应
while True:
response = await websocket.recv()
data = json.loads(response)
print(f"收到: {data}")
# 可以继续发送
await asyncio.sleep(2)
await websocket.send(json.dumps({
"type": "message",
"content": "Keep alive"
}))
# 运行客户端
# asyncio.run(websocket_client())SSE 客户端
import asyncio
import aiohttp
import json
async def sse_client():
url = "http://localhost:8000/stream"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
async for line in response.content:
line = line.decode('utf-8').strip()
if not line:
continue
if line.startswith('data: '):
data = line[6:] # 去掉 "data: " 前缀
if data == '[DONE]':
print("流结束")
break
try:
event = json.loads(data)
print(f"收到事件: {event}")
except json.JSONDecodeError:
print(f"原始数据: {data}")
# 运行客户端
# asyncio.run(sse_client())Node.js 实现对比
WebSocket 服务器
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('客户端已连接');
// 发送欢迎消息
ws.send(JSON.stringify({
type: 'welcome',
content: '连接成功'
}));
// 接收消息
ws.on('message', (message) => {
const data = JSON.parse(message);
console.log('收到:', data);
// 广播给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'broadcast',
content: data.content
}));
}
});
});
// 处理断开
ws.on('close', () => {
console.log('客户端已断开');
});
});
server.listen(8080, () => {
console.log('WebSocket 服务器运行在端口 8080');
});SSE 服务器
const http = require('http');
const clients = new Set();
const server = http.createServer((req, res) => {
if (req.url === '/stream') {
// 设置 SSE 响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
});
// 添加到客户端集合
clients.add(res);
// 发送初始消息
res.write(`data: ${JSON.stringify({type: 'connected', content: '已连接'})}\n\n`);
// 处理客户端断开
req.on('close', () => {
clients.delete(res);
console.log('客户端断开连接');
});
} else {
// 返回简单的 HTML 客户端
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>SSE Demo</title></head>
<body>
<h1>SSE 示例</h1>
<div id="output"></div>
<script>
const eventSource = new EventSource('/stream');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
document.getElementById('output').innerHTML +=
'<p>' + data.content + '</p>';
};
</script>
</body>
</html>
`);
}
});
// 定时广播消息给所有客户端
setInterval(() => {
const message = {
type: 'update',
content: `服务器时间: ${new Date().toLocaleTimeString()}`
};
clients.forEach((res) => {
res.write(`data: ${JSON.stringify(message)}\n\n`);
});
}, 1000);
server.listen(8080, () => {
console.log('SSE 服务器运行在端口 8080');
});实现复杂度对比
| 功能 | WebSocket | SSE |
|---|---|---|
| 服务端实现 | 中等复杂度 | 简单 |
| 客户端实现 | 中等复杂度 | 简单(浏览器) |
| 连接管理 | 需要手动管理 | 自动处理 |
| 重连机制 | 需要手动实现 | 内置 |
| 心跳检测 | 需要 Ping/Pong | 无标准机制 |
| 消息确认 | 可实现 | 不支持 |
| 代码行数 | 较多 | 较少 |
总结
选择建议
┌───────────────────────────────────────────────────────────────┐
│ 快速选择指南 │
├───────────────────────────────────────────────────────────────┤
│ │
│ 场景 │ 推荐 │ 原因 │
│ ────────────────────────┼────────┼───────────────────────── │
│ 聊天应用 │ WebSocket │ 双向实时通信 │
│ 多人游戏 │ WebSocket │ 低延迟、高频交互 │
│ 协作编辑 │ WebSocket │ 复杂状态同步 │
│ 实时交易 │ WebSocket │ 毫秒级响应要求 │
│ LLM流式输出 │ SSE │ 单向数据流 │
│ 服务器通知 │ SSE │ 推送即可 │
│ 日志/监控数据 │ SSE │ 大量单向数据 │
│ 实时比分/股价 │ SSE │ 单向更新 │
│ │
└───────────────────────────────────────────────────────────────┘技术选型决策因素
- 通信方向
- 需要双向 → WebSocket
- 单向即可 → SSE 更简单
- 实时性要求
- 毫秒级 → WebSocket
- 秒级 → SSE 足够
- 实现复杂度
- 团队经验丰富 → 两者皆可
- 快速开发 → SSE 更简单
- 浏览器兼容
- 需要支持旧浏览器 → WebSocket
- 现代浏览器 → SSE 更优雅
- 运维考虑
- 代理/防火墙环境 → SSE 兼容性更好
- 内网环境 → WebSocket 性能更好
混合使用
在复杂应用中,可以混合使用两者:
// 使用 WebSocket 处理双向通信
const ws = new WebSocket('wss://api.example.com/ws');
// 使用 SSE 处理单向推送
const notifications = new EventSource('https://api.example.com/notifications');
// 各司其职,发挥各自优势WebSocket 和 SSE 各有优势,根据具体需求选择合适的技术方案是关键。
评论