实时通信: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

Content-Type: 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)
}

不再需要接收事件时,应主动关闭连接:

source.close()

(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 握手请求:

GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13

服务端确认支持 WebSocket 后返回 101 Switching Protocols

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...

握手成功后,双方开始按照 WebSocket 协议通信。后续每条消息不再携带完整的 HTTP 请求头,而是使用开销较小的 WebSocket 数据帧。HTTP/2 和 HTTP/3 建立 WebSocket 的方式有所不同,但建立后的通信语义基本一致。

(2) 数据传输

WebSocket 主要包含以下几类帧:

  • 文本帧:传输字符串,JSON 通常先序列化为字符串再发送。
  • 二进制帧:传输 BlobArrayBuffer 等二进制数据。
  • 控制帧:包括关闭连接的 Close,以及用于连接探测的 PingPong

连接正常时,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。