Commit 8b4bee69f2d66e01e6d7abe614da22c5df810a45

Authored by haohao
1 parent d70c6986

feat:【IoT 物联网】优化 MQTT 连接逻辑,更新 HTTP 认证端口,简化消息处理流程

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
1 package cn.iocoder.yudao.module.iot.gateway.config; 1 package cn.iocoder.yudao.module.iot.gateway.config;
2 2
3 -import cn.hutool.core.util.StrUtil;  
4 import jakarta.validation.constraints.NotEmpty; 3 import jakarta.validation.constraints.NotEmpty;
5 import jakarta.validation.constraints.NotNull; 4 import jakarta.validation.constraints.NotNull;
6 import lombok.Data; 5 import lombok.Data;
@@ -105,46 +104,53 @@ public class IotGatewayProperties { @@ -105,46 +104,53 @@ public class IotGatewayProperties {
105 @NotNull(message = "是否开启不能为空") 104 @NotNull(message = "是否开启不能为空")
106 private Boolean enabled; 105 private Boolean enabled;
107 106
108 - // TODO @haohao:是不是改成 httpPort?不只认证,目前看。  
109 /** 107 /**
110 - * HTTP 认证端口(默认:8090) 108 + * HTTP 服务端口(默认:8090)
111 */ 109 */
112 - private Integer httpAuthPort = 8090; 110 + private Integer httpPort = 8090;
113 111
114 /** 112 /**
115 * MQTT 服务器地址 113 * MQTT 服务器地址
116 */ 114 */
117 @NotEmpty(message = "MQTT 服务器地址不能为空") 115 @NotEmpty(message = "MQTT 服务器地址不能为空")
118 private String mqttHost; 116 private String mqttHost;
  117 +
119 /** 118 /**
120 * MQTT 服务器端口(默认:1883) 119 * MQTT 服务器端口(默认:1883)
121 */ 120 */
122 @NotNull(message = "MQTT 服务器端口不能为空") 121 @NotNull(message = "MQTT 服务器端口不能为空")
123 private Integer mqttPort = 1883; 122 private Integer mqttPort = 1883;
  123 +
124 /** 124 /**
125 * MQTT 用户名 125 * MQTT 用户名
126 */ 126 */
127 @NotEmpty(message = "MQTT 用户名不能为空") 127 @NotEmpty(message = "MQTT 用户名不能为空")
128 private String mqttUsername; 128 private String mqttUsername;
  129 +
129 /** 130 /**
130 * MQTT 密码 131 * MQTT 密码
131 */ 132 */
132 @NotEmpty(message = "MQTT 密码不能为空") 133 @NotEmpty(message = "MQTT 密码不能为空")
133 private String mqttPassword; 134 private String mqttPassword;
  135 +
134 /** 136 /**
135 * MQTT 客户端的 SSL 开关 137 * MQTT 客户端的 SSL 开关
136 */ 138 */
137 @NotNull(message = "MQTT 是否开启 SSL 不能为空") 139 @NotNull(message = "MQTT 是否开启 SSL 不能为空")
138 private Boolean mqttSsl = false; 140 private Boolean mqttSsl = false;
  141 +
139 /** 142 /**
140 * MQTT 客户端 ID(如果为空,系统将自动生成) 143 * MQTT 客户端 ID(如果为空,系统将自动生成)
141 */ 144 */
  145 + @NotEmpty(message = "MQTT 客户端 ID 不能为空")
142 private String mqttClientId; 146 private String mqttClientId;
  147 +
143 /** 148 /**
144 * MQTT 订阅的主题 149 * MQTT 订阅的主题
145 */ 150 */
146 @NotEmpty(message = "MQTT 主题不能为空") 151 @NotEmpty(message = "MQTT 主题不能为空")
147 private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; 152 private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
  153 +
148 /** 154 /**
149 * 默认 QoS 级别 155 * 默认 QoS 级别
150 * <p> 156 * <p>
@@ -158,24 +164,12 @@ public class IotGatewayProperties { @@ -158,24 +164,12 @@ public class IotGatewayProperties {
158 * 连接超时时间(秒) 164 * 连接超时时间(秒)
159 */ 165 */
160 private Integer connectTimeoutSeconds = 10; 166 private Integer connectTimeoutSeconds = 10;
  167 +
161 /** 168 /**
162 * 重连延迟时间(毫秒) 169 * 重连延迟时间(毫秒)
163 */ 170 */
164 private Long reconnectDelayMs = 5000L; 171 private Long reconnectDelayMs = 5000L;
165 172
166 - // TODO @haohao:貌似可以通过配置文件 + el 表达式;尽量还是配置文件;  
167 - /**  
168 - * 获取 MQTT 客户端 ID,如果未配置则自动生成  
169 - *  
170 - * @return MQTT 客户端 ID  
171 - */  
172 - public String getMqttClientId() {  
173 - if (StrUtil.isBlank(mqttClientId)) {  
174 - mqttClientId = "iot-gateway-mqtt-" + System.currentTimeMillis();  
175 - }  
176 - return mqttClientId;  
177 - }  
178 -  
179 } 173 }
180 174
181 } 175 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 -import cn.hutool.core.util.StrUtil;  
5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 5 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttHttpAuthHandler; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttHttpAuthHandler;
@@ -20,7 +19,10 @@ import jakarta.annotation.PreDestroy; @@ -20,7 +19,10 @@ import jakarta.annotation.PreDestroy;
20 import lombok.Getter; 19 import lombok.Getter;
21 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
22 21
  22 +import java.util.HashMap;
