FastAPI SSE WebSocket 实战教程

FastAPI SSE WebSocket 实战教程

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 协议,使用简单
  • 服务端可以主动向客户端推送数据
  • 客户端使用 EventSource API 接收
  • 适合单向推送场景:通知、行情、日志等

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:SS
  • yield:每次产生一个 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 的核心区别:

特性SSEWebSocket
方向仅服务端→客户端双向通信
协议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() 是一个便捷函数,内部封装了 ConfigServer 的创建和启动逻辑。

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())

优势:

  • 更灵活:可以访问 ConfigServer 对象进行精细控制
  • 优雅关闭: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

  1. 打开浏览器访问 http://127.0.0.1:8000
  2. 页面顶部 SSE 区域会每秒自动更新当前时间
  3. 打开浏览器开发者工具 → 网络标签,可以看到一个持续连接

9.3 测试 WebSocket

  1. 在页面 WebSocket 区域的输入框中输入文字
  2. 点击"发送"按钮
  3. 服务端会返回 收到: 你输入的文字
  4. 消息记录会累积显示在 div 中

第十章 知识总结

核心知识点

知识点说明
uv init使用 uv 初始化 Python 项目
uv add安装依赖,不指定版本自动安装最新版
@app.get()HTTP GET 路由装饰器
@app.websocket()WebSocket 路由装饰器
StreamingResponse流式响应,用于 SSE
HTMLResponseHTML 内容响应
async/await异步编程语法
async for异步迭代器,用于 WebSocket 消息接收
WebSocketDisconnectWebSocket 断开异常
uvicorn.Config + Server现代启动方式
EventSource浏览器端 SSE 客户端 API
WebSocket浏览器端 WebSocket 客户端 API
addEventListener现代 JavaScript 事件监听方式

学习路径建议

  1. 先掌握 HTTP 基础:理解 GET/POST 请求-响应模型
  2. 再学习异步编程:理解 async/await 和事件循环
  3. 然后学习 SSE:单向推送,概念简单,容易上手
  4. 最后学习 WebSocket:双向通信,概念稍复杂
  5. 进阶方向:WebSocket 广播、房间管理、身份认证、心跳检测
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李昊哲小课

桃李不言下自成蹊

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

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

打赏作者

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

抵扣说明:

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

余额充值