Commit 8e4b4cf20a05a16acb91146f5dacbfc569448357

Authored by YunaiV
1 parent cb301eb7

feat(iot):【协议改造】udp 初步改造(100%),基于 /Users/yunai/.claude/plans/iot-udp-fix-plan.md,优化代码以及各种缺陷

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
... ... @@ -155,7 +155,11 @@ public class IotTcpProtocol implements IotProtocol {
155 155 this.downstreamSubscriber.start();
156 156 } catch (Exception e) {
157 157 log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e);
158   - // 启动失败时关闭 Vertx
  158 + // 启动失败时关闭资源
  159 + if (tcpServer != null) {
  160 + tcpServer.close();
  161 + tcpServer = null;
  162 + }
159 163 if (vertx != null) {
160 164 vertx.close();
161 165 vertx = null;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java
... ... @@ -21,17 +21,11 @@ public class IotUdpConfig {
21 21 /**
22 22 * 会话超时时间(毫秒)
23 23 * <p>
24   - * 用于清理不活跃的设备地址映射
  24 + * 基于 Guava Cache 的 expireAfterAccess 实现自动过期清理
25 25 */
26 26 @NotNull(message = "会话超时时间不能为空")
27 27 @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒")
28 28 private Long sessionTimeoutMs = 60000L;
29   - /**
30   - * 会话清理间隔(毫秒)
31   - */
32   - @NotNull(message = "会话清理间隔不能为空")
33   - @Min(value = 1000, message = "会话清理间隔必须大于 1000 毫秒")
34   - private Long sessionCleanIntervalMs = 30000L;
35 29  
36 30 /**
37 31 * 接收缓冲区大小(字节)
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4 3 import cn.hutool.extra.spring.SpringUtil;
5   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
7 5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
8 6 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 7 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
12 8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
13 9 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler;
... ... @@ -16,8 +12,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpU
16 12 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
17 13 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
18 14 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;
  15 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
21 16 import io.vertx.core.Vertx;
22 17 import io.vertx.core.datagram.DatagramSocket;
23 18 import io.vertx.core.datagram.DatagramSocketOptions;
... ... @@ -25,8 +20,6 @@ import lombok.Getter;
25 20 import lombok.extern.slf4j.Slf4j;
26 21 import org.springframework.util.Assert;
27 22  
28   -import java.util.List;
29   -
30 23 /**
31 24 * IoT UDP 协议实现
32 25 * <p>
... ... @@ -78,22 +71,11 @@ public class IotUdpProtocol implements IotProtocol {
78 71 */
79 72 private final IotUdpSessionManager sessionManager;
80 73  
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 74 public IotUdpProtocol(ProtocolInstanceProperties properties) {
91 75 IotUdpConfig udpConfig = properties.getUdp();
92 76 Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空");
93 77 this.properties = properties;
94 78 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
95   - this.deviceService = SpringUtil.getBean(IotDeviceService.class);
96   - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
97 79  
98 80 // 初始化序列化器
99 81 IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize());
... ... @@ -102,7 +84,7 @@ public class IotUdpProtocol implements IotProtocol {
102 84 this.serializer = serializerManager.get(serializeType);
103 85  
104 86 // 初始化会话管理器
105   - this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions());
  87 + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs());
106 88  
107 89 // 初始化下行消息订阅者
108 90 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
... ... @@ -143,24 +125,30 @@ public class IotUdpProtocol implements IotProtocol {
143 125 // 1.4 创建上行消息处理器
144 126 IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer);
145 127  
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   - }
  128 + // 1.5 启动 UDP 服务器(阻塞式)
  129 + try {
  130 + udpSocket.listen(properties.getPort(), "0.0.0.0").result();
152 131 // 设置数据包处理器
153 132 udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket));
154 133 running = true;
155 134 log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]",
156 135 getId(), properties.getPort(), serverId);
157   - // 启动会话清理定时器
158   - startSessionCleanTimer(udpConfig);
159 136  
160 137 // 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 138 this.downstreamSubscriber.start();
163   - });
  139 + } catch (Exception e) {
  140 + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e);
  141 + // 启动失败时关闭资源
  142 + if (udpSocket != null) {
  143 + udpSocket.close();
  144 + udpSocket = null;
  145 + }
  146 + if (vertx != null) {
  147 + vertx.close();
  148 + vertx = null;
  149 + }
  150 + throw e;
  151 + }
