
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:运行演示
-
启动后端:
python sse_step1.py; -
打开sse_step1.html,能看到页面每秒显示 1 条消息:
这是第1条测试消息 这是第2条测试消息 ... 这是第5条测试消息
核心知识点拆解:
-
async def异步函数FastAPI 支持异步接口,async 标识该函数可以执行异步操作(比如
await asyncio.sleep(1)),不会阻塞整个服务的其他请求,性能更好。
-
异步生成器 event_generator()
- 用
async def定义,内部通过yield逐次返回数据(而非return一次性返回); - 每次
yield都会向客户端推送一段数据,直到循环结束; await asyncio.sleep(1)是异步休眠,区别于time.sleep(1)(同步休眠会阻塞),保证服务能同时处理其他请求。
- 用
-
StreamingResponse流式响应FastAPI 提供的专门用于 “流式返回数据” 的响应类,接收一个生成器(或异步生成器)作为参数,会逐次把生成器 yield 的内容发送给客户端。
-
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 条后端处理结果,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 中不再存储纯文本,而是存储包含 event 和 data 的字典,分别标记消息类型(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 分隔;
1387

被折叠的 条评论
为什么被折叠?



