先验证结论:这篇热门短文哪些说对了,哪些说过头了
先说结论:原文的大方向是对的,但口号化得太狠。正确的部分包括三点。第一,SSE 确实非常适合“服务端持续把新内容推给浏览器”的场景。MDN 对 EventSource 的定义就很直接:它是单向的,消息从服务端发到客户端;在没有客户端消息流需求时,这是一个很好的选择。第二,SSE 的接入成本通常比 WebSocket 低,因为它本质上就是一条流式 HTTP 响应,不需要走 WebSocket 的升级握手。第三,在大模型文本流输出里,SSE 的确已经是事实上的主流做法之一。OpenAI 当前的官方文档明确写着:Responses API 的 stream=true 采用的是 server-sent events。
但原文里至少有四个点说过头了。第一,SSE 比 WebSocket 更轻 不能偷换成 WebSocket 该退役了。WebSocket 的价值从来都不是“也能做推送”,而是全双工、低消息开销、长期双向会话和更自由的应用层协议设计。第二,SSE 更省资源 也不能绝对化。SSE 同样是长连接,浏览器和服务端同样要维持连接状态,资源占用差异很大程度上取决于服务器实现、代理层、压缩、缓冲和连接规模,而不是协议名字本身。第三,SSE + BroadcastChannel = 只维护一个连接 也不严谨。BroadcastChannel 只是同源上下文之间的消息广播能力;如果每个标签页都自己建立 SSE,再互相广播,连接数并不会减少。要把“多标签页多个连接”收敛成“单连接共享”,通常还需要 leader tab、SharedWorker 或 Service Worker 一类额外协调机制。第四,ChatGPT 打字机效果就是 SSE 这种说法缺乏公开可核验来源。我们能确认的是 OpenAI 官方文本流接口使用 SSE;但 ChatGPT 产品内部具体怎么组织浏览器到后端的传输,外界并没有完整公开。
如果把这些修正合在一起,更稳妥的说法应该是:在浏览器里的单向文本流和状态流场景中,SSE 往往是默认优先项;而在需要双向、低延迟、音视频或复杂会话控制的场景中,WebSocket、WebRTC,乃至正在发展的 WebTransport,仍然是必要选项。
不是:
- WebSocket 过时了
- SSE 永远更省资源
- BroadcastChannel 天然等于单连接复用
更接近现实的是:
- 单向实时推送,优先看 SSE
- 双向低延迟交互,继续看 WebSocket / WebRTC
- 资源占用取决于实现,不取决于口号
- 多标签页复用连接,需要额外协调层
SSE 的基本知识:它到底是什么
SSE,Server-Sent Events,本质上是一种基于 HTTP 的事件流协议。客户端发起一条普通 HTTP 请求,服务端返回 Content-Type: text/event-stream,然后保持连接不断开,持续把事件一段一段写回去。浏览器侧通常用原生 EventSource 接口消费这条流。
它的协议设计非常朴素。每条消息由一个或多个文本字段组成,常见字段包括 event、data、id 和 retry。消息之间用空行分隔。event 用于区分事件类型,data 是正文,id 用于断线重连后的续传,retry 可以告诉浏览器下次重连等多久。WHATWG HTML 标准和 MDN 都明确说明了这一点。
SSE 最重要的三个特性是:第一,它是单向的,服务端推给客户端;第二,浏览器原生支持自动重连;第三,浏览器会记住最近一个事件的 id,在重连时通过 Last-Event-ID 带回来,方便服务端补发缺失消息。也正因为有这三点,SSE 特别适合“持续有新状态,但交互逻辑不复杂”的 Web 页面。
- 连接模型:普通 HTTP 请求 + 长时间不结束的响应。
- 数据格式:UTF-8 文本事件流,不是通用二进制通道。
- 浏览器能力:原生
EventSource、自动重连、Last-Event-ID。
event: progress
id: 42
data: {"percent": 65, "stage": "embedding"}
event: done
id: 43
data: {"ok": true}
SSE 解决的到底是什么问题
SSE 真正解决的,不是所有实时通信问题,而是一类非常具体的问题:服务端已经知道有新状态要到达浏览器,而且这些状态主要是单向输出,比如任务进度、通知、行情、监控状态、日志尾流、模型文本流。这类场景过去常常被硬塞进轮询、长轮询或者 WebSocket 里。轮询太浪费,长轮询写起来别扭,WebSocket 又常常把问题搞得比实际需要更复杂。
当问题本质是“服务端有新东西就告诉我”,SSE 的优势会一下子显现出来。它保留了 HTTP 生态的兼容性:路由、Cookie、TLS、反向代理、缓存控制、鉴权链路和服务端框架几乎都能沿用。对很多团队来说,这意味着你不是在引入一套新栈,而是在现有 HTTP 栈里开一条可持续刷新的响应流。
在 AI 应用里,SSE 的价值更明显。文本生成天然是追加式输出,最适合被拆成一个个增量 token 或语义事件推给前端。OpenAI 当前官方文档写得很明确:Responses API 的 HTTP 流式模式走的是 SSE;而 Realtime 模型则面向另一类需求,支持 WebRTC、WebSocket 或 SIP。这恰好说明选型标准不是谁替代谁,而是数据形态和交互模式不同。
1. 文本流
- LLM token 流
- 摘要生成进度
- 审批流状态变化
2. 状态流
- 任务进度条
- 订单状态
- 构建日志尾流
3. 通知流
- 系统通知
- 监控告警
- 评论 / 点赞 / 订阅更新
和 WebSocket 到底怎么选:别再用“更先进”做标准
如果只看协议能力,WebSocket 显然更强。它是全双工通道,客户端和服务端都能随时发消息,还可以承载自定义应用层协议,更适合协同编辑、多人游戏、远程控制、语音对话、低延迟双向事件交换等场景。但“更强”不等于“每次都该用”。绝大多数业务系统并不需要把浏览器和服务端绑成一个始终双向对话的会话通道,它们只是想把后端变化更快地显示到前端。
从工程实现上看,两者的心智负担不同。MDN 的 HTTP 协议升级文档明确提到,WebSocket 通常通过 HTTP/1.1 的 Upgrade 机制完成握手,而 HTTP/2 又显式不支持这个升级机制。反过来,SSE 依旧停留在标准 HTTP 响应语义里,不需要升级协议。这个差别会直接影响网关、代理、限流和一些平台托管环境下的可维护性。
但也别把 SSE 神化。MDN 同样提醒过:如果不是跑在 HTTP/2 上,浏览器对 SSE 连接数有很低的限制,同一浏览器对同一域名大约只有 6 条连接额度;到 HTTP/2 才会变成协商后的多路复用流。这意味着如果你有多标签页、多组件、多流来源,还傻乎乎地每个地方都单独起一个 EventSource,SSE 一样会把你拖进工程坑里。
- 需要服务端持续向浏览器推文本或状态:优先看 SSE。
- 需要浏览器高频把事件送回服务端:优先看 WebSocket。
- 需要浏览器端实时语音或音视频:优先看 WebRTC;OpenAI 当前官方也推荐浏览器侧 Realtime 先用 WebRTC。
- 需要 HTTP/3、多流、乱序投递或不可靠数据报:开始关注 WebTransport,但现在还不是所有业务的默认答案。
SSE
- 单向
- 文本优先
- 自动重连
- 浏览器原生友好
- 很适合 LLM 文本流 / 通知 / 进度
WebSocket
- 双向
- 文本 / 二进制都行
- 协议自由度高
- 适合协同编辑 / 游戏 / 控制面
WebRTC
- 超低延迟媒体与数据通道
- 更适合浏览器实时语音视频
WebTransport
- HTTP/3 + 多流 + datagram
- 未来感很强,但生态仍在成熟
代码例子一:用 Node.js 写一个靠谱的 SSE 服务端
下面给一个最小但够工程化的 Node.js 示例。重点不只是“把数据写出去”,而是把几个关键点都补齐:正确的响应头、心跳注释、防止代理缓冲、事件 id、客户端断开后的清理,以及一个广播函数。只要这些东西齐了,大多数仪表盘、通知流和文本流页面就已经能稳定工作。
import express from "express";
const app = express();
const clients = new Map();
let nextClientId = 1;
let nextEventId = 1;
app.get("/api/stream", (req, res) => {
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
res.flushHeaders?.();
const clientId = nextClientId++;
clients.set(clientId, res);
res.write(`event: ready\n`);
res.write(`data: ${JSON.stringify({ clientId })}\n\n`);
const heartbeat = setInterval(() => {
res.write(`: ping ${Date.now()}\n\n`);
}, 15000);
req.on("close", () => {
clearInterval(heartbeat);
clients.delete(clientId);
});
});
function broadcast(event, payload) {
const id = nextEventId++;
const data = JSON.stringify(payload);
for (const res of clients.values()) {
res.write(`id: ${id}\n`);
res.write(`event: ${event}\n`);
res.write(`data: ${data}\n\n`);
}
}
setInterval(() => {
broadcast("progress", {
at: new Date().toISOString(),
value: Math.floor(Math.random() * 100),
});
}, 3000);
app.listen(3000, () => {
console.log("SSE server listening on http://localhost:3000");
});
代码例子二:浏览器和 React 侧怎么接
浏览器端最简单的做法是直接 new 一个 EventSource。但要注意两点。第一,不要在每个组件里各自偷偷起一条连接,最好集中到一个数据层或 store 层。第二,原生 EventSource 构造函数只有 URL 和 withCredentials 选项,没有自定义 header 参数,这意味着如果你依赖 Bearer Token 头来鉴权,通常得改成 Cookie、一次性签名 URL,或者退回 fetch 流式读取方案。
React 里通常可以把 SSE 包成一个 hook,再把解析后的状态交给组件树。下面这个例子同时演示默认消息、命名事件和卸载清理。对于 LLM 文本流,它同样适用,只不过 data 里的内容会换成 token 或语义事件。
const source = new EventSource("/api/stream", {
withCredentials: true,
});
source.onmessage = (event) => {
console.log("default message", event.data);
};
source.addEventListener("progress", (event) => {
const payload = JSON.parse(event.data);
console.log("progress", payload);
});
source.onerror = (err) => {
console.error("SSE error", err);
};
window.addEventListener("beforeunload", () => {
source.close();
});
import { useEffect, useState } from "react";
export function useProgressStream() {
const [items, setItems] = useState([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
const source = new EventSource("/api/stream", {
withCredentials: true,
});
source.addEventListener("open", () => {
setConnected(true);
});
source.addEventListener("progress", (event) => {
const payload = JSON.parse(event.data);
setItems((prev) => [...prev.slice(-49), payload]);
});
source.addEventListener("error", () => {
setConnected(false);
});
return () => {
source.close();
};
}, []);
return { connected, items };
}
代码例子三:断线续传、多标签页同步和工程级细节
很多人第一次用 SSE 会觉得它“比轮询高级、比 WebSocket 简单”,结果第二次上线就翻车。真正的坑一般在工程细节里。第一类坑是断线续传。浏览器会自动带 Last-Event-ID 回来,但前提是服务端真的给关键消息打了 id,并且你在服务端保留了一个可补发窗口。第二类坑是代理层缓冲。SSE 需要尽快把小块数据刷到客户端,反向代理如果启用了缓冲,前端就可能一直等到缓冲区满才看到结果。第三类坑是多标签页。BroadcastChannel 很适合广播事件结果,但它不会凭空帮你把多个连接变成一个连接。
如果只是想做标签页之间的数据同步,可以让每个页签收到数据后往 BroadcastChannel 转发;如果想真正减少连接数,就要引入协调层,比如选举一个 leader tab 持有 SSE,上游数据通过 BroadcastChannel 转发给其他页签,或者干脆用 SharedWorker 统一持有连接。这个差别一定要讲清楚。
- 要续传:给关键事件打
id,服务端保留最近一段事件日志。 - 要实时:关闭代理缓冲,定期发送心跳注释行。
- 要省连接:不要把 BroadcastChannel 误当成连接复用器。
- 要稳:优先跑在 HTTP/2 上,避免 HTTP/1.1 下的 6 连接限制。
app.get("/api/stream", (req, res) => {
const lastEventId = Number(req.get("Last-Event-ID") || 0);
const missed = eventLog.filter((item) => item.id > lastEventId);
for (const item of missed) {
res.write(`id: ${item.id}\n`);
res.write(`event: ${item.event}\n`);
res.write(`data: ${JSON.stringify(item.data)}\n\n`);
}
// 再把当前连接挂到实时广播列表
});
const bc = new BroadcastChannel("app_updates");
bc.onmessage = (event) => {
console.log("other tab says:", event.data);
};
function publishToOtherTabs(data) {
bc.postMessage(data);
}
// 注意:
// 这只是在标签页之间转发消息。
// 如果每个 tab 都 new EventSource(),连接数不会减少。
SSE 最适合和最不适合的场景
如果让我给一条简单规则,我会这么分。最适合 SSE 的,是浏览器拿结果、服务端吐进度或吐文本的场景,比如 AI 聊天文本流、搜索结果逐步补全、后台任务进度、告警推送、日志 tail、管理后台看板、订阅式业务通知。这些场景的共同特点是:数据流基本单向,丢一两条消息通常可以靠重连补回来,数据多数是文本或 JSON,而且浏览器原生就是主要消费端。
最不适合 SSE 的,则是服务端和客户端都要高频互发消息、二进制是主载荷、端到端时延要求很苛刻,或者连接要承载非常复杂的会话协议。例如协同编辑的光标同步、多人游戏状态同步、浏览器端音视频通话、远程设备控制、语音实时对话,这些更适合 WebSocket、WebRTC 或其他更强的实时通道。
如果你的问题是:
"服务端有新状态,尽快告诉浏览器"
先看 SSE
如果你的问题是:
"浏览器和服务端要持续双向对话"
先看 WebSocket
如果你的问题是:
"我要实时音视频或超低延迟语音"
先看 WebRTC
未来的技术演进:SSE 不会统治一切,但位置会更清楚
往后看,SSE 不会吃掉所有实时通信,但它的边界会越来越清楚。第一条演进线,是 fetch 流式读取会在很多场景里和 SSE 并存。因为原生 EventSource 很方便,但它的构造器非常克制,只给 URL 和 withCredentials。如果你需要 POST、需要自定义 header、需要更灵活的 backpressure 管理,开发者会越来越多地转向基于 fetch 和 ReadableStream 的 HTTP 流。
第二条演进线,是浏览器实时 AI 交互正在分层。文本输出继续偏向 SSE 或普通 HTTP 流,语音和多模态实时交互则明显走向 WebRTC。OpenAI 当前官方在浏览器侧 Realtime 场景里就明确推荐优先使用 WebRTC,而 gpt-realtime 也支持 WebRTC、WebSocket 和 SIP。这说明未来不会是某一个协议一统天下,而是文本流、控制流、媒体流各回各位。
第三条演进线,是 WebTransport 值得持续观察。MDN 已经把它描述成对 WebSocket 的现代化更新,基于 HTTP/3,支持多路流和类似 UDP 的 datagram,能解决一部分 TCP 头阻塞和协议灵活性问题。但它离成为通用默认选项还有工程成熟度、平台支持和生态配套要补。短期内它更像前沿武器,而不是普通团队的第一落点。
所以我对 2026 年后的判断是:SSE 不会替代 WebSocket,也不该被包装成“所有实时问题的新答案”;它真正的意义,是把一大类本来不该复杂化的问题重新拉回简单方案。换句话说,SSE 的价值不是更炫,而是让我们终于愿意承认,很多所谓“实时通信”其实只是“持续输出”。
- 文本流:SSE 和
fetchstreaming 会长期共存。 - 语音实时交互:WebRTC 会越来越像浏览器侧默认项。
- 复杂双向控制:WebSocket 仍有稳固地盘。
- 更前沿的低延迟多流场景:继续观察 WebTransport。
还没有评论,你可以写下第一条。