websocket_server.py 15 KB

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