package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.net.JksOptions; import io.vertx.core.net.PemKeyCertOptions; import io.vertx.ext.web.Router; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.mqtt.MqttClient; import io.vertx.mqtt.MqttClientOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * IoT 网关 EMQX 协议实现: *

* 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用 * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker * * @author 芋道源码 */ @Slf4j public class IotEmqxProtocol implements IotProtocol { /** * 协议配置 */ private final ProtocolProperties properties; /** * EMQX 配置 */ private final IotEmqxConfig emqxConfig; /** * 服务器 ID */ @Getter private final String serverId; /** * 运行状态 */ @Getter private volatile boolean running = false; /** * Vert.x 实例 */ private Vertx vertx; /** * HTTP Hook 服务器 */ private HttpServer httpServer; /** * MQTT Client */ private volatile MqttClient mqttClient; /** * MQTT 重连定时器 ID */ private volatile Long reconnectTimerId; /** * 上行消息处理器 */ private final IotEmqxUpstreamHandler upstreamHandler; /** * 下行消息订阅者 */ private IotEmqxDownstreamSubscriber downstreamSubscriber; public IotEmqxProtocol(ProtocolProperties properties) { Assert.notNull(properties, "协议实例配置不能为空"); Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空"); this.properties = properties; this.emqxConfig = properties.getEmqx(); Assert.notNull(emqxConfig.getConnectTimeoutSeconds(), "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); } @Override public String getId() { return properties.getId(); } @Override public IotProtocolTypeEnum getType() { return IotProtocolTypeEnum.EMQX; } @Override public void start() { if (running) { log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId()); return; } // 1.1 创建 Vertx 实例 和 下行消息订阅者 this.vertx = Vertx.vertx(); try { // 1.2 启动 HTTP Hook 服务 startHttpServer(); // 1.3 启动 MQTT Client startMqttClient(); running = true; log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]", getId(), properties.getPort(), serverId); // 2. 启动下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); // 启动失败时,关闭资源 stop0(); throw e; } } @Override public void stop() { if (!running) { return; } stop0(); } private void stop0() { // 1. 停止下行消息订阅者 if (downstreamSubscriber != null) { try { downstreamSubscriber.stop(); log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); } catch (Exception e) { log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); } downstreamSubscriber = null; } // 2.1 先置为 false:避免 closeHandler 触发重连 running = false; stopMqttClientReconnectChecker(); // 2.2 停止 MQTT Client stopMqttClient(); // 2.3 停止 HTTP Hook 服务 stopHttpServer(); // 2.4 关闭 Vertx if (vertx != null) { try { vertx.close().toCompletionStage().toCompletableFuture() .get(10, TimeUnit.SECONDS); log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId()); } catch (Exception e) { log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e); } vertx = null; } log.info("[stop][IoT EMQX 协议 {} 已停止]", getId()); } // ======================================= HTTP Hook Server ======================================= /** * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event) */ private void startHttpServer() { // 1. 创建路由 Router router = Router.router(vertx); router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 // 2. 创建处理器 IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this); router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); // 3. 启动 HTTP Server(支持 HTTPS) IotEmqxConfig.Http httpConfig = emqxConfig.getHttp(); HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空"); Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空"); PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() .setKeyPath(httpConfig.getSslKeyPath()) .setCertPath(httpConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } try { httpServer = vertx.createHttpServer(options) .requestHandler(router) .listen() .toCompletionStage().toCompletableFuture() .get(10, TimeUnit.SECONDS); log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]", getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())); } catch (Exception e) { log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e); throw new RuntimeException("HTTP Hook 服务启动失败", e); } } private void stopHttpServer() { if (httpServer == null) { return; } try { httpServer.close().toCompletionStage().toCompletableFuture() .get(5, TimeUnit.SECONDS); log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId()); } catch (Exception e) { log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e); } finally { httpServer = null; } } // ======================================= MQTT Client ====================================== private void startMqttClient() { // 1.1 创建 MQTT Client MqttClient client = createMqttClient(); this.mqttClient = client; // 1.2 连接 MQTT Broker if (!connectMqttClient(client)) { throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败"); } // 2. 启动定时重连检查 startMqttClientReconnectChecker(); } private void stopMqttClient() { MqttClient client = this.mqttClient; this.mqttClient = null; // 先清理引用 if (client == null) { return; } // 1. 批量取消订阅(仅在连接时) if (client.isConnected()) { List topicList = emqxConfig.getMqttTopics(); if (CollUtil.isNotEmpty(topicList)) { try { client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() .get(5, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); } } } // 2. 断开 MQTT 连接 try { client.disconnect().toCompletionStage().toCompletableFuture() .get(5, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e); } } // ======================================= MQTT 基础方法 ====================================== /** * 创建 MQTT 客户端 * * @return 新创建的 MqttClient */ private MqttClient createMqttClient() { // 1.1 基础配置 MqttClientOptions options = new MqttClientOptions() .setClientId(emqxConfig.getMqttClientId()) .setUsername(emqxConfig.getMqttUsername()) .setPassword(emqxConfig.getMqttPassword()) .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl())) .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession())) .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds()) .setMaxInflightQueue(emqxConfig.getMaxInflightQueue()); options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒 options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll())); // 1.2 配置遗嘱消息 IotEmqxConfig.Will will = emqxConfig.getWill(); if (will != null && will.isEnabled()) { Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空"); Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空"); options.setWillFlag(true) .setWillTopic(will.getTopic()) .setWillMessageBytes(Buffer.buffer(will.getPayload())) .setWillQoS(will.getQos()) .setWillRetain(will.isRetain()); } // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空) IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) && Boolean.FALSE.equals(emqxConfig.getTrustAll()) && sslOptions != null) { if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { options.setTrustStoreOptions(new JksOptions() .setPath(sslOptions.getTrustStorePath()) .setPassword(sslOptions.getTrustStorePassword())); } if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { options.setKeyStoreOptions(new JksOptions() .setPath(sslOptions.getKeyStorePath()) .setPassword(sslOptions.getKeyStorePassword())); } } // 2. 创建客户端 return MqttClient.create(vertx, options); } /** * 连接 MQTT Broker(同步等待) * * @param client MQTT 客户端 * @return 连接成功返回 true,失败返回 false */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") private synchronized boolean connectMqttClient(MqttClient client) { String host = emqxConfig.getMqttHost(); int port = emqxConfig.getMqttPort(); int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds(); try { // 1. 连接 Broker client.connect(port, host).toCompletionStage().toCompletableFuture() .get(timeoutSeconds, TimeUnit.SECONDS); log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]", getId(), host, port); // 2. 设置处理器 setupMqttClientHandlers(client); subscribeMqttClientTopics(client); return true; } catch (Exception e) { log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e); return false; } } /** * 关闭 MQTT 客户端 */ private void closeMqttClient() { MqttClient oldClient = this.mqttClient; this.mqttClient = null; // 先清理引用 if (oldClient == null) { return; } // 尽力释放(无论是否连接都尝试 disconnect) try { oldClient.disconnect().toCompletionStage().toCompletableFuture() .get(5, TimeUnit.SECONDS); } catch (Exception ignored) { } } // ======================================= MQTT 重连机制 ====================================== /** * 启动 MQTT Client 周期性重连检查器 */ private void startMqttClientReconnectChecker() { long interval = emqxConfig.getReconnectDelayMs(); this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> { if (!running) { return; } if (mqttClient != null && mqttClient.isConnected()) { return; } log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId()); // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) vertx.executeBlocking(() -> { tryReconnectMqttClient(); return null; }); }); } /** * 停止 MQTT Client 重连检查器 */ private void stopMqttClientReconnectChecker() { if (reconnectTimerId != null && vertx != null) { try { vertx.cancelTimer(reconnectTimerId); } catch (Exception ignored) { } reconnectTimerId = null; } } /** * 尝试重连 MQTT Client */ private synchronized void tryReconnectMqttClient() { // 1. 前置检查 if (!running) { return; } if (mqttClient != null && mqttClient.isConnected()) { return; } log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId()); try { // 2. 关闭旧客户端 closeMqttClient(); // 3.1 创建新客户端 MqttClient client = createMqttClient(); this.mqttClient = client; // 3.2 连接(失败只打印日志,等下次定时) if (!connectMqttClient(client)) { log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId()); } } catch (Exception e) { log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e); } } // ======================================= MQTT Handler ====================================== /** * 设置 MQTT Client 事件处理器 */ private void setupMqttClientHandlers(MqttClient client) { // 1. 断开重连监听 client.closeHandler(closeEvent -> { if (!running) { return; } log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId()); // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) vertx.executeBlocking(() -> { tryReconnectMqttClient(); return null; }); }); // 2. 异常处理 client.exceptionHandler(exception -> log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception)); // 3. 上行消息处理 client.publishHandler(upstreamHandler::handle); } /** * 订阅 MQTT Client 主题(同步等待) */ private void subscribeMqttClientTopics(MqttClient client) { List topicList = emqxConfig.getMqttTopics(); if (!client.isConnected()) { log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId()); return; } if (CollUtil.isEmpty(topicList)) { log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId()); return; } // 执行订阅 Map topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic, topic -> emqxConfig.getMqttQos()); try { client.subscribe(topics).toCompletionStage().toCompletableFuture() .get(10, TimeUnit.SECONDS); log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size()); } catch (Exception e) { log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e); } } /** * 发布消息到 MQTT Broker * * @param topic 主题 * @param payload 消息内容 */ public void publishMessage(String topic, byte[] payload) { if (mqttClient == null || !mqttClient.isConnected()) { log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId()); return; } MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos()); mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false) .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); } /** * 延迟发布消息到 MQTT Broker * * @param topic 主题 * @param payload 消息内容 * @param delayMs 延迟时间(毫秒) */ public void publishDelayMessage(String topic, byte[] payload, long delayMs) { vertx.setTimer(delayMs, id -> publishMessage(topic, payload)); } }