SSE 协议崛起!AI 实时能力背后的“无声英雄”

大模型时代的宠儿

大模型时代的需求

如果你最近关注 AI,特别是像 ChatGPT 这样的大语言模型(LLM),你会发现它们在生成回答时,通常不是一下子给出完整答案,而是一个字一个字或一个词一个词地"吐"出来。

这种"流式"响应(Streaming Response)的用户体验非常好,用户可以更快地看到部分结果,而不是干等很长时间。

比如下图就是 DeepSeek

那么,这种流式响应是怎么实现的呢?很多场景下,用的就是 SSE!

在早期的 web 开发中,实时数据的传输通常需要通过频繁的轮询(polling)来实现。轮询是指客户端定期向服务器发送请求,询问是否有新的数据。这种方式虽然简单,但却带来了很多问题:

  • 频繁的请求增加了网络负担;
  • 延迟较高,因为每次请求都要经过一定的时间间隔;
  • 无法实现真正的实时性,客户端必须等待下次轮询。

为了改善这些问题,WebSocket 和 SSE(Server-Sent Events)应运而生。WebSocket 主要是双向通信协议,而 SSE 则是单向通信协议,专注于从服务器向客户端推送实时数据。

SSE 协议的设计思想非常简单,它利用了 HTTP 协议的持久连接特性,使得服务器可以持续不断地向客户端发送数据。最初,SSE 主要用于浏览器中的实时通知功能,例如社交网络的实时消息推送、股票行情更新等。随着前端技术的发展,SSE 逐渐成为构建实时应用(如聊天室、AI实时回答)的重要技术之一。

为什么 LLM 场景偏爱 SSE?

  1. 单向通信契合: LLM 的响应本质上是服务器(模型)向客户端(用户界面)单向输出信息。用户发送一个请求,然后等待服务器持续不断地"流"回数据。这正好是 SSE 的强项。
  2. 轻量级: 相比 WebSocket,SSE 更简单、开销更小。对于只需要服务器向客户端推送数据的场景,SSE 足够用,也更容易实现和维护。
  3. 基于 HTTP: SSE 运行在标准的 HTTP 协议之上,这意味着它更容易通过现有的网络基础设施(如防火墙、代理),兼容性更好。
  4. 断线重连: SSE 标准内置了断线重连机制。如果网络不稳定导致连接中断,客户端可以自动尝试重新连接,并可以指定上次接收到的事件 ID,服务器可以根据这个 ID 继续发送后续数据,保证了数据传输的连续性。

SSE 与 WebSocket 的对比

SSE 和 WebSocket 都是为了实现实时通信而设计的协议,但它们各自有不同的特点和适用场景。下面我们用表格来对比两者的区别和优缺点:

特性 SSE WebSocket
协议类型 单向(从服务器到客户端) 双向(客户端和服务器都可以发送消息)
建立连接方式 基于 HTTP 协议(使用持久化连接) 基于 TCP 协议(需要升级 HTTP 协议)
数据传输 服务器主动向客户端推送数据 客户端和服务器都可以主动发送数据
连接保持 HTTP 持久连接 长连接
兼容性 广泛支持,包括大部分现代浏览器 较少浏览器支持,尤其是某些旧版本不支持
消息格式 以事件流格式传输文本数据 二进制或文本数据(如 JSON、文本等)
传输效率 较高,适合数据流式传输 较高,适合双向实时通信
使用场景 服务器推送通知、实时数据流等 聊天室、多人在线游戏、双向消息传输等
适用性 适用于只需单向推送的应用场景 适用于需要双向通信的场景
可靠性 高,因为它是基于 HTTP 协议的 高,但需要管理连接和心跳等问题

资源消耗对比

