我扒了几个AI应用的API,发现了一个“反常识”的秘密……

155 阅读18分钟

没有过时的技术,只有等待“风口”的场景


👋 大家好,我是十三!

作为一名服务端研发,实时通信这块我自认还算有点心得。提到 Web 上的实时技术,WebSocket 几乎是我脑子里唯一的答案。它支持双向通信、性能强悍,几乎成了所有“实时”场景的标配。过去几年,我在项目里用它解决过无数问题,从即时聊天到状态同步,WebSocket 就是我的“万能钥匙”。

但最近,我却被一个现象搞得有点懵。像ChatGPT、Claude这些AI大模型的对话界面,回答时那种“打字机”式的流式效果,实在是太抓眼球了。每次输入一个问题,看着文字一个字一个字地跳出来,那种“临场感”让我着迷。可我转念一想:这背后得用啥技术?WebSocket 吗?毕竟这是我最熟的方案。

于是,我打开浏览器的开发者工具,满怀期待地想看看 ws:// 协议的握手细节。结果,网络面板里啥也没有!取而代之的,是一个挂在那儿老长时间的普通 HTTP 请求。点开一看,一个陌生的响应头映入眼帘:Content-Type: text/event-stream。一番查资料后,我才知道,这玩意儿叫SSE,Server-Sent Events。

Content-Type: text/event-stream正是 Server-Sent Events (SSE) 的典型特征。

它作为 HTML5 标准的一部分,早在十多年前就诞生了,但说实话,我之前几乎没正眼瞧过它。在 WebSocket 的光芒下,SSE 就像个不起眼的路人甲,默默无闻。这个发现让我心里咯噔一下:为啥是 SSE?为啥 AI 大模型偏偏选了这么个“老古董”,而不是我熟悉的 WebSocket?

这感觉就像在技术世界里挖到了一块隐藏的宝藏。我决定把这事儿当成一次“API探案”,非得挖出背后的真相不可。毕竟,技术选型从来不是小事,选错了不仅影响体验,还可能让整个架构背上沉重的维护成本。这次,我要彻彻底底搞清楚,SSE 凭啥能翻身。


Part 1:线索追踪 —— SSE 的“身份档案”

要解开这个“反常识”的谜团,我得先从最基础的线索入手:SSE 到底是个啥?作为一名追求卓越的工程师,我习惯于谋定而后动,所以我把调查分成了几个阶段:先搞懂它的定义,再拆解工作原理,最后对比技术特性。以下是我的“探案笔记”,一步步梳理出 SSE 的“身份档案”。

SSE 是什么?

SSE,全称 Server-Sent Events,翻译过来就是“服务器发送事件”。简单来说,它是一种让服务器通过持久的 HTTP 连接,单向往客户端推送事件和数据的技术。我在笔记本上随手记了这么几笔:

如果把 WebSocket 比作一个能随时双向通话的“对讲机”,客户端和服务器想聊啥就聊啥,妥妥的双向通信

那 SSE 就更像一台只能收听的“收音机”。你调好频道(建立连接)后,就只能听服务器这个“广播站”发来的内容,想回话?抱歉,没这功能。这就是单向通信。这个比喻让我一下子抓住了 SSE 的精髓。

它的工作原理

SSE 厉害的地方在于它简单得离谱,完全基于标准的 HTTP/HTTPS 协议,不像 WebSocket 那样还得搞个“协议升级”,省事多了。我一直认为,技术的美感在于权衡取舍(Trade-offs),SSE 显然是选择了“极简”这条路。

我把它的流程拆解成四个关键步骤,逻辑清晰:

  1. 客户端:浏览器通过一个叫 EventSource 的原生 API,往服务器某个 URL 发一个普通的 HTTP GET 请求。
  2. 服务器:收到请求后,返回个 200 OK,但重点是加了个特别的响应头 Content-Type: text/event-stream。这就等于跟浏览器打招呼:“嘿,接下来我发的不是普通网页或 JSON,而是一串事件流,准备接好啊。”
  3. 保持连接:然后,服务器不像普通 HTTP 请求那样立马断开,而是把连接挂在那儿 (Keep-Alive),一直等着。
  4. 推送数据:只要连接没断,服务器随时可以往客户端发特定格式的文本数据。每条消息都以 data: 开头,末尾用两个换行符 \n\n 标记结束。

