FastAPI: WebSocket — 实时双向通信

最后更新:2026-08-26

HTTP 像寄信——一去一回;WebSocket 像打电话——双方随时可以说话,不用挂断再拨。

1. 你将学到


2. Alice 的真实故事

(1) 痛点:价格变动只能轮询获取

Bob 前端每 5 秒轮询 PriceTracker 的价格接口检查是否有变动,但百万级商品中 99% 的价格在任意 5 秒内没变,轮询浪费了 99% 的请求。更糟的是,价格变动后最长需要等 5 秒才能显示,客户投诉"价格不够实时"。

(2) WebSocket 的解法

WebSocket 建立持久双向连接,服务端价格变动时主动推送给前端,无需轮询——0 浪费、0 延迟。

PYTHON
@app.websocket("/ws/prices")
async def price_websocket(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_json({"price_update": data})

(3) 收益

轮询请求从每秒 200 次降到 0,价格变动延迟从 5 秒降到 50ms,Bob 前端不再浪费 API 配额,客户满意度显著提升。


3. WebSocket 基础

(1) 生命周期状态

100%
stateDiagram-v2
    [*] --> CONNECTING: Client initiates
    CONNECTING --> CONNECTED: accept()
    CONNECTED --> RECEIVING: receive()
    RECEIVING --> CONNECTED: send()
    CONNECTED --> CLOSING: close() / disconnect
    CLOSING --> CLOSED: Connection closed
    CLOSED --> [*]

▶ 示例:最简 WebSocket 端点

PYTHON
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws/echo")
async def websocket_echo(websocket: WebSocket):
    await websocket.accept()  # Accept connection
    try:
        while True:
            data = await websocket.receive_text()
            await websocket.send_text(f"Echo: {data}")
    except Exception:
        await websocket.close()

输出:

TEXT 📖 仅展示
# 函数定义成功

▶ 示例:带路径参数的 WebSocket

PYTHON
@app.websocket("/ws/products/{product_id}/prices")
async def product_price_stream(
    websocket: WebSocket,
    product_id: int,
):
    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_json()
            # Echo back with product context
            await websocket.send_json({
                "product_id": product_id,
                "price": data.get("price"),
                "currency": data.get("currency", "USD"),
            })
    except Exception:
        await websocket.close()

输出:

TEXT 📖 仅展示
# 函数定义成功

4. 连接管理器模式

(1) ConnectionManager 设计

▶ 示例:ConnectionManager 实现

PYTHON
from fastapi import FastAPI, WebSocket
from typing import Dict, List
import json

class ConnectionManager:
    def __init__(self):
        # Map: product_id -> list of active connections
        self.active_connections: Dict[int, List[WebSocket]] = {}

    async def connect(self, websocket: WebSocket, product_id: int):
        await websocket.accept()
        if product_id not in self.active_connections:
            self.active_connections[product_id] = []
        self.active_connections[product_id].append(websocket)

    def disconnect(self, websocket: WebSocket, product_id: int):
        if product_id in self.active_connections:
            self.active_connections[product_id].remove(websocket)
            if not self.active_connections[product_id]:
                del self.active_connections[product_id]

    async def broadcast_to_product(self, product_id: int, message: dict):
        if product_id in self.active_connections:
            dead_connections = []
            for connection in self.active_connections[product_id]:
                try:
                    await connection.send_json(message)
                except Exception:
                    dead_connections.append(connection)
            # Clean up dead connections
            for conn in dead_connections:
                self.disconnect(conn, product_id)

    async def broadcast_all(self, message: dict):
        for product_id in list(self.active_connections.keys()):
            await self.broadcast_to_product(product_id, message)

manager = ConnectionManager()

输出:

TEXT 📖 仅展示
# 函数定义成功

(2) WebSocket 推送架构

100%
flowchart TD
    Update[Price Update via HTTP] --> Handler[API Handler]
    Handler --> Manager[ConnectionManager]
    Manager --> WS1[WebSocket Client 1]
    Manager --> WS2[WebSocket Client 2]
    Manager --> WSN[WebSocket Client N]
    
    subgraph Subscribers
        WS1
        WS2
        WSN
    end

▶ 示例:WebSocket 端点使用 ConnectionManager

PYTHON
app = FastAPI()

@app.websocket("/ws/products/{product_id}/prices")
async def price_websocket(websocket: WebSocket, product_id: int):
    await manager.connect(websocket, product_id)
    try:
        while True:
            # Keep connection alive, receive any client messages
            data = await websocket.receive_text()
    except Exception:
        manager.disconnect(websocket, product_id)

输出:

TEXT 📖 仅展示
# 函数定义成功

5. 认证 WebSocket

(1) 握手阶段验证 JWT

WebSocket 没有标准 Header 机制,Token 通过查询参数传递。

▶ 示例:WebSocket JWT 认证

PYTHON
from fastapi import WebSocket, Query, HTTPException
from jose import jwt, JWTError
from app.core.config import settings

async def verify_ws_token(token: str) -> dict:
    try:
        payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
        return payload
    except JWTError:
        raise ValueError("Invalid token")

@app.websocket("/ws/prices")
async def authenticated_price_ws(
    websocket: WebSocket,
    token: str = Query(..., description="JWT access token"),
):
    # Verify token before accepting connection
    try:
        user = await verify_ws_token(token)
    except ValueError:
        await websocket.close(code=4001, reason="Authentication failed")
        return

    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_text()
            await websocket.send_json({"user": user.get("sub"), "data": data})
    except Exception:
        pass

输出:

TEXT 📖 仅展示
# 函数定义成功
认证方式 实现 优点 缺点
查询参数 ?token=xxx 简单 Token 出现在 URL 日志
首条消息 连接后先发 Token 不暴露在 URL 多一次往返
Sec-WebSocket-Protocol 子协议传 Token 不暴露在 URL 非标准用法

6. HTTP 与 WebSocket 协作

(1) 价格变更触发推送

▶ 示例:HTTP 端点触发 WebSocket 广播

PYTHON
from pydantic import BaseModel, Field
from datetime import datetime

class PriceUpdate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0, description="New price in USD")
    currency: str = Field(default="USD")
    source: str = Field(max_length=100)