资源消耗类型 SSE WebSocket
连接建立 直接通过 HTTP 协议保持长连接,比较简单 需要通过 HTTP 协议进行握手并升级到 WebSocket,建立 TCP 长连接
服务器端资源消耗 服务器保持的是 HTTP 持久连接,虽然每个连接也需要消耗一定资源,但相较 WebSocket 消耗较少。 每个 WebSocket 连接都需要服务器保持一个持久连接,这会占用更多内存和文件描述符。特别是高并发情况下,服务器的负担较重。
客户端资源消耗 客户端仅需要接收数据流,资源消耗相对较低。每次只有数据流的接收,没有额外的双向交互,因此内存和带宽的使用较少。 客户端需要维护与服务器的双向连接,通常需要更多的内存和带宽。尤其是在多个 WebSocket 连接并发时,资源消耗较大。
带宽消耗 SSE 通常用于单向数据推送,带宽消耗相对较低,尤其是在数据流较大时,SSE 更能避免不必要的带宽浪费。 WebSocket 在双向通信的过程中,数据量较大时带宽消耗可能较高,尤其是消息频繁的情况下。
连接管理开销 相比 WebSocket,SSE 更加简单,不需要客户端和服务器进行双向的数据交换,也不需要复杂的心跳检测机制,管理开销较低。 需要管理连接的建立、心跳检测、断开处理等机制。对于高并发连接,可能需要更多的资源来管理和监控每个连接的状态。
内存占用 由于是单向数据流,服务器和客户端内存占用较小,尤其在不频繁更新数据时,内存占用较少。 每个连接都需要维护连接信息和数据缓冲区,尤其是在高并发时,内存占用会显著增加。
系统开销 SSE 使用 HTTP 协议连接,系统开销相对较低,因为它只需要维护持久连接,不涉及复杂的连接管理和双向通信。 WebSocket 长连接占用服务器的文件描述符、线程和 CPU 资源,尤其是多用户并发时,开销比较大。
可伸缩性 SSE 由于连接简单且单向推送,伸缩性较好,服务器资源的压力相对较小。 在高并发的场景中,WebSocket 的伸缩性较差,需要更多的负载均衡和连接池管理。
资源清理 SSE 也需要清理断开连接的客户端,但因为只有单向数据流和自动重连机制,处理起来较为简单。 需要定期处理 WebSocket 连接的清理,包括超时检测、连接断开后的资源回收等。

兼容性

最后让我们看看 SSE 的浏览器兼容情况

SSE 协议 Demo

上面我们说到 SSE 是一种基于 HTTP 的单向通信技术,客户端通过建立持久连接,允许服务器实时地向客户端推送文本数据流。下面就让我们来实现一个简单的 demo,展示如何在前端和后端实现基于 SSE 的实时数据传输。

bash 复制代码
.
├── app.js
├── index.html
├── node_modules
│   └── express
└── package.json

后端实现(Node.js 示例)

javascript 复制代码
const express = require('express');
const app = express();

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

// 设置 SSE 路由
app.get('/events', (req, res) => {
  // SSE 协议的核心头信息,指示服务器将发送 事件流 数据到客户端。
  // 这个值告诉客户端,服务器返回的内容是一个基于事件的流(Event Stream),而不是普通的 HTML 或 JSON 数据。
  res.setHeader('Content-Type', 'text/event-stream');
  // 防止客户端缓存 SSE 流的响应,确保数据实时更新
  res.setHeader('Cache-Control', 'no-cache');
  // 保持 HTTP 连接持续活跃,允许服务器和客户端进行长时间的通信
  res.setHeader('Connection', 'keep-alive');
  
  // 模拟服务器推送数据
  let count = 0;
  setInterval(() => {
    res.write(`data: 当前时间:${new Date().toISOString()} 计数:${count}\n\n`);
    count++;
  }, 1000); // 每秒发送一次数据
});

app.listen(3000, () => {
  console.log('服务器正在监听 3000 端口...');
});

前端实现

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>SSE Demo</title>
</head>
<body>
  <h1>实时数据推送</h1>
  <div id="output"></div>

  <script>
    // 创建 SSE 连接
    const eventSource = new EventSource('/events');

    // 监听服务器发送的消息
    eventSource.onmessage = function(event) {
      const output = document.getElementById('output');
      output.innerHTML = event.data;
    };

    // 监听错误
    eventSource.onerror = function() {
      console.error('发生错误');
    };
  </script>
</body>
</html>

event-stream

在上面的代码中,我们看到有一行很重要的代码

js 复制代码
 res.setHeader('Content-Type', 'text/event-stream');

