Commit cb301eb788704a62a21b490f265c97bf07f7d1a4

Authored by YunaiV
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
1 -/**  
2 - * TODO @芋艿:实现一个 alink 的 xml 版本  
3 - */  
4 -package cn.iocoder.yudao.module.iot.gateway.codec.simple;  
5 \ No newline at end of file 0 \ No newline at end of file
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&lt;NetSocket&gt; { @@ -49,12 +49,10 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
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&lt;NetSocket&gt; { @@ -141,7 +139,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
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&lt;NetSocket&gt; { @@ -158,6 +156,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
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&lt;NetSocket&gt; { @@ -180,7 +179,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
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&lt;NetSocket&gt; { @@ -196,6 +195,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
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&lt;NetSocket&gt; { @@ -246,14 +246,12 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
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 }