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 | 568 | /** |
| 569 | 569 | * 服务端口(CoAP 默认端口 5683) |
| 570 | 570 | */ |
| 571 | - // TODO @AI:默认不为空 | |
| 571 | + @NotNull(message = "服务端口不能为空") | |
| 572 | 572 | private Integer port = 5683; |
| 573 | 573 | |
| 574 | 574 | /** |
| 575 | 575 | * 最大消息大小(字节) |
| 576 | 576 | */ |
| 577 | - // TODO @AI:默认不为空 | |
| 577 | + @NotNull(message = "最大消息大小不能为空") | |
| 578 | 578 | private Integer maxMessageSize = 1024; |
| 579 | 579 | |
| 580 | 580 | /** |
| 581 | 581 | * ACK 超时时间(毫秒) |
| 582 | 582 | */ |
| 583 | - // TODO @AI:默认不为空 | |
| 583 | + @NotNull(message = "ACK 超时时间不能为空") | |
| 584 | 584 | private Integer ackTimeout = 2000; |
| 585 | 585 | |
| 586 | 586 | /** |
| 587 | 587 | * 最大重传次数 |
| 588 | 588 | */ |
| 589 | - // TODO @AI:默认不为空 | |
| 589 | + @NotNull(message = "最大重传次数不能为空") | |
| 590 | 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 | 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 | 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 | 53 | } |
| 54 | 54 | |
| 55 | 55 | @PostConstruct |
| 56 | + @SuppressWarnings("deprecation") | |
| 56 | 57 | public void start() { |
| 57 | 58 | // 1.1 创建服务器选项 |
| 58 | 59 | HttpServerOptions options = new HttpServerOptions() |
| ... | ... | @@ -73,15 +74,14 @@ public class IotWebSocketUpstreamProtocol { |
| 73 | 74 | httpServer.webSocketHandler(socket -> { |
| 74 | 75 | // 验证路径 |
| 75 | 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 | 79 | socket.reject(); |
| 79 | 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 | 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 | 139 | * 客户端 ID |
| 140 | 140 | */ |
| 141 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; | |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 7 | 7 | import lombok.RequiredArgsConstructor; |
| 8 | 8 | import lombok.extern.slf4j.Slf4j; |
| 9 | 9 | |
| 10 | -import java.nio.charset.StandardCharsets; | |
| 11 | - | |
| 12 | 10 | /** |
| 13 | 11 | * IoT 网关 WebSocket 下行消息处理器 |
| 14 | 12 | * |
| ... | ... | @@ -18,9 +16,6 @@ import java.nio.charset.StandardCharsets; |
| 18 | 16 | @RequiredArgsConstructor |
| 19 | 17 | public class IotWebSocketDownstreamHandler { |
| 20 | 18 | |
| 21 | - // TODO @芋艿:codeType 的处理; | |
| 22 | - private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE; | |
| 23 | - | |
| 24 | 19 | private final IotDeviceMessageService deviceMessageService; |
| 25 | 20 | |
| 26 | 21 | private final IotWebSocketConnectionManager connectionManager; |
| ... | ... | @@ -42,8 +37,8 @@ public class IotWebSocketDownstreamHandler { |
| 42 | 37 | } |
| 43 | 38 | |
| 44 | 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 | 42 | boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); |
| 48 | 43 | if (success) { |
| 49 | 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 | 25 | import io.vertx.core.http.ServerWebSocket; |
| 26 | 26 | import lombok.extern.slf4j.Slf4j; |
| 27 | 27 | |
| 28 | -import java.nio.charset.StandardCharsets; | |
| 29 | 28 | |
| 30 | 29 | /** |
| 31 | 30 | * WebSocket 上行消息处理器 |
| ... | ... | @@ -35,7 +34,9 @@ import java.nio.charset.StandardCharsets; |
| 35 | 34 | @Slf4j |
| 36 | 35 | public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 37 | 36 | |
| 38 | - // TODO @芋艿:codeType 的处理; | |
| 37 | + /** | |
| 38 | + * 默认消息编解码类型 | |
| 39 | + */ | |
| 39 | 40 | private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE; |
| 40 | 41 | |
| 41 | 42 | private static final String AUTH_METHOD = "auth"; |
| ... | ... | @@ -63,13 +64,10 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 63 | 64 | |
| 64 | 65 | @Override |
| 65 | 66 | public void handle(ServerWebSocket socket) { |
| 66 | - // 1. 接受 WebSocket 连接 | |
| 67 | 67 | String clientId = IdUtil.simpleUUID(); |
| 68 | 68 | log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); |
| 69 | - // TODO @AI:这个方法已经废弃,看看有没其他替换的 | |
| 70 | - socket.accept(); | |
| 71 | 69 | |
| 72 | - // 2.1 设置异常和关闭处理器 | |
| 70 | + // 1. 设置异常和关闭处理器 | |
| 73 | 71 | socket.exceptionHandler(ex -> { |
| 74 | 72 | log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); |
| 75 | 73 | cleanupConnection(socket); |
| ... | ... | @@ -79,7 +77,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 79 | 77 | cleanupConnection(socket); |
| 80 | 78 | }); |
| 81 | 79 | |
| 82 | - // 2.2 设置文本消息处理器 | |
| 80 | + // 2. 设置文本消息处理器 | |
| 83 | 81 | socket.textMessageHandler(message -> { |
| 84 | 82 | try { |
| 85 | 83 | processMessage(clientId, message, socket); |
| ... | ... | @@ -105,12 +103,13 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 105 | 103 | if (StrUtil.isBlank(message)) { |
| 106 | 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 | 109 | IotDeviceMessage deviceMessage; |
| 111 | 110 | try { |
| 112 | 111 | deviceMessage = deviceMessageService.decodeDeviceMessage( |
| 113 | - message.getBytes(StandardCharsets.UTF_8), CODEC_TYPE); | |
| 112 | + StrUtil.utf8Bytes(message), codecType); | |
| 114 | 113 | if (deviceMessage == null) { |
| 115 | 114 | throw new Exception("解码后消息为空"); |
| 116 | 115 | } |
| ... | ... | @@ -269,7 +268,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 269 | 268 | .setDeviceId(device.getId()) |
| 270 | 269 | .setProductKey(device.getProductKey()) |
| 271 | 270 | .setDeviceName(device.getDeviceName()) |
| 272 | - .setClientId(clientId); | |
| 271 | + .setClientId(clientId) | |
| 272 | + .setCodecType(CODEC_TYPE); | |
| 273 | 273 | // 注册连接 |
| 274 | 274 | connectionManager.registerConnection(socket, device.getId(), connectionInfo); |
| 275 | 275 | } |
| ... | ... | @@ -330,7 +330,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 330 | 330 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); |
| 331 | 331 | |
| 332 | 332 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); |
| 333 | - socket.writeTextMessage(new String(encodedData, StandardCharsets.UTF_8)); | |
| 333 | + socket.writeTextMessage(StrUtil.utf8Str(encodedData)); | |
| 334 | 334 | } catch (Exception e) { |
| 335 | 335 | log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); |
| 336 | 336 | } |
| ... | ... | @@ -472,7 +472,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 472 | 472 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); |
| 473 | 473 | // 2. 发送响应 |
| 474 | 474 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); |
| 475 | - socket.writeTextMessage(new String(encodedData, StandardCharsets.UTF_8)); | |
| 475 | + socket.writeTextMessage(StrUtil.utf8Str(encodedData)); | |
| 476 | 476 | } catch (Exception e) { |
| 477 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | 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 | 13 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 13 | 14 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; |
| 14 | 15 | import io.vertx.core.Vertx; |
| 15 | -import io.vertx.core.http.HttpClient; | |
| 16 | 16 | import io.vertx.core.http.WebSocket; |
| 17 | +import io.vertx.core.http.WebSocketClient; | |
| 17 | 18 | import io.vertx.core.http.WebSocketConnectOptions; |
| 18 | 19 | import lombok.extern.slf4j.Slf4j; |
| 19 | 20 | import org.junit.jupiter.api.AfterAll; |
| 20 | 21 | import org.junit.jupiter.api.BeforeAll; |
| 21 | 22 | import org.junit.jupiter.api.Test; |
| 22 | 23 | |
| 23 | -import java.nio.charset.StandardCharsets; | |
| 24 | 24 | import java.util.concurrent.CountDownLatch; |
| 25 | 25 | import java.util.concurrent.TimeUnit; |
| 26 | 26 | import java.util.concurrent.atomic.AtomicReference; |
| ... | ... | @@ -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 | 85 | * 认证测试:获取设备 Token |
| 88 | 86 | */ |
| 89 | 87 | @Test |
| 90 | 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 | 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 | 129 | @Test |
| 150 | 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 | 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 | 167 | */ |
| 204 | 168 | @Test |
| 205 | 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 | 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 | 210 | */ |
| 289 | 211 | @Test |
| 290 | 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 | 258 | WebSocketConnectOptions options = new WebSocketConnectOptions() |
| 297 | 259 | .setHost(SERVER_HOST) |
| 298 | 260 | .setPort(SERVER_PORT) |
| 299 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | 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 | 16 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 16 | 17 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; |
| 17 | 18 | import io.vertx.core.Vertx; |
| 18 | -import io.vertx.core.http.HttpClient; | |
| 19 | 19 | import io.vertx.core.http.WebSocket; |
| 20 | +import io.vertx.core.http.WebSocketClient; | |
| 20 | 21 | import io.vertx.core.http.WebSocketConnectOptions; |
| 21 | 22 | import lombok.extern.slf4j.Slf4j; |
| 22 | 23 | import org.junit.jupiter.api.AfterAll; |
| 23 | 24 | import org.junit.jupiter.api.BeforeAll; |
| 24 | 25 | import org.junit.jupiter.api.Test; |
| 25 | 26 | |
| 26 | -import java.nio.charset.StandardCharsets; | |
| 27 | 27 | import java.util.Collections; |
| 28 | 28 | import java.util.List; |
| 29 | 29 | import java.util.Map; |
| ... | ... | @@ -100,53 +100,36 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { |
| 100 | 100 | */ |
| 101 | 101 | @Test |
| 102 | 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 | 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 | 139 | */ |
| 157 | 140 | @Test |
| 158 | 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 | 186 | */ |
| 181 | 187 | @Test |
| 182 | 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 | 227 | */ |
| 198 | 228 | @Test |
| 199 | 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 | 268 | */ |
| 215 | 269 | @Test |
| 216 | 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 | 311 | */ |
| 234 | 312 | @Test |
| 235 | 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 | 387 | WebSocketConnectOptions options = new WebSocketConnectOptions() |
| 292 | 388 | .setHost(SERVER_HOST) |
| 293 | 389 | .setPort(SERVER_PORT) |
| 294 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | 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 | 12 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec; |
| 13 | 14 | import io.vertx.core.Vertx; |
| 14 | -import io.vertx.core.http.HttpClient; | |
| 15 | 15 | import io.vertx.core.http.WebSocket; |
| 16 | +import io.vertx.core.http.WebSocketClient; | |
| 16 | 17 | import io.vertx.core.http.WebSocketConnectOptions; |
| 17 | 18 | import lombok.extern.slf4j.Slf4j; |
| 18 | 19 | import org.junit.jupiter.api.AfterAll; |
| 19 | 20 | import org.junit.jupiter.api.BeforeAll; |
| 20 | 21 | import org.junit.jupiter.api.Test; |
| 21 | 22 | |
| 22 | -import java.nio.charset.StandardCharsets; | |
| 23 | 23 | import java.util.concurrent.CountDownLatch; |
| 24 | 24 | import java.util.concurrent.TimeUnit; |
| 25 | 25 | import java.util.concurrent.atomic.AtomicReference; |
| ... | ... | @@ -87,52 +87,35 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { |
| 87 | 87 | */ |
| 88 | 88 | @Test |
| 89 | 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 | 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 | 125 | */ |
| 143 | 126 | @Test |
| 144 | 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 | 170 | */ |
| 164 | 171 | @Test |
| 165 | 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 | 224 | WebSocketConnectOptions options = new WebSocketConnectOptions() |
| 199 | 225 | .setHost(SERVER_HOST) |
| 200 | 226 | .setPort(SERVER_PORT) |
| 201 | 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 | } | ... | ... |