为实时仪表盘解释 WebSockets 与 Server-Sent Events 的差异,给出选择规则、扩展要点,以及连接断开时的应对策略。

实时仪表盘本质上是一个承诺:数据会在你不刷新页面的情况下更新,你看到的内容应接近实时。人们希望更新感觉很快(通常在一两秒内),但也希望页面保持平稳。不要闪烁、图表不要跳动、不要每几分钟就出现“断开连接”横幅。
大多数仪表盘不是聊天应用。它们主要是从服务器推送更新到浏览器:新的指标点、状态变更、新的一批行或告警。常见的形态很熟悉:指标面板(CPU、注册、收入)、告警面板(绿/黄/红)、日志尾(最新事件)或进度视图(任务从 63% 到 64%)。
在 WebSockets 与 Server-Sent Events(SSE)之间的选择不仅仅是技术偏好。它会影响你要写多少代码、需要处理多少奇怪的边缘情况,以及当用户从 50 增长到 5,000 时成本有多高。有些选项更容易做负载均衡,有些让重连和补发逻辑更简单。
目标很简单:一个保持准确、响应迅速且不会随着规模增长成为值班噩梦的仪表盘。
WebSockets 和 Server-Sent Events 都通过保持连接打开来让仪表盘可以在不轮询的情况下更新。不同之处在于对话如何进行。
一句话说 WebSockets:一个单一的长连接,浏览器和服务器都可以随时发送消息。
一句话说 SSE:一个长时间的 HTTP 连接,服务器不断向浏览器推送事件,但浏览器不会在同一条流上发送消息回去。
这个差异通常决定了哪种方式感觉更自然。
一个具体例子:仅展示收入、活跃试用和错误率的销售 KPI 看板可以很舒服地运行在 SSE 上。用户下订单、收到确认并且每个操作都需要即时反馈的交易屏幕更像是 WebSocket 的场景。
无论你选择哪种方式,有几件事不会改变:
传输只是最后一公里。困难的部分很多时候在两者之间是共通的。
主要差别在于谁能发言,以及何时发言。
使用 Server-Sent Events,浏览器打开一个长连接,只有服务器沿着这条通道发送更新。使用 WebSockets,连接是双向的:浏览器和服务器都可以随时发送消息。
对于许多仪表盘,大多数流量是从服务器到浏览器。想想“新订单到达”、“CPU 到 73%”、“工单数量变化”。SSE 很适合这种客户端主要是监听的形态。
当仪表盘也是一个控制面板时,WebSockets 更有意义。如果用户需要频繁发送操作(确认告警、改变共享筛选、协作),双向消息通常比不断创建新的请求更干净。
消息负载通常都是简单的 JSON 事件,无论哪种方式。一个常见模式是发送一个小信封以便客户端能够安全地路由更新:
{"type":"metric","name":"active_users","value":128,"ts":1737052800}
Fan-out(广播)是仪表盘变得有趣的地方:一次更新通常需要同时到达许多观众。SSE 和 WebSockets 都可以向成千上万条打开的连接广播相同事件。不同之处在于运行方式:SSE 表现得像一个长时间的 HTTP 响应,而 WebSockets 在升级后切换到单独的协议。
即便有实时连接,你仍会使用普通的 HTTP 请求来处理初始页面加载、历史数据、导出、创建/删除动作、认证刷新以及不属于实时流的大型查询。
一个实用规则:把实时通道留给小而频繁的事件,把 HTTP 留给其他一切。
如果你的仪表盘只需要向浏览器推送更新,SSE 通常在简单性上占优。它是一个保持打开的 HTTP 响应,按发生顺序发送文本事件。更少的环节意味着更少的边缘情况。
当客户端必须频繁回传时,WebSockets 很棒,但这种自由增加了你必须维护的代码量。
使用 SSE,浏览器连接、监听并处理事件。重连和基本重试行为对大多数浏览器来说是内建的,所以你可以把更多精力放在事件负载上,而不是连接状态管理。
使用 WebSockets,你很快会把 socket 生命周期管理作为一等功能:connect、open、close、error、reconnect,有时还要做 ping/pong。如果你有许多消息类型(筛选、命令、确认、类似 presence 的信号),你还需要在客户端和服务器上实现消息信封和路由。
一个好的经验法则:
SSE 常常更容易调试,因为它表现得像常规 HTTP。你通常可以在浏览器开发者工具中清晰看到事件,许多代理和可观测性工具已经很好地理解 HTTP。
WebSockets 可能会以不那么明显的方式失败。常见问题包括负载均衡器产生的静默断开、空闲超时,以及一方认为连接仍然存在而另一方已断开的“半开”连接。你通常只在用户报告仪表盘停滞时才注意到问题。
示例:如果你构建的是只需要实时总数和最近订单的销售仪表盘,SSE 会让系统保持稳定且易读。如果同一页面还必须发送快速的用户交互(共享筛选、协作编辑),WebSockets 的额外复杂性可能是值得的。
当一个仪表盘从几个观看者增长到数千时,主要问题并不是原始带宽,而是你必须保持活跃的连接数量,以及当一些客户端较慢或不稳定时会发生什么。
在 100 个观看者时,两种选择感觉相似。在 1,000 个时,你开始关注连接限制、超时和客户端重连频率。在 50,000 个时,你就运行着一个连接密集的系统:每个客户端额外缓存的每一千字节都可能变成真实的内存压力。
扩展差异通常在负载均衡器处显现。
WebSockets 是长期存在的双向连接,所以许多架构需要会话粘滞(sticky sessions),除非你有共享的 pub/sub 层并且任何服务器都能处理任何用户。
SSE 也是长连接,但它是普通的 HTTP,因此更容易与现有代理配合并且在 fan-out 时更顺畅。
让服务器保持无状态通常用 SSE 更简单:服务器可以从共享流推送事件,而无需记住太多每个客户端的状态。使用 WebSockets 时,团队往往会存储每个连接的状态(订阅、最后看到的 ID、认证上下文),这会使横向扩展变得更棘手,除非你一开始就为此设计。
慢客户端会在这两种方法中悄悄地损害系统。注意以下故障模式:
针对热门仪表盘的简单规则:保持消息小、发送频率低于你预期,并愿意丢弃或合并更新(例如只发送最新的指标值),以免一个慢客户端拖垮整个系统。
实时仪表盘以平凡的方式失败:笔记本睡眠、Wi-Fi 切换网络、移动设备走入隧道或浏览器挂起后台标签页。你的传输选择比不上当连接断开时你如何恢复来的重要。
使用 SSE,浏览器有内建的重连。如果流中断,会在短暂延迟后重试。许多服务器也支持使用事件 id 回放(通常通过类似 Last-Event-ID 的头)。这让客户端可以说:“我上次看到的是事件 1042,把我错过的发来”,这是一条简单的弹性路径。
WebSockets 通常需要更多的客户端逻辑。当 socket 关闭时,客户端应带退避和抖动重试(以免成千上万客户端同时重连)。重连后,你还需要明确的重新订阅流程:必要时再次认证,然后重新加入正确的频道,再请求任何错过的更新。
更大的风险是静默的数据缺口:UI 看起来正常,但数据已过时。使用以下模式之一以证明仪表盘是最新的:
示例:显示“每分钟订单数”的销售仪表盘如果每 30 秒刷新一次总量可以容忍短暂的缺口。交易仪表盘则不行;它需要序列号并在每次重连时提供快照。
实时仪表盘保持长时间连接打开,因此小的认证错误可能会持续数分钟或数小时。安全性更多关乎你如何认证、授权和使访问过期,而不是传输本身。
从基础做起:使用 HTTPS 并将每个连接视为必须过期的会话。如果你依赖会话 Cookie,确保其作用域正确并在登录时轮换。如果使用 token(如 JWT),让它们短期有效并计划客户端如何刷新它们。
一个实用的陷阱:浏览器 SSE(EventSource)不允许设置自定义头。这通常促使团队使用基于 Cookie 的认证,或把 token 放在 URL 中。URL token 可能会通过日志或复制粘贴泄露,因此如果必须使用,保持短期有效并避免记录完整查询字符串。WebSockets 通常更灵活:你可以在握手期间认证(Cookie 或查询字符串),或在连接后立即通过一条认证消息完成认证。
对于多租户仪表盘,请在连接时和每次订阅时都进行授权。用户应该只能订阅他们拥有的流(例如 org_id=123),服务器即使客户端请求更多也必须强制执行这一点。
为减少滥用,请限制并监控连接使用情况:
这些日志是你的审计轨迹,也是快速解释为什么某人看到空白仪表盘或他人的数据的最快方法。
先从一个问题开始:你的仪表盘主要是在看,还是也在频繁地回写?如果浏览器主要接收更新(图表、计数器、状态灯),而用户动作是偶发的(更改筛选、确认告警),就把实时通道设为单向。
接着,向前看 6 个月。如果你预计会有大量交互功能(内联编辑、类似聊天的控件、拖放操作)和许多事件类型,请为能清晰处理双向通信的通道做规划。
然后决定视图必须有多准确。如果可以容忍丢失一些中间更新(因为下一个更新会替代旧状态),就可以优先简单性。如果需要精确回放(每个事件都重要、审计、金融行情),无论使用哪种传输,你都需要更强的序列、缓冲和重同步逻辑。
最后,估算并发和增长。成千上万的被动观看者通常会把你推向更好配合 HTTP 基础设施和更易横向扩展的选项。
选择 SSE 当:
选择 WebSockets 当:
如果你犹豫不决,先选 SSE 以满足典型的以读为主的仪表盘需求,只有当双向需求变得真实且持续时再切换。
最常见的失败始于选择了比仪表盘需要更复杂的工具。如果 UI 只需要服务器到客户端的更新(价格、计数、任务状态),使用 WebSockets 可能会增加额外的复杂性而带来有限收益。团队最终花时间调试连接状态和消息路由,而不是仪表盘本身。
重连又是一个陷阱。重连通常只恢复连接,而不是丢失的数据。如果用户的笔记本睡眠了 30 秒,他们可能会错过事件,仪表盘会显示错误的总数,除非你设计了补发步骤(例如:最后看到的事件 id 或自某时间点以来的变化,然后重新获取)。
高频广播可能悄悄把你拖垮。发送每一个微小变化(每行更新、每个 CPU 计时)会增加负载、网络噪音和 UI 抖动。批处理和限流往往让仪表盘感觉更快,因为更新以干净的块到达。
注意这些生产环境的陷阱:
示例:支持团队的仪表盘显示实时工单计数。如果你对每张票的每次变化都即时推送,座席会看到数字闪烁并且在重连后有时会后退。更好的做法是每 1–2 秒发送一次更新,并在重连时先获取当前总数再恢复事件流。
想象一个 SaaS 管理仪表盘,显示计费指标(新订阅、流失、MRR)和事故告警(API 错误、队列积压)。大多数观看者只是看数字并希望它们在不刷新页面的情况下更新。只有少数管理员会采取行动。
早期,先用满足需求的最简单流。SSE 往往足够:服务器单向推送指标更新和告警消息到浏览器。这样要管理的状态更少,边缘情况更少,重连行为更可预测。如果错过了一个更新,下一个消息可以包含最新总数以便 UI 快速自愈。
几个月后,使用量增长且仪表盘变得可交互。现在管理员希望有实时筛选(更改时间窗口、切换区域)甚至协作(两个管理员同时确认同一告警并立即看到更新)。这时选择可能会翻转。双向消息使得在同一通道上回传用户动作并保持共享 UI 状态更容易。
如果需要迁移,请以安全的方式进行而不是一夜切换:
在把实时仪表盘交给真实用户之前,假设网络会不稳定并且部分客户端会很慢。
给每个更新一个唯一事件 ID 和时间戳,并写下你的排序规则。如果两个更新乱序到达,哪个胜出?当重连回放旧事件或多个服务发布更新时,这很重要。
重连必须是自动且礼貌的。使用退避(开始快,随后更慢)并在用户登出后停止无限重试。
还要决定当数据陈旧时 UI 的表现。例如:如果 30 秒内没有更新,图表变灰、暂停动画并显示明显的“陈旧”状态,而不是默默显示旧数字。
为每个用户设置限制(连接数、每分钟消息数、负载大小),以免一个标签风暴拖垮其他人。
跟踪每个连接的内存并处理慢客户端。如果浏览器跟不上,不要让缓冲无限增长。断开连接、发送更小的更新或切换到周期性快照。
记录连接、断开、重连和错误原因。对打开连接的异常激增、重连率和消息积压进行告警。
保留一个简单的紧急开关以禁用流并回退到轮询或手动刷新。当凌晨两点出了问题时,你需要一个安全的选项。
在关键数字附近显示“最后更新”,并提供手动刷新按钮。这会减少支持工单并帮助用户信任他们看到的数据。
有意从小处开始。先选一个流(例如 CPU 与请求率,或仅告警),并写下事件契约:事件名、字段、单位以及更新频率。清晰的契约能防止前端与后端走样。
做一个可丢弃的原型,关注行为而非外观。让 UI 展示三种状态:正在连接、实时以及重连后正在追赶。然后强制故障:杀掉标签页、切换飞行模式、重启服务器,观察仪表盘如何表现。
在扩大流量前,决定如何从缺口中恢复。一个简单方法是在连接时发送快照(或重连时发送),然后切回实时更新。
在更广泛上线前要做的实务步骤:
如果你进度很快,Koder.ai (koder.ai) 可以帮你快速原型完整流程:一个 React 仪表盘 UI、一个 Go 后端,以及从聊天提示构建的数据流,支持导出源码和部署选项,当你准备好时即可使用。
一旦你的原型在糟糕的网络条件下幸存下来,扩展基本上是重复工作:增加容量、持续测量延迟,并让重连路径变得无聊且可靠。
在浏览器主要接收更新且服务器主要广播时,使用 SSE。它非常适合指标、告警、状态灯和“最新事件”面板,用户操作是偶发的并且可以通过普通 HTTP 请求处理。
当仪表盘同时是一个控制面板,且客户端需要频繁、低延迟地发送操作时,请选择 WebSockets。如果用户不断发送命令、确认、协作更改或其他实时输入,双向消息通常用 WebSockets 更简单。
SSE 是一个长时间打开的 HTTP 响应,服务器向浏览器推送事件。WebSockets 会将连接升级到一个单独的双向协议,双方都可以随时发送消息。对于以读取为主的仪表盘,这种额外的双向灵活性通常是不必要的开销。
给每个更新添加事件 ID(或序列号),并保留一条清晰的“补偿”路径。重连时,客户端应尝试回放丢失的事件(如可行),或先获取一个最新快照,然后再恢复实时更新,确保 UI 正确。
把陈旧视为真实的 UI 状态,而不是隐藏的故障。在关键数字附近显示“最后更新”/“Last updated”,如果一段时间内没有事件到达,就把视图标记为陈旧,让用户知道数据可能过期,而不是默认信任旧数据。
保持消息小且不要发送每一个微小变化。合并频繁更新(发送最新值而不是每个中间值),并使用周期性快照来统计总量。扩展时最常见的问题通常是打开的连接数量和慢客户端,而不是原始带宽。
慢客户端会导致服务器缓冲增长并消耗每个连接的内存。为每个客户端设置排队数据上限,在客户端跟不上时丢弃或限流更新,并优先发送“最新状态”消息而不是长时间积压的历史数据,以保持系统稳定。
像对待会话一样对每个流进行认证和授权。浏览器中的 SSE(EventSource)通常不支持自定义头,这通常会促使团队使用基于 Cookie 的认证或把 token 放在 URL 中。URL token 可能会在日志或复制粘贴中泄露,所以若使用则应短时有效并避免记录完整查询字符串。WebSockets 通常在握手阶段或连接后用第一条消息完成认证。在任何情况下,服务端必须对租户和流权限进行强制检查,而不要只依赖客户端。
把小而频繁的事件放到实时通道,繁重的查询和大响应放到普通 HTTP 请求。初始页面加载、历史查询、导出和大型响应更适合常规请求,而实时通道应承载轻量更新以保持 UI 的最新状态。
并行运行一段时间,将相同事件同时镜像到两个通道。先把一小部分用户迁移过去,在真实条件下测试重连和服务器重启,然后逐步放大。短期内保留旧路径作为回退可以显著降低风险。