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 就能满足需求。

相关推荐
杨云龙UP2 小时前
MySQL Host is blocked because of many connection errors 导致 JDBC 连接失败排查与解决
linux·运维·网络·数据库·sql·mysql·登录失败
执念WRD2 小时前
Fastjson 1.2.83 RCE 漏洞利用
网络·安全·网络安全
xixiaoyunya2 小时前
局域网多电脑文件互备:从传统痛点到自动化方案的技术路径
网络·安全
Lynne3093 小时前
为什么工业数据越来越需要在现场处理?边缘计算正在解决什么问题?
网络·数据分析·数据
m0_734571764 小时前
深入理解5g <十二> pucch
网络·5g
闲云野鹤在人间4 小时前
Docker入门|第3章 镜像详解
linux·网络·docker·容器·centos·云计算·php
luj_17684 小时前
击穿分析五大关键步骤
开发语言·网络·c++·经验分享·算法
开开心心就好5 小时前
低年级识字练笔顺工具,电脑和安卓端都能用
前端·javascript·网络·安全·scala·erlang·语音识别
个性小王5 小时前
配置kali在线源(阿里云)
网络·网络安全
captain3765 小时前
网络原理(4)-TCP ▲▲▲
java·服务器·网络·网络协议·tcp/ip