一、基础概念部分

  1. 什么是 SSE?它的全称是什么?
  • 全称是 Server-Sent-Events,是 HTML5 规范的一部分。
  • 一种服务端主动向浏览器推送数据的技术,基于 HTTP 协议。
  1. SSE 与 WebSocket 的区别是什么?
维度 SSE WebSocket
协议 HTTP 独立 WS 协议
通信方式 单向(服务器 -> 客户端) 双向
实现复杂度 简单,原生 EventSource 需要握手升级
自动重连 浏览器原生支持 需要手动实现
事件类型 支持自定义 event 需要自定义协议
二进制支持 仅文本 支持二进制
试用场景 通知、消息推送、AI 流式输出 聊天室、协同编辑
  1. 为什么大模型(ChatGPT/DeepSeek)都用SSE 而不是WebSocket?
  • 响应是单向数据流,服务端推送 + 客户端渲染即可。
  • 基于 HTTP 协议,天然兼容 HTTPS、CDN、代理、Nginx。
  • 浏览器原生的 EventSource,断线重连开箱即用。
  • 后端实现简单,无需单独维护 WS 长连接。
  1. SSE 用的是什么HTTP方法和请求头?
  • 方法:GET
  • 请求头:Accept: text/event-stream
  • 响应头:Content-Type: text/event-stream; charset=utf-8
  • 关键响应头:Cache-Control: no-cacheConnection:keep-alive

二、API 与使用

  1. 浏览器端如何创建一个SSE 连接?
const es = new EventSource("/api/stream");
es.onmessage = (e) => console.log(e.data); // 接收数据
es.onopen = () => console.log("连接已建立");
es.onerror = (e) => console.log("连接异常", e);
es.addEventListener("custom", (e) => console.log("自定义事件", e.data)); // 发送数据

SSE 流式输出原理分析

  1. SSE 数据格式是怎么定义的?
event: customEvent
id: 12345
data: { "msg": "hello" }
  • 每条消息以两个换行\n\n结束
  • 字段:data 必需有、event 自定义事件名称、id (断线重连时 Last-Event-ID)、retry(重连间隔 ms)
  1. EventSource 不支持 Post,如何传参?
  • 用 GET 把参数拼接到 URL 上。
  • 或使用 fetch + ReadableStream 自己实现流式读取,这种方式更灵活,可使用 POST 请求,也可以添加 token。
  1. 如何使用 Fetch 实现 SSE 接收?
const res = await fetch("/api/stream", {
  method: "POST",
  header: { "Content-Type": "application/json" },
  body: JSON.stringify({ prompt: "你好" }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value, { stream: true });
  // 按\n\n 切分,逐段解析 data:
}

三、服务端实现

  1. Node.js(Koa/Express)如何返回 SSE 流?
// Koa
ctx.set({
  "Content-Type": "text/event-stream",
  "Cache-Control": "no-cache",
  Connection: "keep-alive",
});

const stream = new ReadableStream({
  start(controller) {
    controller.enqueque(`data: ${JSON.stringify({ msg: "hi" })}\n\n`);
    // 定时推送
    const timer = setInterval(() => {
      constroller.enqueque(`data: ${Date.now()}\n\n`);
    }, 1000);
    // 关闭
    return () => clearInterval(timer);
  },
});
ctx.body = stream;
  1. SSE 在 Nginx 代理是要注意什么?
  • 关闭代理缓冲:proxy_buffering off;
  • 关闭缓存:proxy_cache off;
  • 延长超时:proxy_read_timeout 3600s;
  • 响应头设置X-Accel-Buffering: no
  1. SSE 在 CDN/Cloudflare 下缓存怎么办
  • Cloudflare 默认会缓冲流式响应,需要在 Cloudflare 后台开启 "HTTP/2 with edge buffering off" 或类似选项。
  • 国内 CDN 普遍需要特殊配置或绕过 CDN 直接连接源站点。

四、进阶与原理

  1. SSE 断线重连机制是怎么工作的?
  • 浏览器在连接断开后会自动重连。
  • 服务端发送的 id:xxx 会被浏览器记住。
  • 重连时请求头会带上Last-Event-ID:xxx ,服务端可基于此 id 做断点续传。
  1. 如何手动控制 SSE 重连?
es.addEventListener("error", () => {
  es.close(); // 关闭后浏览器还会自动重连
  // 如需彻底停止,需配合 readyState 处理
});
  • readyState:0 (Connecting)、1(Open)、2(Close)。
  1. SSE 的流式输出在 LLM 场景是如何拼装的?
  • 一次 HTTP 长连接,服务端逐个返回 token。
  • 前端通过 ReadableStream 增量解析,把 token 拼接到 UI (打字机效果)。
  • 涉及“消息合并”算法:用一个 ref 保存历史内容,每次 append 新的 chunk。
  1. SSE 和长轮询(Long Polling)的区别?
  • 长轮询:客户端发起请求,服务端能够承载,有数据后在返回,客户端立即再发请求。
  • SSE:一次连接保持,服务端持续推送。
  • SSE 延迟更低、连接复用更彻底,资源消耗更少。
  1. SSE 存在哪些局限性?
  • 单向推送,只能服务端推送,不能通过客户端主动发送这条请求。
  • 受浏览器同源连接数量限制(HTTP/1.1 下是同时发送 6 个请求)。
  • 部分代理/中间件会缓存,导致假的“流式输出”。
  • 只能传递 UFT-8 文本。

五、场景与方案

  1. 项目中 SSE 用来做什么?举一个具体场景
  • 常见:AI 对话流式输出、实时通知、股票行情、日志实时面板、大文件下载进度
  • 描述时突出:为什么选 SSE 不选 WebSocket、遇到什么坑(缓冲、断线、中文乱码),怎么解决的。
  1. AI 流式输出中“中止生成”怎么实现?
  • 客户端通过调用 constroller.abort() 关闭 fetch 的 reader。
  • 通过 AbortController 传递 signal 给 fetch。
  • 服务端配合:监听 req.on('onclose') 事件,停止调用 LLM 接口。
  1. SSE 接收到的中文数据乱码怎么办?
  • 确认服务端Content-Type 包含 charset=uft-8
  • 前端的 TextDecoder 明确指定 utf-8
  • 检查 Nginx 是否修改了编码。
  1. 如何保证 SSE 消息的顺序和完整性?
  • id字段做消息序号
  • 前端按 id排序、去重
  • 弱网环境下可以加入 ACK 机制(客户端收到响应后回执,服务端重新传送未确认的)
  1. 踩的几个坑
  • "加 gzip 提升 SSE 性能" → 错。gzip 会在 Nginx 端攒够一定字节才压缩,直接破坏流式输出。 服务端配置里应强制 gzip off。
  • Koa 用 ctx.body = ReadableStream 在某些版本下不会真正 pipe → 改用 ctx.respond = false + 原生 res.write,最稳。