前置知识: Python

Python与WebSocket

5 minIntermediate2026/6/14

WebSocket实时通信

什么是 WebSocket

WebSocket 是一种在客户端和服务器之间建立持久双向通信的协议。传统的 HTTP 请求是单向的:客户端发请求,服务器返回响应,然后连接就断开了。如果服务器想主动给客户端推送数据,HTTP 做不到。

WebSocket 解决了这个问题。客户端和服务器通过一次 HTTP 握手建立连接后,双方可以随时互相发送数据,连接会一直保持,直到某一方主动关闭。这使得 WebSocket 特别适合实时聊天、在线协作、实时数据推送等场景。

基础概念

与 HTTP 的区别

HTTP 是请求-响应模式,每次通信都需要客户端先发起请求。WebSocket 在建立连接后,服务器可以主动向客户端推送数据,不需要客户端反复轮询。

连接生命周期

WebSocket 连接经历三个阶段:

  • 握手:客户端发送 HTTP 请求,携带 Upgrade: websocket 头,服务器同意后升级协议
  • 通信:双方通过连接自由发送文本或二进制消息
  • 关闭:任一方发送关闭帧,连接断开

消息

WebSocket 支持两种消息型:

  • 文本消息:UTF-8 编码的字符串,常用于 JSON 数据
  • 二进制消息:原始字节数据,常用于片、音频等

快速上手

安装依赖

# 安装 FastAPI 和 uvicorn
pip install fastapi uvicorn

# 安装 WebSocket 客户端库(用于测试)
pip install websockets

最简单的 WebSocket 服务端

# server.py - 最简单的 WebSocket 服务端
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    # 接受客户端连接
    await websocket.accept()
    try:
        while True:
            # 接收客户端发来的文本消息
            data = await websocket.receive_text()
            # 把消息原样发回(Echo 服务)
            await websocket.send_text(f"Echo: {data}")
    except Exception:
        # 客户端断开连接时退出循环
        pass

运行服务:

uvicorn server:app --reload

最简单的 WebSocket 客户端

# client.py - 最简单的 WebSocket 客户端
import asyncio
import websockets

async def main():
    # 连接到 WebSocket 服务端
    async with websockets.connect("ws://localhost:8000/ws") as ws:
        # 发送消息
        await ws.send("Hello, WebSocket!")
        # 接收回复
        response = await ws.recv()
        print(f"收到回复: {response}")

asyncio.run(main())

详细用法

处理连接和断开事件

在实际应用中,你需要知道客户端何时连接、何时断开,以便做相应的处理(如更新在线用户列表):

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

# 保存所有已连接的客户端
connected_clients = []

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    # 接受连接
    await websocket.accept()
    # 将新客户端加入列表
    connected_clients.append(websocket)
    print(f"客户端已连接,当前在线: {len(connected_clients)}")

    try:
        while True:
            data = await websocket.receive_text()
            # 处理收到的消息
            await websocket.send_text(f"你发送了: {data}")
    except WebSocketDisconnect:
        # 客户端断开连接
        connected_clients.remove(websocket)
        print(f"客户端已断开,当前在线: {len(connected_clients)}")

广播消息

广播是指将一条消息发送给所有已连接的客户端,这是聊天室等场景的核心功能:

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

# 在线客户端列表
clients = []

async def broadcast(message: str):
    """向所有客户端广播消息"""
    for client in clients:
        try:
            await client.send_text(message)
        except Exception:
            # 发送失败说明客户端已断开
            clients.remove(client)

@app.websocket("/ws/chat")
async def chat_endpoint(websocket: WebSocket):
    await websocket.accept()
    clients.append(websocket)

    try:
        while True:
            data = await websocket.receive_text()
            # 将消息广播给所有人
            await broadcast(data)
    except WebSocketDisconnect:
        clients.remove(websocket)
        await broadcast("有人离开了聊天室")

发送和接收 JSON 数据

大多数实际应用中,WebSocket 传输的是结构化的 JSON 数据:

import json
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()

    try:
        while True:
            # 接收文本消息
            raw_data = await websocket.receive_text()
            # 解析 JSON
            data = json.loads(raw_data)

            # 根据消息类型做不同处理
            msg_type = data.get("type")

            if msg_type == "greeting":
                response = {
                    "type": "greeting_reply",
                    "message": f"你好, {data.get('name', '匿名')}!"
                }
            elif msg_type == "ping":
                response = {"type": "pong", "timestamp": data.get("timestamp")}
            else:
                response = {"type": "error", "message": "未知的消息类型"}

            # 发送 JSON 响应
            await websocket.send_text(json.dumps(response))
    except Exception:
        pass

