Python实现WebSocket音频处理完整方案
一、Python WebSocket框架对比
1.1 主流框架
| 框架 | 特点 | 性能 | 推荐指数 | 适用场景 |
|---|---|---|---|---|
| FastAPI + WebSocket | 现代化、类型提示、自动文档 | 高 | ⭐⭐⭐⭐⭐ | 推荐首选 |
| websockets | 纯异步、轻量级 | 极高 | ⭐⭐⭐⭐ | 简单WebSocket服务 |
| Flask-SocketIO | 易用、同步风格 | 中 | ⭐⭐⭐ | 小型项目 |
| Django Channels | Django集成、功能全 | 中 | ⭐⭐⭐ | 已有Django项目 |
| Tornado | 老牌异步框架 | 高 | ⭐⭐ | 遗留系统 |
二、方案1:FastAPI + WebSocket(推荐)
2.1 为什么选择FastAPI?
✅ 优势:
- 现代化的异步框架(基于ASGI)
- 自动API文档(Swagger UI)
- 类型提示和验证(Pydantic)
- 高性能(接近Go、Node.js)
- 易于集成机器学习模型
- 社区活跃,生态丰富
2.2 完整代码实现
安装依赖
bash
# 安装FastAPI和相关库
pip install fastapi uvicorn[standard] python-multipart
# 音频处理库
pip install numpy scipy librosa soundfile
# ASR相关
pip install aliyun-python-sdk-core
pip install websocket-client # 客户端测试用
# 可选:Redis(用于会话管理)
pip install redis aioredis
核心代码:main.py
python
"""
医院语音病历系统 - WebSocket音频处理服务
基于FastAPI实现
"""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import Dict, Optional
import asyncio
import json
import logging
import uuid
import time
import numpy as np
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 创建FastAPI应用
app = FastAPI(
title="医院语音病历系统 - ASR服务",
description="实时语音识别WebSocket API",
version="1.0.0"
)
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境应限制具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ==================== 连接管理器 ====================
class ConnectionManager:
"""
WebSocket连接管理器
管理所有活跃的WebSocket连接
"""
def __init__(self):
# 存储活跃连接:{session_id: WebSocket}
self.active_connections: Dict[str, WebSocket] = {}
# 会话信息:{session_id: session_info}
self.sessions: Dict[str, dict] = {}
async def connect(self, websocket: WebSocket, session_id: str):
"""接受新连接"""
await websocket.accept()
self.active_connections[session_id] = websocket
# 初始化会话信息
self.sessions[session_id] = {
'session_id': session_id,
'connected_at': datetime.now(),
'audio_buffer': bytearray(),
'total_bytes': 0,
'last_heartbeat': time.time(),
}
logger.info(f"新连接建立: {session_id}, 当前连接数: {len(self.active_connections)}")
def disconnect(self, session_id: str):
"""断开连接"""
if session_id in self.active_connections:
del self.active_connections[session_id]
if session_id in self.sessions:
session_info = self.sessions[session_id]
logger.info(
f"连接断开: {session_id}, "
f"总接收字节: {session_info['total_bytes']}, "
f"剩余连接数: {len(self.active_connections)}"
)
del self.sessions[session_id]
async def send_message(self, session_id: str, message: dict):
"""发送消息到指定客户端"""
if session_id in self.active_connections:
websocket = self.active_connections[session_id]
try:
await websocket.send_json(message)
except Exception as e:
logger.error(f"发送消息失败 [{session_id}]: {e}")
async def broadcast(self, message: dict):
"""广播消息到所有客户端"""
for session_id in list(self.active_connections.keys()):
await self.send_message(session_id, message)
def get_session(self, session_id: str) -> Optional[dict]:
"""获取会话信息"""
return self.sessions.get(session_id)
def update_heartbeat(self, session_id: str):
"""更新心跳时间"""
if session_id in self.sessions:
self.sessions[session_id]['last_heartbeat'] = time.time()
# 创建全局连接管理器
manager = ConnectionManager()
# ==================== ASR服务封装 ====================
class ASRService:
"""
语音识别服务封装
支持多种ASR提供商
"""
def __init__(self, provider='mock'):
"""
初始化ASR服务
Args:
provider: 'aliyun' | 'xunfei' | 'mock'
"""
self.provider = provider
self.sample_rate = 16000
self.channels = 1
if provider == 'aliyun':
self._init_aliyun()
elif provider == 'xunfei':
self._init_xunfei()
else:
logger.info("使用Mock ASR服务(仅用于测试)")
def _init_aliyun(self):
"""初始化阿里云ASR"""
# 这里实现阿里云ASR初始化
# from aliyunsdkcore.client import AcsClient
pass
def _init_xunfei(self):
"""初始化讯飞ASR"""
# 这里实现讯飞ASR初始化
pass
async def recognize(self, audio_data: bytes, session_id: str) -> dict:
"""
识别音频
Args:
audio_data: PCM音频数据
session_id: 会话ID
Returns:
识别结果字典
"""
if self.provider == 'mock':
# Mock实现(仅用于测试)
return await self._mock_recognize(audio_data, session_id)
elif self.provider == 'aliyun':
return await self._aliyun_recognize(audio_data, session_id)
elif self.provider == 'xunfei':
return await self._xunfei_recognize(audio_data, session_id)
async def _mock_recognize(self, audio_data: bytes, session_id: str) -> dict:
"""Mock识别(测试用)"""
# 模拟识别延迟
await asyncio.sleep(0.3)
# 计算音频时长
audio_array = np.frombuffer(audio_data, dtype=np.int16)
duration_seconds = len(audio_array) / self.sample_rate
# 返回模拟结果
mock_texts = [
"我头疼",
"有点发烧",
"大概三天了",
"最近血压有点高",
"吃了药没什么效果",
]
import random
return {
'text': random.choice(mock_texts),
'isFinal': duration_seconds > 2.0, # 超过2秒视为句子结束
'confidence': 0.85 + random.random() * 0.1,
'duration': duration_seconds,
}
async def _aliyun_recognize(self, audio_data: bytes, session_id: str) -> dict:
"""阿里云ASR识别"""
# TODO: 实现真实的阿里云ASR调用
# 参考前面提供的阿里云ASR代码
pass
async def _xunfei_recognize(self, audio_data: bytes, session_id: str) -> dict:
"""讯飞ASR识别"""
# TODO: 实现讯飞ASR调用
pass
# 创建ASR服务实例
asr_service = ASRService(provider='mock')
# ==================== 音频处理工具 ====================
class AudioProcessor:
"""
音频处理工具类
"""
@staticmethod
def validate_audio(audio_data: bytes) -> bool:
"""验证音频数据"""
if len(audio_data) < 100: # 至少100字节
return False
return True
@staticmethod
def calculate_rms(audio_data: bytes) -> float:
"""计算音频RMS(响度)"""
audio_array = np.frombuffer(audio_data, dtype=np.int16)
rms = np.sqrt(np.mean(audio_array.astype(np.float32) ** 2))
return float(rms)
@staticmethod
def is_silence(audio_data: bytes, threshold: float = 500.0) -> bool:
"""检测是否为静音"""
rms = AudioProcessor.calculate_rms(audio_data)
return rms < threshold
@staticmethod
def resample(audio_data: bytes, from_rate: int, to_rate: int) -> bytes:
"""重采样音频"""
if from_rate == to_rate:
return audio_data
# 使用scipy进行重采样
from scipy import signal
audio_array = np.frombuffer(audio_data, dtype=np.int16)
num_samples = int(len(audio_array) * to_rate / from_rate)
resampled = signal.resample(audio_array, num_samples)
return resampled.astype(np.int16).tobytes()
# ==================== WebSocket端点 ====================
@app.websocket("/ws/asr")
async def websocket_asr_endpoint(websocket: WebSocket):
"""
实时语音识别WebSocket端点
URL: ws://localhost:8000/ws/asr
通信协议:
1. 客户端发送二进制音频数据
2. 服务端返回JSON格式识别结果
3. 支持心跳检测(客户端发送 {"type": "ping"})
"""
# 生成会话ID
session_id = str(uuid.uuid4())
try:
# 接受连接
await manager.connect(websocket, session_id)
# 发送欢迎消息
await manager.send_message(session_id, {
'type': 'connected',
'session_id': session_id,
'timestamp': time.time(),
'message': '连接成功,开始语音识别'
})
# 主循环:接收和处理消息
while True:
try:
# 接收消息(支持文本和二进制)
message = await websocket.receive()
# 处理二进制音频数据
if 'bytes' in message:
await handle_audio_data(session_id, message['bytes'])
# 处理文本控制消息
elif 'text' in message:
await handle_control_message(session_id, message['text'])
except WebSocketDisconnect:
logger.info(f"客户端主动断开: {session_id}")
break
except Exception as e:
logger.error(f"处理消息异常 [{session_id}]: {e}")
await manager.send_message(session_id, {
'type': 'error',
'message': str(e)
})
except Exception as e:
logger.error(f"WebSocket连接异常 [{session_id}]: {e}")
finally:
# 清理连接
manager.disconnect(session_id)
async def handle_audio_data(session_id: str, audio_data: bytes):
"""
处理音频数据
Args:
session_id: 会话ID
audio_data: 音频数据(PCM格式,16kHz,16bit,单声道)
"""
session = manager.get_session(session_id)
if not session:
return
# 更新统计
session['total_bytes'] += len(audio_data)
# 验证音频
if not AudioProcessor.validate_audio(audio_data):
logger.warning(f"无效音频数据 [{session_id}]")
return
# 检测静音
if AudioProcessor.is_silence(audio_data):
logger.debug(f"检测到静音 [{session_id}]")
return
# 累积音频到缓冲区
session['audio_buffer'].extend(audio_data)
# 当累积到一定量时进行识别(例如:3200字节 = 100ms@16kHz)
min_buffer_size = 3200 * 3 # 300ms
if len(session['audio_buffer']) >= min_buffer_size:
# 提取音频进行识别
audio_to_recognize = bytes(session['audio_buffer'])
# 清空缓冲区(或保留overlap)
# 这里简单清空,实际可以保留最后一部分作为上下文
session['audio_buffer'].clear()
# 异步识别(不阻塞接收)
asyncio.create_task(
recognize_and_send(session_id, audio_to_recognize)
)
async def recognize_and_send(session_id: str, audio_data: bytes):
"""
识别音频并发送结果
Args:
session_id: 会话ID
audio_data: 音频数据
"""
try:
# 调用ASR服务识别
result = await asr_service.recognize(audio_data, session_id)
# 添加时间戳
result['timestamp'] = time.time()
result['type'] = 'transcript'
# 发送结果
await manager.send_message(session_id, result)
logger.info(
f"识别结果 [{session_id}]: {result['text']} "
f"(置信度: {result['confidence']:.2f})"
)
except Exception as e:
logger.error(f"识别异常 [{session_id}]: {e}")
await manager.send_message(session_id, {
'type': 'error',
'message': f'识别失败: {str(e)}'
})
async def handle_control_message(session_id: str, message_text: str):
"""
处理控制消息
Args:
session_id: 会话ID
message_text: 消息文本(JSON)
"""
try:
message = json.loads(message_text)
msg_type = message.get('type')
# 心跳检测
if msg_type == 'ping':
manager.update_heartbeat(session_id)
await manager.send_message(session_id, {
'type': 'pong',
'timestamp': time.time()
})
# 重置会话
elif msg_type == 'reset':
session = manager.get_session(session_id)
if session:
session['audio_buffer'].clear()
session['total_bytes'] = 0
await manager.send_message(session_id, {
'type': 'reset_ok',
'message': '会话已重置'
})
# 其他控制消息...
else:
logger.warning(f"未知消息类型 [{session_id}]: {msg_type}")
except json.JSONDecodeError:
logger.error(f"无效JSON [{session_id}]: {message_text}")
except Exception as e:
logger.error(f"处理控制消息异常 [{session_id}]: {e}")
# ==================== HTTP端点(用于监控和管理)====================
@app.get("/")
async def root():
"""根路径"""
return {
"service": "医院语音病历系统 - ASR服务",
"version": "1.0.0",
"status": "running",
"websocket_endpoint": "/ws/asr"
}
@app.get("/health")
async def health_check():
"""健康检查"""
return {
"status": "healthy",
"active_connections": len(manager.active_connections),
"timestamp": time.time()
}
@app.get("/stats")
async def get_stats():
"""获取统计信息"""
total_bytes = sum(
session.get('total_bytes', 0)
for session in manager.sessions.values()
)
return {
"active_connections": len(manager.active_connections),
"total_sessions": len(manager.sessions),
"total_bytes_received": total_bytes,
"sessions": [
{
"session_id": sid,
"connected_at": session['connected_at'].isoformat(),
"total_bytes": session['total_bytes'],
"buffer_size": len(session['audio_buffer']),
}
for sid, session in manager.sessions.items()
]
}
# ==================== 后台任务:心跳检测 ====================
@app.on_event("startup")
async def startup_event():
"""应用启动时执行"""
logger.info("ASR服务启动")
# 启动心跳检测任务
asyncio.create_task(heartbeat_monitor())
async def heartbeat_monitor():
"""
心跳监控任务
定期检查所有连接的心跳,超时则断开
"""
while True:
await asyncio.sleep(60) # 每60秒检查一次
current_time = time.time()
timeout = 120 # 120秒超时
# 检查所有会话
timeout_sessions = []
for session_id, session in manager.sessions.items():
last_heartbeat = session.get('last_heartbeat', 0)
if current_time - last_heartbeat > timeout:
timeout_sessions.append(session_id)
# 断开超时连接
for session_id in timeout_sessions:
logger.warning(f"会话心跳超时,断开连接: {session_id}")
if session_id in manager.active_connections:
websocket = manager.active_connections[session_id]
try:
await websocket.close(code=1000, reason="心跳超时")
except:
pass
manager.disconnect(session_id)
@app.on_event("shutdown")
async def shutdown_event():
"""应用关闭时执行"""
logger.info("ASR服务关闭")
# 关闭所有连接
for session_id in list(manager.active_connections.keys()):
websocket = manager.active_connections[session_id]
try:
await websocket.close(code=1000, reason="服务关闭")
except:
pass
# ==================== 启动服务 ====================
if __name__ == "__main__":
import uvicorn
# 启动服务
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True, # 开发模式自动重载
log_level="info",
# WebSocket配置
ws_ping_interval=30, # WebSocket ping间隔(秒)
ws_ping_timeout=10, # ping超时(秒)
)
2.3 客户端测试代码
Python测试客户端
python
"""
WebSocket客户端测试脚本
"""
import asyncio
import websockets
import json
import numpy as np
async def test_asr_websocket():
"""测试ASR WebSocket服务"""
uri = "ws://localhost:8000/ws/asr"
async with websockets.connect(uri) as websocket:
print("已连接到ASR服务")
# 接收欢迎消息
welcome = await websocket.recv()
print(f"欢迎消息: {welcome}")
# 发送心跳
await websocket.send(json.dumps({"type": "ping"}))
pong = await websocket.recv()
print(f"心跳响应: {pong}")
# 模拟发送音频数据
for i in range(10):
# 生成模拟音频(1秒,16kHz,16bit)
duration = 1.0
sample_rate = 16000
samples = int(duration * sample_rate)
# 生成正弦波(模拟语音)
frequency = 440 # A4音符
audio = np.sin(2 * np.pi * frequency * np.arange(samples) / sample_rate)
audio = (audio * 32767).astype(np.int16)
# 发送音频数据
await websocket.send(audio.tobytes())
print(f"已发送音频包 {i+1}/10")
# 接收识别结果
try:
result = await asyncio.wait_for(websocket.recv(), timeout=2.0)
result_data = json.loads(result)
print(f"识别结果: {result_data.get('text')} "
f"(置信度: {result_data.get('confidence', 0):.2f})")
except asyncio.TimeoutError:
print("等待结果超时")
await asyncio.sleep(0.5)
print("测试完成")
if __name__ == "__main__":
asyncio.run(test_asr_websocket())
JavaScript客户端(浏览器)
html
<!DOCTYPE html>
<html>
<head>
<title>ASR WebSocket测试</title>
</head>
<body>
<h1>语音识别测试</h1>
<button id="startBtn">开始录音</button>
<button id="stopBtn" disabled>停止录音</button>
<div id="status">未连接</div>
<div id="transcript"></div>
<script>
let ws = null;
let mediaStream = null;
let audioContext = null;
let scriptProcessor = null;
// 连接WebSocket
function connect() {
ws = new WebSocket('ws://localhost:8000/ws/asr');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
console.log('WebSocket已连接');
document.getElementById('status').textContent = '已连接';
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
if (data.type === 'transcript') {
const div = document.getElementById('transcript');
div.innerHTML += `<p>${data.text} (${(data.confidence * 100).toFixed(0)}%)</p>`;
}
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
document.getElementById('status').textContent = '连接错误';
};
ws.onclose = () => {
console.log('WebSocket已关闭');
document.getElementById('status').textContent = '已断开';
};
}
// 开始录音
async function startRecording() {
connect();
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
}
});
audioContext = new AudioContext({ sampleRate: 16000 });
const source = audioContext.createMediaStreamSource(mediaStream);
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
scriptProcessor.onaudioprocess = (e) => {
if (ws && ws.readyState === WebSocket.OPEN) {
const float32Data = e.inputBuffer.getChannelData(0);
const int16Data = new Int16Array(float32Data.length);
for (let i = 0; i < float32Data.length; i++) {
const s = Math.max(-1, Math.min(1, float32Data[i]));
int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
ws.send(int16Data.buffer);
}
};
source.connect(scriptProcessor);
scriptProcessor.connect(audioContext.destination);
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
document.getElementById('status').textContent = '录音中...';
}
// 停止录音
function stopRecording() {
if (scriptProcessor) {
scriptProcessor.disconnect();
scriptProcessor = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
if (ws) {
ws.close();
ws = null;
}
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
document.getElementById('status').textContent = '已停止';
}
document.getElementById('startBtn').onclick = startRecording;
document.getElementById('stopBtn').onclick = stopRecording;
</script>
</body>
</html>
2.4 部署配置
requirements.txt
txt
# Web框架
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
# WebSocket
websockets==12.0
# 异步支持
aiofiles==23.2.1
aioredis==2.0.1
# 音频处理
numpy==1.24.3
scipy==1.11.3
librosa==0.10.1
soundfile==0.12.1
# ASR SDK(按需选择)
aliyun-python-sdk-core==2.14.0
# 或者讯飞SDK
# xfyun-asr==1.0.0
# 工具库
python-dotenv==1.0.0
pydantic==2.5.0
Dockerfile
dockerfile
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
libsndfile1 \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制代码
COPY . .
# 暴露端口
EXPOSE 8000
# 启动服务
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml
yaml
version: '3.8'
services:
asr-service:
build: .
ports:
- "8000:8000"
environment:
- ENV=production
- LOG_LEVEL=info
volumes:
- ./logs:/app/logs
restart: unless-stopped
# 可选:Redis(用于会话管理)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
redis-data:
三、方案2:websockets库(轻量级)
3.1 纯异步实现
python
"""
使用websockets库的简单实现
适合纯WebSocket服务,不需要HTTP API
"""
import asyncio
import websockets
import json
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接管理
connections = set()
async def handle_client(websocket, path):
"""处理客户端连接"""
# 注册连接
connections.add(websocket)
logger.info(f"新连接: {websocket.remote_address}, 当前连接数: {len(connections)}")
try:
async for message in websocket:
# 处理二进制音频数据
if isinstance(message, bytes):
logger.info(f"收到音频数据: {len(message)} 字节")
# 模拟识别
await asyncio.sleep(0.3)
result = {
'type': 'transcript',
'text': '识别结果',
'confidence': 0.85
}
await websocket.send(json.dumps(result))
# 处理文本消息
else:
data = json.loads(message)
if data.get('type') == 'ping':
await websocket.send(json.dumps({'type': 'pong'}))
except websockets.exceptions.ConnectionClosed:
logger.info(f"连接关闭: {websocket.remote_address}")
finally:
# 移除连接
connections.remove(websocket)
logger.info(f"剩余连接数: {len(connections)}")
async def main():
"""启动服务器"""
async with websockets.serve(
handle_client,
"0.0.0.0",
8000,
ping_interval=30,
ping_timeout=10,
max_size=10**7 # 10MB
):
logger.info("WebSocket服务器启动: ws://0.0.0.0:8000")
await asyncio.Future() # 永久运行
if __name__ == "__main__":
asyncio.run(main())
四、性能优化
4.1 多进程部署
python
"""
使用Gunicorn + Uvicorn实现多进程
"""
# gunicorn_config.py
import multiprocessing
# 绑定地址
bind = "0.0.0.0:8000"
# Worker进程数(CPU核心数 × 2 + 1)
workers = multiprocessing.cpu_count() * 2 + 1
# Worker类(使用Uvicorn)
worker_class = "uvicorn.workers.UvicornWorker"
# 日志
accesslog = "/var/log/asr-service/access.log"
errorlog = "/var/log/asr-service/error.log"
loglevel = "info"
# 超时
timeout = 120
keepalive = 5
# 启动命令
# gunicorn -c gunicorn_config.py main:app
4.2 性能测试
python
"""
性能测试脚本
测试并发连接和吞吐量
"""
import asyncio
import websockets
import time
import numpy as np
async def test_single_connection():
"""测试单个连接"""
uri = "ws://localhost:8000/ws/asr"
async with websockets.connect(uri) as ws:
# 发送100个音频包
start_time = time.time()
for i in range(100):
# 生成100ms音频
audio = np.random.randint(-32768, 32767, 1600, dtype=np.int16)
await ws.send(audio.tobytes())
# 接收结果
result = await ws.recv()
elapsed = time.time() - start_time
print(f"单连接性能: {100/elapsed:.2f} 包/秒")
async def test_concurrent_connections(num_connections=100):
"""测试并发连接"""
tasks = [test_single_connection() for _ in range(num_connections)]
start_time = time.time()
await asyncio.gather(*tasks)
elapsed = time.time() - start_time
print(f"并发测试: {num_connections}个连接, 耗时{elapsed:.2f}秒")
if __name__ == "__main__":
# 单连接测试
asyncio.run(test_single_connection())
# 并发测试
asyncio.run(test_concurrent_connections(100))
五、生产环境部署
5.1 Nginx反向代理
nginx
# /etc/nginx/sites-available/asr-service
upstream asr_backend {
# 多个worker实例
server 127.0.0.1:8000 weight=1;
server 127.0.0.1:8001 weight=1;
server 127.0.0.1:8002 weight=1;
}
server {
listen 443 ssl http2;
server_name asr.hospital.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# WebSocket路径
location /ws/ {
proxy_pass http://asr_backend;
# WebSocket必需配置
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时配置
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
# 其他header
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# HTTP API路径
location / {
proxy_pass http://asr_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.2 Systemd服务
ini
# /etc/systemd/system/asr-service.service
[Unit]
Description=ASR WebSocket Service
After=network.target
[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/opt/asr-service
Environment="PATH=/opt/asr-service/venv/bin"
ExecStart=/opt/asr-service/venv/bin/gunicorn -c gunicorn_config.py main:app
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
六、总结
推荐方案
| 场景 | 推荐框架 | 理由 |
|---|---|---|
| 全功能Web应用 | FastAPI | ⭐⭐⭐⭐⭐ 现代化、高性能、易用 |
| 纯WebSocket服务 | websockets | ⭐⭐⭐⭐ 轻量、简单 |
| 已有Flask项目 | Flask-SocketIO | ⭐⭐⭐ 集成方便 |
| 已有Django项目 | Django Channels | ⭐⭐⭐ Django生态 |
性能指标
FastAPI + Uvicorn:
- 并发连接: 10,000+ (单进程)
- 延迟: <50ms (不含ASR)
- 吞吐量: 5000+ 消息/秒
适合医院场景:
- 100个医生同时录音 ✅
- 实时响应 ✅
- 易于维护 ✅
Python实现WebSocket完全可行,而且非常成熟! 🎉