SSE 到底是什么?

专栏:AI 全栈开发|12

|----------------------------------------------------------------------------------------------------------------------------------------------|
| 上一篇我们已经把 HTTP Streaming 跑通了:Response Body 可以持续到达。但裸 Streaming 只解决"能持续传",并没有告诉前端"一条业务消息从哪里开始、在哪里结束"。SSE(Server-Sent Events)就是在这条流上再加一层标准事件格式。 |

一、先看裸 Streaming 为什么还不够

假设后端连续发送:

|------------------------|
| 你好 正在搜索 找到了 3 条资料 回答完成 |

如果只是把字节不停地往浏览器推,前端还要自己约定:

• 哪一段是正文。

• 哪一段是进度。

• 哪一段代表错误。

• 怎样判断一个事件结束。

网络 chunk 本身又不能当业务边界,因为上一章已经讲过:一次 `read()` 可能拆开或合并多个应用数据块。

图 1 SSE 的价值:在持续传输的 HTTP Body 上,增加稳定的事件格式

二、SSE 是什么:一种 text/event-stream 事件格式

SSE 的服务端响应使用:

|---------------------------------|
| Content-Type: text/event-stream |

正文不是随便写,而是按照事件流格式组织。最简单的一条事件:

|----------|
| data: 你好 |

注意最后有一个**空行**,它表示这条事件结束。

如果想给事件命名:

|----------------------------------|
| event: delta data: {"text":"你好"} |

浏览器就可以把它当成 `delta` 类型事件处理。

三、event、data、id、retry 分别做什么

图 2 SSE 常见字段各有职责,而且不是每条事件都必须把四个字段写全

|-----------|-----------------------------|-----------------|
| 字段 | 作用 | 是否每条必须 |
| data | 事件数据;可以出现多行 | 最常用,但协议并不要求每条都有 |
| event | 自定义事件类型 | 可选 |
| id | 设置客户端保存的 Last Event ID | 可选 |
| retry | 建议后续自动重连等待时间(毫秒) | 可选 |
| : comment | 注释;客户端不会作为 message 事件交给业务代码 | 可选,常用于心跳 |

如果一个事件里有多行 `data:`,浏览器会把这些行按规则拼接成事件数据。

所以手写 SSE parser 时,不能只找第一行 `data:` 就结束。

四、EventSource 为什么比手读 ReadableStream 省事

浏览器内置了 `EventSource`:

复制代码
const es = new EventSource("/events");

es.onmessage = (event) => {
  console.log(event.data);
};

es.addEventListener("progress", (event) => {
  console.log("进度:", event.data);
});

es.onerror = () => {
  console.log("连接异常或正在重连");
};

它会帮你做三件很实用的事:

• 解析 SSE 事件格式。

• 按 `event:` 类型分发。

• 连接意外断开时按规则自动尝试重连。

但不要写成"onerror 就说明彻底失败"。EventSource 在准备重连时也会触发 error 事件,具体状态还要结合 `readyState` 和业务需求判断。

五、EventSource 的一个重要限制:请求方式和 Header 不够自由

原生 `EventSource(url)` 是一个面向 SSE 的浏览器 API,它建立的是 GET 请求。

你不能像 `fetch()` 那样随意传一个自定义 `Authorization` Header。

这对 AI Chat 很重要,因为聊天常见的是:

|--------------------------------------|
| POST /chat body = {"message": "..."} |

这时有几种常见设计:

• 直接用 `fetch()` 发 POST,同时读取 `text/event-stream` 响应。

• 先 POST 创建任务,再用 EventSource GET `/tasks/{id}/events`。

• 同源 Web 应用把认证放在安全 Cookie 中,让 EventSource 自动携带合适的 Cookie。

|-----------------------------------------------------------------------------------------------------------------------|
| 所以"SSE = 必须 EventSource"也是错误理解。SSE 是事件流格式;EventSource 是浏览器消费 SSE 的一个方便 API。需要 POST 或自定义 Header 时,可以使用 fetch 流式读取 SSE。 |

六、用 FastAPI 写一个最小 SSE 端点

复制代码
import asyncio

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

async def events():
    for i in range(3):
        yield (
            f"event: progress
"
            f"data: {i + 1}

"
        )
        await asyncio.sleep(1)

    yield "event: done
data: ok

"

