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 的连接、消息帧、心跳和重连又分别解决什么问题。

相关推荐
跨境数据猎手24 分钟前
跨境电商系统三大 Web 漏洞的防护:SQL 注入、XSS、CSRF
前端·sql·xss
冒险的豆子1 小时前
Proxy & Vue 响应式(合并精简版,适合面试)
前端·javascript·vue.js
Java后端的Ai之路1 小时前
前端技术Vue-18-视图中的渲染
前端·javascript·vue.js
lzhdim6 小时前
AI的分类
人工智能·算法·机器学习·分类·数据挖掘
程序员海军8 小时前
用了两年多 GPT,我最近开始换模型了,还折腾了个求职避坑 Agent
前端·人工智能·后端
AIsoft_86898 小时前
人才评估中的关键信息如何沉淀?
人工智能·iphone
紧跟先前的步伐8 小时前
Go AI Agent 实战 06|JSON 结构化输出与 Go 后端校验
人工智能·ai·golang
zttbee8 小时前
echarts图表tooltip色块失效
前端·javascript·echarts
vibecoding779 小时前
Codex 分层用模型完整教程:让 GPT-6.1 Sol 拍板、GPT-6 Luna 跑侦察
人工智能·gpt
新知图书10 小时前
1.3 启动第一个机器人
人工智能·机器人