Commit 1328d91987a1b4635dfe66762b78220bfcb8290b

Authored by YunaiV
1 parent 05ac902d

review:【IoT 物联网】MqTT 协议

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