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 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;
... ...