Commit b0ab37cc0134f994f8c2cf7bc4dc34f46dd54cc5

Authored by YunaiV
1 parent 136da4eb

feat:【iot】TCP 协议:增加 2 套单测(暂未完善,等本周补充完)

yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/binary/IotDirectDeviceTcpBinaryProtocolIntegrationTest.java 0 → 100644
  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/resources/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/binary/tcp-binary-packet-examples.md
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/json/IotDirectDeviceTcpJsonProtocolIntegrationTest.java 0 → 100644
  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/resources/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/json/tcp-json-packet-examples.md