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