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