23 import java.util.List; 23 import java.util.List;
  24 +import java.util.Map;
  25 +import java.util.concurrent.atomic.AtomicInteger;
24 26
25 /** 27 /**
26 * IoT 网关 MQTT 协议:接收设备上行消息 28 * IoT 网关 MQTT 协议:接收设备上行消息
@@ -127,7 +129,7 @@ public class IotMqttUpstreamProtocol { @@ -127,7 +129,7 @@ public class IotMqttUpstreamProtocol {
127 router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(authHandler::handleEvent); 129 router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(authHandler::handleEvent);
128 130
129 // 2. 启动 HTTP 服务器 131 // 2. 启动 HTTP 服务器
130 - int authPort = emqxProperties.getHttpAuthPort(); 132 + int authPort = emqxProperties.getHttpPort();
131 try { 133 try {
132 httpAuthServer = vertx.createHttpServer() 134 httpAuthServer = vertx.createHttpServer()
133 .requestHandler(router) 135 .requestHandler(router)
@@ -169,10 +171,13 @@ public class IotMqttUpstreamProtocol { @@ -169,10 +171,13 @@ public class IotMqttUpstreamProtocol {
169 log.info("[startMqttClient][使用 MQTT 客户端 ID: {}]", emqxProperties.getMqttClientId()); 171 log.info("[startMqttClient][使用 MQTT 客户端 ID: {}]", emqxProperties.getMqttClientId());
170 createMqttClient(); 172 createMqttClient();
171 173
172 - // 3. 连接 MQTT Broker(异步连接,不会抛出异常)  
173 - connectMqtt(false); 174 + // 3. 连接 MQTT Broker(同步等待首次连接结果)
  175 + boolean connected = connectMqttSync();
  176 + if (!connected) {
  177 + throw new RuntimeException("首次连接 MQTT Broker 失败");
  178 + }
174 179
175 - log.info("[startMqttClient][MQTT 客户端启动完成,正在异步连接中...]"); 180 + log.info("[startMqttClient][MQTT 客户端启动完成]");
176 } catch (Exception e) { 181 } catch (Exception e) {
177 log.error("[startMqttClient][MQTT 客户端启动失败]", e); 182 log.error("[startMqttClient][MQTT 客户端启动失败]", e);
178 throw new RuntimeException("MQTT 客户端启动失败", e); 183 throw new RuntimeException("MQTT 客户端启动失败", e);
@@ -180,6 +185,48 @@ public class IotMqttUpstreamProtocol { @@ -180,6 +185,48 @@ public class IotMqttUpstreamProtocol {
180 } 185 }
181 186
182 /** 187 /**
  188 + * 同步连接 MQTT Broker
  189 + *
  190 + * @return 是否连接成功
  191 + */
  192 + private boolean connectMqttSync() {
  193 + String host = emqxProperties.getMqttHost();
  194 + Integer port = emqxProperties.getMqttPort();
  195 + log.info("[connectMqttSync][开始连接 MQTT Broker, host: {}, port: {}]", host, port);
  196 +
  197 + // 使用计数器实现同步等待
  198 + java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
  199 + java.util.concurrent.atomic.AtomicBoolean success = new java.util.concurrent.atomic.AtomicBoolean(false);
  200 +
  201 + mqttClient.connect(port, host, connectResult -> {
  202 + if (connectResult.succeeded()) {
  203 + log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
  204 + // 设置处理器
  205 + setupMqttHandlers();
  206 + // 订阅主题
  207 + subscribeToTopics();
  208 + success.set(true);
  209 + } else {
  210 + log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
  211 + host, port, connectResult.cause());
  212 + // 首次连接失败,启动重连机制
  213 + reconnectWithDelay();
  214 + }
  215 + latch.countDown();
  216 + });
  217 +
  218 + try {
  219 + // 等待连接结果,最多等待10秒
  220 + latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
  221 + } catch (InterruptedException e) {
  222 + Thread.currentThread().interrupt();
  223 + log.error("[connectMqttSync][等待连接结果被中断]", e);
  224 + }
  225 +
  226 + return success.get();
  227 + }
  228 +
  229 + /**
183 * 停止 MQTT 客户端 230 * 停止 MQTT 客户端
184 */ 231 */
185 private void stopMqttClient() { 232 private void stopMqttClient() {
@@ -218,15 +265,6 @@ public class IotMqttUpstreamProtocol { @@ -218,15 +265,6 @@ public class IotMqttUpstreamProtocol {
218 // 1. 参数校验 265 // 1. 参数校验
219 String host = emqxProperties.getMqttHost(); 266 String host = emqxProperties.getMqttHost();
220 Integer port = emqxProperties.getMqttPort(); 267 Integer port = emqxProperties.getMqttPort();
221 - // TODO @haohao:这些参数校验,交给 validator;  
222 - if (StrUtil.isBlank(host)) {  
223 - log.error("[connectMqtt][MQTT Host 为空, 无法连接]");  
224 - throw new IllegalArgumentException("MQTT Host 不能为空");  
225 - }  
226 - if (port == null || port <= 0) {  
227 - log.error("[connectMqtt][MQTT Port({}) 无效]", port);  
228 - throw new IllegalArgumentException("MQTT Port 必须为正整数");  
229 - }  
230 268
231 if (isReconnect) { 269 if (isReconnect) {
232 log.info("[connectMqtt][开始重连 MQTT Broker, host: {}, port: {}]", host, port); 270 log.info("[connectMqtt][开始重连 MQTT Broker, host: {}, port: {}]", host, port);
@@ -238,32 +276,28 @@ public class IotMqttUpstreamProtocol { @@ -238,32 +276,28 @@ public class IotMqttUpstreamProtocol {
238 276
239 // 2. 异步连接 277 // 2. 异步连接
240 mqttClient.connect(port, host, connectResult -> { 278 mqttClient.connect(port, host, connectResult -> {
241 - // TODO @haohao:if return,减少括号哈;  
242 - if (connectResult.succeeded()) {  
243 - if (isReconnect) {  
244 - log.info("[connectMqtt][MQTT 客户端重连成功, host: {}, port: {}]", host, port);  
245 - } else {  
246 - log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port);  
247 - }  
248 -  
249 - // 设置处理器  
250 - setupMqttHandlers();  
251 - // 订阅主题  
252 - subscribeToTopics();  
253 - } else { 279 + if (!connectResult.succeeded()) {
254 log.error("[connectMqtt][连接 MQTT Broker 失败, host: {}, port: {}, isReconnect: {}]", 280 log.error("[connectMqtt][连接 MQTT Broker 失败, host: {}, port: {}, isReconnect: {}]",
255 host, port, isReconnect, connectResult.cause()); 281 host, port, isReconnect, connectResult.cause());
256 282
257 - // TODO @haohao:体感上,是不是首次必须连接成功?类似 mysql;首次要连接上,然后后续可以重连; 283 + // 首次连接失败或重连失败时,尝试重连
258 if (!isReconnect) { 284 if (!isReconnect) {
259 - // 首次连接失败时,也要尝试重连  
260 log.warn("[connectMqtt][首次连接失败,将开始重连机制]"); 285 log.warn("[connectMqtt][首次连接失败,将开始重连机制]");
261 - reconnectWithDelay();  
262 - } else {  
263 - // 重连失败时,继续尝试重连  
264 - reconnectWithDelay();  
265 } 286 }
  287 + reconnectWithDelay();
  288 + return;
  289 + }
  290 +
  291 + if (isReconnect) {
  292 + log.info("[connectMqtt][MQTT 客户端重连成功, host: {}, port: {}]", host, port);
  293 + } else {
  294 + log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
266 } 295 }
  296 +
  297 + // 设置处理器
  298 + setupMqttHandlers();
  299 + // 订阅主题
  300 + subscribeToTopics();
267 }); 301 });
268 } 302 }
269 303
@@ -283,12 +317,7 @@ public class IotMqttUpstreamProtocol { @@ -283,12 +317,7 @@ public class IotMqttUpstreamProtocol {
283 * 设置 MQTT 处理器 317 * 设置 MQTT 处理器
284 */ 318 */
285 private void setupMqttHandlers() { 319 private void setupMqttHandlers() {
286 - // TODO @haohao:mqttClient 一定非空;  
287 - if (mqttClient == null) {  
288 - log.warn("[setupMqttHandlers][MQTT 客户端为空,跳过处理器设置]");  
289 - return;  
290 - }  
291 - 320 + // 由于 mqttClient 在 createMqttClient() 方法中已初始化,此处无需检查
292 // 设置断开重连监听器 321 // 设置断开重连监听器
293 mqttClient.closeHandler(closeEvent -> { 322 mqttClient.closeHandler(closeEvent -> {
294 log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); 323 log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
@@ -301,13 +330,9 @@ public class IotMqttUpstreamProtocol { @@ -301,13 +330,9 @@ public class IotMqttUpstreamProtocol {
301 }); 330 });
302 331
303 // 设置消息处理器 332 // 设置消息处理器
304 - // TODO @haohao:upstreamHandler 一定非空;  
305 - if (upstreamHandler != null) {  
306 - mqttClient.publishHandler(upstreamHandler::handle);  
307 - log.debug("[setupMqttHandlers][MQTT 消息处理器设置完成]");  
308 - } else {  
309 - log.warn("[setupMqttHandlers][上行消息处理器为空,跳过设置]");  
310 - } 333 + // upstreamHandler 在 startMqttClient() 方法中已初始化,此处无需检查
  334 + mqttClient.publishHandler(upstreamHandler::handle);
  335 + log.debug("[setupMqttHandlers][MQTT 消息处理器设置完成]");
311 } 336 }
312 337
313 /** 338 /**
@@ -327,35 +352,39 @@ public class IotMqttUpstreamProtocol { @@ -327,35 +352,39 @@ public class IotMqttUpstreamProtocol {
327 int qos = emqxProperties.getMqttQos(); 352 int qos = emqxProperties.getMqttQos();
328 log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos); 353 log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos);
329 354
330 - // TODO @haohao:使用 atomicinteger 会更合适;  
331 - int[] successCount = { 0 }; // 使用数组以便在 lambda 中修改  
332 - int[] failCount = { 0 }; 355 + // 使用 AtomicInteger 替代数组,线程安全且更简洁
  356 + AtomicInteger successCount = new AtomicInteger(0);
  357 + AtomicInteger failCount = new AtomicInteger(0);
333 358
  359 + // 构建主题-QoS 映射,批量订阅
  360 + Map<String, Integer> topicQosMap = new HashMap<>();
334 for (String topic : topicList) { 361 for (String topic : topicList) {
335 - // TODO @haohao:MqttClient subscribe(Map<String, Integer> topics, 是不是更简洁哈;  
336 - mqttClient.subscribe(topic, qos, subscribeResult -> {  
337 - if (subscribeResult.succeeded()) {  
338 - successCount[0]++;  
339 - log.debug("[subscribeToTopics][订阅主题成功, topic: {}, qos: {}]", topic, qos); 362 + topicQosMap.put(topic, qos);
  363 + }
340 364
341 - // 当所有主题都处理完成时,记录汇总日志  
342 - if (successCount[0] + failCount[0] == topicList.size()) {  
343 - log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]",  
344 - successCount[0], failCount[0], topicList.size());  
345 - }  
346 - } else {  
347 - failCount[0]++;  
348 - log.error("[subscribeToTopics][订阅主题失败, topic: {}, qos: {}, 原因: {}]",  
349 - topic, qos, subscribeResult.cause().getMessage(), subscribeResult.cause());  
350 -  
351 - // 当所有主题都处理完成时,记录汇总日志  
352 - if (successCount[0] + failCount[0] == topicList.size()) {  
353 - log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]",  
354 - successCount[0], failCount[0], topicList.size());  
355 - } 365 + // 批量订阅所有主题
  366 + mqttClient.subscribe(topicQosMap, subscribeResult -> {
  367 + if (subscribeResult.succeeded()) {
  368 + // 批量订阅成功,记录所有主题为成功
  369 + int successful = successCount.addAndGet(topicList.size());
  370 + log.info("[subscribeToTopics][批量订阅主题成功, 共 {} 个主题, QoS: {}]", successful, qos);
  371 + for (String topic : topicList) {
  372 + log.debug("[subscribeToTopics][订阅主题成功, topic: {}, qos: {}]", topic, qos);
356 } 373 }
357 - });  
358 - } 374 + } else {
  375 + // 批量订阅失败,记录所有主题为失败
  376 + int failed = failCount.addAndGet(topicList.size());
  377 + log.error("[subscribeToTopics][批量订阅主题失败, 共 {} 个主题, 原因: {}]",
  378 + failed, subscribeResult.cause().getMessage(), subscribeResult.cause());
  379 + for (String topic : topicList) {
  380 + log.error("[subscribeToTopics][订阅主题失败, topic: {}, qos: {}]", topic, qos);
  381 + }
  382 + }
  383 +
  384 + // 记录汇总日志
  385 + log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]",
  386 + successCount.get(), failCount.get(), topicList.size());
  387 + });
359 } 388 }
360 389
361 /** 390 /**
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java
@@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2 2
3 import cn.hutool.core.util.StrUtil; 3 import cn.hutool.core.util.StrUtil;
4 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
5 -import cn.hutool.json.JSONObject;  
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
10 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 10 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
11 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 11 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -48,52 +48,49 @@ public class IotMqttDownstreamHandler { @@ -48,52 +48,49 @@ public class IotMqttDownstreamHandler {
48 } 48 }
49 49
50 // 2.1 根据方法构建主题 50 // 2.1 根据方法构建主题
51 - String topic = buildTopicByMethod(message.getMethod(), deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 51 + String topic = buildTopicByMethod(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
52 if (StrUtil.isBlank(topic)) { 52 if (StrUtil.isBlank(topic)) {
53 log.warn("[handle][未知的消息方法: {}]", message.getMethod()); 53 log.warn("[handle][未知的消息方法: {}]", message.getMethod());
54 return; 54 return;
55 } 55 }
  56 +
56 // 2.2 构建载荷 57 // 2.2 构建载荷
57 - // TODO @haohao:这里是不是 encode 就可以发拉?因为本身就 json 化了。  
58 - JSONObject payload = buildDownstreamPayload(message); 58 + byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  59 +
59 // 2.3 发布消息 60 // 2.3 发布消息
60 - protocol.publishMessage(topic, payload.toString()); 61 + protocol.publishMessage(topic, new String(payload));
61 } 62 }
62 63
63 - // TODO @haohao:这个是不是也可以计算;IotDeviceMessageUtils 的 isReplyMessage;这样就直接生成了;  
64 /** 64 /**
65 - * 根据方法构建主题 65 + * 根据消息方法和回复状态构建主题
66 * 66 *
67 - * @param method 消息方法 67 + * @param message 设备消息
68 * @param productKey 产品标识 68 * @param productKey 产品标识
69 * @param deviceName 设备名称 69 * @param deviceName 设备名称
70 * @return 构建的主题,如果方法不支持返回 null 70 * @return 构建的主题,如果方法不支持返回 null
71 */ 71 */
72 - private String buildTopicByMethod(String method, String productKey, String deviceName) {  
73 - IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(method); 72 + private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) {
  73 + // 1. 解析消息方法
  74 + IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(message.getMethod());
74 if (methodEnum == null) { 75 if (methodEnum == null) {
  76 + log.warn("[buildTopicByMethod][未知的消息方法: {}]", message.getMethod());
75 return null; 77 return null;
76 } 78 }
77 - return switch (methodEnum) {  
78 - case PROPERTY_POST -> IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);  
79 - case PROPERTY_SET -> IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);  
80 - default -> null;  
81 - };  
82 79
83 - } 80 + // 2. 判断是否回复消息
  81 + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
84 82
85 - /**  
86 - * 构建下行消息载荷  
87 - *  
88 - * @param message 设备消息  
89 - * @return JSON 载荷  
90 - */  
91 - private JSONObject buildDownstreamPayload(IotDeviceMessage message) {  
92 - // 使用 IotDeviceMessageService 进行消息编码  
93 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId());  
94 - byte[] encodedBytes = deviceMessageService.encodeDeviceMessage(message, device.getProductKey(),  
95 - device.getDeviceName());  
96 - return new JSONObject(new String(encodedBytes)); 83 + // 3. 根据消息方法和回复状态,构建主题
  84 + if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST && isReply) {
  85 + return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
  86 + }
  87 + if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_SET && !isReply) {
  88 + return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
  89 + }
  90 +
  91 + log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]",
  92 + message.getMethod(), isReply);
  93 + return null;
97 } 94 }
98 95
99 } 96 }
100 \ No newline at end of file 97 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
@@ -14,8 +14,6 @@ import io.vertx.core.json.JsonObject; @@ -14,8 +14,6 @@ import io.vertx.core.json.JsonObject;
14 import io.vertx.ext.web.RoutingContext; 14 import io.vertx.ext.web.RoutingContext;
15 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
16 16
17 -import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;  
18 -  
19 /** 17 /**
20 * IoT 网关 MQTT HTTP 认证处理器 18 * IoT 网关 MQTT HTTP 认证处理器
21 * <p> 19 * <p>
@@ -67,7 +65,7 @@ public class IotMqttHttpAuthHandler { @@ -67,7 +65,7 @@ public class IotMqttHttpAuthHandler {
67 */ 65 */
68 public void handleAuth(RoutingContext context) { 66 public void handleAuth(RoutingContext context) {
69 try { 67 try {
70 - // 参数校验 68 + // 1. 参数校验
71 JsonObject body = parseRequestBody(context); 69 JsonObject body = parseRequestBody(context);
72 if (body == null) { 70 if (body == null) {
73 return; 71 return;
@@ -78,23 +76,21 @@ public class IotMqttHttpAuthHandler { @@ -78,23 +76,21 @@ public class IotMqttHttpAuthHandler {
78 log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username); 76 log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username);
79 if (StrUtil.hasEmpty(clientId, username, password)) { 77 if (StrUtil.hasEmpty(clientId, username, password)) {
80 log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username); 78 log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username);
81 - sendAuthResponse(context, RESULT_DENY, false, "认证参数不完整"); 79 + sendAuthResponse(context, RESULT_DENY);
82 return; 80 return;
83 } 81 }
84 82
85 - // 执行设备认证 83 + // 2. 执行认证
86 boolean authResult = performDeviceAuth(clientId, username, password); 84 boolean authResult = performDeviceAuth(clientId, username, password);
  85 + log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
87 if (authResult) { 86 if (authResult) {
88 - // TODO @haohao:是不是两条 info,直接打认证结果:authResult  
89 - log.info("[handleAuth][设备认证成功: {}]", username);  
90 - sendAuthResponse(context, RESULT_ALLOW, false, null); 87 + sendAuthResponse(context, RESULT_ALLOW);
91 } else { 88 } else {
92 - log.info("[handleAuth][设备认证失败: {}]", username);  
93 - sendAuthResponse(context, RESULT_DENY, false, DEVICE_AUTH_FAIL.getMsg()); 89 + sendAuthResponse(context, RESULT_DENY);
94 } 90 }
95 } catch (Exception e) { 91 } catch (Exception e) {
96 log.error("[handleAuth][设备认证异常]", e); 92 log.error("[handleAuth][设备认证异常]", e);
97 - sendAuthResponse(context, RESULT_IGNORE, false, "认证服务异常"); 93 + sendAuthResponse(context, RESULT_IGNORE);
98 } 94 }
99 } 95 }
100 96
@@ -104,9 +100,10 @@ public class IotMqttHttpAuthHandler { @@ -104,9 +100,10 @@ public class IotMqttHttpAuthHandler {
104 * 支持的事件类型:client.connected、client.disconnected 等 100 * 支持的事件类型:client.connected、client.disconnected 等
105 */ 101 */
106 public void handleEvent(RoutingContext context) { 102 public void handleEvent(RoutingContext context) {
  103 + JsonObject body = null;
107 try { 104 try {
108 - // 解析请求体  
109 - JsonObject body = parseRequestBody(context); 105 + // 1. 解析请求体
  106 + body = parseRequestBody(context);
110 if (body == null) { 107 if (body == null) {
111 return; 108 return;
112 } 109 }
@@ -114,7 +111,7 @@ public class IotMqttHttpAuthHandler { @@ -114,7 +111,7 @@ public class IotMqttHttpAuthHandler {
114 String username = body.getString("username"); 111 String username = body.getString("username");
115 log.debug("[handleEvent][收到事件: {} - {}]", event, username); 112 log.debug("[handleEvent][收到事件: {} - {}]", event, username);
116 113
117 - // 根据事件类型进行分发处理 114 + // 2. 根据事件类型进行分发处理
118 switch (event) { 115 switch (event) {
119 case EVENT_CLIENT_CONNECTED: 116 case EVENT_CLIENT_CONNECTED:
120 handleClientConnected(body); 117 handleClientConnected(body);
@@ -123,15 +120,13 @@ public class IotMqttHttpAuthHandler { @@ -123,15 +120,13 @@ public class IotMqttHttpAuthHandler {
123 handleClientDisconnected(body); 120 handleClientDisconnected(body);
124 break; 121 break;
125 default: 122 default:
126 - log.debug("[handleEvent][忽略事件: {}]", event);  
127 break; 123 break;
128 } 124 }
129 125
130 // EMQX Webhook 只需要 200 状态码,无需响应体 126 // EMQX Webhook 只需要 200 状态码,无需响应体
131 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); 127 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
132 } catch (Exception e) { 128 } catch (Exception e) {
133 - // TODO @haohao:body 可以打印出来  
134 - log.error("[handleEvent][事件处理失败]", e); 129 + log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
135 // 即使处理失败,也返回 200 避免EMQX重试 130 // 即使处理失败,也返回 200 避免EMQX重试
136 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); 131 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
137 } 132 }
@@ -163,18 +158,19 @@ public class IotMqttHttpAuthHandler { @@ -163,18 +158,19 @@ public class IotMqttHttpAuthHandler {
163 * @return 请求体JSON对象,解析失败时返回null 158 * @return 请求体JSON对象,解析失败时返回null
164 */ 159 */
165 private JsonObject parseRequestBody(RoutingContext context) { 160 private JsonObject parseRequestBody(RoutingContext context) {
  161 + String rawBody = null;
166 try { 162 try {
  163 + rawBody = context.body().asString();
167 JsonObject body = context.body().asJsonObject(); 164 JsonObject body = context.body().asJsonObject();
168 if (body == null) { 165 if (body == null) {
169 - log.info("[parseRequestBody][请求体为空]");  
170 - sendAuthResponse(context, RESULT_IGNORE, false, "请求体不能为空"); 166 + log.info("[parseRequestBody][请求体为空][rawBody={}]", rawBody);
  167 + sendAuthResponse(context, RESULT_IGNORE);
171 return null; 168 return null;
172 } 169 }
173 return body; 170 return body;
174 } catch (Exception e) { 171 } catch (Exception e) {
175 - // TODO @haohao:最好把 body 打印出来;  
176 - log.error("[parseRequestBody][解析请求体失败]", e);  
177 - sendAuthResponse(context, RESULT_IGNORE, false, "请求体格式错误"); 172 + log.error("[parseRequestBody][解析请求体失败][rawBody={}]", rawBody, e);
  173 + sendAuthResponse(context, RESULT_IGNORE);
178 return null; 174 return null;
179 } 175 }
180 } 176 }
@@ -203,13 +199,10 @@ public class IotMqttHttpAuthHandler { @@ -203,13 +199,10 @@ public class IotMqttHttpAuthHandler {
203 * 处理设备状态变化 199 * 处理设备状态变化
204 * 200 *
205 * @param username 用户名 201 * @param username 用户名
206 - * @param online 是否在线 202 + * @param online 是否在线 true 在线 false 离线
207 */ 203 */
208 private void handleDeviceStateChange(String username, boolean online) { 204 private void handleDeviceStateChange(String username, boolean online) {
209 - // 解析设备信息  
210 - if (StrUtil.isEmpty(username) || "undefined".equals(username)) {  
211 - return;  
212 - } 205 + // 1. 解析设备信息
213 IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 206 IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
214 if (deviceInfo == null) { 207 if (deviceInfo == null) {
215 log.debug("[handleDeviceStateChange][跳过非设备连接: {}]", username); 208 log.debug("[handleDeviceStateChange][跳过非设备连接: {}]", username);
@@ -217,24 +210,13 @@ public class IotMqttHttpAuthHandler { @@ -217,24 +210,13 @@ public class IotMqttHttpAuthHandler {
217 } 210 }
218 211
219 try { 212 try {
220 - // TODO @haohao:serverId 获取非空,可以忽略掉;  
221 - String serverId = protocol.getServerId();  
222 - if (StrUtil.isEmpty(serverId)) {  
223 - log.error("[handleDeviceStateChange][获取服务器ID失败]");  
224 - return;  
225 - }  
226 -  
227 - // 构建设备状态消息 213 + // 2. 构建设备状态消息
228 IotDeviceMessage message = online ? IotDeviceMessage.buildStateOnline() 214 IotDeviceMessage message = online ? IotDeviceMessage.buildStateOnline()
229 : IotDeviceMessage.buildStateOffline(); 215 : IotDeviceMessage.buildStateOffline();
230 - // 发送消息到消息总线  
231 - deviceMessageService.sendDeviceMessage(message,  
232 - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);  
233 216
234 - // TODO @haohao:online 不用翻译  
235 - log.info("[handleDeviceStateChange][设备状态更新: {}/{} -> {}]",  
236 - deviceInfo.getProductKey(), deviceInfo.getDeviceName(),  
237 - online ? "在线" : "离线"); 217 + // 3. 发送设备状态消息
  218 + deviceMessageService.sendDeviceMessage(message,
  219 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
238 } catch (Exception e) { 220 } catch (Exception e) {
239 log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, e); 221 log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, e);
240 } 222 }
@@ -244,16 +226,14 @@ public class IotMqttHttpAuthHandler { @@ -244,16 +226,14 @@ public class IotMqttHttpAuthHandler {
244 * 发送 EMQX 认证响应 226 * 发送 EMQX 认证响应
245 * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 227 * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
246 * 228 *
247 - * @param context 路由上下文  
248 - * @param result 认证结果:allow、deny、ignore  
249 - * @param isSuperuser 是否超级用户  
250 - * @param message 日志消息(仅用于日志记录,不返回给EMQX) 229 + * @param context 路由上下文
  230 + * @param result 认证结果:allow、deny、ignore
251 */ 231 */
252 - private void sendAuthResponse(RoutingContext context, String result, boolean isSuperuser, String message) { 232 + private void sendAuthResponse(RoutingContext context, String result) {
253 // 构建符合 EMQX 官方规范的响应 233 // 构建符合 EMQX 官方规范的响应
254 JsonObject response = new JsonObject() 234 JsonObject response = new JsonObject()
255 .put("result", result) 235 .put("result", result)
256 - .put("is_superuser", isSuperuser); 236 + .put("is_superuser", false);
257 237
258 // 可以根据业务需求添加客户端属性 238 // 可以根据业务需求添加客户端属性
259 // response.put("client_attrs", new JsonObject().put("role", "device")); 239 // response.put("client_attrs", new JsonObject().put("role", "device"));
@@ -261,7 +241,6 @@ public class IotMqttHttpAuthHandler { @@ -261,7 +241,6 @@ public class IotMqttHttpAuthHandler {
261 // 可以添加认证过期时间(可选) 241 // 可以添加认证过期时间(可选)
262 // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); 242 // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
263 243
264 - // 记录详细的响应日志(message仅用于日志,不返回给EMQX)  
265 context.response() 244 context.response()
266 .setStatusCode(SUCCESS_STATUS_CODE) 245 .setStatusCode(SUCCESS_STATUS_CODE)
267 .putHeader("Content-Type", "application/json; charset=utf-8") 246 .putHeader("Content-Type", "application/json; charset=utf-8")
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
@@ -7,9 +7,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol @@ -7,9 +7,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol
7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 import io.vertx.mqtt.messages.MqttPublishMessage; 8 import io.vertx.mqtt.messages.MqttPublishMessage;
9 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
10 -import org.springframework.util.Assert;  
11 -  
12 -import java.util.Arrays;  
13 10
14 /** 11 /**
15 * IoT 网关 MQTT 上行消息处理器 12 * IoT 网关 MQTT 上行消息处理器
@@ -32,38 +29,24 @@ public class IotMqttUpstreamHandler { @@ -32,38 +29,24 @@ public class IotMqttUpstreamHandler {
32 * 处理 MQTT 发布消息 29 * 处理 MQTT 发布消息
33 */ 30 */
34 public void handle(MqttPublishMessage mqttMessage) { 31 public void handle(MqttPublishMessage mqttMessage) {
  32 + log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
35 String topic = mqttMessage.topicName(); 33 String topic = mqttMessage.topicName();
36 byte[] payload = mqttMessage.payload().getBytes(); 34 byte[] payload = mqttMessage.payload().getBytes();
37 try { 35 try {
38 - // 1. 前置校验  
39 - if (StrUtil.isBlank(topic)) {  
40 - log.warn("[handle][主题为空, 忽略消息]");  
41 - return;  
42 - }  
43 -  
44 - // 2.1 识别并验证消息类型  
45 - String messageType = getMessageType(topic);  
46 - // TODO @haohao:可以使用 hutool 的,它的字符串拼接更简单;  
47 - Assert.notNull(messageType, String.format("未知的消息类型, topic(%s)", topic));  
48 - // 2.2 解析主题,获取 productKey 和 deviceName  
49 - // TODO @haohao:体感 getMessageType 和下面,都 split;是不是一次就 ok 拉;1)split 掉;2)2、3 位置是 productKey、deviceName;3)4 开始还是 method 36 + // 1. 解析主题,一次性获取所有信息
50 String[] topicParts = topic.split("/"); 37 String[] topicParts = topic.split("/");
51 - if (topicParts.length < 4) {  
52 - log.warn("[handle][topic({}) 格式不正确,无法解析 productKey 和 deviceName]", topic); 38 + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
  39 + log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
53 return; 40 return;
54 } 41 }
  42 +
55 String productKey = topicParts[2]; 43 String productKey = topicParts[2];
56 String deviceName = topicParts[3]; 44 String deviceName = topicParts[3];
57 - // TODO @haohao:是不是要判断,部分为空,就不行呀;  
58 - if (StrUtil.isAllBlank(productKey, deviceName)) {  
59 - log.warn("[handle][topic({}) 格式不正确,productKey 和 deviceName 部分为空]", topic);  
60 - return;  
61 - }  
62 45
63 // 3. 解码消息 46 // 3. 解码消息
64 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); 47 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
65 if (message == null) { 48 if (message == null) {
66 - log.warn("[handle][topic({}) payload({}) 消息解码失败", topic, new String(payload)); 49 + log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
67 return; 50 return;
68 } 51 }
69 52
@@ -74,22 +57,4 @@ public class IotMqttUpstreamHandler { @@ -74,22 +57,4 @@ public class IotMqttUpstreamHandler {
74 } 57 }
75 } 58 }
76 59
77 - // TODO @haohao:是不是 getMethodFromTopic?  
78 - /**  
79 - * 从主题中,获得消息类型  
80 - *  
81 - * @param topic 主题  
82 - * @return 消息类型  
83 - */  
84 - private String getMessageType(String topic) {  
85 - String[] topicParts = topic.split("/");  
86 - // 约定:topic 第 4 个部分开始为消息类型  
87 - // 例如:/sys/{productKey}/{deviceName}/thing/property/post -> thing/property/post  
88 - if (topicParts.length > 4) {  
89 - // TODO @haohao:是不是 subString 前 3 个,性能更好;  
90 - return String.join("/", Arrays.copyOfRange(topicParts, 4, topicParts.length));  
91 - }  
92 - return topicParts[topicParts.length - 1];  
93 - }  
94 -  
95 } 60 }
96 \ No newline at end of file 61 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application-local.yaml
@@ -32,13 +32,13 @@ yudao: @@ -32,13 +32,13 @@ yudao:
32 # ==================================== 32 # ====================================
33 emqx: 33 emqx:
34 enabled: true 34 enabled: true
35 - http-auth-port: 8090 # MQTT HTTP 认证服务端口  
36 - mqtt-host: 127.0.0.1 # MQTT Broker 地址  
37 - mqtt-port: 1883 # MQTT Broker 端口  
38 - mqtt-username: admin # MQTT 用户名  
39 - mqtt-password: public # MQTT 密码  
40 - mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID  
41 - mqtt-ssl: false # 是否开启 SSL 35 + http-port: 8090 # MQTT HTTP 服务端口
  36 + mqtt-host: 127.0.0.1 # MQTT Broker 地址
  37 + mqtt-port: 1883 # MQTT Broker 端口
  38 + mqtt-username: admin # MQTT 用户名
  39 + mqtt-password: public # MQTT 密码
  40 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID
  41 + mqtt-ssl: false # 是否开启 SSL
42 mqtt-topics: 42 mqtt-topics:
43 - "/sys/#" # 系统主题 43 - "/sys/#" # 系统主题
44 44
@@ -55,4 +55,4 @@ logging: @@ -55,4 +55,4 @@ logging:
55 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG 55 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
56 cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG 56 cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
57 # MQTT 客户端日志 57 # MQTT 客户端日志
58 - io.vertx.mqtt: DEBUG  
59 \ No newline at end of file 58 \ No newline at end of file
  59 +# io.vertx.mqtt: DEBUG
60 \ No newline at end of file 60 \ No newline at end of file