打开网易新闻 查看精彩图片

2015年,浏览器厂商往JavaScript里塞了一行代码,然后集体沉默。没人教,文档 bury 在角落,Stack Overflow 上搜到的全是 WebSocket 教程。结果过去十年,无数程序员为实时推送写了成千上万行重连逻辑、心跳检测、状态恢复——而答案早就躺在那儿,一行搞定。

这行代码是 new EventSource('/events')。它做的事情和 WebSocket 很像,但哲学完全相反:WebSocket 是"你们随便聊",EventSource(服务器发送事件,SSE)是"服务器说,客户端听"。

大多数实时场景根本不需要双向对话。股价跳动、构建日志、通知红点、直播弹幕——都是服务器推、客户端收。用 WebSocket 做这些,就像为了收快递专门雇个双向对讲的保安,SSE 则是门铃响了去开门,完事。

一行代码的暴力美学

一行代码的暴力美学

EventSource 的 API 设计带着一种老派的傲慢:连接、监听、完事。

```javascript const source = new EventSource('/events'); source.onmessage = e => document.querySelector('#feed').textContent = e.data; ```

没有 npm install,没有 onclose 回调,没有指数退避的重连算法。浏览器自动处理断线重连,规范强制要求。你写的代码只管消费消息,基础设施的脏活浏览器包了。

对比 WebSocket 的典型实现:onopen 握手、onerror 降级、onclose 启动定时器、重连后比对消息 ID 补漏。这些代码在无数项目里被复制粘贴,调试过凌晨三点的偶发断连,而 SSE 的用户根本没意识到这些问题存在。

Rust 的 axum 框架里,SSE 端点长这样:

```rust async fn events() -> Sse>> { let stream = stream::repeat_with(|| Event::default().data("update")) .map(Ok) .throttle(Duration::from_secs(1)); Sse::new(stream).keep_alive(KeepAlive::default()) } ```

打开网易新闻 查看精彩图片

返回一个流,运行时处理背压、保活、客户端断开。没有连接池管理,没有 upgrade 握手,就是普通 HTTP。每个代理都认识它,每个 CDN 都能透传它。

三个头部搞定一切

三个头部搞定一切

SSE 的协议规范短得像个玩笑:

``` Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive ```

服务器发以 data: 开头的行,浏览器解析。没有帧编码,没有掩码,没有操作码。文本流,换行分割,HTTP 从头到尾。

WebSocket 为了兼容 HTTP 握手,先伪装成 HTTP,再 upgrade 成独立协议。这个设计在 2011 年情有可原——当时很多中间件不认识 WebSocket——但现在成了历史包袱。防火墙、负载均衡、企业代理,每多一层都可能 upgrade 失败。

SSE 不走 upgrade,它就是 HTTP。公司内网的老旧代理?认识。需要走 CDN 边缘缓存?可以。想加个认证中间件?标准 Cookie/Header 直接复用。

协议简单到作弊。

自动恢复:被低估的杀手特性

自动恢复:被低估的杀手特性

WebSocket 断线后,开发者要面对一堆决策:多久重试?退避策略用线性还是指数?重连后怎么知道漏了哪些消息?要不要服务端维护消息队列?

打开网易新闻 查看精彩图片

SSE 的规范直接规定了自动重连,浏览器实现它。断线后,浏览器按退避算法自己连,连上后继续收。服务端不需要为"客户端刚重连"这个状态写特殊逻辑。

当然,消息丢失问题还在。但 SSE 支持 Last-Event-ID 头部,重连时自动带上最后收到的消息 ID,服务端可以据此补发。这个机制是可选的,简单场景直接忽略,复杂场景按需实现——而不是像 WebSocket 那样,重连逻辑是必答题。

一个类比:WebSocket 像租了辆跑车,动力强劲但得自己保养;SSE 像地铁,时刻表固定,坏了等下一班,你不用管调度。

什么时候该用 WebSocket?

什么时候该用 WebSocket?

不是全面替代。聊天室、多人协同编辑、实时游戏——这些需要客户端频繁发消息的,WebSocket 的双向通道更自然。但问问自己:你的功能真的需要双向吗?

Dashboard 实时数据、CI 构建日志、股票行情、社交 Feed、进度条——这些占"实时功能"的绝大多数。它们用 SSE 更轻、更稳、代码更少。

有个细节很多人没注意:SSE 天然支持浏览器的事件流接口,可以 addEventListener 监听特定事件类型,做路由分发。WebSocket 收到消息要自己解析路由

另一个被忽略的点:SSE 基于 HTTP,所以能享受 HTTP/2 的多路复用。一个 TCP 连接可以承载多个 SSE 流,浏览器自动调度。WebSocket 一个连接就是一个连接,开多了就是资源竞争。

技术选型里,"够用且简单"往往胜过"强大但复杂"。

原文作者 Vivian Voss 在文末留了句话:「One rather appreciates infrastructure that does not require a babysitter.」(人总该欣赏那些不需要保姆的基础设施。)

这句话的刻薄之处在于,它暗示了很多程序员正在做的事情——写重连逻辑、调心跳间隔、排查代理问题——本可以不存在。不是技术深度的问题,是信息差的问题。2015 年就有的标准,2025 年还在被忽视。

下次设计实时功能前,你会先问自己"真的需要双向吗",还是直接 npm install ws?