那么这个 event-stream又是啥呢?跟我们常见的 application/json 又有啥不一样?接下来我们详细解释下。

Event Stream 是 SSE 协议的核心传输格式,它定义了服务器如何向客户端发送事件数据。这种格式简单而高效,主要由以下几个关键部分组成:

基本格式

Event Stream 的数据格式遵循以下规则:

  1. 行为单位:每条消息由一行或多行文本组成
  2. 字段格式:每行以字段名开头,后跟冒号,然后是字段值
  3. 消息分隔 :消息之间用空行(两个连续的换行符 \n\n)分隔
makefile 复制代码
field: value\n
field: value\n
\n

标准字段

Event Stream 支持以下几种标准字段:

  1. data:事件数据的主体内容
  2. event:事件的类型名称(可选)
  3. id:事件的唯一标识符(可选,用于断线重连)
  4. retry:重连时间间隔(毫秒,可选)

一个完整的 event-stream 消息可能如下所示:

vbnet 复制代码
event: message
id: 12345
data: {"content": "这是一条消息"}
retry: 10000

对于多行数据,每行都以 data: 开头:

kotlin 复制代码
data: 第一行数据\n
data: 第二行数据\n
data: 第三行数据\n
\n

客户端接收到后,会将它们合并为一个事件,数据内容为:

复制代码
第一行数据
第二行数据
第三行数据

我们也使用 event 字段可以区分不同类型的事件:

vbnet 复制代码
event: update
data: {"type": "status", "value": "processing"}

event: complete
data: {"type": "status", "value": "done"}

前端可以针对不同事件类型注册不同的处理函数:

javascript 复制代码
eventSource.addEventListener('update', function(e) {
  console.log('收到更新事件:', JSON.parse(e.data));
});

eventSource.addEventListener('complete', function(e) {
  console.log('收到完成事件:', JSON.parse(e.data));
});

断线重连机制

当连接断开时,EventSource 会自动尝试重新连接。服务器可以通过 idretry 字段来控制重连行为:

yaml 复制代码
id: 1001
data: 消息内容
retry: 5000
  • id 字段:浏览器会记住最后收到的事件 ID,重连时通过 Last-Event-ID 请求头发送给服务器
  • retry 字段:指定重连间隔时间(毫秒)

这种机制确保了即使在网络不稳定的情况下,客户端也能接收到完整的数据流,不会丢失消息。

综上所述,Event Stream 的核心点可以总结为:

  1. 数据分块:服务器将数据分成多个块(chunks)发送
  2. 流式传输 :客户端通过 EventSource API 接收数据流
  3. 实时解析:浏览器会实时解析接收到的数据,并根据 event-stream 格式转换为事件对象
  4. 事件触发:当接收到完整消息时,触发相应的事件处理函数

SSE 协议的问题

使用 EventSource 需要注意以下问题:

  • 结束标识:服务器端应发送特定的标识来表示数据流的结束,然后前端调用close关闭EventSource。如果不这么做的话,当服务端发送完数据关闭连接后,EventSource默认会自动重新连接。
  • 只支持GET:url可以携带一些简单的查询参数,如果要传输复杂的请求体,可以考虑两次请求的方案。先通过普通的HTTP POST/PUT请求,将请求体传送到服务端。服务端将请求体缓存起来,并返回一个能唯一标识的票据,前端最后使用EventSource在url中带上票据,服务端根据票据从缓存里取出请求体。
  • 不支持自定义Header:接口如果需要鉴权,无法在Header里定义Authorization请求头,那么建议使用Cookie来标识用户,EventSource请求会携带Cookie。

fetch-event-source

上面我们提到使用 EventSource 有一些局限性,比如只支持 GET 请求、无法自定义请求头等。但是我们回过头去看 DeepSeek 的请求,会发现它并不是 GET,而是 POST 请求,这是为啥呢?

这里给大家推荐一个社区比较好的库 fetch-event-source ,它是由微软开源开源的为了解决上述哪些弊端问题,一个基于 Fetch API 并且具有更强大的 SSE 功能的库。

解决的问题

