Commit 4003f4b0280adcbec03fe6e1a43ad1f1de891759

Authored by YunaiV
1 parent b0ab37cc

feat:【iot】TCP 协议:1)合并单测,通过 codec 切换;2)兼容下行的时候,基于连接的 codec 处理

Showing 10 changed files with 255 additions and 633 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
... ... @@ -115,11 +115,9 @@ public class IotGatewayConfiguration {
115 115 @Bean
116 116 public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
117 117 IotDeviceMessageService messageService,
118   - IotDeviceService deviceService,
119 118 IotTcpConnectionManager connectionManager,
120 119 IotMessageBus messageBus) {
121   - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,
122   - messageBus);
  120 + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
123 121 }
124 122  
125 123 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
... ... @@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
9   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
10 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11 10 import jakarta.annotation.PostConstruct;
12 11 import lombok.RequiredArgsConstructor;
... ... @@ -25,8 +24,6 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic
25 24  
26 25 private final IotDeviceMessageService messageService;
27 26  
28   - private final IotDeviceService deviceService;
29   -
30 27 private final IotTcpConnectionManager connectionManager;
31 28  
32 29 private final IotMessageBus messageBus;
... ... @@ -36,7 +33,7 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic
36 33 @PostConstruct
37 34 public void init() {
38 35 // 初始化下游处理器
39   - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager);
  36 + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager);
