Commit 9805cf2463c8504b809673b48d0b564fe75d9f90
1 parent
2cf5bf53
feat:【IoT 物联网】优化 MQTT 客户端连接逻辑
Showing
2 changed files
with
141 additions
and
121 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java
| ... | ... | @@ -10,6 +10,7 @@ import io.vertx.mqtt.MqttClient; |
| 10 | 10 | import io.vertx.mqtt.MqttClientOptions; |
| 11 | 11 | import jakarta.annotation.PostConstruct; |
| 12 | 12 | import jakarta.annotation.PreDestroy; |
| 13 | +import jodd.util.ThreadUtil; | |
| 13 | 14 | import lombok.Getter; |
| 14 | 15 | import lombok.extern.slf4j.Slf4j; |
| 15 | 16 | |
| ... | ... | @@ -65,19 +66,15 @@ public class IotEmqxUpstreamProtocol { |
| 65 | 66 | log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); |
| 66 | 67 | stop(); |
| 67 | 68 | |
| 68 | - // 异步关闭应用,避免阻塞当前线程 | |
| 69 | - // TODO @haohao:是不是阻塞,也没关系哈? | |
| 70 | - new Thread(() -> { | |
| 71 | - try { | |
| 72 | - // TODO @haohao:可以考虑用 ThreadUtil.sleep 更简洁 | |
| 73 | - Thread.sleep(1000); // 等待 1 秒让日志输出完成 | |
| 74 | - log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); | |
| 75 | - } catch (InterruptedException ex) { | |
| 76 | - Thread.currentThread().interrupt(); | |
| 77 | - } finally { | |
| 78 | - System.exit(1); // 直接关闭 JVM | |
| 79 | - } | |
| 80 | - }).start(); | |
| 69 | + // 异步关闭应用 | |
| 70 | + Thread shutdownThread = new Thread(() -> { | |
| 71 | + ThreadUtil.sleep(1000); | |
| 72 | + log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); | |
| 73 | + System.exit(1); | |
| 74 | + }); | |
| 75 | + shutdownThread.setDaemon(true); | |
| 76 | + shutdownThread.setName("emergency-shutdown"); | |
| 77 | + shutdownThread.start(); | |
| 81 | 78 | |
| 82 | 79 | throw e; |
| 83 | 80 | } |
| ... | ... | @@ -114,114 +111,149 @@ public class IotEmqxUpstreamProtocol { |
| 114 | 111 | // 1. 初始化消息处理器 |
| 115 | 112 | this.upstreamHandler = new IotEmqxUpstreamHandler(this); |
| 116 | 113 | |
| 117 | - // 2. 创建 MQTT 客户端,连接 MQTT Broker | |
| 118 | - boolean connected = connectMqttSync(); | |
| 119 | - if (!connected) { | |
| 120 | - throw new RuntimeException("首次连接 MQTT Broker 失败"); | |
| 121 | - } | |
| 114 | + // 2. 创建 MQTT 客户端 | |
| 115 | + createMqttClient(); | |
| 116 | + | |
| 117 | + // 3. 同步连接 MQTT Broker | |
| 118 | + connectMqttSync(); | |
| 122 | 119 | } catch (Exception e) { |
| 123 | 120 | log.error("[startMqttClient][MQTT 客户端启动失败]", e); |
| 124 | - throw new RuntimeException("MQTT 客户端启动失败", e); | |
| 121 | + throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); | |
| 125 | 122 | } |
| 126 | 123 | } |
| 127 | 124 | |
| 128 | 125 | /** |
| 129 | - * 连接 MQTT Broker | |
| 130 | - * | |
| 131 | - * @param isReconnect 是否为重连 | |
| 132 | - * @param isSync 是否同步等待连接结果 | |
| 133 | - * @return 当 isSync 为 true 时返回连接是否成功,否则返回 null | |
| 126 | + * 同步连接 MQTT Broker | |
| 134 | 127 | */ |
| 135 | - // TODO @haohao:是不是不用结果;结束后,直接判断 this.mqttClient.isConnected(); | |
| 136 | - private Boolean connectMqtt(boolean isReconnect, boolean isSync) { | |
| 137 | - // TODO @haohao:这块代码,是不是放到 | |
| 128 | + private void connectMqttSync() { | |
| 138 | 129 | String host = emqxProperties.getMqttHost(); |
| 139 | - Integer port = emqxProperties.getMqttPort(); | |
| 130 | + int port = emqxProperties.getMqttPort(); | |
| 140 | 131 | |
| 141 | - // 2.3.1. 如果是重连,则需要重新创建 MQTT 客户端 | |
| 142 | - // TODO @hoahao:疑惑,为啥这里要重新创建对象呀?另外,创建是不是拿到 reconnectWithDelay 会更合适?这样和 startMqttClient 一样呢; | |
| 143 | - if (isReconnect) { | |
| 144 | - createMqttClient(); | |
| 145 | - } | |
| 132 | + // 1. 创建同步等待对象 | |
| 133 | + CountDownLatch latch = new CountDownLatch(1); | |
| 134 | + AtomicBoolean success = new AtomicBoolean(false); | |
| 146 | 135 | |
| 147 | - // 2.3.2. 连接 MQTT Broker | |
| 148 | - CountDownLatch latch = isSync ? new CountDownLatch(1) : null; | |
| 149 | - AtomicBoolean success = isSync | |
| 150 | - ? new AtomicBoolean(false) | |
| 151 | - : null; | |
| 136 | + // 2. 连接 MQTT Broker | |
| 152 | 137 | mqttClient.connect(port, host, connectResult -> { |
| 153 | 138 | if (connectResult.succeeded()) { |
| 154 | - if (isReconnect) { | |
| 155 | - log.info("[connectMqtt][MQTT 客户端重连成功]"); | |
| 156 | - } else { | |
| 157 | - log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port); | |
| 158 | - } | |
| 159 | - // 设置处理器和订阅主题 | |
| 139 | + log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); | |
| 160 | 140 | setupMqttHandlers(); |
| 161 | 141 | subscribeToTopics(); |
| 162 | - if (success != null) { | |
| 163 | - success.set(true); | |
| 164 | - } | |
| 142 | + success.set(true); | |
| 165 | 143 | } else { |
| 166 | - log.error("[connectMqtt][连接 MQTT Broker 失败, host: {}, port: {}]", host, port, connectResult.cause()); | |
| 167 | - if (isReconnect) { | |
| 168 | - reconnectWithDelay(); | |
| 169 | - } else { | |
| 170 | - log.error("[connectMqtt][首次连接失败,连接终止]"); | |
| 171 | - } | |
| 172 | - } | |
| 173 | - | |
| 174 | - if (latch != null) { | |
| 175 | - latch.countDown(); | |
| 144 | + log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", | |
| 145 | + host, port, connectResult.cause()); | |
| 176 | 146 | } |
| 147 | + latch.countDown(); | |
| 177 | 148 | }); |
| 178 | 149 | |
| 179 | - // 2.3.3. 如果需要同步等待连接结果,则等待 | |
| 180 | - if (isSync) { | |
| 181 | - try { | |
| 182 | - latch.await(10, java.util.concurrent.TimeUnit.SECONDS); | |
| 183 | - } catch (InterruptedException e) { | |
| 184 | - Thread.currentThread().interrupt(); | |
| 185 | - log.error("[connectMqtt][等待连接结果被中断]", e); | |
| 150 | + // 3. 等待连接结果 | |
| 151 | + try { | |
| 152 | + boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); | |
| 153 | + if (!awaitResult) { | |
| 154 | + log.error("[connectMqttSync][等待连接结果超时]"); | |
| 155 | + throw new RuntimeException("连接 MQTT Broker 超时"); | |
| 156 | + } | |
| 157 | + if (!success.get()) { | |
| 158 | + throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", | |
| 159 | + host, port)); | |
| 186 | 160 | } |
| 187 | - return success.get(); | |
| 161 | + } catch (InterruptedException e) { | |
| 162 | + Thread.currentThread().interrupt(); | |
| 163 | + log.error("[connectMqttSync][等待连接结果被中断]", e); | |
| 164 | + throw new RuntimeException("连接 MQTT Broker 被中断", e); | |
| 188 | 165 | } |
| 166 | + } | |
| 167 | + | |
| 168 | + /** | |
| 169 | + * 异步连接 MQTT Broker | |
| 170 | + */ | |
| 171 | + private void connectMqttAsync() { | |
| 172 | + String host = emqxProperties.getMqttHost(); | |
| 173 | + int port = emqxProperties.getMqttPort(); | |
| 189 | 174 | |
| 190 | - return null; | |
| 175 | + mqttClient.connect(port, host, connectResult -> { | |
| 176 | + if (connectResult.succeeded()) { | |
| 177 | + log.info("[connectMqttAsync][MQTT 客户端重连成功]"); | |
| 178 | + setupMqttHandlers(); | |
| 179 | + subscribeToTopics(); | |
| 180 | + } else { | |
| 181 | + log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", | |
| 182 | + host, port, connectResult.cause()); | |
| 183 | + log.warn("[connectMqttAsync][重连失败,将再次尝试]"); | |
| 184 | + reconnectWithDelay(); | |
| 185 | + } | |
| 186 | + }); | |
| 191 | 187 | } |
| 192 | 188 | |
| 193 | 189 | /** |
| 194 | - * 同步连接 MQTT Broker | |
| 195 | - * | |
| 196 | - * @return 是否连接成功 | |
| 190 | + * 延迟重连 | |
| 197 | 191 | */ |
| 198 | - private boolean connectMqttSync() { | |
| 199 | - Boolean result = connectMqtt(false, true); | |
| 200 | - return result != null ? result : false; | |
| 192 | + private void reconnectWithDelay() { | |
| 193 | + if (!isRunning) { | |
| 194 | + return; | |
| 195 | + } | |
| 196 | + if (mqttClient != null && mqttClient.isConnected()) { | |
| 197 | + return; | |
| 198 | + } | |
| 199 | + | |
| 200 | + long delay = emqxProperties.getReconnectDelayMs(); | |
| 201 | + log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); | |
| 202 | + | |
| 203 | + vertx.setTimer(delay, timerId -> { | |
| 204 | + if (!isRunning) { | |
| 205 | + return; | |
| 206 | + } | |
| 207 | + if (mqttClient != null && mqttClient.isConnected()) { | |
| 208 | + return; | |
| 209 | + } | |
| 210 | + | |
| 211 | + log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); | |
| 212 | + try { | |
| 213 | + createMqttClient(); | |
| 214 | + connectMqttAsync(); | |
| 215 | + } catch (Exception e) { | |
| 216 | + log.error("[reconnectWithDelay][重连过程中发生异常]", e); | |
| 217 | + vertx.setTimer(delay, t -> reconnectWithDelay()); | |
| 218 | + } | |
| 219 | + }); | |
| 201 | 220 | } |
| 202 | 221 | |
| 203 | 222 | /** |
| 204 | 223 | * 停止 MQTT 客户端 |
| 205 | 224 | */ |
| 206 | 225 | private void stopMqttClient() { |
| 207 | - // 1. 取消订阅所有主题 | |
| 208 | - if (mqttClient != null && mqttClient.isConnected()) { | |
| 209 | - List<String> topicList = emqxProperties.getMqttTopics(); | |
| 210 | - for (String topic : topicList) { | |
| 226 | + if (mqttClient == null) { | |
| 227 | + return; | |
| 228 | + } | |
| 229 | + | |
| 230 | + try { | |
| 231 | + if (mqttClient.isConnected()) { | |
| 232 | + // 1. 取消订阅所有主题 | |
| 233 | + List<String> topicList = emqxProperties.getMqttTopics(); | |
| 234 | + for (String topic : topicList) { | |
| 235 | + try { | |
| 236 | + mqttClient.unsubscribe(topic); | |
| 237 | + } catch (Exception e) { | |
| 238 | + log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | |
| 239 | + } | |
| 240 | + } | |
| 241 | + | |
| 242 | + // 2. 断开 MQTT 客户端连接 | |
| 211 | 243 | try { |
| 212 | - mqttClient.unsubscribe(topic); | |
| 244 | + CountDownLatch disconnectLatch = new CountDownLatch(1); | |
| 245 | + mqttClient.disconnect(ar -> disconnectLatch.countDown()); | |
| 246 | + if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { | |
| 247 | + log.warn("[stopMqttClient][断开 MQTT 连接超时]"); | |
| 248 | + } | |
| 213 | 249 | } catch (Exception e) { |
| 214 | - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | |
| 250 | + log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); | |
| 215 | 251 | } |
| 216 | 252 | } |
| 217 | - } | |
| 218 | - // 2. 断开 MQTT 客户端连接 | |
| 219 | - if (mqttClient != null && mqttClient.isConnected()) { | |
| 220 | - try { | |
| 221 | - mqttClient.disconnect(); | |
| 222 | - } catch (Exception e) { | |
| 223 | - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); | |
| 224 | - } | |
| 253 | + } catch (Exception e) { | |
| 254 | + log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); | |
| 255 | + } finally { | |
| 256 | + mqttClient = null; | |
| 225 | 257 | } |
| 226 | 258 | } |
| 227 | 259 | |
| ... | ... | @@ -241,7 +273,7 @@ public class IotEmqxUpstreamProtocol { |
| 241 | 273 | * 设置 MQTT 处理器 |
| 242 | 274 | */ |
| 243 | 275 | private void setupMqttHandlers() { |
| 244 | - // 1.1 设置断开重连监听器 | |
| 276 | + // 1. 设置断开重连监听器 | |
| 245 | 277 | mqttClient.closeHandler(closeEvent -> { |
| 246 | 278 | if (!isRunning) { |
| 247 | 279 | return; |
| ... | ... | @@ -249,10 +281,12 @@ public class IotEmqxUpstreamProtocol { |
| 249 | 281 | log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); |
| 250 | 282 | reconnectWithDelay(); |
| 251 | 283 | }); |
| 252 | - // 1.2 设置异常处理器 | |
| 253 | - mqttClient.exceptionHandler(exception -> log.error("[exceptionHandler][MQTT 客户端异常]", exception)); | |
| 254 | 284 | |
| 255 | - // 2. 设置消息处理器 | |
| 285 | + // 2. 设置异常处理器 | |
| 286 | + mqttClient.exceptionHandler(exception -> | |
| 287 | + log.error("[exceptionHandler][MQTT 客户端异常]", exception)); | |
| 288 | + | |
| 289 | + // 3. 设置消息处理器 | |
| 256 | 290 | mqttClient.publishHandler(upstreamHandler::handle); |
| 257 | 291 | } |
| 258 | 292 | |
| ... | ... | @@ -284,34 +318,12 @@ public class IotEmqxUpstreamProtocol { |
| 284 | 318 | } |
| 285 | 319 | |
| 286 | 320 | /** |
| 287 | - * 延迟重连 | |
| 288 | - */ | |
| 289 | - private void reconnectWithDelay() { | |
| 290 | - long delay = emqxProperties.getReconnectDelayMs(); | |
| 291 | - vertx.setTimer(delay, timerId -> { | |
| 292 | - if (!isRunning) { | |
| 293 | - return; | |
| 294 | - } | |
| 295 | - if (mqttClient != null && mqttClient.isConnected()) { | |
| 296 | - return; | |
| 297 | - } | |
| 298 | - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); | |
| 299 | - try { | |
| 300 | - connectMqtt(true, false); | |
| 301 | - } catch (Exception e) { | |
| 302 | - log.error("[reconnectWithDelay][重连失败, 将继续尝试]", e); | |
| 303 | - } | |
| 304 | - // TODO @haohao:是不是把如果连接失败,放到这里处理?继续发起。。。这样,connect 逻辑更简单纯粹;1)首次连接,失败就退出;2)重新连接,如果失败,继续重试! | |
| 305 | - }); | |
| 306 | - } | |
| 307 | - | |
| 308 | - /** | |
| 309 | 321 | * 发布消息到 MQTT Broker |
| 310 | 322 | * |
| 311 | 323 | * @param topic 主题 |
| 312 | 324 | * @param payload 消息内容 |
| 313 | 325 | */ |
| 314 | - public void publishMessage(String topic, String payload) { | |
| 326 | + public void publishMessage(String topic, byte[] payload) { | |
| 315 | 327 | if (mqttClient == null || !mqttClient.isConnected()) { |
| 316 | 328 | log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); |
| 317 | 329 | return; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java
| ... | ... | @@ -56,8 +56,7 @@ public class IotEmqxDownstreamHandler { |
| 56 | 56 | // 2.2 构建载荷 |
| 57 | 57 | byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 58 | 58 | // 2.3 发布消息 |
| 59 | - // TODO @haohao:可以直接使用 bytes 作为 payload 么? | |
| 60 | - protocol.publishMessage(topic, new String(payload)); | |
| 59 | + protocol.publishMessage(topic, payload); | |
| 61 | 60 | } |
| 62 | 61 | |
| 63 | 62 | /** |
| ... | ... | @@ -78,13 +77,22 @@ public class IotEmqxDownstreamHandler { |
| 78 | 77 | |
| 79 | 78 | // 2. 根据消息方法和回复状态,构建 topic |
| 80 | 79 | boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); |
| 81 | - // TODO @haohao:这里判断,要不去掉 methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST?直接构建??如果未来有不回复的,在特殊搞开关; | |
| 82 | - if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST && isReply) { | |
| 83 | - return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName); | |
| 84 | - } | |
| 85 | - if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_SET && !isReply) { | |
| 86 | - return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName); | |
| 80 | + | |
| 81 | + // TODO @芋艿:需要添加对应的 Topic,所以需要先判断消息方法类型 | |
| 82 | + // 根据消息方法和回复状态构建对应的主题 | |
| 83 | + switch (methodEnum) { | |
| 84 | + case PROPERTY_POST: | |
| 85 | + if (isReply) { | |
| 86 | + return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName); | |
| 87 | + } | |
| 88 | + break; | |
| 89 | + case PROPERTY_SET: | |
| 90 | + if (!isReply) { | |
| 91 | + return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName); | |
| 92 | + } | |
| 93 | + break; | |
| 87 | 94 | } |
| 95 | + | |
| 88 | 96 | log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]", |
| 89 | 97 | message.getMethod(), isReply); |
| 90 | 98 | return null; | ... | ... |