一个最简单的 SSE 数据流大概是这个样:

data: 这是第一条消息

data: 这是第二条消息,几秒后发出

id: 1001
event: customEvent
data: 这是一条带有 ID 和自定义事件名的消息

与 WebSocket 的快速对比

为了把两者的区别掰扯清楚,我顺手整理了个对比表格。这不仅是为了直观,更是为了后面分析技术选型的“为什么”做铺垫。作为服务端研发,我始终相信,技术选型的核心在于场景适配,而非单纯的功能强弱。

特性Server-Sent Events (SSE)WebSocket
通信模型单向通信 (服务器 → 客户端)双向通信 (客户端 ↔ 服务器)
底层协议基于标准 HTTP/HTTPS 协议独立的 ws://wss:// 协议
内置特性自动重连事件 ID自定义事件名需要手动实现心跳、重连等机制
实现复杂度超简单,前端 EventSource API,后端改个响应头有点麻烦,得用专门的库处理协议握手和数据帧
适用场景状态更新、消息推送、AI 回答流等单向数据流在线游戏、协同编辑、即时聊天等双向互动场景

通过这张“身份档案”,我算是摸清了 SSE 的底:一个简单、专注,还自带实用小功能的单向通信工具。但我更关心的是,为啥是它? 它的简单背后,到底藏着什么设计哲学,能让 AI 大模型的场景对它“情有独钟”?带着这个问题,我继续往下挖。


Part 2:真相大白 —— 为什么大模型偏爱 SSE?

好了,到了关键问题:为什么像ChatGPT、Claude这样需要巨大计算能力的 AI 大模型,最终选了一个轻量、简单的 SSE 协议,而不是功能更全面、在我看来更“强大”的 WebSocket 呢?我对技术选型从来不是只看表面,功能的“多”不代表“合适”,我更想搞清楚背后的设计哲学和权衡取舍。

当我把所有线索拼凑起来后,真相渐渐浮出水面。我发现,大模型的交互方式,跟 SSE 的技术特性,简直是天作之合,堪称一场技术与场景的“双向奔赴”。 以下是我的三个“突破口”,不仅是结论,更是背后“为什么”的思考。

1. 突破口一:交互模式的完美契合

咱们先来拆解一下跟ChatGPT这种大模型聊天的过程,我习惯于把复杂问题分解为清晰的步骤:

  1. 我(客户端):在对话框里敲下一个问题,比如“帮我写个分布式锁的实现”,然后点击发送。这就是一个一次性的、完整的 HTTP 请求。
  2. AI(服务器):收到我的问题后,开始在后台疯狂计算和推理,生成回答。
  3. AI(服务器):当它吐出第一个字,比如“好的”,它就立刻发给我。然后是第二句话、第三段代码……直到整段回答完成。这就是一个持续的、单向的数据流

在这整个过程中,一旦我把问题发出去,我就成了一个纯粹的“观众”。我啥也不用干,就等着ChatGPT把回答一个字一个字地推送到我的屏幕上,几乎不需要再跟服务器多说一句话。

  • SSE 的“正中下怀”:SSE 的核心就是单向通信,服务器往客户端推数据。它的设计跟 AI 回答的模式简直是量身定做,刚刚好。就像我调好一个收音机频道,静静听着广播,根本不需要回话。为什么选它? 因为 SSE 在这个场景下做到了“职责单一”,没有多余功能,资源占用和实现成本都极低。
  • WebSocket 的“能力过剩”:WebSocket 牛在双向通信,但在这场景下,我往服务器发东西的能力完全用不上。为什么不选它? 因为它的双向特性在这里是冗余的,引入它不仅增加协议复杂度,还可能带来额外的维护成本,比如心跳机制和数据帧处理。这就像我只是想听个广播,却扛了一台昂贵的双向对讲机上阵,纯属浪费资源。

2. 突破口二:“零成本”的协议选择

