Commit 0072482af8faf113b5ed663d15eaae90b5d15dd5
1 parent
610ae6d5
feat(iot): 完善 WebSocket 协议实现,优化代码质量
1. 配置属性校验完善 - CoAP 配置添加 @NotNull 校验注解,替换 TODO 注释 2. WebSocket 协议核心优化 - ConnectionInfo 新增 codecType 字段,支持动态编解码类型 - 上行/下行处理器根据连接的 codecType 进行消息编解码 - 使用 StrUtil.utf8Str/utf8Bytes 替换 StandardCharsets 硬编码 3. 包注释完善 - http/tcp package-info.java 添加规范的包级注释 4. 单元测试重构 - 使用 WebSocketClient.connect() 替换废弃的 HttpClient.webSocket() - 提取公共方法,简化测试代码结构
Showing
10 changed files
with
680 additions
and
610 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -568,25 +568,25 @@ public class IotGatewayProperties { | @@ -568,25 +568,25 @@ public class IotGatewayProperties { | ||
| 568 | /** | 568 | /** |
| 569 | * 服务端口(CoAP 默认端口 5683) | 569 | * 服务端口(CoAP 默认端口 5683) |
| 570 | */ | 570 | */ |
| 571 | - // TODO @AI:默认不为空 | 571 | + @NotNull(message = "服务端口不能为空") |
| 572 | private Integer port = 5683; | 572 | private Integer port = 5683; |
| 573 | 573 | ||
| 574 | /** | 574 | /** |
| 575 | * 最大消息大小(字节) | 575 | * 最大消息大小(字节) |
| 576 | */ | 576 | */ |
| 577 | - // TODO @AI:默认不为空 | 577 | + @NotNull(message = "最大消息大小不能为空") |
| 578 | private Integer maxMessageSize = 1024; | 578 | private Integer maxMessageSize = 1024; |
| 579 | 579 | ||
| 580 | /** | 580 | /** |
| 581 | * ACK 超时时间(毫秒) | 581 | * ACK 超时时间(毫秒) |
| 582 | */ | 582 | */ |
| 583 | - // TODO @AI:默认不为空 | 583 | + @NotNull(message = "ACK 超时时间不能为空") |
| 584 | private Integer ackTimeout = 2000; | 584 | private Integer ackTimeout = 2000; |
| 585 | 585 | ||
| 586 | /** | 586 | /** |
| 587 | * 最大重传次数 | 587 | * 最大重传次数 |
| 588 | */ | 588 | */ |
| 589 | - // TODO @AI:默认不为空 | 589 | + @NotNull(message = "最大重传次数不能为空") |
| 590 | private Integer maxRetransmit = 4; | 590 | private Integer maxRetransmit = 4; |
| 591 | 591 | ||
| 592 | } | 592 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java
| 1 | -// TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java 完善注释; | ||
| 2 | -package cn.iocoder.yudao.module.iot.gateway.protocol.http; | ||
| 3 | \ No newline at end of file | 1 | \ No newline at end of file |
| 2 | +/** | ||
| 3 | + * HTTP 协议实现包 | ||
| 4 | + * <p> | ||
| 5 | + * 提供基于 Vert.x HTTP Server 的 IoT 设备连接和消息处理功能 | ||
| 6 | + */ | ||
| 7 | +package cn.iocoder.yudao.module.iot.gateway.protocol.http; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java
| 1 | -// TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java | ||
| 2 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 3 | \ No newline at end of file | 1 | \ No newline at end of file |
| 2 | +/** | ||
| 3 | + * TCP 协议实现包 | ||
| 4 | + * <p> | ||
| 5 | + * 提供基于 Vert.x TCP Server 的 IoT 设备连接和消息处理功能 | ||
| 6 | + */ | ||
| 7 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java
| @@ -53,6 +53,7 @@ public class IotWebSocketUpstreamProtocol { | @@ -53,6 +53,7 @@ public class IotWebSocketUpstreamProtocol { | ||
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | @PostConstruct | 55 | @PostConstruct |
| 56 | + @SuppressWarnings("deprecation") | ||
| 56 | public void start() { | 57 | public void start() { |
| 57 | // 1.1 创建服务器选项 | 58 | // 1.1 创建服务器选项 |
| 58 | HttpServerOptions options = new HttpServerOptions() | 59 | HttpServerOptions options = new HttpServerOptions() |
| @@ -73,15 +74,14 @@ public class IotWebSocketUpstreamProtocol { | @@ -73,15 +74,14 @@ public class IotWebSocketUpstreamProtocol { | ||
| 73 | httpServer.webSocketHandler(socket -> { | 74 | httpServer.webSocketHandler(socket -> { |
| 74 | // 验证路径 | 75 | // 验证路径 |
| 75 | if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) { | 76 | if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) { |
| 76 | - log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", socket.path(), wsProperties.getPath()); | ||
| 77 | - // TODO @AI:已经被废弃,看看换什么其他方法; | 77 | + log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", |
| 78 | + socket.path(), wsProperties.getPath()); | ||
| 78 | socket.reject(); | 79 | socket.reject(); |
| 79 | return; | 80 | return; |
| 80 | } | 81 | } |
| 81 | - | ||
| 82 | // 创建上行处理器 | 82 | // 创建上行处理器 |
| 83 | - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler( | ||
| 84 | - this, messageService, deviceService, connectionManager); | 83 | + IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, |
| 84 | + messageService, deviceService, connectionManager); | ||
| 85 | handler.handle(socket); | 85 | handler.handle(socket); |
| 86 | }); | 86 | }); |
| 87 | 87 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java
| @@ -139,8 +139,10 @@ public class IotWebSocketConnectionManager { | @@ -139,8 +139,10 @@ public class IotWebSocketConnectionManager { | ||
| 139 | * 客户端 ID | 139 | * 客户端 ID |
| 140 | */ | 140 | */ |
| 141 | private String clientId; | 141 | private String clientId; |
| 142 | - | ||
| 143 | - // TODO @AI:增加有个 codecType 字段;后续可以使用,参考 tcp、udp;然后下行的时候,也基于这个 codeType 去获取; | 142 | + /** |
| 143 | + * 消息编解码类型(认证后确定) | ||
| 144 | + */ | ||
| 145 | + private String codecType; | ||
| 144 | 146 | ||
| 145 | } | 147 | } |
| 146 | 148 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; | ||
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; |
| 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 7 | import lombok.RequiredArgsConstructor; | 7 | import lombok.RequiredArgsConstructor; |
| 8 | import lombok.extern.slf4j.Slf4j; | 8 | import lombok.extern.slf4j.Slf4j; |
| 9 | 9 | ||
| 10 | -import java.nio.charset.StandardCharsets; | ||
| 11 | - | ||
| 12 | /** | 10 | /** |
| 13 | * IoT 网关 WebSocket 下行消息处理器 | 11 | * IoT 网关 WebSocket 下行消息处理器 |
| 14 | * | 12 | * |
| @@ -18,9 +16,6 @@ import java.nio.charset.StandardCharsets; | @@ -18,9 +16,6 @@ import java.nio.charset.StandardCharsets; | ||
| 18 | @RequiredArgsConstructor | 16 | @RequiredArgsConstructor |
| 19 | public class IotWebSocketDownstreamHandler { | 17 | public class IotWebSocketDownstreamHandler { |
| 20 | 18 | ||
| 21 | - // TODO @芋艿:codeType 的处理; | ||
| 22 | - private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE; | ||
| 23 | - | ||
| 24 | private final IotDeviceMessageService deviceMessageService; | 19 | private final IotDeviceMessageService deviceMessageService; |
| 25 | 20 | ||
| 26 | private final IotWebSocketConnectionManager connectionManager; | 21 | private final IotWebSocketConnectionManager connectionManager; |
| @@ -42,8 +37,8 @@ public class IotWebSocketDownstreamHandler { | @@ -42,8 +37,8 @@ public class IotWebSocketDownstreamHandler { | ||
| 42 | } | 37 | } |
| 43 | 38 | ||
| 44 | // 2. 编码消息并发送到设备 | 39 | // 2. 编码消息并发送到设备 |
| 45 | - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, CODEC_TYPE); | ||
| 46 | - String jsonMessage = new String(bytes, StandardCharsets.UTF_8); | 40 | + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); |
| 41 | + String jsonMessage = StrUtil.utf8Str(bytes); | ||
| 47 | boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); | 42 | boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); |
| 48 | if (success) { | 43 | if (success) { |
| 49 | log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | 44 | log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java
| @@ -25,7 +25,6 @@ import io.vertx.core.Handler; | @@ -25,7 +25,6 @@ import io.vertx.core.Handler; | ||
| 25 | import io.vertx.core.http.ServerWebSocket; | 25 | import io.vertx.core.http.ServerWebSocket; |
| 26 | import lombok.extern.slf4j.Slf4j; | 26 | import lombok.extern.slf4j.Slf4j; |
| 27 | 27 | ||
| 28 | -import java.nio.charset.StandardCharsets; | ||
| 29 | 28 | ||
| 30 | /** | 29 | /** |
| 31 | * WebSocket 上行消息处理器 | 30 | * WebSocket 上行消息处理器 |
| @@ -35,7 +34,9 @@ import java.nio.charset.StandardCharsets; | @@ -35,7 +34,9 @@ import java.nio.charset.StandardCharsets; | ||
| 35 | @Slf4j | 34 | @Slf4j |
| 36 | public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | 35 | public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 37 | 36 | ||
| 38 | - // TODO @芋艿:codeType 的处理; | 37 | + /** |
| 38 | + * 默认消息编解码类型 | ||
| 39 | + */ | ||
| 39 | private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE; | 40 | private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE; |
| 40 | 41 | ||
| 41 | private static final String AUTH_METHOD = "auth"; | 42 | private static final String AUTH_METHOD = "auth"; |
| @@ -63,13 +64,10 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -63,13 +64,10 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 63 | 64 | ||
| 64 | @Override | 65 | @Override |
| 65 | public void handle(ServerWebSocket socket) { | 66 | public void handle(ServerWebSocket socket) { |
| 66 | - // 1. 接受 WebSocket 连接 | ||
| 67 | String clientId = IdUtil.simpleUUID(); | 67 | String clientId = IdUtil.simpleUUID(); |
| 68 | log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | 68 | log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); |
| 69 | - // TODO @AI:这个方法已经废弃,看看有没其他替换的 | ||
| 70 | - socket.accept(); | ||
| 71 | 69 | ||
| 72 | - // 2.1 设置异常和关闭处理器 | 70 | + // 1. 设置异常和关闭处理器 |
| 73 | socket.exceptionHandler(ex -> { | 71 | socket.exceptionHandler(ex -> { |
| 74 | log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | 72 | log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); |
| 75 | cleanupConnection(socket); | 73 | cleanupConnection(socket); |
| @@ -79,7 +77,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -79,7 +77,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 79 | cleanupConnection(socket); | 77 | cleanupConnection(socket); |
| 80 | }); | 78 | }); |
| 81 | 79 | ||
| 82 | - // 2.2 设置文本消息处理器 | 80 | + // 2. 设置文本消息处理器 |
| 83 | socket.textMessageHandler(message -> { | 81 | socket.textMessageHandler(message -> { |
| 84 | try { | 82 | try { |
| 85 | processMessage(clientId, message, socket); | 83 | processMessage(clientId, message, socket); |
| @@ -105,12 +103,13 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -105,12 +103,13 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 105 | if (StrUtil.isBlank(message)) { | 103 | if (StrUtil.isBlank(message)) { |
| 106 | return; | 104 | return; |
| 107 | } | 105 | } |
| 108 | - // 1.2 解码消息 | ||
| 109 | - // TODO @AI:应该只有初始使用 CODEC_TYPE 解析,后续基于 | 106 | + // 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE) |
| 107 | + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); | ||
| 108 | + String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE; | ||
| 110 | IotDeviceMessage deviceMessage; | 109 | IotDeviceMessage deviceMessage; |
| 111 | try { | 110 | try { |
| 112 | deviceMessage = deviceMessageService.decodeDeviceMessage( | 111 | deviceMessage = deviceMessageService.decodeDeviceMessage( |
| 113 | - message.getBytes(StandardCharsets.UTF_8), CODEC_TYPE); | 112 | + StrUtil.utf8Bytes(message), codecType); |
| 114 | if (deviceMessage == null) { | 113 | if (deviceMessage == null) { |
| 115 | throw new Exception("解码后消息为空"); | 114 | throw new Exception("解码后消息为空"); |
| 116 | } | 115 | } |
| @@ -269,7 +268,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -269,7 +268,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 269 | .setDeviceId(device.getId()) | 268 | .setDeviceId(device.getId()) |
| 270 | .setProductKey(device.getProductKey()) | 269 | .setProductKey(device.getProductKey()) |
| 271 | .setDeviceName(device.getDeviceName()) | 270 | .setDeviceName(device.getDeviceName()) |
| 272 | - .setClientId(clientId); | 271 | + .setClientId(clientId) |
| 272 | + .setCodecType(CODEC_TYPE); | ||
| 273 | // 注册连接 | 273 | // 注册连接 |
| 274 | connectionManager.registerConnection(socket, device.getId(), connectionInfo); | 274 | connectionManager.registerConnection(socket, device.getId(), connectionInfo); |
| 275 | } | 275 | } |
| @@ -330,7 +330,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -330,7 +330,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 330 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); | 330 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); |
| 331 | 331 | ||
| 332 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); | 332 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); |
| 333 | - socket.writeTextMessage(new String(encodedData, StandardCharsets.UTF_8)); | 333 | + socket.writeTextMessage(StrUtil.utf8Str(encodedData)); |
| 334 | } catch (Exception e) { | 334 | } catch (Exception e) { |
| 335 | log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); | 335 | log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); |
| 336 | } | 336 | } |
| @@ -472,7 +472,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -472,7 +472,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 472 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | 472 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); |
| 473 | // 2. 发送响应 | 473 | // 2. 发送响应 |
| 474 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); | 474 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); |
| 475 | - socket.writeTextMessage(new String(encodedData, StandardCharsets.UTF_8)); | 475 | + socket.writeTextMessage(StrUtil.utf8Str(encodedData)); |
| 476 | } catch (Exception e) { | 476 | } catch (Exception e) { |
| 477 | log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); | 477 | log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); |
| 478 | } | 478 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -12,15 +13,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | @@ -12,15 +13,14 @@ 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.IotDeviceMessageCodec; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; | 14 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; |
| 14 | import io.vertx.core.Vertx; | 15 | import io.vertx.core.Vertx; |
| 15 | -import io.vertx.core.http.HttpClient; | ||
| 16 | import io.vertx.core.http.WebSocket; | 16 | import io.vertx.core.http.WebSocket; |
| 17 | +import io.vertx.core.http.WebSocketClient; | ||
| 17 | import io.vertx.core.http.WebSocketConnectOptions; | 18 | import io.vertx.core.http.WebSocketConnectOptions; |
| 18 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
| 19 | import org.junit.jupiter.api.AfterAll; | 20 | import org.junit.jupiter.api.AfterAll; |
| 20 | import org.junit.jupiter.api.BeforeAll; | 21 | import org.junit.jupiter.api.BeforeAll; |
| 21 | import org.junit.jupiter.api.Test; | 22 | import org.junit.jupiter.api.Test; |
| 22 | 23 | ||
| 23 | -import java.nio.charset.StandardCharsets; | ||
| 24 | import java.util.concurrent.CountDownLatch; | 24 | import java.util.concurrent.CountDownLatch; |
| 25 | import java.util.concurrent.TimeUnit; | 25 | import java.util.concurrent.TimeUnit; |
| 26 | import java.util.concurrent.atomic.AtomicReference; | 26 | import java.util.concurrent.atomic.AtomicReference; |
| @@ -81,119 +81,83 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | @@ -81,119 +81,83 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | ||
| 81 | 81 | ||
| 82 | // ===================== 认证测试 ===================== | 82 | // ===================== 认证测试 ===================== |
| 83 | 83 | ||
| 84 | - // TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java 或 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java 类,优化代码结构 | ||
| 85 | - | ||
| 86 | /** | 84 | /** |
| 87 | * 认证测试:获取设备 Token | 85 | * 认证测试:获取设备 Token |
| 88 | */ | 86 | */ |
| 89 | @Test | 87 | @Test |
| 90 | public void testAuth() throws Exception { | 88 | public void testAuth() throws Exception { |
| 91 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 92 | - AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 93 | - | ||
| 94 | - // 1. 创建 WebSocket 连接 | ||
| 95 | - HttpClient client = vertx.createHttpClient(); | ||
| 96 | - WebSocketConnectOptions options = new WebSocketConnectOptions() | ||
| 97 | - .setHost(SERVER_HOST) | ||
| 98 | - .setPort(SERVER_PORT) | ||
| 99 | - .setURI(WS_PATH); | ||
| 100 | - | ||
| 101 | - // TODO @AI:这里有告警;Deprecate /instead use WebSocketClient.connect(WebSocketConnectOptions) | ||
| 102 | - client.webSocket(options).onComplete(ar -> { | ||
| 103 | - if (ar.succeeded()) { | ||
| 104 | - WebSocket ws = ar.result(); | ||
| 105 | - log.info("[testAuth][WebSocket 连接成功]"); | ||
| 106 | - | ||
| 107 | - // 设置消息处理器 | ||
| 108 | - ws.textMessageHandler(message -> { | ||
| 109 | - log.info("[testAuth][收到响应: {}]", message); | ||
| 110 | - responseRef.set(message); | ||
| 111 | - ws.close(); | ||
| 112 | - latch.countDown(); | ||
| 113 | - }); | ||
| 114 | - | ||
| 115 | - // 2. 构建认证消息 | ||
| 116 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 117 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 118 | - .setClientId(authInfo.getClientId()) | ||
| 119 | - .setUsername(authInfo.getUsername()) | ||
| 120 | - .setPassword(authInfo.getPassword()); | ||
| 121 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 122 | - | ||
| 123 | - // 3. 编码并发送 | ||
| 124 | - byte[] payload = CODEC.encode(request); | ||
| 125 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 126 | - log.info("[testAuth][发送认证请求: {}]", jsonMessage); | ||
| 127 | - ws.writeTextMessage(jsonMessage); | ||
| 128 | - } else { | ||
| 129 | - log.error("[testAuth][WebSocket 连接失败]", ar.cause()); | ||
| 130 | - latch.countDown(); | ||
| 131 | - } | ||
| 132 | - }); | ||
| 133 | - | ||
| 134 | - // 4. 等待响应 | ||
| 135 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 136 | - if (completed && responseRef.get() != null) { | ||
| 137 | - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 138 | - log.info("[testAuth][解码响应: {}]", response); | 89 | + // 1.1 构建认证消息 |
| 90 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 91 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 92 | + .setClientId(authInfo.getClientId()) | ||
| 93 | + .setUsername(authInfo.getUsername()) | ||
| 94 | + .setPassword(authInfo.getPassword()); | ||
| 95 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 96 | + // 1.2 编码 | ||
| 97 | + byte[] payload = CODEC.encode(request); | ||
| 98 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 99 | + log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 100 | + | ||
| 101 | + // 2.1 创建 WebSocket 连接(同步) | ||
| 102 | + WebSocket ws = createWebSocketConnection(); | ||
| 103 | + log.info("[testAuth][WebSocket 连接成功]"); | ||
| 104 | + | ||
| 105 | + // 2.2 发送并等待响应 | ||
| 106 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 107 | + | ||
| 108 | + // 3. 解码响应 | ||
| 109 | + if (response != null) { | ||
| 110 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 111 | + log.info("[testAuth][响应消息: {}]", responseMessage); | ||
| 139 | } else { | 112 | } else { |
| 140 | - log.warn("[testAuth][测试超时或未收到响应]"); | 113 | + log.warn("[testAuth][未收到响应]"); |
| 141 | } | 114 | } |
| 115 | + | ||
| 116 | + // 4. 关闭连接 | ||
| 117 | + ws.close(); | ||
| 142 | } | 118 | } |
| 143 | 119 | ||
| 144 | // ===================== 动态注册测试 ===================== | 120 | // ===================== 动态注册测试 ===================== |
| 145 | 121 | ||
| 146 | /** | 122 | /** |
| 147 | * 直连设备动态注册测试(一型一密) | 123 | * 直连设备动态注册测试(一型一密) |
| 124 | + * <p> | ||
| 125 | + * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) | ||
| 126 | + * <p> | ||
| 127 | + * 注意:此接口不需要认证 | ||
| 148 | */ | 128 | */ |
| 149 | @Test | 129 | @Test |
| 150 | public void testDeviceRegister() throws Exception { | 130 | public void testDeviceRegister() throws Exception { |
| 151 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 152 | - AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 153 | - | ||
| 154 | - HttpClient client = vertx.createHttpClient(); | ||
| 155 | - WebSocketConnectOptions options = new WebSocketConnectOptions() | ||
| 156 | - .setHost(SERVER_HOST) | ||
| 157 | - .setPort(SERVER_PORT) | ||
| 158 | - .setURI(WS_PATH); | ||
| 159 | - | ||
| 160 | - client.webSocket(options).onComplete(ar -> { | ||
| 161 | - if (ar.succeeded()) { | ||
| 162 | - WebSocket ws = ar.result(); | ||
| 163 | - log.info("[testDeviceRegister][WebSocket 连接成功]"); | ||
| 164 | - | ||
| 165 | - ws.textMessageHandler(message -> { | ||
| 166 | - log.info("[testDeviceRegister][收到响应: {}]", message); | ||
| 167 | - responseRef.set(message); | ||
| 168 | - ws.close(); | ||
| 169 | - latch.countDown(); | ||
| 170 | - }); | ||
| 171 | - | ||
| 172 | - // 构建注册消息 | ||
| 173 | - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 174 | - registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 175 | - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); | ||
| 176 | - registerReqDTO.setProductSecret("test-product-secret"); | ||
| 177 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | ||
| 178 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | ||
| 179 | - | ||
| 180 | - byte[] payload = CODEC.encode(request); | ||
| 181 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 182 | - log.info("[testDeviceRegister][发送注册请求: {}]", jsonMessage); | ||
| 183 | - ws.writeTextMessage(jsonMessage); | ||
| 184 | - } else { | ||
| 185 | - log.error("[testDeviceRegister][WebSocket 连接失败]", ar.cause()); | ||
| 186 | - latch.countDown(); | ||
| 187 | - } | ||
| 188 | - }); | ||
| 189 | - | ||
| 190 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 191 | - if (completed && responseRef.get() != null) { | ||
| 192 | - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 193 | - log.info("[testDeviceRegister][解码响应: {}]", response); | 131 | + // 1.1 构建注册消息 |
| 132 | + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 133 | + registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 134 | + registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); | ||
| 135 | + registerReqDTO.setProductSecret("test-product-secret"); | ||
| 136 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | ||
| 137 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | ||
| 138 | + // 1.2 编码 | ||
| 139 | + byte[] payload = CODEC.encode(request); | ||
| 140 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 141 | + log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 142 | + | ||
| 143 | + // 2.1 创建 WebSocket 连接(同步) | ||
| 144 | + WebSocket ws = createWebSocketConnection(); | ||
| 145 | + log.info("[testDeviceRegister][WebSocket 连接成功]"); | ||
| 146 | + | ||
| 147 | + // 2.2 发送并等待响应 | ||
| 148 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 149 | + | ||
| 150 | + // 3. 解码响应 | ||
| 151 | + if (response != null) { | ||
| 152 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 153 | + log.info("[testDeviceRegister][响应消息: {}]", responseMessage); | ||
| 154 | + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 194 | } else { | 155 | } else { |
| 195 | - log.warn("[testDeviceRegister][测试超时或未收到响应]"); | 156 | + log.warn("[testDeviceRegister][未收到响应]"); |
| 196 | } | 157 | } |
| 158 | + | ||
| 159 | + // 4. 关闭连接 | ||
| 160 | + ws.close(); | ||
| 197 | } | 161 | } |
| 198 | 162 | ||
| 199 | // ===================== 直连设备属性上报测试 ===================== | 163 | // ===================== 直连设备属性上报测试 ===================== |
| @@ -203,82 +167,40 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | @@ -203,82 +167,40 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | ||
| 203 | */ | 167 | */ |
| 204 | @Test | 168 | @Test |
| 205 | public void testPropertyPost() throws Exception { | 169 | public void testPropertyPost() throws Exception { |
| 206 | - CountDownLatch latch = new CountDownLatch(2); // 认证 + 属性上报 | ||
| 207 | - AtomicReference<String> authResponseRef = new AtomicReference<>(); | ||
| 208 | - AtomicReference<String> propertyResponseRef = new AtomicReference<>(); | ||
| 209 | - | ||
| 210 | - HttpClient client = vertx.createHttpClient(); | ||
| 211 | - WebSocketConnectOptions options = new WebSocketConnectOptions() | ||
| 212 | - .setHost(SERVER_HOST) | ||
| 213 | - .setPort(SERVER_PORT) | ||
| 214 | - .setURI(WS_PATH); | ||
| 215 | - | ||
| 216 | - client.webSocket(options).onComplete(ar -> { | ||
| 217 | - if (ar.succeeded()) { | ||
| 218 | - WebSocket ws = ar.result(); | ||
| 219 | - log.info("[testPropertyPost][WebSocket 连接成功]"); | ||
| 220 | - | ||
| 221 | - final boolean[] authenticated = {false}; | ||
| 222 | - | ||
| 223 | - ws.textMessageHandler(message -> { | ||
| 224 | - log.info("[testPropertyPost][收到响应: {}]", message); | ||
| 225 | - if (!authenticated[0]) { | ||
| 226 | - authResponseRef.set(message); | ||
| 227 | - authenticated[0] = true; | ||
| 228 | - latch.countDown(); | ||
| 229 | - | ||
| 230 | - // 认证成功后发送属性上报 | ||
| 231 | - IotDeviceMessage propertyRequest = IotDeviceMessage.of( | ||
| 232 | - IdUtil.fastSimpleUUID(), | ||
| 233 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 234 | - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 235 | - .put("width", 1) | ||
| 236 | - .put("height", "2") | ||
| 237 | - .build()), | ||
| 238 | - null, null, null); | ||
| 239 | - byte[] payload = CODEC.encode(propertyRequest); | ||
| 240 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 241 | - log.info("[testPropertyPost][发送属性上报请求: {}]", jsonMessage); | ||
| 242 | - ws.writeTextMessage(jsonMessage); | ||
| 243 | - } else { | ||
| 244 | - propertyResponseRef.set(message); | ||
| 245 | - ws.close(); | ||
| 246 | - latch.countDown(); | ||
| 247 | - } | ||
| 248 | - }); | ||
| 249 | - | ||
| 250 | - // 先发送认证请求 | ||
| 251 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 252 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 253 | - .setClientId(authInfo.getClientId()) | ||
| 254 | - .setUsername(authInfo.getUsername()) | ||
| 255 | - .setPassword(authInfo.getPassword()); | ||
| 256 | - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 257 | - | ||
| 258 | - byte[] payload = CODEC.encode(authRequest); | ||
| 259 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 260 | - log.info("[testPropertyPost][发送认证请求: {}]", jsonMessage); | ||
| 261 | - ws.writeTextMessage(jsonMessage); | ||
| 262 | - } else { | ||
| 263 | - log.error("[testPropertyPost][WebSocket 连接失败]", ar.cause()); | ||
| 264 | - latch.countDown(); | ||
| 265 | - latch.countDown(); | ||
| 266 | - } | ||
| 267 | - }); | ||
| 268 | - | ||
| 269 | - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS); | ||
| 270 | - if (completed) { | ||
| 271 | - if (authResponseRef.get() != null) { | ||
| 272 | - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 273 | - log.info("[testPropertyPost][认证响应: {}]", authResponse); | ||
| 274 | - } | ||
| 275 | - if (propertyResponseRef.get() != null) { | ||
| 276 | - IotDeviceMessage propertyResponse = CODEC.decode(propertyResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 277 | - log.info("[testPropertyPost][属性上报响应: {}]", propertyResponse); | ||
| 278 | - } | 170 | + // 1.1 创建 WebSocket 连接(同步) |
| 171 | + WebSocket ws = createWebSocketConnection(); | ||
| 172 | + log.info("[testPropertyPost][WebSocket 连接成功]"); | ||
| 173 | + | ||
| 174 | + // 1.2 先进行认证 | ||
| 175 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 176 | + log.info("[testPropertyPost][认证响应: {}]", authResponse); | ||
| 177 | + | ||
| 178 | + // 2.1 构建属性上报消息 | ||
| 179 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 180 | + IdUtil.fastSimpleUUID(), | ||
| 181 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 182 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 183 | + .put("width", 1) | ||
| 184 | + .put("height", "2") | ||
| 185 | + .build()), | ||
| 186 | + null, null, null); | ||
| 187 | + // 2.2 编码 | ||
| 188 | + byte[] payload = CODEC.encode(request); | ||
| 189 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 190 | + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 191 | + | ||
| 192 | + // 3.1 发送并等待响应 | ||
| 193 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 194 | + // 3.2 解码响应 | ||
| 195 | + if (response != null) { | ||
| 196 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 197 | + log.info("[testPropertyPost][响应消息: {}]", responseMessage); | ||
| 279 | } else { | 198 | } else { |
| 280 | - log.warn("[testPropertyPost][测试超时]"); | 199 | + log.warn("[testPropertyPost][未收到响应]"); |
| 281 | } | 200 | } |
| 201 | + | ||
| 202 | + // 4. 关闭连接 | ||
| 203 | + ws.close(); | ||
| 282 | } | 204 | } |
| 283 | 205 | ||
| 284 | // ===================== 直连设备事件上报测试 ===================== | 206 | // ===================== 直连设备事件上报测试 ===================== |
| @@ -288,82 +210,111 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | @@ -288,82 +210,111 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | ||
| 288 | */ | 210 | */ |
| 289 | @Test | 211 | @Test |
| 290 | public void testEventPost() throws Exception { | 212 | public void testEventPost() throws Exception { |
| 291 | - CountDownLatch latch = new CountDownLatch(2); // 认证 + 事件上报 | ||
| 292 | - AtomicReference<String> authResponseRef = new AtomicReference<>(); | ||
| 293 | - AtomicReference<String> eventResponseRef = new AtomicReference<>(); | 213 | + // 1.1 创建 WebSocket 连接(同步) |
| 214 | + WebSocket ws = createWebSocketConnection(); | ||
| 215 | + log.info("[testEventPost][WebSocket 连接成功]"); | ||
| 216 | + | ||
| 217 | + // 1.2 先进行认证 | ||
| 218 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 219 | + log.info("[testEventPost][认证响应: {}]", authResponse); | ||
| 220 | + | ||
| 221 | + // 2.1 构建事件上报消息 | ||
| 222 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 223 | + IdUtil.fastSimpleUUID(), | ||
| 224 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 225 | + IotDeviceEventPostReqDTO.of( | ||
| 226 | + "eat", | ||
| 227 | + MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 228 | + System.currentTimeMillis()), | ||
| 229 | + null, null, null); | ||
| 230 | + // 2.2 编码 | ||
| 231 | + byte[] payload = CODEC.encode(request); | ||
| 232 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 233 | + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 234 | + | ||
| 235 | + // 3.1 发送并等待响应 | ||
| 236 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 237 | + // 3.2 解码响应 | ||
| 238 | + if (response != null) { | ||
| 239 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 240 | + log.info("[testEventPost][响应消息: {}]", responseMessage); | ||
| 241 | + } else { | ||
| 242 | + log.warn("[testEventPost][未收到响应]"); | ||
| 243 | + } | ||
| 244 | + | ||
| 245 | + // 4. 关闭连接 | ||
| 246 | + ws.close(); | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + // ===================== 辅助方法 ===================== | ||
| 294 | 250 | ||
| 295 | - HttpClient client = vertx.createHttpClient(); | 251 | + /** |
| 252 | + * 创建 WebSocket 连接(同步) | ||
| 253 | + * | ||
| 254 | + * @return WebSocket 连接 | ||
| 255 | + */ | ||
| 256 | + private WebSocket createWebSocketConnection() throws Exception { | ||
| 257 | + WebSocketClient wsClient = vertx.createWebSocketClient(); | ||
| 296 | WebSocketConnectOptions options = new WebSocketConnectOptions() | 258 | WebSocketConnectOptions options = new WebSocketConnectOptions() |
| 297 | .setHost(SERVER_HOST) | 259 | .setHost(SERVER_HOST) |
| 298 | .setPort(SERVER_PORT) | 260 | .setPort(SERVER_PORT) |
| 299 | .setURI(WS_PATH); | 261 | .setURI(WS_PATH); |
| 262 | + return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 263 | + } | ||
| 300 | 264 | ||
| 301 | - client.webSocket(options).onComplete(ar -> { | ||
| 302 | - if (ar.succeeded()) { | ||
| 303 | - WebSocket ws = ar.result(); | ||
| 304 | - log.info("[testEventPost][WebSocket 连接成功]"); | ||
| 305 | - | ||
| 306 | - final boolean[] authenticated = {false}; | ||
| 307 | - | ||
| 308 | - ws.textMessageHandler(message -> { | ||
| 309 | - log.info("[testEventPost][收到响应: {}]", message); | ||
| 310 | - if (!authenticated[0]) { | ||
| 311 | - authResponseRef.set(message); | ||
| 312 | - authenticated[0] = true; | ||
| 313 | - latch.countDown(); | ||
| 314 | - | ||
| 315 | - // 认证成功后发送事件上报 | ||
| 316 | - IotDeviceMessage eventRequest = IotDeviceMessage.of( | ||
| 317 | - IdUtil.fastSimpleUUID(), | ||
| 318 | - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 319 | - IotDeviceEventPostReqDTO.of( | ||
| 320 | - "eat", | ||
| 321 | - MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 322 | - System.currentTimeMillis()), | ||
| 323 | - null, null, null); | ||
| 324 | - byte[] payload = CODEC.encode(eventRequest); | ||
| 325 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 326 | - log.info("[testEventPost][发送事件上报请求: {}]", jsonMessage); | ||
| 327 | - ws.writeTextMessage(jsonMessage); | ||
| 328 | - } else { | ||
| 329 | - eventResponseRef.set(message); | ||
| 330 | - ws.close(); | ||
| 331 | - latch.countDown(); | ||
| 332 | - } | ||
| 333 | - }); | ||
| 334 | - | ||
| 335 | - // 先发送认证请求 | ||
| 336 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 337 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 338 | - .setClientId(authInfo.getClientId()) | ||
| 339 | - .setUsername(authInfo.getUsername()) | ||
| 340 | - .setPassword(authInfo.getPassword()); | ||
| 341 | - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 342 | - | ||
| 343 | - byte[] payload = CODEC.encode(authRequest); | ||
| 344 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 345 | - log.info("[testEventPost][发送认证请求: {}]", jsonMessage); | ||
| 346 | - ws.writeTextMessage(jsonMessage); | ||
| 347 | - } else { | ||
| 348 | - log.error("[testEventPost][WebSocket 连接失败]", ar.cause()); | ||
| 349 | - latch.countDown(); | ||
| 350 | - latch.countDown(); | ||
| 351 | - } | 265 | + /** |
| 266 | + * 发送消息并等待响应(同步) | ||
| 267 | + * | ||
| 268 | + * @param ws WebSocket 连接 | ||
| 269 | + * @param message 请求消息 | ||
| 270 | + * @return 响应消息 | ||
| 271 | + */ | ||
| 272 | + public static String sendAndReceive(WebSocket ws, String message) throws Exception { | ||
| 273 | + CountDownLatch latch = new CountDownLatch(1); | ||
| 274 | + AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 275 | + | ||
| 276 | + // 设置消息处理器 | ||
| 277 | + ws.textMessageHandler(response -> { | ||
| 278 | + log.info("[sendAndReceive][收到响应: {}]", response); | ||
| 279 | + responseRef.set(response); | ||
| 280 | + latch.countDown(); | ||
| 352 | }); | 281 | }); |
| 353 | 282 | ||
| 354 | - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS); | ||
| 355 | - if (completed) { | ||
| 356 | - if (authResponseRef.get() != null) { | ||
| 357 | - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 358 | - log.info("[testEventPost][认证响应: {}]", authResponse); | ||
| 359 | - } | ||
| 360 | - if (eventResponseRef.get() != null) { | ||
| 361 | - IotDeviceMessage eventResponse = CODEC.decode(eventResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 362 | - log.info("[testEventPost][事件上报响应: {}]", eventResponse); | ||
| 363 | - } | ||
| 364 | - } else { | ||
| 365 | - log.warn("[testEventPost][测试超时]"); | 283 | + // 发送请求 |
| 284 | + log.info("[sendAndReceive][发送请求: {}]", message); | ||
| 285 | + ws.writeTextMessage(message); | ||
| 286 | + | ||
| 287 | + // 等待响应 | ||
| 288 | + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 289 | + if (!completed) { | ||
| 290 | + log.warn("[sendAndReceive][等待响应超时]"); | ||
| 291 | + } | ||
| 292 | + return responseRef.get(); | ||
| 293 | + } | ||
| 294 | + | ||
| 295 | + /** | ||
| 296 | + * 执行设备认证(同步) | ||
| 297 | + * | ||
| 298 | + * @param ws WebSocket 连接 | ||
| 299 | + * @return 认证响应消息 | ||
| 300 | + */ | ||
| 301 | + private IotDeviceMessage authenticate(WebSocket ws) throws Exception { | ||
| 302 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 303 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 304 | + .setClientId(authInfo.getClientId()) | ||
| 305 | + .setUsername(authInfo.getUsername()) | ||
| 306 | + .setPassword(authInfo.getPassword()); | ||
| 307 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 308 | + | ||
| 309 | + byte[] payload = CODEC.encode(request); | ||
| 310 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 311 | + log.info("[authenticate][发送认证请求: {}]", jsonMessage); | ||
| 312 | + | ||
| 313 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 314 | + if (response != null) { | ||
| 315 | + return CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 366 | } | 316 | } |
| 317 | + return null; | ||
| 367 | } | 318 | } |
| 368 | 319 | ||
| 369 | } | 320 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -15,15 +16,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | @@ -15,15 +16,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 15 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | 16 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 16 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; | 17 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; |
| 17 | import io.vertx.core.Vertx; | 18 | import io.vertx.core.Vertx; |
| 18 | -import io.vertx.core.http.HttpClient; | ||
| 19 | import io.vertx.core.http.WebSocket; | 19 | import io.vertx.core.http.WebSocket; |
| 20 | +import io.vertx.core.http.WebSocketClient; | ||
| 20 | import io.vertx.core.http.WebSocketConnectOptions; | 21 | import io.vertx.core.http.WebSocketConnectOptions; |
| 21 | import lombok.extern.slf4j.Slf4j; | 22 | import lombok.extern.slf4j.Slf4j; |
| 22 | import org.junit.jupiter.api.AfterAll; | 23 | import org.junit.jupiter.api.AfterAll; |
| 23 | import org.junit.jupiter.api.BeforeAll; | 24 | import org.junit.jupiter.api.BeforeAll; |
| 24 | import org.junit.jupiter.api.Test; | 25 | import org.junit.jupiter.api.Test; |
| 25 | 26 | ||
| 26 | -import java.nio.charset.StandardCharsets; | ||
| 27 | import java.util.Collections; | 27 | import java.util.Collections; |
| 28 | import java.util.List; | 28 | import java.util.List; |
| 29 | import java.util.Map; | 29 | import java.util.Map; |
| @@ -100,53 +100,36 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | @@ -100,53 +100,36 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | ||
| 100 | */ | 100 | */ |
| 101 | @Test | 101 | @Test |
| 102 | public void testAuth() throws Exception { | 102 | public void testAuth() throws Exception { |
| 103 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 104 | - AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 105 | - | ||
| 106 | - HttpClient client = vertx.createHttpClient(); | ||
| 107 | - WebSocketConnectOptions options = new WebSocketConnectOptions() | ||
| 108 | - .setHost(SERVER_HOST) | ||
| 109 | - .setPort(SERVER_PORT) | ||
| 110 | - .setURI(WS_PATH); | ||
| 111 | - | ||
| 112 | - client.webSocket(options).onComplete(ar -> { | ||
| 113 | - if (ar.succeeded()) { | ||
| 114 | - WebSocket ws = ar.result(); | ||
| 115 | - log.info("[testAuth][WebSocket 连接成功]"); | ||
| 116 | - | ||
| 117 | - ws.textMessageHandler(message -> { | ||
| 118 | - log.info("[testAuth][收到响应: {}]", message); | ||
| 119 | - responseRef.set(message); | ||
| 120 | - ws.close(); | ||
| 121 | - latch.countDown(); | ||
| 122 | - }); | ||
| 123 | - | ||
| 124 | - // 构建认证消息 | ||
| 125 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 126 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | ||
| 127 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 128 | - .setClientId(authInfo.getClientId()) | ||
| 129 | - .setUsername(authInfo.getUsername()) | ||
| 130 | - .setPassword(authInfo.getPassword()); | ||
| 131 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 132 | - | ||
| 133 | - byte[] payload = CODEC.encode(request); | ||
| 134 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 135 | - log.info("[testAuth][发送认证请求: {}]", jsonMessage); | ||
| 136 | - ws.writeTextMessage(jsonMessage); | ||
| 137 | - } else { | ||
| 138 | - log.error("[testAuth][WebSocket 连接失败]", ar.cause()); | ||
| 139 | - latch.countDown(); | ||
| 140 | - } | ||
| 141 | - }); | ||
| 142 | - | ||
| 143 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 144 | - if (completed && responseRef.get() != null) { | ||
| 145 | - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 146 | - log.info("[testAuth][解码响应: {}]", response); | 103 | + // 1.1 构建认证消息 |
| 104 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 105 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | ||
| 106 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 107 | + .setClientId(authInfo.getClientId()) | ||
| 108 | + .setUsername(authInfo.getUsername()) | ||
| 109 | + .setPassword(authInfo.getPassword()); | ||
| 110 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 111 | + // 1.2 编码 | ||
| 112 | + byte[] payload = CODEC.encode(request); | ||
| 113 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 114 | + log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 115 | + | ||
| 116 | + // 2.1 创建 WebSocket 连接(同步) | ||
| 117 | + WebSocket ws = createWebSocketConnection(); | ||
| 118 | + log.info("[testAuth][WebSocket 连接成功]"); | ||
| 119 | + | ||
| 120 | + // 2.2 发送并等待响应 | ||
| 121 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 122 | + | ||
| 123 | + // 3. 解码响应 | ||
| 124 | + if (response != null) { | ||
| 125 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 126 | + log.info("[testAuth][响应消息: {}]", responseMessage); | ||
| 147 | } else { | 127 | } else { |
| 148 | - log.warn("[testAuth][测试超时或未收到响应]"); | 128 | + log.warn("[testAuth][未收到响应]"); |
| 149 | } | 129 | } |
| 130 | + | ||
| 131 | + // 4. 关闭连接 | ||
| 132 | + ws.close(); | ||
| 150 | } | 133 | } |
| 151 | 134 | ||
| 152 | // ===================== 拓扑管理测试 ===================== | 135 | // ===================== 拓扑管理测试 ===================== |
| @@ -156,23 +139,46 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | @@ -156,23 +139,46 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | ||
| 156 | */ | 139 | */ |
| 157 | @Test | 140 | @Test |
| 158 | public void testTopoAdd() throws Exception { | 141 | public void testTopoAdd() throws Exception { |
| 159 | - executeAuthenticatedRequest("testTopoAdd", ws -> { | ||
| 160 | - // 构建子设备认证信息 | ||
| 161 | - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 162 | - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | ||
| 163 | - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | ||
| 164 | - .setClientId(subAuthInfo.getClientId()) | ||
| 165 | - .setUsername(subAuthInfo.getUsername()) | ||
| 166 | - .setPassword(subAuthInfo.getPassword()); | ||
| 167 | - // 构建请求参数 | ||
| 168 | - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); | ||
| 169 | - params.setSubDevices(Collections.singletonList(subDeviceAuth)); | ||
| 170 | - return IotDeviceMessage.of( | ||
| 171 | - IdUtil.fastSimpleUUID(), | ||
| 172 | - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 173 | - params, | ||
| 174 | - null, null, null); | ||
| 175 | - }); | 142 | + // 1.1 创建 WebSocket 连接(同步) |
| 143 | + WebSocket ws = createWebSocketConnection(); | ||
| 144 | + log.info("[testTopoAdd][WebSocket 连接成功]"); | ||
| 145 | + | ||
| 146 | + // 1.2 先进行认证 | ||
| 147 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 148 | + log.info("[testTopoAdd][认证响应: {}]", authResponse); | ||
| 149 | + | ||
| 150 | + // 2.1 构建子设备认证信息 | ||
| 151 | + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 152 | + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | ||
| 153 | + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | ||
| 154 | + .setClientId(subAuthInfo.getClientId()) | ||
| 155 | + .setUsername(subAuthInfo.getUsername()) | ||
| 156 | + .setPassword(subAuthInfo.getPassword()); | ||
| 157 | + // 2.2 构建请求参数 | ||
| 158 | + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); | ||
| 159 | + params.setSubDevices(Collections.singletonList(subDeviceAuth)); | ||
| 160 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 161 | + IdUtil.fastSimpleUUID(), | ||
| 162 | + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 163 | + params, | ||
| 164 | + null, null, null); | ||
| 165 | + // 2.3 编码 | ||
| 166 | + byte[] payload = CODEC.encode(request); | ||
| 167 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 168 | + log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 169 | + | ||
| 170 | + // 3.1 发送并等待响应 | ||
| 171 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 172 | + // 3.2 解码响应 | ||
| 173 | + if (response != null) { | ||
| 174 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 175 | + log.info("[testTopoAdd][响应消息: {}]", responseMessage); | ||
| 176 | + } else { | ||
| 177 | + log.warn("[testTopoAdd][未收到响应]"); | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + // 4. 关闭连接 | ||
| 181 | + ws.close(); | ||
| 176 | } | 182 | } |
| 177 | 183 | ||
| 178 | /** | 184 | /** |
| @@ -180,16 +186,40 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | @@ -180,16 +186,40 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | ||
| 180 | */ | 186 | */ |
| 181 | @Test | 187 | @Test |
| 182 | public void testTopoDelete() throws Exception { | 188 | public void testTopoDelete() throws Exception { |
| 183 | - executeAuthenticatedRequest("testTopoDelete", ws -> { | ||
| 184 | - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); | ||
| 185 | - params.setSubDevices(Collections.singletonList( | ||
| 186 | - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | ||
| 187 | - return IotDeviceMessage.of( | ||
| 188 | - IdUtil.fastSimpleUUID(), | ||
| 189 | - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 190 | - params, | ||
| 191 | - null, null, null); | ||
| 192 | - }); | 189 | + // 1.1 创建 WebSocket 连接(同步) |
| 190 | + WebSocket ws = createWebSocketConnection(); | ||
| 191 | + log.info("[testTopoDelete][WebSocket 连接成功]"); | ||
| 192 | + | ||
| 193 | + // 1.2 先进行认证 | ||
| 194 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 195 | + log.info("[testTopoDelete][认证响应: {}]", authResponse); | ||
| 196 | + | ||
| 197 | + // 2.1 构建请求参数 | ||
| 198 | + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); | ||
| 199 | + params.setSubDevices(Collections.singletonList( | ||
| 200 | + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | ||
| 201 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 202 | + IdUtil.fastSimpleUUID(), | ||
| 203 | + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 204 | + params, | ||
| 205 | + null, null, null); | ||
| 206 | + // 2.2 编码 | ||
| 207 | + byte[] payload = CODEC.encode(request); | ||
| 208 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 209 | + log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 210 | + | ||
| 211 | + // 3.1 发送并等待响应 | ||
| 212 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 213 | + // 3.2 解码响应 | ||
| 214 | + if (response != null) { | ||
| 215 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 216 | + log.info("[testTopoDelete][响应消息: {}]", responseMessage); | ||
| 217 | + } else { | ||
| 218 | + log.warn("[testTopoDelete][未收到响应]"); | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + // 4. 关闭连接 | ||
| 222 | + ws.close(); | ||
| 193 | } | 223 | } |
| 194 | 224 | ||
| 195 | /** | 225 | /** |
| @@ -197,14 +227,38 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | @@ -197,14 +227,38 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | ||
| 197 | */ | 227 | */ |
| 198 | @Test | 228 | @Test |
| 199 | public void testTopoGet() throws Exception { | 229 | public void testTopoGet() throws Exception { |
| 200 | - executeAuthenticatedRequest("testTopoGet", ws -> { | ||
| 201 | - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | ||
| 202 | - return IotDeviceMessage.of( | ||
| 203 | - IdUtil.fastSimpleUUID(), | ||
| 204 | - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 205 | - params, | ||
| 206 | - null, null, null); | ||
| 207 | - }); | 230 | + // 1.1 创建 WebSocket 连接(同步) |
| 231 | + WebSocket ws = createWebSocketConnection(); | ||
| 232 | + log.info("[testTopoGet][WebSocket 连接成功]"); | ||
| 233 | + | ||
| 234 | + // 1.2 先进行认证 | ||
| 235 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 236 | + log.info("[testTopoGet][认证响应: {}]", authResponse); | ||
| 237 | + | ||
| 238 | + // 2.1 构建请求参数 | ||
| 239 | + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | ||
| 240 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 241 | + IdUtil.fastSimpleUUID(), | ||
| 242 | + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 243 | + params, | ||
| 244 | + null, null, null); | ||
| 245 | + // 2.2 编码 | ||
| 246 | + byte[] payload = CODEC.encode(request); | ||
| 247 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 248 | + log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 249 | + | ||
| 250 | + // 3.1 发送并等待响应 | ||
| 251 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 252 | + // 3.2 解码响应 | ||
| 253 | + if (response != null) { | ||
| 254 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 255 | + log.info("[testTopoGet][响应消息: {}]", responseMessage); | ||
| 256 | + } else { | ||
| 257 | + log.warn("[testTopoGet][未收到响应]"); | ||
| 258 | + } | ||
| 259 | + | ||
| 260 | + // 4. 关闭连接 | ||
| 261 | + ws.close(); | ||
| 208 | } | 262 | } |
| 209 | 263 | ||
| 210 | // ===================== 子设备注册测试 ===================== | 264 | // ===================== 子设备注册测试 ===================== |
| @@ -214,16 +268,40 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | @@ -214,16 +268,40 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | ||
| 214 | */ | 268 | */ |
| 215 | @Test | 269 | @Test |
| 216 | public void testSubDeviceRegister() throws Exception { | 270 | public void testSubDeviceRegister() throws Exception { |
| 217 | - executeAuthenticatedRequest("testSubDeviceRegister", ws -> { | ||
| 218 | - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); | ||
| 219 | - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); | ||
| 220 | - subDevice.setDeviceName("mougezishebei-ws"); | ||
| 221 | - return IotDeviceMessage.of( | ||
| 222 | - IdUtil.fastSimpleUUID(), | ||
| 223 | - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | ||
| 224 | - Collections.singletonList(subDevice), | ||
| 225 | - null, null, null); | ||
| 226 | - }); | 271 | + // 1.1 创建 WebSocket 连接(同步) |
| 272 | + WebSocket ws = createWebSocketConnection(); | ||
| 273 | + log.info("[testSubDeviceRegister][WebSocket 连接成功]"); | ||
| 274 | + | ||
| 275 | + // 1.2 先进行认证 | ||
| 276 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 277 | + log.info("[testSubDeviceRegister][认证响应: {}]", authResponse); | ||
| 278 | + | ||
| 279 | + // 2.1 构建请求参数 | ||
| 280 | + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); | ||
| 281 | + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); | ||
| 282 | + subDevice.setDeviceName("mougezishebei-ws"); | ||
| 283 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 284 | + IdUtil.fastSimpleUUID(), | ||
| 285 | + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | ||
| 286 | + Collections.singletonList(subDevice), | ||
| 287 | + null, null, null); | ||
| 288 | + // 2.2 编码 | ||
| 289 | + byte[] payload = CODEC.encode(request); | ||
| 290 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 291 | + log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 292 | + | ||
| 293 | + // 3.1 发送并等待响应 | ||
| 294 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 295 | + // 3.2 解码响应 | ||
| 296 | + if (response != null) { | ||
| 297 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 298 | + log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage); | ||
| 299 | + } else { | ||
| 300 | + log.warn("[testSubDeviceRegister][未收到响应]"); | ||
| 301 | + } | ||
| 302 | + | ||
| 303 | + // 4. 关闭连接 | ||
| 304 | + ws.close(); | ||
| 227 | } | 305 | } |
| 228 | 306 | ||
| 229 | // ===================== 批量上报测试 ===================== | 307 | // ===================== 批量上报测试 ===================== |
| @@ -233,126 +311,140 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | @@ -233,126 +311,140 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { | ||
| 233 | */ | 311 | */ |
| 234 | @Test | 312 | @Test |
| 235 | public void testPropertyPackPost() throws Exception { | 313 | public void testPropertyPackPost() throws Exception { |
| 236 | - executeAuthenticatedRequest("testPropertyPackPost", ws -> { | ||
| 237 | - // 构建【网关设备】自身属性 | ||
| 238 | - Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder() | ||
| 239 | - .put("temperature", 25.5) | ||
| 240 | - .build(); | ||
| 241 | - // 构建【网关设备】自身事件 | ||
| 242 | - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 243 | - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); | ||
| 244 | - gatewayEvent.setTime(System.currentTimeMillis()); | ||
| 245 | - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 246 | - .put("statusReport", gatewayEvent) | ||
| 247 | - .build(); | ||
| 248 | - // 构建【网关子设备】属性 | ||
| 249 | - Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder() | ||
| 250 | - .put("power", 100) | ||
| 251 | - .build(); | ||
| 252 | - // 构建【网关子设备】事件 | ||
| 253 | - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 254 | - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); | ||
| 255 | - subDeviceEvent.setTime(System.currentTimeMillis()); | ||
| 256 | - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 257 | - .put("healthCheck", subDeviceEvent) | ||
| 258 | - .build(); | ||
| 259 | - // 构建子设备数据 | ||
| 260 | - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); | ||
| 261 | - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); | ||
| 262 | - subDeviceData.setProperties(subDeviceProperties); | ||
| 263 | - subDeviceData.setEvents(subDeviceEvents); | ||
| 264 | - // 构建请求参数 | ||
| 265 | - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); | ||
| 266 | - params.setProperties(gatewayProperties); | ||
| 267 | - params.setEvents(gatewayEvents); | ||
| 268 | - params.setSubDevices(List.of(subDeviceData)); | ||
| 269 | - return IotDeviceMessage.of( | ||
| 270 | - IdUtil.fastSimpleUUID(), | ||
| 271 | - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 272 | - params, | ||
| 273 | - null, null, null); | ||
| 274 | - }); | 314 | + // 1.1 创建 WebSocket 连接(同步) |
| 315 | + WebSocket ws = createWebSocketConnection(); | ||
| 316 | + log.info("[testPropertyPackPost][WebSocket 连接成功]"); | ||
| 317 | + | ||
| 318 | + // 1.2 先进行认证 | ||
| 319 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 320 | + log.info("[testPropertyPackPost][认证响应: {}]", authResponse); | ||
| 321 | + | ||
| 322 | + // 2.1 构建【网关设备】自身属性 | ||
| 323 | + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder() | ||
| 324 | + .put("temperature", 25.5) | ||
| 325 | + .build(); | ||
| 326 | + // 2.2 构建【网关设备】自身事件 | ||
| 327 | + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 328 | + gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); | ||
| 329 | + gatewayEvent.setTime(System.currentTimeMillis()); | ||
| 330 | + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 331 | + .put("statusReport", gatewayEvent) | ||
| 332 | + .build(); | ||
| 333 | + // 2.3 构建【网关子设备】属性 | ||
| 334 | + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder() | ||
| 335 | + .put("power", 100) | ||
| 336 | + .build(); | ||
| 337 | + // 2.4 构建【网关子设备】事件 | ||
| 338 | + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 339 | + subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); | ||
| 340 | + subDeviceEvent.setTime(System.currentTimeMillis()); | ||
| 341 | + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 342 | + .put("healthCheck", subDeviceEvent) | ||
| 343 | + .build(); | ||
| 344 | + // 2.5 构建子设备数据 | ||
| 345 | + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); | ||
| 346 | + subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); | ||
| 347 | + subDeviceData.setProperties(subDeviceProperties); | ||
| 348 | + subDeviceData.setEvents(subDeviceEvents); | ||
| 349 | + // 2.6 构建请求参数 | ||
| 350 | + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); | ||
| 351 | + params.setProperties(gatewayProperties); | ||
| 352 | + params.setEvents(gatewayEvents); | ||
| 353 | + params.setSubDevices(List.of(subDeviceData)); | ||
| 354 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 355 | + IdUtil.fastSimpleUUID(), | ||
| 356 | + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 357 | + params, | ||
| 358 | + null, null, null); | ||
| 359 | + // 2.7 编码 | ||
| 360 | + byte[] payload = CODEC.encode(request); | ||
| 361 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 362 | + log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 363 | + | ||
| 364 | + // 3.1 发送并等待响应 | ||
| 365 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 366 | + // 3.2 解码响应 | ||
| 367 | + if (response != null) { | ||
| 368 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 369 | + log.info("[testPropertyPackPost][响应消息: {}]", responseMessage); | ||
| 370 | + } else { | ||
| 371 | + log.warn("[testPropertyPackPost][未收到响应]"); | ||
| 372 | + } | ||
| 373 | + | ||
| 374 | + // 4. 关闭连接 | ||
| 375 | + ws.close(); | ||
| 275 | } | 376 | } |
| 276 | 377 | ||
| 277 | // ===================== 辅助方法 ===================== | 378 | // ===================== 辅助方法 ===================== |
| 278 | 379 | ||
| 279 | /** | 380 | /** |
| 280 | - * 执行需要认证的请求 | 381 | + * 创建 WebSocket 连接(同步) |
| 281 | * | 382 | * |
| 282 | - * @param testName 测试名称 | ||
| 283 | - * @param requestSupplier 请求消息提供者 | 383 | + * @return WebSocket 连接 |
| 284 | */ | 384 | */ |
| 285 | - private void executeAuthenticatedRequest(String testName, java.util.function.Function<WebSocket, IotDeviceMessage> requestSupplier) throws Exception { | ||
| 286 | - CountDownLatch latch = new CountDownLatch(2); | ||
| 287 | - AtomicReference<String> authResponseRef = new AtomicReference<>(); | ||
| 288 | - AtomicReference<String> businessResponseRef = new AtomicReference<>(); | ||
| 289 | - | ||
| 290 | - HttpClient client = vertx.createHttpClient(); | 385 | + private WebSocket createWebSocketConnection() throws Exception { |
| 386 | + WebSocketClient wsClient = vertx.createWebSocketClient(); | ||
| 291 | WebSocketConnectOptions options = new WebSocketConnectOptions() | 387 | WebSocketConnectOptions options = new WebSocketConnectOptions() |
| 292 | .setHost(SERVER_HOST) | 388 | .setHost(SERVER_HOST) |
| 293 | .setPort(SERVER_PORT) | 389 | .setPort(SERVER_PORT) |
| 294 | .setURI(WS_PATH); | 390 | .setURI(WS_PATH); |
| 391 | + return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + /** | ||
| 395 | + * 发送消息并等待响应(同步) | ||
| 396 | + * | ||
| 397 | + * @param ws WebSocket 连接 | ||
| 398 | + * @param message 请求消息 | ||
| 399 | + * @return 响应消息 | ||
| 400 | + */ | ||
| 401 | + private String sendAndReceive(WebSocket ws, String message) throws Exception { | ||
| 402 | + CountDownLatch latch = new CountDownLatch(1); | ||
| 403 | + AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 295 | 404 | ||
| 296 | - client.webSocket(options).onComplete(ar -> { | ||
| 297 | - if (ar.succeeded()) { | ||
| 298 | - WebSocket ws = ar.result(); | ||
| 299 | - log.info("[{}][WebSocket 连接成功]", testName); | ||
| 300 | - | ||
| 301 | - final boolean[] authenticated = {false}; | ||
| 302 | - | ||
| 303 | - ws.textMessageHandler(message -> { | ||
| 304 | - log.info("[{}][收到响应: {}]", testName, message); | ||
| 305 | - if (!authenticated[0]) { | ||
| 306 | - authResponseRef.set(message); | ||
| 307 | - authenticated[0] = true; | ||
| 308 | - latch.countDown(); | ||
| 309 | - | ||
| 310 | - // 认证成功后发送业务请求 | ||
| 311 | - IotDeviceMessage businessRequest = requestSupplier.apply(ws); | ||
| 312 | - byte[] payload = CODEC.encode(businessRequest); | ||
| 313 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 314 | - log.info("[{}][发送业务请求: {}]", testName, jsonMessage); | ||
| 315 | - ws.writeTextMessage(jsonMessage); | ||
| 316 | - } else { | ||
| 317 | - businessResponseRef.set(message); | ||
| 318 | - ws.close(); | ||
| 319 | - latch.countDown(); | ||
| 320 | - } | ||
| 321 | - }); | ||
| 322 | - | ||
| 323 | - // 先发送认证请求 | ||
| 324 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 325 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | ||
| 326 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 327 | - .setClientId(authInfo.getClientId()) | ||
| 328 | - .setUsername(authInfo.getUsername()) | ||
| 329 | - .setPassword(authInfo.getPassword()); | ||
| 330 | - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 331 | - | ||
| 332 | - byte[] payload = CODEC.encode(authRequest); | ||
| 333 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 334 | - log.info("[{}][发送认证请求: {}]", testName, jsonMessage); | ||
| 335 | - ws.writeTextMessage(jsonMessage); | ||
| 336 | - } else { | ||
| 337 | - log.error("[{}][WebSocket 连接失败]", testName, ar.cause()); | ||
| 338 | - latch.countDown(); | ||
| 339 | - latch.countDown(); | ||
| 340 | - } | 405 | + // 设置消息处理器 |
| 406 | + ws.textMessageHandler(response -> { | ||
| 407 | + log.info("[sendAndReceive][收到响应: {}]", response); | ||
| 408 | + responseRef.set(response); | ||
| 409 | + latch.countDown(); | ||
| 341 | }); | 410 | }); |
| 342 | 411 | ||
| 343 | - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS); | ||
| 344 | - if (completed) { | ||
| 345 | - if (authResponseRef.get() != null) { | ||
| 346 | - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 347 | - log.info("[{}][认证响应: {}]", testName, authResponse); | ||
| 348 | - } | ||
| 349 | - if (businessResponseRef.get() != null) { | ||
| 350 | - IotDeviceMessage businessResponse = CODEC.decode(businessResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 351 | - log.info("[{}][业务响应: {}]", testName, businessResponse); | ||
| 352 | - } | ||
| 353 | - } else { | ||
| 354 | - log.warn("[{}][测试超时]", testName); | 412 | + // 发送请求 |
| 413 | + log.info("[sendAndReceive][发送请求: {}]", message); | ||
| 414 | + ws.writeTextMessage(message); | ||
| 415 | + | ||
| 416 | + // 等待响应 | ||
| 417 | + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 418 | + if (!completed) { | ||
| 419 | + log.warn("[sendAndReceive][等待响应超时]"); | ||
| 420 | + } | ||
| 421 | + return responseRef.get(); | ||
| 422 | + } | ||
| 423 | + | ||
| 424 | + /** | ||
| 425 | + * 执行网关设备认证(同步) | ||
| 426 | + * | ||
| 427 | + * @param ws WebSocket 连接 | ||
| 428 | + * @return 认证响应消息 | ||
| 429 | + */ | ||
| 430 | + private IotDeviceMessage authenticate(WebSocket ws) throws Exception { | ||
| 431 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 432 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | ||
| 433 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 434 | + .setClientId(authInfo.getClientId()) | ||
| 435 | + .setUsername(authInfo.getUsername()) | ||
| 436 | + .setPassword(authInfo.getPassword()); | ||
| 437 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 438 | + | ||
| 439 | + byte[] payload = CODEC.encode(request); | ||
| 440 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 441 | + log.info("[authenticate][发送认证请求: {}]", jsonMessage); | ||
| 442 | + | ||
| 443 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 444 | + if (response != null) { | ||
| 445 | + return CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 355 | } | 446 | } |
| 447 | + return null; | ||
| 356 | } | 448 | } |
| 357 | 449 | ||
| 358 | } | 450 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -11,15 +12,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | @@ -11,15 +12,14 @@ 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.IotDeviceMessageCodec; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; | 13 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; |
| 13 | import io.vertx.core.Vertx; | 14 | import io.vertx.core.Vertx; |
| 14 | -import io.vertx.core.http.HttpClient; | ||
| 15 | import io.vertx.core.http.WebSocket; | 15 | import io.vertx.core.http.WebSocket; |
| 16 | +import io.vertx.core.http.WebSocketClient; | ||
| 16 | import io.vertx.core.http.WebSocketConnectOptions; | 17 | import io.vertx.core.http.WebSocketConnectOptions; |
| 17 | import lombok.extern.slf4j.Slf4j; | 18 | import lombok.extern.slf4j.Slf4j; |
| 18 | import org.junit.jupiter.api.AfterAll; | 19 | import org.junit.jupiter.api.AfterAll; |
| 19 | import org.junit.jupiter.api.BeforeAll; | 20 | import org.junit.jupiter.api.BeforeAll; |
| 20 | import org.junit.jupiter.api.Test; | 21 | import org.junit.jupiter.api.Test; |
| 21 | 22 | ||
| 22 | -import java.nio.charset.StandardCharsets; | ||
| 23 | import java.util.concurrent.CountDownLatch; | 23 | import java.util.concurrent.CountDownLatch; |
| 24 | import java.util.concurrent.TimeUnit; | 24 | import java.util.concurrent.TimeUnit; |
| 25 | import java.util.concurrent.atomic.AtomicReference; | 25 | import java.util.concurrent.atomic.AtomicReference; |
| @@ -87,52 +87,35 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { | @@ -87,52 +87,35 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { | ||
| 87 | */ | 87 | */ |
| 88 | @Test | 88 | @Test |
| 89 | public void testAuth() throws Exception { | 89 | public void testAuth() throws Exception { |
| 90 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 91 | - AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 92 | - | ||
| 93 | - HttpClient client = vertx.createHttpClient(); | ||
| 94 | - WebSocketConnectOptions options = new WebSocketConnectOptions() | ||
| 95 | - .setHost(SERVER_HOST) | ||
| 96 | - .setPort(SERVER_PORT) | ||
| 97 | - .setURI(WS_PATH); | ||
| 98 | - | ||
| 99 | - client.webSocket(options).onComplete(ar -> { | ||
| 100 | - if (ar.succeeded()) { | ||
| 101 | - WebSocket ws = ar.result(); | ||
| 102 | - log.info("[testAuth][WebSocket 连接成功]"); | ||
| 103 | - | ||
| 104 | - ws.textMessageHandler(message -> { | ||
| 105 | - log.info("[testAuth][收到响应: {}]", message); | ||
| 106 | - responseRef.set(message); | ||
| 107 | - ws.close(); | ||
| 108 | - latch.countDown(); | ||
| 109 | - }); | ||
| 110 | - | ||
| 111 | - // 构建认证消息 | ||
| 112 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 113 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 114 | - .setClientId(authInfo.getClientId()) | ||
| 115 | - .setUsername(authInfo.getUsername()) | ||
| 116 | - .setPassword(authInfo.getPassword()); | ||
| 117 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 118 | - | ||
| 119 | - byte[] payload = CODEC.encode(request); | ||
| 120 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 121 | - log.info("[testAuth][发送认证请求: {}]", jsonMessage); | ||
| 122 | - ws.writeTextMessage(jsonMessage); | ||
| 123 | - } else { | ||
| 124 | - log.error("[testAuth][WebSocket 连接失败]", ar.cause()); | ||
| 125 | - latch.countDown(); | ||
| 126 | - } | ||
| 127 | - }); | ||
| 128 | - | ||
| 129 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 130 | - if (completed && responseRef.get() != null) { | ||
| 131 | - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 132 | - log.info("[testAuth][解码响应: {}]", response); | 90 | + // 1.1 构建认证消息 |
| 91 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 92 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 93 | + .setClientId(authInfo.getClientId()) | ||
| 94 | + .setUsername(authInfo.getUsername()) | ||
| 95 | + .setPassword(authInfo.getPassword()); | ||
| 96 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 97 | + // 1.2 编码 | ||
| 98 | + byte[] payload = CODEC.encode(request); | ||
| 99 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 100 | + log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 101 | + | ||
| 102 | + // 2.1 创建 WebSocket 连接(同步) | ||
| 103 | + WebSocket ws = createWebSocketConnection(); | ||
| 104 | + log.info("[testAuth][WebSocket 连接成功]"); | ||
| 105 | + | ||
| 106 | + // 2.2 发送并等待响应 | ||
| 107 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 108 | + | ||
| 109 | + // 3. 解码响应 | ||
| 110 | + if (response != null) { | ||
| 111 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 112 | + log.info("[testAuth][响应消息: {}]", responseMessage); | ||
| 133 | } else { | 113 | } else { |
| 134 | - log.warn("[testAuth][测试超时或未收到响应]"); | 114 | + log.warn("[testAuth][未收到响应]"); |
| 135 | } | 115 | } |
| 116 | + | ||
| 117 | + // 4. 关闭连接 | ||
| 118 | + ws.close(); | ||
| 136 | } | 119 | } |
| 137 | 120 | ||
| 138 | // ===================== 子设备属性上报测试 ===================== | 121 | // ===================== 子设备属性上报测试 ===================== |
| @@ -142,18 +125,42 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { | @@ -142,18 +125,42 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { | ||
| 142 | */ | 125 | */ |
| 143 | @Test | 126 | @Test |
| 144 | public void testPropertyPost() throws Exception { | 127 | public void testPropertyPost() throws Exception { |
| 145 | - executeAuthenticatedRequest("testPropertyPost", ws -> { | ||
| 146 | - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); | ||
| 147 | - return IotDeviceMessage.of( | ||
| 148 | - IdUtil.fastSimpleUUID(), | ||
| 149 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 150 | - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 151 | - .put("power", 100) | ||
| 152 | - .put("status", "online") | ||
| 153 | - .put("temperature", 36.5) | ||
| 154 | - .build()), | ||
| 155 | - null, null, null); | ||
| 156 | - }); | 128 | + // 1.1 创建 WebSocket 连接(同步) |
| 129 | + WebSocket ws = createWebSocketConnection(); | ||
| 130 | + log.info("[testPropertyPost][WebSocket 连接成功]"); | ||
| 131 | + | ||
| 132 | + // 1.2 先进行认证 | ||
| 133 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 134 | + log.info("[testPropertyPost][认证响应: {}]", authResponse); | ||
| 135 | + log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); | ||
| 136 | + | ||
| 137 | + // 2.1 构建属性上报消息 | ||
| 138 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 139 | + IdUtil.fastSimpleUUID(), | ||
| 140 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 141 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 142 | + .put("power", 100) | ||
| 143 | + .put("status", "online") | ||
| 144 | + .put("temperature", 36.5) | ||
| 145 | + .build()), | ||
| 146 | + null, null, null); | ||
| 147 | + // 2.2 编码 | ||
| 148 | + byte[] payload = CODEC.encode(request); | ||
| 149 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 150 | + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 151 | + | ||
| 152 | + // 3.1 发送并等待响应 | ||
| 153 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 154 | + // 3.2 解码响应 | ||
| 155 | + if (response != null) { | ||
| 156 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 157 | + log.info("[testPropertyPost][响应消息: {}]", responseMessage); | ||
| 158 | + } else { | ||
| 159 | + log.warn("[testPropertyPost][未收到响应]"); | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + // 4. 关闭连接 | ||
| 163 | + ws.close(); | ||
| 157 | } | 164 | } |
| 158 | 165 | ||
| 159 | // ===================== 子设备事件上报测试 ===================== | 166 | // ===================== 子设备事件上报测试 ===================== |
| @@ -163,102 +170,117 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { | @@ -163,102 +170,117 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { | ||
| 163 | */ | 170 | */ |
| 164 | @Test | 171 | @Test |
| 165 | public void testEventPost() throws Exception { | 172 | public void testEventPost() throws Exception { |
| 166 | - executeAuthenticatedRequest("testEventPost", ws -> { | ||
| 167 | - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); | ||
| 168 | - return IotDeviceMessage.of( | ||
| 169 | - IdUtil.fastSimpleUUID(), | ||
| 170 | - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 171 | - IotDeviceEventPostReqDTO.of( | ||
| 172 | - "alarm", | ||
| 173 | - MapUtil.<String, Object>builder() | ||
| 174 | - .put("level", "warning") | ||
| 175 | - .put("message", "temperature too high") | ||
| 176 | - .put("threshold", 40) | ||
| 177 | - .put("current", 42) | ||
| 178 | - .build(), | ||
| 179 | - System.currentTimeMillis()), | ||
| 180 | - null, null, null); | ||
| 181 | - }); | 173 | + // 1.1 创建 WebSocket 连接(同步) |
| 174 | + WebSocket ws = createWebSocketConnection(); | ||
| 175 | + log.info("[testEventPost][WebSocket 连接成功]"); | ||
| 176 | + | ||
| 177 | + // 1.2 先进行认证 | ||
| 178 | + IotDeviceMessage authResponse = authenticate(ws); | ||
| 179 | + log.info("[testEventPost][认证响应: {}]", authResponse); | ||
| 180 | + log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); | ||
| 181 | + | ||
| 182 | + // 2.1 构建事件上报消息 | ||
| 183 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 184 | + IdUtil.fastSimpleUUID(), | ||
| 185 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 186 | + IotDeviceEventPostReqDTO.of( | ||
| 187 | + "alarm", | ||
| 188 | + MapUtil.<String, Object>builder() | ||
| 189 | + .put("level", "warning") | ||
| 190 | + .put("message", "temperature too high") | ||
| 191 | + .put("threshold", 40) | ||
| 192 | + .put("current", 42) | ||
| 193 | + .build(), | ||
| 194 | + System.currentTimeMillis()), | ||
| 195 | + null, null, null); | ||
| 196 | + // 2.2 编码 | ||
| 197 | + byte[] payload = CODEC.encode(request); | ||
| 198 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 199 | + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); | ||
| 200 | + | ||
| 201 | + // 3.1 发送并等待响应 | ||
| 202 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 203 | + // 3.2 解码响应 | ||
| 204 | + if (response != null) { | ||
| 205 | + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 206 | + log.info("[testEventPost][响应消息: {}]", responseMessage); | ||
| 207 | + } else { | ||
| 208 | + log.warn("[testEventPost][未收到响应]"); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + // 4. 关闭连接 | ||
| 212 | + ws.close(); | ||
| 182 | } | 213 | } |
| 183 | 214 | ||
| 184 | // ===================== 辅助方法 ===================== | 215 | // ===================== 辅助方法 ===================== |
| 185 | 216 | ||
| 186 | /** | 217 | /** |
| 187 | - * 执行需要认证的请求 | 218 | + * 创建 WebSocket 连接(同步) |
| 188 | * | 219 | * |
| 189 | - * @param testName 测试名称 | ||
| 190 | - * @param requestSupplier 请求消息提供者 | 220 | + * @return WebSocket 连接 |
| 191 | */ | 221 | */ |
| 192 | - private void executeAuthenticatedRequest(String testName, java.util.function.Function<WebSocket, IotDeviceMessage> requestSupplier) throws Exception { | ||
| 193 | - CountDownLatch latch = new CountDownLatch(2); | ||
| 194 | - AtomicReference<String> authResponseRef = new AtomicReference<>(); | ||
| 195 | - AtomicReference<String> businessResponseRef = new AtomicReference<>(); | ||
| 196 | - | ||
| 197 | - HttpClient client = vertx.createHttpClient(); | 222 | + private WebSocket createWebSocketConnection() throws Exception { |
| 223 | + WebSocketClient wsClient = vertx.createWebSocketClient(); | ||
| 198 | WebSocketConnectOptions options = new WebSocketConnectOptions() | 224 | WebSocketConnectOptions options = new WebSocketConnectOptions() |
| 199 | .setHost(SERVER_HOST) | 225 | .setHost(SERVER_HOST) |
| 200 | .setPort(SERVER_PORT) | 226 | .setPort(SERVER_PORT) |
| 201 | .setURI(WS_PATH); | 227 | .setURI(WS_PATH); |
| 228 | + return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + /** | ||
| 232 | + * 发送消息并等待响应(同步) | ||
| 233 | + * | ||
| 234 | + * @param ws WebSocket 连接 | ||
| 235 | + * @param message 请求消息 | ||
| 236 | + * @return 响应消息 | ||
| 237 | + */ | ||
| 238 | + private String sendAndReceive(WebSocket ws, String message) throws Exception { | ||
| 239 | + CountDownLatch latch = new CountDownLatch(1); | ||
| 240 | + AtomicReference<String> responseRef = new AtomicReference<>(); | ||
| 202 | 241 | ||
| 203 | - client.webSocket(options).onComplete(ar -> { | ||
| 204 | - if (ar.succeeded()) { | ||
| 205 | - WebSocket ws = ar.result(); | ||
| 206 | - log.info("[{}][WebSocket 连接成功]", testName); | ||
| 207 | - | ||
| 208 | - final boolean[] authenticated = {false}; | ||
| 209 | - | ||
| 210 | - ws.textMessageHandler(message -> { | ||
| 211 | - log.info("[{}][收到响应: {}]", testName, message); | ||
| 212 | - if (!authenticated[0]) { | ||
| 213 | - authResponseRef.set(message); | ||
| 214 | - authenticated[0] = true; | ||
| 215 | - latch.countDown(); | ||
| 216 | - | ||
| 217 | - // 认证成功后发送业务请求 | ||
| 218 | - IotDeviceMessage businessRequest = requestSupplier.apply(ws); | ||
| 219 | - byte[] payload = CODEC.encode(businessRequest); | ||
| 220 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 221 | - log.info("[{}][发送业务请求: {}]", testName, jsonMessage); | ||
| 222 | - ws.writeTextMessage(jsonMessage); | ||
| 223 | - } else { | ||
| 224 | - businessResponseRef.set(message); | ||
| 225 | - ws.close(); | ||
| 226 | - latch.countDown(); | ||
| 227 | - } | ||
| 228 | - }); | ||
| 229 | - | ||
| 230 | - // 先发送认证请求 | ||
| 231 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 232 | - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 233 | - .setClientId(authInfo.getClientId()) | ||
| 234 | - .setUsername(authInfo.getUsername()) | ||
| 235 | - .setPassword(authInfo.getPassword()); | ||
| 236 | - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 237 | - | ||
| 238 | - byte[] payload = CODEC.encode(authRequest); | ||
| 239 | - String jsonMessage = new String(payload, StandardCharsets.UTF_8); | ||
| 240 | - log.info("[{}][发送认证请求: {}]", testName, jsonMessage); | ||
| 241 | - ws.writeTextMessage(jsonMessage); | ||
| 242 | - } else { | ||
| 243 | - log.error("[{}][WebSocket 连接失败]", testName, ar.cause()); | ||
| 244 | - latch.countDown(); | ||
| 245 | - latch.countDown(); | ||
| 246 | - } | 242 | + // 设置消息处理器 |
| 243 | + ws.textMessageHandler(response -> { | ||
| 244 | + log.info("[sendAndReceive][收到响应: {}]", response); | ||
| 245 | + responseRef.set(response); | ||
| 246 | + latch.countDown(); | ||
| 247 | }); | 247 | }); |
| 248 | 248 | ||
| 249 | - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS); | ||
| 250 | - if (completed) { | ||
| 251 | - if (authResponseRef.get() != null) { | ||
| 252 | - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 253 | - log.info("[{}][认证响应: {}]", testName, authResponse); | ||
| 254 | - } | ||
| 255 | - if (businessResponseRef.get() != null) { | ||
| 256 | - IotDeviceMessage businessResponse = CODEC.decode(businessResponseRef.get().getBytes(StandardCharsets.UTF_8)); | ||
| 257 | - log.info("[{}][业务响应: {}]", testName, businessResponse); | ||
| 258 | - } | ||
| 259 | - } else { | ||
| 260 | - log.warn("[{}][测试超时]", testName); | 249 | + // 发送请求 |
| 250 | + log.info("[sendAndReceive][发送请求: {}]", message); | ||
| 251 | + ws.writeTextMessage(message); | ||
| 252 | + | ||
| 253 | + // 等待响应 | ||
| 254 | + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 255 | + if (!completed) { | ||
| 256 | + log.warn("[sendAndReceive][等待响应超时]"); | ||
| 257 | + } | ||
| 258 | + return responseRef.get(); | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + /** | ||
| 262 | + * 执行子设备认证(同步) | ||
| 263 | + * | ||
| 264 | + * @param ws WebSocket 连接 | ||
| 265 | + * @return 认证响应消息 | ||
| 266 | + */ | ||
| 267 | + private IotDeviceMessage authenticate(WebSocket ws) throws Exception { | ||
| 268 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 269 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() | ||
| 270 | + .setClientId(authInfo.getClientId()) | ||
| 271 | + .setUsername(authInfo.getUsername()) | ||
| 272 | + .setPassword(authInfo.getPassword()); | ||
| 273 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); | ||
| 274 | + | ||
| 275 | + byte[] payload = CODEC.encode(request); | ||
| 276 | + String jsonMessage = StrUtil.utf8Str(payload); | ||
| 277 | + log.info("[authenticate][发送认证请求: {}]", jsonMessage); | ||
| 278 | + | ||
| 279 | + String response = sendAndReceive(ws, jsonMessage); | ||
| 280 | + if (response != null) { | ||
| 281 | + return CODEC.decode(StrUtil.utf8Bytes(response)); | ||
| 261 | } | 282 | } |
| 283 | + return null; | ||
| 262 | } | 284 | } |
| 263 | 285 | ||
| 264 | } | 286 | } |