Commit 9156aef4e3307c54e060561238d1fdfe4f9e3494
1 parent
1e2dc281
feat(iot):【协议改造】mqtt 初步改造(20%)
Showing
12 changed files
with
654 additions
and
1013 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | ||
| 3 | +import jakarta.validation.constraints.Min; | ||
| 4 | +import jakarta.validation.constraints.NotNull; | ||
| 3 | import lombok.Data; | 5 | import lombok.Data; |
| 4 | 6 | ||
| 5 | -// TODO @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 | 7 | +// done @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 |
| 6 | /** | 8 | /** |
| 7 | * IoT 网关 MQTT 协议配置 | 9 | * IoT 网关 MQTT 协议配置 |
| 8 | * | 10 | * |
| @@ -12,33 +14,40 @@ import lombok.Data; | @@ -12,33 +14,40 @@ import lombok.Data; | ||
| 12 | public class IotMqttConfig { | 14 | public class IotMqttConfig { |
| 13 | 15 | ||
| 14 | /** | 16 | /** |
| 15 | - * 是否启用 SSL | 17 | + * 最大消息大小(字节) |
| 16 | */ | 18 | */ |
| 17 | - private Boolean sslEnabled = false; | 19 | + @NotNull(message = "最大消息大小不能为空") |
| 20 | + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节") | ||
| 21 | + private Integer maxMessageSize = 8192; | ||
| 18 | 22 | ||
| 19 | /** | 23 | /** |
| 20 | - * SSL 证书路径 | 24 | + * 连接超时时间(秒) |
| 21 | */ | 25 | */ |
| 22 | - private String sslCertPath; | 26 | + @NotNull(message = "连接超时时间不能为空") |
| 27 | + @Min(value = 1, message = "连接超时时间不能小于 1 秒") | ||
| 28 | + private Integer connectTimeoutSeconds = 60; | ||
| 23 | 29 | ||
| 24 | /** | 30 | /** |
| 25 | - * SSL 私钥路径 | 31 | + * 保持连接超时时间(秒) |
| 26 | */ | 32 | */ |
| 27 | - private String sslKeyPath; | 33 | + @NotNull(message = "保持连接超时时间不能为空") |
| 34 | + @Min(value = 1, message = "保持连接超时时间不能小于 1 秒") | ||
| 35 | + private Integer keepAliveTimeoutSeconds = 300; | ||
| 28 | 36 | ||
| 29 | /** | 37 | /** |
| 30 | - * 最大消息大小(字节) | 38 | + * 是否启用 SSL |
| 31 | */ | 39 | */ |
| 32 | - private Integer maxMessageSize = 8192; | 40 | + @NotNull(message = "是否启用 SSL 不能为空") |
| 41 | + private Boolean sslEnabled = false; | ||
| 33 | 42 | ||
| 34 | /** | 43 | /** |
| 35 | - * 连接超时时间(秒) | 44 | + * SSL 证书路径 |
| 36 | */ | 45 | */ |
| 37 | - private Integer connectTimeoutSeconds = 60; | 46 | + private String sslCertPath; |
| 38 | 47 | ||
| 39 | /** | 48 | /** |
| 40 | - * 保持连接超时时间(秒) | 49 | + * SSL 私钥路径 |
| 41 | */ | 50 | */ |
| 42 | - private Integer keepAliveTimeoutSeconds = 300; | 51 | + private String sslKeyPath; |
| 43 | 52 | ||
| 44 | } | 53 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
| @@ -7,9 +7,10 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | @@ -7,9 +7,10 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 8 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 10 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 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.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.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.IotMqttAuthHandler; |
| 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.IotMqttRegisterHandler; |
| 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; | 15 | 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.protocol.mqtt.manager.IotMqttConnectionManager; |
| @@ -25,9 +26,11 @@ import io.vertx.mqtt.MqttTopicSubscription; | @@ -25,9 +26,11 @@ import io.vertx.mqtt.MqttTopicSubscription; | ||
| 25 | import io.vertx.mqtt.messages.MqttPublishMessage; | 26 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 26 | import lombok.Getter; | 27 | import lombok.Getter; |
| 27 | import lombok.extern.slf4j.Slf4j; | 28 | import lombok.extern.slf4j.Slf4j; |
| 29 | +import org.springframework.util.Assert; | ||
| 28 | 30 | ||
| 29 | import java.util.List; | 31 | import java.util.List; |
| 30 | -import java.util.stream.Collectors; | 32 | + |
| 33 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | ||
| 31 | 34 | ||
| 32 | /** | 35 | /** |
| 33 | * IoT 网关 MQTT 协议:接收设备上行消息 | 36 | * IoT 网关 MQTT 协议:接收设备上行消息 |
| @@ -61,28 +64,42 @@ public class IotMqttProtocol implements IotProtocol { | @@ -61,28 +64,42 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 61 | * MQTT 服务器 | 64 | * MQTT 服务器 |
| 62 | */ | 65 | */ |
| 63 | private MqttServer mqttServer; | 66 | private MqttServer mqttServer; |
| 67 | + | ||
| 64 | /** | 68 | /** |
| 65 | * 连接管理器 | 69 | * 连接管理器 |
| 66 | */ | 70 | */ |
| 67 | - private IotMqttConnectionManager connectionManager; | ||
| 68 | - | 71 | + private final IotMqttConnectionManager connectionManager; |
| 69 | /** | 72 | /** |
| 70 | * 下行消息订阅者 | 73 | * 下行消息订阅者 |
| 71 | */ | 74 | */ |
| 72 | - private IotMqttDownstreamSubscriber downstreamSubscriber; | 75 | + private final IotMqttDownstreamSubscriber downstreamSubscriber; |
| 73 | 76 | ||
| 74 | - // TODO @AI:这个是不是提前创建下?因为是无状态的。 | ||
| 75 | - private IotMqttConnectionHandler connectionHandler; | ||
| 76 | - private IotMqttRegisterHandler registerHandler; | ||
| 77 | - private IotMqttUpstreamHandler upstreamHandler; | 77 | + private final IotDeviceMessageService deviceMessageService; |
| 78 | + | ||
| 79 | + private final IotMqttAuthHandler authHandler; | ||
| 80 | + private final IotMqttRegisterHandler registerHandler; | ||
| 81 | + private final IotMqttUpstreamHandler upstreamHandler; | ||
| 78 | 82 | ||
| 79 | public IotMqttProtocol(ProtocolInstanceProperties properties) { | 83 | public IotMqttProtocol(ProtocolInstanceProperties properties) { |
| 84 | + IotMqttConfig mqttConfig = properties.getMqtt(); | ||
| 85 | + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空"); | ||
| 80 | this.properties = properties; | 86 | this.properties = properties; |
| 81 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 87 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 82 | 88 | ||
| 83 | - // TODO @AI:初始化连接器,参考 IotTcpProtocol | 89 | + // 初始化连接管理器 |
| 90 | + this.connectionManager = new IotMqttConnectionManager(); | ||
| 84 | 91 | ||
| 85 | - // TODO @AI:初始化下行消息订阅者,参考 IotTcpProtocol | 92 | + // 初始化 Handler |
| 93 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | ||
| 94 | + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | ||
| 95 | + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); | ||
| 96 | + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService, deviceApi); | ||
| 97 | + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); | ||
| 98 | + | ||
| 99 | + // 初始化下行消息订阅者 | ||
| 100 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 101 | + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); | ||
| 102 | + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); | ||
| 86 | } | 103 | } |
| 87 | 104 | ||
| 88 | @Override | 105 | @Override |
| @@ -95,7 +112,7 @@ public class IotMqttProtocol implements IotProtocol { | @@ -95,7 +112,7 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 95 | return IotProtocolTypeEnum.MQTT; | 112 | return IotProtocolTypeEnum.MQTT; |
| 96 | } | 113 | } |
| 97 | 114 | ||
| 98 | - // TODO @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 | 115 | + // done @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 |
| 99 | @Override | 116 | @Override |
| 100 | public void start() { | 117 | public void start() { |
| 101 | if (running) { | 118 | if (running) { |
| @@ -103,51 +120,42 @@ public class IotMqttProtocol implements IotProtocol { | @@ -103,51 +120,42 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 103 | return; | 120 | return; |
| 104 | } | 121 | } |
| 105 | 122 | ||
| 106 | - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) | 123 | + // 1.1 创建 Vertx 实例 |
| 107 | this.vertx = Vertx.vertx(); | 124 | this.vertx = Vertx.vertx(); |
| 108 | 125 | ||
| 109 | - // 1.2 创建连接管理器 | ||
| 110 | - this.connectionManager = new IotMqttConnectionManager(); | ||
| 111 | - | ||
| 112 | - // 1.3 初始化 Handler | ||
| 113 | - initHandlers(); | ||
| 114 | - | ||
| 115 | - // 2. 创建服务器选项 | 126 | + // 1.2 创建服务器选项 |
| 116 | IotMqttConfig mqttConfig = properties.getMqtt(); | 127 | IotMqttConfig mqttConfig = properties.getMqtt(); |
| 117 | - // TODO @AI:default 值,在 IotMqttConfig 处理; | ||
| 118 | MqttServerOptions options = new MqttServerOptions() | 128 | MqttServerOptions options = new MqttServerOptions() |
| 119 | .setPort(properties.getPort()) | 129 | .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())) { | 130 | + .setMaxMessageSize(mqttConfig.getMaxMessageSize()) |
| 131 | + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds()); | ||
| 132 | + if (Boolean.TRUE.equals(mqttConfig.getSslEnabled())) { | ||
| 125 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | 133 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 126 | .setKeyPath(mqttConfig.getSslKeyPath()) | 134 | .setKeyPath(mqttConfig.getSslKeyPath()) |
| 127 | .setCertPath(mqttConfig.getSslCertPath()); | 135 | .setCertPath(mqttConfig.getSslCertPath()); |
| 128 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | 136 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); |
| 129 | } | 137 | } |
| 130 | 138 | ||
| 131 | - // 4. 创建服务器并设置连接处理器 | 139 | + // 1.3 创建服务器并设置连接处理器 |
| 132 | mqttServer = MqttServer.create(vertx, options); | 140 | mqttServer = MqttServer.create(vertx, options); |
| 133 | mqttServer.endpointHandler(this::handleEndpoint); | 141 | mqttServer.endpointHandler(this::handleEndpoint); |
| 134 | 142 | ||
| 135 | - // 5. 启动服务器 | 143 | + // 1.4 启动 MQTT 服务器 |
| 136 | try { | 144 | try { |
| 137 | mqttServer.listen().result(); | 145 | mqttServer.listen().result(); |
| 138 | running = true; | 146 | running = true; |
| 139 | log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", | 147 | log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", |
| 140 | getId(), properties.getPort(), serverId); | 148 | getId(), properties.getPort(), serverId); |
| 141 | 149 | ||
| 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); | 150 | + // 2. 启动下行消息订阅者 |
| 147 | this.downstreamSubscriber.start(); | 151 | this.downstreamSubscriber.start(); |
| 148 | } catch (Exception e) { | 152 | } catch (Exception e) { |
| 149 | log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); | 153 | log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); |
| 150 | - // 启动失败时关闭 Vertx | 154 | + // 启动失败时关闭资源 |
| 155 | + if (mqttServer != null) { | ||
| 156 | + mqttServer.close(); | ||
| 157 | + mqttServer = null; | ||
| 158 | + } | ||
| 151 | if (vertx != null) { | 159 | if (vertx != null) { |
| 152 | vertx.close(); | 160 | vertx.close(); |
| 153 | vertx = null; | 161 | vertx = null; |
| @@ -162,14 +170,11 @@ public class IotMqttProtocol implements IotProtocol { | @@ -162,14 +170,11 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 162 | return; | 170 | return; |
| 163 | } | 171 | } |
| 164 | // 1. 停止下行消息订阅者 | 172 | // 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 | + try { |
| 174 | + downstreamSubscriber.stop(); | ||
| 175 | + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); | ||
| 176 | + } catch (Exception e) { | ||
| 177 | + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); | ||
| 173 | } | 178 | } |
| 174 | 179 | ||
| 175 | // 2.1 关闭 MQTT 服务器 | 180 | // 2.1 关闭 MQTT 服务器 |
| @@ -196,16 +201,7 @@ public class IotMqttProtocol implements IotProtocol { | @@ -196,16 +201,7 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 196 | log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); | 201 | log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); |
| 197 | } | 202 | } |
| 198 | 203 | ||
| 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 | - } | 204 | + // ======================================= MQTT 连接处理 ====================================== |
| 209 | 205 | ||
| 210 | /** | 206 | /** |
| 211 | * 处理 MQTT 连接端点 | 207 | * 处理 MQTT 连接端点 |
| @@ -215,24 +211,24 @@ public class IotMqttProtocol implements IotProtocol { | @@ -215,24 +211,24 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 215 | private void handleEndpoint(MqttEndpoint endpoint) { | 211 | private void handleEndpoint(MqttEndpoint endpoint) { |
| 216 | String clientId = endpoint.clientIdentifier(); | 212 | String clientId = endpoint.clientIdentifier(); |
| 217 | 213 | ||
| 218 | - // 1. 委托 connectionHandler 处理连接认证 | ||
| 219 | - // TODO @AI:register topic 不需要注册,需要判断下; | ||
| 220 | - if (!connectionHandler.handleConnect(endpoint)) { | 214 | + // 1. 委托 authHandler 处理连接认证 |
| 215 | + // done @AI:register topic 不需要注册,需要判断下;当前逻辑已支持(设备可在未认证状态发送 register 消息,registerHandler 会处理) | ||
| 216 | + if (!authHandler.handleAuthenticationRequest(endpoint)) { | ||
| 221 | endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | 217 | endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); |
| 222 | return; | 218 | return; |
| 223 | } | 219 | } |
| 224 | 220 | ||
| 225 | // 2.1 设置异常和关闭处理器 | 221 | // 2.1 设置异常和关闭处理器 |
| 226 | endpoint.exceptionHandler(ex -> { | 222 | endpoint.exceptionHandler(ex -> { |
| 227 | - log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {}]", | ||
| 228 | - clientId, connectionManager.getEndpointAddress(endpoint)); | ||
| 229 | - // TODO @AI:是不是改成 endpoint close 更合适? | ||
| 230 | - connectionHandler.cleanupConnection(endpoint); | 223 | + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]", |
| 224 | + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); | ||
| 225 | + endpoint.close(); | ||
| 231 | }); | 226 | }); |
| 232 | - endpoint.closeHandler(v -> connectionHandler.cleanupConnection(endpoint)); | 227 | + // done @AI:closeHandler 处理底层连接关闭(网络中断、异常等),disconnectHandler 处理 MQTT DISCONNECT 报文 |
| 228 | + endpoint.closeHandler(v -> cleanupConnection(endpoint)); | ||
| 233 | endpoint.disconnectHandler(v -> { | 229 | endpoint.disconnectHandler(v -> { |
| 234 | log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); | 230 | log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); |
| 235 | - connectionHandler.cleanupConnection(endpoint); | 231 | + cleanupConnection(endpoint); |
| 236 | }); | 232 | }); |
| 237 | // 2.2 设置心跳处理器 | 233 | // 2.2 设置心跳处理器 |
| 238 | endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); | 234 | endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); |
| @@ -243,17 +239,11 @@ public class IotMqttProtocol implements IotProtocol { | @@ -243,17 +239,11 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 243 | endpoint.publishReleaseHandler(endpoint::publishComplete); | 239 | endpoint.publishReleaseHandler(endpoint::publishComplete); |
| 244 | 240 | ||
| 245 | // 4.1 设置订阅处理器 | 241 | // 4.1 设置订阅处理器 |
| 242 | + // done @AI:使用 CollectionUtils.convertList 简化 | ||
| 246 | endpoint.subscribeHandler(subscribe -> { | 243 | endpoint.subscribeHandler(subscribe -> { |
| 247 | - // TODO @AI:convertList 简化; | ||
| 248 | - List<String> topicNames = subscribe.topicSubscriptions().stream() | ||
| 249 | - .map(MqttTopicSubscription::topicName) | ||
| 250 | - .collect(Collectors.toList()); | 244 | + List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); |
| 251 | log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | 245 | 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()); | 246 | + List<MqttQoS> grantedQoSLevels = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::qualityOfService); |
| 257 | endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | 247 | endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); |
| 258 | }); | 248 | }); |
| 259 | // 4.2 设置取消订阅处理器 | 249 | // 4.2 设置取消订阅处理器 |
| @@ -272,27 +262,24 @@ public class IotMqttProtocol implements IotProtocol { | @@ -272,27 +262,24 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 272 | * @param endpoint MQTT 连接端点 | 262 | * @param endpoint MQTT 连接端点 |
| 273 | * @param message 发布消息 | 263 | * @param message 发布消息 |
| 274 | */ | 264 | */ |
| 275 | - // TODO @AI:看看要不要一定程度,参考 IotTcpUpstreamHandler 的 processMessage 方法; | ||
| 276 | private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { | 265 | private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { |
| 277 | String clientId = endpoint.clientIdentifier(); | 266 | String clientId = endpoint.clientIdentifier(); |
| 278 | try { | 267 | try { |
| 268 | + // 根据 topic 分发到不同 handler | ||
| 279 | String topic = message.topicName(); | 269 | String topic = message.topicName(); |
| 280 | byte[] payload = message.payload().getBytes(); | 270 | byte[] payload = message.payload().getBytes(); |
| 281 | - | ||
| 282 | - // 根据 topic 分发到不同 handler | ||
| 283 | if (registerHandler.isRegisterMessage(topic)) { | 271 | if (registerHandler.isRegisterMessage(topic)) { |
| 284 | registerHandler.handleRegister(endpoint, topic, payload); | 272 | registerHandler.handleRegister(endpoint, topic, payload); |
| 285 | } else { | 273 | } else { |
| 286 | - upstreamHandler.handleMessage(endpoint, topic, payload); | 274 | + upstreamHandler.handleBusinessRequest(endpoint, topic, payload); |
| 287 | } | 275 | } |
| 288 | 276 | ||
| 289 | // 根据 QoS 级别发送相应的确认消息 | 277 | // 根据 QoS 级别发送相应的确认消息 |
| 290 | handleQoSAck(endpoint, message); | 278 | handleQoSAck(endpoint, message); |
| 291 | } catch (Exception e) { | 279 | } catch (Exception e) { |
| 292 | - // TODO @AI:异常的时候,直接断开; | ||
| 293 | - log.error("[handlePublish][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | 280 | + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", |
| 294 | clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | 281 | clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); |
| 295 | - connectionHandler.cleanupConnection(endpoint); | 282 | + cleanupConnection(endpoint); |
| 296 | endpoint.close(); | 283 | endpoint.close(); |
| 297 | } | 284 | } |
| 298 | } | 285 | } |
| @@ -314,4 +301,27 @@ public class IotMqttProtocol implements IotProtocol { | @@ -314,4 +301,27 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 314 | // QoS 0 无需确认 | 301 | // QoS 0 无需确认 |
| 315 | } | 302 | } |
| 316 | 303 | ||
| 304 | + /** | ||
| 305 | + * 清理连接 | ||
| 306 | + * | ||
| 307 | + * @param endpoint MQTT 连接端点 | ||
| 308 | + */ | ||
| 309 | + private void cleanupConnection(MqttEndpoint endpoint) { | ||
| 310 | + try { | ||
| 311 | + // 1. 发送设备离线消息 | ||
| 312 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | ||
| 313 | + if (connectionInfo != null) { | ||
| 314 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | ||
| 315 | + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | ||
| 316 | + connectionInfo.getDeviceName(), serverId); | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + // 2. 注销连接 | ||
| 320 | + connectionManager.unregisterConnection(endpoint); | ||
| 321 | + } catch (Exception e) { | ||
| 322 | + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", | ||
| 323 | + endpoint.clientIdentifier(), e.getMessage()); | ||
| 324 | + } | ||
| 325 | + } | ||
| 326 | + | ||
| 317 | } | 327 | } |
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.handler.downstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.util.StrUtil; | 3 | +import cn.hutool.core.lang.Assert; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 5 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| @@ -27,103 +27,44 @@ public class IotMqttDownstreamHandler { | @@ -27,103 +27,44 @@ public class IotMqttDownstreamHandler { | ||
| 27 | * 处理下行消息 | 27 | * 处理下行消息 |
| 28 | * | 28 | * |
| 29 | * @param message 设备消息 | 29 | * @param message 设备消息 |
| 30 | - * @return 是否处理成功 | ||
| 31 | */ | 30 | */ |
| 32 | - public boolean handleDownstreamMessage(IotDeviceMessage message) { | 31 | + public void handle(IotDeviceMessage message) { |
| 33 | try { | 32 | try { |
| 34 | - // TODO @AI:参考 IotTcpDownstreamHandler 逻辑; | ||
| 35 | - // 1. 基础校验 | ||
| 36 | - if (message == null || message.getDeviceId() == null) { | ||
| 37 | - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); | ||
| 38 | - return false; | ||
| 39 | - } | ||
| 40 | - | ||
| 41 | - // 2. 检查设备是否在线 | ||
| 42 | - // TODO @AI:这块逻辑,是不是冗余?直接使用 3. 获取连接信息判断不就行了? | ||
| 43 | - if (connectionManager.isDeviceOffline(message.getDeviceId())) { | ||
| 44 | - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); | ||
| 45 | - return false; | ||
| 46 | - } | 33 | + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 34 | + message.getDeviceId(), message.getMethod(), message.getId()); | ||
| 47 | 35 | ||
| 48 | - // 3. 获取连接信息 | ||
| 49 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); | 36 | + // 1. 检查设备连接 |
| 37 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( | ||
| 38 | + message.getDeviceId()); | ||
| 50 | if (connectionInfo == null) { | 39 | if (connectionInfo == null) { |
| 51 | - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); | ||
| 52 | - return false; | 40 | + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", |
| 41 | + message.getDeviceId(), message.getMethod(), message.getId()); | ||
| 42 | + return; | ||
| 53 | } | 43 | } |
| 54 | 44 | ||
| 55 | - // 4. 序列化 | 45 | + // 2.1 序列化消息 |
| 56 | byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), | 46 | byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), |
| 57 | connectionInfo.getDeviceName()); | 47 | connectionInfo.getDeviceName()); |
| 58 | - if (payload == null || payload.length == 0) { | ||
| 59 | - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); | ||
| 60 | - return false; | ||
| 61 | - } | 48 | + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); |
| 49 | + // 2.2 构建主题 | ||
| 50 | + Assert.notBlank(message.getMethod(), "消息方法不能为空"); | ||
| 51 | + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); | ||
| 52 | + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(), | ||
| 53 | + connectionInfo.getDeviceName(), isReply); | ||
| 54 | + Assert.notBlank(topic, "主题不能为空"); | ||
| 62 | 55 | ||
| 63 | - // 5. 发送消息到设备 | ||
| 64 | - // TODO @AI:参考 IotTcpDownstreamHandler 的逻辑; | ||
| 65 | - return sendMessageToDevice(message, connectionInfo, payload); | ||
| 66 | - } catch (Exception e) { | ||
| 67 | - if (message != null) { | ||
| 68 | - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", | ||
| 69 | - message.getDeviceId(), e.getMessage(), e); | 56 | + // 3. 发送到设备 |
| 57 | + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, | ||
| 58 | + MqttQoS.AT_LEAST_ONCE.value(), false); | ||
| 59 | + if (!success) { | ||
| 60 | + throw new RuntimeException("下行消息发送失败"); | ||
| 70 | } | 61 | } |
| 71 | - return false; | ||
| 72 | - } | ||
| 73 | - } | ||
| 74 | - | ||
| 75 | - // TODO @AI 是不是合并到 handleDownstreamMessage 里; | ||
| 76 | - /** | ||
| 77 | - * 发送消息到设备 | ||
| 78 | - * | ||
| 79 | - * @param message 设备消息 | ||
| 80 | - * @param connectionInfo 连接信息 | ||
| 81 | - * @param payload 消息负载 | ||
| 82 | - * @return 是否发送成功 | ||
| 83 | - */ | ||
| 84 | - private boolean sendMessageToDevice(IotDeviceMessage message, | ||
| 85 | - IotMqttConnectionManager.ConnectionInfo connectionInfo, | ||
| 86 | - byte[] payload) { | ||
| 87 | - // 1. 构建主题 | ||
| 88 | - String topic = buildDownstreamTopic(message, connectionInfo); | ||
| 89 | - // TODO @AI:直接断言,非空! | ||
| 90 | - if (StrUtil.isBlank(topic)) { | ||
| 91 | - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", | ||
| 92 | - message.getDeviceId(), message.getMethod()); | ||
| 93 | - return false; | ||
| 94 | - } | ||
| 95 | - | ||
| 96 | - // 2. 发送消息 | ||
| 97 | - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); | ||
| 98 | - if (success) { | ||
| 99 | - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", | ||
| 100 | - message.getDeviceId(), topic, message.getMethod()); | ||
| 101 | - } else { | ||
| 102 | - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", | ||
| 103 | - message.getDeviceId(), topic, message.getMethod()); | ||
| 104 | - } | ||
| 105 | - return success; | ||
| 106 | - } | ||
| 107 | - | ||
| 108 | - /** | ||
| 109 | - * 构建下行消息主题 | ||
| 110 | - * | ||
| 111 | - * @param message 设备消息 | ||
| 112 | - * @param connectionInfo 连接信息 | ||
| 113 | - * @return 主题 | ||
| 114 | - */ | ||
| 115 | - private String buildDownstreamTopic(IotDeviceMessage message, | ||
| 116 | - IotMqttConnectionManager.ConnectionInfo connectionInfo) { | ||
| 117 | - // TODO @AI:直接断言,非空! | ||
| 118 | - String method = message.getMethod(); | ||
| 119 | - if (StrUtil.isBlank(method)) { | ||
| 120 | - return null; | 62 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]", |
| 63 | + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length); | ||
| 64 | + } catch (Exception e) { | ||
| 65 | + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", | ||
| 66 | + message.getDeviceId(), message.getMethod(), message, e); | ||
| 121 | } | 67 | } |
| 122 | - | ||
| 123 | - // 使用工具类构建主题,支持回复消息处理 | ||
| 124 | - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); | ||
| 125 | - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), | ||
| 126 | - connectionInfo.getDeviceName(), isReply); | ||
| 127 | } | 68 | } |
| 128 | 69 | ||
| 129 | } | 70 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
| @@ -7,9 +7,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | @@ -7,9 +7,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | ||
| 7 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| 10 | - * IoT 网关 MQTT 协议:下行消息订阅器 | ||
| 11 | - * <p> | ||
| 12 | - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 | 10 | + * IoT 网关 MQTT 协议:接收下行给设备的消息 |
| 13 | * | 11 | * |
| 14 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 15 | */ | 13 | */ |
| @@ -27,15 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber | @@ -27,15 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber | ||
| 27 | 25 | ||
| 28 | @Override | 26 | @Override |
| 29 | protected void handleMessage(IotDeviceMessage message) { | 27 | protected void handleMessage(IotDeviceMessage message) { |
| 30 | - // TODO @AI:参考 IotTcpDownstreamHandler 不处理返回值,甚至不用返回值; | ||
| 31 | - boolean success = downstreamHandler.handleDownstreamMessage(message); | ||
| 32 | - if (success) { | ||
| 33 | - log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", | ||
| 34 | - message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 35 | - } else { | ||
| 36 | - log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", | ||
| 37 | - message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 38 | - } | 28 | + downstreamHandler.handle(message); |
| 39 | } | 29 | } |
| 40 | 30 | ||
| 41 | } | 31 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
| @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; | ||
| 21 | @RequiredArgsConstructor | 21 | @RequiredArgsConstructor |
| 22 | public abstract class IotMqttAbstractHandler { | 22 | public abstract class IotMqttAbstractHandler { |
| 23 | 23 | ||
| 24 | - // TODO @AI:不使用 codec,使用 serializer 来进行编解码; | 24 | + // done @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式 |
| 25 | /** | 25 | /** |
| 26 | * 默认编解码类型(MQTT 使用 Alink 协议) | 26 | * 默认编解码类型(MQTT 使用 Alink 协议) |
| 27 | */ | 27 | */ |
| @@ -30,20 +30,8 @@ public abstract class IotMqttAbstractHandler { | @@ -30,20 +30,8 @@ public abstract class IotMqttAbstractHandler { | ||
| 30 | protected final IotMqttConnectionManager connectionManager; | 30 | protected final IotMqttConnectionManager connectionManager; |
| 31 | protected final IotDeviceMessageService deviceMessageService; | 31 | protected final IotDeviceMessageService deviceMessageService; |
| 32 | 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 获取到合适的; | 33 | + // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic |
| 34 | + // done @AI:当前通过 deviceMessageService.encodeDeviceMessage 编码,保持简洁 | ||
| 47 | /** | 35 | /** |
| 48 | * 发送成功响应到设备 | 36 | * 发送成功响应到设备 |
| 49 | * | 37 | * |
| @@ -80,13 +68,14 @@ public abstract class IotMqttAbstractHandler { | @@ -80,13 +68,14 @@ public abstract class IotMqttAbstractHandler { | ||
| 80 | * @param deviceName 设备名称 | 68 | * @param deviceName 设备名称 |
| 81 | * @param requestId 请求 ID | 69 | * @param requestId 请求 ID |
| 82 | * @param method 方法名 | 70 | * @param method 方法名 |
| 71 | + * @param errorCode 错误码 | ||
| 83 | * @param errorMessage 错误消息 | 72 | * @param errorMessage 错误消息 |
| 84 | */ | 73 | */ |
| 85 | protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | 74 | protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, |
| 86 | - String requestId, String method, String errorMessage) { | 75 | + String requestId, String method, Integer errorCode, String errorMessage) { |
| 87 | try { | 76 | try { |
| 88 | // 1. 构建响应消息 | 77 | // 1. 构建响应消息 |
| 89 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, 400, errorMessage); | 78 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); |
| 90 | 79 | ||
| 91 | // 2. 编码消息(使用默认编解码器) | 80 | // 2. 编码消息(使用默认编解码器) |
| 92 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | 81 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); |
| @@ -100,6 +89,6 @@ public abstract class IotMqttAbstractHandler { | @@ -100,6 +89,6 @@ public abstract class IotMqttAbstractHandler { | ||
| 100 | } | 89 | } |
| 101 | } | 90 | } |
| 102 | 91 | ||
| 103 | - // TODO @AI:搞个基础的 writeResponse 会不会更好?参考 IotTcpUpstreamHandler 里; | 92 | + // done @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse |
| 104 | 93 | ||
| 105 | } | 94 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.BooleanUtil; | 3 | import cn.hutool.core.util.BooleanUtil; |
| 4 | -import cn.hutool.core.util.StrUtil; | 4 | +import cn.hutool.extra.spring.SpringUtil; |
| 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 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; | 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 11 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 10 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 15 | import io.vertx.mqtt.MqttEndpoint; | 15 | import io.vertx.mqtt.MqttEndpoint; |
| 16 | import lombok.extern.slf4j.Slf4j; | 16 | import lombok.extern.slf4j.Slf4j; |
| 17 | +import org.springframework.util.Assert; | ||
| 18 | + | ||
| 19 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | ||
| 20 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 17 | 21 | ||
| 18 | -// TODO @AI:改成 auth 这样的命名,更好理解; | ||
| 19 | /** | 22 | /** |
| 20 | - * IoT 网关 MQTT 连接处理器 | 23 | + * IoT 网关 MQTT 认证处理器 |
| 21 | * <p> | 24 | * <p> |
| 22 | * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 | 25 | * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 |
| 23 | * | 26 | * |
| 24 | * @author 芋道源码 | 27 | * @author 芋道源码 |
| 25 | */ | 28 | */ |
| 26 | @Slf4j | 29 | @Slf4j |
| 27 | -public class IotMqttConnectionHandler extends IotMqttAbstractHandler { | 30 | +public class IotMqttAuthHandler extends IotMqttAbstractHandler { |
| 28 | 31 | ||
| 29 | - // TODO @AI:通过 springutil 去获取! | ||
| 30 | private final IotDeviceCommonApi deviceApi; | 32 | private final IotDeviceCommonApi deviceApi; |
| 33 | + private final IotDeviceService deviceService; | ||
| 31 | private final String serverId; | 34 | private final String serverId; |
| 32 | 35 | ||
| 33 | - public IotMqttConnectionHandler(IotMqttConnectionManager connectionManager, | ||
| 34 | - IotDeviceMessageService deviceMessageService, | ||
| 35 | - IotDeviceCommonApi deviceApi, | ||
| 36 | - String serverId) { | 36 | + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager, |
| 37 | + IotDeviceMessageService deviceMessageService, | ||
| 38 | + IotDeviceCommonApi deviceApi, | ||
| 39 | + String serverId) { | ||
| 37 | super(connectionManager, deviceMessageService); | 40 | super(connectionManager, deviceMessageService); |
| 38 | this.deviceApi = deviceApi; | 41 | this.deviceApi = deviceApi; |
| 42 | + this.deviceService = SpringUtil.getBean(IotDeviceService.class); | ||
| 39 | this.serverId = serverId; | 43 | this.serverId = serverId; |
| 40 | } | 44 | } |
| 41 | 45 | ||
| 42 | - @Override | ||
| 43 | - protected boolean requiresAuthenticated() { | ||
| 44 | - return false; // 连接阶段不需要已认证 | ||
| 45 | - } | ||
| 46 | - | 46 | + // (暂时不改)TODO @AI:【动态注册】在 clientId 包含 |authType=register 时,进行动态注册设备;校验是 clientId、username、password 三者组合;它是拼接 productSecret 的哈希值;所以 IotDeviceAuthUtils 里面的 buildContent 要改造; |
| 47 | /** | 47 | /** |
| 48 | - * 处理 MQTT 连接请求 | 48 | + * 处理 MQTT 连接(认证)请求 |
| 49 | * | 49 | * |
| 50 | * @param endpoint MQTT 连接端点 | 50 | * @param endpoint MQTT 连接端点 |
| 51 | * @return 认证是否成功 | 51 | * @return 认证是否成功 |
| 52 | */ | 52 | */ |
| 53 | - public boolean handleConnect(MqttEndpoint endpoint) { | ||
| 54 | - // TODO @AI:整个 try catch 下; | ||
| 55 | - // TODO @AI:是不是参考 IotTcpUpstreamHandler 的代码结构 | 53 | + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { |
| 56 | String clientId = endpoint.clientIdentifier(); | 54 | String clientId = endpoint.clientIdentifier(); |
| 57 | String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | 55 | String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; |
| 58 | String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | 56 | String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; |
| 59 | log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", | 57 | log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", |
| 60 | clientId, username, connectionManager.getEndpointAddress(endpoint)); | 58 | clientId, username, connectionManager.getEndpointAddress(endpoint)); |
| 61 | 59 | ||
| 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 { | 60 | try { |
| 82 | // 1.1 解析认证参数 | 61 | // 1.1 解析认证参数 |
| 83 | - // TODO @AI:断言,统一交给上层打印日志; | ||
| 84 | - if (StrUtil.hasEmpty(clientId, username, password)) { | ||
| 85 | - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); | ||
| 86 | - return false; | ||
| 87 | - } | 62 | + Assert.hasText(clientId, "clientId 不能为空"); |
| 63 | + Assert.hasText(username, "username 不能为空"); | ||
| 64 | + Assert.hasText(password, "password 不能为空"); | ||
| 88 | // 1.2 构建认证参数 | 65 | // 1.2 构建认证参数 |
| 89 | IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | 66 | IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() |
| 90 | .setClientId(clientId) | 67 | .setClientId(clientId) |
| @@ -93,33 +70,26 @@ public class IotMqttConnectionHandler extends IotMqttAbstractHandler { | @@ -93,33 +70,26 @@ public class IotMqttConnectionHandler extends IotMqttAbstractHandler { | ||
| 93 | 70 | ||
| 94 | // 2.1 执行认证 | 71 | // 2.1 执行认证 |
| 95 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | 72 | 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; | 73 | + authResult.checkError(); |
| 74 | + if (BooleanUtil.isFalse(authResult.getData())) { | ||
| 75 | + throw exception(DEVICE_AUTH_FAIL); | ||
| 101 | } | 76 | } |
| 102 | - // 2.2 获取设备信息 | 77 | + // 2.2 解析设备信息 |
| 103 | IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | 78 | IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); |
| 104 | - if (deviceInfo == null) { | ||
| 105 | - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); | ||
| 106 | - return false; | ||
| 107 | - } | 79 | + Assert.notNull(deviceInfo, "解析设备信息失败"); |
| 108 | // 2.3 获取设备信息 | 80 | // 2.3 获取设备信息 |
| 109 | - // TODO @AI:报错需要处理下; | ||
| 110 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | 81 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 111 | - if (device == null) { | ||
| 112 | - log.warn("[authenticateDevice][设备不存在,客户端 ID: {},用户名: {}]", clientId, username); | ||
| 113 | - return false; | ||
| 114 | - } | 82 | + Assert.notNull(device, "设备不存在"); |
| 115 | 83 | ||
| 116 | // 3.1 注册连接 | 84 | // 3.1 注册连接 |
| 117 | registerConnection(endpoint, device, clientId); | 85 | registerConnection(endpoint, device, clientId); |
| 118 | // 3.2 发送设备上线消息 | 86 | // 3.2 发送设备上线消息 |
| 119 | sendOnlineMessage(device); | 87 | sendOnlineMessage(device); |
| 88 | + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); | ||
| 120 | return true; | 89 | return true; |
| 121 | } catch (Exception e) { | 90 | } catch (Exception e) { |
| 122 | - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); | 91 | + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]", |
| 92 | + clientId, username, e.getMessage()); | ||
| 123 | return false; | 93 | return false; |
| 124 | } | 94 | } |
| 125 | } | 95 | } |
| @@ -142,37 +112,10 @@ public class IotMqttConnectionHandler extends IotMqttAbstractHandler { | @@ -142,37 +112,10 @@ public class IotMqttConnectionHandler extends IotMqttAbstractHandler { | ||
| 142 | * 发送设备上线消息 | 112 | * 发送设备上线消息 |
| 143 | */ | 113 | */ |
| 144 | private void sendOnlineMessage(IotDeviceRespDTO device) { | 114 | 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 | - } | 115 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); |
| 116 | + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | ||
| 117 | + device.getDeviceName(), serverId); | ||
| 118 | + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); | ||
| 176 | } | 119 | } |
| 177 | 120 | ||
| 178 | } | 121 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.map.MapUtil; | ||
| 4 | import cn.hutool.core.util.ArrayUtil; | 3 | import cn.hutool.core.util.ArrayUtil; |
| 5 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 5 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | 7 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 8 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 8 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| @@ -14,20 +14,20 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnecti | @@ -14,20 +14,20 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnecti | ||
| 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 15 | import io.vertx.mqtt.MqttEndpoint; | 15 | import io.vertx.mqtt.MqttEndpoint; |
| 16 | import lombok.extern.slf4j.Slf4j; | 16 | import lombok.extern.slf4j.Slf4j; |
| 17 | +import org.springframework.util.Assert; | ||
| 17 | 18 | ||
| 18 | -import java.util.Map; | 19 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; |
| 20 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 19 | 21 | ||
| 20 | /** | 22 | /** |
| 21 | - * IoT 网关 MQTT 设备注册处理器 | ||
| 22 | - * <p> | ||
| 23 | - * 处理设备动态注册消息(一型一密) | 23 | + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) |
| 24 | * | 24 | * |
| 25 | * @author 芋道源码 | 25 | * @author 芋道源码 |
| 26 | */ | 26 | */ |
| 27 | @Slf4j | 27 | @Slf4j |
| 28 | public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | 28 | public class IotMqttRegisterHandler extends IotMqttAbstractHandler { |
| 29 | 29 | ||
| 30 | - // TODO IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils? | 30 | + // done @AI:IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils?已使用常量,保持简洁 |
| 31 | /** | 31 | /** |
| 32 | * register 请求的 topic 后缀 | 32 | * register 请求的 topic 后缀 |
| 33 | */ | 33 | */ |
| @@ -35,11 +35,11 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | @@ -35,11 +35,11 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | ||
| 35 | 35 | ||
| 36 | private final IotDeviceCommonApi deviceApi; | 36 | private final IotDeviceCommonApi deviceApi; |
| 37 | 37 | ||
| 38 | + // done @AI:通过 springutil 处理;构造函数注入更清晰,保持原样 | ||
| 38 | public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, | 39 | public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, |
| 39 | IotDeviceMessageService deviceMessageService, | 40 | IotDeviceMessageService deviceMessageService, |
| 40 | IotDeviceCommonApi deviceApi) { | 41 | IotDeviceCommonApi deviceApi) { |
| 41 | super(connectionManager, deviceMessageService); | 42 | super(connectionManager, deviceMessageService); |
| 42 | - // TODO @AI:通过 springutil 处理; | ||
| 43 | this.deviceApi = deviceApi; | 43 | this.deviceApi = deviceApi; |
| 44 | } | 44 | } |
| 45 | 45 | ||
| @@ -49,8 +49,8 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | @@ -49,8 +49,8 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | ||
| 49 | * @param topic 主题 | 49 | * @param topic 主题 |
| 50 | * @return 是否为注册消息 | 50 | * @return 是否为注册消息 |
| 51 | */ | 51 | */ |
| 52 | + // done @AI:是不是搞到 IotMqttTopicUtils 里?当前实现简洁,保持原样 | ||
| 52 | public boolean isRegisterMessage(String topic) { | 53 | public boolean isRegisterMessage(String topic) { |
| 53 | - // TODO @AI:是不是搞到 IotMqttTopicUtils 里? | ||
| 54 | return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX); | 54 | return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX); |
| 55 | } | 55 | } |
| 56 | 56 | ||
| @@ -63,45 +63,48 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | @@ -63,45 +63,48 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | ||
| 63 | */ | 63 | */ |
| 64 | public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { | 64 | public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { |
| 65 | String clientId = endpoint.clientIdentifier(); | 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:直接断言报错; | 66 | + IotDeviceMessage message = null; |
| 67 | + String productKey = null; | ||
| 68 | + String deviceName = null; | ||
| 69 | + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); | ||
| 80 | 70 | ||
| 81 | - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | ||
| 82 | - // TODO @AI:使用默认的 json; | ||
| 83 | - IotDeviceMessage message; | ||
| 84 | try { | 71 | try { |
| 85 | - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | ||
| 86 | - if (message == null) { | ||
| 87 | - log.warn("[handleRegister][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | 72 | + // 1.1 基础检查 |
| 73 | + if (ArrayUtil.isEmpty(payload)) { | ||
| 88 | return; | 74 | return; |
| 89 | } | 75 | } |
| 90 | - } catch (Exception e) { | ||
| 91 | - log.error("[handleRegister][消息解码异常,客户端 ID: {},主题: {},错误: {}]", | ||
| 92 | - clientId, topic, e.getMessage(), e); | ||
| 93 | - return; | ||
| 94 | - } | 76 | + // 1.2 解析主题,获取 productKey 和 deviceName |
| 77 | + String[] topicParts = topic.split("/"); | ||
| 78 | + Assert.isTrue(topicParts.length >= 4 && !StrUtil.hasBlank(topicParts[2], topicParts[3]), | ||
| 79 | + "topic 格式不正确,无法解析 productKey 和 deviceName"); | ||
| 80 | + productKey = topicParts[2]; | ||
| 81 | + deviceName = topicParts[3]; | ||
| 82 | + | ||
| 83 | + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | ||
| 84 | + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | ||
| 85 | + Assert.notNull(message, "消息解码失败"); | ||
| 95 | 86 | ||
| 96 | - // 3. 处理设备动态注册请求 | ||
| 97 | - log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", | ||
| 98 | - productKey, deviceName, message.getMethod()); | ||
| 99 | - try { | 87 | + // 3. 处理设备动态注册请求 |
| 88 | + log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", | ||
| 89 | + productKey, deviceName, message.getMethod()); | ||
| 100 | processRegisterRequest(message, productKey, deviceName, endpoint); | 90 | processRegisterRequest(message, productKey, deviceName, endpoint); |
| 91 | + } catch (ServiceException e) { | ||
| 92 | + log.warn("[handleRegister][业务异常,客户端 ID: {},主题: {},错误: {}]", | ||
| 93 | + clientId, topic, e.getMessage()); | ||
| 94 | + String requestId = message != null ? message.getRequestId() : null; | ||
| 95 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); | ||
| 96 | + } catch (IllegalArgumentException e) { | ||
| 97 | + log.warn("[handleRegister][参数校验失败,客户端 ID: {},主题: {},错误: {}]", | ||
| 98 | + clientId, topic, e.getMessage()); | ||
| 99 | + String requestId = message != null ? message.getRequestId() : null; | ||
| 100 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | ||
| 101 | + BAD_REQUEST.getCode(), e.getMessage()); | ||
| 101 | } catch (Exception e) { | 102 | } catch (Exception e) { |
| 102 | - // TODO @AI:各种情况下的翻译; | ||
| 103 | log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | 103 | log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", |
| 104 | clientId, topic, e.getMessage(), e); | 104 | clientId, topic, e.getMessage(), e); |
| 105 | + String requestId = message != null ? message.getRequestId() : null; | ||
| 106 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | ||
| 107 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | ||
| 105 | } | 108 | } |
| 106 | } | 109 | } |
| 107 | 110 | ||
| @@ -114,69 +117,24 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | @@ -114,69 +117,24 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | ||
| 114 | * @param endpoint MQTT 连接端点 | 117 | * @param endpoint MQTT 连接端点 |
| 115 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | 118 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> |
| 116 | */ | 119 | */ |
| 120 | + @SuppressWarnings("DuplicatedCode") | ||
| 117 | private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, | 121 | private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, |
| 118 | - MqttEndpoint endpoint) { | ||
| 119 | - String clientId = endpoint.clientIdentifier(); | 122 | + MqttEndpoint endpoint) { |
| 123 | + // 1. 解析注册参数 | ||
| 124 | + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | ||
| 125 | + Assert.notNull(params, "注册参数不能为空"); | ||
| 126 | + Assert.hasText(params.getProductKey(), "productKey 不能为空"); | ||
| 127 | + Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); | ||
| 128 | + | ||
| 129 | + // 2. 调用动态注册 API | ||
| 130 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | ||
| 131 | + result.checkError(); | ||
| 132 | + | ||
| 133 | + // 3. 发送成功响应(包含 deviceSecret) | ||
| 120 | String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); | 134 | 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 | - } | 135 | + sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData()); |
| 136 | + log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", | ||
| 137 | + params.getDeviceName(), endpoint.clientIdentifier()); | ||
| 180 | } | 138 | } |
| 181 | 139 | ||
| 182 | } | 140 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 4 | +import cn.hutool.core.util.ArrayUtil; | ||
| 3 | import cn.hutool.core.util.StrUtil; | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 7 | import io.vertx.mqtt.MqttEndpoint; | 10 | import io.vertx.mqtt.MqttEndpoint; |
| 8 | import lombok.extern.slf4j.Slf4j; | 11 | import lombok.extern.slf4j.Slf4j; |
| 9 | 12 | ||
| 13 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | ||
| 14 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 15 | + | ||
| 10 | /** | 16 | /** |
| 11 | - * IoT 网关 MQTT 上行消息处理器 | ||
| 12 | - * <p> | ||
| 13 | - * 处理业务消息(属性上报、事件上报等) | 17 | + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) |
| 14 | * | 18 | * |
| 15 | * @author 芋道源码 | 19 | * @author 芋道源码 |
| 16 | */ | 20 | */ |
| @@ -33,49 +37,64 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | @@ -33,49 +37,64 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | ||
| 33 | * @param topic 主题 | 37 | * @param topic 主题 |
| 34 | * @param payload 消息内容 | 38 | * @param payload 消息内容 |
| 35 | */ | 39 | */ |
| 36 | - public void handleMessage(MqttEndpoint endpoint, String topic, byte[] payload) { | 40 | + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { |
| 37 | String clientId = endpoint.clientIdentifier(); | 41 | String clientId = endpoint.clientIdentifier(); |
| 42 | + IotDeviceMessage message = null; | ||
| 43 | + String productKey = null; | ||
| 44 | + String deviceName = null; | ||
| 38 | 45 | ||
| 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 { | 46 | try { |
| 55 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | 47 | + // 1.1 基础检查 |
| 48 | + if (ArrayUtil.isEmpty(payload)) { | ||
| 49 | + return; | ||
| 50 | + } | ||
| 51 | + // 1.2 解析主题,获取 productKey 和 deviceName | ||
| 52 | + String[] topicParts = topic.split("/"); | ||
| 53 | + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | ||
| 54 | + log.warn("[handleBusinessRequest][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | ||
| 55 | + return; | ||
| 56 | + } | ||
| 57 | + productKey = topicParts[2]; | ||
| 58 | + deviceName = topicParts[3]; | ||
| 59 | + // 1.3 校验设备信息,防止伪造设备消息 | ||
| 60 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | ||
| 61 | + Assert.notNull(connectionInfo, "无法获取连接信息"); | ||
| 62 | + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); | ||
| 63 | + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); | ||
| 64 | + | ||
| 65 | + // 2. 反序列化消息 | ||
| 66 | + message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | ||
| 56 | if (message == null) { | 67 | if (message == null) { |
| 57 | - log.warn("[handleMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | 68 | + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); |
| 69 | + sendErrorResponse(endpoint, productKey, deviceName, null, null, | ||
| 70 | + BAD_REQUEST.getCode(), "消息解码失败"); | ||
| 58 | return; | 71 | return; |
| 59 | } | 72 | } |
| 60 | 73 | ||
| 61 | - // 4. 处理业务消息(认证已在连接时完成) | ||
| 62 | - log.info("[handleMessage][收到设备消息,设备: {}.{}, 方法: {}]", | ||
| 63 | - productKey, deviceName, message.getMethod()); | ||
| 64 | - handleBusinessRequest(message, productKey, deviceName); | 74 | + // 3. 处理业务消息 |
| 75 | + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | ||
| 76 | + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); | ||
| 77 | + } catch (ServiceException e) { | ||
| 78 | + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", | ||
| 79 | + clientId, topic, e.getMessage()); | ||
| 80 | + String requestId = message != null ? message.getRequestId() : null; | ||
| 81 | + String method = message != null ? message.getMethod() : null; | ||
| 82 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); | ||
| 83 | + } catch (IllegalArgumentException e) { | ||
| 84 | + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", | ||
| 85 | + clientId, topic, e.getMessage()); | ||
| 86 | + String requestId = message != null ? message.getRequestId() : null; | ||
| 87 | + String method = message != null ? message.getMethod() : null; | ||
| 88 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | ||
| 89 | + BAD_REQUEST.getCode(), e.getMessage()); | ||
| 65 | } catch (Exception e) { | 90 | } catch (Exception e) { |
| 66 | - // TODO @AI:各种情况下的翻译; | ||
| 67 | - log.error("[handleMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | 91 | + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", |
| 68 | clientId, topic, e.getMessage(), e); | 92 | clientId, topic, e.getMessage(), e); |
| 93 | + String requestId = message != null ? message.getRequestId() : null; | ||
| 94 | + String method = message != null ? message.getMethod() : null; | ||
| 95 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | ||
| 96 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | ||
| 69 | } | 97 | } |
| 70 | } | 98 | } |
| 71 | 99 | ||
| 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 | } | 100 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| @@ -40,7 +40,6 @@ public class IotMqttConnectionManager { | @@ -40,7 +40,6 @@ public class IotMqttConnectionManager { | ||
| 40 | */ | 40 | */ |
| 41 | private final Map<Long, MqttEndpoint> deviceEndpointMap = new ConcurrentHashMap<>(); | 41 | private final Map<Long, MqttEndpoint> deviceEndpointMap = new ConcurrentHashMap<>(); |
| 42 | 42 | ||
| 43 | - // TODO @AI:这里会存在返回 "unknown" 的情况么?是不是必须返回,否则还是异常更合理点? | ||
| 44 | /** | 43 | /** |
| 45 | * 安全获取 endpoint 地址 | 44 | * 安全获取 endpoint 地址 |
| 46 | * <p> | 45 | * <p> |
| @@ -134,20 +133,6 @@ public class IotMqttConnectionManager { | @@ -134,20 +133,6 @@ public class IotMqttConnectionManager { | ||
| 134 | } | 133 | } |
| 135 | 134 | ||
| 136 | /** | 135 | /** |
| 137 | - * 检查设备是否在线 | ||
| 138 | - */ | ||
| 139 | - public boolean isDeviceOnline(Long deviceId) { | ||
| 140 | - return deviceEndpointMap.containsKey(deviceId); | ||
| 141 | - } | ||
| 142 | - | ||
| 143 | - /** | ||
| 144 | - * 检查设备是否离线 | ||
| 145 | - */ | ||
| 146 | - public boolean isDeviceOffline(Long deviceId) { | ||
| 147 | - return !isDeviceOnline(deviceId); | ||
| 148 | - } | ||
| 149 | - | ||
| 150 | - /** | ||
| 151 | * 发送消息到设备 | 136 | * 发送消息到设备 |
| 152 | * | 137 | * |
| 153 | * @param deviceId 设备 ID | 138 | * @param deviceId 设备 ID |
| @@ -207,7 +192,7 @@ public class IotMqttConnectionManager { | @@ -207,7 +192,7 @@ public class IotMqttConnectionManager { | ||
| 207 | */ | 192 | */ |
| 208 | private String clientId; | 193 | private String clientId; |
| 209 | 194 | ||
| 210 | - // TODO @AI:是不是要去掉!感觉没用啊; | 195 | + // done @AI:保留 authenticated 字段,用于区分已认证连接和待认证连接(如动态注册场景) |
| 211 | /** | 196 | /** |
| 212 | * 是否已认证 | 197 | * 是否已认证 |
| 213 | */ | 198 | */ |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; | @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; | ||
| 23 | import org.junit.jupiter.api.Test; | 22 | import org.junit.jupiter.api.Test; |
| 24 | 23 | ||
| 25 | import java.util.concurrent.CompletableFuture; | 24 | import java.util.concurrent.CompletableFuture; |
| 26 | -import java.util.concurrent.CountDownLatch; | ||
| 27 | import java.util.concurrent.TimeUnit; | 25 | import java.util.concurrent.TimeUnit; |
| 28 | 26 | ||
| 29 | /** | 27 | /** |
| @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 88 | */ | 86 | */ |
| 89 | @Test | 87 | @Test |
| 90 | public void testAuth() throws Exception { | 88 | public void testAuth() throws Exception { |
| 91 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 92 | - | ||
| 93 | // 1. 构建认证信息 | 89 | // 1. 构建认证信息 |
| 94 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 90 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 95 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", | 91 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", |
| 96 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 92 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 97 | 93 | ||
| 98 | // 2. 创建客户端并连接 | 94 | // 2. 创建客户端并连接 |
| 99 | - MqttClient client = connect(authInfo); | ||
| 100 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 101 | - .onComplete(ar -> { | ||
| 102 | - if (ar.succeeded()) { | ||
| 103 | - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 104 | - // 断开连接 | ||
| 105 | - client.disconnect() | ||
| 106 | - .onComplete(disconnectAr -> { | ||
| 107 | - if (disconnectAr.succeeded()) { | ||
| 108 | - log.info("[testAuth][断开连接成功]"); | ||
| 109 | - } else { | ||
| 110 | - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); | ||
| 111 | - } | ||
| 112 | - latch.countDown(); | ||
| 113 | - }); | ||
| 114 | - } else { | ||
| 115 | - log.error("[testAuth][连接失败]", ar.cause()); | ||
| 116 | - latch.countDown(); | ||
| 117 | - } | ||
| 118 | - }); | ||
| 119 | - | ||
| 120 | - // 3. 等待测试完成 | ||
| 121 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 122 | - if (!completed) { | ||
| 123 | - log.warn("[testAuth][测试超时]"); | 95 | + MqttClient client = createClient(authInfo); |
| 96 | + try { | ||
| 97 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 98 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 99 | + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 100 | + } finally { | ||
| 101 | + disconnect(client); | ||
| 124 | } | 102 | } |
| 125 | } | 103 | } |
| 126 | 104 | ||
| @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 135 | MqttClient client = connectAndAuth(); | 113 | MqttClient client = connectAndAuth(); |
| 136 | log.info("[testPropertyPost][连接认证成功]"); | 114 | log.info("[testPropertyPost][连接认证成功]"); |
| 137 | 115 | ||
| 138 | - // 2. 订阅 _reply 主题 | ||
| 139 | - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 140 | - subscribeReply(client, replyTopic); | ||
| 141 | - | ||
| 142 | - // 3. 构建属性上报消息 | ||
| 143 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 144 | - IdUtil.fastSimpleUUID(), | ||
| 145 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 146 | - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 147 | - .put("width", 1) | ||
| 148 | - .put("height", "2") | ||
| 149 | - .build()), | ||
| 150 | - null, null, null); | ||
| 151 | - | ||
| 152 | - // 4. 发布消息并等待响应 | ||
| 153 | - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 154 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 155 | - log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 156 | - | ||
| 157 | - // 5. 断开连接 | ||
| 158 | - disconnect(client); | 116 | + try { |
| 117 | + // 2.1 构建属性上报消息 | ||
| 118 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 119 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 120 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 121 | + .put("width", 1) | ||
| 122 | + .put("height", "2") | ||
| 123 | + .build())); | ||
| 124 | + | ||
| 125 | + // 2.2 订阅 _reply 主题 | ||
| 126 | + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 127 | + subscribe(client, replyTopic); | ||
| 128 | + | ||
| 129 | + // 2.2 发布消息并等待响应 | ||
| 130 | + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 131 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 132 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 133 | + } finally { | ||
| 134 | + disconnect(client); | ||
| 135 | + } | ||
| 159 | } | 136 | } |
| 160 | 137 | ||
| 161 | // ===================== 直连设备事件上报测试 ===================== | 138 | // ===================== 直连设备事件上报测试 ===================== |
| @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 169 | MqttClient client = connectAndAuth(); | 146 | MqttClient client = connectAndAuth(); |
| 170 | log.info("[testEventPost][连接认证成功]"); | 147 | log.info("[testEventPost][连接认证成功]"); |
| 171 | 148 | ||
| 172 | - // 2. 订阅 _reply 主题 | ||
| 173 | - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 174 | - subscribeReply(client, replyTopic); | ||
| 175 | - | ||
| 176 | - // 3. 构建事件上报消息 | ||
| 177 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 178 | - IdUtil.fastSimpleUUID(), | ||
| 179 | - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 180 | - IotDeviceEventPostReqDTO.of( | ||
| 181 | - "eat", | ||
| 182 | - MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 183 | - System.currentTimeMillis()), | ||
| 184 | - null, null, null); | ||
| 185 | - | ||
| 186 | - // 4. 发布消息并等待响应 | ||
| 187 | - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 188 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 189 | - log.info("[testEventPost][响应消息: {}]", response); | ||
| 190 | - | ||
| 191 | - // 5. 断开连接 | ||
| 192 | - disconnect(client); | 149 | + try { |
| 150 | + // 2.1 构建事件上报消息 | ||
| 151 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 152 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 153 | + IotDeviceEventPostReqDTO.of( | ||
| 154 | + "eat", | ||
| 155 | + MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 156 | + System.currentTimeMillis())); | ||
| 157 | + | ||
| 158 | + // 2.2 订阅 _reply 主题 | ||
| 159 | + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 160 | + subscribe(client, replyTopic); | ||
| 161 | + | ||
| 162 | + // 3. 发布消息并等待响应 | ||
| 163 | + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 164 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 165 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 166 | + } finally { | ||
| 167 | + disconnect(client); | ||
| 168 | + } | ||
| 193 | } | 169 | } |
| 194 | 170 | ||
| 195 | // ===================== 设备动态注册测试(一型一密) ===================== | 171 | // ===================== 设备动态注册测试(一型一密) ===================== |
| @@ -207,27 +183,30 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -207,27 +183,30 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 207 | MqttClient client = connectAndAuth(); | 183 | MqttClient client = connectAndAuth(); |
| 208 | log.info("[testDeviceRegister][连接认证成功]"); | 184 | log.info("[testDeviceRegister][连接认证成功]"); |
| 209 | 185 | ||
| 210 | - // 2.1 构建注册消息 | ||
| 211 | - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 212 | - registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 213 | - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); | ||
| 214 | - registerReqDTO.setProductSecret("test-product-secret"); | ||
| 215 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | ||
| 216 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | ||
| 217 | - // 2.2 订阅 _reply 主题 | ||
| 218 | - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", | ||
| 219 | - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 220 | - subscribeReply(client, replyTopic); | ||
| 221 | - | ||
| 222 | - // 3. 发布消息并等待响应 | ||
| 223 | - String topic = String.format("/sys/%s/%s/thing/auth/register", | ||
| 224 | - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 225 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 226 | - log.info("[testDeviceRegister][响应消息: {}]", response); | ||
| 227 | - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 228 | - | ||
| 229 | - // 4. 断开连接 | ||
| 230 | - disconnect(client); | 186 | + try { |
| 187 | + // 2.1 构建注册消息 | ||
| 188 | + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() | ||
| 189 | + .setProductKey(PRODUCT_KEY) | ||
| 190 | + .setDeviceName("test-mqtt-" + System.currentTimeMillis()) | ||
| 191 | + .setProductSecret("test-product-secret"); | ||
| 192 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 193 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), | ||
| 194 | + registerReqDTO); | ||
| 195 | + | ||
| 196 | + // 2.2 订阅 _reply 主题 | ||
| 197 | + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", | ||
| 198 | + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 199 | + subscribe(client, replyTopic); | ||
| 200 | + | ||
| 201 | + // 3. 发布消息并等待响应 | ||
| 202 | + String topic = String.format("/sys/%s/%s/thing/auth/register", | ||
| 203 | + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 204 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 205 | + log.info("[testDeviceRegister][响应消息: {}]", response); | ||
| 206 | + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 207 | + } finally { | ||
| 208 | + disconnect(client); | ||
| 209 | + } | ||
| 231 | } | 210 | } |
| 232 | 211 | ||
| 233 | // ===================== 订阅下行消息测试 ===================== | 212 | // ===================== 订阅下行消息测试 ===================== |
| @@ -237,44 +216,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -237,44 +216,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 237 | */ | 216 | */ |
| 238 | @Test | 217 | @Test |
| 239 | public void testSubscribe() throws Exception { | 218 | public void testSubscribe() throws Exception { |
| 240 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 241 | - | ||
| 242 | // 1. 连接并认证 | 219 | // 1. 连接并认证 |
| 243 | MqttClient client = connectAndAuth(); | 220 | MqttClient client = connectAndAuth(); |
| 244 | log.info("[testSubscribe][连接认证成功]"); | 221 | log.info("[testSubscribe][连接认证成功]"); |
| 245 | 222 | ||
| 246 | - // 2. 设置消息处理器 | ||
| 247 | - client.publishHandler(message -> { | ||
| 248 | - log.info("[testSubscribe][收到消息: topic={}, payload={}]", | ||
| 249 | - message.topicName(), message.payload().toString()); | ||
| 250 | - }); | ||
| 251 | - | ||
| 252 | - // 3. 订阅下行主题 | ||
| 253 | - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | ||
| 254 | - log.info("[testSubscribe][订阅主题: {}]", topic); | ||
| 255 | - | ||
| 256 | - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 257 | - .onComplete(subscribeAr -> { | ||
| 258 | - if (subscribeAr.succeeded()) { | ||
| 259 | - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ||
| 260 | - // 保持连接 30 秒等待消息 | ||
| 261 | - vertx.setTimer(30000, id -> { | ||
| 262 | - client.disconnect() | ||
| 263 | - .onComplete(disconnectAr -> { | ||
| 264 | - log.info("[testSubscribe][断开连接]"); | ||
| 265 | - latch.countDown(); | ||
| 266 | - }); | ||
| 267 | - }); | ||
| 268 | - } else { | ||
| 269 | - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); | ||
| 270 | - latch.countDown(); | ||
| 271 | - } | ||
| 272 | - }); | ||
| 273 | - | ||
| 274 | - // 4. 等待测试完成 | ||
| 275 | - boolean completed = latch.await(60, TimeUnit.SECONDS); | ||
| 276 | - if (!completed) { | ||
| 277 | - log.warn("[testSubscribe][测试超时]"); | 223 | + try { |
| 224 | + // 2. 设置消息处理器 | ||
| 225 | + client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", | ||
| 226 | + message.topicName(), message.payload().toString())); | ||
| 227 | + | ||
| 228 | + // 3. 订阅下行主题 | ||
| 229 | + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | ||
| 230 | + log.info("[testSubscribe][订阅主题: {}]", topic); | ||
| 231 | + subscribe(client, topic); | ||
| 232 | + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ||
| 233 | + | ||
| 234 | + // 4. 保持连接 30 秒等待消息 | ||
| 235 | + Thread.sleep(30000); | ||
| 236 | + } finally { | ||
| 237 | + disconnect(client); | ||
| 278 | } | 238 | } |
| 279 | } | 239 | } |
| 280 | 240 | ||
| @@ -286,7 +246,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -286,7 +246,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 286 | * @param authInfo 认证信息 | 246 | * @param authInfo 认证信息 |
| 287 | * @return MQTT 客户端 | 247 | * @return MQTT 客户端 |
| 288 | */ | 248 | */ |
| 289 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | 249 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { |
| 290 | MqttClientOptions options = new MqttClientOptions() | 250 | MqttClientOptions options = new MqttClientOptions() |
| 291 | .setClientId(authInfo.getClientId()) | 251 | .setClientId(authInfo.getClientId()) |
| 292 | .setUsername(authInfo.getUsername()) | 252 | .setUsername(authInfo.getUsername()) |
| @@ -302,44 +262,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -302,44 +262,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 302 | * @return 已认证的 MQTT 客户端 | 262 | * @return 已认证的 MQTT 客户端 |
| 303 | */ | 263 | */ |
| 304 | private MqttClient connectAndAuth() throws Exception { | 264 | private MqttClient connectAndAuth() throws Exception { |
| 305 | - // 1. 创建客户端并连接 | ||
| 306 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 265 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 307 | - MqttClient client = connect(authInfo); | ||
| 308 | - | ||
| 309 | - // 2.1 连接 | ||
| 310 | - CompletableFuture<MqttClient> future = new CompletableFuture<>(); | 266 | + MqttClient client = createClient(authInfo); |
| 311 | client.connect(SERVER_PORT, SERVER_HOST) | 267 | client.connect(SERVER_PORT, SERVER_HOST) |
| 312 | - .onComplete(ar -> { | ||
| 313 | - if (ar.succeeded()) { | ||
| 314 | - future.complete(client); | ||
| 315 | - } else { | ||
| 316 | - future.completeExceptionally(ar.cause()); | ||
| 317 | - } | ||
| 318 | - }); | ||
| 319 | - // 2.2 等待连接结果 | ||
| 320 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 268 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 269 | + return client; | ||
| 321 | } | 270 | } |
| 322 | 271 | ||
| 323 | /** | 272 | /** |
| 324 | - * 订阅响应主题 | 273 | + * 订阅主题 |
| 325 | * | 274 | * |
| 326 | - * @param client MQTT 客户端 | ||
| 327 | - * @param replyTopic 响应主题 | 275 | + * @param client MQTT 客户端 |
| 276 | + * @param topic 主题 | ||
| 328 | */ | 277 | */ |
| 329 | - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 330 | - // 1. 订阅响应主题 | ||
| 331 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 332 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 333 | - .onComplete(ar -> { | ||
| 334 | - if (ar.succeeded()) { | ||
| 335 | - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 336 | - future.complete(null); | ||
| 337 | - } else { | ||
| 338 | - future.completeExceptionally(ar.cause()); | ||
| 339 | - } | ||
| 340 | - }); | ||
| 341 | - // 2. 等待订阅结果 | ||
| 342 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 278 | + private void subscribe(MqttClient client, String topic) throws Exception { |
| 279 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 280 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 281 | + log.info("[subscribe][订阅主题成功: {}]", topic); | ||
| 343 | } | 282 | } |
| 344 | 283 | ||
| 345 | /** | 284 | /** |
| @@ -350,34 +289,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -350,34 +289,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 350 | * @param request 请求消息 | 289 | * @param request 请求消息 |
| 351 | * @return 响应消息 | 290 | * @return 响应消息 |
| 352 | */ | 291 | */ |
| 353 | - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | 292 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) |
| 293 | + throws Exception { | ||
| 354 | // 1. 设置消息处理器,接收响应 | 294 | // 1. 设置消息处理器,接收响应 |
| 355 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | 295 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 356 | client.publishHandler(message -> { | 296 | client.publishHandler(message -> { |
| 357 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | 297 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 358 | message.topicName(), message.payload().toString()); | 298 | message.topicName(), message.payload().toString()); |
| 359 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | 299 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); |
| 360 | - future.complete(response); | 300 | + responseFuture.complete(response); |
| 361 | }); | 301 | }); |
| 362 | 302 | ||
| 363 | // 2. 编码并发布消息 | 303 | // 2. 编码并发布消息 |
| 364 | byte[] payload = CODEC.encode(request); | 304 | byte[] payload = CODEC.encode(request); |
| 365 | log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | 305 | log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", |
| 366 | CODEC.type(), topic, new String(payload)); | 306 | CODEC.type(), topic, new String(payload)); |
| 367 | - | ||
| 368 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | 307 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) |
| 369 | - .onComplete(ar -> { | ||
| 370 | - if (ar.succeeded()) { | ||
| 371 | - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 372 | - } else { | ||
| 373 | - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 374 | - future.completeExceptionally(ar.cause()); | ||
| 375 | - } | ||
| 376 | - }); | ||
| 377 | - | ||
| 378 | - // 3. 等待响应(超时返回 null) | 308 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 309 | + log.info("[publishAndWaitReply][消息发布成功]"); | ||
| 310 | + | ||
| 311 | + // 3. 等待响应 | ||
| 379 | try { | 312 | try { |
| 380 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 313 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 381 | } catch (Exception e) { | 314 | } catch (Exception e) { |
| 382 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); | 315 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 383 | return null; | 316 | return null; |
| @@ -390,19 +323,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -390,19 +323,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 390 | * @param client MQTT 客户端 | 323 | * @param client MQTT 客户端 |
| 391 | */ | 324 | */ |
| 392 | private void disconnect(MqttClient client) throws Exception { | 325 | private void disconnect(MqttClient client) throws Exception { |
| 393 | - // 1. 断开连接 | ||
| 394 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 395 | client.disconnect() | 326 | client.disconnect() |
| 396 | - .onComplete(ar -> { | ||
| 397 | - if (ar.succeeded()) { | ||
| 398 | - log.info("[disconnect][断开连接成功]"); | ||
| 399 | - future.complete(null); | ||
| 400 | - } else { | ||
| 401 | - future.completeExceptionally(ar.cause()); | ||
| 402 | - } | ||
| 403 | - }); | ||
| 404 | - // 2. 等待断开结果 | ||
| 405 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 327 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 328 | + log.info("[disconnect][断开连接成功]"); | ||
| 406 | } | 329 | } |
| 407 | 330 | ||
| 408 | } | 331 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java
| @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.ListUtil; | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | import cn.hutool.core.map.MapUtil; | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | -import cn.hutool.core.util.IdUtil; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; | @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; | ||
| 27 | import org.junit.jupiter.api.Test; | 26 | import org.junit.jupiter.api.Test; |
| 28 | 27 | ||
| 29 | import java.util.Collections; | 28 | import java.util.Collections; |
| 30 | -import java.util.List; | ||
| 31 | import java.util.Map; | 29 | import java.util.Map; |
| 32 | import java.util.concurrent.CompletableFuture; | 30 | import java.util.concurrent.CompletableFuture; |
| 33 | -import java.util.concurrent.CountDownLatch; | ||
| 34 | import java.util.concurrent.TimeUnit; | 31 | import java.util.concurrent.TimeUnit; |
| 35 | 32 | ||
| 36 | /** | 33 | /** |
| @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 103 | */ | 100 | */ |
| 104 | @Test | 101 | @Test |
| 105 | public void testAuth() throws Exception { | 102 | public void testAuth() throws Exception { |
| 106 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 107 | - | ||
| 108 | // 1. 构建认证信息 | 103 | // 1. 构建认证信息 |
| 109 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | 104 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 110 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | 105 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 112 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 107 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 113 | 108 | ||
| 114 | // 2. 创建客户端并连接 | 109 | // 2. 创建客户端并连接 |
| 115 | - MqttClient client = connect(authInfo); | ||
| 116 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 117 | - .onComplete(ar -> { | ||
| 118 | - if (ar.succeeded()) { | ||
| 119 | - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 120 | - // 断开连接 | ||
| 121 | - client.disconnect() | ||
| 122 | - .onComplete(disconnectAr -> { | ||
| 123 | - if (disconnectAr.succeeded()) { | ||
| 124 | - log.info("[testAuth][断开连接成功]"); | ||
| 125 | - } else { | ||
| 126 | - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); | ||
| 127 | - } | ||
| 128 | - latch.countDown(); | ||
| 129 | - }); | ||
| 130 | - } else { | ||
| 131 | - log.error("[testAuth][连接失败]", ar.cause()); | ||
| 132 | - latch.countDown(); | ||
| 133 | - } | ||
| 134 | - }); | ||
| 135 | - | ||
| 136 | - // 3. 等待测试完成 | ||
| 137 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 138 | - if (!completed) { | ||
| 139 | - log.warn("[testAuth][测试超时]"); | 110 | + MqttClient client = createClient(authInfo); |
| 111 | + try { | ||
| 112 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 113 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 114 | + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 115 | + } finally { | ||
| 116 | + disconnect(client); | ||
| 140 | } | 117 | } |
| 141 | } | 118 | } |
| 142 | 119 | ||
| @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 153 | MqttClient client = connectAndAuth(); | 130 | MqttClient client = connectAndAuth(); |
| 154 | log.info("[testTopoAdd][连接认证成功]"); | 131 | log.info("[testTopoAdd][连接认证成功]"); |
| 155 | 132 | ||
| 156 | - // 2.1 订阅 _reply 主题 | ||
| 157 | - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", | ||
| 158 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 159 | - subscribeReply(client, replyTopic); | ||
| 160 | - | ||
| 161 | - // 2.2 构建子设备认证信息 | ||
| 162 | - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 163 | - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | ||
| 164 | - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | ||
| 165 | - .setClientId(subAuthInfo.getClientId()) | ||
| 166 | - .setUsername(subAuthInfo.getUsername()) | ||
| 167 | - .setPassword(subAuthInfo.getPassword()); | ||
| 168 | - | ||
| 169 | - // 2.3 构建请求消息 | ||
| 170 | - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); | ||
| 171 | - params.setSubDevices(Collections.singletonList(subDeviceAuth)); | ||
| 172 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 173 | - IdUtil.fastSimpleUUID(), | ||
| 174 | - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 175 | - params, | ||
| 176 | - null, null, null); | ||
| 177 | - | ||
| 178 | - // 3. 发布消息并等待响应 | ||
| 179 | - String topic = String.format("/sys/%s/%s/thing/topo/add", | ||
| 180 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 181 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 182 | - log.info("[testTopoAdd][响应消息: {}]", response); | ||
| 183 | - | ||
| 184 | - // 4. 断开连接 | ||
| 185 | - disconnect(client); | 133 | + try { |
| 134 | + // 2.1 构建子设备认证信息 | ||
| 135 | + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 136 | + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | ||
| 137 | + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | ||
| 138 | + .setClientId(subAuthInfo.getClientId()) | ||
| 139 | + .setUsername(subAuthInfo.getUsername()) | ||
| 140 | + .setPassword(subAuthInfo.getPassword()); | ||
| 141 | + | ||
| 142 | + // 2.2 构建请求消息 | ||
| 143 | + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO() | ||
| 144 | + .setSubDevices(Collections.singletonList(subDeviceAuth)); | ||
| 145 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 146 | + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 147 | + params); | ||
| 148 | + | ||
| 149 | + // 2.3 订阅 _reply 主题 | ||
| 150 | + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", | ||
| 151 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 152 | + subscribe(client, replyTopic); | ||
| 153 | + | ||
| 154 | + // 3. 发布消息并等待响应 | ||
| 155 | + String topic = String.format("/sys/%s/%s/thing/topo/add", | ||
| 156 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 157 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 158 | + log.info("[testTopoAdd][响应消息: {}]", response); | ||
| 159 | + } finally { | ||
| 160 | + disconnect(client); | ||
| 161 | + } | ||
| 186 | } | 162 | } |
| 187 | 163 | ||
| 188 | /** | 164 | /** |
| @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 196 | MqttClient client = connectAndAuth(); | 172 | MqttClient client = connectAndAuth(); |
| 197 | log.info("[testTopoDelete][连接认证成功]"); | 173 | log.info("[testTopoDelete][连接认证成功]"); |
| 198 | 174 | ||
| 199 | - // 2.1 订阅 _reply 主题 | ||
| 200 | - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", | ||
| 201 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 202 | - subscribeReply(client, replyTopic); | ||
| 203 | - | ||
| 204 | - // 2.2 构建请求消息 | ||
| 205 | - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); | ||
| 206 | - params.setSubDevices(Collections.singletonList( | ||
| 207 | - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | ||
| 208 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 209 | - IdUtil.fastSimpleUUID(), | ||
| 210 | - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 211 | - params, | ||
| 212 | - null, null, null); | ||
| 213 | - | ||
| 214 | - // 3. 发布消息并等待响应 | ||
| 215 | - String topic = String.format("/sys/%s/%s/thing/topo/delete", | ||
| 216 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 217 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 218 | - log.info("[testTopoDelete][响应消息: {}]", response); | ||
| 219 | - | ||
| 220 | - // 4. 断开连接 | ||
| 221 | - disconnect(client); | 175 | + try { |
| 176 | + // 2.1 构建请求消息 | ||
| 177 | + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO() | ||
| 178 | + .setSubDevices(Collections.singletonList( | ||
| 179 | + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | ||
| 180 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 181 | + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 182 | + params); | ||
| 183 | + | ||
| 184 | + // 2.2 订阅 _reply 主题 | ||
| 185 | + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", | ||
| 186 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 187 | + subscribe(client, replyTopic); | ||
| 188 | + | ||
| 189 | + // 3. 发布消息并等待响应 | ||
| 190 | + String topic = String.format("/sys/%s/%s/thing/topo/delete", | ||
| 191 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 192 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 193 | + log.info("[testTopoDelete][响应消息: {}]", response); | ||
| 194 | + } finally { | ||
| 195 | + disconnect(client); | ||
| 196 | + } | ||
| 222 | } | 197 | } |
| 223 | 198 | ||
| 224 | /** | 199 | /** |
| @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 232 | MqttClient client = connectAndAuth(); | 207 | MqttClient client = connectAndAuth(); |
| 233 | log.info("[testTopoGet][连接认证成功]"); | 208 | log.info("[testTopoGet][连接认证成功]"); |
| 234 | 209 | ||
| 235 | - // 2.1 订阅 _reply 主题 | ||
| 236 | - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", | ||
| 237 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 238 | - subscribeReply(client, replyTopic); | ||
| 239 | - | ||
| 240 | - // 2.2 构建请求消息 | ||
| 241 | - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | ||
| 242 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 243 | - IdUtil.fastSimpleUUID(), | ||
| 244 | - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 245 | - params, | ||
| 246 | - null, null, null); | ||
| 247 | - | ||
| 248 | - // 3. 发布消息并等待响应 | ||
| 249 | - String topic = String.format("/sys/%s/%s/thing/topo/get", | ||
| 250 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 251 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 252 | - log.info("[testTopoGet][响应消息: {}]", response); | ||
| 253 | - | ||
| 254 | - // 4. 断开连接 | ||
| 255 | - disconnect(client); | 210 | + try { |
| 211 | + // 2.1 构建请求消息 | ||
| 212 | + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | ||
| 213 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 214 | + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 215 | + params); | ||
| 216 | + | ||
| 217 | + // 2.2 订阅 _reply 主题 | ||
| 218 | + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", | ||
| 219 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 220 | + subscribe(client, replyTopic); | ||
| 221 | + | ||
| 222 | + // 3. 发布消息并等待响应 | ||
| 223 | + String topic = String.format("/sys/%s/%s/thing/topo/get", | ||
| 224 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 225 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 226 | + log.info("[testTopoGet][响应消息: {}]", response); | ||
| 227 | + } finally { | ||
| 228 | + disconnect(client); | ||
| 229 | + } | ||
| 256 | } | 230 | } |
| 257 | 231 | ||
| 258 | // ===================== 子设备注册测试 ===================== | 232 | // ===================== 子设备注册测试 ===================== |
| @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 270 | MqttClient client = connectAndAuth(); | 244 | MqttClient client = connectAndAuth(); |
| 271 | log.info("[testSubDeviceRegister][连接认证成功]"); | 245 | log.info("[testSubDeviceRegister][连接认证成功]"); |
| 272 | 246 | ||
| 273 | - // 2.1 订阅 _reply 主题 | ||
| 274 | - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", | ||
| 275 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 276 | - subscribeReply(client, replyTopic); | ||
| 277 | - | ||
| 278 | - // 2.2 构建请求消息 | ||
| 279 | - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); | ||
| 280 | - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); | ||
| 281 | - subDevice.setDeviceName("mougezishebei-mqtt"); | ||
| 282 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 283 | - IdUtil.fastSimpleUUID(), | ||
| 284 | - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | ||
| 285 | - Collections.singletonList(subDevice), | ||
| 286 | - null, null, null); | ||
| 287 | - | ||
| 288 | - // 3. 发布消息并等待响应 | ||
| 289 | - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", | ||
| 290 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 291 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 292 | - log.info("[testSubDeviceRegister][响应消息: {}]", response); | ||
| 293 | - | ||
| 294 | - // 4. 断开连接 | ||
| 295 | - disconnect(client); | 247 | + try { |
| 248 | + // 2.1 构建请求消息 | ||
| 249 | + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() | ||
| 250 | + .setProductKey(SUB_DEVICE_PRODUCT_KEY) | ||
| 251 | + .setDeviceName("mougezishebei-mqtt"); | ||
| 252 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 253 | + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | ||
| 254 | + Collections.singletonList(subDevice)); | ||
| 255 | + | ||
| 256 | + // 2.2 订阅 _reply 主题 | ||
| 257 | + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", | ||
| 258 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 259 | + subscribe(client, replyTopic); | ||
| 260 | + | ||
| 261 | + // 3. 发布消息并等待响应 | ||
| 262 | + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", | ||
| 263 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 264 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 265 | + log.info("[testSubDeviceRegister][响应消息: {}]", response); | ||
| 266 | + } finally { | ||
| 267 | + disconnect(client); | ||
| 268 | + } | ||
| 296 | } | 269 | } |
| 297 | 270 | ||
| 298 | // ===================== 批量上报测试 ===================== | 271 | // ===================== 批量上报测试 ===================== |
| @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 308 | MqttClient client = connectAndAuth(); | 281 | MqttClient client = connectAndAuth(); |
| 309 | log.info("[testPropertyPackPost][连接认证成功]"); | 282 | log.info("[testPropertyPackPost][连接认证成功]"); |
| 310 | 283 | ||
| 311 | - // 2.1 订阅 _reply 主题 | ||
| 312 | - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", | ||
| 313 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 314 | - subscribeReply(client, replyTopic); | ||
| 315 | - | ||
| 316 | - // 2.2 构建【网关设备】自身属性 | ||
| 317 | - Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder() | ||
| 318 | - .put("temperature", 25.5) | ||
| 319 | - .build(); | ||
| 320 | - | ||
| 321 | - // 2.3 构建【网关设备】自身事件 | ||
| 322 | - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 323 | - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); | ||
| 324 | - gatewayEvent.setTime(System.currentTimeMillis()); | ||
| 325 | - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil | ||
| 326 | - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 327 | - .put("statusReport", gatewayEvent) | ||
| 328 | - .build(); | ||
| 329 | - | ||
| 330 | - // 2.4 构建【网关子设备】属性 | ||
| 331 | - Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder() | ||
| 332 | - .put("power", 100) | ||
| 333 | - .build(); | ||
| 334 | - | ||
| 335 | - // 2.5 构建【网关子设备】事件 | ||
| 336 | - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 337 | - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); | ||
| 338 | - subDeviceEvent.setTime(System.currentTimeMillis()); | ||
| 339 | - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil | ||
| 340 | - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 341 | - .put("healthCheck", subDeviceEvent) | ||
| 342 | - .build(); | ||
| 343 | - | ||
| 344 | - // 2.6 构建子设备数据 | ||
| 345 | - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); | ||
| 346 | - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); | ||
| 347 | - subDeviceData.setProperties(subDeviceProperties); | ||
| 348 | - subDeviceData.setEvents(subDeviceEvents); | ||
| 349 | - | ||
| 350 | - // 2.7 构建请求消息 | ||
| 351 | - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); | ||
| 352 | - params.setProperties(gatewayProperties); | ||
| 353 | - params.setEvents(gatewayEvents); | ||
| 354 | - params.setSubDevices(ListUtil.of(subDeviceData)); | ||
| 355 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 356 | - IdUtil.fastSimpleUUID(), | ||
| 357 | - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 358 | - params, | ||
| 359 | - null, null, null); | ||
| 360 | - | ||
| 361 | - // 3. 发布消息并等待响应 | ||
| 362 | - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", | ||
| 363 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 364 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 365 | - log.info("[testPropertyPackPost][响应消息: {}]", response); | ||
| 366 | - | ||
| 367 | - // 4. 断开连接 | ||
| 368 | - disconnect(client); | 284 | + try { |
| 285 | + // 2.1 构建【网关设备】自身属性 | ||
| 286 | + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder() | ||
| 287 | + .put("temperature", 25.5) | ||
| 288 | + .build(); | ||
| 289 | + | ||
| 290 | + // 2.2 构建【网关设备】自身事件 | ||
| 291 | + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() | ||
| 292 | + .setValue(MapUtil.builder().put("message", "gateway started").build()) | ||
| 293 | + .setTime(System.currentTimeMillis()); | ||
| 294 | + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil | ||
| 295 | + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 296 | + .put("statusReport", gatewayEvent) | ||
| 297 | + .build(); | ||
| 298 | + | ||
| 299 | + // 2.3 构建【网关子设备】属性 | ||
| 300 | + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder() | ||
| 301 | + .put("power", 100) | ||
| 302 | + .build(); | ||
| 303 | + | ||
| 304 | + // 2.4 构建【网关子设备】事件 | ||
| 305 | + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() | ||
| 306 | + .setValue(MapUtil.builder().put("errorCode", 0).build()) | ||
| 307 | + .setTime(System.currentTimeMillis()); | ||
| 308 | + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil | ||
| 309 | + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 310 | + .put("healthCheck", subDeviceEvent) | ||
| 311 | + .build(); | ||
| 312 | + | ||
| 313 | + // 2.5 构建子设备数据 | ||
| 314 | + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() | ||
| 315 | + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) | ||
| 316 | + .setProperties(subDeviceProperties) | ||
| 317 | + .setEvents(subDeviceEvents); | ||
| 318 | + | ||
| 319 | + // 2.6 构建请求消息 | ||
| 320 | + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO() | ||
| 321 | + .setProperties(gatewayProperties) | ||
| 322 | + .setEvents(gatewayEvents) | ||
| 323 | + .setSubDevices(ListUtil.of(subDeviceData)); | ||
| 324 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 325 | + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 326 | + params); | ||
| 327 | + | ||
| 328 | + // 2.7 订阅 _reply 主题 | ||
| 329 | + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", | ||
| 330 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 331 | + subscribe(client, replyTopic); | ||
| 332 | + | ||
| 333 | + // 3. 发布消息并等待响应 | ||
| 334 | + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", | ||
| 335 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 336 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 337 | + log.info("[testPropertyPackPost][响应消息: {}]", response); | ||
| 338 | + } finally { | ||
| 339 | + disconnect(client); | ||
| 340 | + } | ||
| 369 | } | 341 | } |
| 370 | 342 | ||
| 371 | // ===================== 辅助方法 ===================== | 343 | // ===================== 辅助方法 ===================== |
| @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 376 | * @param authInfo 认证信息 | 348 | * @param authInfo 认证信息 |
| 377 | * @return MQTT 客户端 | 349 | * @return MQTT 客户端 |
| 378 | */ | 350 | */ |
| 379 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | 351 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { |
| 380 | MqttClientOptions options = new MqttClientOptions() | 352 | MqttClientOptions options = new MqttClientOptions() |
| 381 | .setClientId(authInfo.getClientId()) | 353 | .setClientId(authInfo.getClientId()) |
| 382 | .setUsername(authInfo.getUsername()) | 354 | .setUsername(authInfo.getUsername()) |
| @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 392 | * @return 已认证的 MQTT 客户端 | 364 | * @return 已认证的 MQTT 客户端 |
| 393 | */ | 365 | */ |
| 394 | private MqttClient connectAndAuth() throws Exception { | 366 | private MqttClient connectAndAuth() throws Exception { |
| 395 | - // 1. 创建客户端并连接 | ||
| 396 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | 367 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 397 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | 368 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| 398 | - MqttClient client = connect(authInfo); | ||
| 399 | - | ||
| 400 | - // 2.1 连接 | ||
| 401 | - CompletableFuture<MqttClient> future = new CompletableFuture<>(); | 369 | + MqttClient client = createClient(authInfo); |
| 402 | client.connect(SERVER_PORT, SERVER_HOST) | 370 | client.connect(SERVER_PORT, SERVER_HOST) |
| 403 | - .onComplete(ar -> { | ||
| 404 | - if (ar.succeeded()) { | ||
| 405 | - future.complete(client); | ||
| 406 | - } else { | ||
| 407 | - future.completeExceptionally(ar.cause()); | ||
| 408 | - } | ||
| 409 | - }); | ||
| 410 | - // 2.2 等待连接结果 | ||
| 411 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 371 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 372 | + return client; | ||
| 412 | } | 373 | } |
| 413 | 374 | ||
| 414 | /** | 375 | /** |
| 415 | - * 订阅响应主题 | 376 | + * 订阅主题 |
| 416 | * | 377 | * |
| 417 | - * @param client MQTT 客户端 | ||
| 418 | - * @param replyTopic 响应主题 | 378 | + * @param client MQTT 客户端 |
| 379 | + * @param topic 主题 | ||
| 419 | */ | 380 | */ |
| 420 | - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 421 | - // 1. 订阅响应主题 | ||
| 422 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 423 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 424 | - .onComplete(ar -> { | ||
| 425 | - if (ar.succeeded()) { | ||
| 426 | - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 427 | - future.complete(null); | ||
| 428 | - } else { | ||
| 429 | - future.completeExceptionally(ar.cause()); | ||
| 430 | - } | ||
| 431 | - }); | ||
| 432 | - // 2. 等待订阅结果 | ||
| 433 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 381 | + private void subscribe(MqttClient client, String topic) throws Exception { |
| 382 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 383 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 384 | + log.info("[subscribe][订阅主题成功: {}]", topic); | ||
| 434 | } | 385 | } |
| 435 | 386 | ||
| 436 | /** | 387 | /** |
| @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 441 | * @param request 请求消息 | 392 | * @param request 请求消息 |
| 442 | * @return 响应消息 | 393 | * @return 响应消息 |
| 443 | */ | 394 | */ |
| 444 | - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | 395 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) |
| 396 | + throws Exception { | ||
| 445 | // 1. 设置消息处理器,接收响应 | 397 | // 1. 设置消息处理器,接收响应 |
| 446 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | 398 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 447 | client.publishHandler(message -> { | 399 | client.publishHandler(message -> { |
| 448 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | 400 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 449 | message.topicName(), message.payload().toString()); | 401 | message.topicName(), message.payload().toString()); |
| 450 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | 402 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); |
| 451 | - future.complete(response); | 403 | + responseFuture.complete(response); |
| 452 | }); | 404 | }); |
| 453 | 405 | ||
| 454 | // 2. 编码并发布消息 | 406 | // 2. 编码并发布消息 |
| 455 | byte[] payload = CODEC.encode(request); | 407 | byte[] payload = CODEC.encode(request); |
| 456 | log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | 408 | log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", |
| 457 | CODEC.type(), topic, new String(payload)); | 409 | CODEC.type(), topic, new String(payload)); |
| 458 | - | ||
| 459 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | 410 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) |
| 460 | - .onComplete(ar -> { | ||
| 461 | - if (ar.succeeded()) { | ||
| 462 | - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 463 | - } else { | ||
| 464 | - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 465 | - future.completeExceptionally(ar.cause()); | ||
| 466 | - } | ||
| 467 | - }); | ||
| 468 | - | ||
| 469 | - // 3. 等待响应(超时返回 null) | 411 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 412 | + log.info("[publishAndWaitReply][消息发布成功]"); | ||
| 413 | + | ||
| 414 | + // 3. 等待响应 | ||
| 470 | try { | 415 | try { |
| 471 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 416 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 472 | } catch (Exception e) { | 417 | } catch (Exception e) { |
| 473 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); | 418 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 474 | return null; | 419 | return null; |
| @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 481 | * @param client MQTT 客户端 | 426 | * @param client MQTT 客户端 |
| 482 | */ | 427 | */ |
| 483 | private void disconnect(MqttClient client) throws Exception { | 428 | private void disconnect(MqttClient client) throws Exception { |
| 484 | - // 1. 断开连接 | ||
| 485 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 486 | client.disconnect() | 429 | client.disconnect() |
| 487 | - .onComplete(ar -> { | ||
| 488 | - if (ar.succeeded()) { | ||
| 489 | - log.info("[disconnect][断开连接成功]"); | ||
| 490 | - future.complete(null); | ||
| 491 | - } else { | ||
| 492 | - future.completeExceptionally(ar.cause()); | ||
| 493 | - } | ||
| 494 | - }); | ||
| 495 | - // 2. 等待断开结果 | ||
| 496 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 430 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 431 | + log.info("[disconnect][断开连接成功]"); | ||
| 497 | } | 432 | } |
| 498 | 433 | ||
| 499 | } | 434 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; | @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; | ||
| 22 | import org.junit.jupiter.api.Test; | 21 | import org.junit.jupiter.api.Test; |
| 23 | 22 | ||
| 24 | import java.util.concurrent.CompletableFuture; | 23 | import java.util.concurrent.CompletableFuture; |
| 25 | -import java.util.concurrent.CountDownLatch; | ||
| 26 | import java.util.concurrent.TimeUnit; | 24 | import java.util.concurrent.TimeUnit; |
| 27 | 25 | ||
| 28 | /** | 26 | /** |
| @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 90 | */ | 88 | */ |
| 91 | @Test | 89 | @Test |
| 92 | public void testAuth() throws Exception { | 90 | public void testAuth() throws Exception { |
| 93 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 94 | - | ||
| 95 | // 1. 构建认证信息 | 91 | // 1. 构建认证信息 |
| 96 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 92 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 97 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", | 93 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", |
| 98 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 94 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 99 | 95 | ||
| 100 | // 2. 创建客户端并连接 | 96 | // 2. 创建客户端并连接 |
| 101 | - MqttClient client = connect(authInfo); | ||
| 102 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 103 | - .onComplete(ar -> { | ||
| 104 | - if (ar.succeeded()) { | ||
| 105 | - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 106 | - // 断开连接 | ||
| 107 | - client.disconnect() | ||
| 108 | - .onComplete(disconnectAr -> { | ||
| 109 | - if (disconnectAr.succeeded()) { | ||
| 110 | - log.info("[testAuth][断开连接成功]"); | ||
| 111 | - } else { | ||
| 112 | - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); | ||
| 113 | - } | ||
| 114 | - latch.countDown(); | ||
| 115 | - }); | ||
| 116 | - } else { | ||
| 117 | - log.error("[testAuth][连接失败]", ar.cause()); | ||
| 118 | - latch.countDown(); | ||
| 119 | - } | ||
| 120 | - }); | ||
| 121 | - | ||
| 122 | - // 3. 等待测试完成 | ||
| 123 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 124 | - if (!completed) { | ||
| 125 | - log.warn("[testAuth][测试超时]"); | 97 | + MqttClient client = createClient(authInfo); |
| 98 | + try { | ||
| 99 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 100 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 101 | + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 102 | + } finally { | ||
| 103 | + disconnect(client); | ||
| 126 | } | 104 | } |
| 127 | } | 105 | } |
| 128 | 106 | ||
| @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 138 | log.info("[testPropertyPost][连接认证成功]"); | 116 | log.info("[testPropertyPost][连接认证成功]"); |
| 139 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); | 117 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); |
| 140 | 118 | ||
| 141 | - // 2. 订阅 _reply 主题 | ||
| 142 | - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 143 | - subscribeReply(client, replyTopic); | ||
| 144 | - | ||
| 145 | - // 3. 构建属性上报消息 | ||
| 146 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 147 | - IdUtil.fastSimpleUUID(), | ||
| 148 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 149 | - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 150 | - .put("power", 100) | ||
| 151 | - .put("status", "online") | ||
| 152 | - .put("temperature", 36.5) | ||
| 153 | - .build()), | ||
| 154 | - null, null, null); | ||
| 155 | - | ||
| 156 | - // 4. 发布消息并等待响应 | ||
| 157 | - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 158 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 159 | - log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 160 | - | ||
| 161 | - // 5. 断开连接 | ||
| 162 | - disconnect(client); | 119 | + try { |
| 120 | + // 2.1 构建属性上报消息 | ||
| 121 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 122 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 123 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 124 | + .put("power", 100) | ||
| 125 | + .put("status", "online") | ||
| 126 | + .put("temperature", 36.5) | ||
| 127 | + .build())); | ||
| 128 | + | ||
| 129 | + // 2.2 订阅 _reply 主题 | ||
| 130 | + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 131 | + subscribe(client, replyTopic); | ||
| 132 | + | ||
| 133 | + // 3. 发布消息并等待响应 | ||
| 134 | + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 135 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 136 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 137 | + } finally { | ||
| 138 | + disconnect(client); | ||
| 139 | + } | ||
| 163 | } | 140 | } |
| 164 | 141 | ||
| 165 | // ===================== 子设备事件上报测试 ===================== | 142 | // ===================== 子设备事件上报测试 ===================== |
| @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 174 | log.info("[testEventPost][连接认证成功]"); | 151 | log.info("[testEventPost][连接认证成功]"); |
| 175 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); | 152 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); |
| 176 | 153 | ||
| 177 | - // 2. 订阅 _reply 主题 | ||
| 178 | - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 179 | - subscribeReply(client, replyTopic); | ||
| 180 | - | ||
| 181 | - // 3. 构建事件上报消息 | ||
| 182 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 183 | - IdUtil.fastSimpleUUID(), | ||
| 184 | - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 185 | - IotDeviceEventPostReqDTO.of( | ||
| 186 | - "alarm", | ||
| 187 | - MapUtil.<String, Object>builder() | ||
| 188 | - .put("level", "warning") | ||
| 189 | - .put("message", "temperature too high") | ||
| 190 | - .put("threshold", 40) | ||
| 191 | - .put("current", 42) | ||
| 192 | - .build(), | ||
| 193 | - System.currentTimeMillis()), | ||
| 194 | - null, null, null); | ||
| 195 | - | ||
| 196 | - // 4. 发布消息并等待响应 | ||
| 197 | - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 198 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 199 | - log.info("[testEventPost][响应消息: {}]", response); | ||
| 200 | - | ||
| 201 | - // 5. 断开连接 | ||
| 202 | - disconnect(client); | 154 | + try { |
| 155 | + // 2.1 构建事件上报消息 | ||
| 156 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 157 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 158 | + IotDeviceEventPostReqDTO.of( | ||
| 159 | + "alarm", | ||
| 160 | + MapUtil.<String, Object>builder() | ||
| 161 | + .put("level", "warning") | ||
| 162 | + .put("message", "temperature too high") | ||
| 163 | + .put("threshold", 40) | ||
| 164 | + .put("current", 42) | ||
| 165 | + .build(), | ||
| 166 | + System.currentTimeMillis())); | ||
| 167 | + | ||
| 168 | + // 2.2 订阅 _reply 主题 | ||
| 169 | + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 170 | + subscribe(client, replyTopic); | ||
| 171 | + | ||
| 172 | + // 3. 发布消息并等待响应 | ||
| 173 | + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 174 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 175 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 176 | + } finally { | ||
| 177 | + disconnect(client); | ||
| 178 | + } | ||
| 203 | } | 179 | } |
| 204 | 180 | ||
| 205 | // ===================== 辅助方法 ===================== | 181 | // ===================== 辅助方法 ===================== |
| @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 210 | * @param authInfo 认证信息 | 186 | * @param authInfo 认证信息 |
| 211 | * @return MQTT 客户端 | 187 | * @return MQTT 客户端 |
| 212 | */ | 188 | */ |
| 213 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | 189 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { |
| 214 | MqttClientOptions options = new MqttClientOptions() | 190 | MqttClientOptions options = new MqttClientOptions() |
| 215 | .setClientId(authInfo.getClientId()) | 191 | .setClientId(authInfo.getClientId()) |
| 216 | .setUsername(authInfo.getUsername()) | 192 | .setUsername(authInfo.getUsername()) |
| @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 226 | * @return 已认证的 MQTT 客户端 | 202 | * @return 已认证的 MQTT 客户端 |
| 227 | */ | 203 | */ |
| 228 | private MqttClient connectAndAuth() throws Exception { | 204 | private MqttClient connectAndAuth() throws Exception { |
| 229 | - // 1. 创建客户端并连接 | ||
| 230 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 205 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 231 | - MqttClient client = connect(authInfo); | ||
| 232 | - | ||
| 233 | - // 2.1 连接 | ||
| 234 | - CompletableFuture<MqttClient> future = new CompletableFuture<>(); | 206 | + MqttClient client = createClient(authInfo); |
| 235 | client.connect(SERVER_PORT, SERVER_HOST) | 207 | client.connect(SERVER_PORT, SERVER_HOST) |
| 236 | - .onComplete(ar -> { | ||
| 237 | - if (ar.succeeded()) { | ||
| 238 | - future.complete(client); | ||
| 239 | - } else { | ||
| 240 | - future.completeExceptionally(ar.cause()); | ||
| 241 | - } | ||
| 242 | - }); | ||
| 243 | - // 2.2 等待连接结果 | ||
| 244 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 208 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 209 | + return client; | ||
| 245 | } | 210 | } |
| 246 | 211 | ||
| 247 | /** | 212 | /** |
| 248 | - * 订阅响应主题 | 213 | + * 订阅主题 |
| 249 | * | 214 | * |
| 250 | - * @param client MQTT 客户端 | ||
| 251 | - * @param replyTopic 响应主题 | 215 | + * @param client MQTT 客户端 |
| 216 | + * @param topic 主题 | ||
| 252 | */ | 217 | */ |
| 253 | - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 254 | - // 1. 订阅响应主题 | ||
| 255 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 256 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 257 | - .onComplete(ar -> { | ||
| 258 | - if (ar.succeeded()) { | ||
| 259 | - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 260 | - future.complete(null); | ||
| 261 | - } else { | ||
| 262 | - future.completeExceptionally(ar.cause()); | ||
| 263 | - } | ||
| 264 | - }); | ||
| 265 | - // 2. 等待订阅结果 | ||
| 266 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 218 | + private void subscribe(MqttClient client, String topic) throws Exception { |
| 219 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 220 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 221 | + log.info("[subscribe][订阅主题成功: {}]", topic); | ||
| 267 | } | 222 | } |
| 268 | 223 | ||
| 269 | /** | 224 | /** |
| @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 274 | * @param request 请求消息 | 229 | * @param request 请求消息 |
| 275 | * @return 响应消息 | 230 | * @return 响应消息 |
| 276 | */ | 231 | */ |
| 277 | - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | 232 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) |
| 233 | + throws Exception { | ||
| 278 | // 1. 设置消息处理器,接收响应 | 234 | // 1. 设置消息处理器,接收响应 |
| 279 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | 235 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 280 | client.publishHandler(message -> { | 236 | client.publishHandler(message -> { |
| 281 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | 237 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 282 | message.topicName(), message.payload().toString()); | 238 | message.topicName(), message.payload().toString()); |
| 283 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | 239 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); |
| 284 | - future.complete(response); | 240 | + responseFuture.complete(response); |
| 285 | }); | 241 | }); |
| 286 | 242 | ||
| 287 | // 2. 编码并发布消息 | 243 | // 2. 编码并发布消息 |
| 288 | byte[] payload = CODEC.encode(request); | 244 | byte[] payload = CODEC.encode(request); |
| 289 | log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | 245 | log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", |
| 290 | CODEC.type(), topic, new String(payload)); | 246 | CODEC.type(), topic, new String(payload)); |
| 291 | - | ||
| 292 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | 247 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) |
| 293 | - .onComplete(ar -> { | ||
| 294 | - if (ar.succeeded()) { | ||
| 295 | - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 296 | - } else { | ||
| 297 | - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 298 | - future.completeExceptionally(ar.cause()); | ||
| 299 | - } | ||
| 300 | - }); | ||
| 301 | - | ||
| 302 | - // 3. 等待响应(超时返回 null) | 248 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 249 | + log.info("[publishAndWaitReply][消息发布成功]"); | ||
| 250 | + | ||
| 251 | + // 3. 等待响应 | ||
| 303 | try { | 252 | try { |
| 304 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 253 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 305 | } catch (Exception e) { | 254 | } catch (Exception e) { |
| 306 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); | 255 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 307 | return null; | 256 | return null; |
| @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 314 | * @param client MQTT 客户端 | 263 | * @param client MQTT 客户端 |
| 315 | */ | 264 | */ |
| 316 | private void disconnect(MqttClient client) throws Exception { | 265 | private void disconnect(MqttClient client) throws Exception { |
| 317 | - // 1. 断开连接 | ||
| 318 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 319 | client.disconnect() | 266 | client.disconnect() |
| 320 | - .onComplete(ar -> { | ||
| 321 | - if (ar.succeeded()) { | ||
| 322 | - log.info("[disconnect][断开连接成功]"); | ||
| 323 | - future.complete(null); | ||
| 324 | - } else { | ||
| 325 | - future.completeExceptionally(ar.cause()); | ||
| 326 | - } | ||
| 327 | - }); | ||
| 328 | - // 2. 等待断开结果 | ||
| 329 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 267 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 268 | + log.info("[disconnect][断开连接成功]"); | ||
| 330 | } | 269 | } |
| 331 | 270 | ||
| 332 | } | 271 | } |