企业实战:SSE快速入门


在这里插入图片描述

1 应用场景

SSE (Server-Sent Events) 是一种 基于 HTTP 的服务端单向推送 技术:浏览器用一个长连接(通常是 GET)订阅,服务端持续向这个连接 按事件(event)流式写数据,浏览器按事件回调接收。

  • 单向推送:服务端 → 客户端(客户端发消息仍走普通 HTTP 请求)

  • 事件化:每条消息带 event 类型 + data 数据

  • 自动重连:浏览器 EventSource 断线会自动重连(可配合 retry)

  • 轻量:基于 HTTP,不需要 WebSocket 的双工协议栈

  • 任务进度/阶段状态:文件上传处理、图执行流程、批处理进度条

  • LLM 流式输出:边生成边展示(token/片段增量 delta)

  • 日志/监控流:持续输出日志、告警、指标变化

  • 通知推送:轻量通知、状态变化(但不适合高频双向交互)

2 数据格式

SSE 协议规定了服务端向前端推送数据的固定核心格式,前端需按此解析,具体规则如下:

[可选] event: <事件名> 用于分类数据(如progress进度、result答案、error报错),前端可按事件名单独处理;
必填   data: <数据内容>\n\n 必填:\n\n(两个连续换行符,作为单条数据的结束标识)
(扩展)id: <唯一ID>/retry: <重连毫秒数>:可选,用于断点续传、自定义重连时间。

其中 event 把数据按事件分类,比如进度变更、答案输出、报错等等。data 是自定义数据。

3 SSE基础入门

场景一:最基础的 SSE

目标:后端每秒推 1 条固定消息,前端实时显示

步骤1:后端代码(sse_step1.py)

import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware

# 1. 初始化+跨域(最基础配置)
app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 仅测试用
    allow_methods=["*"],
    allow_headers=["*"],
)

# 2. 核心:SSE接口(只推固定数据)
@app.get("/simple_stream")
async def simple_stream():
    async def event_generator():
        # 模拟推5条消息,每秒1条
        for i in range(5):
            # ✅ 核心:SSE固定格式 data: 内容\n\n
            # yield f"event:自定义\ndata:xxxx\n\n"
            yield f"data: 这是第{i+1}条测试消息\n\n"
            await asyncio.sleep(1)  # 每秒推1条

    # ✅ 核心:StreamingResponse + media_type=text/event-stream
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream"
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)

步骤2:前端代码(sse_step1.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>步骤1:基础SSE</title>
</head>
<body>
    <h3>步骤1:接收后端固定消息</h3>
    <div id="result"></div>

    <script>
        // ✅ 核心:创建EventSource连接SSE接口
        const eventSource = new EventSource("http://127.0.0.1:8001/simple_stream");
        const resultDom = document.getElementById("result");

        // 监听SSE消息(实时接收)
        eventSource.onmessage = function(event) {
            resultDom.innerHTML += event.data + "<br>";
        };
    </script>
</body>
</html>

步骤3:运行演示

  1. 启动后端:python sse_step1.py

  2. 打开sse_step1.html,能看到页面每秒显示 1 条消息:

    这是第1条测试消息
    这是第2条测试消息
    ...
    这是第5条测试消息
    

核心知识点拆解:

  1. async def 异步函数

    FastAPI 支持异步接口,async 标识该函数可以执行异步操作(比如

    await asyncio.sleep(1)),不会阻塞整个服务的其他请求,性能更好。

  2. 异步生成器 event_generator()

    • async def 定义,内部通过 yield 逐次返回数据(而非 return 一次性返回);
    • 每次 yield 都会向客户端推送一段数据,直到循环结束;
    • await asyncio.sleep(1) 是异步休眠,区别于 time.sleep(1)(同步休眠会阻塞),保证服务能同时处理其他请求。
  3. StreamingResponse 流式响应

    FastAPI 提供的专门用于 “流式返回数据” 的响应类,接收一个生成器(或异步生成器)作为参数,会逐次把生成器 yield 的内容发送给客户端。

  4. SSE 协议核心规则

    • 响应的 media_type 必须设为 text/event-stream,客户端(比如浏览器)才能识别这是 SSE 流;
    • 推送的每条消息必须遵循 data: 内容\n\n 格式(\n\n 是消息结束的分隔符,缺一不可);
    • SSE 是单向通信(服务器→客户端),适合实时推送通知、日志、进度等场景。

场景二:动态传参

目标:前端传会话 ID,后端按 ID 返回专属消息

步骤1:后端代码(sse_step2.py)

import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

# 新增:接口接收session_id参数
@app.get("/stream/{session_id}")
async def stream_by_session(session_id: str):
    async def event_generator():
        for i in range(5):
            # 按session_id定制消息
            yield f"data: 会话{session_id} - 第{i+1}条消息\n\n"
            await asyncio.sleep(1)

    return StreamingResponse(event_generator(), media_type="text/event-stream")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)