SSE 直接构建在无处不在的 HTTP/HTTPS 协议之上。这一点容易被忽视,但在我看来,是第二个关键突破口。作为服务端研发,我深知技术选型中“兼容性”和“成本”的重要性:

  • 无需额外协议升级:它就是一个标准的 HTTP 请求,只是连接时间长一点而已。
  • 极佳的中间件兼容性:现有的几乎所有网络基础设施,如防火墙、反向代理(Nginx、Envoy)、负载均衡器等,都对 HTTP 有着完美的理解和支持。SSE 的数据流可以轻松穿透这些中间件,无需任何特殊配置。
  • 更少的开发心智负担:开发者无需引入额外的 WebSocket 库,也无需处理复杂的协议握手、数据帧、心跳维持等问题。后端只需要修改一个响应头,前端使用一个原生 API 即可,极大降低了开发和维护成本。

为什么选 SSE? 因为它基于 HTTP 的设计,让它几乎“零成本”地融入了现有的技术栈,无论是开发还是部署,都省心省力。相比之下,WebSocket 是一个独立的协议 (ws://),在某些复杂的网络环境下,它的“协议升级”请求可能会被中间设备阻止,需要额外的配置才能保证连接成功。为什么不选 WebSocket? 因为它的独立协议特性,增加了部署复杂度和潜在的兼容性风险,这在追求卓越的工程实践中,是我不愿接受的额外负担。

3. 突破口三:“自带光环”的健壮性

网络世界,连接中断是常态。尤其是在移动网络下,信号切换、短暂掉线时有发生。作为一名追求卓越的工程师,我对技术的健壮性要求极高,而 SSE 在这点上给了我惊喜。它在协议层面就考虑到了网络不稳定,提供了两项让我非常欣赏的“自带”特性:

  • 自动重连:当 SSE 连接意外断开时,浏览器会自动地、默认地尝试重新连接。开发者无需编写任何一行重连逻辑代码。
  • 断点续传(Last-Event-ID:服务器可以在发送的每条消息中附带一个 id。当浏览器重连时,会自动将上一次收到的 id 通过 Last-Event-ID 请求头发送给服务器。服务器拿到这个 ID,就知道客户端上次收到了哪里,可以从中断的地方继续发送,确保了消息在网络抖动的情况下也不会丢失。

为什么选 SSE? 这两个特性对于提升大模型流式回答的体验至关重要。想象一下,如果因为网络问题,AI 的回答只显示了一半就卡住了,用户需要手动刷新才能看完整,体验会大打折扣。而 SSE 的这两个“光环”,优雅地解决了这个问题,省去了开发者手动处理重连和续传的麻烦。为什么不选 WebSocket? 因为 WebSocket 没有内置这些机制,需要开发者自己实现心跳和重连逻辑,这又是一个额外的工程负担。

到此,案情已经非常明朗。SSE 凭借其单向通信的精准定位基于 HTTP 的极简架构内置的健壮性机制,成为了承载 AI 大模型流式响应的“天选之子”。它并非比 WebSocket 更“先进”,而是在这个特定的场景下,它更“合适”。我深刻体会到,技术选型的本质,不是追求“最强”,而是追求“最适”。


Part 3:现场还原 —— 30 行代码复现“秘密通信”

理论分析得再多,不如亲手“还原”一下案发现场。作为一名服务端研发,我始终相信,技术的价值在于落地,所以我决定用最少的代码,模拟一遍 SSE 的工作流程,看看它到底有多简洁高效。这不仅是为了验证我的推测,更是为了感受它在工程实践中的真实表现。

我们将使用 Node.js + Express 作为后端,原生 HTML + JavaScript 作为前端。这套技术栈是我在快速原型验证时常用的,简单直接,能让我聚焦于核心逻辑。

后端:一个不知疲倦的“广播站”

// server.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/events', (req, res) => {
    // 1. 设置 SSE 响应头
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders(); // 立即发送响应头

    // 2. 每秒向客户端发送一次当前时间
    const intervalId = setInterval(() => {
        const date = new Date().toLocaleTimeString();
        res.write(`data: ${date}\n\n`); // 核心:以 `data: ` 开头,`\n\n` 结尾
    }, 1000);

    // 3. 当客户端关闭连接时,停止发送
    req.on('close', () => {
        clearInterval(intervalId);
        res.end();
        console.log('Client closed connection');
    });
});

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});