@app.get("/events")
async def stream_events():
    return StreamingResponse(
        events(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache"},
    )

前端:

复制代码
const es = new EventSource("/events");

es.addEventListener("progress", (e) => {
  console.log("progress:", e.data);
});

es.addEventListener("done", (e) => {
  console.log("done:", e.data);
  es.close();
});

这个例子只做一件事:让你看见 `event + data + 空行` 怎样被浏览器自动变成事件。

七、自动重连到底自动到了什么程度

图 3 EventSource 会尝试自动重连,Last-Event-ID 能帮助服务端定位位置,但它本身不提供 exactly-once 保证

如果连接意外中断,EventSource 通常会重新连接。

如果之前的事件带:

|------------------|
| id: 41 data: ... |

浏览器会记住这个 Last Event ID,并在后续重连时让服务器知道上一次处理到哪个 ID。

但这并不会自动做到"一条不丢、一条不重"。

要真正续传,服务端还需要:

• 保存一定范围的事件历史。

• 收到 Last-Event-ID 后找到后续事件。

• 处理历史过期、服务重启等情况。

• 对于有副作用的业务消费,自己考虑幂等和去重。

SSE 提供的是**重连机制和位置线索**,不是 exactly-once 消息队列。

八、心跳为什么经常是一行冒号

SSE 连接可能很久没有业务事件。某些代理、网关或负载均衡器会把长时间空闲的连接断掉。

服务端可以周期性发送注释:

|--------------|
| : keep-alive |

冒号开头的行是 SSE comment,浏览器不会把它当普通 `message` 事件交给你的业务回调,但网络上确实有数据流过。

心跳间隔没有一个适合所有部署环境的固定数字,要结合代理 idle timeout、成本和业务实时性决定。

九、SSE 不是因为"只能 GET"就自动更安全

原稿把"只能 GET,所以攻击面少一半"写得太简单。

SSE 端点同样需要考虑:

• 谁有权订阅这条数据流。

• Cookie 认证时的跨站请求和 CORS 配置。

• 敏感数据是否会被错误订阅者看到。

• query 参数里的 Token 是否进入 URL、日志和监控系统。

EventSource 不能自定义 Authorization Header,不意味着"只能把 token 放 query"。Cookie、同源部署、任务票据等都是可选方案。

安全设计留到后面 Auth 和 Security 专题,这里只纠正概念。

十、什么时候适合 SSE

SSE 特别适合:

• 服务器向浏览器持续发送状态、日志、进度、通知。

• AI 生成结果以事件形式单向返回。

• 客户端主要通过普通 HTTP 请求发命令,服务器只需要持续推送结果。

如果客户端和服务器都需要高频、长期、双向发送消息,WebSocket 可能更自然。

但具体怎么选留到 14 的 SSE vs WebSocket,对第 12 篇来说先知道这个边界就够了。

十一、一个非常容易忽略的解析问题:fetch 读 SSE 不能只按 \n\n 粗暴切

教学 Demo 里经常这样写:

|------------------------------------------------------------------------------------------|
| buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); |

对于自己严格控制、只使用 LF 的简单服务,这可以工作。

但完整 SSE 解析还要考虑 CRLF、跨 chunk 的字段、多行 `data:`、BOM 等细节。

如果你使用 EventSource,浏览器已经帮你处理了这些规则;如果用 fetch 手写 parser,生产项目更适合使用经过测试的 SSE parser,而不是自己写十几行字符串切分就宣称"完整协议实现"。

十二、几个最容易学错的说法

• 误区 1:SSE 是另一种底层网络协议。它仍然建立在 HTTP 响应之上,关键是 `text/event-stream` 事件格式。

• 误区 2:event / data / id / retry 每条都必须有。它们都是字段,按事件需要使用。

• 误区 3:EventSource 自动重连就能保证不丢不重。服务端必须配合保存和重放,业务仍要考虑幂等。

• 误区 4:SSE 必须用 EventSource。fetch 也可以消费 SSE 流,并能使用 POST / 自定义 Header。

• 误区 5:SSE 只能 GET,所以天然安全。GET 端点一样需要认证、授权、CORS 和敏感数据保护。

• 误区 6:所有 AI 聊天都应该直接用 EventSource。POST + fetch SSE、任务创建 + EventSource 都是常见方案。

十三、这一篇只记住 4 句话

• HTTP Streaming 解决"持续传",SSE 解决"持续传的数据如何组织成事件"。

• EventSource 能自动解析和重连,但不是可靠消息队列。

• `id` / Last-Event-ID 是续传线索,不是 exactly-once 保证。

• SSE 是事件格式,EventSource 只是浏览器消费它的一种 API。

十四、下一篇

下一篇 13《WebSocket 到底是什么?》会进入真正的双向长连接:为什么客户端和服务器都需要主动推消息时,SSE 开始不够用;WebSocket 的连接、消息帧、心跳和重连又分别解决什么问题。

相关推荐
aixingkong9211 小时前
华为麒麟9050 系列芯片深度解析:韬折叠的奇迹
服务器·人工智能·硬件架构·硬件工程
Bigger1 小时前
我把苹果发布会里的折叠屏,真的用网页做出来了
前端·ai编程·交互设计
matlab代码1 小时前
基于直方图优化的图像去雾技术【源码71期】
人工智能·深度学习·计算机视觉
aiot189189352181 小时前
核芯物联蓝牙AOA高精度定位生态合作案例分享金库定位踩坑实录
大数据·运维·网络·人工智能·蓝牙aoa
二川bro1 小时前
大模型训练语料实战:从PDF杂源到干净数据集完整流水线
人工智能
ADAI_Bowen1 小时前
2026 年 8 月建筑设计 AI 工具分析:基于几何保真、可控与编辑能力
人工智能·机器学习·计算机视觉
风合星语1 小时前
2026 机器人行业观察(一):现在入局机器人还来得及吗?——机会、门槛与技术人的切入点
网络·人工智能·机器人
jimmyleeee1 小时前
大模型安全之十一:拆解现代 AI 系统的“骨架”:一份安全架构的完整蓝图
人工智能·安全·安全架构
一切皆是因缘际会2 小时前
事件驱动计算
人工智能