客户端发送 JSON:

import asyncio
import json
import websockets

async def main():
    async with websockets.connect("ws://localhost:8000/ws") as ws:
        # 发送 JSON 格式的消息
        message = {"type": "greeting", "name": "小明"}
        await ws.send(json.dumps(message))

        # 接收并解析 JSON 响应
        response = json.loads(await ws.recv())
        print(f"收到: {response}")

asyncio.run(main())

发送二进制数据

WebSocket 也支持发送二进制数据,适合传输片、文件等:

@app.websocket("/ws/binary")
async def binary_endpoint(websocket: WebSocket):
    await websocket.accept()

    try:
        while True:
            # 接收二进制数据
            data = await websocket.receive_bytes()

            # 处理二进制数据(例如图片缩略图)
            # 这里简单地把数据原样返回
            await websocket.send_bytes(data)
    except Exception:
        pass

使用 WebSocket 路径参数

你可以像普通路由一样在 WebSocket 路径中使用参数:

@app.websocket("/ws/room/{room_id}")
async def room_endpoint(websocket: WebSocket, room_id: str):
    await websocket.accept()

    try:
        while True:
            data = await websocket.receive_text()
            # 消息属于哪个房间
            await websocket.send_text(f"[房间 {room_id}] {data}")
    except WebSocketDisconnect:
        pass

使用查询参数

客户端连接时可以通过查询参数传递信息(如用户名、token):

@app.websocket("/ws")
async def websocket_endpoint(
    websocket: WebSocket,
    token: str = Query(...)  # 从查询参数获取 token
):
    # 先验证 token
    user = verify_token(token)
    if not user:
        await websocket.close(code=4001, reason="认证失败")
        return

    await websocket.accept()
    # 正常通信...

客户端连接时带上查询参数:

# 连接时在 URL 中带上 token
async with websockets.connect("ws://localhost:8000/ws?token=abc123") as ws:
    await ws.send("Hello")

常见场景

实时聊天应用

import json
from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

# 按房间分组的客户端字典
rooms: dict[str, list] = {}

@app.websocket("/ws/chat/{room_id}")
async def chat_room(websocket: WebSocket, room_id: str):
    await websocket.accept()

    # 初始化房间
    if room_id not in rooms:
        rooms[room_id] = []
    rooms[room_id].append(websocket)

    try:
        while True:
            data = await websocket.receive_text()
            msg = json.loads(data)

            # 构建广播消息
            broadcast_msg = json.dumps({
                "user": msg.get("user", "匿名"),
                "text": msg.get("text", ""),
                "room": room_id
            })

            # 向房间内所有人广播
            for client in rooms[room_id]:
                try:
                    await client.send_text(broadcast_msg)
                except Exception:
                    rooms[room_id].remove(client)
    except WebSocketDisconnect:
        rooms[room_id].remove(websocket)
        # 通知房间内其他人
        leave_msg = json.dumps({"system": True, "text": "有人离开了房间"})
        for client in rooms[room_id]:
            await client.send_text(leave_msg)

实时数据推送

服务器定时向客户端推送数据(如股票行情、系统监控):

import asyncio
import json
import random
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws/stock/{symbol}")
async def stock_price(websocket: WebSocket, symbol: str):
    await websocket.accept()

    try:
        while True:
            # 模拟实时股票价格
            price = round(random.uniform(100, 200), 2)
            change = round(random.uniform(-5, 5), 2)

            data = json.dumps({
                "symbol": symbol,
                "price": price,
                "change": change
            })

            await websocket.send_text(data)
            # 每秒推送一次
            await asyncio.sleep(1)
    except Exception:
        pass

进度通知

长时间运行的任务通过 WebSocket 实时报告进度:

import asyncio
from fastapi import FastAPI, WebSocket

app = FastAPI()

async def long_running_task(websocket: WebSocket, task_id: str):
    """模拟一个耗时任务,逐步报告进度"""
    total_steps = 10
    for step in range(1, total_steps + 1):
        # 执行一步任务
        await asyncio.sleep(1)

        # 报告进度
        progress = int(step / total_steps * 100)
        await websocket.send_json({
            "task_id": task_id,
            "progress": progress,
            "status": "running" if progress < 100 else "completed"
        })

