WebSocket 与 SSE:实时通信方案的选择
目录
轮询的代价
传统的 HTTP 交互通常是一问一答:客户端发起请求,服务端返回响应。如果客户端需要不断获取最新数据,最直观的做法是轮询:定期向服务端查询是否有新数据。
javascript
setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(data => updateUI(data));
}, 3000);
这种方式实现简单,但会带来两个问题。
产生大量无效请求。 1000 个用户在线,每人每 3 秒发起一次请求,服务端平均每秒就要处理约 333 次请求,其中大部分可能只返回"没有新数据"。这些无效请求仍然会消耗 HTTP 请求解析、网络传输和连接管理资源。
实时性不够。 假设轮询间隔为 3 秒,一条刚好错过上次轮询的消息,最多需要再等近 3 秒才会被客户端发现。缩短轮询间隔可以降低等待时间,但请求量和服务器压力也会随之增加。把间隔从 3 秒改成 1 秒,请求量变成三倍。
长轮询(Long Polling)对此做了改进:客户端发起请求后,服务端暂时不返回响应,直到产生新数据或等待超时。客户端收到响应后立刻再发一个新请求。这样可以减少无效请求,但每次收到数据后,客户端仍然需要重新发起下一次 HTTP 请求,并重复携带请求头和响应头。
如果希望服务端在连接期间持续推送数据,可以使用 SSE 或 WebSocket。
SSE:单向的服务器推送
SSE(Server-Sent Events)基于 HTTP 协议,实现服务端到客户端的单向推送。
协议原理
SSE 会保持一个 HTTP 响应连接,并按照规定的事件格式持续写入数据。客户端发起 HTTP 请求后,服务端返回 Content-Type: text/event-stream,并保持响应连接不断开,后续的数据会持续写入响应体。

服务端发送的每个事件都遵循 SSE 定义的文本格式:
data: 这是一条消息
每条事件以一个空行结尾。如果消息内容有多行,每行前面加 data: 前缀:
data: 第一行内容
data: 第二行内容
客户端收到时会把多行 data 拼接成一个字符串,中间用换行符连接。
SSE 还支持几个可选字段:
| 字段 | 作用 | 示例 |
|---|---|---|
event |
自定义事件类型,默认是 message |
event: notify |
id |
设置事件 ID;连接恢复时,浏览器会将最近收到的 ID 作为 Last-Event-ID 发送给服务端 |
id: 1001 |
retry |
告诉客户端重连间隔(毫秒) | retry: 5000 |
完整的消息格式:
event: notify
id: 1001
retry: 5000
data: {"user":"张三","action":"登录"}
EventSource API
浏览器提供了 EventSource API,一行代码就能建立 SSE 连接:
javascript
const source = new EventSource('/api/events');
// 监听默认事件(不带 event 字段的消息)
source.onmessage = (e) => {
console.log('收到消息:', e.data);
};
// 监听自定义事件
source.addEventListener('notify', (e) => {
console.log('通知:', e.data);
});
// 连接异常或中断时触发,浏览器通常会自动重连
source.onerror = () => {
console.log('SSE 连接异常');
};
原生 EventSource 内置了自动重连机制。连接中断后,浏览器会自动重新发起请求。如果服务端此前发送过 id 字段,重连请求还会携带对应的 Last-Event-ID。浏览器负责建立新的连接,但消息恢复逻辑仍需要服务端根据业务实现------比如根据 ID 补发断线期间的消息。
限制
SSE 的通信模型比较简单,也因此存在一些限制:
- 单向通信:SSE 只负责服务端到客户端的数据推送;客户端发送数据时,仍需调用普通 HTTP 接口
- 文本:只支持 UTF-8 文本,不支持二进制数据。如果要传二进制,需要先 Base64 编码
- 跨域 :
EventSource跨域请求需要服务端配置 CORS(Access-Control-Allow-Origin) - 请求头受限 :浏览器原生
EventSource不能像fetch一样自由设置自定义请求头。使用 Bearer Token 鉴权时,通常需要改用 Cookie、URL 参数,或者使用基于fetch的 SSE 客户端
WebSocket:双向的实时通道
WebSocket 是一种独立的应用层通信协议,RFC 6455 定义的经典建连方式会先通过 HTTP 完成握手,再切换到 WebSocket 通信。
协议握手
在常见的 HTTP/1.1 场景下,WebSocket 会先通过 HTTP 完成握手:客户端发送带有 Upgrade: websocket 的请求,服务端返回 101 Switching Protocols 后,双方开始按照 WebSocket 帧格式通信。

