WebSocket vs SSE 实测复盘:10万连接下 SSE 节省 40% 内存
有个团队做了实时大盘——价格、库存、阈值告警。选 WebSocket 的时候没犹豫,觉得做实时通信这不就是标准答案吗。等压测报告出来一看内存占用,才发觉不对劲。
先说结论
10万并发连接下,SSE 的内存占用比 WebSocket 低约 40%。
这是 Ark Protocol 团队的真实压测数据。他们用 Rust + Axum 分别实现了两套服务,在相同条件下跑基准测试:
| 指标 | WebSocket | SSE | 差距 |
|---|---|---|---|
| 内存/连接 | ~52KB | ~31KB | -40% |
| 10万连接总内存 | ~5.2GB | ~3.1GB | -2.1GB |
| CPU(低频推送) | 基准 | 基准 | 几乎相同 |
| 横向扩展 | 需 stateful 负载均衡 | 标准 HTTP 负载均衡 | SSE 完胜 |
差距不是来自某个角落的微优化,是两种协议底层机制的根本差异。
协议机制差异:为什么内存差这么多
WebSocket:一条永久的专线电话
WebSocket 建立后,TCP 连接保持不断开,双方随时能互相扔数据。这样一来:
- 每个连接在服务端要维护一个独立的 Task:读写分离、状态追踪、连接生命周期管理
- 协议本身有帧解析开销:Opcode(4位)、Mask 位(1位)、Payload Length(7/16/64位可变)、帧校验
- 应用层心跳另起定时器:即使没有数据传输,心跳包也要占内存
连接数少的时候这不是问题。上了规模,内存就开始尖叫——每个连接都在吃内存。
SSE:挂在门上的信箱
SSE 基于 HTTP,服务端向客户端单向推数据。连接建立后,客户端只需要一个 EventSource API,服务端按需发送文本事件。
- HTTP/1.1 流水线特性让连接可以被复用:一个连接可以承载多个客户端的 SSE 流
- 没有 WebSocket 那种帧解析负担:就是 HTTP 流式响应,Text/Event-Source 类型
- HTTP 中间件天然认识 SSE:NGINX、Cloudflare、AWS ALB 都知道怎么处理,不会误判为空闲连接
代价也清楚:只能服务端→客户端单向。客户端要发数据?得另开一个 HTTP 请求。
实战场景:什么时候该选谁
选 SSE 的场景
数据流向单一的业务最适合 SSE。比如:
- 实时价格推送(服务端推,客户端看)
- 库存数量变化通知
- 日志流、监控大盘、CI/CD 构建状态
- 告警阈值触发通知
这些场景里客户端从来不主动喊话,SSE 刚好 cover。
大规模横向扩展是 SSE 的强项。SSE 跑在标准 HTTP 上,流量可以直接甩给任何能处理 HTTP 的负载均衡器。100台 backend 轻松加,不存在 WebSocket 那样的连接状态共享问题。
选 WebSocket 的场景
需要双向、频繁、低延迟交互的业务,WebSocket 更合适。比如:
- 聊天室、协作编辑(多人同时操作)
- 游戏操作指令(你一动对方马上看到)
- 金融交易下单(双向握手确认)
这些场景双向通道是刚需,SSE 硬上就得开两个连接(一个推一个拉),复杂度反而上来。
中间地带:混合架构
也有团队在两个都用:SSE 处理大规模下行数据(行情、通知),WebSocket 处理小量高频上行指令(下单、聊天)。这是合理的折中。
实测数据解读:CPU 和延迟才是另一面
Ark Protocol 的测试里内存差距 40%,CPU 占用两者差距不大。SSE 省内存,WebSocket 省 CPU?这个结论要小心。
真正影响 CPU 的是消息频率和 payload 大小,不是协议本身。毫秒级高频推送场景,WebSocket 的二进制帧(最小 2 字节帧头)比 SSE 的文本事件(data: ...\n\n)更紧凑,CPU 反倒是 WebSocket 更省。
决策不能只看内存一个指标:
| 维度 | SSE 优势 | WebSocket 优势 |
|---|---|---|
| 内存占用 | ✅ 40%↓ at 100K 连接 | - |
| CPU 效率(高频) | - | ✅ 二进制帧更紧凑 |
| 双向通信 | ❌ | ✅ 原生支持 |
| 横向扩展 | ✅ HTTP 天然支持 | ❌ 需要状态共享 |
| 中间件兼容性 | ✅ 标准 HTTP | ❌ 私有协议 |
| 断线重连 | 浏览器自动重连 | 需手动实现 |
| 自动心跳 | 均衡器代理 | 需应用层实现 |
代码对比:Axum 实现 SSE vs WebSocket
SSE 版本
use axum::{Router, routing::get, response::sse::{Sse, Event}};
use tokio_stream::wrappers::BroadcastStream;
use tokio::sync::broadcast;
use std::time::Duration;
async fn sse_handler(broadcast_rx: broadcast::Receiver<String>) -> Sse<Event> {
let stream = BroadcastStream::new(broadcast_rx).map(|msg| {
Ok(Event::default().data(msg.unwrap_or_default()))
});
Sse::new(stream).keepalive(
axum::response::sse::keep_alive()
.interval(Duration::from_secs(15))
)
}
#[tokio::main]
async fn main() {
let (tx, _rx) = broadcast::channel::<String>(100);
let app = Router::new()
.route("/stream", get(sse_handler));
// 用 tx.send() 向所有订阅者广播
println!("SSE server running on :8080");
}
SSE 核心优势:
- 45 行代码搞定
- 无需维护连接映射表
- 自动重连由浏览器处理
- 负载均衡器天然支持
WebSocket 版本
use axum::{Router, routing::get, ws::{WebSocket, WebSocketUpgrade}};
use tokio::sync::broadcast;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
struct WSState {
peers: Arc<Mutex<HashMap<String, broadcast::Sender<String>>>>,
}
async fn ws_handler(
ws: WebSocketUpgrade,
State(state): State<WSState>,
Path(client_id): Path<String>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, state, client_id))
}
async fn handle_socket(socket: WebSocket, state: WSState, client_id: String) {
let (sender, mut receiver) = broadcast::channel::<String>(100);
{
let mut peers = state.peers.lock().await;
peers.insert(client_id.clone(), sender.clone());
}
let mut rx = state.tx.subscribe();
let (ws_sender, mut ws_receiver) = socket.split();
// 处理 WebSocket 消息
let writer = async {
while let Ok(msg) = rx.recv().await {
if ws_sender.send(axum::extract::ws::Message::Text(msg)).send().await.is_err() {
break;
}
}
};
let reader = async {
while let Ok(msg) = ws_receiver.next().await {
if let Some(Ok(axum::extract::ws::Message::Text(text))) = msg {
println!("Received: {}", text);
}
}
};
tokio::join!(writer, reader);
let mut peers = state.peers.lock().await;
peers.remove(&client_id);
}
WebSocket 核心挑战:
- 需要维护连接映射表(HashMap + Mutex)
- 断线重连需要手动实现
- 负载均衡需要 sticky session 或 WebSocket 感知型 LB
- 代码量比 SSE 多 60-70%
一句话决策
只需要看数据、扩展性优先 → SSE
需要喊话、低延迟双向交互 → WebSocket
大多数监控、通知、实时数据流场景其实选 SSE 就够了。
想跟踪更多 Rust 异步编程、实时系统架构实战?关注「全栈之巅-梦兽编程」公众号,每周更新。
也欢迎了解 梦兽编程 AI 编程助手服务 ,帮你把 AI 编程工具用到生产环境。
