Commit 7e49c901562889b22cbe85e153ad0cf23d51d9bd
1 parent
f1368d9e
review:【IoT 物联网】MqTT 协议
Showing
6 changed files
with
68 additions
and
104 deletions
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
| ... | ... | @@ -23,12 +23,13 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { |
| 23 | 23 | STATE_OFFLINE("thing.state.offline", true), |
| 24 | 24 | |
| 25 | 25 | // ========== 设备属性 ========== |
| 26 | - // 可参考 | |
| 27 | - // https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services | |
| 26 | + // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services | |
| 27 | + | |
| 28 | 28 | PROPERTY_POST("thing.property.post", true), |
| 29 | 29 | PROPERTY_SET("thing.property.set", false), |
| 30 | 30 | |
| 31 | 31 | // ========== 设备事件 ========== |
| 32 | + | |
| 32 | 33 | EVENT_POST("thing.event.post", true), |
| 33 | 34 | |
| 34 | 35 | ; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| ... | ... | @@ -105,6 +105,7 @@ public class IotGatewayProperties { |
| 105 | 105 | @NotNull(message = "是否开启不能为空") |
| 106 | 106 | private Boolean enabled; |
| 107 | 107 | |
| 108 | + // TODO @haohao:是不是改成 httpPort?不只认证,目前看。 | |
| 108 | 109 | /** |
| 109 | 110 | * HTTP 认证端口(默认:8090) |
| 110 | 111 | */ |
| ... | ... | @@ -115,42 +116,35 @@ public class IotGatewayProperties { |
| 115 | 116 | */ |
| 116 | 117 | @NotEmpty(message = "MQTT 服务器地址不能为空") |
| 117 | 118 | private String mqttHost; |
| 118 | - | |
| 119 | 119 | /** |
| 120 | 120 | * MQTT 服务器端口(默认:1883) |
| 121 | 121 | */ |
| 122 | 122 | @NotNull(message = "MQTT 服务器端口不能为空") |
| 123 | 123 | private Integer mqttPort = 1883; |
| 124 | - | |
| 125 | 124 | /** |
| 126 | 125 | * MQTT 用户名 |
| 127 | 126 | */ |
| 128 | 127 | @NotEmpty(message = "MQTT 用户名不能为空") |
| 129 | 128 | private String mqttUsername; |
| 130 | - | |
| 131 | 129 | /** |
| 132 | 130 | * MQTT 密码 |
| 133 | 131 | */ |
| 134 | 132 | @NotEmpty(message = "MQTT 密码不能为空") |
| 135 | 133 | private String mqttPassword; |
| 136 | - | |
| 137 | 134 | /** |
| 138 | 135 | * MQTT 客户端的 SSL 开关 |
| 139 | 136 | */ |
| 140 | 137 | @NotNull(message = "MQTT 是否开启 SSL 不能为空") |
| 141 | 138 | private Boolean mqttSsl = false; |
| 142 | - | |
| 143 | 139 | /** |
| 144 | 140 | * MQTT 客户端 ID(如果为空,系统将自动生成) |
| 145 | 141 | */ |
| 146 | 142 | private String mqttClientId; |
| 147 | - | |
| 148 | 143 | /** |
| 149 | 144 | * MQTT 订阅的主题 |
| 150 | 145 | */ |
| 151 | 146 | @NotEmpty(message = "MQTT 主题不能为空") |
| 152 | - private List<String> mqttTopics; | |
| 153 | - | |
| 147 | + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; | |
| 154 | 148 | /** |
| 155 | 149 | * 默认 QoS 级别 |
| 156 | 150 | * <p> |
| ... | ... | @@ -164,12 +158,12 @@ public class IotGatewayProperties { |
| 164 | 158 | * 连接超时时间(秒) |
| 165 | 159 | */ |
| 166 | 160 | private Integer connectTimeoutSeconds = 10; |
| 167 | - | |
| 168 | 161 | /** |
| 169 | 162 | * 重连延迟时间(毫秒) |
| 170 | 163 | */ |
| 171 | 164 | private Long reconnectDelayMs = 5000L; |
| 172 | 165 | |
| 166 | + // TODO @haohao:貌似可以通过配置文件 + el 表达式;尽量还是配置文件; | |
| 173 | 167 | /** |
| 174 | 168 | * 获取 MQTT 客户端 ID,如果未配置则自动生成 |
| 175 | 169 | * | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
| ... | ... | @@ -35,6 +35,11 @@ public class IotMqttUpstreamProtocol { |
| 35 | 35 | |
| 36 | 36 | private final IotGatewayProperties.EmqxProperties emqxProperties; |
| 37 | 37 | |
| 38 | + /** | |
| 39 | + * 服务运行状态标志 | |
| 40 | + */ | |
| 41 | + private volatile boolean isRunning = false; | |
| 42 | + | |
| 38 | 43 | private Vertx vertx; |
| 39 | 44 | |
| 40 | 45 | @Getter |
| ... | ... | @@ -47,11 +52,6 @@ public class IotMqttUpstreamProtocol { |
| 47 | 52 | // HTTP 认证服务相关 |
| 48 | 53 | private HttpServer httpAuthServer; |
| 49 | 54 | |
| 50 | - /** | |
| 51 | - * 服务运行状态标志 | |
| 52 | - */ | |
| 53 | - private volatile boolean isRunning = false; | |
| 54 | - | |
| 55 | 55 | public IotMqttUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties) { |
| 56 | 56 | this.emqxProperties = emqxProperties; |
| 57 | 57 | this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); |
| ... | ... | @@ -66,13 +66,12 @@ public class IotMqttUpstreamProtocol { |
| 66 | 66 | log.info("[start][启动 MQTT 协议服务]"); |
| 67 | 67 | |
| 68 | 68 | try { |
| 69 | - // 1. 创建共享的 Vertx 实例 | |
| 70 | 69 | this.vertx = Vertx.vertx(); |
| 71 | 70 | |
| 72 | - // 2. 启动 HTTP 认证服务 | |
| 71 | + // 1. 启动 HTTP 认证服务 | |
| 73 | 72 | startHttpAuthServer(); |
| 74 | 73 | |
| 75 | - // 3. 启动 MQTT 客户端 | |
| 74 | + // 2. 启动 MQTT 客户端 | |
| 76 | 75 | startMqttClient(); |
| 77 | 76 | |
| 78 | 77 | isRunning = true; |
| ... | ... | @@ -119,16 +118,15 @@ public class IotMqttUpstreamProtocol { |
| 119 | 118 | private void startHttpAuthServer() { |
| 120 | 119 | log.info("[startHttpAuthServer][启动 HTTP 认证服务]"); |
| 121 | 120 | |
| 122 | - // 创建路由 | |
| 121 | + // 1.1 创建路由 | |
| 123 | 122 | Router router = Router.router(vertx); |
| 124 | 123 | router.route().handler(BodyHandler.create()); |
| 125 | - | |
| 126 | - // 创建认证处理器 | |
| 124 | + // 1.2 创建认证处理器 | |
| 127 | 125 | IotMqttHttpAuthHandler authHandler = new IotMqttHttpAuthHandler(this); |
| 128 | 126 | router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(authHandler::handleAuth); |
| 129 | 127 | router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(authHandler::handleEvent); |
| 130 | 128 | |
| 131 | - // 启动 HTTP 服务器 | |
| 129 | + // 2. 启动 HTTP 服务器 | |
| 132 | 130 | int authPort = emqxProperties.getHttpAuthPort(); |
| 133 | 131 | try { |
| 134 | 132 | httpAuthServer = vertx.createHttpServer() |
| ... | ... | @@ -172,7 +170,7 @@ public class IotMqttUpstreamProtocol { |
| 172 | 170 | createMqttClient(); |
| 173 | 171 | |
| 174 | 172 | // 3. 连接 MQTT Broker(异步连接,不会抛出异常) |
| 175 | - connectMqtt(); | |
| 173 | + connectMqtt(false); | |
| 176 | 174 | |
| 177 | 175 | log.info("[startMqttClient][MQTT 客户端启动完成,正在异步连接中...]"); |
| 178 | 176 | } catch (Exception e) { |
| ... | ... | @@ -213,13 +211,6 @@ public class IotMqttUpstreamProtocol { |
| 213 | 211 | |
| 214 | 212 | /** |
| 215 | 213 | * 连接 MQTT Broker 并订阅主题 |
| 216 | - */ | |
| 217 | - private void connectMqtt() { | |
| 218 | - connectMqtt(false); | |
| 219 | - } | |
| 220 | - | |
| 221 | - /** | |
| 222 | - * 连接 MQTT Broker 并订阅主题 | |
| 223 | 214 | * |
| 224 | 215 | * @param isReconnect 是否为重连 |
| 225 | 216 | */ |
| ... | ... | @@ -227,6 +218,7 @@ public class IotMqttUpstreamProtocol { |
| 227 | 218 | // 1. 参数校验 |
| 228 | 219 | String host = emqxProperties.getMqttHost(); |
| 229 | 220 | Integer port = emqxProperties.getMqttPort(); |
| 221 | + // TODO @haohao:这些参数校验,交给 validator; | |
| 230 | 222 | if (StrUtil.isBlank(host)) { |
| 231 | 223 | log.error("[connectMqtt][MQTT Host 为空, 无法连接]"); |
| 232 | 224 | throw new IllegalArgumentException("MQTT Host 不能为空"); |
| ... | ... | @@ -246,6 +238,7 @@ public class IotMqttUpstreamProtocol { |
| 246 | 238 | |
| 247 | 239 | // 2. 异步连接 |
| 248 | 240 | mqttClient.connect(port, host, connectResult -> { |
| 241 | + // TODO @haohao:if return,减少括号哈; | |
| 249 | 242 | if (connectResult.succeeded()) { |
| 250 | 243 | if (isReconnect) { |
| 251 | 244 | log.info("[connectMqtt][MQTT 客户端重连成功, host: {}, port: {}]", host, port); |
| ... | ... | @@ -253,16 +246,15 @@ public class IotMqttUpstreamProtocol { |
| 253 | 246 | log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port); |
| 254 | 247 | } |
| 255 | 248 | |
| 256 | - // 3. 设置处理器 | |
| 249 | + // 设置处理器 | |
| 257 | 250 | setupMqttHandlers(); |
| 258 | - | |
| 259 | - // 4. 订阅主题 | |
| 251 | + // 订阅主题 | |
| 260 | 252 | subscribeToTopics(); |
| 261 | - | |
| 262 | 253 | } else { |
| 263 | 254 | log.error("[connectMqtt][连接 MQTT Broker 失败, host: {}, port: {}, isReconnect: {}]", |
| 264 | 255 | host, port, isReconnect, connectResult.cause()); |
| 265 | 256 | |
| 257 | + // TODO @haohao:体感上,是不是首次必须连接成功?类似 mysql;首次要连接上,然后后续可以重连; | |
| 266 | 258 | if (!isReconnect) { |
| 267 | 259 | // 首次连接失败时,也要尝试重连 |
| 268 | 260 | log.warn("[connectMqtt][首次连接失败,将开始重连机制]"); |
| ... | ... | @@ -279,14 +271,11 @@ public class IotMqttUpstreamProtocol { |
| 279 | 271 | * 创建 MQTT 客户端 |
| 280 | 272 | */ |
| 281 | 273 | private void createMqttClient() { |
| 282 | - log.debug("[createMqttClient][创建 MQTT 客户端, clientId: {}]", emqxProperties.getMqttClientId()); | |
| 283 | - | |
| 284 | 274 | MqttClientOptions options = new MqttClientOptions() |
| 285 | 275 | .setClientId(emqxProperties.getMqttClientId()) |
| 286 | 276 | .setUsername(emqxProperties.getMqttUsername()) |
| 287 | 277 | .setPassword(emqxProperties.getMqttPassword()) |
| 288 | 278 | .setSsl(emqxProperties.getMqttSsl()); |
| 289 | - | |
| 290 | 279 | this.mqttClient = MqttClient.create(vertx, options); |
| 291 | 280 | } |
| 292 | 281 | |
| ... | ... | @@ -294,6 +283,7 @@ public class IotMqttUpstreamProtocol { |
| 294 | 283 | * 设置 MQTT 处理器 |
| 295 | 284 | */ |
| 296 | 285 | private void setupMqttHandlers() { |
| 286 | + // TODO @haohao:mqttClient 一定非空; | |
| 297 | 287 | if (mqttClient == null) { |
| 298 | 288 | log.warn("[setupMqttHandlers][MQTT 客户端为空,跳过处理器设置]"); |
| 299 | 289 | return; |
| ... | ... | @@ -311,6 +301,7 @@ public class IotMqttUpstreamProtocol { |
| 311 | 301 | }); |
| 312 | 302 | |
| 313 | 303 | // 设置消息处理器 |
| 304 | + // TODO @haohao:upstreamHandler 一定非空; | |
| 314 | 305 | if (upstreamHandler != null) { |
| 315 | 306 | mqttClient.publishHandler(upstreamHandler::handle); |
| 316 | 307 | log.debug("[setupMqttHandlers][MQTT 消息处理器设置完成]"); |
| ... | ... | @@ -328,7 +319,6 @@ public class IotMqttUpstreamProtocol { |
| 328 | 319 | log.warn("[subscribeToTopics][订阅主题列表为空, 跳过订阅]"); |
| 329 | 320 | return; |
| 330 | 321 | } |
| 331 | - | |
| 332 | 322 | if (mqttClient == null || !mqttClient.isConnected()) { |
| 333 | 323 | log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); |
| 334 | 324 | return; |
| ... | ... | @@ -337,10 +327,12 @@ public class IotMqttUpstreamProtocol { |
| 337 | 327 | int qos = emqxProperties.getMqttQos(); |
| 338 | 328 | log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos); |
| 339 | 329 | |
| 330 | + // TODO @haohao:使用 atomicinteger 会更合适; | |
| 340 | 331 | int[] successCount = { 0 }; // 使用数组以便在 lambda 中修改 |
| 341 | 332 | int[] failCount = { 0 }; |
| 342 | 333 | |
| 343 | 334 | for (String topic : topicList) { |
| 335 | + // TODO @haohao:MqttClient subscribe(Map<String, Integer> topics, 是不是更简洁哈; | |
| 344 | 336 | mqttClient.subscribe(topic, qos, subscribeResult -> { |
| 345 | 337 | if (subscribeResult.succeeded()) { |
| 346 | 338 | successCount[0]++; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java
| ... | ... | @@ -15,7 +15,7 @@ import lombok.extern.slf4j.Slf4j; |
| 15 | 15 | /** |
| 16 | 16 | * IoT 网关 MQTT 下行消息处理器 |
| 17 | 17 | * <p> |
| 18 | - * 从消息总线接收到下行消息,然后发布到 MQTT Broker | |
| 18 | + * 从消息总线接收到下行消息,然后发布到 MQTT Broker,从而被设备所接收 | |
| 19 | 19 | * |
| 20 | 20 | * @author 芋道源码 |
| 21 | 21 | */ |
| ... | ... | @@ -23,7 +23,9 @@ import lombok.extern.slf4j.Slf4j; |
| 23 | 23 | public class IotMqttDownstreamHandler { |
| 24 | 24 | |
| 25 | 25 | private final IotMqttUpstreamProtocol protocol; |
| 26 | + | |
| 26 | 27 | private final IotDeviceService deviceService; |
| 28 | + | |
| 27 | 29 | private final IotDeviceMessageService deviceMessageService; |
| 28 | 30 | |
| 29 | 31 | public IotMqttDownstreamHandler(IotMqttUpstreamProtocol protocol) { |
| ... | ... | @@ -41,25 +43,24 @@ public class IotMqttDownstreamHandler { |
| 41 | 43 | // 1. 获取设备信息(使用缓存) |
| 42 | 44 | IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); |
| 43 | 45 | if (deviceInfo == null) { |
| 44 | - log.warn("[handle][设备信息不存在, deviceId: {}]", message.getDeviceId()); | |
| 46 | + log.error("[handle][设备信息({})不存在]", message.getDeviceId()); | |
| 45 | 47 | return; |
| 46 | 48 | } |
| 47 | 49 | |
| 48 | - // 2. 根据方法构建主题 | |
| 50 | + // 2.1 根据方法构建主题 | |
| 49 | 51 | String topic = buildTopicByMethod(message.getMethod(), deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 50 | 52 | if (StrUtil.isBlank(topic)) { |
| 51 | 53 | log.warn("[handle][未知的消息方法: {}]", message.getMethod()); |
| 52 | 54 | return; |
| 53 | 55 | } |
| 54 | - | |
| 55 | - // 3. 构建载荷 | |
| 56 | + // 2.2 构建载荷 | |
| 57 | + // TODO @haohao:这里是不是 encode 就可以发拉?因为本身就 json 化了。 | |
| 56 | 58 | JSONObject payload = buildDownstreamPayload(message); |
| 57 | - | |
| 58 | - // 4. 发布消息 | |
| 59 | + // 2.3 发布消息 | |
| 59 | 60 | protocol.publishMessage(topic, payload.toString()); |
| 60 | - log.debug("[handle][发布下行消息成功, method: {}, topic: {}]", message.getMethod(), topic); | |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | + // TODO @haohao:这个是不是也可以计算;IotDeviceMessageUtils 的 isReplyMessage;这样就直接生成了; | |
| 63 | 64 | /** |
| 64 | 65 | * 根据方法构建主题 |
| 65 | 66 | * | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
| ... | ... | @@ -35,12 +35,10 @@ public class IotMqttHttpAuthHandler { |
| 35 | 35 | * 认证允许结果 |
| 36 | 36 | */ |
| 37 | 37 | private static final String RESULT_ALLOW = "allow"; |
| 38 | - | |
| 39 | 38 | /** |
| 40 | 39 | * 认证拒绝结果 |
| 41 | 40 | */ |
| 42 | 41 | private static final String RESULT_DENY = "deny"; |
| 43 | - | |
| 44 | 42 | /** |
| 45 | 43 | * 认证忽略结果 |
| 46 | 44 | */ |
| ... | ... | @@ -53,14 +51,11 @@ public class IotMqttHttpAuthHandler { |
| 53 | 51 | private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; |
| 54 | 52 | |
| 55 | 53 | private final IotMqttUpstreamProtocol protocol; |
| 54 | + | |
| 56 | 55 | private final IotDeviceMessageService deviceMessageService; |
| 56 | + | |
| 57 | 57 | private final IotDeviceCommonApi deviceApi; |
| 58 | 58 | |
| 59 | - /** | |
| 60 | - * 构造器 | |
| 61 | - * | |
| 62 | - * @param protocol MQTT 协议实例 | |
| 63 | - */ | |
| 64 | 59 | public IotMqttHttpAuthHandler(IotMqttUpstreamProtocol protocol) { |
| 65 | 60 | this.protocol = protocol; |
| 66 | 61 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| ... | ... | @@ -72,19 +67,15 @@ public class IotMqttHttpAuthHandler { |
| 72 | 67 | */ |
| 73 | 68 | public void handleAuth(RoutingContext context) { |
| 74 | 69 | try { |
| 75 | - // 解析请求体 | |
| 70 | + // 参数校验 | |
| 76 | 71 | JsonObject body = parseRequestBody(context); |
| 77 | 72 | if (body == null) { |
| 78 | 73 | return; |
| 79 | 74 | } |
| 80 | - | |
| 81 | 75 | String clientId = body.getString("clientid"); |
| 82 | 76 | String username = body.getString("username"); |
| 83 | 77 | String password = body.getString("password"); |
| 84 | - | |
| 85 | 78 | log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username); |
| 86 | - | |
| 87 | - // 参数校验 | |
| 88 | 79 | if (StrUtil.hasEmpty(clientId, username, password)) { |
| 89 | 80 | log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username); |
| 90 | 81 | sendAuthResponse(context, RESULT_DENY, false, "认证参数不完整"); |
| ... | ... | @@ -94,13 +85,13 @@ public class IotMqttHttpAuthHandler { |
| 94 | 85 | // 执行设备认证 |
| 95 | 86 | boolean authResult = performDeviceAuth(clientId, username, password); |
| 96 | 87 | if (authResult) { |
| 88 | + // TODO @haohao:是不是两条 info,直接打认证结果:authResult | |
| 97 | 89 | log.info("[handleAuth][设备认证成功: {}]", username); |
| 98 | 90 | sendAuthResponse(context, RESULT_ALLOW, false, null); |
| 99 | 91 | } else { |
| 100 | 92 | log.info("[handleAuth][设备认证失败: {}]", username); |
| 101 | 93 | sendAuthResponse(context, RESULT_DENY, false, DEVICE_AUTH_FAIL.getMsg()); |
| 102 | 94 | } |
| 103 | - | |
| 104 | 95 | } catch (Exception e) { |
| 105 | 96 | log.error("[handleAuth][设备认证异常]", e); |
| 106 | 97 | sendAuthResponse(context, RESULT_IGNORE, false, "认证服务异常"); |
| ... | ... | @@ -119,10 +110,8 @@ public class IotMqttHttpAuthHandler { |
| 119 | 110 | if (body == null) { |
| 120 | 111 | return; |
| 121 | 112 | } |
| 122 | - | |
| 123 | 113 | String event = body.getString("event"); |
| 124 | 114 | String username = body.getString("username"); |
| 125 | - | |
| 126 | 115 | log.debug("[handleEvent][收到事件: {} - {}]", event, username); |
| 127 | 116 | |
| 128 | 117 | // 根据事件类型进行分发处理 |
| ... | ... | @@ -138,12 +127,12 @@ public class IotMqttHttpAuthHandler { |
| 138 | 127 | break; |
| 139 | 128 | } |
| 140 | 129 | |
| 141 | - // EMQX Webhook 只需要200状态码,无需响应体 | |
| 130 | + // EMQX Webhook 只需要 200 状态码,无需响应体 | |
| 142 | 131 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 143 | - | |
| 144 | 132 | } catch (Exception e) { |
| 133 | + // TODO @haohao:body 可以打印出来 | |
| 145 | 134 | log.error("[handleEvent][事件处理失败]", e); |
| 146 | - // 即使处理失败,也返回200避免EMQX重试 | |
| 135 | + // 即使处理失败,也返回 200 避免EMQX重试 | |
| 147 | 136 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 148 | 137 | } |
| 149 | 138 | } |
| ... | ... | @@ -163,7 +152,6 @@ public class IotMqttHttpAuthHandler { |
| 163 | 152 | private void handleClientDisconnected(JsonObject body) { |
| 164 | 153 | String username = body.getString("username"); |
| 165 | 154 | String reason = body.getString("reason"); |
| 166 | - | |
| 167 | 155 | log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); |
| 168 | 156 | handleDeviceStateChange(username, false); |
| 169 | 157 | } |
| ... | ... | @@ -184,6 +172,7 @@ public class IotMqttHttpAuthHandler { |
| 184 | 172 | } |
| 185 | 173 | return body; |
| 186 | 174 | } catch (Exception e) { |
| 175 | + // TODO @haohao:最好把 body 打印出来; | |
| 187 | 176 | log.error("[parseRequestBody][解析请求体失败]", e); |
| 188 | 177 | sendAuthResponse(context, RESULT_IGNORE, false, "请求体格式错误"); |
| 189 | 178 | return null; |
| ... | ... | @@ -201,10 +190,7 @@ public class IotMqttHttpAuthHandler { |
| 201 | 190 | private boolean performDeviceAuth(String clientId, String username, String password) { |
| 202 | 191 | try { |
| 203 | 192 | CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() |
| 204 | - .setClientId(clientId) | |
| 205 | - .setUsername(username) | |
| 206 | - .setPassword(password)); | |
| 207 | - | |
| 193 | + .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 208 | 194 | result.checkError(); |
| 209 | 195 | return BooleanUtil.isTrue(result.getData()); |
| 210 | 196 | } catch (Exception e) { |
| ... | ... | @@ -220,11 +206,10 @@ public class IotMqttHttpAuthHandler { |
| 220 | 206 | * @param online 是否在线 |
| 221 | 207 | */ |
| 222 | 208 | private void handleDeviceStateChange(String username, boolean online) { |
| 209 | + // 解析设备信息 | |
| 223 | 210 | if (StrUtil.isEmpty(username) || "undefined".equals(username)) { |
| 224 | 211 | return; |
| 225 | 212 | } |
| 226 | - | |
| 227 | - // 解析设备信息 | |
| 228 | 213 | IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); |
| 229 | 214 | if (deviceInfo == null) { |
| 230 | 215 | log.debug("[handleDeviceStateChange][跳过非设备连接: {}]", username); |
| ... | ... | @@ -232,6 +217,7 @@ public class IotMqttHttpAuthHandler { |
| 232 | 217 | } |
| 233 | 218 | |
| 234 | 219 | try { |
| 220 | + // TODO @haohao:serverId 获取非空,可以忽略掉; | |
| 235 | 221 | String serverId = protocol.getServerId(); |
| 236 | 222 | if (StrUtil.isEmpty(serverId)) { |
| 237 | 223 | log.error("[handleDeviceStateChange][获取服务器ID失败]"); |
| ... | ... | @@ -241,15 +227,14 @@ public class IotMqttHttpAuthHandler { |
| 241 | 227 | // 构建设备状态消息 |
| 242 | 228 | IotDeviceMessage message = online ? IotDeviceMessage.buildStateOnline() |
| 243 | 229 | : IotDeviceMessage.buildStateOffline(); |
| 244 | - | |
| 245 | 230 | // 发送消息到消息总线 |
| 246 | 231 | deviceMessageService.sendDeviceMessage(message, |
| 247 | 232 | deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); |
| 248 | 233 | |
| 234 | + // TODO @haohao:online 不用翻译 | |
| 249 | 235 | log.info("[handleDeviceStateChange][设备状态更新: {}/{} -> {}]", |
| 250 | 236 | deviceInfo.getProductKey(), deviceInfo.getDeviceName(), |
| 251 | 237 | online ? "在线" : "离线"); |
| 252 | - | |
| 253 | 238 | } catch (Exception e) { |
| 254 | 239 | log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, e); |
| 255 | 240 | } |
| ... | ... | @@ -277,10 +262,6 @@ public class IotMqttHttpAuthHandler { |
| 277 | 262 | // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); |
| 278 | 263 | |
| 279 | 264 | // 记录详细的响应日志(message仅用于日志,不返回给EMQX) |
| 280 | - if (StrUtil.isNotEmpty(message)) { | |
| 281 | - log.debug("[sendAuthResponse][响应详情: result={}, message={}]", result, message); | |
| 282 | - } | |
| 283 | - | |
| 284 | 265 | context.response() |
| 285 | 266 | .setStatusCode(SUCCESS_STATUS_CODE) |
| 286 | 267 | .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
| ... | ... | @@ -20,6 +20,7 @@ import java.util.Arrays; |
| 20 | 20 | public class IotMqttUpstreamHandler { |
| 21 | 21 | |
| 22 | 22 | private final IotDeviceMessageService deviceMessageService; |
| 23 | + | |
| 23 | 24 | private final String serverId; |
| 24 | 25 | |
| 25 | 26 | public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol) { |
| ... | ... | @@ -30,56 +31,50 @@ public class IotMqttUpstreamHandler { |
| 30 | 31 | /** |
| 31 | 32 | * 处理 MQTT 发布消息 |
| 32 | 33 | */ |
| 33 | - public void handle(MqttPublishMessage message) { | |
| 34 | - String topic = message.topicName(); | |
| 35 | - byte[] payload = message.payload().getBytes(); | |
| 36 | - | |
| 37 | - log.debug("[handle][收到 MQTT 消息, topic: {}]", topic); | |
| 38 | - | |
| 34 | + public void handle(MqttPublishMessage mqttMessage) { | |
| 35 | + String topic = mqttMessage.topicName(); | |
| 36 | + byte[] payload = mqttMessage.payload().getBytes(); | |
| 39 | 37 | try { |
| 40 | 38 | // 1. 前置校验 |
| 41 | 39 | if (StrUtil.isBlank(topic)) { |
| 42 | 40 | log.warn("[handle][主题为空, 忽略消息]"); |
| 43 | 41 | return; |
| 44 | 42 | } |
| 45 | - // 注意:payload 可以为空 | |
| 46 | 43 | |
| 47 | - // 2. 识别并验证消息类型 | |
| 44 | + // 2.1 识别并验证消息类型 | |
| 48 | 45 | String messageType = getMessageType(topic); |
| 46 | + // TODO @haohao:可以使用 hutool 的,它的字符串拼接更简单; | |
| 49 | 47 | Assert.notNull(messageType, String.format("未知的消息类型, topic(%s)", topic)); |
| 50 | - log.debug("[handle][接收到上行消息({}), topic: {}]", messageType, topic); | |
| 51 | - | |
| 52 | - // 3. 解析主题,获取 productKey 和 deviceName | |
| 48 | + // 2.2 解析主题,获取 productKey 和 deviceName | |
| 49 | + // TODO @haohao:体感 getMessageType 和下面,都 split;是不是一次就 ok 拉;1)split 掉;2)2、3 位置是 productKey、deviceName;3)4 开始还是 method | |
| 53 | 50 | String[] topicParts = topic.split("/"); |
| 54 | 51 | if (topicParts.length < 4) { |
| 55 | - log.warn("[handle][主题格式不正确,无法解析 productKey 和 deviceName][topic: {}]", topic); | |
| 52 | + log.warn("[handle][topic({}) 格式不正确,无法解析 productKey 和 deviceName]", topic); | |
| 56 | 53 | return; |
| 57 | 54 | } |
| 58 | 55 | String productKey = topicParts[2]; |
| 59 | 56 | String deviceName = topicParts[3]; |
| 57 | + // TODO @haohao:是不是要判断,部分为空,就不行呀; | |
| 60 | 58 | if (StrUtil.isAllBlank(productKey, deviceName)) { |
| 61 | - log.warn("[handle][主题中 productKey 或 deviceName 为空][topic: {}]", topic); | |
| 59 | + log.warn("[handle][topic({}) 格式不正确,productKey 和 deviceName 部分为空]", topic); | |
| 62 | 60 | return; |
| 63 | 61 | } |
| 64 | 62 | |
| 65 | - // 4. 解码消息 | |
| 66 | - IotDeviceMessage deviceMessage = deviceMessageService.decodeDeviceMessage( | |
| 67 | - payload, productKey, deviceName); | |
| 68 | - if (deviceMessage == null) { | |
| 69 | - log.warn("[handle][消息解码失败][topic: {}]", topic); | |
| 63 | + // 3. 解码消息 | |
| 64 | + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 65 | + if (message == null) { | |
| 66 | + log.warn("[handle][topic({}) payload({}) 消息解码失败", topic, new String(payload)); | |
| 70 | 67 | return; |
| 71 | 68 | } |
| 72 | 69 | |
| 73 | - // 5. 发送消息到队列 | |
| 74 | - deviceMessageService.sendDeviceMessage(deviceMessage, productKey, deviceName, serverId); | |
| 75 | - | |
| 76 | - // 6. 记录成功日志 | |
| 77 | - log.debug("[handle][处理上行消息({})成功, topic: {}]", messageType, topic); | |
| 70 | + // 4. 发送消息到队列 | |
| 71 | + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 78 | 72 | } catch (Exception e) { |
| 79 | - log.error("[handle][处理 MQTT 消息失败][topic: {}][payload: {}]", topic, new String(payload), e); | |
| 73 | + log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); | |
| 80 | 74 | } |
| 81 | 75 | } |
| 82 | 76 | |
| 77 | + // TODO @haohao:是不是 getMethodFromTopic? | |
| 83 | 78 | /** |
| 84 | 79 | * 从主题中,获得消息类型 |
| 85 | 80 | * |
| ... | ... | @@ -89,9 +84,9 @@ public class IotMqttUpstreamHandler { |
| 89 | 84 | private String getMessageType(String topic) { |
| 90 | 85 | String[] topicParts = topic.split("/"); |
| 91 | 86 | // 约定:topic 第 4 个部分开始为消息类型 |
| 92 | - // 例如:/sys/{productKey}/{deviceName}/thing/property/post -> | |
| 93 | - // thing/property/post | |
| 87 | + // 例如:/sys/{productKey}/{deviceName}/thing/property/post -> thing/property/post | |
| 94 | 88 | if (topicParts.length > 4) { |
| 89 | + // TODO @haohao:是不是 subString 前 3 个,性能更好; | |
| 95 | 90 | return String.join("/", Arrays.copyOfRange(topicParts, 4, topicParts.length)); |
| 96 | 91 | } |
| 97 | 92 | return topicParts[topicParts.length - 1]; | ... | ... |