Sec-WebSocket-Key 和 Sec-WebSocket-Accept 用于验证服务端理解并接受了这次 WebSocket 握手,避免普通 HTTP 服务误处理该请求。
握手完成后,底层 TCP 连接保持不断,双方通过 WebSocket 帧(Frame)通信。对于长度较短的数据,WebSocket 帧头最小为 2 字节;浏览器发送的客户端帧还必须携带 4 字节掩码。连接建立后不需要为每条消息重复发送完整的 HTTP 请求头,因此更适合高频通信。
全双工通信
握手完成后,客户端和服务端都可以随时主动发送数据,这种通信方式称为全双工通信。
javascript
const ws = new WebSocket('ws://localhost:8080/chat');
ws.onopen = () => {
// 连接建立后随时可以发数据
ws.send('你好');
};
ws.onmessage = (e) => {
// 随时可以收数据
console.log('收到:', e.data);
};
ws.onclose = (e) => {
console.log('连接关闭', e.code, e.reason);
};
连接建立后,客户端和服务端都可以独立发送数据。
WebSocket 支持文本帧和二进制帧。send() 传字符串就发文本帧,传 Blob 或 ArrayBuffer 就发二进制帧。因此,图片、音频或 Protobuf 数据可以直接以二进制形式传输,不需要先转换为 Base64 文本。
断线重连
和 SSE 的 EventSource 不同,WebSocket 没有内置的自动重连机制。连接中断后,需要由应用自行实现重连逻辑:
javascript
function connect() {
const ws = new WebSocket('ws://localhost:8080/chat');
ws.onmessage = (e) => {
handleMessage(e.data);
};
ws.onclose = (e) => {
// 断线后延迟重连
console.log('连接断开,3秒后重连...');
setTimeout(connect, 3000);
};
ws.onerror = (e) => {
ws.close();
};
}
connect();
自动重连只能恢复连接,不能恢复断线期间的消息;如果业务不能丢消息,还需要在应用层实现消息 ID、确认或补发机制。
核心差异
| 维度 | SSE | WebSocket |
|---|---|---|
| 协议 | 基于 HTTP | 独立协议(RFC 6455) |
| 通信方向 | 单向(服务端 → 客户端) | 双向(全双工) |
| 数据格式 | 仅文本(UTF-8) | 文本 + 二进制 |
| 建连方式 | 普通 HTTP 请求并保持响应流 | 先完成协议握手,再传输 WebSocket 帧 |
| 传输开销 | 文本事件格式,建连后持续写入响应流 | 帧格式更紧凑,适合高频消息 |
| 自动重连 | 原生 EventSource 内置重连 |
需要应用自行实现 |
| 浏览器支持 | 所有现代浏览器 | 所有现代浏览器 |
| 代理/防火墙兼容 | 复用 HTTP 基础设施,但需关闭代理缓冲 | 通常受支持,但代理需要正确处理协议升级 |
怎么选
不是所有"服务端推送"都需要 WebSocket。对于单向的数据流,SSE 往往更容易接入和维护。
判断标准一:客户端需不需要主动发数据给服务端?
通信方向通常是首先需要考虑的因素。如果主要由服务端持续向客户端推送数据,SSE 通常已经能够满足需求。如果客户端需要高频地向服务端发数据,WebSocket 更合适。
- 实时通知、AI 输出流、任务进度推送 → SSE
- 实时聊天、多人协作编辑、在线游戏、高频双向行情通信 → WebSocket
判断标准二:需不需要传二进制数据?
SSE 只支持文本。如果需要传输图片、音频或 Protobuf 数据,SSE 通常需要先编码成文本,而 WebSocket 可以直接发送二进制帧。
判断标准三:环境限制
SSE 复用了普通 HTTP 连接,对现有基础设施比较友好,但仍需注意代理缓冲、空闲超时和 CDN 对流式响应的支持。WebSocket 则要求代理和负载均衡器正确支持协议升级,并配置合适的连接超时时间。如果部署链路主要围绕普通 HTTP 构建,SSE 通常更容易接入;使用 WebSocket 前则需要确认代理、网关和负载均衡配置。
小结
SSE 和 WebSocket 都能让服务端持续向客户端推送数据,但两者面向的通信场景不同。SSE 基于 HTTP 事件流,主要用于服务端到客户端的单向文本推送,原生 EventSource 还提供自动重连;WebSocket 使用独立的帧协议,支持双向通信和二进制数据,但应用通常需要自行处理重连和连接状态。
选型时可以先看通信方向,再考虑消息频率、二进制传输和部署环境。主要由服务端推送数据时,可以优先考虑 SSE;客户端和服务端都需要高频发送数据时,可以优先考虑 WebSocket。如果业务只需要单向文本流,直接使用 SSE 往往已经足够,没有必要额外维护 WebSocket 的连接和重连逻辑。例如在 AI Agent 的流式回答中,服务端会把模型生成的数据块持续返回给前端,这种以服务端输出为主的场景通常使用 SSE 就能满足需求。