Files
aistream-test/python/ws_message_manager.py
2025-12-01 03:42:34 +08:00

143 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ws_message_manager.py
import asyncio
import logging
from enum import IntEnum
from typing import Dict, Any, Optional
from fastapi import WebSocket
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ws_message_manager")
# ==================== 1. 前端→服务端 枚举(接收消息类型) ====================
# 前端发给服务端的消息类型(如音频、文本、操作指令)
class ClientMsgType(IntEnum):
AUDIO = 0x01 # 前端发送的音频二进制
TEXT = 0x02 # 前端发送的文本消息(如手动输入)
ACTION = 0x03 # 前端发送的操作指令(如开始录音、停止播放)
PING = 0x04 # 前端心跳
# 扩展:前端新增消息类型(如配置修改)
# CONFIG = 0x05
# ==================== 2. 服务端→前端 枚举(发送消息类型) ====================
# 服务端推给前端的消息类型(如文字回复、语音、动画)
class ServerMsgType(IntEnum):
TEXT = 0x01 # 服务端推送的文字回复(和前端TEXT枚举值相同但语义不同)
VOICE = 0x02 # 服务端推送的语音二进制
ANIMATION = 0x03 # 服务端推送的数字人动画
ACTION = 0x04 # 服务端推送的前端操作指令
ERROR = 0x05 # 服务端推送的错误信息
PONG = 0x06 # 服务端心跳响应(前端PING对应)
# 扩展:服务端新增推送类型(如视频流)
# VIDEO = 0x07
# 标准化消息结构
class WsMessage:
def __init__(self, type: ServerMsgType, data: Any):
self.type = type
self.data = data
def to_dict(self) -> Dict[str, Any]:
"""转换为可序列化的字典(前端解析用)"""
return {
"type": self.type.value,
"data": self.data
}
# 全局连接-队列映射(每个连接对应一个队列)
class WsQueueManager:
def __init__(self):
# 结构:{websocket对象: (消息队列, 推送协程任务)}
self.queue_map: Dict[WebSocket, tuple[asyncio.Queue, asyncio.Task]] = {}
async def create_queue(self, websocket: WebSocket) -> asyncio.Queue:
"""为新连接创建专属队列,并启动推送协程"""
if websocket in self.queue_map:
logger.warning(f"连接已存在队列: {id(websocket)}")
return self.queue_map[websocket][0]
# 创建队列(最大100条,避免堆积)
msg_queue = asyncio.Queue(maxsize=100)
# 启动推送协程(单协程推送,避免并发send)
push_task = asyncio.create_task(self._push_worker(websocket, msg_queue))
self.queue_map[websocket] = (msg_queue, push_task)
logger.info(f"为连接 {id(websocket)} 创建队列,推送协程启动")
return msg_queue
async def _push_worker(self, websocket: WebSocket, msg_queue: asyncio.Queue):
"""推送协程:从队列读取消息并发送给前端"""
try:
while True:
# 阻塞获取队列中的消息
message: Optional[WsMessage] = await msg_queue.get()
if message is None: # 连接关闭标记
logger.info(f"连接 {id(websocket)} 推送协程收到关闭标记")
break
# 根据消息类型选择发送方式(文本/二进制)
try:
if message.type == ServerMsgType.VOICE:
# 语音是二进制数据,直接发送bytes
await websocket.send_bytes(message.data)
else:
# 其他类型序列化为JSON字符串
await websocket.send_json(message.to_dict())
except Exception as e:
logger.error(f"推送消息失败: {e},消息类型: {message.type}")
finally:
msg_queue.task_done() # 标记队列任务完成
except asyncio.CancelledError:
logger.info(f"连接 {id(websocket)} 推送协程被取消")
except Exception as e:
logger.error(f"推送协程异常: {e}")
async def send_message(self, websocket: WebSocket, msg_type: ServerMsgType, data: Any):
"""对外暴露的发送方法:封装消息类型并入队"""
if websocket not in self.queue_map:
raise ValueError(f"连接 {id(websocket)} 未创建队列")
msg_queue, _ = self.queue_map[websocket]
# 封装标准化消息
message = WsMessage(type=msg_type, data=data)
try:
# 非阻塞入队(避免处理协程被阻塞)
await asyncio.wait_for(msg_queue.put(message), timeout=1.0)
except asyncio.QueueFull:
logger.error(f"队列已满,丢弃消息(类型: {msg_type}")
except asyncio.TimeoutError:
logger.error(f"消息入队超时(类型: {msg_type}")
async def close_queue(self, websocket: WebSocket):
"""关闭连接对应的队列和推送协程"""
if websocket not in self.queue_map:
return
msg_queue, push_task = self.queue_map.pop(websocket)
# 发送关闭标记,终止推送协程
try:
await msg_queue.put(None)
except:
pass
# 取消推送协程并等待结束
push_task.cancel()
try:
await push_task
except asyncio.CancelledError:
pass
# 清空队列
while not msg_queue.empty():
try:
msg_queue.get_nowait()
except asyncio.QueueEmpty:
break
logger.info(f"连接 {id(websocket)} 队列已清理")
# 创建全局单例(所有连接共用一个管理器)
ws_queue_manager = WsQueueManager()