package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager; import cn.hutool.core.util.StrUtil; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.buffer.Buffer; import io.vertx.mqtt.MqttEndpoint; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * IoT 网关 MQTT 连接管理器 *

* 统一管理 MQTT 连接的认证状态、设备会话和消息发送功能: * 1. 管理 MQTT 连接的认证状态 * 2. 管理设备会话和在线状态 * 3. 管理消息发送到设备 * * @author 芋道源码 */ @Slf4j @Component public class IotMqttConnectionManager { /** * 未知地址常量(当获取端点地址失败时使用) */ private static final String UNKNOWN_ADDRESS = "unknown"; /** * 连接信息映射:MqttEndpoint -> 连接信息 */ private final Map connectionMap = new ConcurrentHashMap<>(); /** * 设备 ID -> MqttEndpoint 的映射 */ private final Map deviceEndpointMap = new ConcurrentHashMap<>(); /** * 安全获取 endpoint 地址 *

* 优先从缓存获取地址,缓存为空时再尝试实时获取 * * @param endpoint MQTT 连接端点 * @return 地址字符串,获取失败时返回 "unknown" */ public String getEndpointAddress(MqttEndpoint endpoint) { String realTimeAddress = UNKNOWN_ADDRESS; if (endpoint == null) { return realTimeAddress; } // 1. 优先从缓存获取(避免连接关闭时的异常) ConnectionInfo connectionInfo = connectionMap.get(endpoint); if (connectionInfo != null && StrUtil.isNotBlank(connectionInfo.getRemoteAddress())) { return connectionInfo.getRemoteAddress(); } // 2. 缓存为空时尝试实时获取 try { realTimeAddress = endpoint.remoteAddress().toString(); } catch (Exception ignored) { // 连接已关闭,忽略异常 } return realTimeAddress; } /** * 注册设备连接(包含认证信息) * * @param endpoint MQTT 连接端点 * @param connectionInfo 连接信息 */ public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { Long deviceId = connectionInfo.getDeviceId(); // 如果设备已有其他连接,先清理旧连接 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); if (oldEndpoint != null && oldEndpoint != endpoint) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, getEndpointAddress(oldEndpoint)); // 先清理映射,再关闭连接(避免旧连接处理器干扰) connectionMap.remove(oldEndpoint); oldEndpoint.close(); } // 注册新连接 connectionMap.put(endpoint, connectionInfo); deviceEndpointMap.put(deviceId, endpoint); log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); } /** * 注销设备连接 * * @param endpoint MQTT 连接端点 */ public void unregisterConnection(MqttEndpoint endpoint) { ConnectionInfo connectionInfo = connectionMap.remove(endpoint); if (connectionInfo == null) { return; } Long deviceId = connectionInfo.getDeviceId(); deviceEndpointMap.remove(deviceId); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, getEndpointAddress(endpoint)); } /** * 获取连接信息 */ public ConnectionInfo getConnectionInfo(MqttEndpoint endpoint) { return connectionMap.get(endpoint); } /** * 根据设备 ID 获取连接信息 * * @param deviceId 设备 ID * @return 连接信息 */ public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { // 通过设备 ID 获取连接端点 MqttEndpoint endpoint = getDeviceEndpoint(deviceId); if (endpoint == null) { return null; } // 获取连接信息 return getConnectionInfo(endpoint); } /** * 发送消息到设备 * * @param deviceId 设备 ID * @param topic 主题 * @param payload 消息内容 * @param qos 服务质量 * @param retain 是否保留消息 * @return 是否发送成功 */ public boolean sendToDevice(Long deviceId, String topic, byte[] payload, int qos, boolean retain) { MqttEndpoint endpoint = deviceEndpointMap.get(deviceId); if (endpoint == null) { log.warn("[sendToDevice][设备离线,无法发送消息,设备 ID: {},主题: {}]", deviceId, topic); return false; } try { endpoint.publish(topic, Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain); log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos); return true; } catch (Exception e) { log.error("[sendToDevice][发送消息失败,设备 ID: {},主题: {},错误: {}]", deviceId, topic, e.getMessage()); return false; } } /** * 获取设备连接端点 */ public MqttEndpoint getDeviceEndpoint(Long deviceId) { return deviceEndpointMap.get(deviceId); } /** * 关闭所有连接 */ public void closeAll() { // 1. 先复制再清空,避免 closeHandler 回调时并发修改 List endpoints = new ArrayList<>(connectionMap.keySet()); connectionMap.clear(); deviceEndpointMap.clear(); // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) for (MqttEndpoint endpoint : endpoints) { try { endpoint.close(); } catch (Exception ignored) { // 连接可能已关闭,忽略异常 } } } /** * 连接信息 */ @Data public static class ConnectionInfo { /** * 设备 ID */ private Long deviceId; /** * 产品 Key */ private String productKey; /** * 设备名称 */ private String deviceName; /** * 连接地址 */ private String remoteAddress; } }