websocket_server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import asyncio
  2. import logging
  3. from typing import Optional
  4. import websockets
  5. from config.logger import setup_logging
  6. class SuppressInvalidHandshakeFilter(logging.Filter):
  7. """过滤掉无效握手错误日志(如HTTPS访问WS端口)"""
  8. def filter(self, record):
  9. msg = record.getMessage()
  10. suppress_keywords = [
  11. "opening handshake failed",
  12. "did not receive a valid HTTP request",
  13. "connection closed while reading HTTP request",
  14. "line without CRLF",
  15. ]
  16. return not any(keyword in msg for keyword in suppress_keywords)
  17. def _setup_websockets_logger():
  18. """配置 websockets 相关的所有 logger,过滤无效握手错误"""
  19. filter_instance = SuppressInvalidHandshakeFilter()
  20. for logger_name in ["websockets", "websockets.server", "websockets.client"]:
  21. logger = logging.getLogger(logger_name)
  22. logger.addFilter(filter_instance)
  23. _setup_websockets_logger()
  24. from core.connection import ConnectionHandler
  25. from config.config_loader import get_config_from_api_async
  26. from core.auth import AuthManager, AuthenticationError
  27. from core.utils.modules_initialize import initialize_modules
  28. from core.utils.util import check_vad_update, check_asr_update
  29. TAG = __name__
  30. class WebSocketServer:
  31. def __init__(self, config: dict):
  32. self.config = config
  33. self.logger = setup_logging()
  34. self.config_lock = asyncio.Lock()
  35. self.connections_lock = asyncio.Lock()
  36. self.active_connections = {}
  37. modules = initialize_modules(
  38. self.logger,
  39. self.config,
  40. "VAD" in self.config["selected_module"],
  41. "ASR" in self.config["selected_module"],
  42. "LLM" in self.config["selected_module"],
  43. False,
  44. "Memory" in self.config["selected_module"],
  45. "Intent" in self.config["selected_module"],
  46. )
  47. self._vad = modules["vad"] if "vad" in modules else None
  48. self._asr = modules["asr"] if "asr" in modules else None
  49. self._llm = modules["llm"] if "llm" in modules else None
  50. self._intent = modules["intent"] if "intent" in modules else None
  51. self._memory = modules["memory"] if "memory" in modules else None
  52. auth_config = self.config["server"].get("auth", {})
  53. self.auth_enable = auth_config.get("enabled", False)
  54. # 设备白名单
  55. self.allowed_devices = set(auth_config.get("allowed_devices", []))
  56. secret_key = self.config["server"]["auth_key"]
  57. expire_seconds = auth_config.get("expire_seconds", None)
  58. self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
  59. async def start(self):
  60. server_config = self.config["server"]
  61. host = server_config.get("ip", "0.0.0.0")
  62. port = int(server_config.get("port", 8000))
  63. async with websockets.serve(
  64. self._handle_connection, host, port, process_request=self._http_response
  65. ):
  66. await asyncio.Future()
  67. async def register_connection(self, conn) -> None:
  68. device_id = getattr(conn, "device_id", None)
  69. if not device_id:
  70. return
  71. async with self.connections_lock:
  72. self.active_connections[device_id] = conn
  73. self.logger.bind(tag=TAG).info(f"Registered active device connection: {device_id}")
  74. async def unregister_connection(self, conn) -> None:
  75. device_id = getattr(conn, "device_id", None)
  76. if not device_id:
  77. return
  78. removed = False
  79. async with self.connections_lock:
  80. current = self.active_connections.get(device_id)
  81. if current is conn:
  82. self.active_connections.pop(device_id, None)
  83. removed = True
  84. if removed:
  85. self.logger.bind(tag=TAG).info(f"Unregistered active device connection: {device_id}")
  86. async def get_connection(self, device_id: Optional[str] = None):
  87. async with self.connections_lock:
  88. if device_id:
  89. conn = self.active_connections.get(device_id)
  90. return conn if self._is_connection_alive(conn) else None
  91. items = list(self.active_connections.items())
  92. for _, conn in reversed(items):
  93. if self._is_connection_alive(conn):
  94. return conn
  95. return None
  96. async def list_active_device_ids(self) -> list[str]:
  97. async with self.connections_lock:
  98. return [
  99. device_id
  100. for device_id, conn in self.active_connections.items()
  101. if self._is_connection_alive(conn)
  102. ]
  103. @staticmethod
  104. def _is_connection_alive(conn) -> bool:
  105. if conn is None:
  106. return False
  107. stop_event = getattr(conn, "stop_event", None)
  108. if stop_event is not None and stop_event.is_set():
  109. return False
  110. websocket = getattr(conn, "websocket", None)
  111. if websocket is None:
  112. return False
  113. if hasattr(websocket, "closed"):
  114. return not websocket.closed
  115. if hasattr(websocket, "state"):
  116. return websocket.state.name != "CLOSED"
  117. return True
  118. async def _handle_connection(self, websocket: websockets.ServerConnection):
  119. headers = dict(websocket.request.headers)
  120. if headers.get("device-id", None) is None:
  121. # 尝试从 URL 的查询参数中获取 device-id
  122. from urllib.parse import parse_qs, urlparse
  123. # 从 WebSocket 请求中获取路径
  124. request_path = websocket.request.path
  125. if not request_path:
  126. self.logger.bind(tag=TAG).error("无法获取请求路径")
  127. await websocket.close()
  128. return
  129. parsed_url = urlparse(request_path)
  130. query_params = parse_qs(parsed_url.query)
  131. if "device-id" not in query_params:
  132. await websocket.send("端口正常,如需测试连接,请使用test_page.html")
  133. await websocket.close()
  134. return
  135. else:
  136. websocket.request.headers["device-id"] = query_params["device-id"][0]
  137. if "client-id" in query_params:
  138. websocket.request.headers["client-id"] = query_params["client-id"][0]
  139. if "authorization" in query_params:
  140. websocket.request.headers["authorization"] = query_params[
  141. "authorization"
  142. ][0]
  143. """处理新连接,每次创建独立的ConnectionHandler"""
  144. # 先认证,后建立连接
  145. try:
  146. await self._handle_auth(websocket)
  147. except AuthenticationError:
  148. await websocket.send("认证失败")
  149. await websocket.close()
  150. return
  151. # 创建ConnectionHandler时传入当前server实例
  152. handler = ConnectionHandler(
  153. self.config,
  154. self._vad,
  155. self._asr,
  156. self._llm,
  157. self._memory,
  158. self._intent,
  159. self, # 传入server实例
  160. )
  161. try:
  162. await handler.handle_connection(websocket)
  163. except Exception as e:
  164. self.logger.bind(tag=TAG).error(f"处理连接时出错: {e}")
  165. finally:
  166. # 强制关闭连接(如果还没有关闭的话)
  167. try:
  168. # 安全地检查WebSocket状态并关闭
  169. if hasattr(websocket, "closed") and not websocket.closed:
  170. await websocket.close()
  171. elif hasattr(websocket, "state") and websocket.state.name != "CLOSED":
  172. await websocket.close()
  173. else:
  174. # 如果没有closed属性,直接尝试关闭
  175. await websocket.close()
  176. except Exception as close_error:
  177. self.logger.bind(tag=TAG).error(
  178. f"服务器端强制关闭连接时出错: {close_error}"
  179. )
  180. async def _http_response(self, websocket, request_headers):
  181. # 检查是否为 WebSocket 升级请求
  182. connection_header = request_headers.headers.get("connection", "").lower()
  183. upgrade_header = request_headers.headers.get("upgrade", "").lower()
  184. if "upgrade" in connection_header and "websocket" in upgrade_header:
  185. # 如果是 WebSocket 请求,返回 None 允许握手继续
  186. return None
  187. else:
  188. # 如果是普通 HTTP 请求,返回 "server is running"
  189. return websocket.respond(200, "Server is running\n")
  190. async def update_config(self) -> bool:
  191. """更新服务器配置并重新初始化组件
  192. Returns:
  193. bool: 更新是否成功
  194. """
  195. try:
  196. async with self.config_lock:
  197. # 重新获取配置(使用异步版本)
  198. new_config = await get_config_from_api_async(self.config)
  199. if new_config is None:
  200. self.logger.bind(tag=TAG).error("获取新配置失败")
  201. return False
  202. self.logger.bind(tag=TAG).info(f"获取新配置成功")
  203. # 检查 VAD 和 ASR 类型是否需要更新
  204. update_vad = check_vad_update(self.config, new_config)
  205. update_asr = check_asr_update(self.config, new_config)
  206. self.logger.bind(tag=TAG).info(
  207. f"检查VAD和ASR类型是否需要更新: {update_vad} {update_asr}"
  208. )
  209. # 更新配置
  210. self.config = new_config
  211. # 重新初始化组件
  212. modules = initialize_modules(
  213. self.logger,
  214. new_config,
  215. update_vad,
  216. update_asr,
  217. "LLM" in new_config["selected_module"],
  218. False,
  219. "Memory" in new_config["selected_module"],
  220. "Intent" in new_config["selected_module"],
  221. )
  222. # 更新组件实例
  223. if "vad" in modules:
  224. self._vad = modules["vad"]
  225. if "asr" in modules:
  226. self._asr = modules["asr"]
  227. if "llm" in modules:
  228. self._llm = modules["llm"]
  229. if "intent" in modules:
  230. self._intent = modules["intent"]
  231. if "memory" in modules:
  232. self._memory = modules["memory"]
  233. self.logger.bind(tag=TAG).info(f"更新配置任务执行完毕")
  234. return True
  235. except Exception as e:
  236. self.logger.bind(tag=TAG).error(f"更新服务器配置失败: {str(e)}")
  237. return False
  238. async def _handle_auth(self, websocket: websockets.ServerConnection):
  239. # 先认证,后建立连接
  240. if self.auth_enable:
  241. headers = dict(websocket.request.headers)
  242. device_id = headers.get("device-id", None)
  243. client_id = headers.get("client-id", None)
  244. if self.allowed_devices and device_id in self.allowed_devices:
  245. # 如果属于白名单内的设备,不校验token,直接放行
  246. return
  247. else:
  248. # 否则校验token
  249. token = headers.get("authorization", "")
  250. if token.startswith("Bearer "):
  251. token = token[7:] # 移除'Bearer '前缀
  252. else:
  253. raise AuthenticationError("Missing or invalid Authorization header")
  254. # 进行认证
  255. auth_success = self.auth.verify_token(
  256. token, client_id=client_id, username=device_id
  257. )
  258. if not auth_success:
  259. raise AuthenticationError("Invalid token")