app.listen(port, () => {
    console.log(`SSE server listening at http://localhost:${port}`);
});

代码解读

  • 我们创建了一个 /events 路由,作为本次模拟的“广播站”。这是在服务端设计中常见的职责分离思路,一个接口只干一件事。
  • 设置三个关键的响应头是 SSE 的“身份标识”。res.flushHeaders() 用于立刻将这些头信息发送给客户端,使其知道连接已建立。这点在实际部署中很重要,尤其是通过 Nginx 反向代理时,及时发送头信息能避免缓冲问题。
  • 通过 setInterval,服务器每秒钟通过 res.write() 发送一条格式化的时间数据。
  • 监听 req.on('close') 事件是必不可少的,它能确保在用户关闭页面后,我们能清理定时器,释放服务器资源。这一点在实际开发中非常重要,能避免内存泄漏。作为追求卓越的工程师,我对这种细节格外关注。

前端:一个忠实的“收音机监听者”

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SSE Demo</title>
</head>
<body>
    <h1>十三Tech时间播报: <span id="time"></span></h1>

    <script>
        // 1. 创建 EventSource 实例,指向我们的后端路由
        const eventSource = new EventSource('/events');

        // 2. 监听 message 事件
        eventSource.onmessage = function(event) {
            // event.data 中包含了服务器发送的数据
            document.getElementById('time').textContent = event.data;
        };

        // 3. (可选)监听错误事件
        eventSource.onerror = function(err) {
            console.error("EventSource failed:", err);
            eventSource.close(); // 发生错误时可以手动关闭连接
        };
    </script>
</body>
</html>

代码解读

  • 前端的实现简单到令人惊讶。我们只需要 new EventSource('/events') 就能建立一个到服务端的持久连接。
  • 通过 eventSource.onmessage 就能监听到所有未命名事件,服务器 res.write() 的内容就静静地躺在 event.data 里。
  • 你甚至不需要编写任何连接管理和错误重试的代码,浏览器已经帮你处理好了一切。这点让我非常欣赏,SSE 的设计哲学显然是“让开发者少操心”。

把这两个文件放在一起,运行 node server.js,然后打开浏览器访问 http://localhost:3000,你就能看到页面上十三在为你每秒播报一次时间。这次“现场还原”完美地展示了 SSE 的魅力:用最简单的方式,解决最核心的问题。在这次探案中发现,SSE 的简洁性真是让人惊叹。更重要的是,它让我思考,如果未来我要部署一个类似流式响应的应用,SSE 绝对会是我的首选,因为它不仅好用,还能轻松适配阿里云OSS这样的云服务环境。


Part 4:我的“探案”心法 —— API 技术选型哲学

这次“API探案”让我收获颇丰,不光是搞懂了 SSE 为啥被 AI 大模型选中,更让我重新梳理了一套技术选型的思路。我始终相信,技术选型不是拍脑袋,而是要回归本质,抓住场景的核心需求。所以,面对五花八门的通信协议,我的“探案心法”很简单,就是问自己两个关键问题:

  1. 通信的方向是单向还是双向?
  2. 通信的主要发起方是谁?

有了这两个问题,我脑子里渐渐形成了一张决策地图,下面跟大家分享一下我的“探案心得”,希望也能帮到你。这张图不是随便画的,而是基于我多年服务端研发经验,结合这次探案总结出的结构化思考。

graph TD
    A[开始:我需要实时通信] --> B{通信是双向的吗?};
    B -- 是 --> C[WebSocket:果断选择,无需犹豫<br>适用于聊天、协同编辑、在线游戏等];
    B -- 否 --> D{主要是谁在推数据?};
    D -- 服务器推 --> E{数据格式复杂或需要POST?<br>例如需要传递大型JSON体};
    E -- 是 --> F[Fetch API + ReadableStream<br>手动实现SSE,更灵活];
    E -- 否 --> G[**SSE**:你的最佳选择<br>适用于状态更新、消息通知、AI流式响应];
    D -- 客户端拉 --> H{轮询频率要求高吗?};
    H -- 是 --> I[长轮询 (Long-Polling)<br>作为SSE的降级兼容方案];
    H -- 否 --> J[短轮询 (Short-Polling)<br>简单粗暴,但服务器压力大,慎用];

    style C fill:#d4edda,stroke:#333,stroke-width:2px
    style G fill:#d4edda,stroke:#333,stroke-width:2px

