实时通信:WebSocket、SSE 与轮询对比
传统 HTTP 通信通常由客户端发起请求、服务端返回响应。如果页面需要持续获得服务端的最新数据,可以通过重复请求、保持响应流或建立双向长连接来实现。前端常见的实时通信方案有以下几种:
1. 轮询
- 短轮询:前端通过定时器每隔一段固定时间向服务端发送请求。
- 缺点:如果数据没有频繁更新,会产生大量无效的请求,造成带宽和服务器资源的冗余开销。
- 长轮询:客户端发送请求后,服务端如果没有新数据,会将请求挂起阻塞。直到有数据产生或达到超时时间,服务端才返回响应。客户端收到响应后立即再次发起挂起请求。
- 特点:相比短轮询相对减少了请求次数,但服务端需要维护大量的挂起并发连接状态。
2. Server-Sent Events
SSE(Server-Sent Events)是一种基于 HTTP 的单向通信机制,允许服务端通过一个持续保持的 HTTP 响应,不断向客户端推送 UTF-8 文本事件。
这里的“单向”是指 SSE 连接本身只能由服务端向客户端发送数据。客户端仍然可以另外使用 fetch 等普通 HTTP 请求向服务端提交数据。
(1) 建立连接
浏览器原生提供了 EventSource API。创建实例后,浏览器会发起 HTTP GET 请求:
const source = new EventSource('/api/events')
服务端必须返回 text/event-stream 响应并保持响应不结束。为了避免事件流被缓存,通常还会同时设置 Cache-Control: no-cache:
连接建立后,服务端可以持续写入事件,而不需要客户端重复发起请求。
(2) 事件流格式
SSE 不是直接传输任意数据包,而是使用约定好的文本格式。每条事件由若干字段组成,并以一个空行表示结束:
event: progress
id: 101
retry: 3000
data: {"percent": 80}
data:事件数据,可以是普通文本,也可以放入序列化后的 JSON。
event:自定义事件名称;未设置时,客户端收到默认的 message 事件。
id:当前事件 ID,用于断线重连后的数据续传。
retry:建议浏览器断线后等待多久再重连,单位为毫秒。
- 以
: 开头的行是注释,不会触发事件,常被用作心跳包维持连接。
如果一条事件包含多个 data 字段,浏览器会使用换行符将它们拼接成一份数据。
(3) 接收事件
没有指定 event 字段的消息,通过 onmessage 接收:
source.onmessage = event => {
const data = JSON.parse(event.data)
console.log(data)
}
指定了事件名称的消息,需要通过 addEventListener 监听:
source.addEventListener('progress', event => {
const data = JSON.parse(event.data)
console.log(`当前进度:${data.percent}%`)
})
source.onerror = error => {
console.error('SSE 连接异常', error)
}
不再需要接收事件时,应主动关闭连接:
(4) 自动重连与断点续传
当连接因为网络异常等原因中断时,EventSource 默认会尝试自动重连。重连间隔可以由服务端通过 retry 字段调整。如果服务端希望客户端停止重连,可以返回 HTTP 204 No Content;客户端也可以调用 close() 主动终止连接。
如果服务端发送的事件带有 id,浏览器重连时会通过 Last-Event-ID 请求头告知服务端最后收到的事件 ID。服务端可以从下一条事件开始补发:
服务端发送 id: 101
↓
连接中断,102、103 未收到
↓
浏览器重连并携带 Last-Event-ID: 101
↓
服务端从 102 开始补发
需要注意,自动重连只负责重新建立连接,并不天然保证消息不丢失。要实现可靠续传,服务端还需要保存一段时间内的事件,并根据 Last-Event-ID 查询和补发缺失数据。
(5) 优势、限制与工程注意事项
- 优势:
- 基于标准 HTTP,接入成本通常低于 WebSocket。
- 浏览器原生支持
EventSource,内置事件解析和断线重连。
- 很适合持续下发少量状态更新。
- 限制:
- SSE 连接只支持服务端向客户端发送数据,不适合高频双向交互。
- 原生
EventSource 只接收 URL 和 withCredentials 配置,不方便添加自定义请求头或 POST 请求体。
- 事件流只能使用 UTF-8 文本,不能像 WebSocket 一样直接传输二进制帧。
- 跨域连接需要正确配置 CORS;携带跨域 Cookie 时还需要启用
withCredentials。
- 在 HTTP/1.x 下会受到浏览器同域连接数限制,多标签页可能互相占用连接;HTTP/2 可以通过多路复用缓解这一问题。
- 工程注意事项:
- 服务端写入事件后要及时刷新响应,避免数据一直停留在应用缓冲区。
- Nginx 等反向代理需要关闭响应缓冲,否则客户端可能一次收到积攒的多条事件,失去实时性。
- 长时间没有业务消息时,可以定期发送注释心跳,避免代理或网关关闭空闲连接。
- 客户端断开后,服务端应停止推送并及时释放连接相关资源。
(6) 典型使用场景
SSE 更适合“连接建立后,主要由服务端持续向客户端下发文本数据”的场景:
- 消息通知:服务端推送新消息提醒、审核结果、订单状态变化,客户端通常只负责展示。
- 实时数据看板:持续更新在线人数、访问量、监控指标等数据,客户端很少需要通过同一连接反向发送消息。
- 任务进度:推送文件导出、代码构建、批量处理等长任务的进度和最终结果。
- 日志滚动:持续追加服务端日志、流水记录或告警信息,数据天然按照时间顺序向下游传递。
- AI 流式文本:服务端生成一部分内容就下发一部分,让用户逐步看到回答;如果请求必须使用 POST 或自定义请求头,通常会改用
fetch 读取流式响应。
- 资讯与比分更新:推送新闻动态、赛事比分等以服务端更新为主、频率相对可控的数据。
如果客户端也需要通过同一连接频繁发送消息,或者需要直接传输二进制数据,WebSocket 通常更合适。
3. WebSocket
WebSocket 是一种面向长连接的全双工通信协议。连接建立后,客户端和服务端都可以随时主动发送数据,不需要遵循 HTTP 一问一答的请求-响应顺序。
(1) 建立连接
浏览器原生提供了 WebSocket API:
const socket = new WebSocket('wss://example.com/chat')
ws://:普通 WebSocket 连接。
wss://:使用 TLS 加密的 WebSocket 连接,生产环境通常应使用该方式。
在经典的 HTTP/1.1 场景中,客户端先发送一次 HTTP 握手请求:
服务端确认支持 WebSocket 后返回 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
握手成功后,双方开始按照 WebSocket 协议通信。后续每条消息不再携带完整的 HTTP 请求头,而是使用开销较小的 WebSocket 数据帧。HTTP/2 和 HTTP/3 建立 WebSocket 的方式有所不同,但建立后的通信语义基本一致。
(2) 数据传输
WebSocket 主要包含以下几类帧:
- 文本帧:传输字符串,JSON 通常先序列化为字符串再发送。
- 二进制帧:传输
Blob、ArrayBuffer 等二进制数据。
- 控制帧:包括关闭连接的
Close,以及用于连接探测的 Ping、Pong。
连接正常时,WebSocket 会保证消息按顺序到达。客户端和服务端可以独立发送数据,因此很适合持续、频繁的双向交互。
(3) 发送与接收消息
const socket = new WebSocket('wss://example.com/chat')
socket.onopen = () => {
socket.send(JSON.stringify({ type: 'join', roomId: 1001 }))
}
socket.onmessage = event => {
const message = JSON.parse(event.data)
console.log('收到消息', message)
}
socket.onerror = error => {
console.error('WebSocket 连接异常', error)
}
socket.onclose = event => {
console.log('连接关闭', event.code, event.reason)
}
WebSocket 的连接状态包括:
CONNECTING → OPEN → CLOSING → CLOSED
只有连接进入 OPEN 状态后才能正常发送数据。不再需要通信时,可以主动关闭:
socket.close(1000, '页面退出')
(4) 心跳与断线重连
WebSocket 协议本身定义了 Ping/Pong 控制帧,服务端和部分 WebSocket 库可以使用它们检测连接是否仍然存活。但是浏览器原生 WebSocket API 没有提供直接发送 Ping 帧的方法,因此前端应用常使用普通业务消息实现心跳:
socket.send(JSON.stringify({ type: 'ping' }))
与 EventSource 不同,WebSocket 没有内置自动重连机制。网络断开后,应用通常需要自行完成:
监听 close 事件
↓
等待一段时间后重新连接
↓
重新鉴权和恢复订阅
↓
根据业务需要补拉断线期间的数据
连续重连失败时,应逐渐增加等待时间,避免大量客户端同时频繁重连。
(5) 优势、限制与工程注意事项
- 优势:
- 支持真正的双向通信,客户端和服务端都可以主动发送数据。
- 建立连接后消息帧开销较小,适合高频、低延迟通信。
- 同时支持文本和二进制数据。
- 限制:
- 需要服务端支持 WebSocket 握手、连接管理和消息处理,但不一定要单独部署一套服务器。
- 浏览器不提供自动重连,心跳、重连和断线数据恢复通常需要业务层处理。
- 长连接会长期占用服务端资源,服务端需要及时清理已经失效的连接。
- 工程注意事项:
- 生产环境应优先使用
wss://,并在服务端完成身份认证和来源校验。
- 代理服务器、网关和负载均衡器需要支持 WebSocket,并合理配置空闲连接超时时间。
- 如果客户端发送速度持续高于网络传输速度,可以通过
bufferedAmount 观察尚未发送的数据量,避免消息大量堆积。
(6) 典型使用场景
WebSocket 更适合“客户端和服务端都需要主动发送数据,并且交互频繁、延迟敏感”的场景:
- 即时聊天与在线客服:用户发送消息,服务端同时推送新消息、已读状态和在线状态。
- 多人在线协作:实时同步文档编辑操作、光标位置、画布变化和成员状态。
- 多人游戏:持续交换玩家操作、位置、房间状态和游戏事件,对延迟比较敏感。
- 实时控制:控制远程设备、机器人或大屏展示,并及时接收设备状态和执行结果。
- 交易与竞价交互:行情持续更新的同时,用户还需要提交订单、报价或撤销操作。
- 实时音视频的信令通道:用于交换房间状态、呼叫控制和 WebRTC 协商信息;真正的音视频数据通常由 WebRTC 传输。
如果业务只是偶尔接收服务端通知,不需要频繁双向交互,使用 SSE 往往实现更简单。
4. 选型总结
在实际系统搭建中,应根据通信方向、实时性和实现成本进行选择:
| 业务特点 | 推荐方案 | 典型场景 |
|---|
| 数据更新不频繁,允许一定延迟 | 轮询 | 后台状态查询、低频数据刷新 |
| 主要由服务端持续下发文本数据 | SSE | 消息通知、任务进度、日志、数据看板、AI 流式文本 |
| 双方都需要频繁发送数据,延迟敏感或需要二进制传输 | WebSocket | 即时聊天、在线协作、多人游戏、实时控制 |
选型的关键不是“哪种技术更新”,而是业务是否需要双向通信。只需要服务端持续推送时可以优先考虑 SSE;真正需要持续双向交互时再选择 WebSocket。