<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>技术选型 on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/</link>
        <description>Recent content in 技术选型 on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Fri, 12 Jun 2026 00:42:56 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>WebSocket 是个好东西，但你不需要它——从 AI 流式到实时推送，SSE 的逆袭</title>
            <link>https://www.wujiachen.com.cn/posts/sse-vs-websocket/</link>
            <pubDate>Fri, 12 Jun 2026 00:42:55 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/sse-vs-websocket/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/cover.png&#34; alt=&#34;Featured image of post WebSocket 是个好东西，但你不需要它——从 AI 流式到实时推送，SSE 的逆袭&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;WebSocket 是个好东西。真的，我没在阴阳。&lt;/p&gt;&#xA;&lt;p&gt;2008 年诞生，2011 年标准化，它解决了 HTTP 协议的一个根本缺陷：服务端不能主动给客户端发消息。有了 WebSocket，实时推送、在线协作、即时通讯——这些以前要靠轮询或长轮询才能勉强实现的功能——一下子变得自然了。&lt;/p&gt;&#xA;&lt;p&gt;但问题恰恰在这里。&lt;/p&gt;&#xA;&lt;p&gt;因为 WebSocket 解决了&amp;quot;实时推送&amp;quot;这个痛点，它变成了一个默认选项。看到&amp;quot;实时&amp;quot;两个字，很多团队不加思考就选了 WebSocket。就像看到&amp;quot;性能优化&amp;quot;就想到加缓存一样——不是不对，是太粗糙了。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;大多数只需要单向推送的场景，WebSocket 是过度设计。而且这不是技术判断，是成本账。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;一你的-websocket-可能是个豪华车&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e4%bd%a0%e7%9a%84-websocket-%e5%8f%af%e8%83%bd%e6%98%af%e4%b8%aa%e8%b1%aa%e5%8d%8e%e8%bd%a6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、你的 WebSocket 可能是个&amp;quot;豪华车&amp;quot;&#xA;&lt;/h2&gt;&lt;p&gt;我之前在一个后台系统项目里做过推送功能。&lt;/p&gt;&#xA;&lt;p&gt;需求很简单：服务端任务状态变更时，通知前端更新界面。典型的管理后台场景——任务队列进度、系统告警、数据同步状态。没有一个场景需要前端往服务端发消息。&lt;/p&gt;&#xA;&lt;p&gt;你猜我们选了什么？&lt;/p&gt;&#xA;&lt;p&gt;WebSocket。&lt;/p&gt;&#xA;&lt;p&gt;理由当时觉得非常充分：&amp;ldquo;实时推送嘛，当然用 WebSocket。稳定、成熟、大家都在用。&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;然后踩坑就开始了。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;首先是连接维护。&lt;/strong&gt; WebSocket 建连要走 6 次消息交换——TCP 三次握手 + HTTP 升级 + WS 帧交换。而且建连只是开始，连接建立后每隔几十秒要发一次心跳，否则代理层（Nginx、ALB 等）默认超时会断开连接。每个连接服务端都要维护状态。前端还需要自己实现重连逻辑——因为 WebSocket 没有内置重连。你需要在客户端维护一个完整的连接状态机：连接中、已连接、断开中、重连中——每个状态都要处理边界情况。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;基础设施那边也不省心。&lt;/strong&gt; 负载均衡器要开粘性会话，不然客户端换了个实例连接就断了。Nginx 要配 &lt;code&gt;proxy_set_header Upgrade&lt;/code&gt; 和 &lt;code&gt;proxy_set_header Connection &amp;quot;upgrade&amp;quot;&lt;/code&gt;。AWS ALB 要开 WebSocket 支持。防火墙要放行 WebSocket 端口。如果是公司内部网络，可能还要申请安全策略变更——一套流程下来两三天。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;运维更烦人。&lt;/strong&gt; 连接断开的场景太多了——浏览器休眠、网络切换、代理超时。每断一次，前端要重新走一遍握手流程。而且 WebSocket 的调试比 HTTP 复杂得多，出了问题上 Wireshark 抓包才能看清。HTTP 接口出问题，curl 一下就知道。WebSocket 呢？你得装专门的 WebSocket 客户端工具，还要理解帧结构。&lt;/p&gt;&#xA;&lt;p&gt;回头看，这个场景的实际需求只有一条：&lt;strong&gt;&amp;ldquo;服务端有更新时通知前端&amp;rdquo;&lt;/strong&gt;。纯单向。这不只是管理后台的问题——2025-2026 年增长最快的实时通信场景，AI 流式输出，也是这个模式：你发一个请求，服务端把 Token 一条一条吐回来。同样是纯单向。&lt;/p&gt;&#xA;&lt;p&gt;而我们选择了一个为&amp;quot;双向实时通信&amp;quot;设计的协议，为不需要的能力付了全价。&lt;/p&gt;&#xA;&lt;p&gt;这不是个例。我见过太多团队掉进同一个坑里：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一个内部运营后台，用 WebSocket 做通知推送——需求就是&amp;quot;任务完成时弹个 toast&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;一个数据看板，用 WebSocket 推送图表更新——需求就是&amp;quot;新数据来了刷新图表&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;一个告警系统，用 WebSocket 推送告警消息——需求就是&amp;quot;有告警时显示在屏幕上&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;一个 DevOps 面板，用 WebSocket 推送部署日志——需求就是&amp;quot;日志一行一行出来&amp;quot;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;它们大部分是单向的——至少单向占了 90% 以上的流量。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/p1-%e5%9b%9b%e7%a7%8d%e5%8d%95%e5%90%91%e5%9c%ba%e6%99%af.png&#34; alt=&#34;四种典型的&amp;#34;单向场景但用了 WebSocket&amp;#34;的对比插图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;你可能会说：&amp;ldquo;用 WebSocket 也没什么大问题吧？反正也能工作。&amp;rdquo; 对，能工作。就像一个超市用冷藏车运矿泉水一样——能运到，但成本不对。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;二websocket-的奢侈税&#34;&gt;&lt;a href=&#34;#%e4%ba%8cwebsocket-%e7%9a%84%e5%a5%a2%e4%be%88%e7%a8%8e&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、WebSocket 的&amp;quot;奢侈税&amp;quot;&#xA;&lt;/h2&gt;&lt;p&gt;单向推送用 WebSocket，到底多花了多少冤枉钱？我们来算一笔账。&lt;/p&gt;&#xA;&lt;h3 id=&#34;建连成本&#34;&gt;&lt;a href=&#34;#%e5%bb%ba%e8%bf%9e%e6%88%90%e6%9c%ac&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;建连成本&#xA;&lt;/h3&gt;&lt;p&gt;WebSocket 建连需要 2 个网络往返（RTT），而 SSE 只需要 1 个。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;WebSocket 建连：TCP SYN → SYN-ACK → ACK → HTTP Upgrade → 101 Switching → 数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;SSE 建连：     HTTP GET → 200 OK + 流式数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;建连阶段，WebSocket 比 SSE 多 1 个 RTT + 约 400 字节的 HTTP 头。这些额外头包括：&lt;code&gt;Upgrade: websocket&lt;/code&gt;、&lt;code&gt;Connection: Upgrade&lt;/code&gt;、&lt;code&gt;Sec-WebSocket-Key&lt;/code&gt;（16 字节随机值）、&lt;code&gt;Sec-WebSocket-Version: 13&lt;/code&gt;。服务端回复的 &lt;code&gt;101 Switching Protocols&lt;/code&gt; 还包含 &lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt; 头。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/p2-%e5%bb%ba%e8%bf%9e%e5%af%b9%e6%af%94.png&#34; alt=&#34;WS 6 次握手 vs SSE 1 次 HTTP 请求的建连过程对比&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;可能你会说：&amp;ldquo;才 1 个 RTT，不在乎。&amp;rdquo; 但连接不是一次性的——每次断线重连，你都要多付这 1 个 RTT。如果客户端网络不稳定（移动端尤其），一天断连十几次，累计的成本就很可观了。&lt;/p&gt;&#xA;&lt;h3 id=&#34;维护成本&#34;&gt;&lt;a href=&#34;#%e7%bb%b4%e6%8a%a4%e6%88%90%e6%9c%ac&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;维护成本&#xA;&lt;/h3&gt;&lt;p&gt;WebSocket 没有内置心跳，但生产环境中几乎每个实现都要加。具体间隔取决于代理层配置（Nginx 的 &lt;code&gt;proxy_read_timeout&lt;/code&gt; 默认 60s，ALB 默认 350s），通常每 30-60 秒一次 Ping/Pong，双向交换。按 30 秒间隔算，每小时每连接约 120 次心跳。&lt;/p&gt;&#xA;&lt;p&gt;SSE 也不需要心跳——但生产环境中同样需要维持连接。区别在于 SSE 的心跳是服务端单向发送的注释行（&lt;code&gt;: keepalive\n\n&lt;/code&gt;），不需要客户端响应。这比 WebSocket 的 Ping/Pong 省了一半的带宽和一次额外的处理开销。&lt;/p&gt;&#xA;&lt;p&gt;从协议状态的角度看，差距更大。WebSocket 服务端需要为每个连接维护的状态比 SSE 复杂得多——TCP 连接状态、协议帧缓冲、掩码键、应用层会话、粘性会话绑定。大致来说，每个连接约 2-5 KB 的协议层状态开销。而 SSE 走标准 HTTP 响应，每个连接约 0.5-1 KB——少了约 75%。在千级连接规模下，这意味着几 MB 到十几 MB 的内存差异。对单台服务器来说不算大，但如果你有几十台实例，差距就出来了。&lt;/p&gt;&#xA;&lt;p&gt;还有一个很多人忽略的点：WebSocket 的粘性会话要求意味着你无法使用轮询（round-robin）负载均衡。如果你的某个实例挂了，上面维护的所有 WebSocket 连接都会中断。而 SSE 基于标准 HTTP，任何实例都可以接管。&lt;/p&gt;&#xA;&lt;p&gt;这就是差距。&lt;/p&gt;&#xA;&lt;h3 id=&#34;数据说话&#34;&gt;&lt;a href=&#34;#%e6%95%b0%e6%8d%ae%e8%af%b4%e8%af%9d&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;数据说话&#xA;&lt;/h3&gt;&lt;p&gt;光算理论太虚。我在本地跑了一组 benchmark：1000 个并发连接，每个连接推送 100 条消息，模拟典型的管理后台推送场景。&lt;/p&gt;&#xA;&lt;p&gt;服务端用 Go 实现，SSE 走标准 &lt;code&gt;text/event-stream&lt;/code&gt;，WebSocket 走简易文本帧（无 mask，纯服务端推送）。两者推送相同内容（JSON 格式，每条约 200 字节），客户端用并发 HTTP 请求模拟。&lt;/p&gt;&#xA;&lt;p&gt;测试环境：Apple M4 Pro / Go 1.26.4&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;指标&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;SSE&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;WebSocket&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;差异&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;耗时&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1125ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;2324ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;WebSocket 慢 106%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;内存分配&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;75MB&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;93MB&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;WebSocket 多 23%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;分配次数&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;552K&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;730K&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;WebSocket 多 32%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;strong&gt;在同等推送量的单向场景下，WebSocket 的耗时是 SSE 的两倍多，内存分配多近四分之一。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/p3-benchmark%e6%9f%b1%e7%8a%b6%e5%9b%be.png&#34; alt=&#34;benchmark 数据可视化——柱状图对比 SSE vs WS 的耗时/内存/分配次数&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这不是说 WebSocket 性能差——它只是在做它该做的事：维护双向通道、支持帧级控制、处理掩码。问题是，这些能力在你的场景中用不上。你为&amp;quot;双向&amp;quot;付了钱，但只用了&amp;quot;单向&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;有人可能会说：&amp;ldquo;那用 WebSocket 做单向推送，加上心跳，成本也没高到哪去吧？&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;算一笔账。假设你有 1 万个在线连接，每个连接每 30 秒一次心跳。WebSocket 每小时需要处理约 120 万次 Ping/Pong 交换。这些心跳本身也是资源——CPU 处理帧、网络带宽、中断处理。SSE 的注释行虽然也占带宽，但不需要客户端响应，服务端少了一半的工作。&lt;/p&gt;&#xA;&lt;p&gt;做会议机器人基础设施的 recall.ai 在 2024 年分享过他们的经验：WebSocket 导致的额外 CPU 开销让他们每年多付了约 $100 万的 AWS 费用。通过用共享内存替代 WebSocket 做进程间通信，他们将 CPU 使用率降低了 50%，节省了超过 $100 万/年的 AWS 成本。虽然 recall.ai 的优化方案不是简单的 SSE 替换，但故事的核心是一样的——&lt;strong&gt;你为 WebSocket 的&amp;quot;双向&amp;quot;能力付出的隐性成本，远比你想的大。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;三ai-来了sse-的春天来了&#34;&gt;&lt;a href=&#34;#%e4%b8%89ai-%e6%9d%a5%e4%ba%86sse-%e7%9a%84%e6%98%a5%e5%a4%a9%e6%9d%a5%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、AI 来了，SSE 的春天来了&#xA;&lt;/h2&gt;&lt;p&gt;如果 SSE 只是&amp;quot;更轻量的推送方案&amp;quot;，它不会成为 2026 年的热门话题。真正让 SSE 翻身的，是 AI。&lt;/p&gt;&#xA;&lt;p&gt;想想看，LLM 的流式输出是什么样的？你发一个 Prompt 过去，服务端把 Token 一个接一个吐回来。这是一次 POST 请求 + 持续的、单向的 Token 流。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;这就是 SSE 最擅长的模式。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;OpenAI 的 Chat Completions API 用 SSE，Anthropic 的 Messages API 用 SSE，Google Gemini 的 StreamGenerateContent 也用 SSE。这不是巧合——在&amp;quot;一问多答&amp;quot;的场景下，SSE 是天然的匹配。具体来说：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;OpenAI&lt;/strong&gt;：&lt;code&gt;/v1/chat/completions&lt;/code&gt; 带上 &lt;code&gt;stream: true&lt;/code&gt;，返回的就是 &lt;code&gt;text/event-stream&lt;/code&gt; 格式。每条事件包含一个 &lt;code&gt;data: {&amp;quot;choices&amp;quot;:[{&amp;quot;delta&amp;quot;:{&amp;quot;content&amp;quot;:&amp;quot;token&amp;quot;}}]}&lt;/code&gt;，最后以 &lt;code&gt;data: [DONE]&lt;/code&gt; 结束&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Anthropic&lt;/strong&gt;：&lt;code&gt;/v1/messages&lt;/code&gt; 带上 &lt;code&gt;stream: true&lt;/code&gt;，返回 SSE 流。事件类型包括 &lt;code&gt;message_start&lt;/code&gt;、&lt;code&gt;content_block_delta&lt;/code&gt;、&lt;code&gt;message_stop&lt;/code&gt; 等，粒度比 OpenAI 更细&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Google Gemini&lt;/strong&gt;：&lt;code&gt;/v1/models/{model}:streamGenerateContent&lt;/code&gt;，同样是 SSE 格式&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;而 WebSocket 在这个场景下反而尴尬：你需要先建立双向通道，然后告诉服务端&amp;quot;我只收不发&amp;quot;，然后维持一个不需要双向能力的双向连接。相当于你买了一辆能跑 300 码的跑车，每天只开 30 码去菜市场。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/p4-AI%e6%b5%81%e5%bc%8f%e6%97%b6%e5%ba%8f%e5%9b%be.png&#34; alt=&#34;AI 流式输出时序图——POST 请求发出后 Token 逐字推送的 SSE 流式过程&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;AI 流式输出不是一个边缘场景——从 2023 到 2026 年，OpenAI、Anthropic、Google 的流式 API 调用量增长了数个数量级，它已经成为实时通信中增长最快的场景之一。&lt;/strong&gt; 大多数 AI 应用的背后，都有一条 SSE 数据流。Claude、Gemini、通义千问、文心一言——所有主流 LLM 的流式 API 都基于 SSE。&lt;/p&gt;&#xA;&lt;p&gt;这个趋势把 SSE 从一个&amp;quot;有人用但不多&amp;quot;的协议，推到了基础设施的位置。就像 HTTP/2 普及让长连接不再是问题一样，AI 普及让 SSE 从一个&amp;quot;替代方案&amp;quot;变成了&amp;quot;默认方案&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;如果你正在开发一个 AI 应用——不管是大模型对话、代码生成、还是文档总结——你大概率已经在用 SSE 了，只是你可能没意识到它叫 SSE。你的前端代码里可能早就写好了 &lt;code&gt;onmessage&lt;/code&gt; 回调，只是你以为那是 WebSocket。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;四http2-帮-sse-补上了最后一课&#34;&gt;&lt;a href=&#34;#%e5%9b%9bhttp2-%e5%b8%ae-sse-%e8%a1%a5%e4%b8%8a%e4%ba%86%e6%9c%80%e5%90%8e%e4%b8%80%e8%af%be&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、HTTP/2 帮 SSE 补上了最后一课&#xA;&lt;/h2&gt;&lt;p&gt;SSE 有一个历史问题：HTTP/1.1 下，每个源最多 6 个并发连接。如果一个页面需要同时打开多个 SSE 通道（比如一个推通知、一个推数据更新），超过 6 个后新的连接会被阻塞。&lt;/p&gt;&#xA;&lt;p&gt;这是 SSE 最大的硬伤，也是很多人放弃它的理由。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;但这个限制在 HTTP/2 时代已经不存在了。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;HTTP/2 的多路复用（Multiplexing）允许在单个 TCP 连接上承载最多 100 个并发流。每个 SSE 连接对应一个流，帧（frame）在连接上交错传输。&lt;/p&gt;&#xA;&lt;p&gt;换句话说，HTTP/1.1 下你需要 6 个 TCP 连接来跑 6 个 SSE 通道，HTTP/2 下 1 个 TCP 连接就够了——而且能跑 100 个。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;HTTP/1.1 + SSE：&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;浏览器 ←─── TCP 连接 1 (SSE 通道 1) ───→ 服务器&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ←─── TCP 连接 2 (SSE 通道 2) ───→ 服务器&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ←─── ...（最多 6 个，第 7 个排队等待）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;HTTP/2 + SSE：&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;浏览器 ←─── 1 个 TCP 连接 ───→ 服务器&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              ├── 流 1 (SSE 通道 1)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              ├── 流 2 (SSE 通道 2)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              ├── 流 3 (SSE 通道 3)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              ├── ...（最多 100 个流）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不仅如此，HTTP/2 还带来了头部压缩（HPACK）。HTTP/1.1 下每个 SSE 响应都重复发送相同的 HTTP 头（Content-Type、Cache-Control 等），HPACK 压缩后这些重复头的开销几乎为零。&lt;/p&gt;&#xA;&lt;p&gt;2026 年，Chrome、Firefox、Safari 15+ 都已默认启用 HTTP/2。大部分生产环境的反向代理（Nginx 1.9+ 需主动开启 &lt;code&gt;http2&lt;/code&gt; 配置、ALB、Cloudflare）都支持 HTTP/2 转发。不过要注意一个边界条件：浏览器支持 HTTP/2 不等于连接一定使用 HTTP/2——如果客户端网络环境受限（如企业代理拦截 HTTP/2 升级、CDN 回源到后端的连接仍是 HTTP/1.1），SSE 的连接数限制仍然存在。生产环境中建议确认从客户端到代理的整条链路都支持 HTTP/2。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/p5-HTTP2%e5%a4%9a%e8%b7%af%e5%a4%8d%e7%94%a8.png&#34; alt=&#34;HTTP/1.1 6 连接 vs HTTP/2 多路复用的对比示意&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;这意味着：如果你已经在用 HTTP/2，SSE 的连接数限制对你来说不存在。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这是一个&amp;quot;补课式&amp;quot;的解决——传输层升级让这个 bug 不再是问题。就像 IPv4 地址不够用，不是改了 IP 协议，是 NAT 帮忙扛了十几年。至于 HTTP/3（基于 QUIC），它对 SSE 更友好——0-RTT 建连、连接迁移——但 2026 年的生产环境，HTTP/2 已经够用了。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;五什么时候选什么决策框架&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e9%80%89%e4%bb%80%e4%b9%88%e5%86%b3%e7%ad%96%e6%a1%86%e6%9e%b6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、什么时候选什么——决策框架&#xA;&lt;/h2&gt;&lt;p&gt;说了这么多，到底怎么选？&lt;/p&gt;&#xA;&lt;p&gt;我总结了一个四维决策框架：&lt;strong&gt;通信模式 → 可靠性要求 → 基础设施 → 性能边界&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第一步你需要双向通信吗&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%80%e6%ad%a5%e4%bd%a0%e9%9c%80%e8%a6%81%e5%8f%8c%e5%90%91%e9%80%9a%e4%bf%a1%e5%90%97&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第一步：你需要双向通信吗？&#xA;&lt;/h3&gt;&lt;p&gt;这是最关键的一问。如果答案是&amp;quot;不需要&amp;quot;或&amp;quot;很少需要&amp;quot;（偶尔发个指令，大部分时间在收推送），SSE 是更好的选择。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;单向推送用 SSE，双向交互用 WebSocket。就这么简单。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;判断标准：数一下你的应用里，服务端发消息给客户端的次数和客户端发消息给服务端的次数。如果比例超过 10:1（收 10 条才发 1 条），SSE 值得考虑。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第二步你需要多高的可靠性&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e4%bd%a0%e9%9c%80%e8%a6%81%e5%a4%9a%e9%ab%98%e7%9a%84%e5%8f%af%e9%9d%a0%e6%80%a7&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第二步：你需要多高的可靠性？&#xA;&lt;/h3&gt;&lt;p&gt;如果每条消息都必须送达、顺序必须保证、失败必须重试——这是 WebSocket 的领地，它有标准的 ACK 机制。金融交易、实时协作编辑这类场景需要这种保证。WebSocket 的应用层可以自定义确认帧，服务端知道&amp;quot;客户端确实收到了这条消息&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;如果丢几条没关系、断线后自动恢复就行——SSE 的 EventSource 内置重连，大部分场景够用。状态更新、进度推送、通知提醒这类场景，SSE 的可靠性已经足够。EventSource 断线后会自动重连，并携带 &lt;code&gt;Last-Event-ID&lt;/code&gt; 头告诉服务端&amp;quot;我从哪里开始重连&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;有一个中间方案值得注意：&lt;strong&gt;SSE + 定期快照&lt;/strong&gt;。服务端每隔一段时间（如 30 秒）发一条完整状态快照，客户端即使丢了几条增量事件，下次快照来了也能恢复。这比 WebSocket 的逐条 ACK 更轻量，对大部分&amp;quot;实时展示&amp;quot;场景已经足够。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第三步你的基础设施长什么样&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%89%e6%ad%a5%e4%bd%a0%e7%9a%84%e5%9f%ba%e7%a1%80%e8%ae%be%e6%96%bd%e9%95%bf%e4%bb%80%e4%b9%88%e6%a0%b7&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第三步：你的基础设施长什么样？&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;对比项&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;SSE&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;WebSocket&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;负载均衡&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;标准 HTTP（无需特殊配置）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需粘性会话（sticky sessions）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;防火墙&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;标准 HTTPS 端口（443）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;可能被企业防火墙阻断&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;客户端实现&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;浏览器内置 EventSource&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需引入第三方库（socket.io、SockJS 等）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;代理兼容性&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;天然兼容（标准 HTTP 流）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需额外配置（Nginx Upgrade、ALB WebSocket 支持）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;从基础设施角度看，SSE 在几乎所有维度上都更&amp;quot;便宜&amp;quot;。如果你的团队不大、运维能力有限，SSE 的低维护成本是实实在在的优势。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第四步你有性能方面的特殊要求吗&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e5%9b%9b%e6%ad%a5%e4%bd%a0%e6%9c%89%e6%80%a7%e8%83%bd%e6%96%b9%e9%9d%a2%e7%9a%84%e7%89%b9%e6%ae%8a%e8%a6%81%e6%b1%82%e5%90%97&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第四步：你有性能方面的特殊要求吗？&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;需要传输二进制数据？→ WebSocket（SSE 需要 Base64 编码，增加 33% 开销）&lt;/li&gt;&#xA;&lt;li&gt;需要超低延迟（&amp;lt;50ms）？→ WebRTC / WebTransport（还在等 Safari 默认启用）&lt;/li&gt;&#xA;&lt;li&gt;需要在 HTTP/1.1 下开大量推送通道？→ 合并为单个 SSE 连接，用 &lt;code&gt;event&lt;/code&gt; 字段区分&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;决策速查&#34;&gt;&lt;a href=&#34;#%e5%86%b3%e7%ad%96%e9%80%9f%e6%9f%a5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;决策速查&#xA;&lt;/h3&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;需要双向通信？      → 是 → 需要超低延迟或 P2P？→ 是 → WebRTC / WebTransport&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                                      → 否 → WebSocket&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                  → 否 → 需要传输二进制？→ 是 → WebSocket&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                                       → 否 → 需要自定义请求头鉴权？→ 是 → fetch + ReadableStream&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                                                                     → 否 → 浏览器支持 EventSource？→ 是 → SSE&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                                                                                                       → 否 → 长轮询&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;核心判断：大多数实时推送场景不需要 WebSocket。这不是技术判断——是成本账。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/sse-vs-websocket/p6-%e5%86%b3%e7%ad%96%e6%a1%86%e6%9e%b6%e5%9b%be.png&#34; alt=&#34;决策框架四维速查图——通信模式/可靠性/基础设施/性能边界的选型路径&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;六生产陷阱与救火指南&#34;&gt;&lt;a href=&#34;#%e5%85%ad%e7%94%9f%e4%ba%a7%e9%99%b7%e9%98%b1%e4%b8%8e%e6%95%91%e7%81%ab%e6%8c%87%e5%8d%97&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;六、生产陷阱与救火指南&#xA;&lt;/h2&gt;&lt;p&gt;选了 SSE 不代表万事大吉。生产环境中 SSE 有几个常见的坑，踩一个就够你折腾半天。&lt;/p&gt;&#xA;&lt;h3 id=&#34;陷阱-1nginx-缓冲&#34;&gt;&lt;a href=&#34;#%e9%99%b7%e9%98%b1-1nginx-%e7%bc%93%e5%86%b2&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;陷阱 1：Nginx 缓冲&#xA;&lt;/h3&gt;&lt;p&gt;这是 SSE 最常见的生产 bug。Nginx 默认缓冲响应体，SSE 的流式数据会被缓存后才一次性推给客户端。表现是 EventSource 连接正常，但数据&amp;quot;卡住&amp;quot;——等了半天突然一次性收到大量数据。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：设置响应头 &lt;code&gt;X-Accel-Buffering: no&lt;/code&gt;，或者在 Nginx 配置中关掉 &lt;code&gt;proxy_buffering&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;location&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/sse&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;proxy_pass&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;http://backend&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;proxy_buffering&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;proxy_cache&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果后端是 Go，在 &lt;code&gt;http.ResponseWriter&lt;/code&gt; 里设置头：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;w&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Header&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;X-Accel-Buffering&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;no&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;陷阱-2反向代理超时&#34;&gt;&lt;a href=&#34;#%e9%99%b7%e9%98%b1-2%e5%8f%8d%e5%90%91%e4%bb%a3%e7%90%86%e8%b6%85%e6%97%b6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;陷阱 2：反向代理超时&#xA;&lt;/h3&gt;&lt;p&gt;SSE 是长连接，但反向代理有默认超时。AWS ALB 默认 60s，Cloudflare 默认 100s，Nginx &lt;code&gt;proxy_read_timeout&lt;/code&gt; 默认 60s。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：把超时设大（如 3600s），同时在应用层每 30-60s 发一条 &lt;code&gt;: keepalive\n\n&lt;/code&gt; 注释行刷新超时计数器。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;location&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/sse&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;proxy_pass&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;http://backend&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;proxy_read_timeout&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;3600s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;proxy_buffering&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Go 服务端的心跳：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;ticker&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;NewTicker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Second&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;defer&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ticker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Stop&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;select&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;case&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ticker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;C&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Fprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;w&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;: keepalive\n\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;flusher&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Flush&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;case&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Done&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 客户端断开，安全退出&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;陷阱-3浏览器连接数限制&#34;&gt;&lt;a href=&#34;#%e9%99%b7%e9%98%b1-3%e6%b5%8f%e8%a7%88%e5%99%a8%e8%bf%9e%e6%8e%a5%e6%95%b0%e9%99%90%e5%88%b6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;陷阱 3：浏览器连接数限制&#xA;&lt;/h3&gt;&lt;p&gt;HTTP/1.1 下最多 6 个 SSE 连接。虽然 HTTP/2 解决了这个问题，但如果你还在用 HTTP/1.1：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：把多个推送通道合并为单个 SSE 连接，用 &lt;code&gt;event&lt;/code&gt; 字段区分不同类型。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;evtSource&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;EventSource&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/sse/all&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;evtSource&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;addEventListener&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;notification&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;cm&#34;&gt;/* 处理通知 */&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;evtSource&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;addEventListener&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;data_update&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;cm&#34;&gt;/* 处理数据更新 */&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;陷阱-4eventsource-的羊群效应&#34;&gt;&lt;a href=&#34;#%e9%99%b7%e9%98%b1-4eventsource-%e7%9a%84%e7%be%8a%e7%be%a4%e6%95%88%e5%ba%94&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;陷阱 4：EventSource 的羊群效应&#xA;&lt;/h3&gt;&lt;p&gt;EventSource 在连接断开时会自动重连。服务端宕机恢复后，大量客户端同时重连——这就是羊群效应（thundering herd），可能导致服务端再次过载。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：服务端实现指数退避，或者用 &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;ReadableStream&lt;/code&gt; 替代 EventSource 来精细控制重连策略。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;createSSEWithBackoff&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;maxRetries&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;maxRetries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;response&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;fetch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;reader&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;response&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;getReader&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;decoder&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;TextDecoder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;buffer&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;while&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;done&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;reader&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;done&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;nx&#34;&gt;buffer&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;decoder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;stream&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;c1&#34;&gt;// 按 SSE 协议分割：每条消息以 \n\n 结尾&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;parts&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;split&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;\n\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;nx&#34;&gt;buffer&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;parts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pop&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 保留未完成的部分&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;part&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;of&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;parts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;line&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;of&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;part&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;split&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;startsWith&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;data: &amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                            &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[DONE]&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                            &lt;span class=&#34;nx&#34;&gt;console&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;收到事件:&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;JSON&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;parse&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;delay&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Math&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;min&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1000&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Math&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Promise&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;setTimeout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;delay&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;生产环境-checklist&#34;&gt;&lt;a href=&#34;#%e7%94%9f%e4%ba%a7%e7%8e%af%e5%a2%83-checklist&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;生产环境 Checklist&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Nginx 关掉 proxy_buffering 或设置 &lt;code&gt;X-Accel-Buffering: no&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 反向代理超时设到 3600s 或以上&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 应用层心跳（每 30-60s 发 &lt;code&gt;: keepalive&lt;/code&gt;）&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 使用 HTTP/2 避免连接数限制&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 优雅关闭处理（部署时旧进程不立刻断开连接）&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 监控连接数，异常增长及时告警&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 考虑用 &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;ReadableStream&lt;/code&gt; 替代 EventSource（更灵活）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;回到开头&#34;&gt;&lt;a href=&#34;#%e5%9b%9e%e5%88%b0%e5%bc%80%e5%a4%b4&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;回到开头&#xA;&lt;/h2&gt;&lt;p&gt;WebSocket 是个好东西。它解决了实时通信的核心问题，是双向交互场景的最佳选择。&lt;/p&gt;&#xA;&lt;p&gt;但大部分实时推送场景不需要双向。&lt;/p&gt;&#xA;&lt;p&gt;SSE 不是 WebSocket 的&amp;quot;替代品&amp;quot;——它是不同场景的&amp;quot;正确选择&amp;quot;。WebSocket 是&amp;quot;我能做所有事&amp;quot;，SSE 是&amp;quot;我只做一件事，但做得更好&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;选技术不是选最厉害的，是选最合适的。下次有人跟你说&amp;quot;我们需要 WebSocket&amp;quot;，先问一句：&lt;strong&gt;&amp;ldquo;我们真的需要双向吗？&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;你可能会发现，答案比你想象的更简单。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/sse-vs-websocket&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;附录实验代码和原始数据&#34;&gt;&lt;a href=&#34;#%e9%99%84%e5%bd%95%e5%ae%9e%e9%aa%8c%e4%bb%a3%e7%a0%81%e5%92%8c%e5%8e%9f%e5%a7%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;附录：实验代码和原始数据&#xA;&lt;/h2&gt;&lt;p&gt;本文 1 组 Benchmark 实验的代码和原始数据已开源：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;GitHub：&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/sse-vs-websocket&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;zhiyulab-evidence/sse-vs-websocket&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;benchmark/&lt;/code&gt;：SSE vs WebSocket 1000 并发推送 Benchmark（Go 实现，可独立运行）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;每个子目录都有独立 README，说明如何复现。二进制编译产物不入库，跑实验前自己 &lt;code&gt;go build&lt;/code&gt;。&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