40 37  
41 38 messageBus.register(this);
42 39 log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
... ... @@ -78,7 +78,7 @@ public class IotTcpConnectionManager {
78 78 */
79 79 public boolean isAuthenticated(NetSocket socket) {
80 80 ConnectionInfo info = connectionMap.get(socket);
81   - return info != null && info.isAuthenticated();
  81 + return info != null;
82 82 }
83 83  
84 84 /**
... ... @@ -96,17 +96,11 @@ public class IotTcpConnectionManager {
96 96 }
97 97  
98 98 /**
99   - * 检查设备是否在线
  99 + * 根据设备 ID 获取连接信息
100 100 */
101   - public boolean isDeviceOnline(Long deviceId) {
102   - return deviceSocketMap.containsKey(deviceId);
103   - }
104   -
105   - /**
106   - * 检查设备是否离线
107   - */
108   - public boolean isDeviceOffline(Long deviceId) {
109   - return !isDeviceOnline(deviceId);
  101 + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
  102 + NetSocket socket = deviceSocketMap.get(deviceId);
  103 + return socket != null ? connectionMap.get(socket) : null;
110 104 }
111 105  
112 106 /**
... ... @@ -158,11 +152,6 @@ public class IotTcpConnectionManager {
158 152 * 消息编解码类型(认证后确定)
159 153 */
160 154 private String codecType;
161   - // TODO @haohao:有没可能不要 authenticated 字段,通过 deviceId 或者其他的?进一步简化,想的是哈。
162   - /**
163   - * 是否已认证
164   - */
165   - private boolean authenticated;
166 155  
167 156 }
168 157  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
2 2  
3   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
4 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 4 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
6   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
7 5 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 6 import lombok.RequiredArgsConstructor;
9 7 import lombok.extern.slf4j.Slf4j;
... ... @@ -19,8 +17,6 @@ public class IotTcpDownstreamHandler {
19 17  
20 18 private final IotDeviceMessageService deviceMessageService;
21 19  
22   - private final IotDeviceService deviceService;
23   -
24 20 private final IotTcpConnectionManager connectionManager;
25 21  
26 22 /**
... ... @@ -31,21 +27,16 @@ public class IotTcpDownstreamHandler {
31 27 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
32 28 message.getDeviceId(), message.getMethod(), message.getId());
33 29  
34   - // 1.1 获取设备信息
35   - IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
36   - if (deviceInfo == null) {
37   - log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId());
38   - return;
39   - }
40   - // 1.2 检查设备是否在线
41   - if (connectionManager.isDeviceOffline(message.getDeviceId())) {
42   - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
  30 + // 1. 获取连接信息(包含 codecType)
  31 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
  32 + message.getDeviceId());
  33 + if (connectionInfo == null) {
  34 + log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
43 35 return;
44 36 }
45 37  
46   - // 2. 根据产品 Key 和设备名称编码消息,并发送到设备
47   - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
48   - deviceInfo.getDeviceName());
  38 + // 2. 使用连接时的 codecType 编码消息,并发送到设备
  39 + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
49 40 boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
50 41 if (success) {
51 42 log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
... ... @@ -230,8 +230,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
230 230 private String getMessageCodecType(Buffer buffer, NetSocket socket) {
231 231 // 1. 如果已认证,优先使用缓存的编解码类型
232 232 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
233   - if (connectionInfo != null && connectionInfo.isAuthenticated() &&
234   - StrUtil.isNotBlank(connectionInfo.getCodecType())) {
  233 + if (connectionInfo != null
  234 + && StrUtil.isNotBlank(connectionInfo.getCodecType())) {
235 235 return connectionInfo.getCodecType();
236 236 }
237 237  
... ... @@ -255,8 +255,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
255 255 .setProductKey(device.getProductKey())
256 256 .setDeviceName(device.getDeviceName())
257 257 .setClientId(clientId)
258   - .setCodecType(codecType)
259   - .setAuthenticated(true);
  258 + .setCodecType(codecType);
260 259 // 注册连接
261 260 connectionManager.registerConnection(socket, device.getId(), connectionInfo);
262 261 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.HexUtil;
  5 +import cn.hutool.core.util.IdUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  9 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  13 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
  14 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +import org.junit.jupiter.api.Test;
  17 +
  18 +import java.io.InputStream;
  19 +import java.io.OutputStream;
  20 +import java.net.Socket;
  21 +
  22 +/**
  23 + * IoT 直连设备 TCP 协议集成测试(手动测试)
  24 + *
  25 + * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台
  26 + *
  27 + * <p>支持两种编解码格式:
  28 + * <ul>
  29 + * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
  30 + * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
  31 + * </ul>
  32 + *
  33 + * <p>使用步骤:
  34 + * <ol>
  35 + * <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
  36 + * <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
  37 + * <li>运行以下测试方法:
  38 + * <ul>
  39 + * <li>{@link #testAuth()} - 设备认证</li>
  40 + * <li>{@link #testPropertyPost()} - 设备属性上报</li>
  41 + * <li>{@link #testEventPost()} - 设备事件上报</li>
  42 + * </ul>
  43 + * </li>
  44 + * </ol>
  45 + *
  46 + * <p>注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
  47 + *
  48 + * @author 芋道源码
  49 + */
  50 +@Slf4j
  51 +public class IotDirectDeviceTcpProtocolIntegrationTest {
  52 +
  53 + private static final String SERVER_HOST = "127.0.0.1";
  54 + private static final int SERVER_PORT = 8091;
  55 + private static final int TIMEOUT_MS = 5000;
  56 +
  57 + // ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
  58 +// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
  59 + private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
  60 +
  61 + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
  62 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  63 + private static final String DEVICE_NAME = "small";
  64 + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
  65 +
  66 + // ===================== 认证测试 =====================
  67 +
  68 + /**
  69 + * 认证测试:设备认证
  70 + */
  71 + @Test
  72 + public void testAuth() throws Exception {
  73 + // 1.1 构建认证消息
  74 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  75 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  76 + .setClientId(authInfo.getClientId())
  77 + .setUsername(authInfo.getUsername())
  78 + .setPassword(authInfo.getPassword());
  79 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  80 + // 1.2 编码
  81 + byte[] payload = CODEC.encode(request);
  82 + log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
  83 + if (CODEC instanceof IotTcpBinaryDeviceMessageCodec) {
  84 + log.info("[testAuth][二进制数据包(HEX): {}]", HexUtil.encodeHexStr(payload));
  85 + }
  86 +
  87 + // 2.1 发送请求
  88 + try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
  89 + socket.setSoTimeout(TIMEOUT_MS);
  90 + byte[] responseBytes = sendAndReceive(socket, payload);
  91 + // 2.2 解码响应
  92 + if (responseBytes != null) {
  93 + IotDeviceMessage response = CODEC.decode(responseBytes);
  94 + log.info("[testAuth][响应消息: {}]", response);
  95 + } else {
  96 + log.warn("[testAuth][未收到响应]");
  97 + }
  98 + }
  99 + }
  100 +
  101 + // ===================== 直连设备属性上报测试 =====================
  102 +
  103 + /**
  104 + * 属性上报测试
  105 + */
  106 + @Test
  107 + public void testPropertyPost() throws Exception {
  108 + try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
  109 + socket.setSoTimeout(TIMEOUT_MS);
  110 +
  111 + // 1. 先进行认证
  112 + IotDeviceMessage authResponse = authenticate(socket);
  113 + log.info("[testPropertyPost][认证响应: {}]", authResponse);
  114 +
  115 + // 2.1 构建属性上报消息
  116 + IotDeviceMessage request = IotDeviceMessage.of(
  117 + IdUtil.fastSimpleUUID(),
  118 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  119 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  120 + .put("width", 1)
  121 + .put("height", "2")
  122 + .build()),
  123 + null, null, null);
  124 + // 2.2 编码
  125 + byte[] payload = CODEC.encode(request);
  126 + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  127 +
  128 + // 3.1 发送请求
  129 + byte[] responseBytes = sendAndReceive(socket, payload);
  130 + // 3.2 解码响应
  131 + if (responseBytes != null) {
  132 + IotDeviceMessage response = CODEC.decode(responseBytes);
  133 + log.info("[testPropertyPost][响应消息: {}]", response);
  134 + } else {
  135 + log.warn("[testPropertyPost][未收到响应]");
  136 + }
  137 + }
  138 + }
  139 +
  140 + // ===================== 直连设备事件上报测试 =====================
  141 +
  142 + /**
  143 + * 事件上报测试
  144 + */
  145 + @Test
  146 + public void testEventPost() throws Exception {
  147 + try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
  148 + socket.setSoTimeout(TIMEOUT_MS);
  149 +
  150 + // 1. 先进行认证
  151 + IotDeviceMessage authResponse = authenticate(socket);
  152 + log.info("[testEventPost][认证响应: {}]", authResponse);
  153 +
  154 + // 2.1 构建事件上报消息
  155 + IotDeviceMessage request = IotDeviceMessage.of(
  156 + IdUtil.fastSimpleUUID(),
  157 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  158 + IotDeviceEventPostReqDTO.of(
  159 + "eat",
  160 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  161 + System.currentTimeMillis()),
  162 + null, null, null);
  163 + // 2.2 编码
  164 + byte[] payload = CODEC.encode(request);
  165 + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  166 +
  167 + // 3.1 发送请求
  168 + byte[] responseBytes = sendAndReceive(socket, payload);
  169 + // 3.2 解码响应
  170 + if (responseBytes != null) {
  171 + IotDeviceMessage response = CODEC.decode(responseBytes);
  172 + log.info("[testEventPost][响应消息: {}]", response);
  173 + } else {
  174 + log.warn("[testEventPost][未收到响应]");
  175 + }
  176 + }
  177 + }
  178 +
  179 + // ===================== 辅助方法 =====================
  180 +
  181 + /**
  182 + * 执行设备认证
  183 + *
  184 + * @param socket TCP 连接
  185 + * @return 认证响应消息
  186 + */
  187 + private IotDeviceMessage authenticate(Socket socket) throws Exception {
  188 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  189 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  190 + .setClientId(authInfo.getClientId())
  191 + .setUsername(authInfo.getUsername())
  192 + .setPassword(authInfo.getPassword());
  193 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  194 + byte[] payload = CODEC.encode(request);
  195 + byte[] responseBytes = sendAndReceive(socket, payload);
  196 + if (responseBytes != null) {
  197 + log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]",
  198 + responseBytes.length,
  199 + String.format("%02X", responseBytes[0]),
  200 + HexUtil.encodeHexStr(responseBytes));
  201 + return CODEC.decode(responseBytes);
  202 + }
  203 + return null;
  204 + }
  205 +
  206 + /**
  207 + * 发送 TCP 请求并接收响应
  208 + *
  209 + * @param socket TCP Socket
  210 + * @param payload 请求数据
  211 + * @return 响应数据
  212 + */
  213 + private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
  214 + // 1. 发送请求
  215 + OutputStream out = socket.getOutputStream();
  216 + InputStream in = socket.getInputStream();
  217 + out.write(payload);
  218 + out.flush();
  219 +
  220 + // 2.1 等待一小段时间让服务器处理
  221 + Thread.sleep(100);
  222 + // 2.2 接收响应
  223 + byte[] buffer = new byte[4096];
  224 + try {
  225 + int length = in.read(buffer);
  226 + if (length > 0) {
  227 + byte[] response = new byte[length];
  228 + System.arraycopy(buffer, 0, response, 0, length);
  229 + return response;
  230 + }
  231 + return null;
  232 + } catch (java.net.SocketTimeoutException e) {
  233 + log.warn("[sendAndReceive][接收响应超时]");
  234 + return null;
  235 + }
  236 + }
  237 +
  238 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/binary/IotDirectDeviceTcpBinaryProtocolIntegrationTest.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.binary;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5   -import cn.hutool.core.util.StrUtil;