164 152 }
165 153  
166 154 @Override
... ... @@ -176,13 +164,7 @@ public class IotUdpProtocol implements IotProtocol {
176 164 log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e);
177 165 }
178 166  
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
  167 + // 2.1 关闭 UDP Socket
186 168 if (udpSocket != null) {
187 169 try {
188 170 udpSocket.close().result();
... ... @@ -192,7 +174,7 @@ public class IotUdpProtocol implements IotProtocol {
192 174 }
193 175 udpSocket = null;
194 176 }
195   - // 2.3 关闭 Vertx 实例
  177 + // 2.2 关闭 Vertx 实例
196 178 if (vertx != null) {
197 179 try {
198 180 vertx.close().result();
... ... @@ -206,54 +188,4 @@ public class IotUdpProtocol implements IotProtocol {
206 188 log.info("[stop][IoT UDP 协议 {} 已停止]", getId());
207 189 }
208 190  
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 191 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java
... ... @@ -34,7 +34,7 @@ public class IotUdpDownstreamHandler {
34 34 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
35 35 message.getDeviceId(), message.getMethod(), message.getId());
36 36 // 1. 检查设备会话
37   - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId());
  37 + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSession(message.getDeviceId());
38 38 if (sessionInfo == null) {
39 39 log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
40 40 message.getDeviceId(), message.getMethod(), message.getId());
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -85,8 +85,7 @@ yudao:
85 85 serialize: json
86 86 udp:
87 87 max-sessions: 1000 # 最大会话数
88   - session-timeout-ms: 60000 # 会话超时时间(毫秒)
89   - session-clean-interval-ms: 30000 # 会话清理间隔(毫秒)
  88 + session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期
90 89 receive-buffer-size: 65536 # 接收缓冲区大小(字节)
91 90 send-buffer-size: 65536 # 发送缓冲区大小(字节)
92 91  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
... ... @@ -65,7 +65,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
65 65 /**
66 66 * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
67 67 */
68   - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo";
  68 + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc3MDUyNTA0MywiZGV2aWNlTmFtZSI6InNtYWxsIn0.W9Mo-Oe1ZNLDkINndKieUeW1XhDzhVp0W0zTAwO6hJM";
69 69  
70 70 // ===================== 认证测试 =====================
71 71  
... ... @@ -120,7 +120,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
120 120 */
121 121 @Test
122 122 public void testPropertyPost() throws Exception {
123   - // 1. 构建属性上报消息(UDP 协议:token 放在 params 中)
  123 + // 1. 构建属性上报消息
124 124 IotDeviceMessage request = IotDeviceMessage.requestOf(
125 125 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
126 126 withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java
... ... @@ -98,7 +98,7 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
98 98 */
99 99 @Test
100 100 public void testPropertyPost() throws Exception {
101   - // 1. 构建属性上报消息(UDP 协议:token 放在 params 中)
  101 + // 1. 构建属性上报消息
102 102 IotDeviceMessage request = IotDeviceMessage.requestOf(
103 103 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
104 104 withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
... ... @@ -120,7 +120,7 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
120 120 */
121 121 @Test
122 122 public void testEventPost() throws Exception {
123   - // 1. 构建事件上报消息(UDP 协议:token 放在 params 中)
  123 + // 1. 构建事件上报消息
124 124 IotDeviceMessage request = IotDeviceMessage.requestOf(
125 125 IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
126 126 withToken(IotDeviceEventPostReqDTO.of(
... ...