FastAPI SSE WebSocket 实战教程

第一章 项目初始化与环境搭建
1.1 使用 uv 初始化项目
uv 是新一代 Python 项目管理和包管理工具,速度比 pip 快 10-100 倍。
# 初始化项目(--app 表示这是一个应用项目,--name 指定包名)
uv init --app --name demo-app
执行后生成 pyproject.toml,这是项目的配置文件,记录项目元数据和依赖信息。
1.2 安装依赖
使用 uv add 安装依赖,不指定版本号会自动安装最新版本:
uv add fastapi uvicorn websockets
各依赖的作用:
- fastapi:Web 框架,提供路由、请求处理等核心功能
- uvicorn:ASGI 服务器,负责运行 FastAPI 应用
- websockets:WebSocket 协议支持库
安装完成后,项目根目录会生成 .venv 虚拟环境目录和 uv.lock 依赖锁定文件。
1.3 项目目录结构
项目根目录/
├── src/
│ └── main.py # Python 主文件(后端代码)
├── templates/
│ └── index.html # HTML 页面(前端代码)
├── pyproject.toml # 项目配置文件
├── uv.lock # 依赖锁定文件
└── .venv/ # 虚拟环境
第二章 创建 FastAPI 应用
2.1 导入模块并创建实例
import asyncio
import datetime
from pathlib import Path
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse, HTMLResponse
app = FastAPI()
FastAPI():创建应用实例,所有路由和功能都注册在这个实例上WebSocket:WebSocket 连接的类,提供收发消息的方法WebSocketDisconnect:客户端断开连接时触发的异常,需要捕获处理StreamingResponse:流式响应,用于 SSE 场景HTMLResponse:返回 HTML 内容的响应类型
2.2 启动服务
if __name__ == "__main__":
config = uvicorn.Config(app, host="127.0.0.1", port=8000)
server = uvicorn.Server(config)
asyncio.run(server.serve())
uvicorn.Config + uvicorn.Server 是比 uvicorn.run() 更现代的启动方式:
Config封装所有配置参数(主机、端口、日志级别等)Server提供精细化的生命周期控制(优雅关闭等)asyncio.run()运行异步入口
运行命令:
.venv\Scripts\python src\main.py
打开浏览器访问 http://127.0.0.1:8000,会看到空白页面(因为我们还没写路由),终端会输出:
INFO: Started server process [xxxxx]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000
第三章 返回静态页面
3.1 为什么需要独立 HTML 文件
将 HTML 代码从 Python 中分离出来,好处很明显:
- 前后端解耦,修改样式无需改动 Python 代码
- HTML 文件获得 IDE 的高亮、补全支持
- 文件结构清晰,维护方便
3.2 编写首页路由
@app.get("/")
async def index():
html_path = Path(__file__).parent.parent / "templates" / "index.html"
html_content = html_path.read_text(encoding="utf-8")
return HTMLResponse(html_content)
@app.get("/"):装饰器,将函数注册为 HTTP GET 请求的处理函数Path(__file__):当前 Python 文件(src/main.py)的路径.parent.parent:上两级目录,即项目根目录read_text(encoding="utf-8"):以 UTF-8 编码读取文件内容HTMLResponse():将字符串包装为 HTML 响应返回给浏览器
这样,访问 http://127.0.0.1:8000 时,服务端读取 templates/index.html 文件内容并返回给浏览器渲染。
第四章 异步编程基础
在进入 SSE 和 WebSocket 之前,需要先理解异步编程的几个核心概念。
4.1 async / await
import asyncio
async def my_function():
# async 定义异步函数
print("开始执行")
await asyncio.sleep(1) # await 等待异步操作完成
print("1秒后执行")
async def:定义异步函数(协程),调用时返回一个协程对象,不会立即执行await:等待一个异步操作完成,同时让出事件循环,让其他任务执行
4.2 异步生成器
async def my_generator():
for i in range(5):
await asyncio.sleep(1)
yield i # yield 每次返回一个值,函数暂停,下次继续
async def+yield:定义异步生成器- 每次
yield返回一个值后函数暂停执行 - 下次迭代时从
yield之后继续 - 这使得函数可以"边生产边消费",不需要一次性生成所有数据
第五章 SSE 服务端推送
5.1 SSE 协议简介
SSE(Server-Sent Events,服务端推送事件)是一种轻量级的实时通信协议:
- 基于 HTTP 协议,使用简单
- 服务端可以主动向客户端推送数据
- 客户端使用
EventSourceAPI 接收 - 适合单向推送场景:通知、行情、日志等
SSE 数据格式:
data: 消息内容\n\n
必须以 data: 开头,\n\n(两个换行)结尾。
5.2 编写 SSE 事件生成器
async def event_generator():
while True:
now = datetime.datetime.now().strftime("%H:%M:%S")
yield f"data: 当前时间: {now}\n\n"
await asyncio.sleep(1)
while True:持续循环,每个连接生命周期内运行datetime.datetime.now().strftime("%H:%M:%S"):获取当前时间并格式化为HH:MM:SSyield:每次产生一个 SSE 事件后暂停,等待下一次迭代await asyncio.sleep(1):休眠 1 秒,让出事件循环给其他请求处理
关键理解:这不是死循环。每次 yield 后函数暂停,等待 StreamingResponse 消费;sleep 期间事件循环可处理其他请求;客户端断开时生成器被自动取消。
5.3 注册 SSE 路由
@app.get("/sse")
async def sse():
return StreamingResponse(event_generator(), media_type="text/event-stream")
@app.get("/sse"):注册 GET 请求路由StreamingResponse():将异步生成器包装为流式 HTTP 响应media_type="text/event-stream":设置 Content-Type,浏览器识别此类型后会保持连接并持续接收
5.4 前端接收 SSE
const evtSource = new EventSource('/sse');
evtSource.addEventListener('message', (e) => {
sseDiv.innerHTML = `<div>${e.data}</div>`;
});
EventSource('/sse'):创建 SSE 连接,自动连接到/sse端点addEventListener('message', ...):监听message事件,每次服务端推送触发e.data:服务端推送的数据内容innerHTML =:覆盖显示最新一条数据(替换之前的)
第六章 WebSocket 双向通信
6.1 WebSocket 协议简介
WebSocket 与 SSE 的核心区别:
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 仅服务端→客户端 | 双向通信 |
| 协议 | HTTP | 独立协议(ws://) |
| 数据格式 | 文本 | 文本/二进制 |
| 自动重连 | 内置支持 | 需手动实现 |
| 适用场景 | 通知、推送 | 聊天、游戏、协作 |
6.2 编写 WebSocket 端点
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
async for data in websocket.iter_text():
await websocket.send_text(f"收到: {data}")
except WebSocketDisconnect:
pass
@app.websocket("/ws"):注册 WebSocket 路由await websocket.accept():接受客户端的握手请求,完成 HTTP → WebSocket 协议升级async for data in websocket.iter_text():iter_text()是 Starlette 提供的异步迭代器- 逐个接收客户端发来的文本消息
- 客户端断开时会自动抛出
WebSocketDisconnect - 比
while True: data = await websocket.receive_text()更简洁
await websocket.send_text(f"收到: {data}"):向客户端发送文本消息except WebSocketDisconnect:捕获客户端断开异常,避免服务端报错
6.3 前端 WebSocket 客户端
const ws = new WebSocket('ws://127.0.0.1:8000/ws');
ws.addEventListener('open', () => {
wsDiv.innerHTML += '<div style="color:green">已连接</div>';
});
ws.addEventListener('message', (e) => {
wsDiv.innerHTML += `<div>${e.data}</div>`;
});
function send() {
const input = document.getElementById('msg');
ws.send(input.value);
input.value = '';
}
WebSocket('ws://...'):创建 WebSocket 连接addEventListener('open', ...):连接建立成功时触发addEventListener('message', ...):收到服务端消息时触发ws.send(input.value):向服务端发送消息- 使用
addEventListener而非onmessage/onopen属性赋值,是更现代的 JavaScript 实践,支持绑定多个监听器
第七章 启动方式演进
7.1 传统方式:uvicorn.run()
# 旧写法(仍然可用)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
uvicorn.run() 是一个便捷函数,内部封装了 Config 和 Server 的创建和启动逻辑。
7.2 现代方式:uvicorn.Config + Server
# 新写法(推荐)
if __name__ == "__main__":
config = uvicorn.Config(app, host="127.0.0.1", port=8000)
server = uvicorn.Server(config)
asyncio.run(server.serve())
优势:
- 更灵活:可以访问
Config和Server对象进行精细控制 - 优雅关闭:
Server支持should_exit信号,可以平滑关闭 - 便于扩展:可以同时运行多个服务或与其他异步任务一起启动
第八章 完整项目代码
8.1 src/main.py
"""FastAPI SSE + WebSocket 最简单案例(最新语法)"""
import asyncio
import datetime
from pathlib import Path
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse, HTMLResponse
# 创建 FastAPI 应用实例
app = FastAPI()
# ========== SSE 服务端推送 ==========
async def event_generator():
"""
异步生成器:每秒产生一个 SSE 事件
SSE 格式:data: 内容\n\n(两个换行结尾)
"""
while True:
# 获取当前时间并格式化为 HH:MM:SS
now = datetime.datetime.now().strftime("%H:%M:%S")
# yield 返回 SSE 数据行(data: 前缀 + 两个换行是 SSE 协议要求)
yield f"data: 当前时间: {now}\n\n"
# 休眠 1 秒,避免无限循环占用 CPU
await asyncio.sleep(1)
@app.get("/sse")
async def sse():
"""
SSE 端点
StreamingResponse 将异步生成器的内容以 text/event-stream 格式流式返回
浏览器端的 EventSource 会自动解析此格式
"""
return StreamingResponse(event_generator(), media_type="text/event-stream")
# ========== WebSocket 双向通信 ==========
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket 端点
@app.websocket 装饰器将函数注册为 WebSocket 处理器
"""
# 接受客户端的 WebSocket 握手请求(完成 HTTP 升级)
await websocket.accept()
try:
# iter_text() 是 Starlette 提供的异步迭代器
# 它会逐个接收客户端发来的文本消息
# 当客户端断开时自动抛 WebSocketDisconnect,无需手动 while True
async for data in websocket.iter_text():
# 将收到的消息原样返回(回声)
# 使用 f-string 构造回复字符串
await websocket.send_text(f"收到: {data}")
except WebSocketDisconnect:
# 客户端断开连接时捕获此异常(非错误,正常行为)
pass
# ========== 首页 ==========
@app.get("/")
async def index():
"""
首页:返回 HTML 测试页面
Path(__file__).parent.parent 定位到项目根目录(src/ 的上级)
"""
# 构建 HTML 文件的绝对路径
html_path = Path(__file__).parent.parent / "templates" / "index.html"
# 读取 HTML 文件内容(UTF-8 编码)
html_content = html_path.read_text(encoding="utf-8")
# 包装为 HTMLResponse 返回
return HTMLResponse(html_content)
# ========== 入口 ==========
if __name__ == "__main__":
# 使用 uvicorn.Config + uvicorn.Server 方式启动
# 相比 uvicorn.run(),这种方式支持更灵活的控制和优雅关闭
config = uvicorn.Config(app, host="127.0.0.1", port=8000)
server = uvicorn.Server(config)
# asyncio.run() 运行异步入口
asyncio.run(server.serve())
8.2 templates/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SSE + WebSocket 测试</title>
</head>
<body>
<!-- SSE 测试区域 -->
<h2>SSE 测试</h2>
<!-- SSE 消息会动态追加到这个 div 中 -->
<div id="sse" style="border:1px solid #ccc; padding:10px; min-height:60px;"></div>
<!-- WebSocket 测试区域 -->
<h2>WebSocket 测试</h2>
<!-- 用户输入消息的文本框 -->
<input id="msg" placeholder="输入消息" />
<!-- 点击按钮发送消息 -->
<button onclick="send()">发送</button>
<!-- WebSocket 消息会动态追加到这个 div 中 -->
<div id="ws" style="border:1px solid #ccc; padding:10px; margin-top:10px; min-height:60px;"></div>
<script>
// ====== SSE 客户端 ======
// 获取 SSE 显示区域的 DOM 元素
const sseDiv = document.getElementById('sse');
// 创建 EventSource 对象,连接到 /sse 端点
const evtSource = new EventSource('/sse');
// 监听 message 事件:每当服务端推送 SSE 事件时触发
evtSource.addEventListener('message', (e) => {
// e.data 包含服务端发送的数据(即 "当前时间: HH:MM:SS")
// 将数据覆盖显示(替换上一次结果)
sseDiv.innerHTML = `<div>${e.data}</div>`;
});
// ====== WebSocket 客户端 ======
// 获取 WebSocket 显示区域的 DOM 元素
const wsDiv = document.getElementById('ws');
// 创建 WebSocket 连接,连接到 ws://127.0.0.1:8000/ws
const ws = new WebSocket('ws://127.0.0.1:8000/ws');
// 监听 message 事件:服务端通过 WebSocket 发送消息时触发
ws.addEventListener('message', (e) => {
// e.data 包含服务端返回的文本
wsDiv.innerHTML = `<div>${e.data}</div>`;
});
// 监听 open 事件:WebSocket 连接建立成功时触发
ws.addEventListener('open', () => {
// 在显示区域追加绿色提示文字
wsDiv.innerHTML = '<div style="color:green">已连接</div>';
});
// 发送消息函数(点击按钮时调用)
function send() {
// 获取输入框中的文本
const input = document.getElementById('msg');
// 通过 WebSocket 发送文本到服务端
ws.send(input.value);
// 清空输入框,方便下一条消息
input.value = '';
}
</script>
</body>
</html>
8.3 pyproject.toml
[project]
name = "demo-app"
version = "0.1.0"
description = "FastAPI SSE + WebSocket 演示项目"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"fastapi",
"uvicorn",
"websockets",
]
第九章 运行与测试
9.1 启动服务
cd 项目根目录
.venv\Scripts\python src\main.py
终端输出:
INFO: Started server process [xxxxx]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000
9.2 测试 SSE
- 打开浏览器访问
http://127.0.0.1:8000 - 页面顶部 SSE 区域会每秒自动更新当前时间
- 打开浏览器开发者工具 → 网络标签,可以看到一个持续连接
9.3 测试 WebSocket
- 在页面 WebSocket 区域的输入框中输入文字
- 点击"发送"按钮
- 服务端会返回
收到: 你输入的文字 - 消息记录会累积显示在 div 中
第十章 知识总结
核心知识点
| 知识点 | 说明 |
|---|---|
uv init | 使用 uv 初始化 Python 项目 |
uv add | 安装依赖,不指定版本自动安装最新版 |
@app.get() | HTTP GET 路由装饰器 |
@app.websocket() | WebSocket 路由装饰器 |
StreamingResponse | 流式响应,用于 SSE |
HTMLResponse | HTML 内容响应 |
async/await | 异步编程语法 |
async for | 异步迭代器,用于 WebSocket 消息接收 |
WebSocketDisconnect | WebSocket 断开异常 |
uvicorn.Config + Server | 现代启动方式 |
EventSource | 浏览器端 SSE 客户端 API |
WebSocket | 浏览器端 WebSocket 客户端 API |
addEventListener | 现代 JavaScript 事件监听方式 |
学习路径建议
- 先掌握 HTTP 基础:理解 GET/POST 请求-响应模型
- 再学习异步编程:理解 async/await 和事件循环
- 然后学习 SSE:单向推送,概念简单,容易上手
- 最后学习 WebSocket:双向通信,概念稍复杂
- 进阶方向:WebSocket 广播、房间管理、身份认证、心跳检测
3841

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



