后端 Web 协议(04):WebSocket 与 SSE——实时通信方案对比、心跳、性能
更新时间:2026-09-01。本文是
backend/web-protocol/Web 协议第 04 篇,接 gRPC 与 Protobuf。WebSocket 和 SSE(Server-Sent Events)是服务端推送的两种主要方案。WebSocket 双向通信,SSE 单向推送。理解它们的区别和适用场景,才能设计出高效的实时通信系统。
本文要回答的问题
- WebSocket 是什么?和 HTTP 有什么关系?
- SSE 是什么?和 WebSocket 比有什么优缺点?
- 心跳机制怎么设计?为什么需要心跳?
- 连接管理怎么做?重连、关闭、负载均衡?
一、WebSocket 协议
http
# WebSocket 握手:从 HTTP 升级到 WebSocket 协议
# 请求
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbLi1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
# 响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=特点:
- 全双工通信:客户端和服务端都可以主动发消息
- 保持连接:握手后,复用 TCP 连接,不需要每次建立新连接
- 帧格式:二进制帧,帧头小(2-14 字节),开销小
- 无队头阻塞:WebSocket 在应用层自己管理帧,不在 HTTP 层
二、SSE(Server-Sent Events)
http
# SSE 是 HTTP 标准,服务端单向推送
# 客户端请求
GET /events HTTP/1.1
Host: example.com
Accept: text/event-stream
# 服务端响应
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
# 数据格式
data: {"message": "hello"}
data: {"message": "world"}
# 带事件类型和 ID
event: update
id: 123
data: {"status": "ok"}特点:
- 单向:服务端 → 客户端
- 自动重连:浏览器原生支持,断线后自动重连,带上 Last-Event-ID
- 基于 HTTP:不需要额外协议,防火墙友好
- 文本格式:只能传文本,不能直接传二进制
三、WebSocket vs SSE 对比
| 对比 | WebSocket | SSE |
|---|---|---|
| 方向 | 双向(客户端 ↔ 服务端) | 单向(服务端 → 客户端) |
| 协议 | 独立协议(ws/wss) | 基于 HTTP |
| 协议切换 | 需要握手升级(101) | 普通 HTTP 请求 |
| 传输 | 二进制或文本 | 纯文本 |
| 自动重连 | 需要自己实现 | 原生支持(Last-Event-ID) |
| 浏览器支持 | 所有现代浏览器 | 所有现代浏览器(IE 不支持) |
| 防火墙友好 | 可能被拦截 | 好,基于 HTTP |
| 适用场景 | 聊天、游戏、实时协作 | 通知推送、实时数据更新 |
选择建议:
- 双向通信(聊天、游戏、协作编辑):WebSocket
- 单向推送(通知、股票行情、日志流):SSE(更简单、自动重连)
四、心跳机制
javascript
// 客户端心跳
// 每隔一段时间发 ping,服务端回 pong,检查连接是否存活
const WS_HEARTBEAT_INTERVAL = 30000; // 30 秒
const WS_HEARTBEAT_TIMEOUT = 10000; // 10 秒超时
let heartbeatTimer = null;
let heartbeatTimeout = null;
function startHeartbeat(ws) {
heartbeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
// 设置超时
heartbeatTimeout = setTimeout(() => {
console.log('心跳超时,关闭连接');
ws.close();
}, WS_HEARTBEAT_TIMEOUT);
}
}, WS_HEARTBEAT_INTERVAL);
}
function onMessage(event) {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
clearTimeout(heartbeatTimeout);
}
}为什么需要心跳:
- 检测网络断开(TCP 连接可能已经断开,但应用层不知道)
- 保持 NAT 和防火墙的连接映射(防止被关闭)
- 检测僵尸连接,释放服务端资源
五、连接管理
javascript
// 重连策略
// 指数退避 + 随机抖动
function connect(url, retryCount = 0) {
const ws = new WebSocket(url);
ws.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
const jitter = Math.random() * 1000;
setTimeout(() => {
connect(url, retryCount + 1);
}, delay + jitter);
};
}连接上限:
- 一个服务端进程能同时处理几千到几万个 WebSocket 连接
- 限制因素:文件描述符、内存(每个连接有缓冲区)
- 多进程/多节点:需要用 Redis 或消息队列广播消息
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 心跳频率太快 | 大量无用包,浪费带宽 | 30 秒一次,超时 10 秒 |
| 没有心跳 | 僵尸连接越来越多 | 必须实现心跳机制 |
| 服务端重启后连接断开 | 客户端不知道 | 客户端实现自动重连 |
| 消息顺序问题 | 重连后收到重复消息 | 用消息 ID 去重 |
相关与延伸
下一篇:HTTP 版本演进——HTTP/1.1、HTTP/2、HTTP/3 对比、性能优化;gRPC 流式通信,见 gRPC 与 Protobuf。
一句话总结
WebSocket 与 SSE:WebSocket 全双工通信,需要协议升级握手,适合聊天和实时协作;SSE 单向推送,基于 HTTP,自动重连(Last-Event-ID),适合通知和实时数据更新;心跳机制必须实现(30 秒 ping,10 秒超时),检测网络断开和僵尸连接;重连用指数退避 + 随机抖动避免惊群;WebSocket 服务端连接上限受限于文件描述符和内存,多节点广播用 Redis 或消息队列。