步骤2:前端代码(sse_step2.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>步骤2:按会话ID推数据</title>
</head>
<body>
    <h3>步骤2:输入会话ID,接收专属消息</h3>
    <input type="text" id="sessionIdInput" placeholder="输入会话ID(如123)" value="123">
    <button onclick="connectSSE()">连接SSE</button>
    <div id="result"></div>

    <script>
        let eventSource = null;
        const resultDom = document.getElementById("result");

        function connectSSE() {
            const sessionId = document.getElementById("sessionIdInput").value;
            resultDom.innerHTML = "";
            
            // ✅ 新增:URL带session_id参数
            eventSource = new EventSource(`http://127.0.0.1:8001/stream/${sessionId}`);
            eventSource.onmessage = function(event) {
                resultDom.innerHTML += event.data + "<br>";
            };
        }
    </script>
</body>
</html>

步骤3:运行演示

启动后端,打开前端,输入 “abc123” 点击连接,页面显示:

会话abc123 - 第1条消息
会话abc123 - 第2条消息
...

核心知识点

  • 后端:通过 URL 路径参数(session_id)接收前端传参;
  • 前端:SSE 连接的 URL 可动态拼接参数,实现 “一对一” 推送。

场景三:异步任务

目标:后端先接收查询请求,后台处理,SSE 推处理结果

步骤1: 后端代码(sse_step3.py)

import asyncio
from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

# 核心优化:用异步队列存储每个会话的待推送数据(替代列表+轮询)
task_queues = {}


# 异步耗时任务:直接往队列丢数据,不用列表累加
async def long_task(session_id: str):
    # 为当前会话创建专属队列
    queue = asyncio.Queue()
    task_queues[session_id] = queue

    # 模拟5秒处理,每秒生成1条结果并丢进队列
    for i in range(5):
        msg = f"会话{session_id}处理结果{i + 1}"
        await queue.put(msg)  # 把数据丢进队列
        await asyncio.sleep(1)

    # 关键:丢一个"结束标记",告诉SSE可以停止了
    await queue.put(None)


# 提交任务接口(逻辑不变)
@app.get("/submit/{session_id}")
async def submit_task(session_id: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(long_task, session_id)
    return {"message": "任务已启动", "session_id": session_id}


# 简化后的SSE接口:直接从队列取数据,没有轮询!
@app.get("/stream/{session_id}")
async def stream_result(session_id: str):
    async def event_generator():
        # 获取当前会话的队列(没有则等待任务创建)
        while session_id not in task_queues:
            await asyncio.sleep(0.1)
        queue = task_queues[session_id]

        # 核心:循环从队列取数据,有数据就推,收到结束标记就停
        while True:
            msg = await queue.get()  # 阻塞等待队列数据(比轮询高效)
            if msg is None:  # 收到结束标记,退出循环
                break
            yield f"data: {msg}\n\n"  # 推送数据

    return StreamingResponse(event_generator(), media_type="text/event-stream")


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=8001)

asyncio.Queue 是 Python 异步编程(asyncio 框架)里的异步队列,专门解决异步场景下 “生产者 - 消费者” 的通信问题,你可以把它理解成一个「异步版的消息中转站」—— 生产者(比如你的后台任务)往里面丢数据,消费者(比如你的 SSE 接口)从里面取数据,全程不阻塞、不轮询,比你之前用的 “列表 + 轮询” 高效得多。

先讲核心特点(和普通列表 / 同步队列的区别):

特性普通列表asyncio.Queue(异步队列)
取数据的方式主动轮询(每 0.5s 查一次)被动等待(有数据才唤醒)
是否阻塞不阻塞(空列表也能查)异步阻塞(没数据就暂停,不占 CPU)
线程 / 协程安全不安全(多协程操作易出错)天然安全(专为异步场景设计)
代码复杂度高(要判断长度、处理重复)低(只需要 put/get

核心用法(通俗解释):

asyncio.Queue 的用法特别简单,核心就 3 个方法,且都需要用 await 调用(因为是异步操作):

创建队列

# 创建一个无界异步队列(能装无限多数据)
queue = asyncio.Queue()
# 也可以指定最大容量(比如最多装10条,满了之后put会等待)
queue = asyncio.Queue(maxsize=10)

生产者:往队列里放数据(put

await queue.put("要推送的消息")  # 把数据丢进队列
# 如果队列满了(指定了maxsize),这行代码会暂停,直到队列有空闲位置

对应你代码里的后台任务:await queue.put(msg),每秒往队列丢一条消息。

消费者:从队列里取数据(get

msg = await queue.get()  # 从队列取数据
# 如果队列为空,这行代码会「异步暂停」,直到队列里有新数据才唤醒

对应你代码里的 SSE 接口:msg = await queue.get(),没有数据就等着,有数据就立刻取,不用你手动轮询。

步骤2:前端代码(sse_step3.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>步骤3:异步任务+SSE推送</title>
</head>
<body>
    <h3>步骤3:提交任务,SSE接收处理结果</h3>
    <input type="text" id="sessionIdInput" placeholder="会话ID" value="test001">
    <button onclick="submitTask()">提交任务</button>
    <div id="result"></div>

    <script>
        let eventSource = null;
        const resultDom = document.getElementById("result");

        // 提交任务(触发后端异步处理)
        async function submitTask() {
            const sessionId = document.getElementById("sessionIdInput").value;
            resultDom.innerHTML = "";

            // 1. 调用提交接口
            await fetch(`http://127.0.0.1:8001/submit/${sessionId}`);
            
            // 2. 立即建立SSE连接,等结果
            eventSource = new EventSource(`http://127.0.0.1:8001/stream/${sessionId}`);
            eventSource.onmessage = function(event) {
                resultDom.innerHTML += event.data + "<br>";
            };
        }
    </script>
</body>
</html>

步骤3:运行演示

  1. 启动后端,打开前端,点击 “提交任务”;
  2. 页面每秒显示 1 条后端处理结果,5 秒后停止。

核心知识点

  • 后端:BackgroundTasks 实现异步任务,避免阻塞 SSE 连接;
  • 核心逻辑:“提交任务→后台处理→SSE 监听结果→实时推送”。

场景四:前端输入查询内容

目标:前端输入查询内容,后端按查询词返回结果

步骤1:后端代码(sse_step4.py)

import asyncio
from fastapi import FastAPI, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

app = FastAPI()
# 跨域配置(保持不变)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

# 替换列表:用异步队列存储每个会话的待推送数据
task_queues = {}

# 新增:定义请求体模型(保持不变)
class QueryRequest(BaseModel):
    query: str
    session_id: str

# 重构异步任务:往队列丢数据(替代列表累加)
async def long_task(session_id: str, query: str):
    # 为当前会话创建专属异步队列
    queue = asyncio.Queue()
    task_queues[session_id] = queue
    
    # 按查询词生成5条结果,每秒1条丢进队列
    for i in range(5):
        msg = f"【{query}】的第{i+1}段回答:xxx{i+1}"
        await queue.put(msg)  # 数据入队
        await asyncio.sleep(1)
    
    # 关键:放入结束标记,告诉SSE停止推送
    await queue.put(None)

# POST接口(逻辑不变,仅任务内部实现变了)
@app.post("/submit_query")
async def submit_query(req: QueryRequest, background_tasks: BackgroundTasks):
    # 把查询词和会话ID传给后台任务
    background_tasks.add_task(long_task, req.session_id, req.query)
    return {"message": "任务已启动", "session_id": req.session_id}

# 简化SSE接口:从队列取数据,无轮询
@app.get("/stream/{session_id}")
async def stream_result(session_id: str):
    async def event_generator():
        # 等待当前会话的队列创建(防止SSE比任务先启动)
        while session_id not in task_queues:
            await asyncio.sleep(0.1)
        queue = task_queues[session_id]
        
        # 循环取队列数据,有数据就推,收到结束标记就停
        while True:
            msg = await queue.get()  # 异步阻塞等待数据(无轮询)
            if msg is None:  # 收到结束标记,退出循环
                break
            yield f"data: {msg}\n\n"  # 推送SSE数据
    
    return StreamingResponse(event_generator(), media_type="text/event-stream")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)

步骤2:前端代码(sse_step4.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>步骤4:输入查询内容</title>
</head>
<body>
    <h3>步骤4:输入查询内容,SSE接收专属回答</h3>
    <input type="text" id="queryInput" placeholder="输入查询内容" value="Python SSE怎么用">
    <button onclick="submitQuery()">提交查询</button>
    <div id="result"></div>

    <script>
        let eventSource = null;
        const resultDom = document.getElementById("result");

        async function submitQuery() {
            const query = document.getElementById("queryInput").value;
            const sessionId = "query_" + Date.now(); // 自动生成会话ID
            resultDom.innerHTML = "";

            // 1. POST提交查询内容
            await fetch("http://127.0.0.1:8001/submit_query", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({query: query, session_id: sessionId})
            });

            // 2. 建立SSE连接
            eventSource = new EventSource(`http://127.0.0.1:8001/stream/${sessionId}`);
            eventSource.onmessage = function(event) {
                resultDom.innerHTML += event.data + "<br>";
            };
        }
    </script>
</body>
</html>

步骤3:运行演示

输入 “FastAPI 教程”,点击提交,页面显示:

【FastAPI教程】的第1段回答:xxx1
【FastAPI教程】的第2段回答:xxx2
...

核心知识点

  • 后端:用Pydantic模型接收 POST 请求体,解析查询内容;
  • 前端:POST 请求传 JSON 数据,实现 “用户输入→后端处理→实时返回”。

场景五:SSE 事件属性说明

SSE 协议中,除了核心的 data 字段,还支持 event(自定义事件类型)、id(消息 ID)、retry(重连时间)等属性:

  • event:用于给不同类型的消息标记自定义事件名,前端可以根据事件名区分处理不同消息(比如 “进度更新”“完成通知”);
  • 格式要求:event: 事件名\n 必须在 data: 内容\n\n 之前;
  • 改造方向:在生成 SSE 响应时,为不同阶段的消息(进度、完成)添加不同的 event 属性。

步骤1:后端代码(sse_step5.py)

import asyncio
import uuid
from fastapi import FastAPI, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

app = FastAPI()
# 跨域配置(保持不变)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"]
)

# ✅ 替换列表缓存:用异步队列存储每个会话的消息(key: session_id, value: asyncio.Queue)
task_queues = {}

class QueryRequest(BaseModel):
    query: str
    session_id: str = None

@app.post("/submit_query")
async def submit_query(req: QueryRequest, background_tasks: BackgroundTasks):
    # 生成或使用用户传入的session_id
    session_id = req.session_id or str(uuid.uuid4())
    # 将耗时任务加入后台执行
    background_tasks.add_task(long_task, session_id, req.query)
    return {"message": "任务已经启动", "session_id": session_id}

async def long_task(session_id: str, query: str):
    """模拟耗时的异步任务,分阶段往队列丢消息(替代列表累加)"""
    # 为当前会话创建专属异步队列
    queue = asyncio.Queue()
    task_queues[session_id] = queue
    
    # 模拟5个进度步骤,往队列丢进度消息
    for i in range(5):
        progress_msg = {
            "event": "progress",
            "data": f"【{query}】的第{i+1}段回答:xxx{i+1}"
        }
        await queue.put(progress_msg)  # 进度消息入队
        await asyncio.sleep(1)
    
    # 任务完成,往队列丢完成消息
    complete_msg = {
        "event": "complete",
        "data": f"【{session_id}】查询完成!所有结果已返回"
    }
    await queue.put(complete_msg)
    # 关键:丢结束标记,告诉SSE可以停止监听
    await queue.put(None)

@app.get("/stream/{session_id}")
async def stream(session_id: str):
    """SSE流式返回任务结果,基于队列实现(无轮询)"""
    async def event_generator():
        # 等待当前会话的队列创建(防止SSE比任务先启动)
        while session_id not in task_queues:
            await asyncio.sleep(0.1)
        queue = task_queues[session_id]
        
        # 循环从队列取消息,有消息就推,收到结束标记就停
        while True:
            msg = await queue.get()  # 异步阻塞等待消息(无轮询)
            if msg is None:  # 收到结束标记,退出循环
                break
            
            # 拼接自定义Event的SSE格式(和你原逻辑一致)
            yield f"event: {msg['event']}\n"
            yield f"data: {msg['data']}\n\n"
    
    return StreamingResponse(event_generator(), media_type="text/event-stream")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)

long_task 中不再存储纯文本,而是存储包含 eventdata 的字典,分别标记消息类型(progress/complete)和内容;

这样可以区分 “进度更新” 和 “任务完成” 两类消息,前端能针对性处理。

标准 SSE 格式要求:event: 事件名\n + data: 内容\n\n

示例输出(前端收到的原始数据):

event: progress
data: 【测试】的第1段回答:xxx1

event: complete
data: 【xxx-xxx-xxx】查询完成!所有结果已返回

步骤2:前端代码(sse_step5.py)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>FastAPI SSE 测试</title>
    <!-- 仅保留必要的基础样式,无复杂装饰 -->
    <style>
        body { padding: 20px; }
        #response { margin-top: 20px; white-space: pre-wrap; }
        .progress { color: #666; }
        .complete { color: green; }
        .error { color: red; }
    </style>
</head>
<body>
    <!-- 核心交互区域 -->
    <input type="text" id="query" placeholder="输入查询内容" style="width: 400px; padding: 5px;">
    <button onclick="submitQuery()">提交查询</button>

    <!-- 流式响应展示区 -->
    <div id="response"></div>

    <script>
        // 后端接口地址(和你的FastAPI启动地址一致)
        const API_BASE = 'http://127.0.0.1:8001';
        let eventSource = null; // 存储SSE连接实例

        // 提交查询的核心函数
        async function submitQuery() {
            const query = document.getElementById('query').value.trim();
            if (!query) {
                alert('请输入查询内容!');
                return;
            }

            // 清空之前的响应内容
            document.getElementById('response').innerHTML = '';

            // 1. 调用submit_query接口获取session_id
            try {
                const res = await fetch(`${API_BASE}/submit_query`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ query: query })
                });
                const data = await res.json();
                const sessionId = data.session_id;

                // 2. 建立SSE连接监听流式响应
                connectSSE(sessionId);
            } catch (err) {
                document.getElementById('response').innerHTML = `<div class="error">提交失败:${err.message}</div>`;
            }
        }

        // 建立SSE连接的函数
        function connectSSE(sessionId) {
            // 关闭已有连接,避免重复监听
            if (eventSource) eventSource.close();

            // 创建新的SSE连接
            eventSource = new EventSource(`${API_BASE}/stream/${sessionId}`);

            // 监听进度事件(progress)
            eventSource.addEventListener('progress', (e) => {
                const responseDiv = document.getElementById('response');
                responseDiv.innerHTML += `<div class="progress">${e.data}</div>`;
            });

            // 监听完成事件(complete)
            eventSource.addEventListener('complete', (e) => {
                const responseDiv = document.getElementById('response');
                responseDiv.innerHTML += `<div class="complete">${e.data}</div>`;
                eventSource.close(); // 完成后关闭连接
            });

            // 监听错误事件
            eventSource.onerror = (e) => {
                document.getElementById('response').innerHTML += `<div class="error">连接异常:${e.message || '未知错误'}</div>`;
                eventSource.close();
            };
        }
    </script>
</body>
</html>

event 是 SSE 协议的扩展属性,用于分类消息,前端可通过 addEventListener(事件名) 监听;

SSE 消息必须以 \n\n 结尾,多个字段(event/data)之间用 \n 分隔;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赵广陆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值