@app.websocket("/ws/task/{task_id}")
async def task_progress(websocket: WebSocket, task_id: str):
    await websocket.accept()
    try:
        await long_running_task(websocket, task_id)
    except Exception:
        pass

注意事项与常见错误

必须调用 accept()

在 FastAPI 中,WebSocket 处理函数的第一步必须是调用 await websocket.accept(),否则客户端无法建立连接。

处理断开连接

客户端可能随时断开连接(网络中断、用户关闭页面等),你的代码必须能正确处理这种情况。使用 try-except 捕获 WebSocketDisconnect 异常,清理资源。

不要在 WebSocket 中执行阻塞操作

WebSocket 处理函数是异步的,不要在其中执行阻塞的同步操作(如 time.sleep、同步数据库查询),否则会阻塞整个事件循环。使用 asyncio.sleep 替代 time.sleep。

连接数限制

每个 WebSocket 连接都会占用服务器资源。如果你的应用需要支持大量并发连接,需要注意:

  • 使用负载均衡分散连接
  • 设置心跳机制及时清理断开的连接
  • 考虑使用专业的 WebSocket 服务(如 Redis Pub/Sub 做消息分发)

心跳保活

某些网络环境(如反向代理、防火墙)会自动关闭长时间空闲的连接。通过定期发送心跳消息来保持连接:

import asyncio
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()

    # 启动心跳任务
    async def heartbeat():
        while True:
            await asyncio.sleep(30)  # 每 30 秒发送一次心跳
            try:
                await websocket.send_json({"type": "ping"})
            except Exception:
                break

    heartbeat_task = asyncio.create_task(heartbeat())

    try:
        while True:
            data = await websocket.receive_text()
            # 处理消息...
    except Exception:
        pass
    finally:
        heartbeat_task.cancel()

进阶用法

使用连接管理器封装

对于复杂的应用,建议把连接管理逻辑封装到一个中:

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Dict, List

class ConnectionManager:
    """WebSocket 连接管理器"""

    def __init__(self):
        # 按组管理的连接字典
        self.active_connections: Dict[str, List[WebSocket]] = {}

    async def connect(self, websocket: WebSocket, group: str):
        """接受新连接并加入指定组"""
        await websocket.accept()
        if group not in self.active_connections:
            self.active_connections[group] = []
        self.active_connections[group].append(websocket)

    def disconnect(self, websocket: WebSocket, group: str):
        """断开连接并从组中移除"""
        if group in self.active_connections:
            self.active_connections[group].remove(websocket)
            if not self.active_connections[group]:
                del self.active_connections[group]

    async def broadcast(self, message: str, group: str):
        """向指定组的所有连接广播消息"""
        if group not in self.active_connections:
            return
        for connection in self.active_connections[group]:
            try:
                await connection.send_text(message)
            except Exception:
                self.disconnect(connection, group)

    async def send_personal(self, message: str, websocket: WebSocket):
        """向单个连接发送消息"""
        try:
            await websocket.send_text(message)
        except Exception:
            pass

# 使用连接管理器
manager = ConnectionManager()
app = FastAPI()

@app.websocket("/ws/{group}")
async def websocket_endpoint(websocket: WebSocket, group: str):
    await manager.connect(websocket, group)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(data, group)
    except WebSocketDisconnect:
        manager.disconnect(websocket, group)

配合 Redis 实现跨进程通信

当你的应用运行多个进程时,不同进程的 WebSocket 连接无法直接通信。通过 Redis 的发布/订阅功能可以实现跨进程消息传递:

import asyncio
import json
import redis.asyncio as redis
from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

# Redis 客户端
redis_client = redis.from_url("redis://localhost:6379")

# 本进程的连接管理
local_connections: list[WebSocket] = []

async def redis_subscriber():
    """订阅 Redis 频道,接收其他进程的消息"""
    pubsub = redis_client.pubsub()
    await pubsub.subscribe("chat_channel")
    async for message in pubsub.listen():
        if message["type"] == "message":
            data = message["data"].decode()
            # 向本进程的所有连接广播
            for ws in local_connections[:]:
                try:
                    await ws.send_text(data)
                except Exception:
                    local_connections.remove(ws)

# 应用启动时启动 Redis 订阅
@app.on_event("startup")
async def startup():
    asyncio.create_task(redis_subscriber())

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    local_connections.append(websocket)

    try:
        while True:
            data = await websocket.receive_text()
            # 发布到 Redis,让所有进程都能收到
            await redis_client.publish("chat_channel", data)
    except WebSocketDisconnect:
        local_connections.remove(websocket)