Commit 1e2dc281e3ad2f7064eb0a857e2a0bc9ece9733a
1 parent
c105ecea
feat(iot):【协议改造】mqtt 初步改造(20%)
Showing
16 changed files
with
964 additions
and
784 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -5,12 +5,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | |
| 12 | 8 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; |
| 13 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 14 | 9 | import io.vertx.core.Vertx; |
| 15 | 10 | import lombok.extern.slf4j.Slf4j; |
| 16 | 11 | import org.springframework.beans.factory.annotation.Qualifier; |
| ... | ... | @@ -67,41 +62,4 @@ public class IotGatewayConfiguration { |
| 67 | 62 | } |
| 68 | 63 | } |
| 69 | 64 | |
| 70 | - /** | |
| 71 | - * IoT 网关 MQTT 协议配置类 | |
| 72 | - */ | |
| 73 | - @Configuration | |
| 74 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") | |
| 75 | - @Slf4j | |
| 76 | - public static class MqttProtocolConfiguration { | |
| 77 | - | |
| 78 | - @Bean(name = "mqttVertx", destroyMethod = "close") | |
| 79 | - public Vertx mqttVertx() { | |
| 80 | - return Vertx.vertx(); | |
| 81 | - } | |
| 82 | - | |
| 83 | - @Bean | |
| 84 | - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 85 | - IotDeviceMessageService messageService, | |
| 86 | - IotMqttConnectionManager connectionManager, | |
| 87 | - @Qualifier("mqttVertx") Vertx mqttVertx) { | |
| 88 | - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, | |
| 89 | - connectionManager, mqttVertx); | |
| 90 | - } | |
| 91 | - | |
| 92 | - @Bean | |
| 93 | - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, | |
| 94 | - IotMqttConnectionManager connectionManager) { | |
| 95 | - return new IotMqttDownstreamHandler(messageService, connectionManager); | |
| 96 | - } | |
| 97 | - | |
| 98 | - @Bean | |
| 99 | - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, | |
| 100 | - IotMqttDownstreamHandler downstreamHandler, | |
| 101 | - IotMessageBus messageBus) { | |
| 102 | - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); | |
| 103 | - } | |
| 104 | - | |
| 105 | - } | |
| 106 | - | |
| 107 | 65 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; |
| 9 | -import io.vertx.core.net.KeyCertOptions; | |
| 10 | -import io.vertx.core.net.TrustOptions; | |
| 11 | 9 | import jakarta.validation.Valid; |
| 12 | 10 | import jakarta.validation.constraints.NotEmpty; |
| 13 | 11 | import jakarta.validation.constraints.NotNull; |
| ... | ... | @@ -87,11 +85,6 @@ public class IotGatewayProperties { |
| 87 | 85 | */ |
| 88 | 86 | private EmqxProperties emqx; |
| 89 | 87 | |
| 90 | - /** | |
| 91 | - * MQTT 组件配置 | |
| 92 | - */ | |
| 93 | - private MqttProperties mqtt; | |
| 94 | - | |
| 95 | 88 | } |
| 96 | 89 | |
| 97 | 90 | @Data |
| ... | ... | @@ -263,79 +256,6 @@ public class IotGatewayProperties { |
| 263 | 256 | |
| 264 | 257 | } |
| 265 | 258 | |
| 266 | - @Data | |
| 267 | - public static class MqttProperties { | |
| 268 | - | |
| 269 | - /** | |
| 270 | - * 是否开启 | |
| 271 | - */ | |
| 272 | - @NotNull(message = "是否开启不能为空") | |
| 273 | - private Boolean enabled; | |
| 274 | - | |
| 275 | - /** | |
| 276 | - * 服务器端口 | |
| 277 | - */ | |
| 278 | - private Integer port = 1883; | |
| 279 | - | |
| 280 | - /** | |
| 281 | - * 最大消息大小(字节) | |
| 282 | - */ | |
| 283 | - private Integer maxMessageSize = 8192; | |
| 284 | - | |
| 285 | - /** | |
| 286 | - * 连接超时时间(秒) | |
| 287 | - */ | |
| 288 | - private Integer connectTimeoutSeconds = 60; | |
| 289 | - /** | |
| 290 | - * 保持连接超时时间(秒) | |
| 291 | - */ | |
| 292 | - private Integer keepAliveTimeoutSeconds = 300; | |
| 293 | - | |
| 294 | - // NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低) | |
| 295 | - /** | |
| 296 | - * 是否启用 SSL | |
| 297 | - */ | |
| 298 | - private Boolean sslEnabled = false; | |
| 299 | - /** | |
| 300 | - * SSL 配置 | |
| 301 | - */ | |
| 302 | - private SslOptions sslOptions = new SslOptions(); | |
| 303 | - | |
| 304 | - /** | |
| 305 | - * SSL 配置选项 | |
| 306 | - */ | |
| 307 | - @Data | |
| 308 | - public static class SslOptions { | |
| 309 | - | |
| 310 | - /** | |
| 311 | - * 密钥证书选项 | |
| 312 | - */ | |
| 313 | - private KeyCertOptions keyCertOptions; | |
| 314 | - /** | |
| 315 | - * 信任选项 | |
| 316 | - */ | |
| 317 | - private TrustOptions trustOptions; | |
| 318 | - /** | |
| 319 | - * SSL 证书路径 | |
| 320 | - */ | |
| 321 | - private String certPath; | |
| 322 | - /** | |
| 323 | - * SSL 私钥路径 | |
| 324 | - */ | |
| 325 | - private String keyPath; | |
| 326 | - /** | |
| 327 | - * 信任存储路径 | |
| 328 | - */ | |
| 329 | - private String trustStorePath; | |
| 330 | - /** | |
| 331 | - * 信任存储密码 | |
| 332 | - */ | |
| 333 | - private String trustStorePassword; | |
| 334 | - | |
| 335 | - } | |
| 336 | - | |
| 337 | - } | |
| 338 | - | |
| 339 | 259 | // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整 |
| 340 | 260 | /** |
| 341 | 261 | * 协议实例配置 |
| ... | ... | @@ -376,6 +296,8 @@ public class IotGatewayProperties { |
| 376 | 296 | */ |
| 377 | 297 | private String serialize; |
| 378 | 298 | |
| 299 | + // ========== 各协议配置 ========== | |
| 300 | + | |
| 379 | 301 | /** |
| 380 | 302 | * HTTP 协议配置 |
| 381 | 303 | */ |
| ... | ... | @@ -406,6 +328,12 @@ public class IotGatewayProperties { |
| 406 | 328 | @Valid |
| 407 | 329 | private IotWebSocketConfig websocket; |
| 408 | 330 | |
| 331 | + /** | |
| 332 | + * MQTT 协议配置 | |
| 333 | + */ | |
| 334 | + @Valid | |
| 335 | + private IotMqttConfig mqtt; | |
| 336 | + | |
| 409 | 337 | } |
| 410 | 338 | |
| 411 | 339 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
| ... | ... | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | |
| 9 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; |
| ... | ... | @@ -106,6 +107,8 @@ public class IotProtocolManager implements SmartLifecycle { |
| 106 | 107 | return createCoapProtocol(config); |
| 107 | 108 | case WEBSOCKET: |
| 108 | 109 | return createWebSocketProtocol(config); |
| 110 | + case MQTT: | |
| 111 | + return createMqttProtocol(config); | |
| 109 | 112 | default: |
| 110 | 113 | throw new IllegalArgumentException(String.format( |
| 111 | 114 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| ... | ... | @@ -162,4 +165,14 @@ public class IotProtocolManager implements SmartLifecycle { |
| 162 | 165 | return new IotWebSocketProtocol(config); |
| 163 | 166 | } |
| 164 | 167 | |
| 168 | + /** | |
| 169 | + * 创建 MQTT 协议实例 | |
| 170 | + * | |
| 171 | + * @param config 协议实例配置 | |
| 172 | + * @return MQTT 协议实例 | |
| 173 | + */ | |
| 174 | + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 175 | + return new IotMqttProtocol(config); | |
| 176 | + } | |
| 177 | + | |
| 165 | 178 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +// TODO @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 | |
| 6 | +/** | |
| 7 | + * IoT 网关 MQTT 协议配置 | |
| 8 | + * | |
| 9 | + * @author 芋道源码 | |
| 10 | + */ | |
| 11 | +@Data | |
| 12 | +public class IotMqttConfig { | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * 是否启用 SSL | |
| 16 | + */ | |
| 17 | + private Boolean sslEnabled = false; | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * SSL 证书路径 | |
| 21 | + */ | |
| 22 | + private String sslCertPath; | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * SSL 私钥路径 | |
| 26 | + */ | |
| 27 | + private String sslKeyPath; | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 最大消息大小(字节) | |
| 31 | + */ | |
| 32 | + private Integer maxMessageSize = 8192; | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 连接超时时间(秒) | |
| 36 | + */ | |
| 37 | + private Integer connectTimeoutSeconds = 60; | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * 保持连接超时时间(秒) | |
| 41 | + */ | |
| 42 | + private Integer keepAliveTimeoutSeconds = 300; | |
| 43 | + | |
| 44 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | + | |
| 3 | +import cn.hutool.extra.spring.SpringUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttConnectionHandler; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 17 | +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | |
| 18 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
| 19 | +import io.vertx.core.Vertx; | |
| 20 | +import io.vertx.core.net.PemKeyCertOptions; | |
| 21 | +import io.vertx.mqtt.MqttEndpoint; | |
| 22 | +import io.vertx.mqtt.MqttServer; | |
| 23 | +import io.vertx.mqtt.MqttServerOptions; | |
| 24 | +import io.vertx.mqtt.MqttTopicSubscription; | |
| 25 | +import io.vertx.mqtt.messages.MqttPublishMessage; | |
| 26 | +import lombok.Getter; | |
| 27 | +import lombok.extern.slf4j.Slf4j; | |
| 28 | + | |
| 29 | +import java.util.List; | |
| 30 | +import java.util.stream.Collectors; | |
| 31 | + | |
| 32 | +/** | |
| 33 | + * IoT 网关 MQTT 协议:接收设备上行消息 | |
| 34 | + * | |
| 35 | + * @author 芋道源码 | |
| 36 | + */ | |
| 37 | +@Slf4j | |
| 38 | +public class IotMqttProtocol implements IotProtocol { | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 协议配置 | |
| 42 | + */ | |
| 43 | + private final ProtocolInstanceProperties properties; | |
| 44 | + /** | |
| 45 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 46 | + */ | |
| 47 | + @Getter | |
| 48 | + private final String serverId; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 运行状态 | |
| 52 | + */ | |
| 53 | + @Getter | |
| 54 | + private volatile boolean running = false; | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * Vert.x 实例 | |
| 58 | + */ | |
| 59 | + private Vertx vertx; | |
| 60 | + /** | |
| 61 | + * MQTT 服务器 | |
| 62 | + */ | |
| 63 | + private MqttServer mqttServer; | |
| 64 | + /** | |
| 65 | + * 连接管理器 | |
| 66 | + */ | |
| 67 | + private IotMqttConnectionManager connectionManager; | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * 下行消息订阅者 | |
| 71 | + */ | |
| 72 | + private IotMqttDownstreamSubscriber downstreamSubscriber; | |
| 73 | + | |
| 74 | + // TODO @AI:这个是不是提前创建下?因为是无状态的。 | |
| 75 | + private IotMqttConnectionHandler connectionHandler; | |
| 76 | + private IotMqttRegisterHandler registerHandler; | |
| 77 | + private IotMqttUpstreamHandler upstreamHandler; | |
| 78 | + | |
| 79 | + public IotMqttProtocol(ProtocolInstanceProperties properties) { | |
| 80 | + this.properties = properties; | |
| 81 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 82 | + | |
| 83 | + // TODO @AI:初始化连接器,参考 IotTcpProtocol | |
| 84 | + | |
| 85 | + // TODO @AI:初始化下行消息订阅者,参考 IotTcpProtocol | |
| 86 | + } | |
| 87 | + | |
| 88 | + @Override | |
| 89 | + public String getId() { | |
| 90 | + return properties.getId(); | |
| 91 | + } | |
| 92 | + | |
| 93 | + @Override | |
| 94 | + public IotProtocolTypeEnum getType() { | |
| 95 | + return IotProtocolTypeEnum.MQTT; | |
| 96 | + } | |
| 97 | + | |
| 98 | + // TODO @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 | |
| 99 | + @Override | |
| 100 | + public void start() { | |
| 101 | + if (running) { | |
| 102 | + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId()); | |
| 103 | + return; | |
| 104 | + } | |
| 105 | + | |
| 106 | + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) | |
| 107 | + this.vertx = Vertx.vertx(); | |
| 108 | + | |
| 109 | + // 1.2 创建连接管理器 | |
| 110 | + this.connectionManager = new IotMqttConnectionManager(); | |
| 111 | + | |
| 112 | + // 1.3 初始化 Handler | |
| 113 | + initHandlers(); | |
| 114 | + | |
| 115 | + // 2. 创建服务器选项 | |
| 116 | + IotMqttConfig mqttConfig = properties.getMqtt(); | |
| 117 | + // TODO @AI:default 值,在 IotMqttConfig 处理; | |
| 118 | + MqttServerOptions options = new MqttServerOptions() | |
| 119 | + .setPort(properties.getPort()) | |
| 120 | + .setMaxMessageSize(mqttConfig != null ? mqttConfig.getMaxMessageSize() : 8192) | |
| 121 | + .setTimeoutOnConnect(mqttConfig != null ? mqttConfig.getConnectTimeoutSeconds() : 60); | |
| 122 | + | |
| 123 | + // 3. 配置 SSL(如果启用) | |
| 124 | + if (mqttConfig != null && Boolean.TRUE.equals(mqttConfig.getSslEnabled())) { | |
| 125 | + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | |
| 126 | + .setKeyPath(mqttConfig.getSslKeyPath()) | |
| 127 | + .setCertPath(mqttConfig.getSslCertPath()); | |
| 128 | + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 129 | + } | |
| 130 | + | |
| 131 | + // 4. 创建服务器并设置连接处理器 | |
| 132 | + mqttServer = MqttServer.create(vertx, options); | |
| 133 | + mqttServer.endpointHandler(this::handleEndpoint); | |
| 134 | + | |
| 135 | + // 5. 启动服务器 | |
| 136 | + try { | |
| 137 | + mqttServer.listen().result(); | |
| 138 | + running = true; | |
| 139 | + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", | |
| 140 | + getId(), properties.getPort(), serverId); | |
| 141 | + | |
| 142 | + // 6. 启动下行消息订阅者 | |
| 143 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 144 | + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler( | |
| 145 | + SpringUtil.getBean(IotDeviceMessageService.class), connectionManager); | |
| 146 | + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 147 | + this.downstreamSubscriber.start(); | |
| 148 | + } catch (Exception e) { | |
| 149 | + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); | |
| 150 | + // 启动失败时关闭 Vertx | |
| 151 | + if (vertx != null) { | |
| 152 | + vertx.close(); | |
| 153 | + vertx = null; | |
| 154 | + } | |
| 155 | + throw e; | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 159 | + @Override | |
| 160 | + public void stop() { | |
| 161 | + if (!running) { | |
| 162 | + return; | |
| 163 | + } | |
| 164 | + // 1. 停止下行消息订阅者 | |
| 165 | + if (downstreamSubscriber != null) { | |
| 166 | + try { | |
| 167 | + downstreamSubscriber.stop(); | |
| 168 | + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); | |
| 169 | + } catch (Exception e) { | |
| 170 | + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 171 | + } | |
| 172 | + downstreamSubscriber = null; | |
| 173 | + } | |
| 174 | + | |
| 175 | + // 2.1 关闭 MQTT 服务器 | |
| 176 | + if (mqttServer != null) { | |
| 177 | + try { | |
| 178 | + mqttServer.close().result(); | |
| 179 | + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId()); | |
| 180 | + } catch (Exception e) { | |
| 181 | + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e); | |
| 182 | + } | |
| 183 | + mqttServer = null; | |
| 184 | + } | |
| 185 | + // 2.2 关闭 Vertx 实例 | |
| 186 | + if (vertx != null) { | |
| 187 | + try { | |
| 188 | + vertx.close().result(); | |
| 189 | + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId()); | |
| 190 | + } catch (Exception e) { | |
| 191 | + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e); | |
| 192 | + } | |
| 193 | + vertx = null; | |
| 194 | + } | |
| 195 | + running = false; | |
| 196 | + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); | |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * 初始化 Handler | |
| 201 | + */ | |
| 202 | + private void initHandlers() { | |
| 203 | + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 204 | + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 205 | + this.connectionHandler = new IotMqttConnectionHandler(connectionManager, messageService, deviceApi, serverId); | |
| 206 | + this.registerHandler = new IotMqttRegisterHandler(connectionManager, messageService, deviceApi); | |
| 207 | + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, messageService, serverId); | |
| 208 | + } | |
| 209 | + | |
| 210 | + /** | |
| 211 | + * 处理 MQTT 连接端点 | |
| 212 | + * | |
| 213 | + * @param endpoint MQTT 连接端点 | |
| 214 | + */ | |
| 215 | + private void handleEndpoint(MqttEndpoint endpoint) { | |
| 216 | + String clientId = endpoint.clientIdentifier(); | |
| 217 | + | |
| 218 | + // 1. 委托 connectionHandler 处理连接认证 | |
| 219 | + // TODO @AI:register topic 不需要注册,需要判断下; | |
| 220 | + if (!connectionHandler.handleConnect(endpoint)) { | |
| 221 | + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | |
| 222 | + return; | |
| 223 | + } | |
| 224 | + | |
| 225 | + // 2.1 设置异常和关闭处理器 | |
| 226 | + endpoint.exceptionHandler(ex -> { | |
| 227 | + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {}]", | |
| 228 | + clientId, connectionManager.getEndpointAddress(endpoint)); | |
| 229 | + // TODO @AI:是不是改成 endpoint close 更合适? | |
| 230 | + connectionHandler.cleanupConnection(endpoint); | |
| 231 | + }); | |
| 232 | + endpoint.closeHandler(v -> connectionHandler.cleanupConnection(endpoint)); | |
| 233 | + endpoint.disconnectHandler(v -> { | |
| 234 | + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); | |
| 235 | + connectionHandler.cleanupConnection(endpoint); | |
| 236 | + }); | |
| 237 | + // 2.2 设置心跳处理器 | |
| 238 | + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); | |
| 239 | + | |
| 240 | + // 3.1 设置消息处理器 | |
| 241 | + endpoint.publishHandler(message -> processMessage(endpoint, message)); | |
| 242 | + // 3.2 设置 QoS 2 消息的 PUBREL 处理器 | |
| 243 | + endpoint.publishReleaseHandler(endpoint::publishComplete); | |
| 244 | + | |
| 245 | + // 4.1 设置订阅处理器 | |
| 246 | + endpoint.subscribeHandler(subscribe -> { | |
| 247 | + // TODO @AI:convertList 简化; | |
| 248 | + List<String> topicNames = subscribe.topicSubscriptions().stream() | |
| 249 | + .map(MqttTopicSubscription::topicName) | |
| 250 | + .collect(Collectors.toList()); | |
| 251 | + log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | |
| 252 | + | |
| 253 | + // TODO @AI:convertList 简化; | |
| 254 | + List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream() | |
| 255 | + .map(MqttTopicSubscription::qualityOfService) | |
| 256 | + .collect(Collectors.toList()); | |
| 257 | + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | |
| 258 | + }); | |
| 259 | + // 4.2 设置取消订阅处理器 | |
| 260 | + endpoint.unsubscribeHandler(unsubscribe -> { | |
| 261 | + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); | |
| 262 | + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); | |
| 263 | + }); | |
| 264 | + | |
| 265 | + // 5. 接受连接 | |
| 266 | + endpoint.accept(false); | |
| 267 | + } | |
| 268 | + | |
| 269 | + /** | |
| 270 | + * 处理消息(发布) | |
| 271 | + * | |
| 272 | + * @param endpoint MQTT 连接端点 | |
| 273 | + * @param message 发布消息 | |
| 274 | + */ | |
| 275 | + // TODO @AI:看看要不要一定程度,参考 IotTcpUpstreamHandler 的 processMessage 方法; | |
| 276 | + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { | |
| 277 | + String clientId = endpoint.clientIdentifier(); | |
| 278 | + try { | |
| 279 | + String topic = message.topicName(); | |
| 280 | + byte[] payload = message.payload().getBytes(); | |
| 281 | + | |
| 282 | + // 根据 topic 分发到不同 handler | |
| 283 | + if (registerHandler.isRegisterMessage(topic)) { | |
| 284 | + registerHandler.handleRegister(endpoint, topic, payload); | |
| 285 | + } else { | |
| 286 | + upstreamHandler.handleMessage(endpoint, topic, payload); | |
| 287 | + } | |
| 288 | + | |
| 289 | + // 根据 QoS 级别发送相应的确认消息 | |
| 290 | + handleQoSAck(endpoint, message); | |
| 291 | + } catch (Exception e) { | |
| 292 | + // TODO @AI:异常的时候,直接断开; | |
| 293 | + log.error("[handlePublish][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | |
| 294 | + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | |
| 295 | + connectionHandler.cleanupConnection(endpoint); | |
| 296 | + endpoint.close(); | |
| 297 | + } | |
| 298 | + } | |
| 299 | + | |
| 300 | + /** | |
| 301 | + * 处理 QoS 确认 | |
| 302 | + * | |
| 303 | + * @param endpoint MQTT 连接端点 | |
| 304 | + * @param message 发布消息 | |
| 305 | + */ | |
| 306 | + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) { | |
| 307 | + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | |
| 308 | + // QoS 1: 发送 PUBACK 确认 | |
| 309 | + endpoint.publishAcknowledge(message.messageId()); | |
| 310 | + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { | |
| 311 | + // QoS 2: 发送 PUBREC 确认 | |
| 312 | + endpoint.publishReceived(message.messageId()); | |
| 313 | + } | |
| 314 | + // QoS 0 无需确认 | |
| 315 | + } | |
| 316 | + | |
| 317 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 10 | -import io.vertx.core.Vertx; | |
| 11 | -import io.vertx.mqtt.MqttServer; | |
| 12 | -import io.vertx.mqtt.MqttServerOptions; | |
| 13 | -import jakarta.annotation.PostConstruct; | |
| 14 | -import jakarta.annotation.PreDestroy; | |
| 15 | -import lombok.Getter; | |
| 16 | -import lombok.extern.slf4j.Slf4j; | |
| 17 | - | |
| 18 | -/** | |
| 19 | - * IoT 网关 MQTT 协议:接收设备上行消息 | |
| 20 | - * | |
| 21 | - * @author 芋道源码 | |
| 22 | - */ | |
| 23 | -@Slf4j | |
| 24 | -public class IotMqttUpstreamProtocol implements IotProtocol { | |
| 25 | - | |
| 26 | - private static final String ID = "mqtt"; | |
| 27 | - | |
| 28 | - private volatile boolean running = false; | |
| 29 | - | |
| 30 | - private final IotGatewayProperties.MqttProperties mqttProperties; | |
| 31 | - | |
| 32 | - private final IotDeviceMessageService messageService; | |
| 33 | - | |
| 34 | - private final IotMqttConnectionManager connectionManager; | |
| 35 | - | |
| 36 | - private final Vertx vertx; | |
| 37 | - | |
| 38 | - @Getter | |
| 39 | - private final String serverId; | |
| 40 | - | |
| 41 | - private MqttServer mqttServer; | |
| 42 | - | |
| 43 | - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, | |
| 44 | - IotDeviceMessageService messageService, | |
| 45 | - IotMqttConnectionManager connectionManager, | |
| 46 | - Vertx vertx) { | |
| 47 | - this.mqttProperties = mqttProperties; | |
| 48 | - this.messageService = messageService; | |
| 49 | - this.connectionManager = connectionManager; | |
| 50 | - this.vertx = vertx; | |
| 51 | - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); | |
| 52 | - } | |
| 53 | - | |
| 54 | - @Override | |
| 55 | - public String getId() { | |
| 56 | - return ID; | |
| 57 | - } | |
| 58 | - | |
| 59 | - @Override | |
| 60 | - public IotProtocolTypeEnum getType() { | |
| 61 | - return IotProtocolTypeEnum.MQTT; | |
| 62 | - } | |
| 63 | - | |
| 64 | - @Override | |
| 65 | - public boolean isRunning() { | |
| 66 | - return running; | |
| 67 | - } | |
| 68 | - | |
| 69 | - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; | |
| 70 | - @Override | |
| 71 | - @PostConstruct | |
| 72 | - public void start() { | |
| 73 | - // 创建服务器选项 | |
| 74 | - MqttServerOptions options = new MqttServerOptions() | |
| 75 | - .setPort(mqttProperties.getPort()) | |
| 76 | - .setMaxMessageSize(mqttProperties.getMaxMessageSize()) | |
| 77 | - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); | |
| 78 | - | |
| 79 | - // 配置 SSL(如果启用) | |
| 80 | - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { | |
| 81 | - options.setSsl(true) | |
| 82 | - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) | |
| 83 | - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); | |
| 84 | - } | |
| 85 | - | |
| 86 | - // 创建服务器并设置连接处理器 | |
| 87 | - mqttServer = MqttServer.create(vertx, options); | |
| 88 | - mqttServer.endpointHandler(endpoint -> { | |
| 89 | - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); | |
| 90 | - handler.handle(endpoint); | |
| 91 | - }); | |
| 92 | - | |
| 93 | - // 启动服务器 | |
| 94 | - try { | |
| 95 | - mqttServer.listen().result(); | |
| 96 | - running = true; | |
| 97 | - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); | |
| 98 | - } catch (Exception e) { | |
| 99 | - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); | |
| 100 | - throw e; | |
| 101 | - } | |
| 102 | - } | |
| 103 | - | |
| 104 | - @Override | |
| 105 | - @PreDestroy | |
| 106 | - public void stop() { | |
| 107 | - if (mqttServer != null) { | |
| 108 | - try { | |
| 109 | - mqttServer.close().result(); | |
| 110 | - running = false; | |
| 111 | - log.info("[stop][IoT 网关 MQTT 协议已停止]"); | |
| 112 | - } catch (Exception e) { | |
| 113 | - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); | |
| 114 | - } | |
| 115 | - } | |
| 116 | - } | |
| 117 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| ... | ... | @@ -7,31 +7,22 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnecti |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; |
| 9 | 9 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 10 | 11 | import lombok.extern.slf4j.Slf4j; |
| 11 | 12 | |
| 12 | 13 | /** |
| 13 | 14 | * IoT 网关 MQTT 协议:下行消息处理器 |
| 14 | - * <p> | |
| 15 | - * 专门处理下行消息的业务逻辑,包括: | |
| 16 | - * 1. 消息编码 | |
| 17 | - * 2. 主题构建 | |
| 18 | - * 3. 消息发送 | |
| 19 | 15 | * |
| 20 | 16 | * @author 芋道源码 |
| 21 | 17 | */ |
| 22 | 18 | @Slf4j |
| 19 | +@RequiredArgsConstructor | |
| 23 | 20 | public class IotMqttDownstreamHandler { |
| 24 | 21 | |
| 25 | 22 | private final IotDeviceMessageService deviceMessageService; |
| 26 | 23 | |
| 27 | 24 | private final IotMqttConnectionManager connectionManager; |
| 28 | 25 | |
| 29 | - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, | |
| 30 | - IotMqttConnectionManager connectionManager) { | |
| 31 | - this.deviceMessageService = deviceMessageService; | |
| 32 | - this.connectionManager = connectionManager; | |
| 33 | - } | |
| 34 | - | |
| 35 | 26 | /** |
| 36 | 27 | * 处理下行消息 |
| 37 | 28 | * |
| ... | ... | @@ -40,6 +31,7 @@ public class IotMqttDownstreamHandler { |
| 40 | 31 | */ |
| 41 | 32 | public boolean handleDownstreamMessage(IotDeviceMessage message) { |
| 42 | 33 | try { |
| 34 | + // TODO @AI:参考 IotTcpDownstreamHandler 逻辑; | |
| 43 | 35 | // 1. 基础校验 |
| 44 | 36 | if (message == null || message.getDeviceId() == null) { |
| 45 | 37 | log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); |
| ... | ... | @@ -47,6 +39,7 @@ public class IotMqttDownstreamHandler { |
| 47 | 39 | } |
| 48 | 40 | |
| 49 | 41 | // 2. 检查设备是否在线 |
| 42 | + // TODO @AI:这块逻辑,是不是冗余?直接使用 3. 获取连接信息判断不就行了? | |
| 50 | 43 | if (connectionManager.isDeviceOffline(message.getDeviceId())) { |
| 51 | 44 | log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); |
| 52 | 45 | return false; |
| ... | ... | @@ -59,7 +52,7 @@ public class IotMqttDownstreamHandler { |
| 59 | 52 | return false; |
| 60 | 53 | } |
| 61 | 54 | |
| 62 | - // 4. 编码消息 | |
| 55 | + // 4. 序列化 | |
| 63 | 56 | byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), |
| 64 | 57 | connectionInfo.getDeviceName()); |
| 65 | 58 | if (payload == null || payload.length == 0) { |
| ... | ... | @@ -68,6 +61,7 @@ public class IotMqttDownstreamHandler { |
| 68 | 61 | } |
| 69 | 62 | |
| 70 | 63 | // 5. 发送消息到设备 |
| 64 | + // TODO @AI:参考 IotTcpDownstreamHandler 的逻辑; | |
| 71 | 65 | return sendMessageToDevice(message, connectionInfo, payload); |
| 72 | 66 | } catch (Exception e) { |
| 73 | 67 | if (message != null) { |
| ... | ... | @@ -78,6 +72,7 @@ public class IotMqttDownstreamHandler { |
| 78 | 72 | } |
| 79 | 73 | } |
| 80 | 74 | |
| 75 | + // TODO @AI 是不是合并到 handleDownstreamMessage 里; | |
| 81 | 76 | /** |
| 82 | 77 | * 发送消息到设备 |
| 83 | 78 | * |
| ... | ... | @@ -91,6 +86,7 @@ public class IotMqttDownstreamHandler { |
| 91 | 86 | byte[] payload) { |
| 92 | 87 | // 1. 构建主题 |
| 93 | 88 | String topic = buildDownstreamTopic(message, connectionInfo); |
| 89 | + // TODO @AI:直接断言,非空! | |
| 94 | 90 | if (StrUtil.isBlank(topic)) { |
| 95 | 91 | log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", |
| 96 | 92 | message.getDeviceId(), message.getMethod()); |
| ... | ... | @@ -118,6 +114,7 @@ public class IotMqttDownstreamHandler { |
| 118 | 114 | */ |
| 119 | 115 | private String buildDownstreamTopic(IotDeviceMessage message, |
| 120 | 116 | IotMqttConnectionManager.ConnectionInfo connectionInfo) { |
| 117 | + // TODO @AI:直接断言,非空! | |
| 121 | 118 | String method = message.getMethod(); |
| 122 | 119 | if (StrUtil.isBlank(method)) { |
| 123 | 120 | return null; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| 9 | 9 | /** |
| ... | ... | @@ -18,7 +18,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber |
| 18 | 18 | |
| 19 | 19 | private final IotMqttDownstreamHandler downstreamHandler; |
| 20 | 20 | |
| 21 | - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, | |
| 21 | + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, | |
| 22 | 22 | IotMqttDownstreamHandler downstreamHandler, |
| 23 | 23 | IotMessageBus messageBus) { |
| 24 | 24 | super(protocol, messageBus); |
| ... | ... | @@ -27,6 +27,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber |
| 27 | 27 | |
| 28 | 28 | @Override |
| 29 | 29 | protected void handleMessage(IotDeviceMessage message) { |
| 30 | + // TODO @AI:参考 IotTcpDownstreamHandler 不处理返回值,甚至不用返回值; | |
| 30 | 31 | boolean success = downstreamHandler.handleDownstreamMessage(message); |
| 31 | 32 | if (success) { |
| 32 | 33 | log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 7 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
| 8 | +import io.vertx.core.buffer.Buffer; | |
| 9 | +import io.vertx.mqtt.MqttEndpoint; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * IoT 网关 MQTT 协议的处理器抽象基类 | |
| 15 | + * <p> | |
| 16 | + * 提供通用的连接校验、响应发送等功能 | |
| 17 | + * | |
| 18 | + * @author 芋道源码 | |
| 19 | + */ | |
| 20 | +@Slf4j | |
| 21 | +@RequiredArgsConstructor | |
| 22 | +public abstract class IotMqttAbstractHandler { | |
| 23 | + | |
| 24 | + // TODO @AI:不使用 codec,使用 serializer 来进行编解码; | |
| 25 | + /** | |
| 26 | + * 默认编解码类型(MQTT 使用 Alink 协议) | |
| 27 | + */ | |
| 28 | + protected static final String DEFAULT_CODEC_TYPE = "Alink"; | |
| 29 | + | |
| 30 | + protected final IotMqttConnectionManager connectionManager; | |
| 31 | + protected final IotDeviceMessageService deviceMessageService; | |
| 32 | + | |
| 33 | + // TODO @AI:这个是否需要???!!! | |
| 34 | + /** | |
| 35 | + * 是否需要连接已认证(默认 true) | |
| 36 | + * <p> | |
| 37 | + * 仅 IotMqttConnectionHandler 覆盖为 false | |
| 38 | + * | |
| 39 | + * @return 是否需要连接已认证 | |
| 40 | + */ | |
| 41 | + protected boolean requiresAuthenticated() { | |
| 42 | + return true; | |
| 43 | + } | |
| 44 | + | |
| 45 | + // TODO @AI:不确定,是不是基于 method 就可以算出来 reply topic ???!!! | |
| 46 | + // TODO @AI:需要传递 seriabler 序列对象,不是通过 deviceMessageService.encodeDeviceMessage 获取到合适的; | |
| 47 | + /** | |
| 48 | + * 发送成功响应到设备 | |
| 49 | + * | |
| 50 | + * @param endpoint MQTT 连接端点 | |
| 51 | + * @param productKey 产品 Key | |
| 52 | + * @param deviceName 设备名称 | |
| 53 | + * @param requestId 请求 ID | |
| 54 | + * @param method 方法名 | |
| 55 | + * @param data 响应数据 | |
| 56 | + */ | |
| 57 | + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 58 | + String requestId, String method, Object data) { | |
| 59 | + try { | |
| 60 | + // 1. 构建响应消息 | |
| 61 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); | |
| 62 | + | |
| 63 | + // 2. 编码消息(使用默认编解码器) | |
| 64 | + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 65 | + | |
| 66 | + // 3. 构建响应主题并发送 | |
| 67 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); | |
| 68 | + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); | |
| 69 | + log.debug("[sendSuccessResponse][发送成功响应,主题: {}]", replyTopic); | |
| 70 | + } catch (Exception e) { | |
| 71 | + log.error("[sendSuccessResponse][发送成功响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * 发送错误响应到设备 | |
| 77 | + * | |
| 78 | + * @param endpoint MQTT 连接端点 | |
| 79 | + * @param productKey 产品 Key | |
| 80 | + * @param deviceName 设备名称 | |
| 81 | + * @param requestId 请求 ID | |
| 82 | + * @param method 方法名 | |
| 83 | + * @param errorMessage 错误消息 | |
| 84 | + */ | |
| 85 | + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 86 | + String requestId, String method, String errorMessage) { | |
| 87 | + try { | |
| 88 | + // 1. 构建响应消息 | |
| 89 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, 400, errorMessage); | |
| 90 | + | |
| 91 | + // 2. 编码消息(使用默认编解码器) | |
| 92 | + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 93 | + | |
| 94 | + // 3. 构建响应主题并发送 | |
| 95 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); | |
| 96 | + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); | |
| 97 | + log.debug("[sendErrorResponse][发送错误响应,主题: {}]", replyTopic); | |
| 98 | + } catch (Exception e) { | |
| 99 | + log.error("[sendErrorResponse][发送错误响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | |
| 100 | + } | |
| 101 | + } | |
| 102 | + | |
| 103 | + // TODO @AI:搞个基础的 writeResponse 会不会更好?参考 IotTcpUpstreamHandler 里; | |
| 104 | + | |
| 105 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.BooleanUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 12 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 15 | +import io.vertx.mqtt.MqttEndpoint; | |
| 16 | +import lombok.extern.slf4j.Slf4j; | |
| 17 | + | |
| 18 | +// TODO @AI:改成 auth 这样的命名,更好理解; | |
| 19 | +/** | |
| 20 | + * IoT 网关 MQTT 连接处理器 | |
| 21 | + * <p> | |
| 22 | + * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +@Slf4j | |
| 27 | +public class IotMqttConnectionHandler extends IotMqttAbstractHandler { | |
| 28 | + | |
| 29 | + // TODO @AI:通过 springutil 去获取! | |
| 30 | + private final IotDeviceCommonApi deviceApi; | |
| 31 | + private final String serverId; | |
| 32 | + | |
| 33 | + public IotMqttConnectionHandler(IotMqttConnectionManager connectionManager, | |
| 34 | + IotDeviceMessageService deviceMessageService, | |
| 35 | + IotDeviceCommonApi deviceApi, | |
| 36 | + String serverId) { | |
| 37 | + super(connectionManager, deviceMessageService); | |
| 38 | + this.deviceApi = deviceApi; | |
| 39 | + this.serverId = serverId; | |
| 40 | + } | |
| 41 | + | |
| 42 | + @Override | |
| 43 | + protected boolean requiresAuthenticated() { | |
| 44 | + return false; // 连接阶段不需要已认证 | |
| 45 | + } | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * 处理 MQTT 连接请求 | |
| 49 | + * | |
| 50 | + * @param endpoint MQTT 连接端点 | |
| 51 | + * @return 认证是否成功 | |
| 52 | + */ | |
| 53 | + public boolean handleConnect(MqttEndpoint endpoint) { | |
| 54 | + // TODO @AI:整个 try catch 下; | |
| 55 | + // TODO @AI:是不是参考 IotTcpUpstreamHandler 的代码结构 | |
| 56 | + String clientId = endpoint.clientIdentifier(); | |
| 57 | + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | |
| 58 | + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | |
| 59 | + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", | |
| 60 | + clientId, username, connectionManager.getEndpointAddress(endpoint)); | |
| 61 | + | |
| 62 | + // 进行认证 | |
| 63 | + if (!authenticateDevice(clientId, username, password, endpoint)) { | |
| 64 | + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 65 | + return false; | |
| 66 | + } | |
| 67 | + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 68 | + return true; | |
| 69 | + } | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * 在 MQTT 连接时进行设备认证 | |
| 73 | + * | |
| 74 | + * @param clientId 客户端 ID | |
| 75 | + * @param username 用户名 | |
| 76 | + * @param password 密码 | |
| 77 | + * @param endpoint MQTT 连接端点 | |
| 78 | + * @return 认证是否成功 | |
| 79 | + */ | |
| 80 | + private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { | |
| 81 | + try { | |
| 82 | + // 1.1 解析认证参数 | |
| 83 | + // TODO @AI:断言,统一交给上层打印日志; | |
| 84 | + if (StrUtil.hasEmpty(clientId, username, password)) { | |
| 85 | + log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); | |
| 86 | + return false; | |
| 87 | + } | |
| 88 | + // 1.2 构建认证参数 | |
| 89 | + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | |
| 90 | + .setClientId(clientId) | |
| 91 | + .setUsername(username) | |
| 92 | + .setPassword(password); | |
| 93 | + | |
| 94 | + // 2.1 执行认证 | |
| 95 | + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 96 | + // TODO @AI:断言,统一交给上层打印日志; | |
| 97 | + if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { | |
| 98 | + log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 99 | + clientId, username, authResult.getMsg()); | |
| 100 | + return false; | |
| 101 | + } | |
| 102 | + // 2.2 获取设备信息 | |
| 103 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 104 | + if (deviceInfo == null) { | |
| 105 | + log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); | |
| 106 | + return false; | |
| 107 | + } | |
| 108 | + // 2.3 获取设备信息 | |
| 109 | + // TODO @AI:报错需要处理下; | |
| 110 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 111 | + if (device == null) { | |
| 112 | + log.warn("[authenticateDevice][设备不存在,客户端 ID: {},用户名: {}]", clientId, username); | |
| 113 | + return false; | |
| 114 | + } | |
| 115 | + | |
| 116 | + // 3.1 注册连接 | |
| 117 | + registerConnection(endpoint, device, clientId); | |
| 118 | + // 3.2 发送设备上线消息 | |
| 119 | + sendOnlineMessage(device); | |
| 120 | + return true; | |
| 121 | + } catch (Exception e) { | |
| 122 | + log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); | |
| 123 | + return false; | |
| 124 | + } | |
| 125 | + } | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * 注册连接 | |
| 129 | + */ | |
| 130 | + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { | |
| 131 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() | |
| 132 | + .setDeviceId(device.getId()) | |
| 133 | + .setProductKey(device.getProductKey()) | |
| 134 | + .setDeviceName(device.getDeviceName()) | |
| 135 | + .setClientId(clientId) | |
| 136 | + .setAuthenticated(true) | |
| 137 | + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); | |
| 138 | + connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); | |
| 139 | + } | |
| 140 | + | |
| 141 | + /** | |
| 142 | + * 发送设备上线消息 | |
| 143 | + */ | |
| 144 | + private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 145 | + try { | |
| 146 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 147 | + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 148 | + device.getDeviceName(), serverId); | |
| 149 | + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); | |
| 150 | + } catch (Exception e) { | |
| 151 | + log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); | |
| 152 | + } | |
| 153 | + } | |
| 154 | + | |
| 155 | + /** | |
| 156 | + * 清理连接 | |
| 157 | + * | |
| 158 | + * @param endpoint MQTT 连接端点 | |
| 159 | + */ | |
| 160 | + public void cleanupConnection(MqttEndpoint endpoint) { | |
| 161 | + try { | |
| 162 | + // 1. 发送设备离线消息 | |
| 163 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 164 | + if (connectionInfo != null) { | |
| 165 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 166 | + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 167 | + connectionInfo.getDeviceName(), serverId); | |
| 168 | + } | |
| 169 | + | |
| 170 | + // 2. 注销连接 | |
| 171 | + connectionManager.unregisterConnection(endpoint); | |
| 172 | + } catch (Exception e) { | |
| 173 | + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", | |
| 174 | + endpoint.clientIdentifier(), e.getMessage()); | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.map.MapUtil; | |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 6 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 7 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 12 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 15 | +import io.vertx.mqtt.MqttEndpoint; | |
| 16 | +import lombok.extern.slf4j.Slf4j; | |
| 17 | + | |
| 18 | +import java.util.Map; | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * IoT 网关 MQTT 设备注册处理器 | |
| 22 | + * <p> | |
| 23 | + * 处理设备动态注册消息(一型一密) | |
| 24 | + * | |
| 25 | + * @author 芋道源码 | |
| 26 | + */ | |
| 27 | +@Slf4j | |
| 28 | +public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | |
| 29 | + | |
| 30 | + // TODO IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils? | |
| 31 | + /** | |
| 32 | + * register 请求的 topic 后缀 | |
| 33 | + */ | |
| 34 | + public static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; | |
| 35 | + | |
| 36 | + private final IotDeviceCommonApi deviceApi; | |
| 37 | + | |
| 38 | + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, | |
| 39 | + IotDeviceMessageService deviceMessageService, | |
| 40 | + IotDeviceCommonApi deviceApi) { | |
| 41 | + super(connectionManager, deviceMessageService); | |
| 42 | + // TODO @AI:通过 springutil 处理; | |
| 43 | + this.deviceApi = deviceApi; | |
| 44 | + } | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 判断是否为注册消息 | |
| 48 | + * | |
| 49 | + * @param topic 主题 | |
| 50 | + * @return 是否为注册消息 | |
| 51 | + */ | |
| 52 | + public boolean isRegisterMessage(String topic) { | |
| 53 | + // TODO @AI:是不是搞到 IotMqttTopicUtils 里? | |
| 54 | + return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX); | |
| 55 | + } | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * 处理注册消息 | |
| 59 | + * | |
| 60 | + * @param endpoint MQTT 连接端点 | |
| 61 | + * @param topic 主题 | |
| 62 | + * @param payload 消息内容 | |
| 63 | + */ | |
| 64 | + public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { | |
| 65 | + String clientId = endpoint.clientIdentifier(); | |
| 66 | + // 1.1 基础检查 | |
| 67 | + if (ArrayUtil.isEmpty(payload)) { | |
| 68 | + return; | |
| 69 | + } | |
| 70 | + // 1.2 解析主题,获取 productKey 和 deviceName | |
| 71 | + // TODO @AI:直接断言报错; | |
| 72 | + String[] topicParts = topic.split("/"); | |
| 73 | + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 74 | + log.warn("[handleRegister][topic({}) 格式不正确]", topic); | |
| 75 | + return; | |
| 76 | + } | |
| 77 | + String productKey = topicParts[2]; | |
| 78 | + String deviceName = topicParts[3]; | |
| 79 | + // TODO @AI:直接断言报错; | |
| 80 | + | |
| 81 | + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | |
| 82 | + // TODO @AI:使用默认的 json; | |
| 83 | + IotDeviceMessage message; | |
| 84 | + try { | |
| 85 | + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | |
| 86 | + if (message == null) { | |
| 87 | + log.warn("[handleRegister][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 88 | + return; | |
| 89 | + } | |
| 90 | + } catch (Exception e) { | |
| 91 | + log.error("[handleRegister][消息解码异常,客户端 ID: {},主题: {},错误: {}]", | |
| 92 | + clientId, topic, e.getMessage(), e); | |
| 93 | + return; | |
| 94 | + } | |
| 95 | + | |
| 96 | + // 3. 处理设备动态注册请求 | |
| 97 | + log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", | |
| 98 | + productKey, deviceName, message.getMethod()); | |
| 99 | + try { | |
| 100 | + processRegisterRequest(message, productKey, deviceName, endpoint); | |
| 101 | + } catch (Exception e) { | |
| 102 | + // TODO @AI:各种情况下的翻译; | |
| 103 | + log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 104 | + clientId, topic, e.getMessage(), e); | |
| 105 | + } | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** | |
| 109 | + * 处理设备动态注册请求(一型一密,不需要 deviceSecret) | |
| 110 | + * | |
| 111 | + * @param message 消息信息 | |
| 112 | + * @param productKey 产品 Key | |
| 113 | + * @param deviceName 设备名称 | |
| 114 | + * @param endpoint MQTT 连接端点 | |
| 115 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 116 | + */ | |
| 117 | + private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, | |
| 118 | + MqttEndpoint endpoint) { | |
| 119 | + String clientId = endpoint.clientIdentifier(); | |
| 120 | + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); | |
| 121 | + try { | |
| 122 | + // 1. 解析注册参数 | |
| 123 | + IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | |
| 124 | + if (params == null) { | |
| 125 | + log.warn("[processRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); | |
| 126 | + sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册参数不完整"); | |
| 127 | + return; | |
| 128 | + } | |
| 129 | + | |
| 130 | + // 2. 调用动态注册 API | |
| 131 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 132 | + if (result.isError()) { | |
| 133 | + log.warn("[processRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | |
| 134 | + sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getMsg()); | |
| 135 | + return; | |
| 136 | + } | |
| 137 | + | |
| 138 | + // 3. 发送成功响应(包含 deviceSecret) | |
| 139 | + sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData()); | |
| 140 | + log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", | |
| 141 | + params.getDeviceName(), clientId); | |
| 142 | + } catch (Exception e) { | |
| 143 | + log.error("[processRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); | |
| 144 | + sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册处理异常"); | |
| 145 | + } | |
| 146 | + } | |
| 147 | + | |
| 148 | + // TODO @AI:解析可以简化,参考别的 tcp 对应的 | |
| 149 | + /** | |
| 150 | + * 解析注册参数 | |
| 151 | + * | |
| 152 | + * @param params 参数对象(通常为 Map 类型) | |
| 153 | + * @return 注册参数 DTO,解析失败时返回 null | |
| 154 | + */ | |
| 155 | + @SuppressWarnings("unchecked") | |
| 156 | + private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | |
| 157 | + if (params == null) { | |
| 158 | + return null; | |
| 159 | + } | |
| 160 | + try { | |
| 161 | + // 参数默认为 Map 类型,直接转换 | |
| 162 | + if (params instanceof Map) { | |
| 163 | + Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 164 | + return new IotDeviceRegisterReqDTO() | |
| 165 | + .setProductKey(MapUtil.getStr(paramMap, "productKey")) | |
| 166 | + .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | |
| 167 | + .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | |
| 168 | + } | |
| 169 | + // 如果已经是目标类型,直接返回 | |
| 170 | + if (params instanceof IotDeviceRegisterReqDTO) { | |
| 171 | + return (IotDeviceRegisterReqDTO) params; | |
| 172 | + } | |
| 173 | + | |
| 174 | + // 其他情况尝试 JSON 转换 | |
| 175 | + return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); | |
| 176 | + } catch (Exception e) { | |
| 177 | + log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | |
| 178 | + return null; | |
| 179 | + } | |
| 180 | + } | |
| 181 | + | |
| 182 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 7 | +import io.vertx.mqtt.MqttEndpoint; | |
| 8 | +import lombok.extern.slf4j.Slf4j; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT 网关 MQTT 上行消息处理器 | |
| 12 | + * <p> | |
| 13 | + * 处理业务消息(属性上报、事件上报等) | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@Slf4j | |
| 18 | +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | |
| 19 | + | |
| 20 | + private final String serverId; | |
| 21 | + | |
| 22 | + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, | |
| 23 | + IotDeviceMessageService deviceMessageService, | |
| 24 | + String serverId) { | |
| 25 | + super(connectionManager, deviceMessageService); | |
| 26 | + this.serverId = serverId; | |
| 27 | + } | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 处理业务消息 | |
| 31 | + * | |
| 32 | + * @param endpoint MQTT 连接端点 | |
| 33 | + * @param topic 主题 | |
| 34 | + * @param payload 消息内容 | |
| 35 | + */ | |
| 36 | + public void handleMessage(MqttEndpoint endpoint, String topic, byte[] payload) { | |
| 37 | + String clientId = endpoint.clientIdentifier(); | |
| 38 | + | |
| 39 | + // 1. 基础检查 | |
| 40 | + if (payload == null || payload.length == 0) { | |
| 41 | + return; | |
| 42 | + } | |
| 43 | + | |
| 44 | + // 2. 解析主题,获取 productKey 和 deviceName | |
| 45 | + String[] topicParts = topic.split("/"); | |
| 46 | + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 47 | + log.warn("[handleMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | |
| 48 | + return; | |
| 49 | + } | |
| 50 | + | |
| 51 | + // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) | |
| 52 | + String productKey = topicParts[2]; | |
| 53 | + String deviceName = topicParts[3]; | |
| 54 | + try { | |
| 55 | + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 56 | + if (message == null) { | |
| 57 | + log.warn("[handleMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 58 | + return; | |
| 59 | + } | |
| 60 | + | |
| 61 | + // 4. 处理业务消息(认证已在连接时完成) | |
| 62 | + log.info("[handleMessage][收到设备消息,设备: {}.{}, 方法: {}]", | |
| 63 | + productKey, deviceName, message.getMethod()); | |
| 64 | + handleBusinessRequest(message, productKey, deviceName); | |
| 65 | + } catch (Exception e) { | |
| 66 | + // TODO @AI:各种情况下的翻译; | |
| 67 | + log.error("[handleMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 68 | + clientId, topic, e.getMessage(), e); | |
| 69 | + } | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 处理业务请求 | |
| 74 | + */ | |
| 75 | + private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { | |
| 76 | + // 发送消息到消息总线 | |
| 77 | + message.setServerId(serverId); | |
| 78 | + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 79 | + } | |
| 80 | + | |
| 81 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| ... | ... | @@ -40,6 +40,7 @@ public class IotMqttConnectionManager { |
| 40 | 40 | */ |
| 41 | 41 | private final Map<Long, MqttEndpoint> deviceEndpointMap = new ConcurrentHashMap<>(); |
| 42 | 42 | |
| 43 | + // TODO @AI:这里会存在返回 "unknown" 的情况么?是不是必须返回,否则还是异常更合理点? | |
| 43 | 44 | /** |
| 44 | 45 | * 安全获取 endpoint 地址 |
| 45 | 46 | * <p> |
| ... | ... | @@ -66,7 +67,6 @@ public class IotMqttConnectionManager { |
| 66 | 67 | } catch (Exception ignored) { |
| 67 | 68 | // 连接已关闭,忽略异常 |
| 68 | 69 | } |
| 69 | - | |
| 70 | 70 | return realTimeAddress; |
| 71 | 71 | } |
| 72 | 72 | |
| ... | ... | @@ -129,7 +129,6 @@ public class IotMqttConnectionManager { |
| 129 | 129 | if (endpoint == null) { |
| 130 | 130 | return null; |
| 131 | 131 | } |
| 132 | - | |
| 133 | 132 | // 获取连接信息 |
| 134 | 133 | return getConnectionInfo(endpoint); |
| 135 | 134 | } |
| ... | ... | @@ -208,6 +207,7 @@ public class IotMqttConnectionManager { |
| 208 | 207 | */ |
| 209 | 208 | private String clientId; |
| 210 | 209 | |
| 210 | + // TODO @AI:是不是要去掉!感觉没用啊; | |
| 211 | 211 | /** |
| 212 | 212 | * 是否已认证 |
| 213 | 213 | */ | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.hutool.core.util.ArrayUtil; | |
| 5 | -import cn.hutool.core.util.BooleanUtil; | |
| 6 | -import cn.hutool.core.util.StrUtil; | |
| 7 | -import cn.hutool.extra.spring.SpringUtil; | |
| 8 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 9 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 11 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 12 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | |
| 13 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 14 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 15 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 16 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 17 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 18 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 19 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 20 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | |
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 23 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 24 | -import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | |
| 25 | -import io.netty.handler.codec.mqtt.MqttQoS; | |
| 26 | -import io.vertx.mqtt.MqttEndpoint; | |
| 27 | -import io.vertx.mqtt.MqttTopicSubscription; | |
| 28 | -import lombok.extern.slf4j.Slf4j; | |
| 29 | - | |
| 30 | -import java.util.List; | |
| 31 | -import java.util.Map; | |
| 32 | - | |
| 33 | -/** | |
| 34 | - * MQTT 上行消息处理器 | |
| 35 | - * | |
| 36 | - * @author 芋道源码 | |
| 37 | - */ | |
| 38 | -@Slf4j | |
| 39 | -public class IotMqttUpstreamHandler { | |
| 40 | - | |
| 41 | - /** | |
| 42 | - * 默认编解码类型(MQTT 使用 Alink 协议) | |
| 43 | - */ | |
| 44 | - private static final String DEFAULT_CODEC_TYPE = "Alink"; | |
| 45 | - | |
| 46 | - /** | |
| 47 | - * register 请求的 topic 后缀 | |
| 48 | - */ | |
| 49 | - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; | |
| 50 | - | |
| 51 | - private final IotDeviceMessageService deviceMessageService; | |
| 52 | - | |
| 53 | - private final IotMqttConnectionManager connectionManager; | |
| 54 | - | |
| 55 | - private final IotDeviceCommonApi deviceApi; | |
| 56 | - | |
| 57 | - private final String serverId; | |
| 58 | - | |
| 59 | - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, | |
| 60 | - IotDeviceMessageService deviceMessageService, | |
| 61 | - IotMqttConnectionManager connectionManager) { | |
| 62 | - this.deviceMessageService = deviceMessageService; | |
| 63 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 64 | - this.connectionManager = connectionManager; | |
| 65 | - this.serverId = protocol.getServerId(); | |
| 66 | - } | |
| 67 | - | |
| 68 | - /** | |
| 69 | - * 处理 MQTT 连接 | |
| 70 | - * | |
| 71 | - * @param endpoint MQTT 连接端点 | |
| 72 | - */ | |
| 73 | - public void handle(MqttEndpoint endpoint) { | |
| 74 | - String clientId = endpoint.clientIdentifier(); | |
| 75 | - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | |
| 76 | - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | |
| 77 | - | |
| 78 | - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", | |
| 79 | - clientId, username, connectionManager.getEndpointAddress(endpoint)); | |
| 80 | - | |
| 81 | - // 1. 先进行认证 | |
| 82 | - if (!authenticateDevice(clientId, username, password, endpoint)) { | |
| 83 | - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 84 | - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | |
| 85 | - return; | |
| 86 | - } | |
| 87 | - | |
| 88 | - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 89 | - | |
| 90 | - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息) | |
| 91 | - endpoint.pingHandler(v -> { | |
| 92 | - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId); | |
| 93 | - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理 | |
| 94 | - }); | |
| 95 | - | |
| 96 | - // 3. 设置异常和关闭处理器 | |
| 97 | - endpoint.exceptionHandler(ex -> { | |
| 98 | - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); | |
| 99 | - cleanupConnection(endpoint); | |
| 100 | - }); | |
| 101 | - endpoint.closeHandler(v -> { | |
| 102 | - cleanupConnection(endpoint); | |
| 103 | - }); | |
| 104 | - | |
| 105 | - // 4. 设置消息处理器 | |
| 106 | - endpoint.publishHandler(mqttMessage -> { | |
| 107 | - try { | |
| 108 | - // 4.1 根据 topic 判断是否为 register 请求 | |
| 109 | - String topic = mqttMessage.topicName(); | |
| 110 | - byte[] payload = mqttMessage.payload().getBytes(); | |
| 111 | - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { | |
| 112 | - // register 请求:使用默认编解码器处理(设备可能未注册) | |
| 113 | - processRegisterMessage(clientId, topic, payload, endpoint); | |
| 114 | - } else { | |
| 115 | - // 业务请求:正常处理 | |
| 116 | - processMessage(clientId, topic, payload); | |
| 117 | - } | |
| 118 | - | |
| 119 | - // 4.2 根据 QoS 级别发送相应的确认消息 | |
| 120 | - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | |
| 121 | - // QoS 1: 发送 PUBACK 确认 | |
| 122 | - endpoint.publishAcknowledge(mqttMessage.messageId()); | |
| 123 | - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { | |
| 124 | - // QoS 2: 发送 PUBREC 确认 | |
| 125 | - endpoint.publishReceived(mqttMessage.messageId()); | |
| 126 | - } | |
| 127 | - // QoS 0 无需确认 | |
| 128 | - } catch (Exception e) { | |
| 129 | - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | |
| 130 | - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | |
| 131 | - cleanupConnection(endpoint); | |
| 132 | - endpoint.close(); | |
| 133 | - } | |
| 134 | - }); | |
| 135 | - | |
| 136 | - // 5. 设置订阅处理器 | |
| 137 | - endpoint.subscribeHandler(subscribe -> { | |
| 138 | - // 提取主题名称列表用于日志显示 | |
| 139 | - List<String> topicNames = subscribe.topicSubscriptions().stream() | |
| 140 | - .map(MqttTopicSubscription::topicName) | |
| 141 | - .collect(java.util.stream.Collectors.toList()); | |
| 142 | - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | |
| 143 | - | |
| 144 | - // 提取 QoS 列表 | |
| 145 | - List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream() | |
| 146 | - .map(MqttTopicSubscription::qualityOfService) | |
| 147 | - .collect(java.util.stream.Collectors.toList()); | |
| 148 | - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | |
| 149 | - }); | |
| 150 | - | |
| 151 | - // 6. 设置取消订阅处理器 | |
| 152 | - endpoint.unsubscribeHandler(unsubscribe -> { | |
| 153 | - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); | |
| 154 | - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); | |
| 155 | - }); | |
| 156 | - | |
| 157 | - // 7. 设置 QoS 2消息的 PUBREL 处理器 | |
| 158 | - endpoint.publishReleaseHandler(endpoint::publishComplete); | |
| 159 | - | |
| 160 | - // 8. 设置断开连接处理器 | |
| 161 | - endpoint.disconnectHandler(v -> { | |
| 162 | - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); | |
| 163 | - cleanupConnection(endpoint); | |
| 164 | - }); | |
| 165 | - | |
| 166 | - // 9. 接受连接 | |
| 167 | - endpoint.accept(false); | |
| 168 | - } | |
| 169 | - | |
| 170 | - /** | |
| 171 | - * 处理消息 | |
| 172 | - * | |
| 173 | - * @param clientId 客户端 ID | |
| 174 | - * @param topic 主题 | |
| 175 | - * @param payload 消息内容 | |
| 176 | - */ | |
| 177 | - private void processMessage(String clientId, String topic, byte[] payload) { | |
| 178 | - // 1. 基础检查 | |
| 179 | - if (payload == null || payload.length == 0) { | |
| 180 | - return; | |
| 181 | - } | |
| 182 | - | |
| 183 | - // 2. 解析主题,获取 productKey 和 deviceName | |
| 184 | - String[] topicParts = topic.split("/"); | |
| 185 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 186 | - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | |
| 187 | - return; | |
| 188 | - } | |
| 189 | - | |
| 190 | - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) | |
| 191 | - String productKey = topicParts[2]; | |
| 192 | - String deviceName = topicParts[3]; | |
| 193 | - try { | |
| 194 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 195 | - if (message == null) { | |
| 196 | - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 197 | - return; | |
| 198 | - } | |
| 199 | - | |
| 200 | - // 4. 处理业务消息(认证已在连接时完成) | |
| 201 | - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", | |
| 202 | - productKey, deviceName, message.getMethod()); | |
| 203 | - handleBusinessRequest(message, productKey, deviceName); | |
| 204 | - } catch (Exception e) { | |
| 205 | - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 206 | - clientId, topic, e.getMessage(), e); | |
| 207 | - } | |
| 208 | - } | |
| 209 | - | |
| 210 | - /** | |
| 211 | - * 在 MQTT 连接时进行设备认证 | |
| 212 | - * | |
| 213 | - * @param clientId 客户端 ID | |
| 214 | - * @param username 用户名 | |
| 215 | - * @param password 密码 | |
| 216 | - * @param endpoint MQTT 连接端点 | |
| 217 | - * @return 认证是否成功 | |
| 218 | - */ | |
| 219 | - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { | |
| 220 | - try { | |
| 221 | - // 1. 参数校验 | |
| 222 | - if (StrUtil.hasEmpty(clientId, username, password)) { | |
| 223 | - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); | |
| 224 | - return false; | |
| 225 | - } | |
| 226 | - | |
| 227 | - // 2. 构建认证参数 | |
| 228 | - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | |
| 229 | - .setClientId(clientId) | |
| 230 | - .setUsername(username) | |
| 231 | - .setPassword(password); | |
| 232 | - | |
| 233 | - // 3. 调用设备认证 API | |
| 234 | - CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 235 | - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { | |
| 236 | - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 237 | - clientId, username, authResult.getMsg()); | |
| 238 | - return false; | |
| 239 | - } | |
| 240 | - | |
| 241 | - // 4. 获取设备信息 | |
| 242 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 243 | - if (deviceInfo == null) { | |
| 244 | - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); | |
| 245 | - return false; | |
| 246 | - } | |
| 247 | - | |
| 248 | - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() | |
| 249 | - .setProductKey(deviceInfo.getProductKey()) | |
| 250 | - .setDeviceName(deviceInfo.getDeviceName()); | |
| 251 | - | |
| 252 | - CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO); | |
| 253 | - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { | |
| 254 | - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 255 | - clientId, username, deviceResult.getMsg()); | |
| 256 | - return false; | |
| 257 | - } | |
| 258 | - | |
| 259 | - // 5. 注册连接 | |
| 260 | - IotDeviceRespDTO device = deviceResult.getData(); | |
| 261 | - registerConnection(endpoint, device, clientId); | |
| 262 | - | |
| 263 | - // 6. 发送设备上线消息 | |
| 264 | - sendOnlineMessage(device); | |
| 265 | - | |
| 266 | - return true; | |
| 267 | - } catch (Exception e) { | |
| 268 | - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); | |
| 269 | - return false; | |
| 270 | - } | |
| 271 | - } | |
| 272 | - | |
| 273 | - /** | |
| 274 | - * 处理 register 消息(设备动态注册,使用默认编解码器) | |
| 275 | - * | |
| 276 | - * @param clientId 客户端 ID | |
| 277 | - * @param topic 主题 | |
| 278 | - * @param payload 消息内容 | |
| 279 | - * @param endpoint MQTT 连接端点 | |
| 280 | - */ | |
| 281 | - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { | |
| 282 | - // 1.1 基础检查 | |
| 283 | - if (ArrayUtil.isEmpty(payload)) { | |
| 284 | - return; | |
| 285 | - } | |
| 286 | - // 1.2 解析主题,获取 productKey 和 deviceName | |
| 287 | - String[] topicParts = topic.split("/"); | |
| 288 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 289 | - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); | |
| 290 | - return; | |
| 291 | - } | |
| 292 | - String productKey = topicParts[2]; | |
| 293 | - String deviceName = topicParts[3]; | |
| 294 | - | |
| 295 | - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | |
| 296 | - IotDeviceMessage message; | |
| 297 | - try { | |
| 298 | - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | |
| 299 | - if (message == null) { | |
| 300 | - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 301 | - return; | |
| 302 | - } | |
| 303 | - } catch (Exception e) { | |
| 304 | - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", | |
| 305 | - clientId, topic, e.getMessage(), e); | |
| 306 | - return; | |
| 307 | - } | |
| 308 | - | |
| 309 | - // 3. 处理设备动态注册请求 | |
| 310 | - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", | |
| 311 | - productKey, deviceName, message.getMethod()); | |
| 312 | - try { | |
| 313 | - handleRegisterRequest(message, productKey, deviceName, endpoint); | |
| 314 | - } catch (Exception e) { | |
| 315 | - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 316 | - clientId, topic, e.getMessage(), e); | |
| 317 | - } | |
| 318 | - } | |
| 319 | - | |
| 320 | - /** | |
| 321 | - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) | |
| 322 | - * | |
| 323 | - * @param message 消息信息 | |
| 324 | - * @param productKey 产品 Key | |
| 325 | - * @param deviceName 设备名称 | |
| 326 | - * @param endpoint MQTT 连接端点 | |
| 327 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 328 | - */ | |
| 329 | - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { | |
| 330 | - String clientId = endpoint.clientIdentifier(); | |
| 331 | - try { | |
| 332 | - // 1. 解析注册参数 | |
| 333 | - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | |
| 334 | - if (params == null) { | |
| 335 | - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); | |
| 336 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); | |
| 337 | - return; | |
| 338 | - } | |
| 339 | - | |
| 340 | - // 2. 调用动态注册 API | |
| 341 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 342 | - if (result.isError()) { | |
| 343 | - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | |
| 344 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); | |
| 345 | - return; | |
| 346 | - } | |
| 347 | - | |
| 348 | - // 3. 发送成功响应(包含 deviceSecret) | |
| 349 | - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); | |
| 350 | - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", | |
| 351 | - params.getDeviceName(), clientId); | |
| 352 | - } catch (Exception e) { | |
| 353 | - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); | |
| 354 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); | |
| 355 | - } | |
| 356 | - } | |
| 357 | - | |
| 358 | - /** | |
| 359 | - * 解析注册参数 | |
| 360 | - * | |
| 361 | - * @param params 参数对象(通常为 Map 类型) | |
| 362 | - * @return 注册参数 DTO,解析失败时返回 null | |
| 363 | - */ | |
| 364 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 365 | - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | |
| 366 | - if (params == null) { | |
| 367 | - return null; | |
| 368 | - } | |
| 369 | - try { | |
| 370 | - // 参数默认为 Map 类型,直接转换 | |
| 371 | - if (params instanceof Map) { | |
| 372 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 373 | - return new IotDeviceRegisterReqDTO() | |
| 374 | - .setProductKey(MapUtil.getStr(paramMap, "productKey")) | |
| 375 | - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | |
| 376 | - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | |
| 377 | - } | |
| 378 | - // 如果已经是目标类型,直接返回 | |
| 379 | - if (params instanceof IotDeviceRegisterReqDTO) { | |
| 380 | - return (IotDeviceRegisterReqDTO) params; | |
| 381 | - } | |
| 382 | - | |
| 383 | - // 其他情况尝试 JSON 转换 | |
| 384 | - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); | |
| 385 | - } catch (Exception e) { | |
| 386 | - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | |
| 387 | - return null; | |
| 388 | - } | |
| 389 | - } | |
| 390 | - | |
| 391 | - /** | |
| 392 | - * 发送注册成功响应(包含 deviceSecret) | |
| 393 | - * | |
| 394 | - * @param endpoint MQTT 连接端点 | |
| 395 | - * @param productKey 产品 Key | |
| 396 | - * @param deviceName 设备名称 | |
| 397 | - * @param requestId 请求 ID | |
| 398 | - * @param registerResp 注册响应 | |
| 399 | - */ | |
| 400 | - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 401 | - String requestId, IotDeviceRegisterRespDTO registerResp) { | |
| 402 | - try { | |
| 403 | - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | |
| 404 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 405 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | |
| 406 | - | |
| 407 | - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | |
| 408 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 409 | - | |
| 410 | - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | |
| 411 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | |
| 412 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | |
| 413 | - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | |
| 414 | - MqttQoS.AT_LEAST_ONCE, false, false); | |
| 415 | - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); | |
| 416 | - } catch (Exception e) { | |
| 417 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", | |
| 418 | - endpoint.clientIdentifier(), e); | |
| 419 | - } | |
| 420 | - } | |
| 421 | - | |
| 422 | - /** | |
| 423 | - * 发送注册错误响应 | |
| 424 | - * | |
| 425 | - * @param endpoint MQTT 连接端点 | |
| 426 | - * @param productKey 产品 Key | |
| 427 | - * @param deviceName 设备名称 | |
| 428 | - * @param requestId 请求 ID | |
| 429 | - * @param errorMessage 错误消息 | |
| 430 | - */ | |
| 431 | - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 432 | - String requestId, String errorMessage) { | |
| 433 | - try { | |
| 434 | - // 1. 构建响应消息 | |
| 435 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 436 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); | |
| 437 | - | |
| 438 | - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | |
| 439 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 440 | - | |
| 441 | - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | |
| 442 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | |
| 443 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | |
| 444 | - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | |
| 445 | - MqttQoS.AT_LEAST_ONCE, false, false); | |
| 446 | - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); | |
| 447 | - } catch (Exception e) { | |
| 448 | - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", | |
| 449 | - endpoint.clientIdentifier(), e); | |
| 450 | - } | |
| 451 | - } | |
| 452 | - | |
| 453 | - /** | |
| 454 | - * 处理业务请求 | |
| 455 | - */ | |
| 456 | - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { | |
| 457 | - // 发送消息到消息总线 | |
| 458 | - message.setServerId(serverId); | |
| 459 | - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 460 | - } | |
| 461 | - | |
| 462 | - /** | |
| 463 | - * 注册连接 | |
| 464 | - */ | |
| 465 | - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { | |
| 466 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() | |
| 467 | - .setDeviceId(device.getId()) | |
| 468 | - .setProductKey(device.getProductKey()) | |
| 469 | - .setDeviceName(device.getDeviceName()) | |
| 470 | - .setClientId(clientId) | |
| 471 | - .setAuthenticated(true) | |
| 472 | - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); | |
| 473 | - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); | |
| 474 | - } | |
| 475 | - | |
| 476 | - /** | |
| 477 | - * 发送设备上线消息 | |
| 478 | - */ | |
| 479 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 480 | - try { | |
| 481 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 482 | - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 483 | - device.getDeviceName(), serverId); | |
| 484 | - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); | |
| 485 | - } catch (Exception e) { | |
| 486 | - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); | |
| 487 | - } | |
| 488 | - } | |
| 489 | - | |
| 490 | - /** | |
| 491 | - * 清理连接 | |
| 492 | - */ | |
| 493 | - private void cleanupConnection(MqttEndpoint endpoint) { | |
| 494 | - try { | |
| 495 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 496 | - if (connectionInfo != null) { | |
| 497 | - // 发送设备离线消息 | |
| 498 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 499 | - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 500 | - connectionInfo.getDeviceName(), serverId); | |
| 501 | - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); | |
| 502 | - } | |
| 503 | - | |
| 504 | - // 注销连接 | |
| 505 | - connectionManager.unregisterConnection(endpoint); | |
| 506 | - } catch (Exception e) { | |
| 507 | - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage()); | |
| 508 | - } | |
| 509 | - } | |
| 510 | - | |
| 511 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
| ... | ... | @@ -45,8 +45,8 @@ public class IotTcpDownstreamHandler { |
| 45 | 45 | } |
| 46 | 46 | |
| 47 | 47 | // 2. 序列化 + 帧编码 |
| 48 | - byte[] serializedData = serializer.serialize(message); | |
| 49 | - Buffer frameData = codec.encode(serializedData); | |
| 48 | + byte[] payload = serializer.serialize(message); | |
| 49 | + Buffer frameData = codec.encode(payload); | |
| 50 | 50 | |
| 51 | 51 | // 3. 发送到设备 |
| 52 | 52 | boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| ... | ... | @@ -57,9 +57,9 @@ yudao: |
| 57 | 57 | # 针对引入的 TCP 组件的配置 |
| 58 | 58 | # ==================================== |
| 59 | 59 | - id: tcp-json |
| 60 | + enabled: false | |
| 60 | 61 | type: tcp |
| 61 | 62 | port: 8091 |
| 62 | - enabled: false | |
| 63 | 63 | serialize: json |
| 64 | 64 | tcp: |
| 65 | 65 | max-connections: 1000 |
| ... | ... | @@ -79,9 +79,9 @@ yudao: |
| 79 | 79 | # 针对引入的 UDP 组件的配置 |
| 80 | 80 | # ==================================== |
| 81 | 81 | - id: udp-json |
| 82 | + enabled: false | |
| 82 | 83 | type: udp |
| 83 | 84 | port: 8093 |
| 84 | - enabled: false | |
| 85 | 85 | serialize: json |
| 86 | 86 | udp: |
| 87 | 87 | max-sessions: 1000 # 最大会话数 |
| ... | ... | @@ -92,9 +92,9 @@ yudao: |
| 92 | 92 | # 针对引入的 WebSocket 组件的配置 |
| 93 | 93 | # ==================================== |
| 94 | 94 | - id: websocket-json |
| 95 | + enabled: true | |
| 95 | 96 | type: websocket |
| 96 | 97 | port: 8094 |
| 97 | - enabled: true | |
| 98 | 98 | serialize: json |
| 99 | 99 | websocket: |
| 100 | 100 | path: /ws |
| ... | ... | @@ -106,13 +106,26 @@ yudao: |
| 106 | 106 | # 针对引入的 CoAP 组件的配置 |
| 107 | 107 | # ==================================== |
| 108 | 108 | - id: coap-json |
| 109 | + enabled: false | |
| 109 | 110 | type: coap |
| 110 | 111 | port: 5683 |
| 111 | - enabled: true | |
| 112 | 112 | coap: |
| 113 | 113 | max-message-size: 1024 # 最大消息大小(字节) |
| 114 | 114 | ack-timeout-ms: 2000 # ACK 超时时间(毫秒) |
| 115 | 115 | max-retransmit: 4 # 最大重传次数 |
| 116 | + # ==================================== | |
| 117 | + # 针对引入的 MQTT 组件的配置 | |
| 118 | + # ==================================== | |
| 119 | + - id: mqtt-json | |
| 120 | + enabled: true | |
| 121 | + type: mqtt | |
| 122 | + port: 1883 | |
| 123 | + serialize: json | |
| 124 | + mqtt: | |
| 125 | + max-message-size: 8192 # 最大消息大小(字节) | |
| 126 | + connect-timeout-seconds: 60 # 连接超时时间(秒) | |
| 127 | + keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) | |
| 128 | + ssl-enabled: false # 是否启用 SSL | |
| 116 | 129 | |
| 117 | 130 | # 协议配置(旧版,保持兼容) |
| 118 | 131 | protocol: |
| ... | ... | @@ -150,15 +163,6 @@ yudao: |
| 150 | 163 | key-store-password: "your-keystore-password" # 客户端证书库密码 |
| 151 | 164 | trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 |
| 152 | 165 | trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 |
| 153 | - # ==================================== | |
| 154 | - # 针对引入的 MQTT 组件的配置 | |
| 155 | - # ==================================== | |
| 156 | - mqtt: | |
| 157 | - enabled: false | |
| 158 | - port: 1883 | |
| 159 | - max-message-size: 8192 | |
| 160 | - connect-timeout-seconds: 60 | |
| 161 | - ssl-enabled: false | |
| 162 | 166 | |
| 163 | 167 | --- #################### 日志相关配置 #################### |
| 164 | 168 | ... | ... |