6   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
7   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
8   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9   -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10   -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
12   -import io.vertx.core.buffer.Buffer;
13   -import lombok.extern.slf4j.Slf4j;
14   -import org.junit.jupiter.api.Test;
15   -
16   -import java.io.InputStream;
17   -import java.io.OutputStream;
18   -import java.net.Socket;
19   -import java.nio.charset.StandardCharsets;
20   -
21   -/**
22   - * IoT 直连设备 TCP 二进制协议集成测试(手动测试)
23   - *
24   - * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 二进制协议直接连接平台
25   - *
26   - * <p>使用步骤:
27   - * <ol>
28   - * <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
29   - * <li>运行 {@link #testAuth()} 获取设备认证,认证成功后连接保持</li>
30   - * <li>运行以下测试方法:
31   - * <ul>
32   - * <li>{@link #testPropertyPost()} - 设备属性上报</li>
33   - * <li>{@link #testEventPost()} - 设备事件上报</li>
34   - * </ul>
35   - * </li>
36   - * </ol>
37   - *
38   - * <p>注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
39   - *
40   - * <p>二进制协议格式说明请参考:{@code tcp-binary-packet-examples.md}
41   - *
42   - * @author 芋道源码
43   - */
44   -@Slf4j
45   -public class IotDirectDeviceTcpBinaryProtocolIntegrationTest {
46   -
47   - private static final String SERVER_HOST = "127.0.0.1";
48   - private static final int SERVER_PORT = 8091;
49   - private static final int TIMEOUT_MS = 5000;
50   -
51   - // ===================== 二进制协议常量 =====================
52   - /**
53   - * 协议魔术字,用于协议识别
54   - */
55   - private static final byte MAGIC_NUMBER = (byte) 0x7E;
56   -
57   - /**
58   - * 协议版本号
59   - */
60   - private static final byte PROTOCOL_VERSION = (byte) 0x01;
61   -
62   - /**
63   - * 请求消息类型
64   - */
65   - private static final byte REQUEST = (byte) 0x01;
66   -
67   - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
68   - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
69   - private static final String DEVICE_NAME = "small";
70   - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
71   -
72   - // ===================== 认证测试 =====================
73   -
74   - /**
75   - * 认证测试:设备认证(二进制格式)
76   - */
77   - @Test
78   - public void testAuth() throws Exception {
79   - // 1.1 构建认证参数
80   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
81   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
82   - .setClientId(authInfo.getClientId())
83   - .setUsername(authInfo.getUsername())
84   - .setPassword(authInfo.getPassword());
85   - // 1.2 构建二进制请求
86   - String messageId = IdUtil.fastSimpleUUID();
87   - String method = "auth";
88   - byte[] payload = buildBinaryRequest(messageId, method, authReqDTO);
89   - // 1.3 输出请求
90   - log.info("[testAuth][消息ID: {}, 方法: {}, 参数: {}]", messageId, method, JsonUtils.toJsonString(authReqDTO));
91   - log.info("[testAuth][二进制数据包长度: {} 字节]", payload.length);
92   - log.info("[testAuth][二进制数据包(HEX): {}]", bytesToHex(payload));
93   -
94   - // 2.1 发送请求
95   - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
96   - socket.setSoTimeout(TIMEOUT_MS);
97   - byte[] response = sendAndReceiveBinary(socket, payload);
98   - // 2.2 输出结果
99   - if (response != null) {
100   - log.info("[testAuth][响应数据包长度: {} 字节]", response.length);
101   - log.info("[testAuth][响应数据包(HEX): {}]", bytesToHex(response));
102   - parseBinaryResponse(response);
103   - } else {
104   - log.warn("[testAuth][未收到响应]");
105   - }
106   - }
107   - }
108   -
109   - // ===================== 直连设备属性上报测试 =====================
110   -
111   - /**
112   - * 属性上报测试(二进制格式)
113   - *
114   - * 注意:TCP 协议需要先认证,这里为了简化测试,在同一连接上先认证再上报
115   - */
116   - @Test
117   - public void testPropertyPost() throws Exception {
118   - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
119   - socket.setSoTimeout(TIMEOUT_MS);
120   -
121   - // 1. 先进行认证
122   - byte[] authResponse = authenticateBinary(socket);
123   - log.info("[testPropertyPost][认证响应长度: {} 字节]", authResponse != null ? authResponse.length : 0);
124   - if (authResponse != null) {
125   - parseBinaryResponse(authResponse);
126   - }
127   -
128   - // 2.1 构建属性上报请求
129   - String messageId = IdUtil.fastSimpleUUID();
130   - String method = IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod();
131   - Object params = IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
132   - .put("width", 1)
133   - .put("height", "2")
134   - .build());
135   - byte[] payload = buildBinaryRequest(messageId, method, params);
136   - // 2.2 输出请求
137   - log.info("[testPropertyPost][消息ID: {}, 方法: {}, 参数: {}]", messageId, method, JsonUtils.toJsonString(params));
138   - log.info("[testPropertyPost][二进制数据包长度: {} 字节]", payload.length);
139   -
140   - // 3.1 发送请求
141   - byte[] response = sendAndReceiveBinary(socket, payload);
142   - // 3.2 输出结果
143   - if (response != null) {
144   - log.info("[testPropertyPost][响应数据包长度: {} 字节]", response.length);
145   - parseBinaryResponse(response);
146   - } else {
147   - log.warn("[testPropertyPost][未收到响应]");
148   - }
149   - }
150   - }
151   -
152   - // ===================== 直连设备事件上报测试 =====================
153   -
154   - /**
155   - * 事件上报测试(二进制格式)
156   - *
157   - * 注意:TCP 协议需要先认证,这里为了简化测试,在同一连接上先认证再上报
158   - */
159   - @Test
160   - public void testEventPost() throws Exception {
161   - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
162   - socket.setSoTimeout(TIMEOUT_MS);
163   -
164   - // 1. 先进行认证
165   - byte[] authResponse = authenticateBinary(socket);
166   - log.info("[testEventPost][认证响应长度: {} 字节]", authResponse != null ? authResponse.length : 0);
167   - if (authResponse != null) {
168   - parseBinaryResponse(authResponse);
169   - }
170   -
171   - // 2.1 构建事件上报请求
172   - String messageId = IdUtil.fastSimpleUUID();
173   - String method = IotDeviceMessageMethodEnum.EVENT_POST.getMethod();
174   - Object params = IotDeviceEventPostReqDTO.of(
175   - "eat",
176   - MapUtil.<String, Object>builder().put("rice", 3).build(),
177   - System.currentTimeMillis());
178   - byte[] payload = buildBinaryRequest(messageId, method, params);
179   - // 2.2 输出请求
180   - log.info("[testEventPost][消息ID: {}, 方法: {}, 参数: {}]", messageId, method, JsonUtils.toJsonString(params));
181   - log.info("[testEventPost][二进制数据包长度: {} 字节]", payload.length);
182   -
183   - // 3.1 发送请求
184   - byte[] response = sendAndReceiveBinary(socket, payload);
185   - // 3.2 输出结果
186   - if (response != null) {
187   - log.info("[testEventPost][响应数据包长度: {} 字节]", response.length);
188   - parseBinaryResponse(response);
189   - } else {
190   - log.warn("[testEventPost][未收到响应]");
191   - }
192   - }
193   - }
194   -
195   - // ===================== 辅助方法 =====================
196   -
197   - /**
198   - * 执行设备认证(二进制格式)
199   - *
200   - * @param socket TCP 连接
201   - * @return 认证响应
202   - */
203   - private byte[] authenticateBinary(Socket socket) throws Exception {
204   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
205   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
206   - .setClientId(authInfo.getClientId())
207   - .setUsername(authInfo.getUsername())
208   - .setPassword(authInfo.getPassword());
209   - String messageId = IdUtil.fastSimpleUUID();
210   - byte[] payload = buildBinaryRequest(messageId, "auth", authReqDTO);
211   - return sendAndReceiveBinary(socket, payload);
212   - }
213   -
214   - /**
215   - * 构建二进制请求数据包
216   - *
217   - * <p>协议格式:
218   - * <pre>
219   - * +--------+--------+--------+---------------------------+
220   - * | 魔术字 | 版本号 | 消息类型| 消息长度(4字节) |
221   - * +--------+--------+--------+---------------------------+
222   - * | 消息ID长度(2字节) | 消息ID(变长字符串) |
223   - * +--------+--------+--------+--------+--------+--------+
224   - * | 方法名长度(2字节) | 方法名(变长字符串) |
225   - * +--------+--------+--------+--------+--------+--------+
226   - * | 消息体数据(变长) |
227   - * +--------+--------+--------+--------+--------+--------+
228   - * </pre>
229   - *
230   - * @param messageId 消息 ID
231   - * @param method 方法名
232   - * @param params 请求参数
233   - * @return 二进制数据包
234   - */
235   - private byte[] buildBinaryRequest(String messageId, String method, Object params) {
236   - Buffer buffer = Buffer.buffer();
237   - // 1. 写入协议头部
238   - buffer.appendByte(MAGIC_NUMBER);
239   - buffer.appendByte(PROTOCOL_VERSION);
240   - buffer.appendByte(REQUEST);
241   - // 2. 预留消息长度位置
242   - int lengthPosition = buffer.length();
243   - buffer.appendInt(0);
244   - // 3. 写入消息 ID
245   - byte[] messageIdBytes = StrUtil.utf8Bytes(messageId);
246   - buffer.appendShort((short) messageIdBytes.length);
247   - buffer.appendBytes(messageIdBytes);
248   - // 4. 写入方法名
249   - byte[] methodBytes = StrUtil.utf8Bytes(method);
250   - buffer.appendShort((short) methodBytes.length);
251   - buffer.appendBytes(methodBytes);
252   - // 5. 写入消息体(params 序列化为 JSON)
253   - if (params != null) {
254   - buffer.appendBytes(JsonUtils.toJsonByte(params));
255   - }
256   - // 6. 更新消息长度
257   - buffer.setInt(lengthPosition, buffer.length());
258   - return buffer.getBytes();
259   - }
260   -
261   - /**
262   - * 解析二进制响应数据包
263   - *
264   - * @param data 响应数据
265   - */
266   - private void parseBinaryResponse(byte[] data) {
267   - if (data == null || data.length < 11) {
268   - log.warn("[parseBinaryResponse][数据包过短]");
269   - return;
270   - }
271   -
272   - Buffer buffer = Buffer.buffer(data);
273   - int index = 0;
274   -
275   - // 1. 解析魔术字
276   - byte magic = buffer.getByte(index++);
277   - log.info("[parseBinaryResponse][魔术字: 0x{} (预期: 0x7E)]", String.format("%02X", magic));
278   -
279   - // 2. 解析版本号
280   - byte version = buffer.getByte(index++);
281   - log.info("[parseBinaryResponse][版本号: 0x{}]", String.format("%02X", version));
282   -
283   - // 3. 解析消息类型
284   - byte messageType = buffer.getByte(index++);
285   - log.info("[parseBinaryResponse][消息类型: 0x{} (0x01=请求, 0x02=响应)]", String.format("%02X", messageType));
286   -
287   - // 4. 解析消息长度
288   - int messageLength = buffer.getInt(index);
289   - index += 4;
290   - log.info("[parseBinaryResponse][消息长度: {}]", messageLength);
291   -
292   - // 5. 解析消息 ID
293   - short messageIdLength = buffer.getShort(index);
294   - index += 2;
295   - String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name());
296   - index += messageIdLength;
297   - log.info("[parseBinaryResponse][消息ID: {}]", messageId);
298   -
299   - // 6. 解析方法名
300   - short methodLength = buffer.getShort(index);
301   - index += 2;
302   - String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name());
303   - index += methodLength;
304   - log.info("[parseBinaryResponse][方法名: {}]", method);
305   -
306   - // 7. 解析消息体
307   - if (messageType == 0x02) { // RESPONSE
308   - // 响应消息:code(4) + msgLen(2) + msg + data(JSON)
309   - if (index + 4 <= buffer.length()) {
310   - int code = buffer.getInt(index);
311   - index += 4;
312   - log.info("[parseBinaryResponse][响应码: {}]", code);
313   -
314   - if (index + 2 <= buffer.length()) {
315   - short msgLength = buffer.getShort(index);
316   - index += 2;
317   - if (msgLength > 0 && index + msgLength <= buffer.length()) {
318   - String msg = buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name());
319   - index += msgLength;
320   - log.info("[parseBinaryResponse][响应消息: {}]", msg);
321   - }
322   -
323   - if (index < buffer.length()) {
324   - String dataJson = buffer.getString(index, buffer.length(), StandardCharsets.UTF_8.name());
325   - log.info("[parseBinaryResponse][响应数据: {}]", dataJson);
326   - }
327   - }
328   - }
329   - } else {
330   - // 请求消息:params(JSON)
331   - if (index < buffer.length()) {
332   - String paramsJson = buffer.getString(index, buffer.length(), StandardCharsets.UTF_8.name());
333   - log.info("[parseBinaryResponse][请求参数: {}]", paramsJson);
334   - }
335   - }
336   - }
337   -
338   - /**
339   - * 发送二进制请求并接收响应
340   - *
341   - * @param socket TCP Socket
342   - * @param payload 二进制数据包
343   - * @return 响应数据
344   - */
345   - private byte[] sendAndReceiveBinary(Socket socket, byte[] payload) throws Exception {
346   - // 1. 发送请求
347   - OutputStream out = socket.getOutputStream();
348   - InputStream in = socket.getInputStream();
349   - out.write(payload);
350   - out.flush();
351   -
352   - // 2.1 等待一小段时间让服务器处理
353   - Thread.sleep(100);
354   - // 2.2 接收响应
355   - byte[] buffer = new byte[4096];
356   - try {
357   - int length = in.read(buffer);
358   - if (length > 0) {
359   - byte[] response = new byte[length];
360   - System.arraycopy(buffer, 0, response, 0, length);
361   - return response;
362   - }
363   - return null;
364   - } catch (java.net.SocketTimeoutException e) {
365   - log.warn("[sendAndReceiveBinary][接收响应超时]");
366   - return null;
367   - }
368   - }
369   -
370   - // TODO @AI:hutool 简化下;
371   - /**
372   - * 字节数组转十六进制字符串
373   - *
374   - * @param bytes 字节数组
375   - * @return 十六进制字符串
376   - */
377   - private String bytesToHex(byte[] bytes) {
378   - StringBuilder sb = new StringBuilder();
379   - for (byte b : bytes) {
380   - sb.append(String.format("%02X ", b));
381   - }
382   - return sb.toString().trim();
383   - }
384   -
385   -}
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/json/IotDirectDeviceTcpJsonProtocolIntegrationTest.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.json;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8   -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9   -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11   -import lombok.extern.slf4j.Slf4j;
12   -import org.junit.jupiter.api.Test;
13   -
14   -import java.io.InputStream;
15   -import java.io.OutputStream;
16   -import java.net.Socket;
17   -import java.nio.charset.StandardCharsets;
18   -
19   -/**
20   - * IoT 直连设备 TCP JSON 协议集成测试(手动测试)
21   - *
22   - * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP JSON 协议直接连接平台
23   - *
24   - * <p>使用步骤:
25   - * <ol>
26   - * <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
27   - * <li>运行 {@link #testAuth()} 获取设备认证,认证成功后连接保持</li>
28   - * <li>运行以下测试方法:
29   - * <ul>
30   - * <li>{@link #testPropertyPost()} - 设备属性上报</li>
31   - * <li>{@link #testEventPost()} - 设备事件上报</li>
32   - * </ul>
33   - * </li>
34   - * </ol>
35   - *
36   - * <p>注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
37   - *
38   - * @author 芋道源码
39   - */
40   -@Slf4j
41   -public class IotDirectDeviceTcpJsonProtocolIntegrationTest {
42   -
43   - private static final String SERVER_HOST = "127.0.0.1";
44   - private static final int SERVER_PORT = 8091;
45   - private static final int TIMEOUT_MS = 5000;
46   -
47   - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
48   - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
49   - private static final String DEVICE_NAME = "small";
50   - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
51   -
52   - // ===================== 认证测试 =====================
53   -
54   - /**
55   - * 认证测试:设备认证
56   - */
57   - @Test
58   - public void testAuth() throws Exception {
59   - // 1.1 构建请求
60   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
61   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
62   - .setClientId(authInfo.getClientId())
63   - .setUsername(authInfo.getUsername())
64   - .setPassword(authInfo.getPassword());
65   - String payload = JsonUtils.toJsonString(MapUtil.builder()
66   - .put("id", IdUtil.fastSimpleUUID())
67   - .put("method", "auth")
68   - .put("params", authReqDTO)
69   - .put("timestamp", System.currentTimeMillis())
70   - .build());
71   - // 1.2 输出请求
72   - log.info("[testAuth][请求体: {}]", payload);
73   -
74   - // 2.1 发送请求
75   - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
76   - socket.setSoTimeout(TIMEOUT_MS);
77   - String response = sendAndReceive(socket, payload);
78   - // 2.2 输出结果
79   - log.info("[testAuth][响应体: {}]", response);
80   - }
81   - }
82   -
83   - // ===================== 直连设备属性上报测试 =====================
84   -
85   - /**
86   - * 属性上报测试
87   - */
88   - @Test
89   - public void testPropertyPost() throws Exception {
90   - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
91   - socket.setSoTimeout(TIMEOUT_MS);
92   -
93   - // 1. 先进行认证
94   - String authResponse = authenticate(socket);
95   - log.info("[testPropertyPost][认证响应: {}]", authResponse);
96   -
97   - // 2.1 构建属性上报请求
98   - String payload = JsonUtils.toJsonString(MapUtil.builder()
99   - .put("id", IdUtil.fastSimpleUUID())
100   - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
101   - .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
102   - .put("width", 1)
103   - .put("height", "2")
104   - .build()))
105   - .put("timestamp", System.currentTimeMillis())
106   - .build());
107   - // 2.2 输出请求
108   - log.info("[testPropertyPost][请求体: {}]", payload);
109   -
110   - // 3.1 发送请求
111   - String response = sendAndReceive(socket, payload);
112   - // 3.2 输出结果
113   - log.info("[testPropertyPost][响应体: {}]", response);
114   - }
115   - }
116   -
117   - // ===================== 直连设备事件上报测试 =====================
118   -
119   - /**
120   - * 事件上报测试
121   - */
122   - @Test
123   - public void testEventPost() throws Exception {
124   - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
125   - socket.setSoTimeout(TIMEOUT_MS);
126   -
127   - // 1. 先进行认证
128   - String authResponse = authenticate(socket);
129   - log.info("[testEventPost][认证响应: {}]", authResponse);
130   -
131   - // 2.1 构建事件上报请求
132   - String payload = JsonUtils.toJsonString(MapUtil.builder()
133   - .put("id", IdUtil.fastSimpleUUID())
134   - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
135   - .put("params", IotDeviceEventPostReqDTO.of(
136   - "eat",
137   - MapUtil.<String, Object>builder().put("rice", 3).build(),
138   - System.currentTimeMillis()))
139   - .put("timestamp", System.currentTimeMillis())
140   - .build());
141   - // 2.2 输出请求
142   - log.info("[testEventPost][请求体: {}]", payload);
143   -
144   - // 3.1 发送请求
145   - String response = sendAndReceive(socket, payload);
146   - // 3.2 输出结果
147   - log.info("[testEventPost][响应体: {}]", response);
148   - }
149   - }
150   -
151   - // ===================== 辅助方法 =====================
152   -
153   - /**
154   - * 执行设备认证
155   - *
156   - * @param socket TCP 连接
157   - * @return 认证响应
158   - */
159   - private String authenticate(Socket socket) throws Exception {
160   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
161   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
162   - .setClientId(authInfo.getClientId())
163   - .setUsername(authInfo.getUsername())
164   - .setPassword(authInfo.getPassword());
165   - String payload = JsonUtils.toJsonString(MapUtil.builder()
166   - .put("id", IdUtil.fastSimpleUUID())
167   - .put("method", "auth")
168   - .put("params", authReqDTO)
169   - .put("timestamp", System.currentTimeMillis())
170   - .build());
171   - return sendAndReceive(socket, payload);
172   - }
173   -
174   - /**
175   - * 发送 TCP 请求并接收响应
176   - *
177   - * @param socket TCP Socket
178   - * @param payload 请求体
179   - * @return 响应内容
180   - */
181   - private String sendAndReceive(Socket socket, String payload) throws Exception {
182   - // 1. 发送请求
183   - OutputStream out = socket.getOutputStream();
184   - InputStream in = socket.getInputStream();
185   - byte[] sendData = payload.getBytes(StandardCharsets.UTF_8);
186   - out.write(sendData);
187   - out.flush();
188   -
189   - // 2.1 等待一小段时间让服务器处理
190   - Thread.sleep(100);
191   - // 2.2 接收响应
192   - byte[] buffer = new byte[4096];
193   - try {
194   - int length = in.read(buffer);
195   - if (length > 0) {
196   - return new String(buffer, 0, length, StandardCharsets.UTF_8);
197   - }
198   - return null;
199   - } catch (java.net.SocketTimeoutException e) {
200   - log.warn("[sendAndReceive][接收响应超时]");
201   - return null;
202   - }
203   - }
204   -
205   -}
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/binary/tcp-binary-packet-examples.md renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/json/tcp-json-packet-examples.md renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md