SSE知识梳理(1)

发布时间:2026/10/4 17:20:28
SSE知识梳理(1) 作者没有四次元口袋的蓝胖日期2026-10-03标签SSE, 流式响应SSE知识梳理(1)你有没有注意过 ChatGPT 的回答是一个字一个字打出来的这不是前端特效而是后端真的在一边生成一边发送数据——这就是流式响应。实现流式响应有多种技术方案其中SSEServer-Sent Events是最轻量、最适合服务端单向推送场景的方案。这篇笔记聚焦 SSE 的基础知识协议是什么、数据格式长什么样、和 WebSocket 有什么区别以及如何在 Python/Java/前端实现同步流式调用。掌握这些内容面试时就能把 SSE 的基本面讲清楚。核心掌握SSE 协议原理、数据格式、与 WebSocket 对比、FastAPI StreamingResponse、前端 EventSource、Java SseEmitter。一、SSE是什么1.1 基本概念SSEServer-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: 你} # 第二个chunk100ms后发送 data: {content: 好} # 第三个chunk200ms后发送 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:可选事件类型默认messageevent: notificationid:可选消息ID用于断线重连id: 42retry:可选重连间隔毫秒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 长轮询这是面试高频对比题维度SSEWebSocket长轮询通信方向单向服务器→客户端双向请求-响应协议标准 HTTPws:// / 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 appFastAPI()defgenerate_content():模拟AI逐字生成内容content你好我是AI助手很高兴为你服务forcharincontent:yieldfdata: {{\content\: \{char}\}}\n\ntime.sleep(0.1)# 模拟生成延迟yielddata: [DONE]\n\n# 结束标记app.get(/api/chat)defchat():returnStreamingResponse(generate_content(),media_typetext/event-stream,headers{Cache-Control:no-cache,Connection:keep-alive,X-Accel-Buffering:no,# 禁用 Nginx 缓冲})关键点解读配置作用yieldPython 生成器每次产出一块数据函数不退出保持连接text/event-streamSSE 标准 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. 生成器抛出 StopIterationStreamingResponse 关闭响应# 这就是流的本质函数没有退出而是一点一点地流出数据2.2 客户端同步接收前端有两种方式接收 SSE 数据方式1EventSource API最简单浏览器原生支持consteventSourcenewEventSource(/api/chat);// EventSource 有三个状态readyState// 0 CONNECTING 正在连接// 1 OPEN 连接已建立可以接收数据// 2 CLOSED 连接已关闭eventSource.onopenfunction(){console.log(SSE连接已建立);};eventSource.onmessagefunction(event){console.log(收到数据:,event.data);if(event.data[DONE]){eventSource.close();// 收到结束标记关闭连接console.log(生成完毕);return;}constdataJSON.parse(event.data);displayTextdata.content;// 追加到页面renderToDOM(displayText);// 渲染到页面打字机效果};eventSource.onerrorfunction(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 可以接收指定类型的事件方式2fetch ReadableStream更灵活支持POST请求asyncfunctionfetchStream(){constresponseawaitfetch(/api/chat);constreaderresponse.body.getReader();constdecodernewTextDecoder();letdisplayText;while(true){const{done,value}awaitreader.read();if(done)break;constchunkdecoder.decode(value,{stream:true});// 解析SSE格式提取data字段constlineschunk.split(\n);for(constlineoflines){if(line.startsWith(data: )){constdataline.slice(6);if(data[DONE])return;constparsedJSON.parse(data);displayTextparsed.content;renderToDOM(displayText);}}}}两种方式的对比EventSource使用简单自带断线重连但只支持 GET 请求不能发送自定义请求头。fetch ReadableStream灵活支持 POST、自定义请求头但需要自己处理缓冲区和重连。实际项目中聊天场景因为需要 POST 传递消息体更多用 fetch 方式。EventSource 适合简单的 GET 场景如通知推送、行情订阅。2.3 Java实现示例Spring Boot作为 Java 方向的面试也需要了解 Java 端的实现。Spring Boot 提供了两种方式方式1Spring WebFlux响应式WebFlux 是 Spring 5 引入的响应式 Web 框架基于 Reactor 模式。它天生支持非阻塞流式响应是 Spring 生态中做 SSE 的正统方式。RestControllerRequestMapping(/api)publicclassChatController{GetMapping(value/chat,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxServerSentEventStringchat(){// 使用 Reactor 的 Flux 实现流式响应Stringcontent你好我是AI助手很高兴为你服务;returnFlux.fromArray(content.split()).delayElements(Duration.ofMillis(100))// 每个字符间隔100ms.map(char_-ServerSentEvent.Stringbuilder().data({\content\: \char_\}).build()).concatWith(Flux.just(ServerSentEvent.Stringbuilder().data([DONE]).build()));}}核心概念解读FluxTReactor 中的响应式序列表示 0 到 N 个元素的异步序列。类似于 Java 8 的Stream但是异步非阻塞的。delayElements()在每个元素之间插入延迟不会阻塞线程而是调度到事件循环。ServerSentEventTSpring 提供的 SSE 消息封装支持data、event、id、retry等字段。MediaType.TEXT_EVENT_STREAM_VALUE等同于text/event-streamSpring 的常量定义。方式2SseEmitter传统 Servlet 方式不依赖 Reactor如果你的项目是 Spring MVC基于 Servlet不想引入 WebFlux 的依赖可以用SseEmitter。它是 Spring MVC 对 SSE 的原生支持。GetMapping(value/chat/traditional,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchatTraditional(){SseEmitteremitternewSseEmitter(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 两种方式对比FluxServerSentEvent响应式编程非阻塞适合高并发场景但学习曲线较陡。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 APIOpenAI、通义千问等响应时间长并发量大几十上百个同时在线用户对资源利用率有要求不想为每个连接分配一个线程一个简单的性能对比假设后端需要处理 100 个并发的 SSE 连接每个连接持续 30 秒模拟 AI 生成回答。方案线程数内存占用能否处理同步每连接一线程100~800MB每线程8MB栈空间勉强可以但资源浪费异步asyncio1~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: FluxServerSentEvent响应式 │ │ ├── Spring MVC: SseEmitter传统Servlet方式 │ │ └── WebFlux非阻塞 vs SseEmitter异步线程 │ │ │ └── 前端 │ ├── EventSource │ │ ├── readyState: CONNECTING/OPEN/CLOSED │ │ ├── onmessage / addEventListener自定义事件 │ │ └── 简单、GET、自动重连 │ └── fetchReadableStream │ ├── 灵活、支持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 二进制同步流式调用PythonFastAPI StreamingResponse yield 生成器和 JavaWebFlux 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 的传统方式并解释区别会是很大的加分项。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询