SSE知识梳理(1)

作者:没有四次元口袋的蓝胖

日期: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 为例:

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import time

app = FastAPI()

def generate_content():
    """模拟AI逐字生成内容"""
    content = "你好,我是AI助手,很高兴为你服务!"
    for char in content:
        yield f"data: {{\"content\": \"{char}\"}}\n\n"
        time.sleep(0.1)           # 模拟生成延迟
    yield "data: [DONE]\n\n"      # 结束标记

@app.get("/api/chat")
def chat():
    return StreamingResponse(
        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 响应体并立即发送给客户端。

生成器的工作原理:

python 复制代码
# 生成器的执行流程:
# 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(最简单,浏览器原生支持)

javascript 复制代码
const eventSource = new EventSource('/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;
    }
    
    const data = JSON.parse(event.data);
    displayText += data.content;  // 追加到页面
    renderToDOM(displayText);     // 渲染到页面(打字机效果)
};

eventSource.onerror = function(error) {
    console.error('SSE连接错误:', error);
    // 注意:如果不手动 close(),EventSource 会自动重连
    // 如果不想重连,需要在这里手动 close
    eventSource.close();
};

EventSource 的进阶用法------监听自定义事件类型:

javascript 复制代码
// 如果服务端发送了 event: notification 类型的事件
// 不能用 onmessage 接收,需要用 addEventListener
eventSource.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请求)

javascript 复制代码
async function fetchStream() {
    const response = await fetch('/api/chat');
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let displayText = '';
    
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const chunk = decoder.decode(value, { stream: true });
        // 解析SSE格式,提取data字段
        const lines = chunk.split('\n');
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = line.slice(6);
                if (data === '[DONE]') return;
                const parsed = 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 的"正统"方式。

java 复制代码
@RestController
@RequestMapping("/api")
public class ChatController {

    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> chat() {
        // 使用 Reactor 的 Flux 实现流式响应
        String content = "你好,我是AI助手,很高兴为你服务!";
        
        return Flux.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 的原生支持。

java 复制代码
@GetMapping(value = "/chat/traditional", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatTraditional() {
    SseEmitter emitter = new SseEmitter(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 {
            String content = "你好,我是AI助手!";
            for (char c : content.toCharArray()) {
                emitter.send(SseEmitter.event()
                        .data("{\"content\": \"" + c + "\"}"));
                Thread.sleep(100);
            }
            emitter.send(SseEmitter.event().data("[DONE]"));
            emitter.complete();     // 标记完成
        } catch (Exception e) {
            emitter.completeWithError(e);   // 标记错误
        }
    });
    
    return emitter;
}

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,手动处理)
  • 同步流式的局限性:阻塞线程,不适合高并发,引出异步流式的需求

学习建议

  1. 先跑通最简单的例子:写一个 FastAPI 的 SSE 接口 + 一个 EventSource 的前端页面,看到逐字输出的效果,理解整个链路。
  2. 重点理解"流"的概念 :流式响应不是"一次性返回所有数据",而是"边生成边发送"。yield 是 Python 中实现这个概念的关键工具。理解生成器的"暂停→返回→继续"机制,就理解了流式响应的核心。
  3. 理解 HTTP 底层:SSE 之所以能"流式"传输,是因为 HTTP 的 Chunked Transfer Encoding。面试追问"和长连接有什么区别"时,能说出 chunked 编码会非常加分。
  4. 对比表格要记牢:SSE vs WebSocket 的对比是面试必考题。不需要逐字背诵,但要能说出核心区别:通信方向、协议、数据格式、重连机制、适用场景。
  5. Java 两种实现都了解:面试问到 Java 方向时,能说出 WebFlux 的响应式方式和 SseEmitter 的传统方式,并解释区别,会是很大的加分项。
相关推荐
Java后端的Ai之路6 天前
SSE 接口设计 vs Agent UI:四个开源项目,把「模型吐词」和「界面更新」拆开后,我看懂了差距
ui·开源·sse·agui·agentui
扉伟庆1 个月前
API 网关真流式改造实战:长文翻译首字延迟从 15 秒级降到亚秒级
性能优化·sse·api网关·deepseek·流式输出
thesky1234561 个月前
27届大模型面试准备(七十二):大模型流式推理服务与长连接工程——SSE、背压与首包优化
大模型·sse·长连接·背压·流式推理·首包优化·ttft
名字还没想好☜1 个月前
Next.js Route Handler 做 SSE 服务端推送:实时进度条、自动重连与什么时候别用 WebSocket
开发语言·javascript·websocket·react·sse·next.js
罗小爬EX2 个月前
SSE 流式响应多行文本编码方案
ai·sse
weixin_431600442 个月前
前端对接 SSE 的两种常见方式
前端·后端·学习·ai·sse·nest.js
我欲扶摇九万里2 个月前
SpringAI前置基础学习记录(一)|SSE协议及具体实现(SseEmitter)
java·人工智能·sse
FeelTouch Labs2 个月前
将Mcp stdio托管为SE / StreamableHTTP实现方案
sse·mcp·stdio·streamablehttp