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 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 11 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
12 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
13 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 14 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
19 15 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
20 16 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
... ... @@ -42,10 +38,8 @@ public class IotGatewayConfiguration {
42 38 }
43 39  
44 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 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 115 * IoT 网关 CoAP 协议配置类
161 116 */
162 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 3 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
6 7 import io.vertx.core.net.KeyCertOptions;
7 8 import io.vertx.core.net.TrustOptions;
8 9 import jakarta.validation.Valid;
... ... @@ -35,7 +36,7 @@ public class IotGatewayProperties {
35 36 private ProtocolProperties protocol;
36 37  
37 38 /**
38   - * 协议实例列表(新版)
  39 + * 协议实例列表
39 40 */
40 41 private List<ProtocolInstanceProperties> protocols;
41 42  
... ... @@ -90,11 +91,6 @@ public class IotGatewayProperties {
90 91 private MqttProperties mqtt;
91 92  
92 93 /**
93   - * UDP 组件配置
94   - */
95   - private UdpProperties udp;
96   -
97   - /**
98 94 * CoAP 组件配置
99 95 */
100 96 private CoapProperties coap;
... ... @@ -349,44 +345,6 @@ public class IotGatewayProperties {
349 345 }
350 346  
351 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 348 public static class CoapProperties {
391 349  
392 350 /**
... ... @@ -525,6 +483,12 @@ public class IotGatewayProperties {
525 483 @Valid
526 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 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.BooleanUtil;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
7 6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
8 7 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
9 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 11 import lombok.extern.slf4j.Slf4j;
12 12 import org.springframework.context.SmartLifecycle;
13 13  
... ... @@ -24,20 +24,16 @@ public class IotProtocolManager implements SmartLifecycle {
24 24  
25 25 private final IotGatewayProperties gatewayProperties;
26 26  
27   - private final IotMessageSerializerManager serializerManager;
28   -
29   - private final IotMessageBus messageBus;
30   -
  27 + /**
  28 + * 协议实例列表
  29 + */
31 30 private final List<IotProtocol> protocols = new ArrayList<>();
32 31  
  32 + @Getter
33 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 36 this.gatewayProperties = gatewayProperties;
39   - this.serializerManager = serializerManager;
40   - this.messageBus = messageBus;
41 37 }
42 38  
43 39 @Override
... ... @@ -84,11 +80,6 @@ public class IotProtocolManager implements SmartLifecycle {
84 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 98 return createHttpProtocol(config);
108 99 case TCP:
109 100 return createTcpProtocol(config);
110   - // TODO 后续添加其他协议类型
  101 + case UDP:
  102 + return createUdpProtocol(config);
111 103 default:
112 104 throw new IllegalArgumentException(String.format(
113 105 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
... ... @@ -121,7 +113,7 @@ public class IotProtocolManager implements SmartLifecycle {
121 113 * @return HTTP 协议实例
122 114 */
123 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 123 * @return TCP 协议实例
132 124 */
133 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 1 package cn.iocoder.yudao.module.iot.gateway.protocol.http;
2 2  
  3 +import cn.hutool.extra.spring.SpringUtil;
3 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
5 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
... ... @@ -59,11 +60,12 @@ public class IotHttpProtocol implements IotProtocol {
59 60 */
60 61 private IotHttpDownstreamSubscriber downstreamSubscriber;
61 62  
62   - public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) {
  63 + public IotHttpProtocol(ProtocolInstanceProperties properties) {
63 64 this.properties = properties;
64 65 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
65 66  
66 67 // 初始化下行消息订阅者
  68 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
67 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 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
2 2  
  3 +import cn.hutool.extra.spring.SpringUtil;
3 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
5 6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
... ... @@ -73,31 +74,30 @@ public class IotTcpProtocol implements IotProtocol {
73 74 private final IotTcpFrameCodec frameCodec;
74 75  
75 76 /**
76   - * TCP 连接管理器(每个 Protocol 实例独立)
  77 + * TCP 连接管理器
77 78 */
78 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 85 this.properties = properties;
83 86 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
84 87  
85 88 // 初始化序列化器
86 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 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 94 this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec());
96 95  
97 96 // 初始化连接管理器
98 97 this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections());
99 98  
100 99 // 初始化下行消息订阅者
  100 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
101 101 IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer);
102 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 49 */
50 50 private final IotMessageSerializer serializer;
51 51 /**
52   - * TCP 连接管理器(每个 Protocol 实例独立)
  52 + * TCP 连接管理器
53 53 */
54 54 private final IotTcpConnectionManager connectionManager;
55 55  
56   - // ===================== Spring 依赖(构造时注入) =====================
57   -
58 56 private final IotDeviceMessageService deviceMessageService;
59 57 private final IotDeviceService deviceService;
60 58 private final IotDeviceCommonApi deviceApi;
... ... @@ -141,7 +139,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
141 139 String method = message != null ? message.getMethod() : null;
142 140 sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage());
143 141 } catch (Exception e) {
144   - // 其他异常,返回 500 并重新抛出让上层关闭连接
  142 + // 其他异常,返回 500,并重新抛出让上层关闭连接
145 143 log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e);
146 144 String requestId = message != null ? message.getRequestId() : null;
147 145 String method = message != null ? message.getMethod() : null;
... ... @@ -158,6 +156,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
158 156 * @param message 消息信息
159 157 * @param socket 网络连接
160 158 */
  159 + @SuppressWarnings("DuplicatedCode")
161 160 private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) {
162 161 // 1. 解析认证参数
163 162 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
... ... @@ -180,7 +179,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
180 179 Assert.notNull(device, "设备不存在");
181 180  
182 181 // 3.1 注册连接
183   - registerConnection(socket, device, clientId);
  182 + registerConnection(socket, device);
184 183 // 3.2 发送上线消息
185 184 sendOnlineMessage(device);
186 185 // 3.3 发送成功响应
... ... @@ -196,6 +195,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
196 195 * @param socket 网络连接
197 196 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
198 197 */
  198 + @SuppressWarnings("DuplicatedCode")
199 199 private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) {
200 200 // 1. 解析注册参数
201 201 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
... ... @@ -246,14 +246,12 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
246 246 *
247 247 * @param socket 网络连接
248 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 251 IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo()
253 252 .setDeviceId(device.getId())
254 253 .setProductKey(device.getProductKey())
255   - .setDeviceName(device.getDeviceName())
256   - .setClientId(clientId);
  254 + .setDeviceName(device.getDeviceName());
257 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 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 46 * @param socket TCP 连接
... ... @@ -155,11 +141,6 @@ public class IotTcpConnectionManager {
155 141 */
156 142 private String deviceName;
157 143  
158   - /**
159   - * 客户端 ID
160   - */
161   - private String clientId;
162   -
163 144 }
164 145  
165 146 }
166 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 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 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 7 import io.vertx.core.datagram.DatagramSocket;
  8 +import lombok.RequiredArgsConstructor;
8 9 import lombok.extern.slf4j.Slf4j;
9 10  
10 11 /**
... ... @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j;
13 14 * @author 芋道源码
14 15 */
15 16 @Slf4j
  17 +@RequiredArgsConstructor
16 18 public class IotUdpDownstreamHandler {
17 19  
18   - private final IotDeviceMessageService deviceMessageService;
  20 + private final IotUdpProtocol protocol;
19 21  
20 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 32 public void handle(IotDeviceMessage message) {
38 33 try {
39 34 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
40 35 message.getDeviceId(), message.getMethod(), message.getId());
41   -
42   - // 1. 获取会话信息(包含 codecType)
  36 + // 1. 检查设备会话
43 37 IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId());
44 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 41 return;
47 42 }
48   -
49   - // 2. 使用会话中的 codecType 编码消息,并发送到设备
50   - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType());
51 43 DatagramSocket socket = protocol.getUdpSocket();
52 44 if (socket == null) {
53 45 log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId());
54 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 59 } catch (Exception e) {
65 60 log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
66 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 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
5 6 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -16,7 +16,7 @@ public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber
16 16  
17 17 private final IotUdpDownstreamHandler downstreamHandler;
18 18  
19   - public IotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocol,
  19 + public IotUdpDownstreamSubscriber(IotProtocol protocol,
20 20 IotUdpDownstreamHandler downstreamHandler,
21 21 IotMessageBus messageBus) {
22 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 4 import io.vertx.core.datagram.DatagramSocket;
5 5 import lombok.Data;
6 6 import lombok.extern.slf4j.Slf4j;
7   -import org.springframework.stereotype.Component;
8 7  
9 8 import java.net.InetSocketAddress;
10 9 import java.time.LocalDateTime;
... ... @@ -17,89 +16,120 @@ import java.util.concurrent.ConcurrentHashMap;
17 16 /**
18 17 * IoT 网关 UDP 会话管理器
19 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 24 * @author 芋道源码
27 25 */
28 26 @Slf4j
29   -@Component
30 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 40 * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步)
44 41 */
  42 + // TODO @AI:1)这个变量是否必须?2)unregisterSession 这个方法是否必须?
45 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 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 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 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 146 log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId);
117 147 return false;
118 148 }
119   -
120 149 InetSocketAddress address = sessionInfo.getAddress();
121 150 try {
122 151 socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
123 152 if (result.succeeded()) {
124 153 log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
125 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 160 return true;
132 161 } catch (Exception e) {
... ... @@ -136,37 +165,31 @@ public class IotUdpSessionManager {
136 165 }
137 166  
138 167 /**
139   - * 定期清理不活跃的设备地址映射
  168 + * 定期清理不活跃的设备会话
140 169 *
141 170 * @param timeoutMs 超时时间(毫秒)
142 171 * @return 清理的设备 ID 列表(用于发送离线消息)
143 172 */
144   - public List<Long> cleanExpiredMappings(long timeoutMs) {
  173 + public List<Long> cleanExpiredSessions(long timeoutMs) {
145 174 List<Long> offlineDeviceIds = new ArrayList<>();
146 175 LocalDateTime now = LocalDateTime.now();
147 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 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 184 continue;
154 185 }
155 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 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 193 iterator.remove();
171 194 }
172 195 return offlineDeviceIds;
... ... @@ -183,20 +206,32 @@ public class IotUdpSessionManager {
183 206 }
184 207  
185 208 /**
186   - * 会话信息
  209 + * 会话信息(包含认证信息)
187 210 */
188 211 @Data
189 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 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 50 - id: http-json
51 51 type: http
52 52 port: 8092
53   - enabled: true
  53 + enabled: false
54 54 http:
55 55 ssl-enabled: false
56 56 # ====================================
... ... @@ -59,7 +59,7 @@ yudao:
59 59 - id: tcp-json
60 60 type: tcp
61 61 port: 8091
62   - enabled: true
  62 + enabled: false
63 63 serialize: json
64 64 tcp:
65 65 max-connections: 1000
... ... @@ -75,6 +75,20 @@ yudao:
75 75 # initial-bytes-to-strip: 4 # 初始跳过的字节数
76 76 # type: fixed_length # 拆包类型:length_field / delimiter / fixed_length
77 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 94 protocol:
... ... @@ -113,16 +127,6 @@ yudao:
113 127 trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
114 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 130 # 针对引入的 MQTT 组件的配置
127 131 # ====================================
128 132 mqtt:
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 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 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 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 13 import lombok.extern.slf4j.Slf4j;
16 14 import org.junit.jupiter.api.Disabled;
17 15 import org.junit.jupiter.api.Test;
... ... @@ -27,16 +25,9 @@ import java.util.Map;
27 25 *
28 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 28 * <p>使用步骤:
37 29 * <ol>
38 30 * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
39   - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
40 31 * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
41 32 * <li>运行以下测试方法:
42 33 * <ul>
... ... @@ -58,10 +49,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
58 49 private static final int SERVER_PORT = 8093;
59 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 59 // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
67 60  
... ... @@ -81,30 +74,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
81 74 */
82 75 @Test
83 76 public void testAuth() throws Exception {
84   - // 1.1 构建认证消息
  77 + // 1. 构建认证消息
85 78 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
86 79 IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
87 80 .setClientId(authInfo.getClientId())
88 81 .setUsername(authInfo.getUsername())
89 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 99 */
119 100 @Test
120 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 120 */
152 121 @Test
153 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 125 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
158 126 withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
159 127 .put("width", 1)
160 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 140 */
186 141 @Test
187 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 145 IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
192 146 withToken(IotDeviceEventPostReqDTO.of(
193 147 "eat",
194 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 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 2  
3 3 import cn.hutool.core.collection.ListUtil;
4 4 import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.IdUtil;
6 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 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 12 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
14 13 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
15 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 17 import lombok.extern.slf4j.Slf4j;
20 18 import org.junit.jupiter.api.Disabled;
21 19 import org.junit.jupiter.api.Test;
22 20  
  21 +import java.net.DatagramPacket;
23 22 import java.net.DatagramSocket;
  23 +import java.net.InetAddress;
24 24 import java.util.Collections;
25 25 import java.util.HashMap;
26   -import java.util.List;
27 26 import java.util.Map;
28 27  
29   -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive;
30   -
31 28 /**
32 29 * IoT 网关设备 UDP 协议集成测试(手动测试)
33 30 *
34 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 33 * <p>使用步骤:
43 34 * <ol>
44 35 * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
45   - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
46 36 * <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
47 37 * <li>运行以下测试方法:
48 38 * <ul>
... ... @@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd
63 53 @Disabled
64 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 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 67 // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
74 68  
... ... @@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
93 87 */
94 88 @Test
95 89 public void testAuth() throws Exception {
96   - // 1.1 构建认证消息
  90 + // 1. 构建认证消息
97 91 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
98 92 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
99 93 IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
100 94 .setClientId(authInfo.getClientId())
101 95 .setUsername(authInfo.getUsername())
102 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 110 @Test
131 111 public void testTopoAdd() throws Exception {
132   - // 1.1 构建子设备认证信息
  112 + // 1. 构建子设备认证信息
133 113 IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
134 114 SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
135 115 IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
136 116 .setClientId(subAuthInfo.getClientId())
137 117 .setUsername(subAuthInfo.getUsername())
138 118 .setPassword(subAuthInfo.getPassword());
139   - // 1.2 构建请求参数
140 119 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
141 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 132 @Test
171 133 public void testTopoDelete() throws Exception {
172   - // 1.1 构建请求参数
  134 + // 1. 构建请求参数
173 135 IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
174 136 params.setSubDevices(Collections.singletonList(
175 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 149 @Test
205 150 public void testTopoGet() throws Exception {
206   - // 1.1 构建请求参数(目前为空,预留扩展)
  151 + // 1. 构建请求参数
207 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 166 @Test
241 167 public void testSubDeviceRegister() throws Exception {
242   - // 1.1 构建请求参数
  168 + // 1. 构建请求参数
243 169 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
244 170 subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
245 171 subDevice.setDeviceName("mougezishebei");
246   - IotDeviceMessage request = IotDeviceMessage.of(
247   - IdUtil.fastSimpleUUID(),
  172 + IotDeviceMessage request = IotDeviceMessage.requestOf(
248 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 186 @Test
277 187 public void testPropertyPackPost() throws Exception {
... ... @@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
307 217 params.setProperties(gatewayProperties);
308 218 params.setEvents(gatewayEvents);
309 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 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 233 private Map<String, Object> withToken(Object params) {
346 234 Map<String, Object> result = new HashMap<>();
... ... @@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
349 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 1 package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 7 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 8 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 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 12 import lombok.extern.slf4j.Slf4j;
15 13 import org.junit.jupiter.api.Disabled;
16 14 import org.junit.jupiter.api.Test;
17 15  
  16 +import java.net.DatagramPacket;
18 17 import java.net.DatagramSocket;
  18 +import java.net.InetAddress;
19 19 import java.util.HashMap;
20 20 import java.util.Map;
21 21  
22   -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive;
23   -
24 22 /**
25 23 * IoT 网关子设备 UDP 协议集成测试(手动测试)
26 24 *
... ... @@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd
29 27 * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
30 28 * <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。
31 29 *
32   - * <p>支持两种编解码格式:
33   - * <ul>
34   - * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
35   - * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
36   - * </ul>
37   - *
38 30 * <p>使用步骤:
39 31 * <ol>
40 32 * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
41 33 * <li>确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
42   - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
43 34 * <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
44 35 * <li>运行以下测试方法:
45 36 * <ul>
... ... @@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd
57 48 @Disabled
58 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 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 62 // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
68 63  
... ... @@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
82 77 */
83 78 @Test
84 79 public void testAuth() throws Exception {
85   - // 1.1 构建认证消息
  80 + // 1. 构建认证消息
86 81 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
87 82 IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
88 83 .setClientId(authInfo.getClientId())
89 84 .setUsername(authInfo.getUsername())
90 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 98 */
116 99 @Test
117 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 103 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
122 104 withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
123 105 .put("power", 100)
124 106 .put("status", "online")
125 107 .put("temperature", 36.5)
126   - .build())),
127   - null, null, null);
128   - // 1.2 编码
129   - byte[] payload = CODEC.encode(request);
  108 + .build())));
130 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 120 */
152 121 @Test
153 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 125 IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
158 126 withToken(IotDeviceEventPostReqDTO.of(
159 127 "alarm",
... ... @@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
163 131 .put("threshold", 40)
164 132 .put("current", 42)
165 133 .build(),
166   - System.currentTimeMillis())),
167   - null, null, null);
168   - // 1.2 编码
169   - byte[] payload = CODEC.encode(request);
  134 + System.currentTimeMillis())));
170 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 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 147 private Map<String, Object> withToken(Object params) {
200 148 Map<String, Object> result = new HashMap<>();
... ... @@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
203 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 }
... ...