Server-Sent Events (SSE):让服务器“主动开口”的实时推送技术

你用过 ChatGPT 吗?当你问它"帮我写一篇关于 AI 的文章"时,屏幕上的文字不是一个字一个字突然蹦出来的,而是像有人在你面前打字一样,一个字一个字地流式浮现。这种感觉极其自然,就像 AI 在"思考"和"输出"同步进行。

但如果用传统的 Web 技术来实现,情况会非常滑稽:你发出提问后,服务器要憋 30 秒,等整篇 1000 字的文章全部生成完毕,一次性把完整的文本打包发给你。在这 30 秒里,你只能面对一个白屏或者转圈的 loading 动画,什么也看不到。30 秒后,文章"咣当"一下全砸你脸上。

这在技术上是可行的,但用户体验糟糕透顶。你完全不知道进度,不知道它是不是卡死了,只能焦虑地盯着转圈圈。

为了解决"服务器要花很长时间处理,但客户端要实时看到进度"的问题,Web 世界诞生了一种轻量级的技术:SSE(Server-Sent Events,服务器发送事件)。

本文将站在 Web 用户和开发者的双重角度,讲清楚 SSE 是什么、它和普通 HTTP 请求有什么区别、为什么它是 AI 流式输出的标配,以及如何在 FastAPI 中优雅地实现它。

一、什么是 SSE?(餐厅的"传呼机"类比)

想象你去一家火爆的网红餐厅(后端服务器)吃饭,点了一份复杂的菜(用户发起的耗时请求,比如让 AI 写文章)。

普通 HTTP 请求(非流式):就像你去柜台点完菜,然后傻站在柜台前。厨师在后厨慢悠悠地切菜、炒菜,你在前台一等就是 30 分钟,脚都站麻了。直到 30 分钟后厨师把菜端出来递给你。这期间你没有收到任何"正在做"的信号,以为厨师把你忘了。这就是"请求-响应"模型------要么没有,要么全有。

SSE 流式响应:就像点完菜后,服务员递给你一个传呼机(SSE 连接)。厨师切好第一段葱,传呼机"嘀"一声,屏幕上显示"正在切菜";炒到一半,传呼机又"嘀"一声:"正在大火爆炒";菜出锅装盘,传呼机"嘀"一声:"您的菜即将上桌"。整个过程,你坐在座位上,看着传呼机不断更新进度,心里特别踏实。

SSE 的核心机制就是:客户端发起一个普通 HTTP 请求后,服务器不关闭连接,而是保持这个连接"打开"状态,并持续不断地向客户端推送数据块(数据块),直到数据传输完毕或超时。

在技术实现上,SSE 非常轻量。它不需要 WebSocket 那样复杂的握手协议,只需要在 HTTP 响应头里设置 Content-Type: text/event-stream,然后像写文件一样,把数据一点一点写进响应体里(格式为 data: xxx\n\n)。

二、没有 SSE 的混乱现场(传统轮询的噩梦)

在 SSE 出现之前,或者如果开发者不用它,想实现"实时进度反馈",只能用一种极其愚蠢且耗费资源的方法:轮询(Polling)。

比如,你的 AI 生成文章要 30 秒。前端(浏览器)会这样做:

第 0 秒:点击"生成",发请求 POST /generate。服务器说"收到,任务 ID 是 123",然后前端开始疯狂循环。

第 1 秒:前端问 GET /status/123?服务器回:"还没好"。

第 2 秒:前端又问:"还没好?"服务器回:"还没好。"

第 3 秒~第 29 秒:前端每隔 1 秒就发一次请求,服务器每次都回复"还没好"。

第 30 秒:前端问:"好了没?"服务器回:"好了!"然后把完整的 1000 字一次性传过来。

灾难性后果:

服务器压力巨大:一个用户 30 秒内发了 30 次请求。如果有 1000 个用户,每秒就要处理 3 万个无效的"查状态"请求,严重浪费服务器 CPU 和数据库连接数(惊群效应)。

实时性差:如果轮询间隔是 1 秒,用户最多可能延迟 1 秒才知道结果。如果把间隔缩到 0.1 秒,服务器就直接被冲垮了。

用户体验割裂:用户只能看到"生成中..."的固定文字,中间没有任何进度变化。如果生成失败了,要等到 30 秒后才报错。

三、FastAPI 实现 SSE(代码实战:让你的 AI"开口说话")

