Commit 1328d91987a1b4635dfe66762b78220bfcb8290b
1 parent
05ac902d
review:【IoT 物联网】MqTT 协议
Showing
3 changed files
with
38 additions
and
50 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; |
| ... | ... | @@ -67,9 +66,11 @@ public class IotEmqxUpstreamProtocol { |
| 67 | 66 | stop(); |
| 68 | 67 | |
| 69 | 68 | // 异步关闭应用,避免阻塞当前线程 |
| 69 | + // TODO @haohao:是不是阻塞,也没关系哈? | |
| 70 | 70 | new Thread(() -> { |
| 71 | 71 | try { |
| 72 | - Thread.sleep(1000); // 等待1秒让日志输出完成 | |
| 72 | + // TODO @haohao:可以考虑用 ThreadUtil.sleep 更简洁 | |
| 73 | + Thread.sleep(1000); // 等待 1 秒让日志输出完成 | |
| 73 | 74 | log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); |
| 74 | 75 | } catch (InterruptedException ex) { |
| 75 | 76 | Thread.currentThread().interrupt(); |
| ... | ... | @@ -110,13 +111,10 @@ public class IotEmqxUpstreamProtocol { |
| 110 | 111 | */ |
| 111 | 112 | private void startMqttClient() { |
| 112 | 113 | try { |
| 113 | - // 2.1. 初始化消息处理器 | |
| 114 | + // 1. 初始化消息处理器 | |
| 114 | 115 | this.upstreamHandler = new IotEmqxUpstreamHandler(this); |
| 115 | 116 | |
| 116 | - // 2.2. 创建 MQTT 客户端 | |
| 117 | - createMqttClient(); | |
| 118 | - | |
| 119 | - // 2.3. 连接 MQTT Broker(同步等待首次连接结果) | |
| 117 | + // 2. 创建 MQTT 客户端,连接 MQTT Broker | |
| 120 | 118 | boolean connected = connectMqttSync(); |
| 121 | 119 | if (!connected) { |
| 122 | 120 | throw new RuntimeException("首次连接 MQTT Broker 失败"); |
| ... | ... | @@ -134,11 +132,14 @@ public class IotEmqxUpstreamProtocol { |
| 134 | 132 | * @param isSync 是否同步等待连接结果 |
| 135 | 133 | * @return 当 isSync 为 true 时返回连接是否成功,否则返回 null |
| 136 | 134 | */ |
| 135 | + // TODO @haohao:是不是不用结果;结束后,直接判断 this.mqttClient.isConnected(); | |
| 137 | 136 | private Boolean connectMqtt(boolean isReconnect, boolean isSync) { |
| 137 | + // TODO @haohao:这块代码,是不是放到 | |
| 138 | 138 | String host = emqxProperties.getMqttHost(); |
| 139 | 139 | Integer port = emqxProperties.getMqttPort(); |
| 140 | 140 | |
| 141 | 141 | // 2.3.1. 如果是重连,则需要重新创建 MQTT 客户端 |
| 142 | + // TODO @hoahao:疑惑,为啥这里要重新创建对象呀?另外,创建是不是拿到 reconnectWithDelay 会更合适?这样和 startMqttClient 一样呢; | |
| 142 | 143 | if (isReconnect) { |
| 143 | 144 | createMqttClient(); |
| 144 | 145 | } |
| ... | ... | @@ -148,7 +149,6 @@ public class IotEmqxUpstreamProtocol { |
| 148 | 149 | AtomicBoolean success = isSync |
| 149 | 150 | ? new AtomicBoolean(false) |
| 150 | 151 | : null; |
| 151 | - | |
| 152 | 152 | mqttClient.connect(port, host, connectResult -> { |
| 153 | 153 | if (connectResult.succeeded()) { |
| 154 | 154 | if (isReconnect) { |
| ... | ... | @@ -204,21 +204,18 @@ public class IotEmqxUpstreamProtocol { |
| 204 | 204 | * 停止 MQTT 客户端 |
| 205 | 205 | */ |
| 206 | 206 | private void stopMqttClient() { |
| 207 | - // 1.1. 取消订阅所有主题 | |
| 207 | + // 1. 取消订阅所有主题 | |
| 208 | 208 | if (mqttClient != null && mqttClient.isConnected()) { |
| 209 | 209 | List<String> topicList = emqxProperties.getMqttTopics(); |
| 210 | - if (CollUtil.isNotEmpty(topicList)) { | |
| 211 | - for (String topic : topicList) { | |
| 212 | - try { | |
| 213 | - mqttClient.unsubscribe(topic); | |
| 214 | - } catch (Exception e) { | |
| 215 | - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | |
| 216 | - } | |
| 210 | + for (String topic : topicList) { | |
| 211 | + try { | |
| 212 | + mqttClient.unsubscribe(topic); | |
| 213 | + } catch (Exception e) { | |
| 214 | + log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | |
| 217 | 215 | } |
| 218 | 216 | } |
| 219 | 217 | } |
| 220 | - | |
| 221 | - // 1.2. 断开 MQTT 客户端连接 | |
| 218 | + // 2. 断开 MQTT 客户端连接 | |
| 222 | 219 | if (mqttClient != null && mqttClient.isConnected()) { |
| 223 | 220 | try { |
| 224 | 221 | mqttClient.disconnect(); |
| ... | ... | @@ -244,18 +241,18 @@ public class IotEmqxUpstreamProtocol { |
| 244 | 241 | * 设置 MQTT 处理器 |
| 245 | 242 | */ |
| 246 | 243 | private void setupMqttHandlers() { |
| 247 | - // 1. 设置断开重连监听器 | |
| 244 | + // 1.1 设置断开重连监听器 | |
| 248 | 245 | mqttClient.closeHandler(closeEvent -> { |
| 249 | - if (isRunning) { | |
| 250 | - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); | |
| 251 | - reconnectWithDelay(); | |
| 246 | + if (!isRunning) { | |
| 247 | + return; | |
| 252 | 248 | } |
| 249 | + log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); | |
| 250 | + reconnectWithDelay(); | |
| 253 | 251 | }); |
| 254 | - | |
| 255 | - // 2. 设置异常处理器 | |
| 252 | + // 1.2 设置异常处理器 | |
| 256 | 253 | mqttClient.exceptionHandler(exception -> log.error("[exceptionHandler][MQTT 客户端异常]", exception)); |
| 257 | 254 | |
| 258 | - // 3. 设置消息处理器 | |
| 255 | + // 2. 设置消息处理器 | |
| 259 | 256 | mqttClient.publishHandler(upstreamHandler::handle); |
| 260 | 257 | } |
| 261 | 258 | |
| ... | ... | @@ -270,16 +267,13 @@ public class IotEmqxUpstreamProtocol { |
| 270 | 267 | return; |
| 271 | 268 | } |
| 272 | 269 | |
| 270 | + // 2. 批量订阅所有主题 | |
| 271 | + Map<String, Integer> topics = new HashMap<>(); | |
| 273 | 272 | int qos = emqxProperties.getMqttQos(); |
| 274 | - | |
| 275 | - // 2. 构建主题-QoS 映射,批量订阅 | |
| 276 | - Map<String, Integer> topicQosMap = new HashMap<>(); | |
| 277 | 273 | for (String topic : topicList) { |
| 278 | - topicQosMap.put(topic, qos); | |
| 274 | + topics.put(topic, qos); | |
| 279 | 275 | } |
| 280 | - | |
| 281 | - // 3. 批量订阅所有主题 | |
| 282 | - mqttClient.subscribe(topicQosMap, subscribeResult -> { | |
| 276 | + mqttClient.subscribe(topics, subscribeResult -> { | |
| 283 | 277 | if (subscribeResult.succeeded()) { |
| 284 | 278 | log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); |
| 285 | 279 | } else { |
| ... | ... | @@ -307,6 +301,7 @@ public class IotEmqxUpstreamProtocol { |
| 307 | 301 | } catch (Exception e) { |
| 308 | 302 | log.error("[reconnectWithDelay][重连失败, 将继续尝试]", e); |
| 309 | 303 | } |
| 304 | + // TODO @haohao:是不是把如果连接失败,放到这里处理?继续发起。。。这样,connect 逻辑更简单纯粹;1)首次连接,失败就退出;2)重新连接,如果失败,继续重试! | |
| 310 | 305 | }); |
| 311 | 306 | } |
| 312 | 307 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java
| ... | ... | @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; |
| 19 | 19 | * 为 EMQX 提供 HTTP 接口服务,包括: |
| 20 | 20 | * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 |
| 21 | 21 | * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 |
| 22 | - * 提供统一的错误处理和参数校验 | |
| 23 | 22 | * |
| 24 | 23 | * @author 芋道源码 |
| 25 | 24 | */ |
| ... | ... | @@ -83,7 +82,7 @@ public class IotEmqxAuthEventHandler { |
| 83 | 82 | } |
| 84 | 83 | |
| 85 | 84 | // 2. 执行认证 |
| 86 | - boolean authResult = performDeviceAuth(clientId, username, password); | |
| 85 | + boolean authResult = handleDeviceAuth(clientId, username, password); | |
| 87 | 86 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); |
| 88 | 87 | if (authResult) { |
| 89 | 88 | sendAuthResponse(context, RESULT_ALLOW); |
| ... | ... | @@ -97,8 +96,7 @@ public class IotEmqxAuthEventHandler { |
| 97 | 96 | } |
| 98 | 97 | |
| 99 | 98 | /** |
| 100 | - * EMQX 统一事件处理接口 | |
| 101 | - * 根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 | |
| 99 | + * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 | |
| 102 | 100 | * 支持的事件类型:client.connected、client.disconnected 等 |
| 103 | 101 | */ |
| 104 | 102 | public void handleEvent(RoutingContext context) { |
| ... | ... | @@ -160,18 +158,16 @@ public class IotEmqxAuthEventHandler { |
| 160 | 158 | * @return 请求体JSON对象,解析失败时返回null |
| 161 | 159 | */ |
| 162 | 160 | private JsonObject parseRequestBody(RoutingContext context) { |
| 163 | - String rawBody = null; | |
| 164 | 161 | try { |
| 165 | - rawBody = context.body().asString(); | |
| 166 | 162 | JsonObject body = context.body().asJsonObject(); |
| 167 | 163 | if (body == null) { |
| 168 | - log.info("[parseRequestBody][请求体为空][rawBody={}]", rawBody); | |
| 164 | + log.info("[parseRequestBody][请求体为空]"); | |
| 169 | 165 | sendAuthResponse(context, RESULT_IGNORE); |
| 170 | 166 | return null; |
| 171 | 167 | } |
| 172 | 168 | return body; |
| 173 | 169 | } catch (Exception e) { |
| 174 | - log.error("[parseRequestBody][解析请求体失败][rawBody={}]", rawBody, e); | |
| 170 | + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); | |
| 175 | 171 | sendAuthResponse(context, RESULT_IGNORE); |
| 176 | 172 | return null; |
| 177 | 173 | } |
| ... | ... | @@ -185,14 +181,14 @@ public class IotEmqxAuthEventHandler { |
| 185 | 181 | * @param password 密码 |
| 186 | 182 | * @return 认证是否成功 |
| 187 | 183 | */ |
| 188 | - private boolean performDeviceAuth(String clientId, String username, String password) { | |
| 184 | + private boolean handleDeviceAuth(String clientId, String username, String password) { | |
| 189 | 185 | try { |
| 190 | 186 | CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() |
| 191 | 187 | .setClientId(clientId).setUsername(username).setPassword(password)); |
| 192 | 188 | result.checkError(); |
| 193 | 189 | return BooleanUtil.isTrue(result.getData()); |
| 194 | 190 | } catch (Exception e) { |
| 195 | - log.error("[performDeviceAuth][认证接口调用失败: {}]", username, e); | |
| 191 | + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); | |
| 196 | 192 | throw e; |
| 197 | 193 | } |
| 198 | 194 | } |
| ... | ... | @@ -207,7 +203,7 @@ public class IotEmqxAuthEventHandler { |
| 207 | 203 | // 1. 解析设备信息 |
| 208 | 204 | IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); |
| 209 | 205 | if (deviceInfo == null) { |
| 210 | - log.debug("[handleDeviceStateChange][跳过非设备连接: {}]", username); | |
| 206 | + log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username); | |
| 211 | 207 | return; |
| 212 | 208 | } |
| 213 | 209 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java
| ... | ... | @@ -40,7 +40,7 @@ public class IotEmqxDownstreamHandler { |
| 40 | 40 | * @param message 设备消息 |
| 41 | 41 | */ |
| 42 | 42 | public void handle(IotDeviceMessage message) { |
| 43 | - // 1. 获取设备信息(使用缓存) | |
| 43 | + // 1. 获取设备信息 | |
| 44 | 44 | IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); |
| 45 | 45 | if (deviceInfo == null) { |
| 46 | 46 | log.error("[handle][设备信息({})不存在]", message.getDeviceId()); |
| ... | ... | @@ -53,11 +53,10 @@ public class IotEmqxDownstreamHandler { |
| 53 | 53 | log.warn("[handle][未知的消息方法: {}]", message.getMethod()); |
| 54 | 54 | return; |
| 55 | 55 | } |
| 56 | - | |
| 57 | 56 | // 2.2 构建载荷 |
| 58 | 57 | byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 59 | - | |
| 60 | 58 | // 2.3 发布消息 |
| 59 | + // TODO @haohao:可以直接使用 bytes 作为 payload 么? | |
| 61 | 60 | protocol.publishMessage(topic, new String(payload)); |
| 62 | 61 | } |
| 63 | 62 | |
| ... | ... | @@ -77,17 +76,15 @@ public class IotEmqxDownstreamHandler { |
| 77 | 76 | return null; |
| 78 | 77 | } |
| 79 | 78 | |
| 80 | - // 2. 判断是否回复消息 | |
| 79 | + // 2. 根据消息方法和回复状态,构建 topic | |
| 81 | 80 | boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); |
| 82 | - | |
| 83 | - // 3. 根据消息方法和回复状态,构建主题 | |
| 81 | + // TODO @haohao:这里判断,要不去掉 methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST?直接构建??如果未来有不回复的,在特殊搞开关; | |
| 84 | 82 | if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST && isReply) { |
| 85 | 83 | return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName); |
| 86 | 84 | } |
| 87 | 85 | if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_SET && !isReply) { |
| 88 | 86 | return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName); |
| 89 | 87 | } |
| 90 | - | |
| 91 | 88 | log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]", |
| 92 | 89 | message.getMethod(), isReply); |
| 93 | 90 | return null; | ... | ... |