作者:没有四次元口袋的蓝胖
日期:2026-10-03
标签:SSE, 流式响应
SSE知识梳理(1)
你有没有注意过 ChatGPT 的回答是一个字一个字"打"出来的?这不是前端特效,而是后端真的在一边生成一边发送数据——这就是流式响应。实现流式响应有多种技术方案,其中SSE(Server-Sent Events)是最轻量、最适合"服务端单向推送"场景的方案。
这篇笔记聚焦 SSE 的基础知识:协议是什么、数据格式长什么样、和 WebSocket 有什么区别,以及如何在 Python/Java/前端实现同步流式调用。掌握这些内容,面试时就能把 SSE 的基本面讲清楚。
核心掌握:SSE 协议原理、数据格式、与 WebSocket 对比、FastAPI StreamingResponse、前端 EventSource、Java SseEmitter。
一、SSE是什么
1.1 基本概念
SSE(Server-Sent Events,服务器推送事件)是一种基于 HTTP 的单向流式传输技术。它允许服务器在建立连接后,持续向客户端推送文本数据,而客户端只需被动接收。
核心特点:
- 单向通信:服务器 → 客户端,客户端不能通过 SSE 向服务器发送数据
- 基于 HTTP:使用标准 HTTP 协议,不需要额外协议握手
- 文本格式:只支持文本数据(UTF-8),不支持二进制
- 自动重连:浏览器原生 EventSource API 自带断线重连机制
- 轻量简单:比 WebSocket 简单得多,不需要独立的服务器组件
要点:SSE 的本质是"服务器往客户端推数据的单向管道"。它不改变 HTTP 的请求-响应模型,只是让响应体变得"无限长"——服务器可以持续往里写数据,客户端持续读取。
1.2 SSE在HTTP协议层面的工作原理
理解 SSE,需要先理解它是如何在 HTTP 协议层面工作的。
传统 HTTP 请求-响应:客户端发送请求 → 服务器处理 → 一次性返回完整响应 → 连接关闭。
SSE 的 HTTP 请求-响应:客户端发送请求 → 服务器返回响应头(Content-Type: text/event-stream) →连接不关闭→ 服务器持续往响应体中写数据 → 客户端持续读取 → 直到服务器发送结束标记或连接断开。
这里的关键是 HTTP 的Chunked Transfer Encoding(分块传输编码)。当服务器使用 chunked 编码时,响应头中没有Content-Length,服务器可以将响应体分成多个"块"(chunk)逐步发送。每个 chunk 包含一块数据和长度信息,客户端收到一个 chunk 就处理一个 chunk,不需要等全部数据到达。
HTTP/1.1 200 OK Content-Type: text/event-stream Transfer-Encoding: chunked Cache-Control: no-cache Connection: keep-alive # 第一个chunk data: {"content": "你"} # 第二个chunk(100ms后发送) data: {"content": "好"} # 第三个chunk(200ms后发送) data: {"content": "!"} # 结束标记 data: [DONE]为什么要理解 Chunked Transfer Encoding:面试中如果被追问"SSE 和普通的 HTTP 长连接有什么区别",核心答案就是 chunked 编码。普通的 HTTP 响应是有
Content-Length的,客户端知道什么时候接收完毕;SSE 的响应使用 chunked 编码,没有Content-Length,客户端需要一直等待,直到收到结束标记。这就是 SSE 能实现"流式"的底层 HTTP 机制。
1.3 SSE的数据格式
SSE 使用纯文本格式传输数据,每条消息以data:开头,以\n\n结尾:
# SSE协议格式 event: message\n id: 1\n retry: 5000\n data: {"text": "Hello"}\n \n data: {"text": "World"}\n \n # 解读: # event: 事件类型(可选,默认"message") # id: 消息ID(可选,用于断线重连时的 Last-Event-ID) # retry: 重连间隔毫秒数(可选,客户端多久后尝试重连) # data: 消息内容(必填,可以多行,每行一个 data: 前缀) # \n\n: 空行表示一条消息结束字段说明:
| 字段 | 是否必须 | 作用 | 示例 |
|---|---|---|---|
data: | ✅ 必填 | 消息内容,可以多行 | data: {"msg": "hello"} |
event: | 可选 | 事件类型,默认message | event: notification |
id: | 可选 | 消息ID,用于断线重连 | id: 42 |
retry: | 可选 | 重连间隔(毫秒) | retry: 5000 |
空行\n\n | ✅ 必须 | 表示一条消息结束 | (空行) |
多行数据示例:
data: {"line1": "这是第一行"} data: {"line2": "这是第二行"} # 客户端收到的 event.data 会是: # {"line1": "这是第一行"}\n{"line2": "这是第二行"} # 注意多行之间用 \n 连接一个实际的SSE响应示例(模拟AI对话流式输出):
HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive data: {"content": "你"} data: {"content": "好"} data: {"content": "!"} data: [DONE]要点:
Content-Type: text/event-stream是 SSE 的标志性响应头,浏览器据此识别这是一个 SSE 流。每条消息之间用空行(\n\n)分隔,这个空行不能省略。实际开发中,大部分场景只需要data:字段就够了,event:、id:、retry:在需要高级功能时才使用。
1.4 SSE vs WebSocket vs 长轮询
这是面试高频对比题:
| 维度 | SSE | WebSocket | 长轮询 |
|---|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 | 请求-响应 |
| 协议 | 标准 HTTP | ws:// / wss:// | 标准 HTTP |
| 数据格式 | 文本(UTF-8) | 文本 + 二进制 | 文本 |
| 浏览器支持 | 原生 EventSource | 原生 WebSocket | 原生 XMLHttpRequest |
| 断线重连 | 自动重连(内置) | 需手动实现 | 需手动实现 |
| 实现复杂度 | 低 | 中 | 中 |
| 服务器兼容性 | 好(标准HTTP) | 需独立WebSocket服务器 | 好 |
| 代理/防火墙穿透 | 好 | 可能被拦截 | 好 |
| 适用场景 | AI对话、通知推送、实时行情 | 聊天室、游戏、协作编辑 | 简单实时通知 |
| 并发连接开销 | 中 | 低 | 高(频繁建立连接) |
逐项解读:
- 通信方向:SSE 是单向管道,服务器只能往客户端推数据;WebSocket 是全双工通道,双方都能随时发消息;长轮询本质还是请求-响应,只是服务器"hold住"请求直到有新数据才返回。
- 协议:SSE 基于标准 HTTP,和普通的 GET 请求没有本质区别,只是响应体是持续的;WebSocket 需要先通过 HTTP 做协议升级(
Upgrade: websocket),之后切换到 ws:// 协议。 - 断线重连:这是 SSE 最大的优势之一。浏览器 EventSource 在连接断开时会自动重连,并在请求头中携带
Last-Event-ID,服务端可以据此补发遗漏消息。WebSocket 的断线重连需要开发者自己实现。 - 代理/防火墙穿透:SSE 走标准 HTTP,和普通的网页请求走一样的通道,几乎不会被拦截。WebSocket 使用自定义协议,有些公司防火墙会拦截 ws:// 协议的连接。
选型建议:
- 只需要服务器向客户端推送 →SSE(如 ChatGPT 的流式输出、股票行情推送)
- 需要双向实时通信 →WebSocket(如在线聊天、多人协作)
- 兼容极老旧浏览器 →长轮询(现在几乎不需要了)
1.5 常见面试注意点
- SSE 只支持文本:不能传二进制数据(图片、音频等),需要二进制用 WebSocket。
- SSE 基于 HTTP:不需要像 WebSocket 那样做协议升级,天然兼容负载均衡、CDN、代理等基础设施。
- 自动重连是内置的:浏览器 EventSource 在连接断开时会自动重连,并携带
Last-Event-ID请求头,服务端可以据此补发遗漏的消息(这个特性在第三篇会详细讲)。 - SSE 有浏览器连接数限制:同一域名下,HTTP/1.1 最多 6 个 SSE 连接。HTTP/2 下没有此限制(多路复用)。面试提到这点会加分。
- SSE 的历史:SSE 最早由 WHATWG 在 HTML5 规范中定义,2009 年就被 Firefox 支持。虽然历史比 WebSocket 更早,但因为功能相对简单(只支持单向),一直没有 WebSocket 那么"出名"。直到 AI 大模型兴起,SSE 因为轻量、简单、适合单向推送的特点,才重新被广泛关注。
- SSE 没有标准化客户端库的"统一体验":虽然 EventSource 是 W3C 标准,但不同浏览器在实现细节上有差异(如重连行为)。生产环境建议使用
@microsoft/fetch-event-source等封装库来统一行为。
二、同步流式调用
2.1 服务端逐chunk返回(Python FastAPI)
流式响应的核心思想:不等待整个响应生成完毕,而是边生成边发送。以 Python 的 FastAPI 为例:
fromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseimporttime app=FastAPI()defgenerate_content():"""模拟AI逐字生成内容"""content="你好,我是AI助手,很高兴为你服务!"forcharincontent:yieldf"data: {{\"content\": \"{char}\"}}\n\n"time.sleep(0.1)# 模拟生成延迟yield"data: [DONE]\n\n"# 结束标记@app.get("/api/chat")defchat():returnStreamingResponse(generate_content(),media_type="text/event-stream",headers={"Cache-Control":"no-cache","Connection":"keep-alive","X-Accel-Buffering":"no",# 禁用 Nginx 缓冲})关键点解读:
| 配置 | 作用 |
|---|---|
yield | Python 生成器,每次产出一块数据,函数不退出,保持连接 |
text/event-stream | SSE 标准 Content-Type,浏览器据此识别为 SSE 流 |
Cache-Control: no-cache | 禁止缓存,每条数据都要实时送达 |
X-Accel-Buffering: no | 告诉 Nginx 不要缓冲响应,直接转发给客户端 |
要点:
yield是同步流式的核心。Python 的生成器(generator)每次执行到yield时会暂停并返回一个值,下次迭代时从暂停处继续。StreamingResponse会不断调用生成器,把每次yield的数据写入 HTTP 响应体并立即发送给客户端。
生成器的工作原理:
# 生成器的执行流程:# 1. 调用 generate_content() 时,不会立即执行函数体# 2. 第一次 next() 时,执行到第一个 yield,返回 "data: {...你...}\n\n"# 3. StreamingResponse 把这个数据写入 HTTP 响应体并发送# 4. 第二次 next() 时,从 time.sleep(0.1) 之后继续执行# 直到下一个 yield,返回 "data: {...好...}\n\n"# 5. 重复步骤 3-4,直到循环结束# 6. 最后一个 yield 返回 "data: [DONE]\n\n"# 7. 生成器抛出 StopIteration,StreamingResponse 关闭响应# 这就是"流"的本质:函数没有退出,而是一点一点地"流"出数据2.2 客户端同步接收
前端有两种方式接收 SSE 数据:
方式1:EventSource API(最简单,浏览器原生支持)
consteventSource=newEventSource('/api/chat');// EventSource 有三个状态(readyState):// 0 = CONNECTING 正在连接// 1 = OPEN 连接已建立,可以接收数据// 2 = CLOSED 连接已关闭eventSource.onopen=function(){console.log('SSE连接已建立');};eventSource.onmessage=function(event){console.log('收到数据:',event.data);if(event.data==='[DONE]'){eventSource.close();// 收到结束标记,关闭连接console.log('生成完毕');return;}constdata=JSON.parse(event.data);displayText+=data.content;// 追加到页面renderToDOM(displayText);// 渲染到页面(打字机效果)};eventSource.onerror=function(error){console.error('SSE连接错误:',error);// 注意:如果不手动 close(),EventSource 会自动重连// 如果不想重连,需要在这里手动 closeeventSource.close();};EventSource 的进阶用法——监听自定义事件类型:
// 如果服务端发送了 event: notification 类型的事件// 不能用 onmessage 接收,需要用 addEventListenereventSource.addEventListener('notification',function(event){console.log('收到通知:',event.data);});eventSource.addEventListener('price_update',function(event){console.log('价格更新:',event.data);});// onmessage 只接收默认类型(没有 event 字段或 event: message)的消息// addEventListener 可以接收指定类型的事件方式2:fetch + ReadableStream(更灵活,支持POST请求)
asyncfunctionfetchStream(){constresponse=awaitfetch('/api/chat');constreader=response.body.getReader();constdecoder=newTextDecoder();letdisplayText='';while(true){const{done,value}=awaitreader.read();if(done)break;constchunk=decoder.decode(value,{stream:true});// 解析SSE格式,提取data字段constlines=chunk.split('\n');for(constlineoflines){if(line.startsWith('data: ')){constdata=line.slice(6);if(data==='[DONE]')return;constparsed=JSON.parse(data);displayText+=parsed.content;renderToDOM(displayText);}}}}两种方式的对比:
EventSource:使用简单,自带断线重连,但只支持 GET 请求,不能发送自定义请求头。fetch + ReadableStream:灵活,支持 POST、自定义请求头,但需要自己处理缓冲区和重连。- 实际项目中,聊天场景因为需要 POST 传递消息体,更多用 fetch 方式。EventSource 适合简单的 GET 场景(如通知推送、行情订阅)。
2.3 Java实现示例(Spring Boot)
作为 Java 方向的面试,也需要了解 Java 端的实现。Spring Boot 提供了两种方式:
方式1:Spring WebFlux(响应式)
WebFlux 是 Spring 5 引入的响应式 Web 框架,基于 Reactor 模式。它天生支持非阻塞流式响应,是 Spring 生态中做 SSE 的"正统"方式。
@RestController@RequestMapping("/api")publicclassChatController{@GetMapping(value="/chat",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<String>>chat(){// 使用 Reactor 的 Flux 实现流式响应Stringcontent="你好,我是AI助手,很高兴为你服务!";returnFlux.fromArray(content.split("")).delayElements(Duration.ofMillis(100))// 每个字符间隔100ms.map(char_->ServerSentEvent.<String>builder().data("{\"content\": \""+char_+"\"}").build()).concatWith(Flux.just(ServerSentEvent.<String>builder().data("[DONE]").build()));}}核心概念解读:
Flux<T>:Reactor 中的响应式序列,表示 0 到 N 个元素的异步序列。类似于 Java 8 的Stream,但是异步非阻塞的。delayElements():在每个元素之间插入延迟,不会阻塞线程,而是调度到事件循环。ServerSentEvent<T>:Spring 提供的 SSE 消息封装,支持data、event、id、retry等字段。MediaType.TEXT_EVENT_STREAM_VALUE:等同于"text/event-stream",Spring 的常量定义。
方式2:SseEmitter(传统 Servlet 方式,不依赖 Reactor)
如果你的项目是 Spring MVC(基于 Servlet),不想引入 WebFlux 的依赖,可以用SseEmitter。它是 Spring MVC 对 SSE 的原生支持。
@GetMapping(value="/chat/traditional",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchatTraditional(){SseEmitteremitter=newSseEmitter(60_000L);// 超时60秒// 注册回调(可选)emitter.onCompletion(()->System.out.println("SSE连接完成"));emitter.onTimeout(()->System.out.println("SSE连接超时"));emitter.onError(e->System.err.println("SSE连接错误: "+e));CompletableFuture.runAsync(()->{try{Stringcontent="你好,我是AI助手!";for(charc:content.toCharArray()){emitter.send(SseEmitter.event().data("{\"content\": \""+c+"\"}"));Thread.sleep(100);}emitter.send(SseEmitter.event().data("[DONE]"));emitter.complete();// 标记完成}catch(Exceptione){emitter.completeWithError(e);// 标记错误}});returnemitter;}Java 两种方式对比:
Flux<ServerSentEvent>:响应式编程,非阻塞,适合高并发场景,但学习曲线较陡。SseEmitter:传统方式,异步但底层仍是 Servlet 模型,上手更快,适合 Spring MVC 项目。- 面试中如果被问到"SSE 在 Java 中怎么实现",能把两种方式都提到并说清区别,会非常加分。
- 注意
SseEmitter的超时设置:new SseEmitter(60_000L)表示 60 秒无数据则超时断开。AI 对话场景中可能需要设更大的值。
2.5 同步流式的局限性
同步流式虽然简单直观,但在实际场景中有明显的局限性:
- 阻塞线程:
time.sleep(0.1)或Thread.sleep(100)会阻塞当前线程。如果服务器只有 4 个 worker 线程,同时就只能处理 4 个 SSE 请求。第 5 个请求必须等待。 - 无法并行调用外部服务:如果后端需要调用 AI API(响应时间几秒到几十秒),同步方式会长时间占用线程。
- 不适合高并发:每个 SSE 连接独占一个线程,100 个并发用户就需要 100 个线程,资源消耗大。
同步流式的适用场景:
- 数据量小、并发量低的场景(如内部工具、demo 演示)
- 不需要调用外部服务的场景(如从本地文件或数据库读取数据)
- 快速验证想法、原型开发
什么时候必须用异步流式:
- 需要调用外部 AI API(OpenAI、通义千问等),响应时间长
- 并发量大(几十上百个同时在线用户)
- 对资源利用率有要求(不想为每个连接分配一个线程)
一个简单的性能对比:假设后端需要处理 100 个并发的 SSE 连接,每个连接持续 30 秒(模拟 AI 生成回答)。
| 方案 | 线程数 | 内存占用 | 能否处理 |
|---|---|---|---|
| 同步(每连接一线程) | 100 | ~800MB(每线程8MB栈空间) | 勉强可以,但资源浪费 |
| 异步(asyncio) | 1 | ~50MB | 轻松处理 |
这就是异步的核心优势:同样的硬件,能处理更多的并发连接。对于 SSE 这种"长连接、低带宽"的场景,异步模型几乎是唯一合理的选择。
解决方案:这就是为什么需要异步流式——用 asyncio 的事件循环代替线程阻塞,单线程就能处理大量并发连接。这将在下一篇详细讲解。
🗺️ 思维导图速览
SSE基础:协议原理与同步流式 │ ├── SSE是什么 │ ├── 基于HTTP的单向流式传输 │ ├── 服务器→客户端,客户端被动接收 │ ├── Content-Type: text/event-stream │ ├── 只支持文本(UTF-8),不支持二进制 │ └── HTTP底层:Chunked Transfer Encoding │ ├── 无Content-Length,响应体持续写入 │ └── 客户端边读边处理,不等全部数据 │ ├── SSE数据格式 │ ├── data: 消息内容\n\n(必填) │ ├── event: 事件类型(可选,默认message) │ ├── id: 消息ID(可选,用于断线重连) │ ├── retry: 重连间隔毫秒数(可选) │ ├── 多行data用\n连接 │ └── 空行\n\n表示一条消息结束 │ ├── 技术对比(面试高频) │ ├── SSE:单向、HTTP、自动重连、简单 → AI对话/推送 │ ├── WebSocket:双向、ws协议、手动重连 → 聊天/游戏 │ └── 长轮询:请求-响应、HTTP、手动重连 → 旧项目兼容 │ ├── 同步流式实现 │ ├── Python │ │ ├── FastAPI + StreamingResponse + yield 生成器 │ │ ├── 生成器原理:yield暂停→返回→继续 │ │ ├── 关键配置:Cache-Control、X-Accel-Buffering │ │ └── [DONE]标记表示流结束 │ │ │ ├── Java │ │ ├── Spring WebFlux: Flux<ServerSentEvent>(响应式) │ │ ├── Spring MVC: SseEmitter(传统Servlet方式) │ │ └── WebFlux非阻塞 vs SseEmitter异步线程 │ │ │ └── 前端 │ ├── EventSource │ │ ├── readyState: CONNECTING/OPEN/CLOSED │ │ ├── onmessage / addEventListener(自定义事件) │ │ └── 简单、GET、自动重连 │ └── fetch+ReadableStream │ ├── 灵活、支持POST、自定义Header │ └── 需手动处理缓冲区和重连 │ ├── 同步流式局限性 │ ├── 阻塞线程(time.sleep/Thread.sleep) │ ├── 无法并行调用外部服务 │ └── 不适合高并发 → 需要异步流式 │ └── 关键注意点 ├── yield 是流式的核心:暂停→返回→继续 ├── [DONE] 标记表示流结束 ├── X-Accel-Buffering: no 禁用Nginx缓冲 ├── HTTP/1.1同一域名6个SSE连接限制 └── Chunked Transfer Encoding 是底层机制📝 写在最后
本篇要点回顾
这篇笔记覆盖了 SSE 的基础面:
- 协议是什么:基于 HTTP 的单向流式传输,底层依赖 Chunked Transfer Encoding
- 数据格式:
data:+\n\n,可选event:、id:、retry:字段 - 与 WebSocket 的对比:单向 vs 双向、HTTP vs ws、自动重连 vs 手动重连、文本 vs 二进制
- 同步流式调用:Python(FastAPI StreamingResponse + yield 生成器)和 Java(WebFlux Flux / SseEmitter)两端的实现
- 前端接收:EventSource(简单,GET,自动重连)vs fetch + ReadableStream(灵活,POST,手动处理)
- 同步流式的局限性:阻塞线程,不适合高并发,引出异步流式的需求
学习建议
- 先跑通最简单的例子:写一个 FastAPI 的 SSE 接口 + 一个 EventSource 的前端页面,看到逐字输出的效果,理解整个链路。
- 重点理解"流"的概念:流式响应不是"一次性返回所有数据",而是"边生成边发送"。
yield是 Python 中实现这个概念的关键工具。理解生成器的"暂停→返回→继续"机制,就理解了流式响应的核心。 - 理解 HTTP 底层:SSE 之所以能"流式"传输,是因为 HTTP 的 Chunked Transfer Encoding。面试追问"和长连接有什么区别"时,能说出 chunked 编码会非常加分。
- 对比表格要记牢:SSE vs WebSocket 的对比是面试必考题。不需要逐字背诵,但要能说出核心区别:通信方向、协议、数据格式、重连机制、适用场景。
- Java 两种实现都了解:面试问到 Java 方向时,能说出 WebFlux 的响应式方式和 SseEmitter 的传统方式,并解释区别,会是很大的加分项。