Commit 9805cf2463c8504b809673b48d0b564fe75d9f90

Authored by haohao
1 parent 2cf5bf53

feat:【IoT 物联网】优化 MQTT 客户端连接逻辑

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,6 +10,7 @@ import io.vertx.mqtt.MqttClient;
10 import io.vertx.mqtt.MqttClientOptions; 10 import io.vertx.mqtt.MqttClientOptions;
11 import jakarta.annotation.PostConstruct; 11 import jakarta.annotation.PostConstruct;
12 import jakarta.annotation.PreDestroy; 12 import jakarta.annotation.PreDestroy;
  13 +import jodd.util.ThreadUtil;
13 import lombok.Getter; 14 import lombok.Getter;
14 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
15 16
@@ -65,19 +66,15 @@ public class IotEmqxUpstreamProtocol { @@ -65,19 +66,15 @@ public class IotEmqxUpstreamProtocol {
65 log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); 66 log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
66 stop(); 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 throw e; 79 throw e;
83 } 80 }
@@ -114,114 +111,149 @@ public class IotEmqxUpstreamProtocol { @@ -114,114 +111,149 @@ public class IotEmqxUpstreamProtocol {
114 // 1. 初始化消息处理器 111 // 1. 初始化消息处理器
115 this.upstreamHandler = new IotEmqxUpstreamHandler(this); 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 } catch (Exception e) { 119 } catch (Exception e) {
123 log.error("[startMqttClient][MQTT 客户端启动失败]", e); 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 String host = emqxProperties.getMqttHost(); 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 mqttClient.connect(port, host, connectResult -> { 137 mqttClient.connect(port, host, connectResult -> {
153 if (connectResult.succeeded()) { 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 setupMqttHandlers(); 140 setupMqttHandlers();
161 subscribeToTopics(); 141 subscribeToTopics();
162 - if (success != null) {  
163 - success.set(true);  
164 - } 142 + success.set(true);
165 } else { 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 * 停止 MQTT 客户端 223 * 停止 MQTT 客户端
205 */ 224 */
206 private void stopMqttClient() { 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 try { 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 } catch (Exception e) { 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,7 +273,7 @@ public class IotEmqxUpstreamProtocol {
241 * 设置 MQTT 处理器 273 * 设置 MQTT 处理器
242 */ 274 */
243 private void setupMqttHandlers() { 275 private void setupMqttHandlers() {
244 - // 1.1 设置断开重连监听器 276 + // 1. 设置断开重连监听器
245 mqttClient.closeHandler(closeEvent -> { 277 mqttClient.closeHandler(closeEvent -> {
246 if (!isRunning) { 278 if (!isRunning) {
247 return; 279 return;
@@ -249,10 +281,12 @@ public class IotEmqxUpstreamProtocol { @@ -249,10 +281,12 @@ public class IotEmqxUpstreamProtocol {
249 log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); 281 log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
250 reconnectWithDelay(); 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 mqttClient.publishHandler(upstreamHandler::handle); 290 mqttClient.publishHandler(upstreamHandler::handle);
257 } 291 }
258 292
@@ -284,34 +318,12 @@ public class IotEmqxUpstreamProtocol { @@ -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 * 发布消息到 MQTT Broker 321 * 发布消息到 MQTT Broker
310 * 322 *
311 * @param topic 主题 323 * @param topic 主题
312 * @param payload 消息内容 324 * @param payload 消息内容
313 */ 325 */
314 - public void publishMessage(String topic, String payload) { 326 + public void publishMessage(String topic, byte[] payload) {
315 if (mqttClient == null || !mqttClient.isConnected()) { 327 if (mqttClient == null || !mqttClient.isConnected()) {
316 log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); 328 log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]");
317 return; 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,8 +56,7 @@ public class IotEmqxDownstreamHandler {
56 // 2.2 构建载荷 56 // 2.2 构建载荷
57 byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 57 byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
58 // 2.3 发布消息 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,13 +77,22 @@ public class IotEmqxDownstreamHandler {
78 77
79 // 2. 根据消息方法和回复状态,构建 topic 78 // 2. 根据消息方法和回复状态,构建 topic
80 boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); 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 log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]", 96 log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]",
89 message.getMethod(), isReply); 97 message.getMethod(), isReply);
90 return null; 98 return null;