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