在 FastAPI 中,实现 SSE 出奇地简单。核心是利用 StreamingResponse 配合 Python 的 异步生成器(async generator)。

  1. 基础实现:一个每秒报时的"时钟"

    from fastapi import FastAPI
    from fastapi.responses import StreamingResponse
    import asyncio

    app = FastAPI()

    async def event_generator():
    """这是一个异步生成器,会不断产出数据"""
    for i in range(10):
    # 格式化数据:SSE 标准要求每段数据前加 "data: ",并以 \n\n 结尾
    yield f"data: 当前进度 {i}/10\n\n"
    await asyncio.sleep(1) # 模拟耗时操作
    yield f"data: 完成!\n\n"

    @app.get("/stream")
    async def stream_endpoint():
    # 返回 StreamingResponse,媒体类型设为 text/event-stream
    return StreamingResponse(event_generator(), media_type="text/event-stream")

前端接收:只需监听 EventSource 接口,就能实时收到数据。

复制代码
const eventSource = new EventSource('/stream');
eventSource.onmessage = function(event) {
    console.log('收到数据:', event.data);
    // 你的界面更新逻辑
};
  1. 实战:AI 流式对话(逐字输出打字机效果)

这是大模型应用中最经典的用法。假设你在调用一个异步的 LLM,它支持流式返回 tokens:

复制代码
import httpx
from fastapi.responses import StreamingResponse

async def stream_llm_response(prompt: str):
    """模拟/真实调用大模型,流式产出 token"""
    async with httpx.AsyncClient() as client:
        # 假设远程大模型支持流式 API(如 OpenAI 的 stream=True)
        async with client.stream(
            "POST",
            "https://api.openai.com/v1/chat/completions",
            json={"model": "gpt-4", "messages": [{"role": "user", "content": prompt}], "stream": True},
            headers={"Authorization": "Bearer YOUR_KEY"}
        ) as response:
            async for chunk in response.aiter_bytes():
                # 处理原始数据,提取文本增量,然后推给前端
                # 这里简化逻辑,实际需要解析 SSE 格式
                yield f"data: {chunk.decode('utf-8')}\n\n"

@app.post("/chat")
async def chat(prompt: str):
    return StreamingResponse(stream_llm_response(prompt), media_type="text/event-stream")

此时,用户打开网页,会看到文字一个字一个字地往外蹦,就像 ChatGPT 一样。用户能提前看到生成内容,可以随时决定"结果不理想,提前中断",不用干等 30 秒。

四、SSE 的四大核心作用(站在用户/运维视角)

  1. 极致的用户体验(感知延迟降低)

这是最直接的作用。心理学研究表明,如果系统在 100ms 内给出反馈(哪怕是进度条),用户的焦虑感会大大降低。SSE 把"等待 30 秒结果"变成了"30 秒内持续看到变化"。

用户感知:APP 响应很快,很"智能",用起来很爽。

  1. 大幅降低服务器负载(告别轮询)

与轮询相比,SSE 只占用一个长连接。没有成千上万无效的"查状态"请求撞击数据库,服务器可以轻松支持上万并发用户。

运维感知:服务器 CPU 占用率显著下降,不用频繁扩容。

  1. 自动断线重连(网络稳定性保障)

SSE 协议内置了断线重连机制。如果用户的 WiFi 闪断了一下,浏览器会自动重新连接,并告知服务器上一次收到的数据 ID,服务器可以从此处继续推送,用户感知不到断线。

  1. 轻量级实时通道(比 WebSocket 简单得多)

如果你只需要服务器单向推送数据(前端不需要发送数据,或者发送数据用普通 POST 就行),SSE 比 WebSocket 简单得多。它基于 HTTP,不需要复杂的协议升级,能穿透绝大多数防火墙。

五、SSE vs WebSocket vs 普通 HTTP:一张表看懂选型

如果数据主要从服务器流向客户端(推送),用 SSE;如果需要双向实时交互(比如你一言我一语的游戏),用 WebSocket。

六、高级进阶:SSE 中的"中断控制"与"心跳保活"

  1. 让前端可以"停止生成"(中断控制)

SSE 是长连接。如果用户觉得 AI 生成的内容不对,点了"停止"按钮,前端需要关掉连接。前端调用 eventSource.close() 即可,FastAPI 端的生成器会检测到连接断开,从而自动停止消耗资源。

