Commit cb301eb788704a62a21b490f265c97bf07f7d1a4
1 parent
09041a24
feat(iot):【协议改造】udp 初步改造(50%),主流程跑通
Showing
20 changed files
with
1095 additions
and
1361 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java deleted
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| @@ -11,10 +11,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr | @@ -11,10 +11,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr | ||
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 12 | 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.protocol.mqtt.router.IotMqttDownstreamHandler; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; |
| 14 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; | ||
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; | ||
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | ||
| 17 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; | ||
| 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; |
| 19 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; |
| 20 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; | 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; |
| @@ -42,10 +38,8 @@ public class IotGatewayConfiguration { | @@ -42,10 +38,8 @@ public class IotGatewayConfiguration { | ||
| 42 | } | 38 | } |
| 43 | 39 | ||
| 44 | @Bean | 40 | @Bean |
| 45 | - public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties, | ||
| 46 | - IotMessageSerializerManager serializerManager, | ||
| 47 | - IotMessageBus messageBus) { | ||
| 48 | - return new IotProtocolManager(gatewayProperties, serializerManager, messageBus); | 41 | + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) { |
| 42 | + return new IotProtocolManager(gatewayProperties); | ||
| 49 | } | 43 | } |
| 50 | 44 | ||
| 51 | /** | 45 | /** |
| @@ -118,45 +112,6 @@ public class IotGatewayConfiguration { | @@ -118,45 +112,6 @@ public class IotGatewayConfiguration { | ||
| 118 | } | 112 | } |
| 119 | 113 | ||
| 120 | /** | 114 | /** |
| 121 | - * IoT 网关 UDP 协议配置类 | ||
| 122 | - */ | ||
| 123 | - @Configuration | ||
| 124 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true") | ||
| 125 | - @Slf4j | ||
| 126 | - public static class UdpProtocolConfiguration { | ||
| 127 | - | ||
| 128 | - @Bean(name = "udpVertx", destroyMethod = "close") | ||
| 129 | - public Vertx udpVertx() { | ||
| 130 | - return Vertx.vertx(); | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - @Bean | ||
| 134 | - public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties, | ||
| 135 | - IotDeviceService deviceService, | ||
| 136 | - IotDeviceMessageService messageService, | ||
| 137 | - IotUdpSessionManager sessionManager, | ||
| 138 | - @Qualifier("udpVertx") Vertx udpVertx) { | ||
| 139 | - return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(), | ||
| 140 | - deviceService, messageService, sessionManager, udpVertx); | ||
| 141 | - } | ||
| 142 | - | ||
| 143 | - @Bean | ||
| 144 | - public IotUdpDownstreamHandler iotUdpDownstreamHandler(IotDeviceMessageService messageService, | ||
| 145 | - IotUdpSessionManager sessionManager, | ||
| 146 | - IotUdpUpstreamProtocol protocol) { | ||
| 147 | - return new IotUdpDownstreamHandler(messageService, sessionManager, protocol); | ||
| 148 | - } | ||
| 149 | - | ||
| 150 | - @Bean | ||
| 151 | - public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, | ||
| 152 | - IotUdpDownstreamHandler downstreamHandler, | ||
| 153 | - IotMessageBus messageBus) { | ||
| 154 | - return new IotUdpDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); | ||
| 155 | - } | ||
| 156 | - | ||
| 157 | - } | ||
| 158 | - | ||
| 159 | - /** | ||
| 160 | * IoT 网关 CoAP 协议配置类 | 115 | * IoT 网关 CoAP 协议配置类 |
| 161 | */ | 116 | */ |
| 162 | @Configuration | 117 | @Configuration |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; | ||
| 6 | import io.vertx.core.net.KeyCertOptions; | 7 | import io.vertx.core.net.KeyCertOptions; |
| 7 | import io.vertx.core.net.TrustOptions; | 8 | import io.vertx.core.net.TrustOptions; |
| 8 | import jakarta.validation.Valid; | 9 | import jakarta.validation.Valid; |
| @@ -35,7 +36,7 @@ public class IotGatewayProperties { | @@ -35,7 +36,7 @@ public class IotGatewayProperties { | ||
| 35 | private ProtocolProperties protocol; | 36 | private ProtocolProperties protocol; |
| 36 | 37 | ||
| 37 | /** | 38 | /** |
| 38 | - * 协议实例列表(新版) | 39 | + * 协议实例列表 |
| 39 | */ | 40 | */ |
| 40 | private List<ProtocolInstanceProperties> protocols; | 41 | private List<ProtocolInstanceProperties> protocols; |
| 41 | 42 | ||
| @@ -90,11 +91,6 @@ public class IotGatewayProperties { | @@ -90,11 +91,6 @@ public class IotGatewayProperties { | ||
| 90 | private MqttProperties mqtt; | 91 | private MqttProperties mqtt; |
| 91 | 92 | ||
| 92 | /** | 93 | /** |
| 93 | - * UDP 组件配置 | ||
| 94 | - */ | ||
| 95 | - private UdpProperties udp; | ||
| 96 | - | ||
| 97 | - /** | ||
| 98 | * CoAP 组件配置 | 94 | * CoAP 组件配置 |
| 99 | */ | 95 | */ |
| 100 | private CoapProperties coap; | 96 | private CoapProperties coap; |
| @@ -349,44 +345,6 @@ public class IotGatewayProperties { | @@ -349,44 +345,6 @@ public class IotGatewayProperties { | ||
| 349 | } | 345 | } |
| 350 | 346 | ||
| 351 | @Data | 347 | @Data |
| 352 | - public static class UdpProperties { | ||
| 353 | - | ||
| 354 | - /** | ||
| 355 | - * 是否开启 | ||
| 356 | - */ | ||
| 357 | - @NotNull(message = "是否开启不能为空") | ||
| 358 | - private Boolean enabled; | ||
| 359 | - | ||
| 360 | - /** | ||
| 361 | - * 服务端口(默认 8093) | ||
| 362 | - */ | ||
| 363 | - private Integer port = 8093; | ||
| 364 | - | ||
| 365 | - /** | ||
| 366 | - * 接收缓冲区大小(默认 64KB) | ||
| 367 | - */ | ||
| 368 | - private Integer receiveBufferSize = 65536; | ||
| 369 | - | ||
| 370 | - /** | ||
| 371 | - * 发送缓冲区大小(默认 64KB) | ||
| 372 | - */ | ||
| 373 | - private Integer sendBufferSize = 65536; | ||
| 374 | - | ||
| 375 | - /** | ||
| 376 | - * 会话超时时间(毫秒,默认 60 秒) | ||
| 377 | - * <p> | ||
| 378 | - * 用于清理不活跃的设备地址映射 | ||
| 379 | - */ | ||
| 380 | - private Long sessionTimeoutMs = 60000L; | ||
| 381 | - | ||
| 382 | - /** | ||
| 383 | - * 会话清理间隔(毫秒,默认 30 秒) | ||
| 384 | - */ | ||
| 385 | - private Long sessionCleanIntervalMs = 30000L; | ||
| 386 | - | ||
| 387 | - } | ||
| 388 | - | ||
| 389 | - @Data | ||
| 390 | public static class CoapProperties { | 348 | public static class CoapProperties { |
| 391 | 349 | ||
| 392 | /** | 350 | /** |
| @@ -525,6 +483,12 @@ public class IotGatewayProperties { | @@ -525,6 +483,12 @@ public class IotGatewayProperties { | ||
| 525 | @Valid | 483 | @Valid |
| 526 | private IotTcpConfig tcp; | 484 | private IotTcpConfig tcp; |
| 527 | 485 | ||
| 486 | + /** | ||
| 487 | + * UDP 协议配置 | ||
| 488 | + */ | ||
| 489 | + @Valid | ||
| 490 | + private IotUdpConfig udp; | ||
| 491 | + | ||
| 528 | } | 492 | } |
| 529 | 493 | ||
| 530 | } | 494 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
| @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.iot.gateway.protocol; | @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.iot.gateway.protocol; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | import cn.hutool.core.util.BooleanUtil; | 4 | import cn.hutool.core.util.BooleanUtil; |
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 7 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| 10 | +import lombok.Getter; | ||
| 11 | import lombok.extern.slf4j.Slf4j; | 11 | import lombok.extern.slf4j.Slf4j; |
| 12 | import org.springframework.context.SmartLifecycle; | 12 | import org.springframework.context.SmartLifecycle; |
| 13 | 13 | ||
| @@ -24,20 +24,16 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -24,20 +24,16 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 24 | 24 | ||
| 25 | private final IotGatewayProperties gatewayProperties; | 25 | private final IotGatewayProperties gatewayProperties; |
| 26 | 26 | ||
| 27 | - private final IotMessageSerializerManager serializerManager; | ||
| 28 | - | ||
| 29 | - private final IotMessageBus messageBus; | ||
| 30 | - | 27 | + /** |
| 28 | + * 协议实例列表 | ||
| 29 | + */ | ||
| 31 | private final List<IotProtocol> protocols = new ArrayList<>(); | 30 | private final List<IotProtocol> protocols = new ArrayList<>(); |
| 32 | 31 | ||
| 32 | + @Getter | ||
| 33 | private volatile boolean running = false; | 33 | private volatile boolean running = false; |
| 34 | 34 | ||
| 35 | - public IotProtocolManager(IotGatewayProperties gatewayProperties, | ||
| 36 | - IotMessageSerializerManager serializerManager, | ||
| 37 | - IotMessageBus messageBus) { | 35 | + public IotProtocolManager(IotGatewayProperties gatewayProperties) { |
| 38 | this.gatewayProperties = gatewayProperties; | 36 | this.gatewayProperties = gatewayProperties; |
| 39 | - this.serializerManager = serializerManager; | ||
| 40 | - this.messageBus = messageBus; | ||
| 41 | } | 37 | } |
| 42 | 38 | ||
| 43 | @Override | 39 | @Override |
| @@ -84,11 +80,6 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -84,11 +80,6 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 84 | log.info("[stop][协议管理器已停止]"); | 80 | log.info("[stop][协议管理器已停止]"); |
| 85 | } | 81 | } |
| 86 | 82 | ||
| 87 | - @Override | ||
| 88 | - public boolean isRunning() { | ||
| 89 | - return running; | ||
| 90 | - } | ||
| 91 | - | ||
| 92 | /** | 83 | /** |
| 93 | * 创建协议实例 | 84 | * 创建协议实例 |
| 94 | * | 85 | * |
| @@ -107,7 +98,8 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -107,7 +98,8 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 107 | return createHttpProtocol(config); | 98 | return createHttpProtocol(config); |
| 108 | case TCP: | 99 | case TCP: |
| 109 | return createTcpProtocol(config); | 100 | return createTcpProtocol(config); |
| 110 | - // TODO 后续添加其他协议类型 | 101 | + case UDP: |
| 102 | + return createUdpProtocol(config); | ||
| 111 | default: | 103 | default: |
| 112 | throw new IllegalArgumentException(String.format( | 104 | throw new IllegalArgumentException(String.format( |
| 113 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); | 105 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| @@ -121,7 +113,7 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -121,7 +113,7 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 121 | * @return HTTP 协议实例 | 113 | * @return HTTP 协议实例 |
| 122 | */ | 114 | */ |
| 123 | private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 115 | private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { |
| 124 | - return new IotHttpProtocol(config, messageBus); | 116 | + return new IotHttpProtocol(config); |
| 125 | } | 117 | } |
| 126 | 118 | ||
| 127 | /** | 119 | /** |
| @@ -131,7 +123,17 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -131,7 +123,17 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 131 | * @return TCP 协议实例 | 123 | * @return TCP 协议实例 |
| 132 | */ | 124 | */ |
| 133 | private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 125 | private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { |
| 134 | - return new IotTcpProtocol(config, messageBus, serializerManager); | 126 | + return new IotTcpProtocol(config); |
| 127 | + } | ||
| 128 | + | ||
| 129 | + /** | ||
| 130 | + * 创建 UDP 协议实例 | ||
| 131 | + * | ||
| 132 | + * @param config 协议实例配置 | ||
| 133 | + * @return UDP 协议实例 | ||
| 134 | + */ | ||
| 135 | + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | ||
| 136 | + return new IotUdpProtocol(config); | ||
| 135 | } | 137 | } |
| 136 | 138 | ||
| 137 | } | 139 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http; |
| 2 | 2 | ||
| 3 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 5 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| @@ -59,11 +60,12 @@ public class IotHttpProtocol implements IotProtocol { | @@ -59,11 +60,12 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 59 | */ | 60 | */ |
| 60 | private IotHttpDownstreamSubscriber downstreamSubscriber; | 61 | private IotHttpDownstreamSubscriber downstreamSubscriber; |
| 61 | 62 | ||
| 62 | - public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) { | 63 | + public IotHttpProtocol(ProtocolInstanceProperties properties) { |
| 63 | this.properties = properties; | 64 | this.properties = properties; |
| 64 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 65 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 65 | 66 | ||
| 66 | // 初始化下行消息订阅者 | 67 | // 初始化下行消息订阅者 |
| 68 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 67 | this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); | 69 | this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); |
| 68 | } | 70 | } |
| 69 | 71 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
| 2 | 2 | ||
| 3 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| @@ -73,31 +74,30 @@ public class IotTcpProtocol implements IotProtocol { | @@ -73,31 +74,30 @@ public class IotTcpProtocol implements IotProtocol { | ||
| 73 | private final IotTcpFrameCodec frameCodec; | 74 | private final IotTcpFrameCodec frameCodec; |
| 74 | 75 | ||
| 75 | /** | 76 | /** |
| 76 | - * TCP 连接管理器(每个 Protocol 实例独立) | 77 | + * TCP 连接管理器 |
| 77 | */ | 78 | */ |
| 78 | private final IotTcpConnectionManager connectionManager; | 79 | private final IotTcpConnectionManager connectionManager; |
| 79 | 80 | ||
| 80 | - public IotTcpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus, | ||
| 81 | - IotMessageSerializerManager serializerManager) { | 81 | + public IotTcpProtocol(ProtocolInstanceProperties properties) { |
| 82 | + IotTcpConfig tcpConfig = properties.getTcp(); | ||
| 83 | + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); | ||
| 84 | + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); | ||
| 82 | this.properties = properties; | 85 | this.properties = properties; |
| 83 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 86 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 84 | 87 | ||
| 85 | // 初始化序列化器 | 88 | // 初始化序列化器 |
| 86 | IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); | 89 | IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); |
| 87 | - if (serializeType == null) { | ||
| 88 | - serializeType = IotSerializeTypeEnum.JSON; // 默认 JSON | ||
| 89 | - } | 90 | + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); |
| 91 | + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); | ||
| 90 | this.serializer = serializerManager.get(serializeType); | 92 | this.serializer = serializerManager.get(serializeType); |
| 91 | // 初始化帧编解码器 | 93 | // 初始化帧编解码器 |
| 92 | - IotTcpConfig tcpConfig = properties.getTcp(); | ||
| 93 | - Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); | ||
| 94 | - Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); | ||
| 95 | this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); | 94 | this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); |
| 96 | 95 | ||
| 97 | // 初始化连接管理器 | 96 | // 初始化连接管理器 |
| 98 | this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); | 97 | this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); |
| 99 | 98 | ||
| 100 | // 初始化下行消息订阅者 | 99 | // 初始化下行消息订阅者 |
| 100 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 101 | IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); | 101 | IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); |
| 102 | this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); | 102 | this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); |
| 103 | } | 103 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
| @@ -49,12 +49,10 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -49,12 +49,10 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 49 | */ | 49 | */ |
| 50 | private final IotMessageSerializer serializer; | 50 | private final IotMessageSerializer serializer; |
| 51 | /** | 51 | /** |
| 52 | - * TCP 连接管理器(每个 Protocol 实例独立) | 52 | + * TCP 连接管理器 |
| 53 | */ | 53 | */ |
| 54 | private final IotTcpConnectionManager connectionManager; | 54 | private final IotTcpConnectionManager connectionManager; |
| 55 | 55 | ||
| 56 | - // ===================== Spring 依赖(构造时注入) ===================== | ||
| 57 | - | ||
| 58 | private final IotDeviceMessageService deviceMessageService; | 56 | private final IotDeviceMessageService deviceMessageService; |
| 59 | private final IotDeviceService deviceService; | 57 | private final IotDeviceService deviceService; |
| 60 | private final IotDeviceCommonApi deviceApi; | 58 | private final IotDeviceCommonApi deviceApi; |
| @@ -141,7 +139,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -141,7 +139,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 141 | String method = message != null ? message.getMethod() : null; | 139 | String method = message != null ? message.getMethod() : null; |
| 142 | sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); | 140 | sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); |
| 143 | } catch (Exception e) { | 141 | } catch (Exception e) { |
| 144 | - // 其他异常,返回 500 并重新抛出让上层关闭连接 | 142 | + // 其他异常,返回 500,并重新抛出让上层关闭连接 |
| 145 | log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); | 143 | log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); |
| 146 | String requestId = message != null ? message.getRequestId() : null; | 144 | String requestId = message != null ? message.getRequestId() : null; |
| 147 | String method = message != null ? message.getMethod() : null; | 145 | String method = message != null ? message.getMethod() : null; |
| @@ -158,6 +156,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -158,6 +156,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 158 | * @param message 消息信息 | 156 | * @param message 消息信息 |
| 159 | * @param socket 网络连接 | 157 | * @param socket 网络连接 |
| 160 | */ | 158 | */ |
| 159 | + @SuppressWarnings("DuplicatedCode") | ||
| 161 | private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) { | 160 | private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) { |
| 162 | // 1. 解析认证参数 | 161 | // 1. 解析认证参数 |
| 163 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | 162 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| @@ -180,7 +179,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -180,7 +179,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 180 | Assert.notNull(device, "设备不存在"); | 179 | Assert.notNull(device, "设备不存在"); |
| 181 | 180 | ||
| 182 | // 3.1 注册连接 | 181 | // 3.1 注册连接 |
| 183 | - registerConnection(socket, device, clientId); | 182 | + registerConnection(socket, device); |
| 184 | // 3.2 发送上线消息 | 183 | // 3.2 发送上线消息 |
| 185 | sendOnlineMessage(device); | 184 | sendOnlineMessage(device); |
| 186 | // 3.3 发送成功响应 | 185 | // 3.3 发送成功响应 |
| @@ -196,6 +195,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -196,6 +195,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 196 | * @param socket 网络连接 | 195 | * @param socket 网络连接 |
| 197 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | 196 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> |
| 198 | */ | 197 | */ |
| 198 | + @SuppressWarnings("DuplicatedCode") | ||
| 199 | private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) { | 199 | private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) { |
| 200 | // 1. 解析注册参数 | 200 | // 1. 解析注册参数 |
| 201 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | 201 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| @@ -246,14 +246,12 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -246,14 +246,12 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 246 | * | 246 | * |
| 247 | * @param socket 网络连接 | 247 | * @param socket 网络连接 |
| 248 | * @param device 设备 | 248 | * @param device 设备 |
| 249 | - * @param clientId 客户端 ID | ||
| 250 | */ | 249 | */ |
| 251 | - private void registerConnection(NetSocket socket, IotDeviceRespDTO device, String clientId) { | 250 | + private void registerConnection(NetSocket socket, IotDeviceRespDTO device) { |
| 252 | IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() | 251 | IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() |
| 253 | .setDeviceId(device.getId()) | 252 | .setDeviceId(device.getId()) |
| 254 | .setProductKey(device.getProductKey()) | 253 | .setProductKey(device.getProductKey()) |
| 255 | - .setDeviceName(device.getDeviceName()) | ||
| 256 | - .setClientId(clientId); | 254 | + .setDeviceName(device.getDeviceName()); |
| 257 | connectionManager.registerConnection(socket, device.getId(), connectionInfo); | 255 | connectionManager.registerConnection(socket, device.getId(), connectionInfo); |
| 258 | } | 256 | } |
| 259 | 257 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
| @@ -41,20 +41,6 @@ public class IotTcpConnectionManager { | @@ -41,20 +41,6 @@ public class IotTcpConnectionManager { | ||
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | /** | 43 | /** |
| 44 | - * 获取当前连接数 | ||
| 45 | - */ | ||
| 46 | - public int getConnectionCount() { | ||
| 47 | - return connectionMap.size(); | ||
| 48 | - } | ||
| 49 | - | ||
| 50 | - /** | ||
| 51 | - * 检查是否可以接受新连接 | ||
| 52 | - */ | ||
| 53 | - public boolean canAcceptConnection() { | ||
| 54 | - return connectionMap.size() < maxConnections; | ||
| 55 | - } | ||
| 56 | - | ||
| 57 | - /** | ||
| 58 | * 注册设备连接(包含认证信息) | 44 | * 注册设备连接(包含认证信息) |
| 59 | * | 45 | * |
| 60 | * @param socket TCP 连接 | 46 | * @param socket TCP 连接 |
| @@ -155,11 +141,6 @@ public class IotTcpConnectionManager { | @@ -155,11 +141,6 @@ public class IotTcpConnectionManager { | ||
| 155 | */ | 141 | */ |
| 156 | private String deviceName; | 142 | private String deviceName; |
| 157 | 143 | ||
| 158 | - /** | ||
| 159 | - * 客户端 ID | ||
| 160 | - */ | ||
| 161 | - private String clientId; | ||
| 162 | - | ||
| 163 | } | 144 | } |
| 164 | 145 | ||
| 165 | } | 146 | } |
| 166 | \ No newline at end of file | 147 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | ||
| 2 | + | ||
| 3 | +import jakarta.validation.constraints.Min; | ||
| 4 | +import jakarta.validation.constraints.NotNull; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * IoT UDP 协议配置 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +public class IotUdpConfig { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 最大会话数 | ||
| 17 | + */ | ||
| 18 | + @NotNull(message = "最大会话数不能为空") | ||
| 19 | + @Min(value = 1, message = "最大会话数必须大于 0") | ||
| 20 | + private Integer maxSessions = 1000; | ||
| 21 | + /** | ||
| 22 | + * 会话超时时间(毫秒) | ||
| 23 | + * <p> | ||
| 24 | + * 用于清理不活跃的设备地址映射 | ||
| 25 | + */ | ||
| 26 | + @NotNull(message = "会话超时时间不能为空") | ||
| 27 | + @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒") | ||
| 28 | + private Long sessionTimeoutMs = 60000L; | ||
| 29 | + /** | ||
| 30 | + * 会话清理间隔(毫秒) | ||
| 31 | + */ | ||
| 32 | + @NotNull(message = "会话清理间隔不能为空") | ||
| 33 | + @Min(value = 1000, message = "会话清理间隔必须大于 1000 毫秒") | ||
| 34 | + private Long sessionCleanIntervalMs = 30000L; | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 接收缓冲区大小(字节) | ||
| 38 | + */ | ||
| 39 | + @NotNull(message = "接收缓冲区大小不能为空") | ||
| 40 | + @Min(value = 1024, message = "接收缓冲区大小必须大于 1024 字节") | ||
| 41 | + private Integer receiveBufferSize = 65536; | ||
| 42 | + /** | ||
| 43 | + * 发送缓冲区大小(字节) | ||
| 44 | + */ | ||
| 45 | + @NotNull(message = "发送缓冲区大小不能为空") | ||
| 46 | + @Min(value = 1024, message = "发送缓冲区大小必须大于 1024 字节") | ||
| 47 | + private Integer sendBufferSize = 65536; | ||
| 48 | + | ||
| 49 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | ||
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | ||
| 8 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 10 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | ||
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | ||
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; | ||
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; | ||
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpUpstreamHandler; | ||
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | ||
| 17 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | ||
| 18 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | ||
| 19 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 20 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 21 | +import io.vertx.core.Vertx; | ||
| 22 | +import io.vertx.core.datagram.DatagramSocket; | ||
| 23 | +import io.vertx.core.datagram.DatagramSocketOptions; | ||
| 24 | +import lombok.Getter; | ||
| 25 | +import lombok.extern.slf4j.Slf4j; | ||
| 26 | +import org.springframework.util.Assert; | ||
| 27 | + | ||
| 28 | +import java.util.List; | ||
| 29 | + | ||
| 30 | +/** | ||
| 31 | + * IoT UDP 协议实现 | ||
| 32 | + * <p> | ||
| 33 | + * 基于 Vert.x 实现 UDP 服务器,接收设备上行消息 | ||
| 34 | + * | ||
| 35 | + * @author 芋道源码 | ||
| 36 | + */ | ||
| 37 | +@Slf4j | ||
| 38 | +public class IotUdpProtocol implements IotProtocol { | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 协议配置 | ||
| 42 | + */ | ||
| 43 | + private final ProtocolInstanceProperties properties; | ||
| 44 | + /** | ||
| 45 | + * 服务器 ID(用于消息追踪,全局唯一) | ||
| 46 | + */ | ||
| 47 | + @Getter | ||
| 48 | + private final String serverId; | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 运行状态 | ||
| 52 | + */ | ||
| 53 | + @Getter | ||
| 54 | + private volatile boolean running = false; | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * Vert.x 实例 | ||
| 58 | + */ | ||
| 59 | + private Vertx vertx; | ||
| 60 | + /** | ||
| 61 | + * UDP 服务器 | ||
| 62 | + */ | ||
| 63 | + @Getter | ||
| 64 | + private DatagramSocket udpSocket; | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 下行消息订阅者 | ||
| 68 | + */ | ||
| 69 | + private final IotUdpDownstreamSubscriber downstreamSubscriber; | ||
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * 消息序列化器 | ||
| 73 | + */ | ||
| 74 | + private final IotMessageSerializer serializer; | ||
| 75 | + | ||
| 76 | + /** | ||
| 77 | + * UDP 会话管理器 | ||
| 78 | + */ | ||
| 79 | + private final IotUdpSessionManager sessionManager; | ||
| 80 | + | ||
| 81 | + private final IotDeviceService deviceService; | ||
| 82 | + private final IotDeviceMessageService deviceMessageService; | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 会话清理定时器 ID | ||
| 86 | + */ | ||
| 87 | + // TODO @AI:会话清理,是不是放到 sessionManager 更合适? | ||
| 88 | + private Long cleanTimerId; | ||
| 89 | + | ||
| 90 | + public IotUdpProtocol(ProtocolInstanceProperties properties) { | ||
| 91 | + IotUdpConfig udpConfig = properties.getUdp(); | ||
| 92 | + Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); | ||
| 93 | + this.properties = properties; | ||
| 94 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | ||
| 95 | + this.deviceService = SpringUtil.getBean(IotDeviceService.class); | ||
| 96 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | ||
| 97 | + | ||
| 98 | + // 初始化序列化器 | ||
| 99 | + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); | ||
| 100 | + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); | ||
| 101 | + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); | ||
| 102 | + this.serializer = serializerManager.get(serializeType); | ||
| 103 | + | ||
| 104 | + // 初始化会话管理器 | ||
| 105 | + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions()); | ||
| 106 | + | ||
| 107 | + // 初始化下行消息订阅者 | ||
| 108 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 109 | + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); | ||
| 110 | + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + @Override | ||
| 114 | + public String getId() { | ||
| 115 | + return properties.getId(); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + @Override | ||
| 119 | + public IotProtocolTypeEnum getType() { | ||
| 120 | + return IotProtocolTypeEnum.UDP; | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + @Override | ||
| 124 | + public void start() { | ||
| 125 | + if (running) { | ||
| 126 | + log.warn("[start][IoT UDP 协议 {} 已经在运行中]", getId()); | ||
| 127 | + return; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + // 1.1 创建 Vertx 实例 | ||
| 131 | + this.vertx = Vertx.vertx(); | ||
| 132 | + | ||
| 133 | + // 1.2 创建 UDP Socket 选项 | ||
| 134 | + IotUdpConfig udpConfig = properties.getUdp(); | ||
| 135 | + DatagramSocketOptions options = new DatagramSocketOptions() | ||
| 136 | + .setReceiveBufferSize(udpConfig.getReceiveBufferSize()) | ||
| 137 | + .setSendBufferSize(udpConfig.getSendBufferSize()) | ||
| 138 | + .setReuseAddress(true); | ||
| 139 | + | ||
| 140 | + // 1.3 创建 UDP Socket | ||
| 141 | + udpSocket = vertx.createDatagramSocket(options); | ||
| 142 | + | ||
| 143 | + // 1.4 创建上行消息处理器 | ||
| 144 | + IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer); | ||
| 145 | + | ||
| 146 | + // 1.5 监听端口 | ||
| 147 | + udpSocket.listen(properties.getPort(), "0.0.0.0", result -> { | ||
| 148 | + if (result.failed()) { | ||
| 149 | + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), result.cause()); | ||
| 150 | + return; | ||
| 151 | + } | ||
| 152 | + // 设置数据包处理器 | ||
| 153 | + udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); | ||
| 154 | + running = true; | ||
| 155 | + log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]", | ||
| 156 | + getId(), properties.getPort(), serverId); | ||
| 157 | + // 启动会话清理定时器 | ||
| 158 | + startSessionCleanTimer(udpConfig); | ||
| 159 | + | ||
| 160 | + // 2. 启动下行消息订阅者 | ||
| 161 | + // TODO @AI:这里会导致 Thread Thread[vert.x-eventloop-thread-0,5,main] has been blocked for 2992 ms, time limit is 2000 ms | ||
| 162 | + this.downstreamSubscriber.start(); | ||
| 163 | + }); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + @Override | ||
| 167 | + public void stop() { | ||
| 168 | + if (!running) { | ||
| 169 | + return; | ||
| 170 | + } | ||
| 171 | + // 1. 停止下行消息订阅者 | ||
| 172 | + try { | ||
| 173 | + downstreamSubscriber.stop(); | ||
| 174 | + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); | ||
| 175 | + } catch (Exception e) { | ||
| 176 | + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + // 2.1 取消会话清理定时器 | ||
| 180 | + if (cleanTimerId != null) { | ||
| 181 | + vertx.cancelTimer(cleanTimerId); | ||
| 182 | + cleanTimerId = null; | ||
| 183 | + log.info("[stop][IoT UDP 协议 {} 会话清理定时器已取消]", getId()); | ||
| 184 | + } | ||
| 185 | + // 2.2 关闭 UDP Socket | ||
| 186 | + if (udpSocket != null) { | ||
| 187 | + try { | ||
| 188 | + udpSocket.close().result(); | ||
| 189 | + log.info("[stop][IoT UDP 协议 {} 服务器已停止]", getId()); | ||
| 190 | + } catch (Exception e) { | ||
| 191 | + log.error("[stop][IoT UDP 协议 {} 服务器停止失败]", getId(), e); | ||
| 192 | + } | ||
| 193 | + udpSocket = null; | ||
| 194 | + } | ||
| 195 | + // 2.3 关闭 Vertx 实例 | ||
| 196 | + if (vertx != null) { | ||
| 197 | + try { | ||
| 198 | + vertx.close().result(); | ||
| 199 | + log.info("[stop][IoT UDP 协议 {} Vertx 已关闭]", getId()); | ||
| 200 | + } catch (Exception e) { | ||
| 201 | + log.error("[stop][IoT UDP 协议 {} Vertx 关闭失败]", getId(), e); | ||
| 202 | + } | ||
| 203 | + vertx = null; | ||
| 204 | + } | ||
| 205 | + running = false; | ||
| 206 | + log.info("[stop][IoT UDP 协议 {} 已停止]", getId()); | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + /** | ||
| 210 | + * 启动会话清理定时器 | ||
| 211 | + */ | ||
| 212 | + // TODO @AI:这个放到 | ||
| 213 | + private void startSessionCleanTimer(IotUdpConfig udpConfig) { | ||
| 214 | + cleanTimerId = vertx.setPeriodic(udpConfig.getSessionCleanIntervalMs(), id -> { | ||
| 215 | + try { | ||
| 216 | + // 1. 清理超时的设备会话,并获取离线设备列表 | ||
| 217 | + List<Long> offlineDeviceIds = sessionManager.cleanExpiredSessions(udpConfig.getSessionTimeoutMs()); | ||
| 218 | + | ||
| 219 | + // 2. 为每个离线设备发送离线消息 | ||
| 220 | + for (Long deviceId : offlineDeviceIds) { | ||
| 221 | + sendOfflineMessage(deviceId); | ||
| 222 | + } | ||
| 223 | + if (CollUtil.isNotEmpty(offlineDeviceIds)) { | ||
| 224 | + log.info("[cleanExpiredSessions][本次清理 {} 个超时设备]", offlineDeviceIds.size()); | ||
| 225 | + } | ||
| 226 | + } catch (Exception e) { | ||
| 227 | + log.error("[cleanExpiredSessions][清理超时会话失败]", e); | ||
| 228 | + } | ||
| 229 | + }); | ||
| 230 | + log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", | ||
| 231 | + udpConfig.getSessionCleanIntervalMs(), udpConfig.getSessionTimeoutMs()); | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + /** | ||
| 235 | + * 发送设备离线消息 | ||
| 236 | + * | ||
| 237 | + * @param deviceId 设备 ID | ||
| 238 | + */ | ||
| 239 | + private void sendOfflineMessage(Long deviceId) { | ||
| 240 | + try { | ||
| 241 | + // 获取设备信息 | ||
| 242 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); | ||
| 243 | + if (device == null) { | ||
| 244 | + log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); | ||
| 245 | + return; | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + // 发送离线消息 | ||
| 249 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | ||
| 250 | + deviceMessageService.sendDeviceMessage(offlineMessage, device.getProductKey(), | ||
| 251 | + device.getDeviceName(), serverId); | ||
| 252 | + log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", | ||
| 253 | + deviceId, device.getDeviceName()); | ||
| 254 | + } catch (Exception e) { | ||
| 255 | + log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); | ||
| 256 | + } | ||
| 257 | + } | ||
| 258 | + | ||
| 259 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.collection.CollUtil; | ||
| 4 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 5 | -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | ||
| 6 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 7 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 8 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | ||
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | ||
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; | ||
| 12 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 13 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 14 | -import io.vertx.core.Vertx; | ||
| 15 | -import io.vertx.core.datagram.DatagramSocket; | ||
| 16 | -import io.vertx.core.datagram.DatagramSocketOptions; | ||
| 17 | -import jakarta.annotation.PostConstruct; | ||
| 18 | -import jakarta.annotation.PreDestroy; | ||
| 19 | -import lombok.Getter; | ||
| 20 | -import lombok.extern.slf4j.Slf4j; | ||
| 21 | - | ||
| 22 | -import java.util.List; | ||
| 23 | - | ||
| 24 | -/** | ||
| 25 | - * IoT 网关 UDP 协议:接收设备上行消息 | ||
| 26 | - * <p> | ||
| 27 | - * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能: | ||
| 28 | - * 1. 监听 UDP 端口,接收设备消息 | ||
| 29 | - * 2. 定期清理不活跃的设备地址映射 | ||
| 30 | - * 3. 提供 UDP Socket 用于下行消息发送 | ||
| 31 | - * | ||
| 32 | - * @author 芋道源码 | ||
| 33 | - */ | ||
| 34 | -@Slf4j | ||
| 35 | -public class IotUdpUpstreamProtocol implements IotProtocol { | ||
| 36 | - | ||
| 37 | - private static final String ID = "udp"; | ||
| 38 | - | ||
| 39 | - private final IotGatewayProperties.UdpProperties udpProperties; | ||
| 40 | - | ||
| 41 | - private final IotDeviceService deviceService; | ||
| 42 | - | ||
| 43 | - private final IotDeviceMessageService messageService; | ||
| 44 | - | ||
| 45 | - private final IotUdpSessionManager sessionManager; | ||
| 46 | - | ||
| 47 | - private final Vertx vertx; | ||
| 48 | - | ||
| 49 | - @Getter | ||
| 50 | - private final String serverId; | ||
| 51 | - | ||
| 52 | - @Getter | ||
| 53 | - private DatagramSocket udpSocket; | ||
| 54 | - | ||
| 55 | - /** | ||
| 56 | - * 会话清理定时器 ID | ||
| 57 | - */ | ||
| 58 | - private Long cleanTimerId; | ||
| 59 | - | ||
| 60 | - private IotUdpUpstreamHandler upstreamHandler; | ||
| 61 | - | ||
| 62 | - private volatile boolean running = false; | ||
| 63 | - | ||
| 64 | - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, | ||
| 65 | - IotDeviceService deviceService, | ||
| 66 | - IotDeviceMessageService messageService, | ||
| 67 | - IotUdpSessionManager sessionManager, | ||
| 68 | - Vertx vertx) { | ||
| 69 | - this.udpProperties = udpProperties; | ||
| 70 | - this.deviceService = deviceService; | ||
| 71 | - this.messageService = messageService; | ||
| 72 | - this.sessionManager = sessionManager; | ||
| 73 | - this.vertx = vertx; | ||
| 74 | - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); | ||
| 75 | - } | ||
| 76 | - | ||
| 77 | - @Override | ||
| 78 | - public String getId() { | ||
| 79 | - return ID; | ||
| 80 | - } | ||
| 81 | - | ||
| 82 | - @Override | ||
| 83 | - public IotProtocolTypeEnum getType() { | ||
| 84 | - return IotProtocolTypeEnum.UDP; | ||
| 85 | - } | ||
| 86 | - | ||
| 87 | - @Override | ||
| 88 | - @PostConstruct | ||
| 89 | - public void start() { | ||
| 90 | - // 1. 初始化上行消息处理器 | ||
| 91 | - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager); | ||
| 92 | - | ||
| 93 | - // 2. 创建 UDP Socket 选项 | ||
| 94 | - DatagramSocketOptions options = new DatagramSocketOptions() | ||
| 95 | - .setReceiveBufferSize(udpProperties.getReceiveBufferSize()) | ||
| 96 | - .setSendBufferSize(udpProperties.getSendBufferSize()) | ||
| 97 | - .setReuseAddress(true); | ||
| 98 | - | ||
| 99 | - // 3. 创建 UDP Socket | ||
| 100 | - udpSocket = vertx.createDatagramSocket(options); | ||
| 101 | - | ||
| 102 | - // 4. 监听端口 | ||
| 103 | - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> { | ||
| 104 | - if (result.failed()) { | ||
| 105 | - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause()); | ||
| 106 | - return; | ||
| 107 | - } | ||
| 108 | - // 设置数据包处理器 | ||
| 109 | - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); | ||
| 110 | - running = true; | ||
| 111 | - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", | ||
| 112 | - udpProperties.getPort(), udpProperties.getReceiveBufferSize(), | ||
| 113 | - udpProperties.getSendBufferSize()); | ||
| 114 | - | ||
| 115 | - // 5. 启动会话清理定时器 | ||
| 116 | - startSessionCleanTimer(); | ||
| 117 | - }); | ||
| 118 | - } | ||
| 119 | - | ||
| 120 | - @Override | ||
| 121 | - @PreDestroy | ||
| 122 | - public void stop() { | ||
| 123 | - // 1. 取消会话清理定时器 | ||
| 124 | - if (cleanTimerId != null) { | ||
| 125 | - vertx.cancelTimer(cleanTimerId); | ||
| 126 | - cleanTimerId = null; | ||
| 127 | - log.info("[stop][会话清理定时器已取消]"); | ||
| 128 | - } | ||
| 129 | - | ||
| 130 | - // 2. 关闭 UDP Socket | ||
| 131 | - if (udpSocket != null) { | ||
| 132 | - try { | ||
| 133 | - udpSocket.close().result(); | ||
| 134 | - running = false; | ||
| 135 | - log.info("[stop][IoT 网关 UDP 协议已停止]"); | ||
| 136 | - } catch (Exception e) { | ||
| 137 | - log.error("[stop][IoT 网关 UDP 协议停止失败]", e); | ||
| 138 | - } | ||
| 139 | - } | ||
| 140 | - } | ||
| 141 | - | ||
| 142 | - @Override | ||
| 143 | - public boolean isRunning() { | ||
| 144 | - return running; | ||
| 145 | - } | ||
| 146 | - | ||
| 147 | - /** | ||
| 148 | - * 启动会话清理定时器 | ||
| 149 | - */ | ||
| 150 | - private void startSessionCleanTimer() { | ||
| 151 | - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> { | ||
| 152 | - try { | ||
| 153 | - // 1. 清理超时的设备地址映射,并获取离线设备列表 | ||
| 154 | - List<Long> offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs()); | ||
| 155 | - | ||
| 156 | - // 2. 为每个离线设备发送离线消息 | ||
| 157 | - for (Long deviceId : offlineDeviceIds) { | ||
| 158 | - sendOfflineMessage(deviceId); | ||
| 159 | - } | ||
| 160 | - if (CollUtil.isNotEmpty(offlineDeviceIds)) { | ||
| 161 | - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size()); | ||
| 162 | - } | ||
| 163 | - } catch (Exception e) { | ||
| 164 | - log.error("[cleanExpiredMappings][清理超时会话失败]", e); | ||
| 165 | - } | ||
| 166 | - }); | ||
| 167 | - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", | ||
| 168 | - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs()); | ||
| 169 | - } | ||
| 170 | - | ||
| 171 | - /** | ||
| 172 | - * 发送设备离线消息 | ||
| 173 | - * | ||
| 174 | - * @param deviceId 设备 ID | ||
| 175 | - */ | ||
| 176 | - private void sendOfflineMessage(Long deviceId) { | ||
| 177 | - try { | ||
| 178 | - // 获取设备信息 | ||
| 179 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); | ||
| 180 | - if (device == null) { | ||
| 181 | - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); | ||
| 182 | - return; | ||
| 183 | - } | ||
| 184 | - | ||
| 185 | - // 发送离线消息 | ||
| 186 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | ||
| 187 | - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), | ||
| 188 | - device.getDeviceName(), serverId); | ||
| 189 | - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", | ||
| 190 | - deviceId, device.getDeviceName()); | ||
| 191 | - } catch (Exception e) { | ||
| 192 | - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); | ||
| 193 | - } | ||
| 194 | - } | ||
| 195 | - | ||
| 196 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; | 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 6 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 7 | import io.vertx.core.datagram.DatagramSocket; | 7 | import io.vertx.core.datagram.DatagramSocket; |
| 8 | +import lombok.RequiredArgsConstructor; | ||
| 8 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 9 | 10 | ||
| 10 | /** | 11 | /** |
| @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j; | @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j; | ||
| 13 | * @author 芋道源码 | 14 | * @author 芋道源码 |
| 14 | */ | 15 | */ |
| 15 | @Slf4j | 16 | @Slf4j |
| 17 | +@RequiredArgsConstructor | ||
| 16 | public class IotUdpDownstreamHandler { | 18 | public class IotUdpDownstreamHandler { |
| 17 | 19 | ||
| 18 | - private final IotDeviceMessageService deviceMessageService; | 20 | + private final IotUdpProtocol protocol; |
| 19 | 21 | ||
| 20 | private final IotUdpSessionManager sessionManager; | 22 | private final IotUdpSessionManager sessionManager; |
| 21 | 23 | ||
| 22 | - private final IotUdpUpstreamProtocol protocol; | ||
| 23 | - | ||
| 24 | - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, | ||
| 25 | - IotUdpSessionManager sessionManager, | ||
| 26 | - IotUdpUpstreamProtocol protocol) { | ||
| 27 | - this.deviceMessageService = deviceMessageService; | ||
| 28 | - this.sessionManager = sessionManager; | ||
| 29 | - this.protocol = protocol; | ||
| 30 | - } | 24 | + /** |
| 25 | + * 消息序列化器(处理业务消息序列化/反序列化) | ||
| 26 | + */ | ||
| 27 | + private final IotMessageSerializer serializer; | ||
| 31 | 28 | ||
| 32 | /** | 29 | /** |
| 33 | * 处理下行消息 | 30 | * 处理下行消息 |
| 34 | - * | ||
| 35 | - * @param message 下行消息 | ||
| 36 | */ | 31 | */ |
| 37 | public void handle(IotDeviceMessage message) { | 32 | public void handle(IotDeviceMessage message) { |
| 38 | try { | 33 | try { |
| 39 | log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", | 34 | log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 40 | message.getDeviceId(), message.getMethod(), message.getId()); | 35 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 41 | - | ||
| 42 | - // 1. 获取会话信息(包含 codecType) | 36 | + // 1. 检查设备会话 |
| 43 | IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); | 37 | IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); |
| 44 | if (sessionInfo == null) { | 38 | if (sessionInfo == null) { |
| 45 | - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); | 39 | + log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]", |
| 40 | + message.getDeviceId(), message.getMethod(), message.getId()); | ||
| 46 | return; | 41 | return; |
| 47 | } | 42 | } |
| 48 | - | ||
| 49 | - // 2. 使用会话中的 codecType 编码消息,并发送到设备 | ||
| 50 | - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); | ||
| 51 | DatagramSocket socket = protocol.getUdpSocket(); | 43 | DatagramSocket socket = protocol.getUdpSocket(); |
| 52 | if (socket == null) { | 44 | if (socket == null) { |
| 53 | log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); | 45 | log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); |
| 54 | return; | 46 | return; |
| 55 | } | 47 | } |
| 56 | - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket); | ||
| 57 | - if (success) { | ||
| 58 | - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | ||
| 59 | - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); | ||
| 60 | - } else { | ||
| 61 | - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", | ||
| 62 | - message.getDeviceId(), message.getMethod(), message.getId()); | 48 | + |
| 49 | + // 2. 序列化消息 | ||
| 50 | + byte[] serializedData = serializer.serialize(message); | ||
| 51 | + | ||
| 52 | + // 3. 发送到设备 | ||
| 53 | + boolean success = sessionManager.sendToDevice(message.getDeviceId(), serializedData, socket); | ||
| 54 | + if (!success) { | ||
| 55 | + throw new RuntimeException("下行消息发送失败"); | ||
| 63 | } | 56 | } |
| 57 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | ||
| 58 | + message.getDeviceId(), message.getMethod(), message.getId(), serializedData.length); | ||
| 64 | } catch (Exception e) { | 59 | } catch (Exception e) { |
| 65 | log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", | 60 | log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", |
| 66 | message.getDeviceId(), message.getMethod(), message, e); | 61 | message.getDeviceId(), message.getMethod(), message, e); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 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.gateway.protocol.IotProtocol; | ||
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; | ||
| 7 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| @@ -16,7 +16,7 @@ public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber | @@ -16,7 +16,7 @@ public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber | ||
| 16 | 16 | ||
| 17 | private final IotUdpDownstreamHandler downstreamHandler; | 17 | private final IotUdpDownstreamHandler downstreamHandler; |
| 18 | 18 | ||
| 19 | - public IotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocol, | 19 | + public IotUdpDownstreamSubscriber(IotProtocol protocol, |
| 20 | IotUdpDownstreamHandler downstreamHandler, | 20 | IotUdpDownstreamHandler downstreamHandler, |
| 21 | IotMessageBus messageBus) { | 21 | IotMessageBus messageBus) { |
| 22 | super(protocol, messageBus); | 22 | super(protocol, messageBus); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.ArrayUtil; | ||
| 4 | +import cn.hutool.core.util.IdUtil; | ||
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 6 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 7 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 8 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 9 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 10 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 11 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 12 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 13 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 14 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | ||
| 15 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 16 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | ||
| 17 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 18 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | ||
| 19 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | ||
| 20 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | ||
| 21 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 22 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 23 | +import io.vertx.core.buffer.Buffer; | ||
| 24 | +import io.vertx.core.datagram.DatagramPacket; | ||
| 25 | +import io.vertx.core.datagram.DatagramSocket; | ||
| 26 | +import lombok.extern.slf4j.Slf4j; | ||
| 27 | +import org.springframework.util.Assert; | ||
| 28 | + | ||
| 29 | +import java.net.InetSocketAddress; | ||
| 30 | +import java.time.LocalDateTime; | ||
| 31 | +import java.util.Map; | ||
| 32 | + | ||
| 33 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | ||
| 34 | + | ||
| 35 | +/** | ||
| 36 | + * UDP 上行消息处理器 | ||
| 37 | + * <p> | ||
| 38 | + * 采用无状态 Token 机制(每次请求携带 token): | ||
| 39 | + * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password | ||
| 40 | + * 2. 返回 Token:服务端验证后返回 JWT token | ||
| 41 | + * 3. 后续请求:每次请求在 params 中携带 token | ||
| 42 | + * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 | ||
| 43 | + * | ||
| 44 | + * @author 芋道源码 | ||
| 45 | + */ | ||
| 46 | +@Slf4j | ||
| 47 | +public class IotUdpUpstreamHandler { | ||
| 48 | + | ||
| 49 | + private static final String AUTH_METHOD = "auth"; | ||
| 50 | + | ||
| 51 | + /** | ||
| 52 | + * Token 参数 Key | ||
| 53 | + */ | ||
| 54 | + private static final String PARAM_KEY_TOKEN = "token"; | ||
| 55 | + /** | ||
| 56 | + * Body 参数 Key(实际请求内容) | ||
| 57 | + */ | ||
| 58 | + private static final String PARAM_KEY_BODY = "body"; | ||
| 59 | + | ||
| 60 | + private final String serverId; | ||
| 61 | + | ||
| 62 | + /** | ||
| 63 | + * 消息序列化器(处理业务消息序列化/反序列化) | ||
| 64 | + */ | ||
| 65 | + private final IotMessageSerializer serializer; | ||
| 66 | + /** | ||
| 67 | + * UDP 会话管理器 | ||
| 68 | + */ | ||
| 69 | + private final IotUdpSessionManager sessionManager; | ||
| 70 | + | ||
| 71 | + private final IotDeviceMessageService deviceMessageService; | ||
| 72 | + private final IotDeviceService deviceService; | ||
| 73 | + private final IotDeviceTokenService deviceTokenService; | ||
| 74 | + private final IotDeviceCommonApi deviceApi; | ||
| 75 | + | ||
| 76 | + public IotUdpUpstreamHandler(String serverId, | ||
| 77 | + IotUdpSessionManager sessionManager, | ||
| 78 | + IotMessageSerializer serializer) { | ||
| 79 | + Assert.notNull(serializer, "消息序列化器必须配置"); | ||
| 80 | + Assert.notNull(sessionManager, "会话管理器不能为空"); | ||
| 81 | + this.serverId = serverId; | ||
| 82 | + this.sessionManager = sessionManager; | ||
| 83 | + this.serializer = serializer; | ||
| 84 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | ||
| 85 | + this.deviceService = SpringUtil.getBean(IotDeviceService.class); | ||
| 86 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | ||
| 87 | + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + // TODO @AI:vertx 有 udp 的实现么? | ||
| 91 | + /** | ||
| 92 | + * 处理 UDP 数据包 | ||
| 93 | + * | ||
| 94 | + * @param packet 数据包 | ||
| 95 | + * @param socket UDP Socket | ||
| 96 | + */ | ||
| 97 | + public void handle(DatagramPacket packet, DatagramSocket socket) { | ||
| 98 | + InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); | ||
| 99 | + Buffer data = packet.data(); | ||
| 100 | + String addressKey = sessionManager.buildAddressKey(senderAddress); | ||
| 101 | + log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); | ||
| 102 | + try { | ||
| 103 | + processMessage(data, senderAddress, socket); | ||
| 104 | + } catch (IllegalArgumentException e) { | ||
| 105 | + // 参数校验失败,返回 400 | ||
| 106 | + log.warn("[handle][参数校验失败,来源: {},错误: {}]", addressKey, e.getMessage()); | ||
| 107 | + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), e.getMessage()); | ||
| 108 | + } catch (Exception e) { | ||
| 109 | + // 其他异常,返回 500 | ||
| 110 | + log.error("[handle][处理消息失败,来源: {}]", addressKey, e); | ||
| 111 | + sendErrorResponse(socket, senderAddress, null, null, | ||
| 112 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + /** | ||
| 117 | + * 处理消息 | ||
| 118 | + * | ||
| 119 | + * @param buffer 消息 | ||
| 120 | + * @param senderAddress 发送者地址 | ||
| 121 | + * @param socket UDP Socket | ||
| 122 | + */ | ||
| 123 | + private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { | ||
| 124 | + // 1.1 基础检查 | ||
| 125 | + if (ArrayUtil.isEmpty(buffer)) { | ||
| 126 | + return; | ||
| 127 | + } | ||
| 128 | + // 1.2 反序列化消息 | ||
| 129 | + IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); | ||
| 130 | + if (message == null) { | ||
| 131 | + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); | ||
| 132 | + return; | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + // 2. 根据消息类型路由处理 | ||
| 136 | + if (AUTH_METHOD.equals(message.getMethod())) { | ||
| 137 | + // 认证请求 | ||
| 138 | + handleAuthenticationRequest(message, senderAddress, socket); | ||
| 139 | + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | ||
| 140 | + // 设备动态注册请求 | ||
| 141 | + handleRegisterRequest(message, senderAddress, socket); | ||
| 142 | + } else { | ||
| 143 | + // 业务消息 | ||
| 144 | + handleBusinessRequest(message, senderAddress, socket); | ||
| 145 | + } | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + /** | ||
| 149 | + * 处理认证请求 | ||
| 150 | + * | ||
| 151 | + * @param message 消息信息 | ||
| 152 | + * @param senderAddress 发送者地址 | ||
| 153 | + * @param socket UDP Socket | ||
| 154 | + */ | ||
| 155 | + @SuppressWarnings("DuplicatedCode") | ||
| 156 | + private void handleAuthenticationRequest(IotDeviceMessage message, InetSocketAddress senderAddress, | ||
| 157 | + DatagramSocket socket) { | ||
| 158 | + String clientId = IdUtil.simpleUUID(); | ||
| 159 | + // 1. 解析认证参数 | ||
| 160 | + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | ||
| 161 | + Assert.notNull(authParams, "认证参数不能为空"); | ||
| 162 | + Assert.hasText(authParams.getUsername(), "username 不能为空"); | ||
| 163 | + Assert.hasText(authParams.getPassword(), "password 不能为空"); | ||
| 164 | + | ||
| 165 | + // 2.1 执行认证 | ||
| 166 | + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | ||
| 167 | + if (authResult.isError()) { | ||
| 168 | + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", | ||
| 169 | + clientId, authParams.getUsername()); | ||
| 170 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, | ||
| 171 | + authResult.getCode(), authResult.getMsg()); | ||
| 172 | + return; | ||
| 173 | + } | ||
| 174 | + // 2.2 解析设备信息 | ||
| 175 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); | ||
| 176 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | ||
| 177 | + // 2.3 获取设备信息 | ||
| 178 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | ||
| 179 | + deviceInfo.getDeviceName()); | ||
| 180 | + Assert.notNull(device, "设备不存在"); | ||
| 181 | + | ||
| 182 | + // 3. 生成 JWT Token(无状态) | ||
| 183 | + String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); | ||
| 184 | + | ||
| 185 | + // 4.1 注册会话 | ||
| 186 | + registerSession(senderAddress, device, clientId); | ||
| 187 | + // 4.2 发送上线消息 | ||
| 188 | + sendOnlineMessage(device); | ||
| 189 | + // 4.3 发送成功响应(包含 token) | ||
| 190 | + sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token); | ||
| 191 | + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", | ||
| 192 | + device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + /** | ||
| 196 | + * 处理设备动态注册请求(一型一密,不需要认证) | ||
| 197 | + * | ||
| 198 | + * @param message 消息信息 | ||
| 199 | + * @param senderAddress 发送者地址 | ||
| 200 | + * @param socket UDP Socket | ||
| 201 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | ||
| 202 | + */ | ||
| 203 | + @SuppressWarnings("DuplicatedCode") | ||
| 204 | + private void handleRegisterRequest(IotDeviceMessage message, InetSocketAddress senderAddress, | ||
| 205 | + DatagramSocket socket) { | ||
| 206 | + // 1. 解析注册参数 | ||
| 207 | + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | ||
| 208 | + Assert.notNull(params, "注册参数不能为空"); | ||
| 209 | + Assert.hasText(params.getProductKey(), "productKey 不能为空"); | ||
| 210 | + Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); | ||
| 211 | + | ||
| 212 | + // 2. 调用动态注册 | ||
| 213 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | ||
| 214 | + if (result.isError()) { | ||
| 215 | + log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", | ||
| 216 | + sessionManager.buildAddressKey(senderAddress), result.getMsg()); | ||
| 217 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), | ||
| 218 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getCode(), result.getMsg()); | ||
| 219 | + return; | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + // 3. 发送成功响应 | ||
| 223 | + sendSuccessResponse(socket, senderAddress, message.getRequestId(), | ||
| 224 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); | ||
| 225 | + log.info("[handleRegisterRequest][注册成功,来源: {},设备名: {}]", | ||
| 226 | + sessionManager.buildAddressKey(senderAddress), params.getDeviceName()); | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + /** | ||
| 230 | + * 处理业务请求 | ||
| 231 | + * <p> | ||
| 232 | + * 请求参数格式: | ||
| 233 | + * - token:JWT 令牌 | ||
| 234 | + * - body:实际请求内容(可以是 Map、List 或其他类型) | ||
| 235 | + * | ||
| 236 | + * @param message 消息信息 | ||
| 237 | + * @param senderAddress 发送者地址 | ||
| 238 | + * @param socket UDP Socket | ||
| 239 | + */ | ||
| 240 | + @SuppressWarnings("unchecked") | ||
| 241 | + private void handleBusinessRequest(IotDeviceMessage message, InetSocketAddress senderAddress, | ||
| 242 | + DatagramSocket socket) { | ||
| 243 | + String addressKey = sessionManager.buildAddressKey(senderAddress); | ||
| 244 | + // 1.1 从消息中提取 token 和 body | ||
| 245 | + String token = null; | ||
| 246 | + Object body = null; | ||
| 247 | + if (message.getParams() instanceof Map) { | ||
| 248 | + Map<String, Object> paramsMap = (Map<String, Object>) message.getParams(); | ||
| 249 | + token = (String) paramsMap.get(PARAM_KEY_TOKEN); | ||
| 250 | + body = paramsMap.get(PARAM_KEY_BODY); | ||
| 251 | + } | ||
| 252 | + if (StrUtil.isBlank(token)) { | ||
| 253 | + log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); | ||
| 254 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | ||
| 255 | + UNAUTHORIZED.getCode(), "请先进行认证"); | ||
| 256 | + return; | ||
| 257 | + } | ||
| 258 | + // 1.2 验证 token,获取设备信息 | ||
| 259 | + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); | ||
| 260 | + if (deviceInfo == null) { | ||
| 261 | + log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); | ||
| 262 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | ||
| 263 | + UNAUTHORIZED.getCode(), "token 无效或已过期"); | ||
| 264 | + return; | ||
| 265 | + } | ||
| 266 | + // 1.3 获取设备详细信息 | ||
| 267 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | ||
| 268 | + deviceInfo.getDeviceName()); | ||
| 269 | + if (device == null) { | ||
| 270 | + log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", | ||
| 271 | + addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | ||
| 272 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | ||
| 273 | + BAD_REQUEST.getCode(), "设备不存在"); | ||
| 274 | + return; | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + // 2. 更新会话活跃时间和地址 | ||
| 278 | + // TODO @AI:是不是合并到 sessionManager 里面更好? | ||
| 279 | + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(device.getId()); | ||
| 280 | + if (sessionInfo != null) { | ||
| 281 | + // 检查地址是否变化,变化则更新 | ||
| 282 | + if (!senderAddress.equals(sessionInfo.getAddress())) { | ||
| 283 | + sessionManager.updateSessionAddress(device.getId(), senderAddress); | ||
| 284 | + } else { | ||
| 285 | + sessionManager.updateSessionActivity(device.getId()); | ||
| 286 | + } | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + // 3. 将 body 设置为实际的 params,发送消息到消息总线 | ||
| 290 | + message.setParams(body); | ||
| 291 | + deviceMessageService.sendDeviceMessage(message, device.getProductKey(), | ||
| 292 | + device.getDeviceName(), serverId); | ||
| 293 | + log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", | ||
| 294 | + device.getId(), message.getMethod(), addressKey); | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + /** | ||
| 298 | + * 注册会话信息 | ||
| 299 | + * | ||
| 300 | + * @param address 设备地址 | ||
| 301 | + * @param device 设备 | ||
| 302 | + * @param clientId 客户端 ID | ||
| 303 | + */ | ||
| 304 | + private void registerSession(InetSocketAddress address, IotDeviceRespDTO device, String clientId) { | ||
| 305 | + IotUdpSessionManager.SessionInfo sessionInfo = new IotUdpSessionManager.SessionInfo() | ||
| 306 | + .setDeviceId(device.getId()) | ||
| 307 | + .setProductKey(device.getProductKey()) | ||
| 308 | + .setDeviceName(device.getDeviceName()) | ||
| 309 | + .setAddress(address) | ||
| 310 | + .setLastActiveTime(LocalDateTime.now()); | ||
| 311 | + sessionManager.registerSession(device.getId(), sessionInfo); | ||
| 312 | + } | ||
| 313 | + | ||
| 314 | + /** | ||
| 315 | + * 发送设备上线消息 | ||
| 316 | + * | ||
| 317 | + * @param device 设备信息 | ||
| 318 | + */ | ||
| 319 | + private void sendOnlineMessage(IotDeviceRespDTO device) { | ||
| 320 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | ||
| 321 | + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | ||
| 322 | + device.getDeviceName(), serverId); | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + // ===================== 发送响应消息 ===================== | ||
| 326 | + | ||
| 327 | + /** | ||
| 328 | + * 发送认证成功响应(包含 token) | ||
| 329 | + * | ||
| 330 | + * @param socket UDP Socket | ||
| 331 | + * @param address 目标地址 | ||
| 332 | + * @param requestId 请求 ID | ||
| 333 | + * @param token JWT Token | ||
| 334 | + */ | ||
| 335 | + private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, | ||
| 336 | + String requestId, String token) { | ||
| 337 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, token, | ||
| 338 | + SUCCESS.getCode(), null); | ||
| 339 | + writeResponse(socket, address, responseMessage); | ||
| 340 | + } | ||
| 341 | + | ||
| 342 | + /** | ||
| 343 | + * 发送成功响应 | ||
| 344 | + * | ||
| 345 | + * @param socket UDP Socket | ||
| 346 | + * @param address 目标地址 | ||
| 347 | + * @param requestId 请求 ID | ||
| 348 | + * @param method 方法名 | ||
| 349 | + * @param data 响应数据 | ||
| 350 | + */ | ||
| 351 | + private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address, | ||
| 352 | + String requestId, String method, Object data) { | ||
| 353 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); | ||
| 354 | + writeResponse(socket, address, responseMessage); | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + /** | ||
| 358 | + * 发送错误响应 | ||
| 359 | + * | ||
| 360 | + * @param socket UDP Socket | ||
| 361 | + * @param address 目标地址 | ||
| 362 | + * @param requestId 请求 ID | ||
| 363 | + * @param method 方法名 | ||
| 364 | + * @param code 错误码 | ||
| 365 | + * @param msg 错误消息 | ||
| 366 | + */ | ||
| 367 | + private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, | ||
| 368 | + String requestId, String method, Integer code, String msg) { | ||
| 369 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); | ||
| 370 | + writeResponse(socket, address, responseMessage); | ||
| 371 | + } | ||
| 372 | + | ||
| 373 | + /** | ||
| 374 | + * 写入响应到 Socket | ||
| 375 | + * | ||
| 376 | + * @param socket UDP Socket | ||
| 377 | + * @param address 目标地址 | ||
| 378 | + * @param responseMessage 响应消息 | ||
| 379 | + */ | ||
| 380 | + private void writeResponse(DatagramSocket socket, InetSocketAddress address, IotDeviceMessage responseMessage) { | ||
| 381 | + try { | ||
| 382 | + byte[] serializedData = serializer.serialize(responseMessage); | ||
| 383 | + socket.send(Buffer.buffer(serializedData), address.getPort(), address.getHostString(), result -> { | ||
| 384 | + if (result.failed()) { | ||
| 385 | + log.error("[writeResponse][发送响应失败,地址: {}]", | ||
| 386 | + sessionManager.buildAddressKey(address), result.cause()); | ||
| 387 | + } | ||
| 388 | + }); | ||
| 389 | + } catch (Exception e) { | ||
| 390 | + log.error("[writeResponse][发送响应异常,地址: {}]", | ||
| 391 | + sessionManager.buildAddressKey(address), e); | ||
| 392 | + } | ||
| 393 | + } | ||
| 394 | + | ||
| 395 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
| @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer; | @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer; | ||
| 4 | import io.vertx.core.datagram.DatagramSocket; | 4 | import io.vertx.core.datagram.DatagramSocket; |
| 5 | import lombok.Data; | 5 | import lombok.Data; |
| 6 | import lombok.extern.slf4j.Slf4j; | 6 | import lombok.extern.slf4j.Slf4j; |
| 7 | -import org.springframework.stereotype.Component; | ||
| 8 | 7 | ||
| 9 | import java.net.InetSocketAddress; | 8 | import java.net.InetSocketAddress; |
| 10 | import java.time.LocalDateTime; | 9 | import java.time.LocalDateTime; |
| @@ -17,89 +16,120 @@ import java.util.concurrent.ConcurrentHashMap; | @@ -17,89 +16,120 @@ import java.util.concurrent.ConcurrentHashMap; | ||
| 17 | /** | 16 | /** |
| 18 | * IoT 网关 UDP 会话管理器 | 17 | * IoT 网关 UDP 会话管理器 |
| 19 | * <p> | 18 | * <p> |
| 20 | - * 采用无状态设计,SessionManager 主要用于: | ||
| 21 | - * 1. 管理设备地址映射(用于下行消息发送) | ||
| 22 | - * 2. 定期清理不活跃的设备地址映射 | ||
| 23 | - * <p> | ||
| 24 | - * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态 | 19 | + * 统一管理 UDP 会话的认证状态、设备会话和消息发送功能: |
| 20 | + * 1. 管理 UDP 会话的认证状态 | ||
| 21 | + * 2. 管理设备会话和在线状态 | ||
| 22 | + * 3. 管理消息发送到设备 | ||
| 25 | * | 23 | * |
| 26 | * @author 芋道源码 | 24 | * @author 芋道源码 |
| 27 | */ | 25 | */ |
| 28 | @Slf4j | 26 | @Slf4j |
| 29 | -@Component | ||
| 30 | public class IotUdpSessionManager { | 27 | public class IotUdpSessionManager { |
| 31 | 28 | ||
| 32 | /** | 29 | /** |
| 33 | - * 设备 ID -> 会话信息(包含地址和 codecType) | 30 | + * 最大会话数 |
| 34 | */ | 31 | */ |
| 35 | - private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>(); | 32 | + private final int maxSessions; |
| 36 | 33 | ||
| 37 | /** | 34 | /** |
| 38 | - * 设备地址 Key -> 最后活跃时间(用于清理) | 35 | + * 设备 ID -> 会话信息 |
| 39 | */ | 36 | */ |
| 40 | - private final Map<String, LocalDateTime> lastActiveTimeMap = new ConcurrentHashMap<>(); | 37 | + private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>(); |
| 41 | 38 | ||
| 42 | /** | 39 | /** |
| 43 | * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) | 40 | * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) |
| 44 | */ | 41 | */ |
| 42 | + // TODO @AI:1)这个变量是否必须?2)unregisterSession 这个方法是否必须? | ||
| 45 | private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>(); | 43 | private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>(); |
| 46 | 44 | ||
| 45 | + public IotUdpSessionManager(int maxSessions) { | ||
| 46 | + this.maxSessions = maxSessions; | ||
| 47 | + } | ||
| 48 | + | ||
| 47 | /** | 49 | /** |
| 48 | - * 更新设备会话(每次收到上行消息时调用) | 50 | + * 注册设备会话(包含认证信息) |
| 49 | * | 51 | * |
| 50 | - * @param deviceId 设备 ID | ||
| 51 | - * @param address 设备地址 | ||
| 52 | - * @param codecType 消息编解码类型 | 52 | + * @param deviceId 设备 ID |
| 53 | + * @param sessionInfo 会话信息 | ||
| 53 | */ | 54 | */ |
| 54 | - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { | ||
| 55 | - String addressKey = buildAddressKey(address); | ||
| 56 | - // 更新设备会话映射 | ||
| 57 | - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); | ||
| 58 | - lastActiveTimeMap.put(addressKey, LocalDateTime.now()); | 55 | + public void registerSession(Long deviceId, SessionInfo sessionInfo) { |
| 56 | + // 检查会话数是否已达上限 | ||
| 57 | + if (deviceSessionMap.size() >= maxSessions) { | ||
| 58 | + throw new IllegalStateException("会话数已达上限: " + maxSessions); | ||
| 59 | + } | ||
| 60 | + // 如果设备已有其他会话,先清理旧会话 | ||
| 61 | + SessionInfo oldSessionInfo = deviceSessionMap.get(deviceId); | ||
| 62 | + if (oldSessionInfo != null) { | ||
| 63 | + String oldAddressKey = buildAddressKey(oldSessionInfo.getAddress()); | ||
| 64 | + addressDeviceMap.remove(oldAddressKey, deviceId); | ||
| 65 | + log.info("[registerSession][设备已有其他会话,清理旧会话,设备 ID: {},旧地址: {}]", | ||
| 66 | + deviceId, oldAddressKey); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + // 注册新会话 | ||
| 70 | + String addressKey = buildAddressKey(sessionInfo.getAddress()); | ||
| 71 | + deviceSessionMap.put(deviceId, sessionInfo); | ||
| 59 | addressDeviceMap.put(addressKey, deviceId); | 72 | addressDeviceMap.put(addressKey, deviceId); |
| 60 | - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); | 73 | + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},product key: {},device name: {}]", |
| 74 | + deviceId, addressKey, sessionInfo.getProductKey(), sessionInfo.getDeviceName()); | ||
| 61 | } | 75 | } |
| 62 | 76 | ||
| 63 | /** | 77 | /** |
| 64 | - * 更新设备地址(兼容旧接口,默认不更新 codecType) | 78 | + * 注销设备会话 |
| 65 | * | 79 | * |
| 66 | * @param deviceId 设备 ID | 80 | * @param deviceId 设备 ID |
| 67 | - * @param address 设备地址 | ||
| 68 | */ | 81 | */ |
| 69 | - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { | ||
| 70 | - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | ||
| 71 | - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null; | ||
| 72 | - updateDeviceSession(deviceId, address, codecType); | 82 | + public void unregisterSession(Long deviceId) { |
| 83 | + SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); | ||
| 84 | + if (sessionInfo == null) { | ||
| 85 | + return; | ||
| 86 | + } | ||
| 87 | + String addressKey = buildAddressKey(sessionInfo.getAddress()); | ||
| 88 | + // 仅当 addressDeviceMap 中的 deviceId 是当前 deviceId 时才移除,避免误删新会话 | ||
| 89 | + addressDeviceMap.remove(addressKey, deviceId); | ||
| 90 | + log.info("[unregisterSession][注销设备会话,设备 ID: {},地址: {}]", deviceId, addressKey); | ||
| 73 | } | 91 | } |
| 74 | 92 | ||
| 75 | /** | 93 | /** |
| 76 | - * 获取设备会话信息 | 94 | + * 更新会话活跃时间(每次收到上行消息时调用) |
| 77 | * | 95 | * |
| 78 | * @param deviceId 设备 ID | 96 | * @param deviceId 设备 ID |
| 79 | - * @return 会话信息 | ||
| 80 | */ | 97 | */ |
| 81 | - public SessionInfo getSessionInfo(Long deviceId) { | ||
| 82 | - return deviceSessionMap.get(deviceId); | 98 | + public void updateSessionActivity(Long deviceId) { |
| 99 | + SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | ||
| 100 | + if (sessionInfo != null) { | ||
| 101 | + sessionInfo.setLastActiveTime(LocalDateTime.now()); | ||
| 102 | + } | ||
| 83 | } | 103 | } |
| 84 | 104 | ||
| 85 | /** | 105 | /** |
| 86 | - * 检查设备是否在线(即是否有地址映射) | 106 | + * 更新设备会话地址(设备地址变更时调用) |
| 87 | * | 107 | * |
| 88 | - * @param deviceId 设备 ID | ||
| 89 | - * @return 是否在线 | 108 | + * @param deviceId 设备 ID |
| 109 | + * @param newAddress 新地址 | ||
| 90 | */ | 110 | */ |
| 91 | - public boolean isDeviceOnline(Long deviceId) { | ||
| 92 | - return deviceSessionMap.containsKey(deviceId); | 111 | + public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { |
| 112 | + SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | ||
| 113 | + if (sessionInfo == null) { | ||
| 114 | + return; | ||
| 115 | + } | ||
| 116 | + // 清理旧地址映射 | ||
| 117 | + String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); | ||
| 118 | + addressDeviceMap.remove(oldAddressKey, deviceId); | ||
| 119 | + | ||
| 120 | + // 更新新地址 | ||
| 121 | + String newAddressKey = buildAddressKey(newAddress); | ||
| 122 | + sessionInfo.setAddress(newAddress); | ||
| 123 | + sessionInfo.setLastActiveTime(LocalDateTime.now()); | ||
| 124 | + addressDeviceMap.put(newAddressKey, deviceId); | ||
| 125 | + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},新地址: {}]", deviceId, newAddressKey); | ||
| 93 | } | 126 | } |
| 94 | 127 | ||
| 95 | /** | 128 | /** |
| 96 | - * 检查设备是否离线 | ||
| 97 | - * | ||
| 98 | - * @param deviceId 设备 ID | ||
| 99 | - * @return 是否离线 | 129 | + * 获取会话信息 |
| 100 | */ | 130 | */ |
| 101 | - public boolean isDeviceOffline(Long deviceId) { | ||
| 102 | - return !isDeviceOnline(deviceId); | 131 | + public SessionInfo getSessionInfo(Long deviceId) { |
| 132 | + return deviceSessionMap.get(deviceId); | ||
| 103 | } | 133 | } |
| 104 | 134 | ||
| 105 | /** | 135 | /** |
| @@ -116,17 +146,16 @@ public class IotUdpSessionManager { | @@ -116,17 +146,16 @@ public class IotUdpSessionManager { | ||
| 116 | log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); | 146 | log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); |
| 117 | return false; | 147 | return false; |
| 118 | } | 148 | } |
| 119 | - | ||
| 120 | InetSocketAddress address = sessionInfo.getAddress(); | 149 | InetSocketAddress address = sessionInfo.getAddress(); |
| 121 | try { | 150 | try { |
| 122 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { | 151 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 123 | if (result.succeeded()) { | 152 | if (result.succeeded()) { |
| 124 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", | 153 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", |
| 125 | deviceId, buildAddressKey(address), data.length); | 154 | deviceId, buildAddressKey(address), data.length); |
| 126 | - } else { | ||
| 127 | - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | ||
| 128 | - deviceId, buildAddressKey(address), result.cause()); | 155 | + return; |
| 129 | } | 156 | } |
| 157 | + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | ||
| 158 | + deviceId, buildAddressKey(address), result.cause()); | ||
| 130 | }); | 159 | }); |
| 131 | return true; | 160 | return true; |
| 132 | } catch (Exception e) { | 161 | } catch (Exception e) { |
| @@ -136,37 +165,31 @@ public class IotUdpSessionManager { | @@ -136,37 +165,31 @@ public class IotUdpSessionManager { | ||
| 136 | } | 165 | } |
| 137 | 166 | ||
| 138 | /** | 167 | /** |
| 139 | - * 定期清理不活跃的设备地址映射 | 168 | + * 定期清理不活跃的设备会话 |
| 140 | * | 169 | * |
| 141 | * @param timeoutMs 超时时间(毫秒) | 170 | * @param timeoutMs 超时时间(毫秒) |
| 142 | * @return 清理的设备 ID 列表(用于发送离线消息) | 171 | * @return 清理的设备 ID 列表(用于发送离线消息) |
| 143 | */ | 172 | */ |
| 144 | - public List<Long> cleanExpiredMappings(long timeoutMs) { | 173 | + public List<Long> cleanExpiredSessions(long timeoutMs) { |
| 145 | List<Long> offlineDeviceIds = new ArrayList<>(); | 174 | List<Long> offlineDeviceIds = new ArrayList<>(); |
| 146 | LocalDateTime now = LocalDateTime.now(); | 175 | LocalDateTime now = LocalDateTime.now(); |
| 147 | LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); | 176 | LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); |
| 148 | - Iterator<Map.Entry<String, LocalDateTime>> iterator = lastActiveTimeMap.entrySet().iterator(); | 177 | + Iterator<Map.Entry<Long, SessionInfo>> iterator = deviceSessionMap.entrySet().iterator(); |
| 178 | + // TODO @AI:改成 for each 会不会更好? | ||
| 149 | while (iterator.hasNext()) { | 179 | while (iterator.hasNext()) { |
| 180 | + Map.Entry<Long, SessionInfo> entry = iterator.next(); | ||
| 181 | + SessionInfo sessionInfo = entry.getValue(); | ||
| 150 | // 未过期,跳过 | 182 | // 未过期,跳过 |
| 151 | - Map.Entry<String, LocalDateTime> entry = iterator.next(); | ||
| 152 | - if (entry.getValue().isAfter(expireTime)) { | 183 | + if (sessionInfo.getLastActiveTime().isAfter(expireTime)) { |
| 153 | continue; | 184 | continue; |
| 154 | } | 185 | } |
| 155 | // 过期处理:记录离线设备 ID | 186 | // 过期处理:记录离线设备 ID |
| 156 | - String addressKey = entry.getKey(); | ||
| 157 | - Long deviceId = addressDeviceMap.remove(addressKey); | ||
| 158 | - if (deviceId == null) { | ||
| 159 | - iterator.remove(); | ||
| 160 | - continue; | ||
| 161 | - } | ||
| 162 | - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); | ||
| 163 | - if (sessionInfo == null) { | ||
| 164 | - iterator.remove(); | ||
| 165 | - continue; | ||
| 166 | - } | 187 | + Long deviceId = entry.getKey(); |
| 188 | + String addressKey = buildAddressKey(sessionInfo.getAddress()); | ||
| 189 | + addressDeviceMap.remove(addressKey, deviceId); | ||
| 167 | offlineDeviceIds.add(deviceId); | 190 | offlineDeviceIds.add(deviceId); |
| 168 | - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", | ||
| 169 | - deviceId, addressKey, entry.getValue()); | 191 | + log.debug("[cleanExpiredSessions][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", |
| 192 | + deviceId, addressKey, sessionInfo.getLastActiveTime()); | ||
| 170 | iterator.remove(); | 193 | iterator.remove(); |
| 171 | } | 194 | } |
| 172 | return offlineDeviceIds; | 195 | return offlineDeviceIds; |
| @@ -183,20 +206,32 @@ public class IotUdpSessionManager { | @@ -183,20 +206,32 @@ public class IotUdpSessionManager { | ||
| 183 | } | 206 | } |
| 184 | 207 | ||
| 185 | /** | 208 | /** |
| 186 | - * 会话信息 | 209 | + * 会话信息(包含认证信息) |
| 187 | */ | 210 | */ |
| 188 | @Data | 211 | @Data |
| 189 | public static class SessionInfo { | 212 | public static class SessionInfo { |
| 190 | 213 | ||
| 191 | /** | 214 | /** |
| 215 | + * 设备 ID | ||
| 216 | + */ | ||
| 217 | + private Long deviceId; | ||
| 218 | + /** | ||
| 219 | + * 产品 Key | ||
| 220 | + */ | ||
| 221 | + private String productKey; | ||
| 222 | + /** | ||
| 223 | + * 设备名称 | ||
| 224 | + */ | ||
| 225 | + private String deviceName; | ||
| 226 | + | ||
| 227 | + /** | ||
| 192 | * 设备地址 | 228 | * 设备地址 |
| 193 | */ | 229 | */ |
| 194 | private InetSocketAddress address; | 230 | private InetSocketAddress address; |
| 195 | - | ||
| 196 | /** | 231 | /** |
| 197 | - * 消息编解码类型 | 232 | + * 最后活跃时间 |
| 198 | */ | 233 | */ |
| 199 | - private String codecType; | 234 | + private LocalDateTime lastActiveTime; |
| 200 | 235 | ||
| 201 | } | 236 | } |
| 202 | 237 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.map.MapUtil; | ||
| 4 | -import cn.hutool.core.util.BooleanUtil; | ||
| 5 | -import cn.hutool.core.util.StrUtil; | ||
| 6 | -import cn.hutool.extra.spring.SpringUtil; | ||
| 7 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 8 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 9 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 11 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 12 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 13 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 14 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | ||
| 15 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 16 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | ||
| 17 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 18 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | ||
| 19 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | ||
| 20 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; | ||
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | ||
| 22 | -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | ||
| 23 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 24 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 25 | -import io.vertx.core.buffer.Buffer; | ||
| 26 | -import io.vertx.core.datagram.DatagramPacket; | ||
| 27 | -import io.vertx.core.datagram.DatagramSocket; | ||
| 28 | -import lombok.extern.slf4j.Slf4j; | ||
| 29 | - | ||
| 30 | -import java.net.InetSocketAddress; | ||
| 31 | -import java.util.Map; | ||
| 32 | - | ||
| 33 | -/** | ||
| 34 | - * UDP 上行消息处理器 | ||
| 35 | - * <p> | ||
| 36 | - * 采用无状态 Token 机制(每次请求携带 token): | ||
| 37 | - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password | ||
| 38 | - * 2. 返回 Token:服务端验证后返回 JWT token | ||
| 39 | - * 3. 后续请求:每次请求在 params 中携带 token | ||
| 40 | - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 | ||
| 41 | - * | ||
| 42 | - * @author 芋道源码 | ||
| 43 | - */ | ||
| 44 | -@Slf4j | ||
| 45 | -public class IotUdpUpstreamHandler { | ||
| 46 | - | ||
| 47 | - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; | ||
| 48 | - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; | ||
| 49 | - | ||
| 50 | - private static final String AUTH_METHOD = "auth"; | ||
| 51 | - /** | ||
| 52 | - * Token 参数 Key | ||
| 53 | - */ | ||
| 54 | - private static final String PARAM_KEY_TOKEN = "token"; | ||
| 55 | - /** | ||
| 56 | - * Body 参数 Key(实际请求内容) | ||
| 57 | - */ | ||
| 58 | - private static final String PARAM_KEY_BODY = "body"; | ||
| 59 | - | ||
| 60 | - private final IotDeviceMessageService deviceMessageService; | ||
| 61 | - | ||
| 62 | - private final IotDeviceService deviceService; | ||
| 63 | - | ||
| 64 | - private final IotUdpSessionManager sessionManager; | ||
| 65 | - | ||
| 66 | - private final IotDeviceTokenService deviceTokenService; | ||
| 67 | - | ||
| 68 | - private final IotDeviceCommonApi deviceApi; | ||
| 69 | - | ||
| 70 | - private final String serverId; | ||
| 71 | - | ||
| 72 | - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol, | ||
| 73 | - IotDeviceMessageService deviceMessageService, | ||
| 74 | - IotDeviceService deviceService, | ||
| 75 | - IotUdpSessionManager sessionManager) { | ||
| 76 | - this.deviceMessageService = deviceMessageService; | ||
| 77 | - this.deviceService = deviceService; | ||
| 78 | - this.sessionManager = sessionManager; | ||
| 79 | - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | ||
| 80 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | ||
| 81 | - this.serverId = protocol.getServerId(); | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | - /** | ||
| 85 | - * 处理 UDP 数据包 | ||
| 86 | - * | ||
| 87 | - * @param packet 数据包 | ||
| 88 | - * @param socket UDP Socket | ||
| 89 | - */ | ||
| 90 | - public void handle(DatagramPacket packet, DatagramSocket socket) { | ||
| 91 | - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); | ||
| 92 | - Buffer data = packet.data(); | ||
| 93 | - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", | ||
| 94 | - sessionManager.buildAddressKey(senderAddress), data.length()); | ||
| 95 | - try { | ||
| 96 | - processMessage(data, senderAddress, socket); | ||
| 97 | - } catch (Exception e) { | ||
| 98 | - log.error("[handle][处理消息失败,来源: {},错误: {}]", | ||
| 99 | - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e); | ||
| 100 | - // UDP 无连接,不需要断开连接,只记录错误 | ||
| 101 | - } | ||
| 102 | - } | ||
| 103 | - | ||
| 104 | - /** | ||
| 105 | - * 处理消息 | ||
| 106 | - * | ||
| 107 | - * @param buffer 消息 | ||
| 108 | - * @param senderAddress 发送者地址 | ||
| 109 | - * @param socket UDP Socket | ||
| 110 | - */ | ||
| 111 | - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { | ||
| 112 | - // 1. 基础检查 | ||
| 113 | - if (buffer == null || buffer.length() == 0) { | ||
| 114 | - return; | ||
| 115 | - } | ||
| 116 | - | ||
| 117 | - // 2. 获取消息格式类型 | ||
| 118 | - String codecType = getMessageCodecType(buffer); | ||
| 119 | - | ||
| 120 | - // 3. 解码消息 | ||
| 121 | - IotDeviceMessage message; | ||
| 122 | - try { | ||
| 123 | - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); | ||
| 124 | - if (message == null) { | ||
| 125 | - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress)); | ||
| 126 | - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType); | ||
| 127 | - return; | ||
| 128 | - } | ||
| 129 | - } catch (Exception e) { | ||
| 130 | - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e); | ||
| 131 | - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType); | ||
| 132 | - return; | ||
| 133 | - } | ||
| 134 | - | ||
| 135 | - // 4. 根据消息类型路由处理 | ||
| 136 | - try { | ||
| 137 | - if (AUTH_METHOD.equals(message.getMethod())) { | ||
| 138 | - // 认证请求 | ||
| 139 | - handleAuthenticationRequest(message, codecType, senderAddress, socket); | ||
| 140 | - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | ||
| 141 | - // 设备动态注册请求 | ||
| 142 | - handleRegisterRequest(message, codecType, senderAddress, socket); | ||
| 143 | - } else { | ||
| 144 | - // 业务消息 | ||
| 145 | - handleBusinessRequest(message, codecType, senderAddress, socket); | ||
| 146 | - } | ||
| 147 | - } catch (Exception e) { | ||
| 148 | - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]", | ||
| 149 | - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e); | ||
| 150 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType); | ||
| 151 | - } | ||
| 152 | - } | ||
| 153 | - | ||
| 154 | - /** | ||
| 155 | - * 处理认证请求 | ||
| 156 | - * | ||
| 157 | - * @param message 消息信息 | ||
| 158 | - * @param codecType 消息编解码类型 | ||
| 159 | - * @param senderAddress 发送者地址 | ||
| 160 | - * @param socket UDP Socket | ||
| 161 | - */ | ||
| 162 | - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType, | ||
| 163 | - InetSocketAddress senderAddress, DatagramSocket socket) { | ||
| 164 | - String addressKey = sessionManager.buildAddressKey(senderAddress); | ||
| 165 | - try { | ||
| 166 | - // 1.1 解析认证参数 | ||
| 167 | - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); | ||
| 168 | - if (authParams == null) { | ||
| 169 | - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey); | ||
| 170 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType); | ||
| 171 | - return; | ||
| 172 | - } | ||
| 173 | - // 1.2 执行认证 | ||
| 174 | - if (!validateDeviceAuth(authParams)) { | ||
| 175 | - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]", | ||
| 176 | - addressKey, authParams.getUsername()); | ||
| 177 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType); | ||
| 178 | - return; | ||
| 179 | - } | ||
| 180 | - | ||
| 181 | - // 2.1 解析设备信息 | ||
| 182 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); | ||
| 183 | - if (deviceInfo == null) { | ||
| 184 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); | ||
| 185 | - return; | ||
| 186 | - } | ||
| 187 | - // 2.2 获取设备信息 | ||
| 188 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | ||
| 189 | - deviceInfo.getDeviceName()); | ||
| 190 | - if (device == null) { | ||
| 191 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); | ||
| 192 | - return; | ||
| 193 | - } | ||
| 194 | - | ||
| 195 | - // 3.1 生成 JWT Token(无状态) | ||
| 196 | - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); | ||
| 197 | - | ||
| 198 | - // 3.2 更新设备会话信息(用于下行消息,保存 codecType) | ||
| 199 | - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); | ||
| 200 | - | ||
| 201 | - // 3.3 发送上线消息 | ||
| 202 | - sendOnlineMessage(device); | ||
| 203 | - | ||
| 204 | - // 3.4 发送成功响应(包含 token) | ||
| 205 | - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType); | ||
| 206 | - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", | ||
| 207 | - device.getId(), device.getDeviceName(), addressKey); | ||
| 208 | - } catch (Exception e) { | ||
| 209 | - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e); | ||
| 210 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType); | ||
| 211 | - } | ||
| 212 | - } | ||
| 213 | - | ||
| 214 | - /** | ||
| 215 | - * 处理设备动态注册请求(一型一密,不需要 Token) | ||
| 216 | - * | ||
| 217 | - * @param message 消息信息 | ||
| 218 | - * @param codecType 消息编解码类型 | ||
| 219 | - * @param senderAddress 发送者地址 | ||
| 220 | - * @param socket UDP Socket | ||
| 221 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | ||
| 222 | - */ | ||
| 223 | - private void handleRegisterRequest(IotDeviceMessage message, String codecType, | ||
| 224 | - InetSocketAddress senderAddress, DatagramSocket socket) { | ||
| 225 | - String addressKey = sessionManager.buildAddressKey(senderAddress); | ||
| 226 | - try { | ||
| 227 | - // 1. 解析注册参数 | ||
| 228 | - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | ||
| 229 | - if (params == null) { | ||
| 230 | - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey); | ||
| 231 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType); | ||
| 232 | - return; | ||
| 233 | - } | ||
| 234 | - | ||
| 235 | - // 2. 调用动态注册 | ||
| 236 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | ||
| 237 | - if (result.isError()) { | ||
| 238 | - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg()); | ||
| 239 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType); | ||
| 240 | - return; | ||
| 241 | - } | ||
| 242 | - | ||
| 243 | - // 3. 发送成功响应(包含 deviceSecret) | ||
| 244 | - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType); | ||
| 245 | - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]", | ||
| 246 | - params.getDeviceName(), addressKey); | ||
| 247 | - } catch (Exception e) { | ||
| 248 | - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e); | ||
| 249 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType); | ||
| 250 | - } | ||
| 251 | - } | ||
| 252 | - | ||
| 253 | - /** | ||
| 254 | - * 处理业务请求 | ||
| 255 | - * <p> | ||
| 256 | - * 请求参数格式: | ||
| 257 | - * - token:JWT 令牌 | ||
| 258 | - * - body:实际请求内容(可以是 Map、List 或其他类型) | ||
| 259 | - * | ||
| 260 | - * @param message 消息信息 | ||
| 261 | - * @param codecType 消息编解码类型 | ||
| 262 | - * @param senderAddress 发送者地址 | ||
| 263 | - * @param socket UDP Socket | ||
| 264 | - */ | ||
| 265 | - @SuppressWarnings("unchecked") | ||
| 266 | - private void handleBusinessRequest(IotDeviceMessage message, String codecType, | ||
| 267 | - InetSocketAddress senderAddress, DatagramSocket socket) { | ||
| 268 | - String addressKey = sessionManager.buildAddressKey(senderAddress); | ||
| 269 | - try { | ||
| 270 | - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}) | ||
| 271 | - String token = null; | ||
| 272 | - Object body = null; | ||
| 273 | - if (message.getParams() instanceof Map) { | ||
| 274 | - Map<String, Object> paramsMap = (Map<String, Object>) message.getParams(); | ||
| 275 | - token = (String) paramsMap.get(PARAM_KEY_TOKEN); | ||
| 276 | - body = paramsMap.get(PARAM_KEY_BODY); | ||
| 277 | - } | ||
| 278 | - if (StrUtil.isBlank(token)) { | ||
| 279 | - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); | ||
| 280 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType); | ||
| 281 | - return; | ||
| 282 | - } | ||
| 283 | - // 1.2 验证 token,获取设备信息 | ||
| 284 | - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); | ||
| 285 | - if (deviceInfo == null) { | ||
| 286 | - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); | ||
| 287 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); | ||
| 288 | - return; | ||
| 289 | - } | ||
| 290 | - | ||
| 291 | - // 2. 获取设备详细信息 | ||
| 292 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | ||
| 293 | - deviceInfo.getDeviceName()); | ||
| 294 | - if (device == null) { | ||
| 295 | - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", | ||
| 296 | - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | ||
| 297 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); | ||
| 298 | - return; | ||
| 299 | - } | ||
| 300 | - | ||
| 301 | - // 3. 更新设备会话信息(保持最新,保存 codecType) | ||
| 302 | - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); | ||
| 303 | - | ||
| 304 | - // 4. 将 body 设置为实际的 params,发送消息到消息总线 | ||
| 305 | - message.setParams(body); | ||
| 306 | - deviceMessageService.sendDeviceMessage(message, device.getProductKey(), | ||
| 307 | - device.getDeviceName(), serverId); | ||
| 308 | - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", | ||
| 309 | - device.getId(), message.getMethod(), addressKey); | ||
| 310 | - } catch (Exception e) { | ||
| 311 | - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e); | ||
| 312 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType); | ||
| 313 | - } | ||
| 314 | - } | ||
| 315 | - | ||
| 316 | - /** | ||
| 317 | - * 获取消息编解码类型 | ||
| 318 | - * | ||
| 319 | - * @param buffer 消息 | ||
| 320 | - * @return 消息编解码类型 | ||
| 321 | - */ | ||
| 322 | - private String getMessageCodecType(Buffer buffer) { | ||
| 323 | - // 检测消息格式类型 | ||
| 324 | - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY | ||
| 325 | - : CODEC_TYPE_JSON; | ||
| 326 | - } | ||
| 327 | - | ||
| 328 | - /** | ||
| 329 | - * 发送设备上线消息 | ||
| 330 | - * | ||
| 331 | - * @param device 设备信息 | ||
| 332 | - */ | ||
| 333 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | ||
| 334 | - try { | ||
| 335 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | ||
| 336 | - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | ||
| 337 | - device.getDeviceName(), serverId); | ||
| 338 | - } catch (Exception e) { | ||
| 339 | - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); | ||
| 340 | - } | ||
| 341 | - } | ||
| 342 | - | ||
| 343 | - /** | ||
| 344 | - * 验证设备认证信息 | ||
| 345 | - * | ||
| 346 | - * @param authParams 认证参数 | ||
| 347 | - * @return 是否认证成功 | ||
| 348 | - */ | ||
| 349 | - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { | ||
| 350 | - try { | ||
| 351 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | ||
| 352 | - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) | ||
| 353 | - .setPassword(authParams.getPassword())); | ||
| 354 | - result.checkError(); | ||
| 355 | - return BooleanUtil.isTrue(result.getData()); | ||
| 356 | - } catch (Exception e) { | ||
| 357 | - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); | ||
| 358 | - return false; | ||
| 359 | - } | ||
| 360 | - } | ||
| 361 | - | ||
| 362 | - /** | ||
| 363 | - * 发送认证成功响应(包含 token) | ||
| 364 | - * | ||
| 365 | - * @param socket UDP Socket | ||
| 366 | - * @param address 目标地址 | ||
| 367 | - * @param requestId 请求 ID | ||
| 368 | - * @param token JWT Token | ||
| 369 | - * @param codecType 消息编解码类型 | ||
| 370 | - */ | ||
| 371 | - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, | ||
| 372 | - String requestId, String token, String codecType) { | ||
| 373 | - try { | ||
| 374 | - // 构建响应数据 | ||
| 375 | - Object responseData = MapUtil.builder() | ||
| 376 | - .put("success", true) | ||
| 377 | - .put("token", token) | ||
| 378 | - .put("message", "认证成功") | ||
| 379 | - .build(); | ||
| 380 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); | ||
| 381 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | ||
| 382 | - // 发送响应 | ||
| 383 | - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { | ||
| 384 | - if (result.failed()) { | ||
| 385 | - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", | ||
| 386 | - sessionManager.buildAddressKey(address), result.cause()); | ||
| 387 | - } | ||
| 388 | - }); | ||
| 389 | - } catch (Exception e) { | ||
| 390 | - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]", | ||
| 391 | - sessionManager.buildAddressKey(address), e); | ||
| 392 | - } | ||
| 393 | - } | ||
| 394 | - | ||
| 395 | - /** | ||
| 396 | - * 发送注册成功响应(包含 deviceSecret) | ||
| 397 | - * | ||
| 398 | - * @param socket UDP Socket | ||
| 399 | - * @param address 目标地址 | ||
| 400 | - * @param requestId 请求 ID | ||
| 401 | - * @param registerResp 注册响应 | ||
| 402 | - * @param codecType 消息编解码类型 | ||
| 403 | - */ | ||
| 404 | - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address, | ||
| 405 | - String requestId, IotDeviceRegisterRespDTO registerResp, | ||
| 406 | - String codecType) { | ||
| 407 | - try { | ||
| 408 | - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | ||
| 409 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | ||
| 410 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | ||
| 411 | - // 2. 发送响应 | ||
| 412 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | ||
| 413 | - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { | ||
| 414 | - if (result.failed()) { | ||
| 415 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]", | ||
| 416 | - sessionManager.buildAddressKey(address), result.cause()); | ||
| 417 | - } | ||
| 418 | - }); | ||
| 419 | - } catch (Exception e) { | ||
| 420 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]", | ||
| 421 | - sessionManager.buildAddressKey(address), e); | ||
| 422 | - } | ||
| 423 | - } | ||
| 424 | - | ||
| 425 | - /** | ||
| 426 | - * 发送错误响应 | ||
| 427 | - * | ||
| 428 | - * @param socket UDP Socket | ||
| 429 | - * @param address 目标地址 | ||
| 430 | - * @param requestId 请求 ID | ||
| 431 | - * @param errorMessage 错误消息 | ||
| 432 | - * @param codecType 消息编解码类型 | ||
| 433 | - */ | ||
| 434 | - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, | ||
| 435 | - String requestId, String errorMessage, String codecType) { | ||
| 436 | - sendResponse(socket, address, false, errorMessage, requestId, codecType); | ||
| 437 | - } | ||
| 438 | - | ||
| 439 | - /** | ||
| 440 | - * 发送响应消息 | ||
| 441 | - * | ||
| 442 | - * @param socket UDP Socket | ||
| 443 | - * @param address 目标地址 | ||
| 444 | - * @param success 是否成功 | ||
| 445 | - * @param message 消息 | ||
| 446 | - * @param requestId 请求 ID | ||
| 447 | - * @param codecType 消息编解码类型 | ||
| 448 | - */ | ||
| 449 | - @SuppressWarnings("SameParameterValue") | ||
| 450 | - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, | ||
| 451 | - String message, String requestId, String codecType) { | ||
| 452 | - try { | ||
| 453 | - // 构建响应数据 | ||
| 454 | - Object responseData = MapUtil.builder() | ||
| 455 | - .put("success", success) | ||
| 456 | - .put("message", message) | ||
| 457 | - .build(); | ||
| 458 | - int code = success ? 0 : 401; | ||
| 459 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | ||
| 460 | - "response", responseData, code, message); | ||
| 461 | - | ||
| 462 | - // 发送响应 | ||
| 463 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | ||
| 464 | - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { | ||
| 465 | - if (ar.failed()) { | ||
| 466 | - log.error("[sendResponse][发送响应失败,地址: {}]", | ||
| 467 | - sessionManager.buildAddressKey(address), ar.cause()); | ||
| 468 | - } | ||
| 469 | - }); | ||
| 470 | - } catch (Exception e) { | ||
| 471 | - log.error("[sendResponse][发送响应异常,地址: {}]", | ||
| 472 | - sessionManager.buildAddressKey(address), e); | ||
| 473 | - } | ||
| 474 | - } | ||
| 475 | - | ||
| 476 | - /** | ||
| 477 | - * 解析认证参数 | ||
| 478 | - * | ||
| 479 | - * @param params 参数对象(通常为 Map 类型) | ||
| 480 | - * @return 认证参数 DTO,解析失败时返回 null | ||
| 481 | - */ | ||
| 482 | - @SuppressWarnings("unchecked") | ||
| 483 | - private IotDeviceAuthReqDTO parseAuthParams(Object params) { | ||
| 484 | - if (params == null) { | ||
| 485 | - return null; | ||
| 486 | - } | ||
| 487 | - try { | ||
| 488 | - // 参数默认为 Map 类型,直接转换 | ||
| 489 | - if (params instanceof Map) { | ||
| 490 | - Map<String, Object> paramMap = (Map<String, Object>) params; | ||
| 491 | - return new IotDeviceAuthReqDTO() | ||
| 492 | - .setClientId(MapUtil.getStr(paramMap, "clientId")) | ||
| 493 | - .setUsername(MapUtil.getStr(paramMap, "username")) | ||
| 494 | - .setPassword(MapUtil.getStr(paramMap, "password")); | ||
| 495 | - } | ||
| 496 | - // 如果已经是目标类型,直接返回 | ||
| 497 | - if (params instanceof IotDeviceAuthReqDTO) { | ||
| 498 | - return (IotDeviceAuthReqDTO) params; | ||
| 499 | - } | ||
| 500 | - | ||
| 501 | - // 其他情况尝试 JSON 转换 | ||
| 502 | - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); | ||
| 503 | - } catch (Exception e) { | ||
| 504 | - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); | ||
| 505 | - return null; | ||
| 506 | - } | ||
| 507 | - } | ||
| 508 | - | ||
| 509 | - /** | ||
| 510 | - * 解析注册参数 | ||
| 511 | - * | ||
| 512 | - * @param params 参数对象(通常为 Map 类型) | ||
| 513 | - * @return 注册参数 DTO,解析失败时返回 null | ||
| 514 | - */ | ||
| 515 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | ||
| 516 | - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | ||
| 517 | - if (params == null) { | ||
| 518 | - return null; | ||
| 519 | - } | ||
| 520 | - try { | ||
| 521 | - // 参数默认为 Map 类型,直接转换 | ||
| 522 | - if (params instanceof Map) { | ||
| 523 | - Map<String, Object> paramMap = (Map<String, Object>) params; | ||
| 524 | - return new IotDeviceRegisterReqDTO() | ||
| 525 | - .setProductKey(MapUtil.getStr(paramMap, "productKey")) | ||
| 526 | - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | ||
| 527 | - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | ||
| 528 | - } | ||
| 529 | - // 如果已经是目标类型,直接返回 | ||
| 530 | - if (params instanceof IotDeviceRegisterReqDTO) { | ||
| 531 | - return (IotDeviceRegisterReqDTO) params; | ||
| 532 | - } | ||
| 533 | - | ||
| 534 | - // 其他情况尝试 JSON 转换 | ||
| 535 | - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); | ||
| 536 | - } catch (Exception e) { | ||
| 537 | - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | ||
| 538 | - return null; | ||
| 539 | - } | ||
| 540 | - } | ||
| 541 | - | ||
| 542 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -50,7 +50,7 @@ yudao: | @@ -50,7 +50,7 @@ yudao: | ||
| 50 | - id: http-json | 50 | - id: http-json |
| 51 | type: http | 51 | type: http |
| 52 | port: 8092 | 52 | port: 8092 |
| 53 | - enabled: true | 53 | + enabled: false |
| 54 | http: | 54 | http: |
| 55 | ssl-enabled: false | 55 | ssl-enabled: false |
| 56 | # ==================================== | 56 | # ==================================== |
| @@ -59,7 +59,7 @@ yudao: | @@ -59,7 +59,7 @@ yudao: | ||
| 59 | - id: tcp-json | 59 | - id: tcp-json |
| 60 | type: tcp | 60 | type: tcp |
| 61 | port: 8091 | 61 | port: 8091 |
| 62 | - enabled: true | 62 | + enabled: false |
| 63 | serialize: json | 63 | serialize: json |
| 64 | tcp: | 64 | tcp: |
| 65 | max-connections: 1000 | 65 | max-connections: 1000 |
| @@ -75,6 +75,20 @@ yudao: | @@ -75,6 +75,20 @@ yudao: | ||
| 75 | # initial-bytes-to-strip: 4 # 初始跳过的字节数 | 75 | # initial-bytes-to-strip: 4 # 初始跳过的字节数 |
| 76 | # type: fixed_length # 拆包类型:length_field / delimiter / fixed_length | 76 | # type: fixed_length # 拆包类型:length_field / delimiter / fixed_length |
| 77 | # fixed-length: 256 # 固定长度 | 77 | # fixed-length: 256 # 固定长度 |
| 78 | + # ==================================== | ||
| 79 | + # 针对引入的 UDP 组件的配置 | ||
| 80 | + # ==================================== | ||
| 81 | + - id: udp-json | ||
| 82 | + type: udp | ||
| 83 | + port: 8093 | ||
| 84 | + enabled: true | ||
| 85 | + serialize: json | ||
| 86 | + udp: | ||
| 87 | + max-sessions: 1000 # 最大会话数 | ||
| 88 | + session-timeout-ms: 60000 # 会话超时时间(毫秒) | ||
| 89 | + session-clean-interval-ms: 30000 # 会话清理间隔(毫秒) | ||
| 90 | + receive-buffer-size: 65536 # 接收缓冲区大小(字节) | ||
| 91 | + send-buffer-size: 65536 # 发送缓冲区大小(字节) | ||
| 78 | 92 | ||
| 79 | # 协议配置(旧版,保持兼容) | 93 | # 协议配置(旧版,保持兼容) |
| 80 | protocol: | 94 | protocol: |
| @@ -113,16 +127,6 @@ yudao: | @@ -113,16 +127,6 @@ yudao: | ||
| 113 | trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 | 127 | trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 |
| 114 | trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 | 128 | trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 |
| 115 | # ==================================== | 129 | # ==================================== |
| 116 | - # 针对引入的 UDP 组件的配置 | ||
| 117 | - # ==================================== | ||
| 118 | - udp: | ||
| 119 | - enabled: false # 是否启用 UDP | ||
| 120 | - port: 8093 # UDP 服务端口 | ||
| 121 | - receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB) | ||
| 122 | - send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB) | ||
| 123 | - session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒) | ||
| 124 | - session-clean-interval-ms: 30000 # 会话清理间隔(毫秒,默认 30 秒) | ||
| 125 | - # ==================================== | ||
| 126 | # 针对引入的 MQTT 组件的配置 | 130 | # 针对引入的 MQTT 组件的配置 |
| 127 | # ==================================== | 131 | # ==================================== |
| 128 | mqtt: | 132 | mqtt: |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp; |
| 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; |
| @@ -9,9 +8,8 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | @@ -9,9 +8,8 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 13 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | ||
| 14 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | 11 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | ||
| 15 | import lombok.extern.slf4j.Slf4j; | 13 | import lombok.extern.slf4j.Slf4j; |
| 16 | import org.junit.jupiter.api.Disabled; | 14 | import org.junit.jupiter.api.Disabled; |
| 17 | import org.junit.jupiter.api.Test; | 15 | import org.junit.jupiter.api.Test; |
| @@ -27,16 +25,9 @@ import java.util.Map; | @@ -27,16 +25,9 @@ import java.util.Map; | ||
| 27 | * | 25 | * |
| 28 | * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 | 26 | * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 |
| 29 | * | 27 | * |
| 30 | - * <p>支持两种编解码格式: | ||
| 31 | - * <ul> | ||
| 32 | - * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li> | ||
| 33 | - * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li> | ||
| 34 | - * </ul> | ||
| 35 | - * | ||
| 36 | * <p>使用步骤: | 28 | * <p>使用步骤: |
| 37 | * <ol> | 29 | * <ol> |
| 38 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> | 30 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> |
| 39 | - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li> | ||
| 40 | * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li> | 31 | * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li> |
| 41 | * <li>运行以下测试方法: | 32 | * <li>运行以下测试方法: |
| 42 | * <ul> | 33 | * <ul> |
| @@ -58,10 +49,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -58,10 +49,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 58 | private static final int SERVER_PORT = 8093; | 49 | private static final int SERVER_PORT = 8093; |
| 59 | private static final int TIMEOUT_MS = 5000; | 50 | private static final int TIMEOUT_MS = 5000; |
| 60 | 51 | ||
| 61 | - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== | 52 | + // ===================== 序列化器 ===================== |
| 62 | 53 | ||
| 63 | - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); | ||
| 64 | -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); | 54 | + /** |
| 55 | + * 消息序列化器 | ||
| 56 | + */ | ||
| 57 | + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); | ||
| 65 | 58 | ||
| 66 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== | 59 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== |
| 67 | 60 | ||
| @@ -81,30 +74,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -81,30 +74,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 81 | */ | 74 | */ |
| 82 | @Test | 75 | @Test |
| 83 | public void testAuth() throws Exception { | 76 | public void testAuth() throws Exception { |
| 84 | - // 1.1 构建认证消息 | 77 | + // 1. 构建认证消息 |
| 85 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 78 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 86 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | 79 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() |
| 87 | .setClientId(authInfo.getClientId()) | 80 | .setClientId(authInfo.getClientId()) |
| 88 | .setUsername(authInfo.getUsername()) | 81 | .setUsername(authInfo.getUsername()) |
| 89 | .setPassword(authInfo.getPassword()); | 82 | .setPassword(authInfo.getPassword()); |
| 90 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 91 | - // 1.2 编码 | ||
| 92 | - byte[] payload = CODEC.encode(request); | ||
| 93 | - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | 83 | + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); |
| 94 | 84 | ||
| 95 | - // 2.1 发送请求 | ||
| 96 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 97 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 98 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 99 | - // 2.2 解码响应 | ||
| 100 | - if (responseBytes != null) { | ||
| 101 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 102 | - log.info("[testAuth][响应消息: {}]", response); | ||
| 103 | - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | ||
| 104 | - } else { | ||
| 105 | - log.warn("[testAuth][未收到响应]"); | ||
| 106 | - } | ||
| 107 | - } | 85 | + // 2. 发送并接收响应 |
| 86 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 87 | + log.info("[testAuth][响应消息: {}]", response); | ||
| 88 | + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | ||
| 108 | } | 89 | } |
| 109 | 90 | ||
| 110 | // ===================== 动态注册测试 ===================== | 91 | // ===================== 动态注册测试 ===================== |
| @@ -118,30 +99,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -118,30 +99,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 118 | */ | 99 | */ |
| 119 | @Test | 100 | @Test |
| 120 | public void testDeviceRegister() throws Exception { | 101 | public void testDeviceRegister() throws Exception { |
| 121 | - // 1.1 构建注册消息 | ||
| 122 | - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 123 | - registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 124 | - registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis()); | ||
| 125 | - registerReqDTO.setProductSecret("test-product-secret"); | ||
| 126 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | ||
| 127 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | ||
| 128 | - // 1.2 编码 | ||
| 129 | - byte[] payload = CODEC.encode(request); | ||
| 130 | - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | ||
| 131 | - | ||
| 132 | - // 2.1 发送请求 | ||
| 133 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 134 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 135 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 136 | - // 2.2 解码响应 | ||
| 137 | - if (responseBytes != null) { | ||
| 138 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 139 | - log.info("[testDeviceRegister][响应消息: {}]", response); | ||
| 140 | - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 141 | - } else { | ||
| 142 | - log.warn("[testDeviceRegister][未收到响应]"); | ||
| 143 | - } | ||
| 144 | - } | 102 | + // 1. 构建注册消息 |
| 103 | + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() | ||
| 104 | + .setProductKey(PRODUCT_KEY) | ||
| 105 | + .setDeviceName("test-udp-" + System.currentTimeMillis()) | ||
| 106 | + .setProductSecret("test-product-secret"); | ||
| 107 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 108 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); | ||
| 109 | + | ||
| 110 | + // 2. 发送并接收响应 | ||
| 111 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 112 | + log.info("[testDeviceRegister][响应消息: {}]", response); | ||
| 113 | + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 145 | } | 114 | } |
| 146 | 115 | ||
| 147 | // ===================== 直连设备属性上报测试 ===================== | 116 | // ===================== 直连设备属性上报测试 ===================== |
| @@ -151,31 +120,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -151,31 +120,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 151 | */ | 120 | */ |
| 152 | @Test | 121 | @Test |
| 153 | public void testPropertyPost() throws Exception { | 122 | public void testPropertyPost() throws Exception { |
| 154 | - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) | ||
| 155 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 156 | - IdUtil.fastSimpleUUID(), | 123 | + // 1. 构建属性上报消息(UDP 协议:token 放在 params 中) |
| 124 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 157 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | 125 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), |
| 158 | withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | 126 | withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() |
| 159 | .put("width", 1) | 127 | .put("width", 1) |
| 160 | .put("height", "2") | 128 | .put("height", "2") |
| 161 | - .build())), | ||
| 162 | - null, null, null); | ||
| 163 | - // 1.2 编码 | ||
| 164 | - byte[] payload = CODEC.encode(request); | ||
| 165 | - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | 129 | + .build()))); |
| 166 | 130 | ||
| 167 | - // 2.1 发送请求 | ||
| 168 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 169 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 170 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 171 | - // 2.2 解码响应 | ||
| 172 | - if (responseBytes != null) { | ||
| 173 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 174 | - log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 175 | - } else { | ||
| 176 | - log.warn("[testPropertyPost][未收到响应]"); | ||
| 177 | - } | ||
| 178 | - } | 131 | + // 2. 发送并接收响应 |
| 132 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 133 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 179 | } | 134 | } |
| 180 | 135 | ||
| 181 | // ===================== 直连设备事件上报测试 ===================== | 136 | // ===================== 直连设备事件上报测试 ===================== |
| @@ -185,31 +140,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -185,31 +140,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 185 | */ | 140 | */ |
| 186 | @Test | 141 | @Test |
| 187 | public void testEventPost() throws Exception { | 142 | public void testEventPost() throws Exception { |
| 188 | - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) | ||
| 189 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 190 | - IdUtil.fastSimpleUUID(), | 143 | + // 1. 构建事件上报消息 |
| 144 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 191 | IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | 145 | IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), |
| 192 | withToken(IotDeviceEventPostReqDTO.of( | 146 | withToken(IotDeviceEventPostReqDTO.of( |
| 193 | "eat", | 147 | "eat", |
| 194 | MapUtil.<String, Object>builder().put("rice", 3).build(), | 148 | MapUtil.<String, Object>builder().put("rice", 3).build(), |
| 195 | - System.currentTimeMillis())), | ||
| 196 | - null, null, null); | ||
| 197 | - // 1.2 编码 | ||
| 198 | - byte[] payload = CODEC.encode(request); | ||
| 199 | - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | 149 | + System.currentTimeMillis()))); |
| 200 | 150 | ||
| 201 | - // 2.1 发送请求 | ||
| 202 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 203 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 204 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 205 | - // 2.2 解码响应 | ||
| 206 | - if (responseBytes != null) { | ||
| 207 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 208 | - log.info("[testEventPost][响应消息: {}]", response); | ||
| 209 | - } else { | ||
| 210 | - log.warn("[testEventPost][未收到响应]"); | ||
| 211 | - } | ||
| 212 | - } | 151 | + // 2. 发送并接收响应 |
| 152 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 153 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 213 | } | 154 | } |
| 214 | 155 | ||
| 215 | // ===================== 辅助方法 ===================== | 156 | // ===================== 辅助方法 ===================== |
| @@ -232,30 +173,36 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -232,30 +173,36 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 232 | } | 173 | } |
| 233 | 174 | ||
| 234 | /** | 175 | /** |
| 235 | - * 发送 UDP 请求并接收响应 | 176 | + * 发送 UDP 消息并接收响应 |
| 236 | * | 177 | * |
| 237 | - * @param socket UDP Socket | ||
| 238 | - * @param payload 请求数据 | ||
| 239 | - * @return 响应数据 | 178 | + * @param request 请求消息 |
| 179 | + * @return 响应消息 | ||
| 240 | */ | 180 | */ |
| 241 | - public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception { | ||
| 242 | - InetAddress address = InetAddress.getByName(SERVER_HOST); | ||
| 243 | - | ||
| 244 | - // 发送请求 | ||
| 245 | - DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); | ||
| 246 | - socket.send(sendPacket); | 181 | + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { |
| 182 | + // 1. 序列化请求 | ||
| 183 | + byte[] payload = SERIALIZER.serialize(request); | ||
| 184 | + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); | ||
| 247 | 185 | ||
| 248 | - // 接收响应 | ||
| 249 | - byte[] receiveData = new byte[4096]; | ||
| 250 | - DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); | ||
| 251 | - try { | ||
| 252 | - socket.receive(receivePacket); | ||
| 253 | - byte[] response = new byte[receivePacket.getLength()]; | ||
| 254 | - System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength()); | ||
| 255 | - return response; | ||
| 256 | - } catch (java.net.SocketTimeoutException e) { | ||
| 257 | - log.warn("[sendAndReceive][接收响应超时]"); | ||
| 258 | - return null; | 186 | + // 2. 发送请求 |
| 187 | + try (DatagramSocket socket = new DatagramSocket()) { | ||
| 188 | + socket.setSoTimeout(TIMEOUT_MS); | ||
| 189 | + InetAddress address = InetAddress.getByName(SERVER_HOST); | ||
| 190 | + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); | ||
| 191 | + socket.send(sendPacket); | ||
| 192 | + | ||
| 193 | + // 3. 接收响应 | ||
| 194 | + byte[] receiveData = new byte[4096]; | ||
| 195 | + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); | ||
| 196 | + try { | ||
| 197 | + socket.receive(receivePacket); | ||
| 198 | + byte[] responseBytes = new byte[receivePacket.getLength()]; | ||
| 199 | + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); | ||
| 200 | + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", responseBytes.length); | ||
| 201 | + return SERIALIZER.deserialize(responseBytes); | ||
| 202 | + } catch (java.net.SocketTimeoutException e) { | ||
| 203 | + log.warn("[sendAndReceive][接收响应超时]"); | ||
| 204 | + return null; | ||
| 205 | + } | ||
| 259 | } | 206 | } |
| 260 | } | 207 | } |
| 261 | 208 |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java
| @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | ||
| 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; |
| @@ -13,36 +12,27 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; | @@ -13,36 +12,27 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; | ||
| 13 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; | 12 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 14 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; | 13 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; |
| 15 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 14 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 17 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | ||
| 18 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | 15 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | ||
| 19 | import lombok.extern.slf4j.Slf4j; | 17 | import lombok.extern.slf4j.Slf4j; |
| 20 | import org.junit.jupiter.api.Disabled; | 18 | import org.junit.jupiter.api.Disabled; |
| 21 | import org.junit.jupiter.api.Test; | 19 | import org.junit.jupiter.api.Test; |
| 22 | 20 | ||
| 21 | +import java.net.DatagramPacket; | ||
| 23 | import java.net.DatagramSocket; | 22 | import java.net.DatagramSocket; |
| 23 | +import java.net.InetAddress; | ||
| 24 | import java.util.Collections; | 24 | import java.util.Collections; |
| 25 | import java.util.HashMap; | 25 | import java.util.HashMap; |
| 26 | -import java.util.List; | ||
| 27 | import java.util.Map; | 26 | import java.util.Map; |
| 28 | 27 | ||
| 29 | -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; | ||
| 30 | - | ||
| 31 | /** | 28 | /** |
| 32 | * IoT 网关设备 UDP 协议集成测试(手动测试) | 29 | * IoT 网关设备 UDP 协议集成测试(手动测试) |
| 33 | * | 30 | * |
| 34 | * <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 | 31 | * <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 |
| 35 | * | 32 | * |
| 36 | - * <p>支持两种编解码格式: | ||
| 37 | - * <ul> | ||
| 38 | - * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li> | ||
| 39 | - * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li> | ||
| 40 | - * </ul> | ||
| 41 | - * | ||
| 42 | * <p>使用步骤: | 33 | * <p>使用步骤: |
| 43 | * <ol> | 34 | * <ol> |
| 44 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> | 35 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> |
| 45 | - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li> | ||
| 46 | * <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li> | 36 | * <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li> |
| 47 | * <li>运行以下测试方法: | 37 | * <li>运行以下测试方法: |
| 48 | * <ul> | 38 | * <ul> |
| @@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd | @@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd | ||
| 63 | @Disabled | 53 | @Disabled |
| 64 | public class IotGatewayDeviceUdpProtocolIntegrationTest { | 54 | public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 65 | 55 | ||
| 56 | + private static final String SERVER_HOST = "127.0.0.1"; | ||
| 57 | + private static final int SERVER_PORT = 8093; | ||
| 66 | private static final int TIMEOUT_MS = 5000; | 58 | private static final int TIMEOUT_MS = 5000; |
| 67 | 59 | ||
| 68 | - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== | 60 | + // ===================== 序列化器 ===================== |
| 69 | 61 | ||
| 70 | - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); | ||
| 71 | -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); | 62 | + /** |
| 63 | + * 消息序列化器 | ||
| 64 | + */ | ||
| 65 | + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); | ||
| 72 | 66 | ||
| 73 | // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== | 67 | // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== |
| 74 | 68 | ||
| @@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { | @@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { | ||
| 93 | */ | 87 | */ |
| 94 | @Test | 88 | @Test |
| 95 | public void testAuth() throws Exception { | 89 | public void testAuth() throws Exception { |
| 96 | - // 1.1 构建认证消息 | 90 | + // 1. 构建认证消息 |
| 97 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | 91 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 98 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | 92 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| 99 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | 93 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() |
| 100 | .setClientId(authInfo.getClientId()) | 94 | .setClientId(authInfo.getClientId()) |
| 101 | .setUsername(authInfo.getUsername()) | 95 | .setUsername(authInfo.getUsername()) |
| 102 | .setPassword(authInfo.getPassword()); | 96 | .setPassword(authInfo.getPassword()); |
| 103 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 104 | - // 1.2 编码 | ||
| 105 | - byte[] payload = CODEC.encode(request); | ||
| 106 | - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | 97 | + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); |
| 107 | 98 | ||
| 108 | - // 2.1 发送请求 | ||
| 109 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 110 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 111 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 112 | - // 2.2 解码响应 | ||
| 113 | - if (responseBytes != null) { | ||
| 114 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 115 | - log.info("[testAuth][响应消息: {}]", response); | ||
| 116 | - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); | ||
| 117 | - } else { | ||
| 118 | - log.warn("[testAuth][未收到响应]"); | ||
| 119 | - } | ||
| 120 | - } | 99 | + // 2. 发送并接收响应 |
| 100 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 101 | + log.info("[testAuth][响应消息: {}]", response); | ||
| 102 | + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); | ||
| 121 | } | 103 | } |
| 122 | 104 | ||
| 123 | // ===================== 拓扑管理测试 ===================== | 105 | // ===================== 拓扑管理测试 ===================== |
| 124 | 106 | ||
| 125 | /** | 107 | /** |
| 126 | * 添加子设备拓扑关系测试 | 108 | * 添加子设备拓扑关系测试 |
| 127 | - * <p> | ||
| 128 | - * 网关设备向平台上报需要绑定的子设备信息 | ||
| 129 | */ | 109 | */ |
| 130 | @Test | 110 | @Test |
| 131 | public void testTopoAdd() throws Exception { | 111 | public void testTopoAdd() throws Exception { |
| 132 | - // 1.1 构建子设备认证信息 | 112 | + // 1. 构建子设备认证信息 |
| 133 | IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | 113 | IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( |
| 134 | SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | 114 | SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); |
| 135 | IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | 115 | IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() |
| 136 | .setClientId(subAuthInfo.getClientId()) | 116 | .setClientId(subAuthInfo.getClientId()) |
| 137 | .setUsername(subAuthInfo.getUsername()) | 117 | .setUsername(subAuthInfo.getUsername()) |
| 138 | .setPassword(subAuthInfo.getPassword()); | 118 | .setPassword(subAuthInfo.getPassword()); |
| 139 | - // 1.2 构建请求参数 | ||
| 140 | IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); | 119 | IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); |
| 141 | params.setSubDevices(Collections.singletonList(subDeviceAuth)); | 120 | params.setSubDevices(Collections.singletonList(subDeviceAuth)); |
| 142 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 143 | - IdUtil.fastSimpleUUID(), | ||
| 144 | - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 145 | - withToken(params), | ||
| 146 | - null, null, null); | ||
| 147 | - // 1.3 编码 | ||
| 148 | - byte[] payload = CODEC.encode(request); | ||
| 149 | - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 150 | - | ||
| 151 | - // 2.1 发送请求 | ||
| 152 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 153 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 154 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 155 | - // 2.2 解码响应 | ||
| 156 | - if (responseBytes != null) { | ||
| 157 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 158 | - log.info("[testTopoAdd][响应消息: {}]", response); | ||
| 159 | - } else { | ||
| 160 | - log.warn("[testTopoAdd][未收到响应]"); | ||
| 161 | - } | ||
| 162 | - } | 121 | + IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 122 | + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), withToken(params)); | ||
| 123 | + | ||
| 124 | + // 2. 发送并接收响应 | ||
| 125 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 126 | + log.info("[testTopoAdd][响应消息: {}]", response); | ||
| 163 | } | 127 | } |
| 164 | 128 | ||
| 165 | /** | 129 | /** |
| 166 | * 删除子设备拓扑关系测试 | 130 | * 删除子设备拓扑关系测试 |
| 167 | - * <p> | ||
| 168 | - * 网关设备向平台上报需要解绑的子设备信息 | ||
| 169 | */ | 131 | */ |
| 170 | @Test | 132 | @Test |
| 171 | public void testTopoDelete() throws Exception { | 133 | public void testTopoDelete() throws Exception { |
| 172 | - // 1.1 构建请求参数 | 134 | + // 1. 构建请求参数 |
| 173 | IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); | 135 | IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); |
| 174 | params.setSubDevices(Collections.singletonList( | 136 | params.setSubDevices(Collections.singletonList( |
| 175 | new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | 137 | new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); |
| 176 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 177 | - IdUtil.fastSimpleUUID(), | ||
| 178 | - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 179 | - withToken(params), | ||
| 180 | - null, null, null); | ||
| 181 | - // 1.2 编码 | ||
| 182 | - byte[] payload = CODEC.encode(request); | ||
| 183 | - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 184 | - | ||
| 185 | - // 2.1 发送请求 | ||
| 186 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 187 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 188 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 189 | - // 2.2 解码响应 | ||
| 190 | - if (responseBytes != null) { | ||
| 191 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 192 | - log.info("[testTopoDelete][响应消息: {}]", response); | ||
| 193 | - } else { | ||
| 194 | - log.warn("[testTopoDelete][未收到响应]"); | ||
| 195 | - } | ||
| 196 | - } | 138 | + IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 139 | + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), withToken(params)); | ||
| 140 | + | ||
| 141 | + // 2. 发送并接收响应 | ||
| 142 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 143 | + log.info("[testTopoDelete][响应消息: {}]", response); | ||
| 197 | } | 144 | } |
| 198 | 145 | ||
| 199 | /** | 146 | /** |
| 200 | * 获取子设备拓扑关系测试 | 147 | * 获取子设备拓扑关系测试 |
| 201 | - * <p> | ||
| 202 | - * 网关设备向平台查询已绑定的子设备列表 | ||
| 203 | */ | 148 | */ |
| 204 | @Test | 149 | @Test |
| 205 | public void testTopoGet() throws Exception { | 150 | public void testTopoGet() throws Exception { |
| 206 | - // 1.1 构建请求参数(目前为空,预留扩展) | 151 | + // 1. 构建请求参数 |
| 207 | IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | 152 | IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); |
| 208 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 209 | - IdUtil.fastSimpleUUID(), | ||
| 210 | - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 211 | - withToken(params), | ||
| 212 | - null, null, null); | ||
| 213 | - // 1.2 编码 | ||
| 214 | - byte[] payload = CODEC.encode(request); | ||
| 215 | - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 216 | - | ||
| 217 | - // 2.1 发送请求 | ||
| 218 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 219 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 220 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 221 | - // 2.2 解码响应 | ||
| 222 | - if (responseBytes != null) { | ||
| 223 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 224 | - log.info("[testTopoGet][响应消息: {}]", response); | ||
| 225 | - } else { | ||
| 226 | - log.warn("[testTopoGet][未收到响应]"); | ||
| 227 | - } | ||
| 228 | - } | 153 | + IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 154 | + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), withToken(params)); | ||
| 155 | + | ||
| 156 | + // 2. 发送并接收响应 | ||
| 157 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 158 | + log.info("[testTopoGet][响应消息: {}]", response); | ||
| 229 | } | 159 | } |
| 230 | 160 | ||
| 231 | // ===================== 子设备注册测试 ===================== | 161 | // ===================== 子设备注册测试 ===================== |
| 232 | 162 | ||
| 233 | /** | 163 | /** |
| 234 | * 子设备动态注册测试 | 164 | * 子设备动态注册测试 |
| 235 | - * <p> | ||
| 236 | - * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret | ||
| 237 | - * <p> | ||
| 238 | - * 注意:此接口需要网关 Token 认证 | ||
| 239 | */ | 165 | */ |
| 240 | @Test | 166 | @Test |
| 241 | public void testSubDeviceRegister() throws Exception { | 167 | public void testSubDeviceRegister() throws Exception { |
| 242 | - // 1.1 构建请求参数 | 168 | + // 1. 构建请求参数 |
| 243 | IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); | 169 | IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); |
| 244 | subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); | 170 | subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); |
| 245 | subDevice.setDeviceName("mougezishebei"); | 171 | subDevice.setDeviceName("mougezishebei"); |
| 246 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 247 | - IdUtil.fastSimpleUUID(), | 172 | + IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 248 | IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | 173 | IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), |
| 249 | - withToken(Collections.singletonList(subDevice)), | ||
| 250 | - null, null, null); | ||
| 251 | - // 1.2 编码 | ||
| 252 | - byte[] payload = CODEC.encode(request); | ||
| 253 | - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); | 174 | + withToken(Collections.singletonList(subDevice))); |
| 254 | 175 | ||
| 255 | - // 2.1 发送请求 | ||
| 256 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 257 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 258 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 259 | - // 2.2 解码响应 | ||
| 260 | - if (responseBytes != null) { | ||
| 261 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 262 | - log.info("[testSubDeviceRegister][响应消息: {}]", response); | ||
| 263 | - } else { | ||
| 264 | - log.warn("[testSubDeviceRegister][未收到响应]"); | ||
| 265 | - } | ||
| 266 | - } | 176 | + // 2. 发送并接收响应 |
| 177 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 178 | + log.info("[testSubDeviceRegister][响应消息: {}]", response); | ||
| 267 | } | 179 | } |
| 268 | 180 | ||
| 269 | // ===================== 批量上报测试 ===================== | 181 | // ===================== 批量上报测试 ===================== |
| 270 | 182 | ||
| 271 | /** | 183 | /** |
| 272 | * 批量上报属性测试(网关 + 子设备) | 184 | * 批量上报属性测试(网关 + 子设备) |
| 273 | - * <p> | ||
| 274 | - * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 | ||
| 275 | */ | 185 | */ |
| 276 | @Test | 186 | @Test |
| 277 | public void testPropertyPackPost() throws Exception { | 187 | public void testPropertyPackPost() throws Exception { |
| @@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { | @@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { | ||
| 307 | params.setProperties(gatewayProperties); | 217 | params.setProperties(gatewayProperties); |
| 308 | params.setEvents(gatewayEvents); | 218 | params.setEvents(gatewayEvents); |
| 309 | params.setSubDevices(ListUtil.of(subDeviceData)); | 219 | params.setSubDevices(ListUtil.of(subDeviceData)); |
| 310 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 311 | - IdUtil.fastSimpleUUID(), | ||
| 312 | - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 313 | - withToken(params), | ||
| 314 | - null, null, null); | ||
| 315 | - // 1.7 编码 | ||
| 316 | - byte[] payload = CODEC.encode(request); | ||
| 317 | - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 318 | - | ||
| 319 | - // 2.1 发送请求 | ||
| 320 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 321 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 322 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 323 | - // 2.2 解码响应 | ||
| 324 | - if (responseBytes != null) { | ||
| 325 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 326 | - log.info("[testPropertyPackPost][响应消息: {}]", response); | ||
| 327 | - } else { | ||
| 328 | - log.warn("[testPropertyPackPost][未收到响应]"); | ||
| 329 | - } | ||
| 330 | - } | 220 | + IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 221 | + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), withToken(params)); | ||
| 222 | + | ||
| 223 | + // 2. 发送并接收响应 | ||
| 224 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 225 | + log.info("[testPropertyPackPost][响应消息: {}]", response); | ||
| 331 | } | 226 | } |
| 332 | 227 | ||
| 333 | // ===================== 辅助方法 ===================== | 228 | // ===================== 辅助方法 ===================== |
| 334 | 229 | ||
| 335 | /** | 230 | /** |
| 336 | * 构建带 token 的 params | 231 | * 构建带 token 的 params |
| 337 | - * <p> | ||
| 338 | - * 返回格式:{token: "xxx", body: params} | ||
| 339 | - * - token:JWT 令牌 | ||
| 340 | - * - body:实际请求内容(可以是 Map、List 或其他类型) | ||
| 341 | - * | ||
| 342 | - * @param params 原始参数(Map、List 或对象) | ||
| 343 | - * @return 包含 token 和 body 的 Map | ||
| 344 | */ | 232 | */ |
| 345 | private Map<String, Object> withToken(Object params) { | 233 | private Map<String, Object> withToken(Object params) { |
| 346 | Map<String, Object> result = new HashMap<>(); | 234 | Map<String, Object> result = new HashMap<>(); |
| @@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { | @@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { | ||
| 349 | return result; | 237 | return result; |
| 350 | } | 238 | } |
| 351 | 239 | ||
| 240 | + /** | ||
| 241 | + * 发送 UDP 消息并接收响应 | ||
| 242 | + */ | ||
| 243 | + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { | ||
| 244 | + byte[] payload = SERIALIZER.serialize(request); | ||
| 245 | + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); | ||
| 246 | + | ||
| 247 | + try (DatagramSocket socket = new DatagramSocket()) { | ||
| 248 | + socket.setSoTimeout(TIMEOUT_MS); | ||
| 249 | + InetAddress address = InetAddress.getByName(SERVER_HOST); | ||
| 250 | + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); | ||
| 251 | + socket.send(sendPacket); | ||
| 252 | + | ||
| 253 | + byte[] receiveData = new byte[4096]; | ||
| 254 | + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); | ||
| 255 | + try { | ||
| 256 | + socket.receive(receivePacket); | ||
| 257 | + byte[] responseBytes = new byte[receivePacket.getLength()]; | ||
| 258 | + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); | ||
| 259 | + return SERIALIZER.deserialize(responseBytes); | ||
| 260 | + } catch (java.net.SocketTimeoutException e) { | ||
| 261 | + log.warn("[sendAndReceive][接收响应超时]"); | ||
| 262 | + return null; | ||
| 263 | + } | ||
| 264 | + } | ||
| 265 | + } | ||
| 266 | + | ||
| 352 | } | 267 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp; |
| 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; |
| 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 7 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 8 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 9 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 12 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | ||
| 13 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | 10 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | ||
| 14 | import lombok.extern.slf4j.Slf4j; | 12 | import lombok.extern.slf4j.Slf4j; |
| 15 | import org.junit.jupiter.api.Disabled; | 13 | import org.junit.jupiter.api.Disabled; |
| 16 | import org.junit.jupiter.api.Test; | 14 | import org.junit.jupiter.api.Test; |
| 17 | 15 | ||
| 16 | +import java.net.DatagramPacket; | ||
| 18 | import java.net.DatagramSocket; | 17 | import java.net.DatagramSocket; |
| 18 | +import java.net.InetAddress; | ||
| 19 | import java.util.HashMap; | 19 | import java.util.HashMap; |
| 20 | import java.util.Map; | 20 | import java.util.Map; |
| 21 | 21 | ||
| 22 | -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; | ||
| 23 | - | ||
| 24 | /** | 22 | /** |
| 25 | * IoT 网关子设备 UDP 协议集成测试(手动测试) | 23 | * IoT 网关子设备 UDP 协议集成测试(手动测试) |
| 26 | * | 24 | * |
| @@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd | @@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd | ||
| 29 | * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b> | 27 | * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b> |
| 30 | * <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。 | 28 | * <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。 |
| 31 | * | 29 | * |
| 32 | - * <p>支持两种编解码格式: | ||
| 33 | - * <ul> | ||
| 34 | - * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li> | ||
| 35 | - * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li> | ||
| 36 | - * </ul> | ||
| 37 | - * | ||
| 38 | * <p>使用步骤: | 30 | * <p>使用步骤: |
| 39 | * <ol> | 31 | * <ol> |
| 40 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> | 32 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> |
| 41 | * <li>确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li> | 33 | * <li>确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li> |
| 42 | - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li> | ||
| 43 | * <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li> | 34 | * <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li> |
| 44 | * <li>运行以下测试方法: | 35 | * <li>运行以下测试方法: |
| 45 | * <ul> | 36 | * <ul> |
| @@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd | @@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd | ||
| 57 | @Disabled | 48 | @Disabled |
| 58 | public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | 49 | public class IotGatewaySubDeviceUdpProtocolIntegrationTest { |
| 59 | 50 | ||
| 51 | + private static final String SERVER_HOST = "127.0.0.1"; | ||
| 52 | + private static final int SERVER_PORT = 8093; | ||
| 60 | private static final int TIMEOUT_MS = 5000; | 53 | private static final int TIMEOUT_MS = 5000; |
| 61 | 54 | ||
| 62 | - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== | 55 | + // ===================== 序列化器 ===================== |
| 63 | 56 | ||
| 64 | - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); | ||
| 65 | -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); | 57 | + /** |
| 58 | + * 消息序列化器 | ||
| 59 | + */ | ||
| 60 | + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); | ||
| 66 | 61 | ||
| 67 | // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== | 62 | // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== |
| 68 | 63 | ||
| @@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | @@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | ||
| 82 | */ | 77 | */ |
| 83 | @Test | 78 | @Test |
| 84 | public void testAuth() throws Exception { | 79 | public void testAuth() throws Exception { |
| 85 | - // 1.1 构建认证消息 | 80 | + // 1. 构建认证消息 |
| 86 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 81 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 87 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | 82 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() |
| 88 | .setClientId(authInfo.getClientId()) | 83 | .setClientId(authInfo.getClientId()) |
| 89 | .setUsername(authInfo.getUsername()) | 84 | .setUsername(authInfo.getUsername()) |
| 90 | .setPassword(authInfo.getPassword()); | 85 | .setPassword(authInfo.getPassword()); |
| 91 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 92 | - // 1.2 编码 | ||
| 93 | - byte[] payload = CODEC.encode(request); | ||
| 94 | - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | 86 | + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); |
| 95 | 87 | ||
| 96 | - // 2.1 发送请求 | ||
| 97 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 98 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 99 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 100 | - // 2.2 解码响应 | ||
| 101 | - if (responseBytes != null) { | ||
| 102 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 103 | - log.info("[testAuth][响应消息: {}]", response); | ||
| 104 | - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | ||
| 105 | - } else { | ||
| 106 | - log.warn("[testAuth][未收到响应]"); | ||
| 107 | - } | ||
| 108 | - } | 88 | + // 2. 发送并接收响应 |
| 89 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 90 | + log.info("[testAuth][响应消息: {}]", response); | ||
| 91 | + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | ||
| 109 | } | 92 | } |
| 110 | 93 | ||
| 111 | // ===================== 子设备属性上报测试 ===================== | 94 | // ===================== 子设备属性上报测试 ===================== |
| @@ -115,33 +98,19 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | @@ -115,33 +98,19 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | ||
| 115 | */ | 98 | */ |
| 116 | @Test | 99 | @Test |
| 117 | public void testPropertyPost() throws Exception { | 100 | public void testPropertyPost() throws Exception { |
| 118 | - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) | ||
| 119 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 120 | - IdUtil.fastSimpleUUID(), | 101 | + // 1. 构建属性上报消息(UDP 协议:token 放在 params 中) |
| 102 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 121 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | 103 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), |
| 122 | withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | 104 | withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() |
| 123 | .put("power", 100) | 105 | .put("power", 100) |
| 124 | .put("status", "online") | 106 | .put("status", "online") |
| 125 | .put("temperature", 36.5) | 107 | .put("temperature", 36.5) |
| 126 | - .build())), | ||
| 127 | - null, null, null); | ||
| 128 | - // 1.2 编码 | ||
| 129 | - byte[] payload = CODEC.encode(request); | 108 | + .build()))); |
| 130 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); | 109 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); |
| 131 | - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 132 | 110 | ||
| 133 | - // 2.1 发送请求 | ||
| 134 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 135 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 136 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 137 | - // 2.2 解码响应 | ||
| 138 | - if (responseBytes != null) { | ||
| 139 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 140 | - log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 141 | - } else { | ||
| 142 | - log.warn("[testPropertyPost][未收到响应]"); | ||
| 143 | - } | ||
| 144 | - } | 111 | + // 2. 发送并接收响应 |
| 112 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 113 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 145 | } | 114 | } |
| 146 | 115 | ||
| 147 | // ===================== 子设备事件上报测试 ===================== | 116 | // ===================== 子设备事件上报测试 ===================== |
| @@ -151,9 +120,8 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | @@ -151,9 +120,8 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | ||
| 151 | */ | 120 | */ |
| 152 | @Test | 121 | @Test |
| 153 | public void testEventPost() throws Exception { | 122 | public void testEventPost() throws Exception { |
| 154 | - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) | ||
| 155 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 156 | - IdUtil.fastSimpleUUID(), | 123 | + // 1. 构建事件上报消息(UDP 协议:token 放在 params 中) |
| 124 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 157 | IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | 125 | IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), |
| 158 | withToken(IotDeviceEventPostReqDTO.of( | 126 | withToken(IotDeviceEventPostReqDTO.of( |
| 159 | "alarm", | 127 | "alarm", |
| @@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | @@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | ||
| 163 | .put("threshold", 40) | 131 | .put("threshold", 40) |
| 164 | .put("current", 42) | 132 | .put("current", 42) |
| 165 | .build(), | 133 | .build(), |
| 166 | - System.currentTimeMillis())), | ||
| 167 | - null, null, null); | ||
| 168 | - // 1.2 编码 | ||
| 169 | - byte[] payload = CODEC.encode(request); | 134 | + System.currentTimeMillis()))); |
| 170 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); | 135 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); |
| 171 | - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 172 | 136 | ||
| 173 | - // 2.1 发送请求 | ||
| 174 | - try (DatagramSocket socket = new DatagramSocket()) { | ||
| 175 | - socket.setSoTimeout(TIMEOUT_MS); | ||
| 176 | - byte[] responseBytes = sendAndReceive(socket, payload); | ||
| 177 | - // 2.2 解码响应 | ||
| 178 | - if (responseBytes != null) { | ||
| 179 | - IotDeviceMessage response = CODEC.decode(responseBytes); | ||
| 180 | - log.info("[testEventPost][响应消息: {}]", response); | ||
| 181 | - } else { | ||
| 182 | - log.warn("[testEventPost][未收到响应]"); | ||
| 183 | - } | ||
| 184 | - } | 137 | + // 2. 发送并接收响应 |
| 138 | + IotDeviceMessage response = sendAndReceive(request); | ||
| 139 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 185 | } | 140 | } |
| 186 | 141 | ||
| 187 | // ===================== 辅助方法 ===================== | 142 | // ===================== 辅助方法 ===================== |
| 188 | 143 | ||
| 189 | /** | 144 | /** |
| 190 | * 构建带 token 的 params | 145 | * 构建带 token 的 params |
| 191 | - * <p> | ||
| 192 | - * 返回格式:{token: "xxx", body: params} | ||
| 193 | - * - token:JWT 令牌 | ||
| 194 | - * - body:实际请求内容(可以是 Map、List 或其他类型) | ||
| 195 | - * | ||
| 196 | - * @param params 原始参数(Map、List 或对象) | ||
| 197 | - * @return 包含 token 和 body 的 Map | ||
| 198 | */ | 146 | */ |
| 199 | private Map<String, Object> withToken(Object params) { | 147 | private Map<String, Object> withToken(Object params) { |
| 200 | Map<String, Object> result = new HashMap<>(); | 148 | Map<String, Object> result = new HashMap<>(); |
| @@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | @@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { | ||
| 203 | return result; | 151 | return result; |
| 204 | } | 152 | } |
| 205 | 153 | ||
| 154 | + /** | ||
| 155 | + * 发送 UDP 消息并接收响应 | ||
| 156 | + */ | ||
| 157 | + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { | ||
| 158 | + byte[] payload = SERIALIZER.serialize(request); | ||
| 159 | + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); | ||
| 160 | + | ||
| 161 | + try (DatagramSocket socket = new DatagramSocket()) { | ||
| 162 | + socket.setSoTimeout(TIMEOUT_MS); | ||
| 163 | + InetAddress address = InetAddress.getByName(SERVER_HOST); | ||
| 164 | + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); | ||
| 165 | + socket.send(sendPacket); | ||
| 166 | + | ||
| 167 | + byte[] receiveData = new byte[4096]; | ||
| 168 | + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); | ||
| 169 | + try { | ||
| 170 | + socket.receive(receivePacket); | ||
| 171 | + byte[] responseBytes = new byte[receivePacket.getLength()]; | ||
| 172 | + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); | ||
| 173 | + return SERIALIZER.deserialize(responseBytes); | ||
| 174 | + } catch (java.net.SocketTimeoutException e) { | ||
| 175 | + log.warn("[sendAndReceive][接收响应超时]"); | ||
| 176 | + return null; | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + | ||
| 206 | } | 181 | } |