注:图中WebSocket和SSE为推荐选项,视觉上以绿色高亮。

说说我的几点心得吧。其实最开始做技术选型时,我老是陷入一种误区,觉得“功能多就是好”,所以不管三七二十一,先选 WebSocket 准没错。但这次探案让我意识到,技术选型得先看场景。比如需要双向通信,像聊天室、协同文档这种,WebSocket 确实是最佳选择,没得挑。但如果是服务器单方面推送数据,比如 AI 流式响应或者实时通知,SSE 这种轻量方案反而更香,既简单又高效。为什么选 SSE? 因为它在单向推送场景中做到了极致的“职责单一”,没有冗余功能,符合我对 API 设计“分层、单一、面向消费者”的哲学。

还有一点让我印象深刻,如果标准 SSE 不够用,比如你得通过 POST 传一堆复杂参数,那别硬来,可以试试 fetch API 搭配 ReadableStream,手动搞一个“增强版 SSE”。我最近在折腾一个小型 AI 工具时就用了这招,效果还不错,POST 传参和流式响应两不误。为什么选这个方案? 因为它在保留 SSE 流式特性的同时,提供了更高的灵活性,这在实际工程中非常实用。

至于轮询嘛,老实说,除非是迫不得已,比如要兼容一些老古董浏览器,或者网络环境特别奇葩,我是真不建议用。尤其是短轮询,服务器压力大得吓人,简直是自找麻烦。为什么不选轮询? 因为它违背了我对工程卓越性的追求,资源浪费和延迟问题会让最终交付的体验大打折扣。

说到底,技术选型这事儿,其实是工程师对业务场景理解到位后的一种“权衡艺术”。我这次算是又给自己上了一课:技术没有好坏,只有合不合适。未来,我打算在自己的项目中更多地引入 SSE,尤其是在结合 React 前端和阿里云 OSS 存储的场景中,打造一个真正可部署、可展示的作品,把这次探案的成果落地成一个能解决实际问题的工程艺术品。


结案陈词:没有过时的技术,只有等待场景的“英雄”

破解了这个“反常识”的秘密后,我更加确信:技术世界里,没有真正“过时”的老家伙,只有还没找到合适舞台的“英雄”。SSE 就像一位沉寂多年的剑客,在 AI 大模型这个舞台上,终于等到了属于它的“出鞘时刻”。它的锋芒,恰恰在于“简单”和“专注”。

我在这次探案中深刻体会到:技术的价值,永远取决于场景的适配度。找到对的场景,哪怕是十年前的技术,也能焕发出耀眼的光芒。说实话,这次折腾 SSE,让我想起了自己早年做项目时的一些“老朋友”,比如长轮询,那时候也是被逼无奈才用,现在想想,也许它们也在某个角落等着我重新发现呢。这次探案不仅解开了我的困惑,更让我对技术选型的哲学有了新的思考。

写在最后

这一篇“探案笔记”就到此告一段落了。SSE 的故事让我再次感受到技术的魅力:不是越复杂越好,而是越贴合场景越妙。希望我的这些发现和思考,能给你带来一些启发。我始终认为,做项目和写文章是相辅相成的,每一次技术探索,都是值得分享的宝贵素材。如果你也有类似的技术“探案”经历,欢迎来跟我聊聊,咱们一起挖掘更多隐藏的“英雄”。

一起聊聊

  • 你之前在项目里用过 SSE 吗?用在啥场景里,感觉咋样?有没有遇到啥坑?
  • 在你日常用的应用中,有哪些技术实现让你特别好奇,想扒一扒背后的秘密?

👨‍💻 关于十三Tech

资深服务端研发工程师,AI编程实践者。
专注分享真实的技术实践经验,相信AI是程序员的最佳搭档。
希望能和大家一起写出更优雅的代码!

📧 联系方式569893882@qq.com
🌟 GitHub@TriTechAI
💬 微信:TriTechAI(备注:十三Tech)

qrcode_for_gh_013bec198bc7_258.jpg

扫码关注不迷路