VWED_server/services/online_script/async_mqtt_service.py

354 lines
14 KiB
Python
Raw Normal View History

2025-09-20 16:50:45 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
异步MQTT客户端服务模块
提供高性能的异步MQTT连接发布订阅等功能
支持自动重连和异步消息处理
"""
import asyncio
import json
import time
import sys
from typing import Dict, Any, Optional, Callable, List
from utils.logger import get_logger
# Windows兼容性修复
if sys.platform == 'win32':
# 在Windows上使用SelectorEventLoop以支持add_reader/add_writer
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# try:
# import aiomqtt
# except ImportError:
# try:
# # 尝试导入旧版本的asyncio-mqtt
#
# except ImportError:
# aiomqtt = None
# 如果都没有安装回退到paho-mqtt
import aiomqtt
from config.tf_api_config import MQTT_CONFIG
logger = get_logger("services.async_mqtt_service")
class AsyncMQTTService:
"""异步MQTT客户端服务"""
def __init__(self, config: Dict[str, Any] = None):
self.config = config or MQTT_CONFIG
self.client = None
self.connected = False
self.reconnect_attempts = 0
self.message_handlers: Dict[str, List[Callable]] = {}
self._running = False
self._connection_task = None
self._message_task = None
self._use_aiomqtt = aiomqtt is not None
if not self._use_aiomqtt:
logger.warning("aiomqtt未安装将使用paho-mqtt的异步包装")
async def connect(self):
"""连接MQTT服务器"""
try:
if self._use_aiomqtt:
await self._connect_aiomqtt()
else:
await self._connect_paho_async()
return True
except Exception as e:
logger.error(f"连接MQTT服务器失败: {e}")
return False
async def _connect_aiomqtt(self):
"""使用aiomqtt连接"""
client_config = {
"hostname": self.config['host'],
"port": self.config['port'],
# "keepalive": self.config.get('keepalive', 60),
}
if self.config.get('username'):
client_config['username'] = self.config['username']
client_config['password'] = self.config.get('password', '')
# try:
self.client = aiomqtt.Client(**client_config)
# except TypeError as e:
# # 处理版本兼容性问题,移除不支持的参数
# logger.warning(f"aiomqtt版本兼容性问题重试连接: {e}")
# # 创建基础配置,只包含核心参数
# basic_config = {
# "hostname": self.config['host'],
# "port": self.config['port']
# }
# if self.config.get('username'):
# basic_config['username'] = self.config['username']
# basic_config['password'] = self.config.get('password', '')
# self.client = aiomqtt.Client(**basic_config)
self._running = True
self._connection_task = asyncio.create_task(self._maintain_connection())
logger.info(f"正在连接MQTT服务器 {self.config['host']}:{self.config['port']}...")
# 等待连接建立
timeout = 10
start_time = time.time()
while not self.connected and (time.time() - start_time) < timeout:
await asyncio.sleep(0.1)
if self.connected:
logger.info("异步MQTT连接建立成功")
else:
logger.error("异步MQTT连接建立超时")
async def _maintain_connection(self):
"""维护MQTT连接仅用于aiomqtt"""
while self._running:
try:
async with self.client:
self.connected = True
self.reconnect_attempts = 0
logger.info(f"MQTT连接成功: {self.config['host']}:{self.config['port']}")
# 启动消息处理任务
self._message_task = asyncio.create_task(self._handle_messages())
# 重新订阅所有topics
await self._resubscribe_all_topics()
# 等待消息处理任务完成或连接断开
await self._message_task
except Exception as e:
self.connected = False
if self._message_task:
self._message_task.cancel()
if self._running:
self.reconnect_attempts += 1
if self.reconnect_attempts <= self.config.get('max_retries', 3):
delay = self.config.get('reconnect_delay', 5) * self.reconnect_attempts
logger.warning(f"MQTT连接断开: {e}, 将在 {delay} 秒后重连(第{self.reconnect_attempts}次尝试)")
await asyncio.sleep(delay)
else:
logger.error("MQTT重连次数已达上限停止重连")
break
else:
break
async def _handle_messages(self):
"""处理接收到的消息仅用于aiomqtt"""
try:
async for message in self.client.messages:
topic = str(message.topic)
payload = message.payload.decode('utf-8')
# 异步处理消息
await self._process_message(topic, payload)
except asyncio.CancelledError:
logger.debug("消息处理任务被取消")
except Exception as e:
logger.error(f"处理MQTT消息失败: {e}", exc_info=True)
async def _process_message(self, topic: str, payload: str):
"""异步处理单个消息"""
try:
handlers = self.message_handlers.get(topic, [])
for handler in handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(topic, payload)
else:
# 对于同步处理器,在线程池中执行
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, handler, topic, payload)
except Exception as e:
logger.error(f"MQTT消息处理器执行失败: {e}", exc_info=True)
except Exception as e:
logger.error(f"处理MQTT消息异常: {e}", exc_info=True)
async def _resubscribe_all_topics(self):
"""重新订阅所有topics"""
for topic in self.message_handlers.keys():
try:
await self.client.subscribe(topic)
logger.debug(f"重新订阅MQTT主题: {topic}")
except Exception as e:
logger.error(f"重新订阅主题失败 {topic}: {e}")
async def disconnect(self):
"""断开连接"""
self._running = False
self.connected = False
if self._message_task:
self._message_task.cancel()
try:
await self._message_task
except asyncio.CancelledError:
pass
if self._connection_task:
self._connection_task.cancel()
try:
await self._connection_task
except asyncio.CancelledError:
pass
logger.info("异步MQTT连接已断开")
async def subscribe(self, topic: str, handler: Callable = None):
"""订阅主题"""
try:
if not self.connected:
logger.warning(f"MQTT未连接无法订阅主题: {topic}")
return False
if self._use_aiomqtt and self.client:
await self.client.subscribe(topic)
logger.info(f"成功订阅MQTT主题: {topic}")
# 添加消息处理器
if handler:
if topic not in self.message_handlers:
self.message_handlers[topic] = []
if handler not in self.message_handlers[topic]:
self.message_handlers[topic].append(handler)
return True
else:
logger.error("MQTT客户端未初始化")
return False
except Exception as e:
logger.error(f"订阅MQTT主题异常: {e}")
return False
async def unsubscribe(self, topic: str):
"""取消订阅主题"""
try:
if not self.connected:
logger.debug(f"MQTT未连接跳过取消订阅主题: {topic}")
return False
if self._use_aiomqtt and self.client:
await self.client.unsubscribe(topic)
logger.info(f"取消订阅MQTT主题: {topic}")
# 清理消息处理器
if topic in self.message_handlers:
del self.message_handlers[topic]
return True
else:
logger.warning(f"MQTT客户端未初始化无法取消订阅主题: {topic}")
return False
except Exception as e:
logger.error(f"取消订阅MQTT主题异常: {e}")
return False
async def publish(self, topic: str, payload: Any, qos: int = 0, retain: bool = False):
"""发布消息"""
try:
if not self.connected:
logger.warning(f"MQTT未连接无法发布消息到: {topic}")
return False
# 处理不同类型的payload
payload_str = self._serialize_payload(payload)
if self._use_aiomqtt and self.client:
await self.client.publish(topic, payload_str, qos=qos, retain=retain)
logger.debug(f"MQTT消息发布成功: {topic}")
return True
else:
logger.error("MQTT客户端未初始化")
return False
except Exception as e:
logger.error(f"发布MQTT消息异常: {e}")
return False
def _serialize_payload(self, payload: Any) -> str:
"""序列化消息载荷"""
try:
if isinstance(payload, (str, int, float, bool)):
return str(payload)
elif isinstance(payload, dict):
return json.dumps(payload, ensure_ascii=False, default=self._json_default)
elif isinstance(payload, list):
return json.dumps(payload, ensure_ascii=False, default=self._json_default)
elif hasattr(payload, 'to_dict') and callable(payload.to_dict):
# 如果对象有to_dict方法优先使用它
return json.dumps(payload.to_dict(), ensure_ascii=False, default=self._json_default)
elif hasattr(payload, 'dict'):
# 如果是Pydantic模型等尝试使用dict方法
if callable(payload.dict):
return json.dumps(payload.dict(), ensure_ascii=False, default=self._json_default)
else:
return json.dumps(payload.dict, ensure_ascii=False, default=self._json_default)
elif hasattr(payload, '__dict__'):
# 对于自定义对象,尝试序列化其属性
return json.dumps(payload.__dict__, ensure_ascii=False, default=self._json_default)
else:
# 最后尝试直接转换为字符串
return str(payload)
except Exception as e:
logger.warning(f"序列化载荷失败,使用字符串表示: {e}")
return str(payload)
def _json_default(self, obj):
"""JSON序列化的默认处理器处理特殊类型"""
from enum import Enum
if isinstance(obj, Enum):
return obj.value
elif hasattr(obj, 'to_dict') and callable(obj.to_dict):
return obj.to_dict()
elif hasattr(obj, '__dict__'):
return obj.__dict__
else:
return str(obj)
def add_message_handler(self, topic: str, handler: Callable):
"""为指定主题添加消息处理器"""
if topic not in self.message_handlers:
self.message_handlers[topic] = []
if handler not in self.message_handlers[topic]:
self.message_handlers[topic].append(handler)
def remove_message_handler(self, topic: str, handler: Callable):
"""移除指定主题的消息处理器"""
if topic in self.message_handlers:
if handler in self.message_handlers[topic]:
self.message_handlers[topic].remove(handler)
if not self.message_handlers[topic]:
del self.message_handlers[topic]
def is_connected(self) -> bool:
"""检查连接状态"""
return self.connected
def get_connection_info(self) -> Dict[str, Any]:
"""获取连接信息"""
return {
"host": self.config['host'],
"port": self.config['port'],
"connected": self.connected,
"reconnect_attempts": self.reconnect_attempts,
"running": self._running,
"subscribed_topics": list(self.message_handlers.keys()),
"async_mode": self._use_aiomqtt
}
def create_async_mqtt_service(config: Dict[str, Any] = None) -> AsyncMQTTService:
"""创建异步MQTT服务实例"""
return AsyncMQTTService(config)