复制代码
async def event_generator():
    try:
        while True:
            yield "data: ...\n\n"
            await asyncio.sleep(0.1)
    except asyncio.CancelledError:
        # 客户端断开连接时,清理资源(如释放数据库连接)
        print("客户端中断了连接,清理资源中...")
        raise
  1. 心跳保活(防止被 Nginx 踢下线)

Nginx 等反向代理如果长时间没收到数据,会误以为连接空闲而断开(默认 60 秒)。如果生成太慢,需要定时发送"心跳包"(注释行):

复制代码
async def event_generator():
    while True:
        # 发送心跳(注释行,前端 JS 可以忽略,但能保持连接存活)
        yield ": heartbeat\n\n"  
        await asyncio.sleep(15)  # 每 15 秒发一次

七、最佳实践与避坑指南

  1. 前端需要用 EventSource API

不要用普通的 fetch 或 axios,它们会把所有数据等齐了一次性给你。必须使用浏览器原生的 new EventSource(url),或者用 fetch + 手动读取 ReadableStream(更灵活,但代码量大)。

  1. 数据格式必须严格遵守

SSE 要求数据格式为:

复制代码
data: 你的内容\n\n

如果有多行数据,需要多行 data:

复制代码
data: line 1\n
data: line 2\n\n

如果传了非标准格式,前端可能收不到数据。

  1. 注意 media_type 必须正确

在 FastAPI 中,返回 StreamingResponse 时,必须指定 media_type="text/event-stream",否则浏览器不会将其识别为 SSE 流。

  1. 处理代理和负载均衡

如果你使用了 Nginx,需要关闭缓冲,否则 Nginx 会把数据攒到一定大小才发送,破坏流式效果:

复制代码
proxy_buffering off;
  1. 不要用 SSE 传超大二进制文件

SSE 是为文本设计的,传输大文件(图片、视频)会因编码转换导致严重性能损失。传二进制文件请用普通的文件下载接口。

  1. 同源策略限制

EventSource 默认受同源策略限制(不支持跨域发 Authorization 头)。如果需要携带 Token,建议将 Token 放在 URL 查询参数中(如 /stream?token=xxx),或者使用 Fetch API + ReadableStream 代替。

复制代码
// 如果必须带 Header,使用 Fetch 替代 EventSource
const response = await fetch('/stream', {
    headers: { 'Authorization': 'Bearer token' }
});
const reader = response.body.getReader();
// 手动解析流...

八、结语

对于普通 Web 用户而言,SSE 就是你用 AI 工具时,屏幕上那个"一个字一个字蹦出来"的魔法。它让你告别了白屏等待的焦虑,让"思考过程"可见,让互动变得极其自然流畅。你不会在意背后是什么技术,但你会说:"这个网站响应真快,一点都不卡。"

对于 FastAPI 开发者而言,SSE 是构建现代 AI 应用、实时监控系统、消息推送服务不可或缺的"轻骑兵"。它比轮询优雅,比 WebSocket 简单,用极低的成本就把用户体验提升了一个量级。尤其是配合 StreamingResponse 和异步生成器,你几乎不用改动原有的业务逻辑,就能把"一次性返回"秒变"流式输出"。

记住:在 AI 时代,流式输出不是锦上添花,而是用户体验的基本要求。 掌握 SSE,就是掌握了让应用从"能用"迈向"好用"的关键一步。

相关推荐
BerryS3N1 小时前
GitHub Actions自动化运维实战:从CI/CD到云端部署的完整指南
运维·自动化·github
不能跑的代码不是好代码9 小时前
Linux系统常用命令中文速查表
linux·运维·服务器
石一峰69910 小时前
深入理解 Linux 中断三层机制与 1-Wire 时序锁原理
linux·运维·服务器
运维老郭10 小时前
PostgreSQL编译安装实战
运维·postgresql
无限码农11 小时前
Linux上通过cmake编译uchardet
linux·运维·服务器
运维大师12 小时前
【Linux运维极简教程】06-网络配置与管理
linux·运维
007张三丰13 小时前
软件测试专栏(15/20):REST Assured接口自动化框架实战
运维·自动化·jenkins·接口自动化·rest·assured
ACP广源盛1392462567313 小时前
IX8024@ACP# 搭配此芯 AI 服务器 + 爱芯元智产品完整方案
大数据·运维·服务器·人工智能·分布式·嵌入式硬件
Hiyajo pray14 小时前
SDN 访问控制性能调优方法
服务器·网络·网络安全