WebSocket 与 SSE:实时通信方案的选择

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-KeySec-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() 传字符串就发文本帧,传 BlobArrayBuffer 就发二进制帧。因此,图片、音频或 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 就能满足需求。

相关推荐
yagami_gagami1 小时前
第四届黄河流域公安院校电子物证个人赛服务器取证
linux·服务器·网络·mysql·安全·docker·llama
九硕智慧建筑一体化厂家1 小时前
直流照明|档案库房专用照明,防紫外线、防纸张老化、适配24小时值守
运维·网络·笔记·智慧城市
rcms152702692181 小时前
HASETEC GU-208V 电源板
网络
大鱼>2 小时前
从零实现TCP/IP协议栈:AR →IP分片→TCP状态机→拥塞控制
网络·网络协议·tcp/ip
paopaokaka_luck2 小时前
基于springboot3+vue3的乡村医生诊疗管理系统(AI助手、协同过滤算法、webSocket实时聊天、Echarts图形化分析)
前端·网络·人工智能·spring boot·websocket·网络协议·echarts
热心市民R先生2 小时前
IgH EtherCAT Master 1.5 全流程安装部署手册(Git 源码版 + 内核源码修改 + 实时内核适配)
网络·机器人
程序猿乐锅2 小时前
【计算机网络 | 第三章】数据链路层
网络·网络协议·计算机网络
zerwave2 小时前
Docker 学习:多容器网络互通——从 host 模式到自定义 bridge 网络
网络·学习·docker
大模型搬砖师3 小时前
在Kubernetes上部署企业AI网关:一份云原生参考
网络·人工智能·安全