app = FastAPI()
manager = ConnectionManager()

@app.post("/api/v1/prices", response_model=PriceResponse)
async def create_price(
    price: PriceCreate,
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = PriceService(db)
    result = await service.create_price(price)
    
    # Trigger WebSocket broadcast after successful price creation
    await manager.broadcast_to_product(
        product_id=price.product_id,
        message={
            "event": "price_update",
            "product_id": price.product_id,
            "new_price": price.price,
            "currency": price.currency,
            "source": price.source,
            "timestamp": datetime.utcnow().isoformat(),
        },
    )
    return result

输出:

TEXT 📖 仅展示
# 函数定义成功

▶ 示例:全局价格流(订阅所有变动)

PYTHON
@app.websocket("/ws/prices/stream")
async def global_price_stream(websocket: WebSocket):
    await websocket.accept()
    # Add to a global subscriber list
    manager.global_connections.append(websocket)
    try:
        while True:
            # Receive heartbeat/ping from client
            data = await websocket.receive_text()
            if data == "ping":
                await websocket.send_text("pong")
    except Exception:
        manager.global_connections.remove(websocket)

输出:

TEXT 📖 仅展示
# 函数定义成功

❓ 常见问题

Q WebSocket 和 SSE(Server-Sent Events)怎么选?
A 需要双向通信用 WebSocket(如聊天、实时协作),只需服务端推送用 SSE(如日志流、通知)。SSE 更简单、自动重连。
Q WebSocket 连接数有上限吗?
A 单机受限于文件描述符(通常 65535)。生产环境用 Nginx 做负载均衡,多实例共享连接状态(Redis Pub/Sub)。
Q WebSocket 断线怎么处理?
A 客户端实现自动重连(指数退避)。服务端用 ping/pong 心跳检测死连接,ConnectionManager 自动清理。
Q WebSocket 能用 Pydantic 校验消息吗?
A 可以。接收消息后用 MessageModel.model_validate(data) 校验,校验失败发错误消息给客户端。
Q 多个 Worker 进程如何广播?
A 单进程内存的 ConnectionManager 不够。生产环境用 Redis Pub/Sub:价格变更发布到 Redis Channel,每个 Worker 订阅并推送给本地连接。
Q WebSocket 的 CORS 怎么处理?
A 浏览器的 WebSocket 也受同源策略限制。FastAPI 的 CORSMiddleware 自动处理 WebSocket 的跨域握手。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 WebSocket echo 端点,客户端发送文本,服务端原样返回。用浏览器开发者工具或 wscat 测试。提示:@app.websocket("/ws/echo") + accept() + receive_text()
  2. 进阶题(难度⭐⭐):实现 ConnectionManager,支持多个客户端订阅同一商品的价格变动,当新价格创建时广播给所有订阅该商品的 WebSocket 客户端。提示:Dict[int, List[WebSocket]] + broadcast_to_product()
  3. 挑战题(难度⭐⭐⭐):为 WebSocket 添加 JWT 认证(查询参数传 Token),并在 HTTP 价格创建端点中触发 WebSocket 广播,实现完整的"HTTP 写入 → WebSocket 推送"链路。提示:token: str = Query(...) + verify_ws_token() + HTTP 端点调用 manager.broadcast_to_product()

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