websocket_server.py 14 KB

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