原生 EventSource API 有以下限制:

  1. 请求方法受限:只能使用 GET 请求
  2. 无法传递请求体:必须将所有信息编码在 URL 中,而 URL 在大多数浏览器中限制为 2000 个字符
  3. 无法自定义请求头:无法添加 Authorization 等自定义头信息
  4. 重试机制受限:当连接断开时,无法控制重试策略,浏览器会默默尝试几次然后停止
  5. 无法进行响应验证:没有办法在解析事件流之前对响应进行自定义验证

核心理念

fetch-event-source 的核心理念是提供一个基于 Fetch API 的更灵活的接口,使开发者能够:

  1. 完全控制请求:支持任何 HTTP 方法、自定义请求头和请求体

    javascript 复制代码
    fetchEventSource('/api/sse', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token'
      },
      body: JSON.stringify({ query: 'some large query' })
    });
  2. 完全控制重试逻辑:允许开发者自定义断线重连策略

    javascript 复制代码
    fetchEventSource('/api/sse', {
      onerror(err) {
        if (err instanceof FatalError) {
          throw err; // 停止操作
        } else {
          // 自动重试,也可以返回特定的重试间隔
        }
      }
    });
  3. 访问响应对象:在解析事件流之前可以对响应进行验证

    javascript 复制代码
    fetchEventSource('/api/sse', {
      async onopen(response) {
        if (response.ok && response.headers.get('content-type').includes('text/event-stream')) {
          return; // 一切正常
        } else if (response.status >= 400 && response.status < 500) {
          throw new Error('客户端错误');
        } else {
          throw new Error('服务器错误,将重试');
        }
      }
    });
  4. 与页面可见性 API 集成:当页面隐藏时自动关闭连接,页面可见时再自动重连,减少服务器负担

使用示例

从原来的 EventSource:

javascript 复制代码
const sse = new EventSource('/api/sse');
sse.onmessage = (ev) => {
    console.log(ev.data);
};

改为使用 fetch-event-source:

javascript 复制代码
import { fetchEventSource } from '@microsoft/fetch-event-source';

await fetchEventSource('/api/sse', {
    onmessage(ev) {
        console.log(ev.data);
    }
});

这个库非常适合在需要更多控制权的场景下使用,比如:

  • 需要发送复杂请求体的 LLM 流式响应
  • 需要携带认证信息的实时数据流
  • 需要自定义重试策略的关键业务场景
  • 与 API 网关或代理一起工作时需要更好的错误处理

数据流转图

总结

SSE 协议是一个非常轻量且高效的实时数据推送技术,适用于需要单向数据流的应用场景,尤其是在"大模型"时代,AI 和大数据的实时传输需求中,它以"简单、稳定、易用"的优势,成为前端工程师的新宠。

与 WebSocket 相比,SSE 的实现更简单,兼容性更好,适用于大多数浏览器和现代 Web 应用。如果你追求极致的实时体验,WebSocket 依然不可替代;但如果你要的是"服务器单向推送",SSE 绝对值得一试。

技术没有高低,适合场景才是王道

视频版

视频版www.bilibili.com/video/BV1iV...

相关推荐
天天码行空几秒前
UnoCSS原子CSS引擎-前端CSS救星
前端
1_2_3_1 分钟前
抛弃 if-else,让 JavaScript 代码更高效
前端
火星思想1 分钟前
再来看看「从输入 URL 到看到页面」的整个流程
前端·面试
张开心_kx1 分钟前
不要再代码中滥用 useCallback 和useMemo
前端·react.js
Silence_xl2 分钟前
Vue3面包屑效果
前端
奶茶鉴赏专家3 分钟前
🚀 从 Vite 到 Rsbuild:一次意想不到的构建性能飞跃
前端
AronTing3 分钟前
代理模式:控制对象访问的中间层设计
前端·面试
自己记录_理解更深刻4 分钟前
在window系统,安装了全局的pnpm,用trae打开项目pnpm不能使用
前端
超凌4 分钟前
vue2,webpack 老项目清除无用的文件
前端
H5开发新纪元5 分钟前
我是如何用Cursor在10分钟内实现项目管理Mock方案的
前端·vue.js