Commit de1a53a5f14af453f3c22459c409db6d26e5f0e5
1 parent
4003f4b0
feat:【iot】UDP 协议:兼容下行的时候,基于连接的 codec 处理
Showing
8 changed files
with
329 additions
and
240 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -223,11 +223,9 @@ public class IotGatewayConfiguration { |
| 223 | 223 | @Bean |
| 224 | 224 | public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, |
| 225 | 225 | IotDeviceMessageService messageService, |
| 226 | - IotDeviceService deviceService, | |
| 227 | 226 | IotUdpSessionManager sessionManager, |
| 228 | 227 | IotMessageBus messageBus) { |
| 229 | - return new IotUdpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, | |
| 230 | - messageBus); | |
| 228 | + return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus); | |
| 231 | 229 | } |
| 232 | 230 | |
| 233 | 231 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java
| ... | ... | @@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 10 | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | 10 | import jakarta.annotation.PostConstruct; |
| 12 | 11 | import lombok.RequiredArgsConstructor; |
| ... | ... | @@ -25,8 +24,6 @@ public class IotUdpDownstreamSubscriber implements IotMessageSubscriber<IotDevic |
| 25 | 24 | |
| 26 | 25 | private final IotDeviceMessageService messageService; |
| 27 | 26 | |
| 28 | - private final IotDeviceService deviceService; | |
| 29 | - | |
| 30 | 27 | private final IotUdpSessionManager sessionManager; |
| 31 | 28 | |
| 32 | 29 | private final IotMessageBus messageBus; |
| ... | ... | @@ -36,7 +33,7 @@ public class IotUdpDownstreamSubscriber implements IotMessageSubscriber<IotDevic |
| 36 | 33 | @PostConstruct |
| 37 | 34 | public void init() { |
| 38 | 35 | // 初始化下游处理器 |
| 39 | - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, deviceService, sessionManager, protocol); | |
| 36 | + this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol); | |
| 40 | 37 | |
| 41 | 38 | messageBus.register(this); |
| 42 | 39 | log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager; |
| 2 | 2 | |
| 3 | 3 | import io.vertx.core.buffer.Buffer; |
| 4 | 4 | import io.vertx.core.datagram.DatagramSocket; |
| 5 | +import lombok.Data; | |
| 5 | 6 | import lombok.extern.slf4j.Slf4j; |
| 6 | 7 | import org.springframework.stereotype.Component; |
| 7 | 8 | |
| ... | ... | @@ -29,9 +30,9 @@ import java.util.concurrent.ConcurrentHashMap; |
| 29 | 30 | public class IotUdpSessionManager { |
| 30 | 31 | |
| 31 | 32 | /** |
| 32 | - * 设备 ID -> 设备地址(用于下行消息发送) | |
| 33 | + * 设备 ID -> 会话信息(包含地址和 codecType) | |
| 33 | 34 | */ |
| 34 | - private final Map<Long, InetSocketAddress> deviceAddressMap = new ConcurrentHashMap<>(); | |
| 35 | + private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>(); | |
| 35 | 36 | |
| 36 | 37 | /** |
| 37 | 38 | * 设备地址 Key -> 最后活跃时间(用于清理) |
| ... | ... | @@ -44,18 +45,41 @@ public class IotUdpSessionManager { |
| 44 | 45 | private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>(); |
| 45 | 46 | |
| 46 | 47 | /** |
| 47 | - * 更新设备地址(每次收到上行消息时调用) | |
| 48 | + * 更新设备会话(每次收到上行消息时调用) | |
| 48 | 49 | * |
| 49 | - * @param deviceId 设备 ID | |
| 50 | - * @param address 设备地址 | |
| 50 | + * @param deviceId 设备 ID | |
| 51 | + * @param address 设备地址 | |
| 52 | + * @param codecType 消息编解码类型 | |
| 51 | 53 | */ |
| 52 | - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { | |
| 54 | + public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { | |
| 53 | 55 | String addressKey = buildAddressKey(address); |
| 54 | - // 更新设备地址映射 | |
| 55 | - deviceAddressMap.put(deviceId, address); | |
| 56 | + // 更新设备会话映射 | |
| 57 | + deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); | |
| 56 | 58 | lastActiveTimeMap.put(addressKey, LocalDateTime.now()); |
| 57 | 59 | addressDeviceMap.put(addressKey, deviceId); |
| 58 | - log.debug("[updateDeviceAddress][更新设备地址,设备 ID: {},地址: {}]", deviceId, addressKey); | |
| 60 | + log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); | |
| 61 | + } | |
| 62 | + | |
| 63 | + /** | |
| 64 | + * 更新设备地址(兼容旧接口,默认不更新 codecType) | |
| 65 | + * | |
| 66 | + * @param deviceId 设备 ID | |
| 67 | + * @param address 设备地址 | |
| 68 | + */ | |
| 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); | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * 获取设备会话信息 | |
| 77 | + * | |
| 78 | + * @param deviceId 设备 ID | |
| 79 | + * @return 会话信息 | |
| 80 | + */ | |
| 81 | + public SessionInfo getSessionInfo(Long deviceId) { | |
| 82 | + return deviceSessionMap.get(deviceId); | |
| 59 | 83 | } |
| 60 | 84 | |
| 61 | 85 | /** |
| ... | ... | @@ -65,7 +89,7 @@ public class IotUdpSessionManager { |
| 65 | 89 | * @return 是否在线 |
| 66 | 90 | */ |
| 67 | 91 | public boolean isDeviceOnline(Long deviceId) { |
| 68 | - return deviceAddressMap.containsKey(deviceId); | |
| 92 | + return deviceSessionMap.containsKey(deviceId); | |
| 69 | 93 | } |
| 70 | 94 | |
| 71 | 95 | /** |
| ... | ... | @@ -87,12 +111,13 @@ public class IotUdpSessionManager { |
| 87 | 111 | * @return 是否发送成功 |
| 88 | 112 | */ |
| 89 | 113 | public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { |
| 90 | - InetSocketAddress address = deviceAddressMap.get(deviceId); | |
| 91 | - if (address == null) { | |
| 92 | - log.warn("[sendToDevice][设备地址不存在,设备 ID: {}]", deviceId); | |
| 114 | + SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | |
| 115 | + if (sessionInfo == null || sessionInfo.getAddress() == null) { | |
| 116 | + log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); | |
| 93 | 117 | return false; |
| 94 | 118 | } |
| 95 | 119 | |
| 120 | + InetSocketAddress address = sessionInfo.getAddress(); | |
| 96 | 121 | try { |
| 97 | 122 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 98 | 123 | if (result.succeeded()) { |
| ... | ... | @@ -134,8 +159,8 @@ public class IotUdpSessionManager { |
| 134 | 159 | iterator.remove(); |
| 135 | 160 | continue; |
| 136 | 161 | } |
| 137 | - InetSocketAddress address = deviceAddressMap.remove(deviceId); | |
| 138 | - if (address == null) { | |
| 162 | + SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); | |
| 163 | + if (sessionInfo == null) { | |
| 139 | 164 | iterator.remove(); |
| 140 | 165 | continue; |
| 141 | 166 | } |
| ... | ... | @@ -157,4 +182,22 @@ public class IotUdpSessionManager { |
| 157 | 182 | return address.getHostString() + ":" + address.getPort(); |
| 158 | 183 | } |
| 159 | 184 | |
| 185 | + /** | |
| 186 | + * 会话信息 | |
| 187 | + */ | |
| 188 | + @Data | |
| 189 | + public static class SessionInfo { | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * 设备地址 | |
| 193 | + */ | |
| 194 | + private InetSocketAddress address; | |
| 195 | + | |
| 196 | + /** | |
| 197 | + * 消息编解码类型 | |
| 198 | + */ | |
| 199 | + private String codecType; | |
| 200 | + | |
| 201 | + } | |
| 202 | + | |
| 160 | 203 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 8 | 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 9 | 7 | import io.vertx.core.datagram.DatagramSocket; |
| 10 | 8 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -19,18 +17,14 @@ public class IotUdpDownstreamHandler { |
| 19 | 17 | |
| 20 | 18 | private final IotDeviceMessageService deviceMessageService; |
| 21 | 19 | |
| 22 | - private final IotDeviceService deviceService; | |
| 23 | - | |
| 24 | 20 | private final IotUdpSessionManager sessionManager; |
| 25 | 21 | |
| 26 | 22 | private final IotUdpUpstreamProtocol protocol; |
| 27 | 23 | |
| 28 | 24 | public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, |
| 29 | - IotDeviceService deviceService, | |
| 30 | 25 | IotUdpSessionManager sessionManager, |
| 31 | 26 | IotUdpUpstreamProtocol protocol) { |
| 32 | 27 | this.deviceMessageService = deviceMessageService; |
| 33 | - this.deviceService = deviceService; | |
| 34 | 28 | this.sessionManager = sessionManager; |
| 35 | 29 | this.protocol = protocol; |
| 36 | 30 | } |
| ... | ... | @@ -45,21 +39,15 @@ public class IotUdpDownstreamHandler { |
| 45 | 39 | log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 46 | 40 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 47 | 41 | |
| 48 | - // 1.1 获取设备信息 | |
| 49 | - IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 50 | - if (deviceInfo == null) { | |
| 51 | - log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId()); | |
| 52 | - return; | |
| 53 | - } | |
| 54 | - // 1.2 检查设备是否在线(即是否有地址映射) | |
| 55 | - if (sessionManager.isDeviceOffline(message.getDeviceId())) { | |
| 42 | + // 1. 获取会话信息(包含 codecType) | |
| 43 | + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); | |
| 44 | + if (sessionInfo == null) { | |
| 56 | 45 | log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); |
| 57 | 46 | return; |
| 58 | 47 | } |
| 59 | 48 | |
| 60 | - // 2. 根据产品 Key 和设备名称编码消息,并发送到设备 | |
| 61 | - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), | |
| 62 | - deviceInfo.getDeviceName()); | |
| 49 | + // 2. 使用会话中的 codecType 编码消息,并发送到设备 | |
| 50 | + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); | |
| 63 | 51 | DatagramSocket socket = protocol.getUdpSocket(); |
| 64 | 52 | if (socket == null) { |
| 65 | 53 | log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java
| ... | ... | @@ -195,8 +195,8 @@ public class IotUdpUpstreamHandler { |
| 195 | 195 | // 3.1 生成 JWT Token(无状态) |
| 196 | 196 | String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); |
| 197 | 197 | |
| 198 | - // 3.2 更新设备地址映射(用于下行消息) | |
| 199 | - sessionManager.updateDeviceAddress(device.getId(), senderAddress); | |
| 198 | + // 3.2 更新设备会话信息(用于下行消息,保存 codecType) | |
| 199 | + sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); | |
| 200 | 200 | |
| 201 | 201 | // 3.3 发送上线消息 |
| 202 | 202 | sendOnlineMessage(device); |
| ... | ... | @@ -298,8 +298,8 @@ public class IotUdpUpstreamHandler { |
| 298 | 298 | return; |
| 299 | 299 | } |
| 300 | 300 | |
| 301 | - // 3. 更新设备地址映射(保持最新) | |
| 302 | - sessionManager.updateDeviceAddress(device.getId(), senderAddress); | |
| 301 | + // 3. 更新设备会话信息(保持最新,保存 codecType) | |
| 302 | + sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); | |
| 303 | 303 | |
| 304 | 304 | // 4. 将 body 设置为实际的 params,发送消息到消息总线 |
| 305 | 305 | message.setParams(body); | ... | ... |
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.HexUtil; | |
| 4 | 5 | import cn.hutool.core.util.IdUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 11 | 11 | 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; | |
| 12 | 15 | import lombok.extern.slf4j.Slf4j; |
| 13 | 16 | import org.junit.jupiter.api.Test; |
| 14 | 17 | |
| 15 | 18 | import java.net.DatagramPacket; |
| 16 | 19 | import java.net.DatagramSocket; |
| 17 | 20 | import java.net.InetAddress; |
| 18 | -import java.nio.charset.StandardCharsets; | |
| 19 | 21 | import java.util.HashMap; |
| 20 | 22 | import java.util.Map; |
| 21 | 23 | |
| ... | ... | @@ -24,10 +26,16 @@ import java.util.Map; |
| 24 | 26 | * |
| 25 | 27 | * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 |
| 26 | 28 | * |
| 29 | + * <p>支持两种编解码格式: | |
| 30 | + * <ul> | |
| 31 | + * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li> | |
| 32 | + * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li> | |
| 33 | + * </ul> | |
| 34 | + * | |
| 27 | 35 | * <p>使用步骤: |
| 28 | 36 | * <ol> |
| 29 | - * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8092)</li> | |
| 30 | - * <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li> | |
| 37 | + * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> | |
| 38 | + * <li>修改 {@link #CODEC} 选择测试的编解码格式</li> | |
| 31 | 39 | * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li> |
| 32 | 40 | * <li>运行以下测试方法: |
| 33 | 41 | * <ul> |
| ... | ... | @@ -48,6 +56,10 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 48 | 56 | private static final int SERVER_PORT = 8093; |
| 49 | 57 | private static final int TIMEOUT_MS = 5000; |
| 50 | 58 | |
| 59 | + // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== | |
| 60 | + private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); | |
| 61 | +// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); | |
| 62 | + | |
| 51 | 63 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== |
| 52 | 64 | private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; |
| 53 | 65 | private static final String DEVICE_NAME = "small"; |
| ... | ... | @@ -65,27 +77,32 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 65 | 77 | */ |
| 66 | 78 | @Test |
| 67 | 79 | public void testAuth() throws Exception { |
| 68 | - // 1.1 构建请求 | |
| 80 | + // 1.1 构建认证消息 | |
| 69 | 81 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 70 | 82 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() |
| 71 | 83 | .setClientId(authInfo.getClientId()) |
| 72 | 84 | .setUsername(authInfo.getUsername()) |
| 73 | 85 | .setPassword(authInfo.getPassword()); |
| 74 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 75 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 76 | - .put("method", "auth") | |
| 77 | - .put("params", authReqDTO) | |
| 78 | - .build()); | |
| 79 | - // 1.2 输出请求 | |
| 80 | - log.info("[testAuth][请求体: {}]", payload); | |
| 86 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | |
| 87 | + // 1.2 编码 | |
| 88 | + byte[] payload = CODEC.encode(request); | |
| 89 | + log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | |
| 90 | + if (CODEC instanceof IotTcpBinaryDeviceMessageCodec) { | |
| 91 | + log.info("[testAuth][二进制数据包(HEX): {}]", HexUtil.encodeHexStr(payload)); | |
| 92 | + } | |
| 81 | 93 | |
| 82 | 94 | // 2.1 发送请求 |
| 83 | 95 | try (DatagramSocket socket = new DatagramSocket()) { |
| 84 | 96 | socket.setSoTimeout(TIMEOUT_MS); |
| 85 | - String response = sendAndReceive(socket, payload); | |
| 86 | - // 2.2 输出结果 | |
| 87 | - log.info("[testAuth][响应体: {}]", response); | |
| 88 | - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | |
| 97 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 98 | + // 2.2 解码响应 | |
| 99 | + if (responseBytes != null) { | |
| 100 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 101 | + log.info("[testAuth][响应消息: {}]", response); | |
| 102 | + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | |
| 103 | + } else { | |
| 104 | + log.warn("[testAuth][未收到响应]"); | |
| 105 | + } | |
| 89 | 106 | } |
| 90 | 107 | } |
| 91 | 108 | |
| ... | ... | @@ -96,25 +113,30 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 96 | 113 | */ |
| 97 | 114 | @Test |
| 98 | 115 | public void testPropertyPost() throws Exception { |
| 99 | - // 1.1 构建请求(UDP 协议:token 放在 params 中) | |
| 100 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 101 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 102 | - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) | |
| 103 | - .put("version", "1.0") | |
| 104 | - .put("params", withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | |
| 116 | + // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) | |
| 117 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 118 | + IdUtil.fastSimpleUUID(), | |
| 119 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | |
| 120 | + withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | |
| 105 | 121 | .put("width", 1) |
| 106 | 122 | .put("height", "2") |
| 107 | - .build()))) | |
| 108 | - .build()); | |
| 109 | - // 1.2 输出请求 | |
| 110 | - log.info("[testPropertyPost][请求体: {}]", payload); | |
| 123 | + .build())), | |
| 124 | + null, null, null); | |
| 125 | + // 1.2 编码 | |
| 126 | + byte[] payload = CODEC.encode(request); | |
| 127 | + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 111 | 128 | |
| 112 | - // 2. 发送请求 | |
| 129 | + // 2.1 发送请求 | |
| 113 | 130 | try (DatagramSocket socket = new DatagramSocket()) { |
| 114 | 131 | socket.setSoTimeout(TIMEOUT_MS); |
| 115 | - String response = sendAndReceive(socket, payload); | |
| 116 | - // 2.2 输出结果 | |
| 117 | - log.info("[testPropertyPost][响应体: {}]", response); | |
| 132 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 133 | + // 2.2 解码响应 | |
| 134 | + if (responseBytes != null) { | |
| 135 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 136 | + log.info("[testPropertyPost][响应消息: {}]", response); | |
| 137 | + } else { | |
| 138 | + log.warn("[testPropertyPost][未收到响应]"); | |
| 139 | + } | |
| 118 | 140 | } |
| 119 | 141 | } |
| 120 | 142 | |
| ... | ... | @@ -125,60 +147,30 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 125 | 147 | */ |
| 126 | 148 | @Test |
| 127 | 149 | public void testEventPost() throws Exception { |
| 128 | - // 1.1 构建请求(UDP 协议:token 放在 params 中) | |
| 129 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 130 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 131 | - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) | |
| 132 | - .put("version", "1.0") | |
| 133 | - .put("params", withToken(IotDeviceEventPostReqDTO.of( | |
| 150 | + // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) | |
| 151 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 152 | + IdUtil.fastSimpleUUID(), | |
| 153 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | |
| 154 | + withToken(IotDeviceEventPostReqDTO.of( | |
| 134 | 155 | "eat", |
| 135 | 156 | MapUtil.<String, Object>builder().put("rice", 3).build(), |
| 136 | - System.currentTimeMillis()))) | |
| 137 | - .build()); | |
| 138 | - // 1.2 输出请求 | |
| 139 | - log.info("[testEventPost][请求体: {}]", payload); | |
| 140 | - | |
| 141 | - // 2.1 发送请求 | |
| 142 | - try (DatagramSocket socket = new DatagramSocket()) { | |
| 143 | - socket.setSoTimeout(TIMEOUT_MS); | |
| 144 | - String response = sendAndReceive(socket, payload); | |
| 145 | - // 2.2 输出结果 | |
| 146 | - log.info("[testEventPost][响应体: {}]", response); | |
| 147 | - } | |
| 148 | - } | |
| 149 | - | |
| 150 | - // ===================== 动态注册测试 ===================== | |
| 151 | - | |
| 152 | - /** | |
| 153 | - * 直连设备动态注册测试(一型一密) | |
| 154 | - * <p> | |
| 155 | - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) | |
| 156 | - * <p> | |
| 157 | - * 注意:此接口不需要 Token 认证 | |
| 158 | - */ | |
| 159 | - @Test | |
| 160 | - public void testDeviceRegister() throws Exception { | |
| 161 | - // 1.1 构建请求参数 | |
| 162 | - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); | |
| 163 | - reqDTO.setProductKey(PRODUCT_KEY); | |
| 164 | - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); | |
| 165 | - reqDTO.setProductSecret("test-product-secret"); | |
| 166 | - // 1.2 构建请求 | |
| 167 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 168 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 169 | - .put("method", IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod()) | |
| 170 | - .put("params", reqDTO) | |
| 171 | - .build()); | |
| 172 | - // 1.3 输出请求 | |
| 173 | - log.info("[testDeviceRegister][请求体: {}]", payload); | |
| 157 | + System.currentTimeMillis())), | |
| 158 | + null, null, null); | |
| 159 | + // 1.2 编码 | |
| 160 | + byte[] payload = CODEC.encode(request); | |
| 161 | + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 174 | 162 | |
| 175 | 163 | // 2.1 发送请求 |
| 176 | 164 | try (DatagramSocket socket = new DatagramSocket()) { |
| 177 | 165 | socket.setSoTimeout(TIMEOUT_MS); |
| 178 | - String response = sendAndReceive(socket, payload); | |
| 179 | - // 2.2 输出结果 | |
| 180 | - log.info("[testDeviceRegister][响应体: {}]", response); | |
| 181 | - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | |
| 166 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 167 | + // 2.2 解码响应 | |
| 168 | + if (responseBytes != null) { | |
| 169 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 170 | + log.info("[testEventPost][响应消息: {}]", response); | |
| 171 | + } else { | |
| 172 | + log.warn("[testEventPost][未收到响应]"); | |
| 173 | + } | |
| 182 | 174 | } |
| 183 | 175 | } |
| 184 | 176 | |
| ... | ... | @@ -201,20 +193,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 201 | 193 | return result; |
| 202 | 194 | } |
| 203 | 195 | |
| 204 | - | |
| 205 | 196 | /** |
| 206 | 197 | * 发送 UDP 请求并接收响应 |
| 207 | 198 | * |
| 208 | 199 | * @param socket UDP Socket |
| 209 | - * @param payload 请求体 | |
| 210 | - * @return 响应内容 | |
| 200 | + * @param payload 请求数据 | |
| 201 | + * @return 响应数据 | |
| 211 | 202 | */ |
| 212 | - public static String sendAndReceive(DatagramSocket socket, String payload) throws Exception { | |
| 213 | - byte[] sendData = payload.getBytes(StandardCharsets.UTF_8); | |
| 203 | + public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception { | |
| 214 | 204 | InetAddress address = InetAddress.getByName(SERVER_HOST); |
| 215 | 205 | |
| 216 | 206 | // 发送请求 |
| 217 | - DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, SERVER_PORT); | |
| 207 | + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); | |
| 218 | 208 | socket.send(sendPacket); |
| 219 | 209 | |
| 220 | 210 | // 接收响应 |
| ... | ... | @@ -222,7 +212,9 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 222 | 212 | DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); |
| 223 | 213 | try { |
| 224 | 214 | socket.receive(receivePacket); |
| 225 | - return new String(receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8); | |
| 215 | + byte[] response = new byte[receivePacket.getLength()]; | |
| 216 | + System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength()); | |
| 217 | + return response; | |
| 226 | 218 | } catch (java.net.SocketTimeoutException e) { |
| 227 | 219 | log.warn("[sendAndReceive][接收响应超时]"); |
| 228 | 220 | return null; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.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.HexUtil; | |
| 4 | 5 | import cn.hutool.core.util.IdUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 8 | 9 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 9 | 10 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; |
| ... | ... | @@ -12,6 +13,9 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 13 | 14 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; |
| 14 | 15 | 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 | 19 | import lombok.extern.slf4j.Slf4j; |
| 16 | 20 | import org.junit.jupiter.api.Test; |
| 17 | 21 | |
| ... | ... | @@ -28,9 +32,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd |
| 28 | 32 | * |
| 29 | 33 | * <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 |
| 30 | 34 | * |
| 35 | + * <p>支持两种编解码格式: | |
| 36 | + * <ul> | |
| 37 | + * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li> | |
| 38 | + * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li> | |
| 39 | + * </ul> | |
| 40 | + * | |
| 31 | 41 | * <p>使用步骤: |
| 32 | 42 | * <ol> |
| 33 | 43 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> |
| 44 | + * <li>修改 {@link #CODEC} 选择测试的编解码格式</li> | |
| 34 | 45 | * <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li> |
| 35 | 46 | * <li>运行以下测试方法: |
| 36 | 47 | * <ul> |
| ... | ... | @@ -50,10 +61,12 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd |
| 50 | 61 | @Slf4j |
| 51 | 62 | public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 52 | 63 | |
| 53 | - private static final String SERVER_HOST = "127.0.0.1"; | |
| 54 | - private static final int SERVER_PORT = 8093; | |
| 55 | 64 | private static final int TIMEOUT_MS = 5000; |
| 56 | 65 | |
| 66 | + // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== | |
| 67 | + private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); | |
| 68 | +// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); | |
| 69 | + | |
| 57 | 70 | // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== |
| 58 | 71 | private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; |
| 59 | 72 | private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; |
| ... | ... | @@ -76,28 +89,33 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 76 | 89 | */ |
| 77 | 90 | @Test |
| 78 | 91 | public void testAuth() throws Exception { |
| 79 | - // 1.1 构建请求 | |
| 92 | + // 1.1 构建认证消息 | |
| 80 | 93 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 81 | 94 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| 82 | 95 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() |
| 83 | 96 | .setClientId(authInfo.getClientId()) |
| 84 | 97 | .setUsername(authInfo.getUsername()) |
| 85 | 98 | .setPassword(authInfo.getPassword()); |
| 86 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 87 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 88 | - .put("method", "auth") | |
| 89 | - .put("params", authReqDTO) | |
| 90 | - .build()); | |
| 91 | - // 1.2 输出请求 | |
| 92 | - log.info("[testAuth][请求体: {}]", payload); | |
| 99 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | |
| 100 | + // 1.2 编码 | |
| 101 | + byte[] payload = CODEC.encode(request); | |
| 102 | + log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | |
| 103 | + if (CODEC instanceof IotTcpBinaryDeviceMessageCodec) { | |
| 104 | + log.info("[testAuth][二进制数据包(HEX): {}]", HexUtil.encodeHexStr(payload)); | |
| 105 | + } | |
| 93 | 106 | |
| 94 | 107 | // 2.1 发送请求 |
| 95 | 108 | try (DatagramSocket socket = new DatagramSocket()) { |
| 96 | 109 | socket.setSoTimeout(TIMEOUT_MS); |
| 97 | - String response = sendAndReceive(socket, payload); | |
| 98 | - // 2.2 输出结果 | |
| 99 | - log.info("[testAuth][响应体: {}]", response); | |
| 100 | - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); | |
| 110 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 111 | + // 2.2 解码响应 | |
| 112 | + if (responseBytes != null) { | |
| 113 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 114 | + log.info("[testAuth][响应消息: {}]", response); | |
| 115 | + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); | |
| 116 | + } else { | |
| 117 | + log.warn("[testAuth][未收到响应]"); | |
| 118 | + } | |
| 101 | 119 | } |
| 102 | 120 | } |
| 103 | 121 | |
| ... | ... | @@ -120,21 +138,26 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 120 | 138 | // 1.2 构建请求参数 |
| 121 | 139 | IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); |
| 122 | 140 | params.setSubDevices(Collections.singletonList(subDeviceAuth)); |
| 123 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 124 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 125 | - .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) | |
| 126 | - .put("version", "1.0") | |
| 127 | - .put("params", withToken(params)) | |
| 128 | - .build()); | |
| 129 | - // 1.3 输出请求 | |
| 130 | - log.info("[testTopoAdd][请求体: {}]", payload); | |
| 141 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 142 | + IdUtil.fastSimpleUUID(), | |
| 143 | + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | |
| 144 | + withToken(params), | |
| 145 | + null, null, null); | |
| 146 | + // 1.3 编码 | |
| 147 | + byte[] payload = CODEC.encode(request); | |
| 148 | + log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 131 | 149 | |
| 132 | 150 | // 2.1 发送请求 |
| 133 | 151 | try (DatagramSocket socket = new DatagramSocket()) { |
| 134 | 152 | socket.setSoTimeout(TIMEOUT_MS); |
| 135 | - String response = sendAndReceive(socket, payload); | |
| 136 | - // 2.2 输出结果 | |
| 137 | - log.info("[testTopoAdd][响应体: {}]", response); | |
| 153 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 154 | + // 2.2 解码响应 | |
| 155 | + if (responseBytes != null) { | |
| 156 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 157 | + log.info("[testTopoAdd][响应消息: {}]", response); | |
| 158 | + } else { | |
| 159 | + log.warn("[testTopoAdd][未收到响应]"); | |
| 160 | + } | |
| 138 | 161 | } |
| 139 | 162 | } |
| 140 | 163 | |
| ... | ... | @@ -149,21 +172,26 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 149 | 172 | IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); |
| 150 | 173 | params.setSubDevices(Collections.singletonList( |
| 151 | 174 | new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); |
| 152 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 153 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 154 | - .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) | |
| 155 | - .put("version", "1.0") | |
| 156 | - .put("params", withToken(params)) | |
| 157 | - .build()); | |
| 158 | - // 1.2 输出请求 | |
| 159 | - log.info("[testTopoDelete][请求体: {}]", payload); | |
| 175 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 176 | + IdUtil.fastSimpleUUID(), | |
| 177 | + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | |
| 178 | + withToken(params), | |
| 179 | + null, null, null); | |
| 180 | + // 1.2 编码 | |
| 181 | + byte[] payload = CODEC.encode(request); | |
| 182 | + log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 160 | 183 | |
| 161 | 184 | // 2.1 发送请求 |
| 162 | 185 | try (DatagramSocket socket = new DatagramSocket()) { |
| 163 | 186 | socket.setSoTimeout(TIMEOUT_MS); |
| 164 | - String response = sendAndReceive(socket, payload); | |
| 165 | - // 2.2 输出结果 | |
| 166 | - log.info("[testTopoDelete][响应体: {}]", response); | |
| 187 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 188 | + // 2.2 解码响应 | |
| 189 | + if (responseBytes != null) { | |
| 190 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 191 | + log.info("[testTopoDelete][响应消息: {}]", response); | |
| 192 | + } else { | |
| 193 | + log.warn("[testTopoDelete][未收到响应]"); | |
| 194 | + } | |
| 167 | 195 | } |
| 168 | 196 | } |
| 169 | 197 | |
| ... | ... | @@ -176,21 +204,26 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 176 | 204 | public void testTopoGet() throws Exception { |
| 177 | 205 | // 1.1 构建请求参数(目前为空,预留扩展) |
| 178 | 206 | IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); |
| 179 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 180 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 181 | - .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) | |
| 182 | - .put("version", "1.0") | |
| 183 | - .put("params", withToken(params)) | |
| 184 | - .build()); | |
| 185 | - // 1.2 输出请求 | |
| 186 | - log.info("[testTopoGet][请求体: {}]", payload); | |
| 207 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 208 | + IdUtil.fastSimpleUUID(), | |
| 209 | + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | |
| 210 | + withToken(params), | |
| 211 | + null, null, null); | |
| 212 | + // 1.2 编码 | |
| 213 | + byte[] payload = CODEC.encode(request); | |
| 214 | + log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 187 | 215 | |
| 188 | 216 | // 2.1 发送请求 |
| 189 | 217 | try (DatagramSocket socket = new DatagramSocket()) { |
| 190 | 218 | socket.setSoTimeout(TIMEOUT_MS); |
| 191 | - String response = sendAndReceive(socket, payload); | |
| 192 | - // 2.2 输出结果 | |
| 193 | - log.info("[testTopoGet][响应体: {}]", response); | |
| 219 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 220 | + // 2.2 解码响应 | |
| 221 | + if (responseBytes != null) { | |
| 222 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 223 | + log.info("[testTopoGet][响应消息: {}]", response); | |
| 224 | + } else { | |
| 225 | + log.warn("[testTopoGet][未收到响应]"); | |
| 226 | + } | |
| 194 | 227 | } |
| 195 | 228 | } |
| 196 | 229 | |
| ... | ... | @@ -209,21 +242,26 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 209 | 242 | IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); |
| 210 | 243 | subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); |
| 211 | 244 | subDevice.setDeviceName("mougezishebei"); |
| 212 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 213 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 214 | - .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) | |
| 215 | - .put("version", "1.0") | |
| 216 | - .put("params", withToken(Collections.singletonList(subDevice))) | |
| 217 | - .build()); | |
| 218 | - // 1.2 输出请求 | |
| 219 | - log.info("[testSubDeviceRegister][请求体: {}]", payload); | |
| 245 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 246 | + IdUtil.fastSimpleUUID(), | |
| 247 | + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | |
| 248 | + withToken(Collections.singletonList(subDevice)), | |
| 249 | + null, null, null); | |
| 250 | + // 1.2 编码 | |
| 251 | + byte[] payload = CODEC.encode(request); | |
| 252 | + log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 220 | 253 | |
| 221 | 254 | // 2.1 发送请求 |
| 222 | 255 | try (DatagramSocket socket = new DatagramSocket()) { |
| 223 | 256 | socket.setSoTimeout(TIMEOUT_MS); |
| 224 | - String response = sendAndReceive(socket, payload); | |
| 225 | - // 2.2 输出结果 | |
| 226 | - log.info("[testSubDeviceRegister][响应体: {}]", response); | |
| 257 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 258 | + // 2.2 解码响应 | |
| 259 | + if (responseBytes != null) { | |
| 260 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 261 | + log.info("[testSubDeviceRegister][响应消息: {}]", response); | |
| 262 | + } else { | |
| 263 | + log.warn("[testSubDeviceRegister][未收到响应]"); | |
| 264 | + } | |
| 227 | 265 | } |
| 228 | 266 | } |
| 229 | 267 | |
| ... | ... | @@ -268,21 +306,26 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { |
| 268 | 306 | params.setProperties(gatewayProperties); |
| 269 | 307 | params.setEvents(gatewayEvents); |
| 270 | 308 | params.setSubDevices(List.of(subDeviceData)); |
| 271 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 272 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 273 | - .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) | |
| 274 | - .put("version", "1.0") | |
| 275 | - .put("params", withToken(params)) | |
| 276 | - .build()); | |
| 277 | - // 1.7 输出请求 | |
| 278 | - log.info("[testPropertyPackPost][请求体: {}]", payload); | |
| 309 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 310 | + IdUtil.fastSimpleUUID(), | |
| 311 | + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | |
| 312 | + withToken(params), | |
| 313 | + null, null, null); | |
| 314 | + // 1.7 编码 | |
| 315 | + byte[] payload = CODEC.encode(request); | |
| 316 | + log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 279 | 317 | |
| 280 | 318 | // 2.1 发送请求 |
| 281 | 319 | try (DatagramSocket socket = new DatagramSocket()) { |
| 282 | 320 | socket.setSoTimeout(TIMEOUT_MS); |
| 283 | - String response = sendAndReceive(socket, payload); | |
| 284 | - // 2.2 输出结果 | |
| 285 | - log.info("[testPropertyPackPost][响应体: {}]", response); | |
| 321 | + byte[] responseBytes = sendAndReceive(socket, payload); | |
| 322 | + // 2.2 解码响应 | |
| 323 | + if (responseBytes != null) { | |
| 324 | + IotDeviceMessage response = CODEC.decode(responseBytes); | |
| 325 | + log.info("[testPropertyPackPost][响应消息: {}]", response); | |
| 326 | + } else { | |
| 327 | + log.warn("[testPropertyPackPost][未收到响应]"); | |
| 328 | + } | |
| 286 | 329 | } |
| 287 | 330 | } |
| 288 | 331 | ... | ... |
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.HexUtil; | |
| 4 | 5 | import cn.hutool.core.util.IdUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 8 | 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | 11 | 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 | 15 | import lombok.extern.slf4j.Slf4j; |
| 12 | 16 | import org.junit.jupiter.api.Test; |
| 13 | 17 | |
| ... | ... | @@ -25,10 +29,17 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd |
| 25 | 29 | * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b> |
| 26 | 30 | * <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。 |
| 27 | 31 | * |
| 32 | + * <p>支持两种编解码格式: | |
| 33 | + * <ul> | |
| 34 | + * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li> | |
| 35 | + * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li> | |
| 36 | + * </ul> | |
| 37 | + * | |
| 28 | 38 | * <p>使用步骤: |
| 29 | 39 | * <ol> |
| 30 | 40 | * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li> |
| 31 | 41 | * <li>确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li> |
| 42 | + * <li>修改 {@link #CODEC} 选择测试的编解码格式</li> | |
| 32 | 43 | * <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li> |
| 33 | 44 | * <li>运行以下测试方法: |
| 34 | 45 | * <ul> |
| ... | ... | @@ -45,10 +56,12 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd |
| 45 | 56 | @Slf4j |
| 46 | 57 | public class IotGatewaySubDeviceUdpProtocolIntegrationTest { |
| 47 | 58 | |
| 48 | - private static final String SERVER_HOST = "127.0.0.1"; | |
| 49 | - private static final int SERVER_PORT = 8093; | |
| 50 | 59 | private static final int TIMEOUT_MS = 5000; |
| 51 | 60 | |
| 61 | + // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== | |
| 62 | + private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); | |
| 63 | +// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); | |
| 64 | + | |
| 52 | 65 | // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== |
| 53 | 66 | private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; |
| 54 | 67 | private static final String DEVICE_NAME = "chazuo-it"; |
| ... | ... | @@ -66,27 +79,32 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { |
| 66 | 79 | */ |
| 67 | 80 | @Test |
| 68 | 81 | public void testAuth() throws Exception { |
| 69 | - // 1.1 构建请求 | |
| 82 | + // 1.1 构建认证消息 | |
| 70 | 83 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 71 | 84 | IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() |
| 72 | 85 | .setClientId(authInfo.getClientId()) |
| 73 | 86 | .setUsername(authInfo.getUsername()) |
| 74 | 87 | .setPassword(authInfo.getPassword()); |
| 75 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 76 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 77 | - .put("method", "auth") | |
| 78 | - .put("params", authReqDTO) | |
| 79 | - .build()); | |
| 80 | - // 1.2 输出请求 | |
| 81 | - log.info("[testAuth][请求体: {}]", payload); | |
| 88 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | |
| 89 | + // 1.2 编码 | |
| 90 | + byte[] payload = CODEC.encode(request); | |
| 91 | + log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); | |
| 92 | + if (CODEC instanceof IotTcpBinaryDeviceMessageCodec) { | |
| 93 | + log.info("[testAuth][二进制数据包(HEX): {}]", HexUtil.encodeHexStr(payload)); | |
| 94 | + } | |
| 82 | 95 | |
| 83 | 96 | // 2.1 发送请求 |
| 84 | 97 | try (DatagramSocket socket = new DatagramSocket()) { |
| 85 | 98 | socket.setSoTimeout(TIMEOUT_MS); |
| 86 | - String response = sendAndReceive(socket, payload); | |
| 87 | - // 2.2 输出结果 | |
| 88 | - log.info("[testAuth][响应体: {}]", response); | |
| 89 | - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); | |
| 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 | + } | |
| 90 | 108 | } |
| 91 | 109 | } |
| 92 | 110 | |
| ... | ... | @@ -97,27 +115,32 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { |
| 97 | 115 | */ |
| 98 | 116 | @Test |
| 99 | 117 | public void testPropertyPost() throws Exception { |
| 100 | - // 1.1 构建请求(UDP 协议:token 放在 params 中) | |
| 101 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 102 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 103 | - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) | |
| 104 | - .put("version", "1.0") | |
| 105 | - .put("params", withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | |
| 118 | + // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) | |
| 119 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 120 | + IdUtil.fastSimpleUUID(), | |
| 121 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | |
| 122 | + withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | |
| 106 | 123 | .put("power", 100) |
| 107 | 124 | .put("status", "online") |
| 108 | 125 | .put("temperature", 36.5) |
| 109 | - .build()))) | |
| 110 | - .build()); | |
| 111 | - // 1.2 输出请求 | |
| 126 | + .build())), | |
| 127 | + null, null, null); | |
| 128 | + // 1.2 编码 | |
| 129 | + byte[] payload = CODEC.encode(request); | |
| 112 | 130 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); |
| 113 | - log.info("[testPropertyPost][请求体: {}]", payload); | |
| 131 | + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 114 | 132 | |
| 115 | 133 | // 2.1 发送请求 |
| 116 | 134 | try (DatagramSocket socket = new DatagramSocket()) { |
| 117 | 135 | socket.setSoTimeout(TIMEOUT_MS); |
| 118 | - String response = sendAndReceive(socket, payload); | |
| 119 | - // 2.2 输出结果 | |
| 120 | - log.info("[testPropertyPost][响应体: {}]", response); | |
| 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 | + } | |
| 121 | 144 | } |
| 122 | 145 | } |
| 123 | 146 | |
| ... | ... | @@ -128,12 +151,11 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { |
| 128 | 151 | */ |
| 129 | 152 | @Test |
| 130 | 153 | public void testEventPost() throws Exception { |
| 131 | - // 1.1 构建请求(UDP 协议:token 放在 params 中) | |
| 132 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | |
| 133 | - .put("id", IdUtil.fastSimpleUUID()) | |
| 134 | - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) | |
| 135 | - .put("version", "1.0") | |
| 136 | - .put("params", withToken(IotDeviceEventPostReqDTO.of( | |
| 154 | + // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) | |
| 155 | + IotDeviceMessage request = IotDeviceMessage.of( | |
| 156 | + IdUtil.fastSimpleUUID(), | |
| 157 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | |
| 158 | + withToken(IotDeviceEventPostReqDTO.of( | |
| 137 | 159 | "alarm", |
| 138 | 160 | MapUtil.<String, Object>builder() |
| 139 | 161 | .put("level", "warning") |
| ... | ... | @@ -141,18 +163,24 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { |
| 141 | 163 | .put("threshold", 40) |
| 142 | 164 | .put("current", 42) |
| 143 | 165 | .build(), |
| 144 | - System.currentTimeMillis()))) | |
| 145 | - .build()); | |
| 146 | - // 1.2 输出请求 | |
| 166 | + System.currentTimeMillis())), | |
| 167 | + null, null, null); | |
| 168 | + // 1.2 编码 | |
| 169 | + byte[] payload = CODEC.encode(request); | |
| 147 | 170 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); |
| 148 | - log.info("[testEventPost][请求体: {}]", payload); | |
| 171 | + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | |
| 149 | 172 | |
| 150 | 173 | // 2.1 发送请求 |
| 151 | 174 | try (DatagramSocket socket = new DatagramSocket()) { |
| 152 | 175 | socket.setSoTimeout(TIMEOUT_MS); |
| 153 | - String response = sendAndReceive(socket, payload); | |
| 154 | - // 2.2 输出结果 | |
| 155 | - log.info("[testEventPost][响应体: {}]", response); | |
| 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 | + } | |
| 156 | 184 | } |
| 157 | 185 | } |
| 158 | 186 | ... | ... |