From bd8052f56b5254fb073d4b396fcb8326897c79c0 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Tue, 22 Jul 2025 00:11:46 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90IoT=20=E7=89=A9=E8=81=94?= =?UTF-8?q?=E7=BD=91=E3=80=91=E6=96=B0=E5=A2=9E=20TCP=20=E4=BA=8C=E8=BF=9B?= =?UTF-8?q?=E5=88=B6=E5=92=8C=20JSON=20=E7=BC=96=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E9=87=8D=E6=9E=84=20TCP=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 378 +++++++++++++ .../gateway/codec/tcp/IotTcpCodecManager.java | 136 +++++ .../codec/tcp/IotTcpDeviceMessageCodec.java | 389 -------------- .../tcp/IotTcpJsonDeviceMessageCodec.java | 245 +++++++++ .../config/IotGatewayConfiguration.java | 16 +- .../tcp/IotTcpDownstreamSubscriber.java | 140 +---- .../protocol/tcp/IotTcpUpstreamProtocol.java | 129 +---- .../protocol/tcp/client/TcpDeviceClient.java | 220 -------- .../manager/TcpDeviceConnectionManager.java | 506 ------------------ .../protocol/tcp/protocol/TcpDataDecoder.java | 98 ---- .../protocol/tcp/protocol/TcpDataEncoder.java | 159 ------ .../protocol/tcp/protocol/TcpDataPackage.java | 160 ------ .../protocol/tcp/protocol/TcpDataReader.java | 162 ------ .../tcp/router/IotTcpDownstreamHandler.java | 336 +----------- .../tcp/router/IotTcpUpstreamHandler.java | 389 ++------------ .../tcp/TcpBinaryDataPacketExamples.java | 219 ++++++++ .../codec/tcp/TcpJsonDataPacketExamples.java | 253 +++++++++ .../resources/tcp-binary-packet-examples.md | 222 ++++++++ .../resources/tcp-json-packet-examples.md | 286 ++++++++++ 19 files changed, 1868 insertions(+), 2575 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java new file mode 100644 index 0000000000..40c8fcede4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -0,0 +1,378 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import io.vertx.core.buffer.Buffer; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 + * + * 使用自定义二进制协议格式: + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { + + /** + * 编解码器类型 + */ + public static final String TYPE = "TCP_BINARY"; + + // ==================== 常量定义 ==================== + + @Override + public byte[] encode(IotDeviceMessage message) { + if (message == null || StrUtil.isEmpty(message.getMethod())) { + throw new IllegalArgumentException("消息或方法不能为空"); + } + + try { + // 1. 确定功能码(只支持数据上报和心跳) + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? + TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; + + // 2. 构建简化负载 + String payload = buildSimplePayload(message); + + // 3. 构建 TCP 数据包 + String deviceAddr = message.getDeviceId() != null ? String.valueOf(message.getDeviceId()) : "default"; + short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); + TcpDataPackage dataPackage = new TcpDataPackage(deviceAddr, code, mid, payload); + + // 4. 编码为字节流 + return encodeTcpDataPackage(dataPackage).getBytes(); + } catch (Exception e) { + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); + throw new TcpCodecException("TCP 消息编码失败", e); + } + } + + @Override + public IotDeviceMessage decode(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("待解码数据不能为空"); + } + + try { + // 1. 解码 TCP 数据包 + TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); + + // 2. 根据功能码确定方法 + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? + MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; + + // 3. 解析负载数据和请求ID + PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); + + // 4. 构建 IoT 设备消息(设置完整的必要参数) + IotDeviceMessage message = IotDeviceMessage.requestOf( + payloadInfo.getRequestId(), method, payloadInfo.getParams()); + + // 5. 设置设备相关信息 + Long deviceId = parseDeviceId(dataPackage.getAddr()); + message.setDeviceId(deviceId); + + // 6. 设置TCP协议相关信息 + message.setServerId(generateServerId(dataPackage)); + + // 7. 设置租户ID(TODO: 后续可以从设备信息中获取) + // message.setTenantId(getTenantIdByDeviceId(deviceId)); + + if (log.isDebugEnabled()) { + log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}", + deviceId, method, message.getRequestId(), message.getId()); + } + + return message; + } catch (Exception e) { + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); + throw new TcpCodecException("TCP 消息解码失败", e); + } + } + + @Override + public String type() { + return TYPE; + } + + /** + * 构建完整负载 + */ + private String buildSimplePayload(IotDeviceMessage message) { + JSONObject payload = new JSONObject(); + + // 核心字段 + payload.set(PayloadField.METHOD, message.getMethod()); + if (message.getParams() != null) { + payload.set(PayloadField.PARAMS, message.getParams()); + } + + // 标识字段 + if (StrUtil.isNotEmpty(message.getRequestId())) { + payload.set(PayloadField.REQUEST_ID, message.getRequestId()); + } + if (StrUtil.isNotEmpty(message.getId())) { + payload.set(PayloadField.MESSAGE_ID, message.getId()); + } + + // 时间戳 + payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); + + return payload.toString(); + } + + + + // ==================== 编解码方法 ==================== + + /** + * 解析负载信息(包含requestId和params) + */ + private PayloadInfo parsePayloadInfo(String payload) { + if (StrUtil.isEmpty(payload)) { + return new PayloadInfo(null, null); + } + + try { + JSONObject jsonObject = JSONUtil.parseObj(payload); + String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); + if (StrUtil.isEmpty(requestId)) { + requestId = jsonObject.getStr(PayloadField.MESSAGE_ID); + } + Object params = jsonObject.get(PayloadField.PARAMS); + return new PayloadInfo(requestId, params); + } catch (Exception e) { + log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload); + return new PayloadInfo(null, payload); + } + } + + /** + * 从设备地址解析设备ID + * + * @param deviceAddr 设备地址字符串 + * @return 设备ID + */ + private Long parseDeviceId(String deviceAddr) { + if (StrUtil.isEmpty(deviceAddr)) { + log.warn("[parseDeviceId][设备地址为空,返回默认ID]"); + return 0L; + } + + try { + // 尝试直接解析为Long + return Long.parseLong(deviceAddr); + } catch (NumberFormatException e) { + // 如果不是纯数字,可以使用哈希值或其他策略 + log.warn("[parseDeviceId][设备地址不是数字格式: {},使用哈希值]", deviceAddr); + return (long) deviceAddr.hashCode(); + } + } + + /** + * 生成服务ID + * + * @param dataPackage TCP数据包 + * @return 服务ID + */ + private String generateServerId(TcpDataPackage dataPackage) { + // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务ID + return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid()); + } + + // ==================== 内部辅助方法 ==================== + + /** + * 编码 TCP 数据包 + * + * @param dataPackage 数据包对象 + * @return 编码后的字节流 + * @throws IllegalArgumentException 如果数据包对象不正确 + */ + private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { + if (dataPackage == null) { + throw new IllegalArgumentException("数据包对象不能为空"); + } + + // 验证数据包 + if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { + throw new IllegalArgumentException("设备地址不能为空"); + } + if (dataPackage.getPayload() == null) { + throw new IllegalArgumentException("负载不能为空"); + } + + try { + Buffer buffer = Buffer.buffer(); + + // 1. 计算包体长度(除了包头 4 字节) + int payloadLength = dataPackage.getPayload().getBytes().length; + int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; + + // 2.1 写入包头:总长度(4 字节) + buffer.appendInt(totalLength); + // 2.2 写入设备地址长度(2 字节) + buffer.appendShort((short) dataPackage.getAddr().length()); + // 2.3 写入设备地址(不定长) + buffer.appendBytes(dataPackage.getAddr().getBytes()); + // 2.4 写入功能码(2 字节) + buffer.appendShort(dataPackage.getCode()); + // 2.5 写入消息序号(2 字节) + buffer.appendShort(dataPackage.getMid()); + // 2.6 写入包体数据(不定长) + buffer.appendBytes(dataPackage.getPayload().getBytes()); + + if (log.isDebugEnabled()) { + log.debug("[encodeTcpDataPackage][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", + dataPackage.getAddr(), dataPackage.getCode(), dataPackage.getMid(), buffer.length()); + } + return buffer; + } catch (Exception e) { + log.error("[encodeTcpDataPackage][编码失败] 数据包: {}", dataPackage, e); + throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); + } + } + + /** + * 解码 TCP 数据包 + * + * @param buffer 数据缓冲区 + * @return 解码后的数据包 + * @throws IllegalArgumentException 如果数据包格式不正确 + */ + private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { + if (buffer == null || buffer.length() < 8) { + throw new IllegalArgumentException("数据包长度不足"); + } + + try { + int index = 0; + + // 1.1 跳过包头(4字节) + index += 4; + + // 1.2 获取设备地址长度(2字节) + short addrLength = buffer.getShort(index); + index += 2; + + // 1.3 获取设备地址 + String addr = buffer.getBuffer(index, index + addrLength).toString(); + index += addrLength; + + // 1.4 获取功能码(2字节) + short code = buffer.getShort(index); + index += 2; + + // 1.5 获取消息序号(2字节) + short mid = buffer.getShort(index); + index += 2; + + // 1.6 获取包体数据 + String payload = ""; + if (index < buffer.length()) { + payload = buffer.getString(index, buffer.length()); + } + + // 2. 构建数据包对象 + TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload); + + if (log.isDebugEnabled()) { + log.debug("[decodeTcpDataPackage][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", + addr, code, mid, payload.length()); + } + return dataPackage; + } catch (Exception e) { + log.error("[decodeTcpDataPackage][解码失败] 数据长度: {}", buffer.length(), e); + throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); + } + } + + /** + * 消息方法常量 + */ + public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + } + + /** + * 负载字段名 + */ + private static class PayloadField { + public static final String METHOD = "method"; + public static final String PARAMS = "params"; + public static final String TIMESTAMP = "timestamp"; + public static final String REQUEST_ID = "requestId"; + public static final String MESSAGE_ID = "msgId"; + } + + // ==================== TCP 数据包编解码方法 ==================== + + /** + * 负载信息类 + */ + private static class PayloadInfo { + private String requestId; + private Object params; + + public PayloadInfo(String requestId, Object params) { + this.requestId = requestId; + this.params = params; + } + + public String getRequestId() { return requestId; } + public Object getParams() { return params; } + } + + /** + * TCP 数据包内部类 + */ + @Data + private static class TcpDataPackage { + // 功能码定义 + public static final short CODE_REGISTER = 10; + public static final short CODE_REGISTER_REPLY = 11; + public static final short CODE_HEARTBEAT = 20; + public static final short CODE_HEARTBEAT_REPLY = 21; + public static final short CODE_MESSAGE_UP = 30; + public static final short CODE_MESSAGE_DOWN = 40; + + private String addr; + private short code; + private short mid; + private String payload; + + public TcpDataPackage(String addr, short code, short mid, String payload) { + this.addr = addr; + this.code = code; + this.mid = mid; + this.payload = payload; + } + } + + // ==================== 自定义异常 ==================== + + /** + * TCP 编解码异常 + */ + public static class TcpCodecException extends RuntimeException { + + public TcpCodecException(String message) { + super(message); + } + + public TcpCodecException(String message, Throwable cause) { + super(message, cause); + } + + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java new file mode 100644 index 0000000000..aa789c689a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java @@ -0,0 +1,136 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * TCP编解码器管理器(简化版) + * + * 核心功能: + * - 自动协议检测(二进制 vs JSON) + * - 统一编解码接口 + * - 默认使用JSON协议 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpCodecManager implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP"; + + @Autowired + private IotTcpBinaryDeviceMessageCodec binaryCodec; + + @Autowired + private IotTcpJsonDeviceMessageCodec jsonCodec; + + /** + * 当前默认协议(JSON) + */ + private boolean useJsonByDefault = true; + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + // 默认使用JSON协议编码 + return jsonCodec.encode(message); + } + + @Override + public IotDeviceMessage decode(byte[] bytes) { + // 自动检测协议类型并解码 + if (isJsonFormat(bytes)) { + if (log.isDebugEnabled()) { + log.debug("[decode][检测到JSON协议] 数据长度: {}字节", bytes.length); + } + return jsonCodec.decode(bytes); + } else { + if (log.isDebugEnabled()) { + log.debug("[decode][检测到二进制协议] 数据长度: {}字节", bytes.length); + } + return binaryCodec.decode(bytes); + } + } + + // ==================== 便捷方法 ==================== + + /** + * 使用JSON协议编码 + */ + public byte[] encodeJson(IotDeviceMessage message) { + return jsonCodec.encode(message); + } + + /** + * 使用二进制协议编码 + */ + public byte[] encodeBinary(IotDeviceMessage message) { + return binaryCodec.encode(message); + } + + /** + * 获取当前默认协议 + */ + public String getDefaultProtocol() { + return useJsonByDefault ? "JSON" : "BINARY"; + } + + /** + * 设置默认协议 + */ + public void setDefaultProtocol(boolean useJson) { + this.useJsonByDefault = useJson; + log.info("[setDefaultProtocol][设置默认协议] 使用JSON: {}", useJson); + } + + // ==================== 内部方法 ==================== + + /** + * 检测是否为JSON格式 + * + * 检测规则: + * 1. 数据以 '{' 开头 + * 2. 包含 "method" 或 "id" 字段 + */ + private boolean isJsonFormat(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return useJsonByDefault; + } + + try { + // 检测JSON格式:以 '{' 开头 + if (bytes[0] == '{') { + // 进一步验证是否为有效JSON + String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100)); + return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\""); + } + + // 检测二进制格式:长度 >= 8 且符合二进制协议结构 + if (bytes.length >= 8) { + // 读取包头(前4字节表示后续数据长度) + int expectedLength = ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + + // 验证长度是否合理 + if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) { + return false; // 二进制格式 + } + } + } catch (Exception e) { + log.warn("[isJsonFormat][协议检测异常] 使用默认协议: {}", getDefaultProtocol(), e); + } + + // 默认使用当前设置的协议类型 + return useJsonByDefault; + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java deleted file mode 100644 index 6a558b5141..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java +++ /dev/null @@ -1,389 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONException; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -/** - * TCP {@link IotDeviceMessage} 编解码器 - *

- * 参考 EMQX 设计理念: - * 1. 高性能编解码 - * 2. 容错机制 - * 3. 缓存优化 - * 4. 监控统计 - * 5. 资源管理 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { - - /** - * 编解码器类型 - */ - public static final String TYPE = "tcp"; - - // ==================== 方法映射 ==================== - - /** - * 消息方法到功能码的映射 - */ - private static final Map METHOD_TO_CODE_MAP = new ConcurrentHashMap<>(); - - /** - * 功能码到消息方法的映射 - */ - private static final Map CODE_TO_METHOD_MAP = new ConcurrentHashMap<>(); - - static { - // 初始化方法映射 - // TODO @haohao:有没可能去掉这个 code 到 method 的映射哈? - initializeMethodMappings(); - } - - // ==================== 缓存管理 ==================== - - /** - * JSON 缓存,提升编解码性能 - */ - private final Map jsonCache = new ConcurrentHashMap<>(); - - /** - * 缓存最大大小 - */ - private static final int MAX_CACHE_SIZE = 1000; - - // ==================== 常量定义 ==================== - - /** - * 负载字段名 - */ - public static class PayloadField { - - public static final String TIMESTAMP = "timestamp"; - public static final String MESSAGE_ID = "msgId"; - public static final String DEVICE_ID = "deviceId"; - public static final String PARAMS = "params"; - public static final String DATA = "data"; - public static final String CODE = "code"; - public static final String MESSAGE = "message"; - - } - - /** - * 消息方法映射 - */ - public static class MessageMethod { - - public static final String PROPERTY_POST = "thing.property.post"; - public static final String PROPERTY_SET = "thing.property.set"; - public static final String PROPERTY_GET = "thing.property.get"; - public static final String EVENT_POST = "thing.event.post"; - public static final String SERVICE_INVOKE = "thing.service.invoke"; - public static final String CONFIG_PUSH = "thing.config.push"; - public static final String OTA_UPGRADE = "thing.ota.upgrade"; - public static final String STATE_ONLINE = "thing.state.online"; - public static final String STATE_OFFLINE = "thing.state.offline"; - - } - - // ==================== 初始化方法 ==================== - - /** - * 初始化方法映射 - */ - private static void initializeMethodMappings() { - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP); - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET); - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET); - METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP); - METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE); - METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN); - METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN); - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT); - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT); - - // 反向映射 - METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method)); - } - - // ==================== 编解码方法 ==================== - - @Override - public byte[] encode(IotDeviceMessage message) { - validateEncodeParams(message); - - try { - if (log.isDebugEnabled()) { - log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}", - message.getMethod(), message.getRequestId()); - } - - // 1. 获取功能码 - short code = getCodeByMethodSafely(message.getMethod()); - - // 2. 构建负载 - String payload = buildPayloadOptimized(message); - - // 3. 构建 TCP 数据包 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr("") - .code(code) - .mid((short) 0) - .payload(payload) - .build(); - - // 4. 编码为字节流 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] result = buffer.getBytes(); - - // 5. 统计信息 - if (log.isDebugEnabled()) { - log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}", - message.getMethod(), result.length); - } - return result; - } catch (Exception e) { - log.error("[encode][TCP 消息编码失败] 消息: {}", message, e); - throw new TcpCodecException("TCP 消息编码失败", e); - } - } - - @Override - public IotDeviceMessage decode(byte[] bytes) { - validateDecodeParams(bytes); - - try { - if (log.isDebugEnabled()) { - log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length); - } - - // 1. 解码 TCP 数据包 - Buffer buffer = Buffer.buffer(bytes); - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - // 2. 获取消息方法 - String method = getMethodByCodeSafely(dataPackage.getCode()); - // 3. 解析负载数据 - Object params = parsePayloadOptimized(dataPackage.getPayload()); - // 4. 构建 IoT 设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); - - // 5. 统计信息 - if (log.isDebugEnabled()) { - log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}", - method, dataPackage.getCode()); - } - return message; - } catch (Exception e) { - log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}", - bytes.length, truncateData(bytes, 100), e); - throw new TcpCodecException("TCP 消息解码失败", e); - } - } - - @Override - public String type() { - return TYPE; - } - - // ==================== 内部辅助方法 ==================== - - /** - * 验证编码参数 - */ - private void validateEncodeParams(IotDeviceMessage message) { - if (Objects.isNull(message)) { - throw new IllegalArgumentException("IoT 设备消息不能为空"); - } - if (StrUtil.isEmpty(message.getMethod())) { - throw new IllegalArgumentException("消息方法不能为空"); - } - } - - /** - * 验证解码参数 - */ - private void validateDecodeParams(byte[] bytes) { - if (Objects.isNull(bytes) || bytes.length == 0) { - throw new IllegalArgumentException("待解码数据不能为空"); - } - if (bytes.length > 1024 * 1024) { - throw new IllegalArgumentException("数据包过大,超过 1MB 限制"); - } - } - - /** - * 安全获取功能码 - */ - private short getCodeByMethodSafely(String method) { - Short code = METHOD_TO_CODE_MAP.get(method); - // 默认为数据上报 - if (code == null) { - log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method); - return TcpDataPackage.CODE_DATA_UP; - } - return code; - } - - /** - * 安全获取消息方法 - */ - private String getMethodByCodeSafely(short code) { - String method = CODE_TO_METHOD_MAP.get(code); - if (method == null) { - log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code); - return MessageMethod.PROPERTY_POST; // 默认为属性上报 - } - return method; - } - - /** - * 优化的负载构建 - */ - private String buildPayloadOptimized(IotDeviceMessage message) { - // 使用缓存键 - // TODO @haohao:是不是不用缓存哈? - String cacheKey = message.getMethod() + "_" + message.getRequestId(); - JSONObject cachedPayload = jsonCache.get(cacheKey); - - if (cachedPayload != null) { - // 更新时间戳 - cachedPayload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - return cachedPayload.toString(); - } - - // 创建新的负载 - JSONObject payload = new JSONObject(); - // 添加基础字段 - addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId()); - addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId()); - addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams()); - addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData()); - addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode()); - addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg()); - // 添加时间戳 - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - - // 缓存管理 - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(cacheKey, payload); - } else { - cleanJsonCacheIfNeeded(); - } - - return payload.toString(); - } - - /** - * 优化的负载解析 - */ - private Object parsePayloadOptimized(String payload) { - if (StrUtil.isEmpty(payload)) { - return null; - } - - try { - // 尝试从缓存获取 - JSONObject cachedJson = jsonCache.get(payload); - if (cachedJson != null) { - return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson; - } - - // 解析 JSON 对象 - JSONObject jsonObject = JSONUtil.parseObj(payload); - - // 缓存解析结果 - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(payload, jsonObject); - } - - return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject; - } catch (JSONException e) { - log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload); - return payload; - } catch (Exception e) { - log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e); - return payload; - } - } - - /** - * 添加非空值到负载 - */ - private void addToPayloadIfNotNull(JSONObject json, String key, Object value) { - if (ObjectUtil.isNotNull(value)) { - json.set(key, value); - } - } - - /** - * 添加非空字符串到负载 - */ - private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) { - if (StrUtil.isNotEmpty(value)) { - json.set(key, value); - } - } - - /** - * 清理JSON缓存 - */ - private void cleanJsonCacheIfNeeded() { - if (jsonCache.size() > MAX_CACHE_SIZE) { - // 清理一半的缓存 - int clearCount = jsonCache.size() / 2; - jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5); - - if (log.isDebugEnabled()) { - log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size()); - } - } - } - - /** - * 截断数据用于日志输出 - */ - private String truncateData(byte[] data, int maxLength) { - if (data.length <= maxLength) { - return new String(data, StandardCharsets.UTF_8); - } - - byte[] truncated = new byte[maxLength]; - System.arraycopy(data, 0, truncated, 0, maxLength); - return new String(truncated, StandardCharsets.UTF_8) + "...(截断)"; - } - - // ==================== 自定义异常 ==================== - - /** - * TCP 编解码异常 - */ - public static class TcpCodecException extends RuntimeException { - - public TcpCodecException(String message) { - super(message); - } - - public TcpCodecException(String message, Throwable cause) { - super(message, cause); - } - - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java new file mode 100644 index 0000000000..ac8a3d174d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -0,0 +1,245 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * TCP JSON格式 {@link IotDeviceMessage} 编解码器 + * + * 采用纯JSON格式传输,参考EMQX和HTTP模块的数据格式 + * + * JSON消息格式: + * { + * "id": "消息ID", + * "method": "消息方法", + * "deviceId": "设备ID", + * "productKey": "产品Key", + * "deviceName": "设备名称", + * "params": {...}, + * "timestamp": 时间戳 + * } + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP_JSON"; + + // ==================== 常量定义 ==================== + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + if (message == null || StrUtil.isEmpty(message.getMethod())) { + throw new IllegalArgumentException("消息或方法不能为空"); + } + + try { + // 构建JSON消息 + JSONObject jsonMessage = buildJsonMessage(message); + + // 转换为字节数组 + String jsonString = jsonMessage.toString(); + byte[] result = jsonString.getBytes(StandardCharsets.UTF_8); + + if (log.isDebugEnabled()) { + log.debug("[encode][编码成功] 方法: {}, JSON长度: {}字节, 内容: {}", + message.getMethod(), result.length, jsonString); + } + + return result; + } catch (Exception e) { + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); + throw new RuntimeException("JSON消息编码失败", e); + } + } + + // ==================== 编解码方法 ==================== + + @Override + public IotDeviceMessage decode(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("待解码数据不能为空"); + } + + try { + // 转换为JSON字符串 + String jsonString = new String(bytes, StandardCharsets.UTF_8); + + if (log.isDebugEnabled()) { + log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString); + } + + // 解析JSON消息 + JSONObject jsonMessage = JSONUtil.parseObj(jsonString); + + // 构建IoT设备消息 + IotDeviceMessage message = parseJsonMessage(jsonMessage); + + if (log.isDebugEnabled()) { + log.debug("[decode][解码成功] 消息ID: {}, 方法: {}, 设备ID: {}", + message.getId(), message.getMethod(), message.getDeviceId()); + } + + return message; + } catch (Exception e) { + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); + throw new RuntimeException("JSON消息解码失败", e); + } + } + + /** + * 编码数据上报消息 + */ + public byte[] encodeDataReport(Object params, Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = createMessage(MessageMethod.PROPERTY_POST, params, deviceId, productKey, deviceName); + return encode(message); + } + + /** + * 编码心跳消息 + */ + public byte[] encodeHeartbeat(Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = createMessage(MessageMethod.STATE_ONLINE, null, deviceId, productKey, deviceName); + return encode(message); + } + + // ==================== 便捷方法 ==================== + + /** + * 编码事件上报消息 + */ + public byte[] encodeEventReport(Object params, Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = createMessage(MessageMethod.EVENT_POST, params, deviceId, productKey, deviceName); + return encode(message); + } + + /** + * 构建JSON消息 + */ + private JSONObject buildJsonMessage(IotDeviceMessage message) { + JSONObject jsonMessage = new JSONObject(); + + // 基础字段 + jsonMessage.set(JsonField.ID, StrUtil.isNotEmpty(message.getId()) ? message.getId() : IdUtil.fastSimpleUUID()); + jsonMessage.set(JsonField.METHOD, message.getMethod()); + jsonMessage.set(JsonField.TIMESTAMP, System.currentTimeMillis()); + + // 设备信息 + if (message.getDeviceId() != null) { + jsonMessage.set(JsonField.DEVICE_ID, message.getDeviceId()); + } + + // 参数 + if (message.getParams() != null) { + jsonMessage.set(JsonField.PARAMS, message.getParams()); + } + + // 响应码和消息(用于下行消息) + if (message.getCode() != null) { + jsonMessage.set(JsonField.CODE, message.getCode()); + } + if (StrUtil.isNotEmpty(message.getMsg())) { + jsonMessage.set(JsonField.MESSAGE, message.getMsg()); + } + + return jsonMessage; + } + + /** + * 解析JSON消息 + */ + private IotDeviceMessage parseJsonMessage(JSONObject jsonMessage) { + // 提取基础字段 + String id = jsonMessage.getStr(JsonField.ID); + String method = jsonMessage.getStr(JsonField.METHOD); + Object params = jsonMessage.get(JsonField.PARAMS); + + // 创建消息对象 + IotDeviceMessage message = IotDeviceMessage.requestOf(id, method, params); + + // 设置设备信息 + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); + if (deviceId != null) { + message.setDeviceId(deviceId); + } + + // 设置响应信息 + Integer code = jsonMessage.getInt(JsonField.CODE); + if (code != null) { + message.setCode(code); + } + + String msg = jsonMessage.getStr(JsonField.MESSAGE); + if (StrUtil.isNotEmpty(msg)) { + message.setMsg(msg); + } + + // 设置服务ID(基于JSON格式) + message.setServerId(generateServerId(jsonMessage)); + + return message; + } + + // ==================== 内部辅助方法 ==================== + + /** + * 创建消息对象 + */ + private IotDeviceMessage createMessage(String method, Object params, Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); + message.setDeviceId(deviceId); + return message; + } + + /** + * 生成服务ID + */ + private String generateServerId(JSONObject jsonMessage) { + String id = jsonMessage.getStr(JsonField.ID); + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); + return String.format("tcp_json_%s_%s", deviceId != null ? deviceId : "unknown", + StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId"); + } + + /** + * 消息方法常量 + */ + public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + public static final String EVENT_POST = "thing.event.post"; // 事件上报 + public static final String PROPERTY_SET = "thing.property.set"; // 属性设置 + public static final String PROPERTY_GET = "thing.property.get"; // 属性获取 + public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用 + } + + /** + * JSON字段名(参考EMQX和HTTP模块格式) + */ + private static class JsonField { + public static final String ID = "id"; + public static final String METHOD = "method"; + public static final String DEVICE_ID = "deviceId"; + public static final String PRODUCT_KEY = "productKey"; + public static final String DEVICE_NAME = "deviceName"; + public static final String PARAMS = "params"; + public static final String TIMESTAMP = "timestamp"; + public static final String CODE = "code"; + public static final String MESSAGE = "message"; + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index de5f3426be..cd878994c7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; @@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -89,28 +89,22 @@ public class IotGatewayConfiguration { return Vertx.vertx(); } - @Bean - public TcpDeviceConnectionManager tcpDeviceConnectionManager() { - return new TcpDeviceConnectionManager(); - } - @Bean public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - TcpDeviceConnectionManager connectionManager, IotDeviceService deviceService, IotDeviceMessageService messageService, IotDeviceCommonApi deviceApi, + IotTcpCodecManager codecManager, Vertx tcpVertx) { - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager, - deviceService, messageService, deviceApi, tcpVertx); + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), + deviceService, messageService, deviceApi, codecManager, tcpVertx); } @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - TcpDeviceConnectionManager connectionManager, IotDeviceMessageService messageService, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus); + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index 3f47e14080..95d435387e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -4,161 +4,67 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - /** * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 - *

- * 参考 EMQX 设计理念: - * 1. 高性能消息路由 - * 2. 容错机制 - * 3. 状态监控 - * 4. 资源管理 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - private final IotTcpUpstreamProtocol protocolHandler; - - private final TcpDeviceConnectionManager connectionManager; - - private final IotDeviceMessageService messageService; + private final IotTcpDownstreamHandler downstreamHandler; private final IotMessageBus messageBus; - private volatile IotTcpDownstreamHandler downstreamHandler; + private final IotTcpUpstreamProtocol protocol; - private final AtomicBoolean initialized = new AtomicBoolean(false); - - private final AtomicLong processedMessages = new AtomicLong(0); - - private final AtomicLong failedMessages = new AtomicLong(0); + public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, + IotDeviceMessageService messageService, + IotMessageBus messageBus) { + this.protocol = protocol; + this.messageBus = messageBus; + this.downstreamHandler = new IotTcpDownstreamHandler(messageService); + } @PostConstruct public void init() { - if (!initialized.compareAndSet(false, true)) { - log.warn("[init][TCP 下游消息订阅者已初始化,跳过重复初始化]"); - return; - } - - try { - // 初始化下游处理器 - downstreamHandler = new IotTcpDownstreamHandler(connectionManager, messageService); - - // 注册到消息总线 - messageBus.register(this); - - log.info("[init][TCP 下游消息订阅者初始化完成] Topic: {}, Group: {}", - getTopic(), getGroup()); - } catch (Exception e) { - initialized.set(false); - log.error("[init][TCP 下游消息订阅者初始化失败]", e); - throw new RuntimeException("TCP 下游消息订阅者初始化失败", e); - } - } - - @PreDestroy - public void destroy() { - if (!initialized.get()) { - return; - } - - try { - log.info("[destroy][TCP 下游消息订阅者已关闭] 处理消息数: {}, 失败消息数: {}", - processedMessages.get(), failedMessages.get()); - } catch (Exception e) { - log.error("[destroy][TCP 下游消息订阅者关闭失败]", e); - } finally { - initialized.set(false); - } + messageBus.register(this); } @Override public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId()); + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); } @Override public String getGroup() { - return "tcp-downstream-" + protocolHandler.getServerId(); + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); } @Override public void onMessage(IotDeviceMessage message) { - if (!initialized.get()) { - log.warn("[onMessage][订阅者未初始化,跳过消息处理]"); - return; - } - - long startTime = System.currentTimeMillis(); - + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); try { - processedMessages.incrementAndGet(); - - if (log.isDebugEnabled()) { - log.debug("[onMessage][收到下行消息] 设备 ID: {}, 方法: {}, 消息ID: {}", - message.getDeviceId(), message.getMethod(), message.getId()); - } - // 参数校验 - if (message.getDeviceId() == null) { - log.warn("[onMessage][下行消息设备 ID 为空,跳过处理] 消息: {}", message); - return; - } - // 检查连接状态 - if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) { - log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}", - message.getDeviceId(), message.getMethod()); + // 1. 校验 + String method = message.getMethod(); + if (method == null) { + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", + message.getId(), message.getDeviceId()); return; } - // 处理下行消息 + // 2. 处理下行消息 downstreamHandler.handle(message); - - // 性能监控 - long processTime = System.currentTimeMillis() - startTime; - // TODO @haohao:1000 搞成静态变量; - if (processTime > 1000) { // 超过 1 秒的慢消息 - log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms", - message.getDeviceId(), message.getMethod(), processTime); - } } catch (Exception e) { - failedMessages.incrementAndGet(); - log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}", - message.getDeviceId(), message.getMethod(), message, e); + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId(), e); } } - - // TODO @haohao:多余的要不先清理掉; - - /** - * 获取订阅者统计信息 - */ - public String getSubscriberStatistics() { - return String.format("TCP下游订阅者 - 已处理: %d, 失败: %d, 成功率: %.2f%%", - processedMessages.get(), - failedMessages.get(), - processedMessages.get() > 0 - ? (double) (processedMessages.get() - failedMessages.get()) / processedMessages.get() * 100 - : 0.0); - } - - /** - * 检查订阅者健康状态 - */ - public boolean isHealthy() { - return initialized.get() && downstreamHandler != null; - } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java index f9d4bd2d26..0e2ad6c4e1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; @@ -16,19 +16,8 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - /** * IoT 网关 TCP 协议:接收设备上行消息 - *

- * 负责接收设备上行消息,支持: - * 1. 设备注册 - * 2. 心跳保活 - * 3. 属性上报 - * 4. 事件上报 - * 5. 设备连接管理 * * @author 芋道源码 */ @@ -37,14 +26,14 @@ public class IotTcpUpstreamProtocol { private final IotGatewayProperties.TcpProperties tcpProperties; - private final TcpDeviceConnectionManager connectionManager; - private final IotDeviceService deviceService; private final IotDeviceMessageService messageService; private final IotDeviceCommonApi deviceApi; + private final IotTcpCodecManager codecManager; + private final Vertx vertx; @Getter @@ -53,54 +42,30 @@ public class IotTcpUpstreamProtocol { private NetServer netServer; public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, - TcpDeviceConnectionManager connectionManager, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotDeviceCommonApi deviceApi, - Vertx vertx) { + IotDeviceService deviceService, + IotDeviceMessageService messageService, + IotDeviceCommonApi deviceApi, + IotTcpCodecManager codecManager, + Vertx vertx) { this.tcpProperties = tcpProperties; - this.connectionManager = connectionManager; this.deviceService = deviceService; this.messageService = messageService; this.deviceApi = deviceApi; + this.codecManager = codecManager; this.vertx = vertx; this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); } @PostConstruct public void start() { - // 1. 启动 TCP 服务器 - try { - startTcpServer(); - log.info("[start][IoT 网关 TCP 协议处理器启动完成,服务器ID: {}]", serverId); - } catch (Exception e) { - log.error("[start][IoT 网关 TCP 协议处理器启动失败]", e); - // 抛出异常,中断 Spring 容器启动 - throw new RuntimeException("IoT 网关 TCP 协议处理器启动失败", e); - } - } - - @PreDestroy - public void stop() { - if (netServer != null) { - stopTcpServer(); - log.info("[stop][IoT 网关 TCP 协议处理器已停止]"); - } - } - - /** - * 启动 TCP 服务器 - */ - private void startTcpServer() { - // TODO @haohao:同类的,最好使用相同序号前缀,一个方法看起来有段落感。包括同类可以去掉之间的空格。例如说这里的,1. 2. 3. 4. 是初始化;5. 6. 是管理启动 - // 1. 创建服务器选项 + // 创建服务器选项 NetServerOptions options = new NetServerOptions() .setPort(tcpProperties.getPort()) .setTcpKeepAlive(true) .setTcpNoDelay(true) .setReuseAddress(true); - // 2. 配置 SSL(如果启用) + // 配置 SSL(如果启用) if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() .setKeyPath(tcpProperties.getSslKeyPath()) @@ -108,72 +73,32 @@ public class IotTcpUpstreamProtocol { options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } - // 3. 创建 TCP 服务器 + // 创建服务器并设置连接处理器 netServer = vertx.createNetServer(options); - - // 4. 设置连接处理器 netServer.connectHandler(socket -> { - log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress()); - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler( - tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId); + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager); handler.handle(socket); }); - // 5. 同步启动服务器,等待结果 - CountDownLatch latch = new CountDownLatch(1); - AtomicReference failure = new AtomicReference<>(); - netServer.listen(result -> { - if (result.succeeded()) { - log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}", - result.result().actualPort(), serverId); - } else { - log.error("[startTcpServer][TCP 服务器启动失败]", result.cause()); - failure.set(result.cause()); - } - latch.countDown(); - }); - - // 6. 等待启动结果,设置超时 + // 启动服务器 try { - if (!latch.await(10, TimeUnit.SECONDS)) { - throw new RuntimeException("TCP 服务器启动超时"); - } - if (failure.get() != null) { - throw new RuntimeException("TCP 服务器启动失败", failure.get()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("TCP 服务器启动被中断", e); + netServer.listen().result(); + log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 TCP 协议启动失败]", e); + throw e; } } - /** - * 停止 TCP 服务器 - */ - private void stopTcpServer() { - if (netServer == null) { - return; - } - log.info("[stopTcpServer][准备关闭 TCP 服务器]"); - CountDownLatch latch = new CountDownLatch(1); - // 异步关闭,并使用 Latch 等待结果 - netServer.close(result -> { - if (result.succeeded()) { - log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]"); - } else { - log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause()); + @PreDestroy + public void stop() { + if (netServer != null) { + try { + netServer.close().result(); + log.info("[stop][IoT 网关 TCP 协议已停止]"); + } catch (Exception e) { + log.error("[stop][IoT 网关 TCP 协议停止失败]", e); } - latch.countDown(); - }); - - try { - // 等待关闭完成,设置超时 - if (!latch.await(10, TimeUnit.SECONDS)) { - log.warn("[stopTcpServer][关闭 TCP 服务器超时]"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e); } } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java deleted file mode 100644 index f4d1761c9e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java +++ /dev/null @@ -1,220 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import io.vertx.core.parsetools.RecordParser; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * TCP 设备客户端:封装设备连接的基本信息和操作 - *

- * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。 - * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。 - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDeviceClient { - - @Getter - private final String clientId; - - @Getter - @Setter - private String deviceAddr; // 从 final 移除,因为在注册后才设置 - - @Getter - @Setter - private String productKey; - - @Getter - @Setter - private String deviceName; - - @Getter - @Setter - private Long deviceId; - - @Getter - private NetSocket socket; - - @Getter - @Setter - private RecordParser parser; - - @Getter - private final long keepAliveTimeoutMs; - - private volatile long lastKeepAliveTime; - - private final AtomicBoolean authenticated = new AtomicBoolean(false); - private final AtomicBoolean closed = new AtomicBoolean(false); - - /** - * 构造函数 - * - * @param clientId 客户端 ID,全局唯一 - * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取 - */ - public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) { - this.clientId = clientId; - this.keepAliveTimeoutMs = keepAliveTimeoutMs; - this.lastKeepAliveTime = System.currentTimeMillis(); - } - - /** - * 绑定网络套接字,并设置相关处理器。 - * 此方法应在 Vert.x Event Loop 线程中调用 - * - * @param socket 网络套接字 - */ - public void setSocket(NetSocket socket) { - // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理 - if (this.socket != null && this.socket != socket) { - log.warn("[setSocket][客户端({}) 正在用新的 socket 替换旧的,旧 socket 将被关闭]", clientId); - this.socket.close(); - } - this.socket = socket; - - // 注册处理器 - if (socket != null) { - // 1. 设置关闭处理器 - socket.closeHandler(v -> { - log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId); - shutdown(); // 统一调用 shutdown 进行资源清理 - }); - - // 2. 设置异常处理器 - socket.exceptionHandler(e -> { - log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e); - shutdown(); // 出现异常时也关闭连接 - }); - - // 3. 设置数据处理器 - socket.handler(buffer -> { - // 任何数据往来都表示连接是活跃的 - keepAlive(); - - if (parser != null) { - parser.handle(buffer); - } else { - log.warn("[setSocket][设备客户端({}) 未设置解析器(parser),原始数据被忽略: {}]", clientId, buffer.toString()); - } - }); - } - } - - /** - * 更新心跳时间,表示设备仍然活跃 - */ - public void keepAlive() { - this.lastKeepAliveTime = System.currentTimeMillis(); - } - - /** - * 检查连接是否在线 - * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内 - * - * @return 是否在线 - */ - public boolean isOnline() { - if (closed.get() || socket == null) { - return false; - } - long idleTime = System.currentTimeMillis() - lastKeepAliveTime; - return idleTime < keepAliveTimeoutMs; - } - - // TODO @haohao:1)是不是简化下:productKey 和 deviceName 非空,就认为是已认证;2)如果是的话,productKey 和 deviceName 搞成一个设置方法?setAuthenticated(productKey、deviceName) - - public boolean isAuthenticated() { - return authenticated.get(); - } - - public void setAuthenticated(boolean authenticated) { - this.authenticated.set(authenticated); - } - - /** - * 向设备发送消息 - * - * @param buffer 消息内容 - */ - public void sendMessage(Buffer buffer) { - if (closed.get() || socket == null) { - log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId); - return; - } - - // Vert.x 的 write 是异步的,不会阻塞 - socket.write(buffer, result -> { - // 发送失败可能意味着连接已断开,主动关闭 - if (!result.succeeded()) { - log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause()); - shutdown(); - return; - } - - // 发送成功也更新心跳,表示连接活跃 - if (log.isDebugEnabled()) { - log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); - } - keepAlive(); - }); - } - - // TODO @haohao:是不是叫 close 好点?或者问问大模型 - /** - * 关闭客户端连接并清理资源。 - * 这是一个幂等操作,可以被多次安全调用。 - */ - public void shutdown() { - // 使用原子操作保证只执行一次关闭逻辑 - if (closed.getAndSet(true)) { - return; - } - - log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId); - - // 先将 socket 引用置空,再关闭,避免并发问题 - NetSocket socketToClose = this.socket; - this.socket = null; - - if (socketToClose != null) { - try { - // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭 - socketToClose.close(); - } catch (Exception e) { - log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e); - } - } - - // 重置认证状态 - authenticated.set(false); - } - - public String getConnectionInfo() { - NetSocket currentSocket = this.socket; - if (currentSocket != null && currentSocket.remoteAddress() != null) { - return currentSocket.remoteAddress().toString(); - } - return "disconnected"; - } - - @Override - public String toString() { - return "TcpDeviceClient{" + - "clientId='" + clientId + '\'' + - ", deviceAddr='" + deviceAddr + '\'' + - ", deviceId=" + deviceId + - ", authenticated=" + authenticated.get() + - ", online=" + isOnline() + - ", connection=" + getConnectionInfo() + - '}'; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java deleted file mode 100644 index b2b6b3c31e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java +++ /dev/null @@ -1,506 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; - -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * TCP 设备连接管理器 - *

- * 参考 EMQX 设计理念: - * 1. 高性能连接管理 - * 2. 连接池和资源管理 - * 3. 流量控制 TODO @haohao:这个要不先去掉 - * 4. 监控统计 TODO @haohao:这个要不先去掉 - * 5. 自动清理和容错 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class TcpDeviceConnectionManager { - - // ==================== 连接存储 ==================== - - /** - * 设备客户端映射 - * Key: 设备地址, Value: 设备客户端 - */ - private final ConcurrentMap clientMap = new ConcurrentHashMap<>(); - - /** - * 设备ID到设备地址的映射 - * Key: 设备ID, Value: 设备地址 - */ - private final ConcurrentMap deviceIdToAddrMap = new ConcurrentHashMap<>(); - - /** - * 套接字到客户端的映射,用于快速查找 - * Key: NetSocket, Value: 设备地址 - */ - private final ConcurrentMap socketToAddrMap = new ConcurrentHashMap<>(); - - // ==================== 读写锁 ==================== - - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); - private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); - - // ==================== 定时任务 ==================== - - /** - * 定时任务执行器 - */ - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> { - Thread t = new Thread(r, "tcp-connection-manager"); - t.setDaemon(true); - return t; - }); - - // ==================== 统计信息 ==================== - - private final AtomicLong totalConnections = new AtomicLong(0); - private final AtomicLong totalDisconnections = new AtomicLong(0); - private final AtomicLong totalMessages = new AtomicLong(0); - private final AtomicLong totalFailedMessages = new AtomicLong(0); - private final AtomicLong totalBytes = new AtomicLong(0); - - // ==================== 配置参数 ==================== - - private static final int MAX_CONNECTIONS = 10000; - private static final int HEARTBEAT_CHECK_INTERVAL = 30; // 秒 - private static final int CONNECTION_CLEANUP_INTERVAL = 60; // 秒 - private static final int STATS_LOG_INTERVAL = 300; // 秒 - - /** - * 构造函数,启动定时任务 - */ - public TcpDeviceConnectionManager() { - startScheduledTasks(); - } - - /** - * 启动定时任务 - */ - private void startScheduledTasks() { - // 心跳检查任务 - scheduler.scheduleAtFixedRate(this::checkHeartbeat, - HEARTBEAT_CHECK_INTERVAL, HEARTBEAT_CHECK_INTERVAL, TimeUnit.SECONDS); - - // 连接清理任务 - scheduler.scheduleAtFixedRate(this::cleanupConnections, - CONNECTION_CLEANUP_INTERVAL, CONNECTION_CLEANUP_INTERVAL, TimeUnit.SECONDS); - - // 统计日志任务 - scheduler.scheduleAtFixedRate(this::logStatistics, - STATS_LOG_INTERVAL, STATS_LOG_INTERVAL, TimeUnit.SECONDS); - } - - /** - * 添加设备客户端 - */ - public boolean addClient(String deviceAddr, TcpDeviceClient client) { - // TODO @haohao:这个要不去掉;目前看着没做 result 的处理; - if (clientMap.size() >= MAX_CONNECTIONS) { - log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr); - return false; - } - - writeLock.lock(); - try { - log.info("[addClient][添加设备客户端: {}]", deviceAddr); - - // 关闭之前的连接(如果存在) - TcpDeviceClient existingClient = clientMap.get(deviceAddr); - if (existingClient != null) { - log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr); - removeClientInternal(deviceAddr, existingClient); - } - - // 添加新连接 - clientMap.put(deviceAddr, client); - - // 添加套接字映射 - if (client.getSocket() != null) { - socketToAddrMap.put(client.getSocket(), deviceAddr); - } - - // 如果客户端已设置设备 ID,更新映射 - if (client.getDeviceId() != null) { - deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr); - } - - totalConnections.incrementAndGet(); - return true; - } finally { - writeLock.unlock(); - } - } - - /** - * 移除设备客户端 - */ - public void removeClient(String deviceAddr) { - writeLock.lock(); - try { - TcpDeviceClient client = clientMap.get(deviceAddr); - if (client != null) { - removeClientInternal(deviceAddr, client); - } - } finally { - writeLock.unlock(); - } - } - - /** - * 内部移除客户端方法(无锁) - */ - private void removeClientInternal(String deviceAddr, TcpDeviceClient client) { - log.info("[removeClient][移除设备客户端: {}]", deviceAddr); - - // 从映射中移除 - clientMap.remove(deviceAddr); - - // 移除套接字映射 - if (client.getSocket() != null) { - socketToAddrMap.remove(client.getSocket()); - } - - // 移除设备ID映射 - if (client.getDeviceId() != null) { - deviceIdToAddrMap.remove(client.getDeviceId()); - } - - // 关闭连接 - client.shutdown(); - - totalDisconnections.incrementAndGet(); - } - - /** - * 通过设备地址获取客户端 - */ - public TcpDeviceClient getClient(String deviceAddr) { - readLock.lock(); - try { - return clientMap.get(deviceAddr); - } finally { - readLock.unlock(); - } - } - - /** - * 通过设备 ID 获取客户端 - */ - public TcpDeviceClient getClientByDeviceId(Long deviceId) { - readLock.lock(); - try { - String deviceAddr = deviceIdToAddrMap.get(deviceId); - return deviceAddr != null ? clientMap.get(deviceAddr) : null; - } finally { - readLock.unlock(); - } - } - - // TODO @haohao:getClientBySocket、isDeviceOnline、sendMessage、sendMessageByDeviceId、broadcastMessage 用不到的方法,要不先暂时不提供?保持简洁、更容易理解哈。 - - /** - * 通过网络连接获取客户端 - */ - public TcpDeviceClient getClientBySocket(NetSocket socket) { - readLock.lock(); - try { - String deviceAddr = socketToAddrMap.get(socket); - return deviceAddr != null ? clientMap.get(deviceAddr) : null; - } finally { - readLock.unlock(); - } - } - - /** - * 检查设备是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - TcpDeviceClient client = getClientByDeviceId(deviceId); - return client != null && client.isOnline(); - } - - /** - * 设置设备 ID 映射 - */ - public void setDeviceIdMapping(String deviceAddr, Long deviceId) { - writeLock.lock(); - try { - TcpDeviceClient client = clientMap.get(deviceAddr); - if (client != null) { - client.setDeviceId(deviceId); - deviceIdToAddrMap.put(deviceId, deviceAddr); - log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId); - } - } finally { - writeLock.unlock(); - } - } - - /** - * 发送消息给设备 - */ - public boolean sendMessage(String deviceAddr, Buffer buffer) { - TcpDeviceClient client = getClient(deviceAddr); - if (client != null && client.isOnline()) { - try { - client.sendMessage(buffer); - totalMessages.incrementAndGet(); - totalBytes.addAndGet(buffer.length()); - return true; - } catch (Exception e) { - totalFailedMessages.incrementAndGet(); - log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e); - return false; - } - } - log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr); - return false; - } - - /** - * 通过设备ID发送消息 - */ - public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) { - TcpDeviceClient client = getClientByDeviceId(deviceId); - if (client != null && client.isOnline()) { - try { - client.sendMessage(buffer); - totalMessages.incrementAndGet(); - totalBytes.addAndGet(buffer.length()); - return true; - } catch (Exception e) { - totalFailedMessages.incrementAndGet(); - log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e); - return false; - } - } - log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId); - return false; - } - - /** - * 广播消息给所有在线设备 - */ - public int broadcastMessage(Buffer buffer) { - int successCount = 0; - readLock.lock(); - try { - for (TcpDeviceClient client : clientMap.values()) { - if (client.isOnline()) { - try { - client.sendMessage(buffer); - successCount++; - } catch (Exception e) { - log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e); - } - } - } - } finally { - readLock.unlock(); - } - - totalMessages.addAndGet(successCount); - totalBytes.addAndGet((long) successCount * buffer.length()); - return successCount; - } - - /** - * 获取在线设备数量 - */ - public int getOnlineCount() { - readLock.lock(); - try { - return (int) clientMap.values().stream() - .filter(TcpDeviceClient::isOnline) - .count(); - } finally { - readLock.unlock(); - } - } - - /** - * 获取总连接数 - */ - public int getTotalCount() { - return clientMap.size(); - } - - /** - * 获取认证设备数量 - */ - public int getAuthenticatedCount() { - readLock.lock(); - try { - return (int) clientMap.values().stream() - .filter(TcpDeviceClient::isAuthenticated) - .count(); - } finally { - readLock.unlock(); - } - } - - // TODO @haohao:心跳超时,需要 close 么? - /** - * 心跳检查任务 - */ - private void checkHeartbeat() { - try { - int offlineCount = 0; - - readLock.lock(); - try { - for (TcpDeviceClient client : clientMap.values()) { - if (!client.isOnline()) { - offlineCount++; - } - } - } finally { - readLock.unlock(); - } - - if (offlineCount > 0) { - log.info("[checkHeartbeat][发现 {} 个离线设备,将在清理任务中处理]", offlineCount); - } - } catch (Exception e) { - log.error("[checkHeartbeat][心跳检查任务异常]", e); - } - } - - /** - * 连接清理任务 - */ - private void cleanupConnections() { - try { - int beforeSize = clientMap.size(); - - writeLock.lock(); - try { - clientMap.entrySet().removeIf(entry -> { - TcpDeviceClient client = entry.getValue(); - if (!client.isOnline()) { - log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey()); - - // 清理相关映射 - if (client.getSocket() != null) { - socketToAddrMap.remove(client.getSocket()); - } - if (client.getDeviceId() != null) { - deviceIdToAddrMap.remove(client.getDeviceId()); - } - - client.shutdown(); - totalDisconnections.incrementAndGet(); - return true; - } - return false; - }); - } finally { - writeLock.unlock(); - } - - int afterSize = clientMap.size(); - if (beforeSize != afterSize) { - log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}", - beforeSize, afterSize, beforeSize - afterSize); - } - } catch (Exception e) { - log.error("[cleanupConnections][连接清理任务异常]", e); - } - } - - /** - * 统计日志任务 - */ - private void logStatistics() { - try { - long totalConn = totalConnections.get(); - long totalDisconnections = this.totalDisconnections.get(); - long totalMsg = totalMessages.get(); - long totalFailedMsg = totalFailedMessages.get(); - long totalBytesValue = totalBytes.get(); - - log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " + - "总消息: {}, 失败消息: {}, 总字节: {}", - totalConn, totalDisconnections, getOnlineCount(), getAuthenticatedCount(), - totalMsg, totalFailedMsg, totalBytesValue); - } catch (Exception e) { - log.error("[logStatistics][统计日志任务异常]", e); - } - } - - /** - * 关闭连接管理器 - */ - public void shutdown() { - log.info("[shutdown][关闭TCP连接管理器]"); - - // 关闭定时任务 - scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - } - } catch (InterruptedException e) { - scheduler.shutdownNow(); - Thread.currentThread().interrupt(); - } - - // 关闭所有连接 - writeLock.lock(); - try { - clientMap.values().forEach(TcpDeviceClient::shutdown); - clientMap.clear(); - deviceIdToAddrMap.clear(); - socketToAddrMap.clear(); - } finally { - writeLock.unlock(); - } - } - - /** - * 获取连接状态信息 - */ - public String getConnectionStatus() { - return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%", - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), - totalMessages.get() > 0 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 - : 0.0); - } - - /** - * 获取详细统计信息 - */ - public String getDetailedStatistics() { - return String.format( - "TCP连接管理器统计:\n" + - "- 当前连接数: %d\n" + - "- 在线设备数: %d\n" + - "- 认证设备数: %d\n" + - "- 历史总连接: %d\n" + - "- 历史总断开: %d\n" + - "- 总消息数: %d\n" + - "- 失败消息数: %d\n" + - "- 总字节数: %d\n" + - "- 消息成功率: %.2f%%", - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), - totalConnections.get(), totalDisconnections.get(), - totalMessages.get(), totalFailedMessages.get(), totalBytes.get(), - totalMessages.get() > 0 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 - : 0.0); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java deleted file mode 100644 index ed4b2ebaa0..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; - -// TODO @haohao:“设备地址长度”是不是不需要。 -/** - * TCP 数据解码器 - *

- * 负责将字节流解码为 TcpDataPackage 对象 - *

- * 数据包格式: - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataDecoder { - - /** - * 解码数据包 - * - * @param buffer 数据缓冲区 - * @return 解码后的数据包 - * @throws IllegalArgumentException 如果数据包格式不正确 - */ - public static TcpDataPackage decode(Buffer buffer) { - if (buffer == null || buffer.length() < 8) { - throw new IllegalArgumentException("数据包长度不足"); - } - - try { - int index = 0; - - // 1.1 获取设备地址长度(2字节) - short addrLength = buffer.getShort(index); - index += 2; - - // 1.2 校验数据包长度 - int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号 - if (buffer.length() < expectedLength) { - throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节"); - } - - // 1.3 获取设备地址 - String addr = buffer.getBuffer(index, index + addrLength).toString(); - index += addrLength; - - // 1.4 获取功能码(2字节) - short code = buffer.getShort(index); - index += 2; - - // 1.5 获取消息序号(2字节) - short mid = buffer.getShort(index); - index += 2; - - // 1.6 获取包体数据 - String payload = ""; - if (index < buffer.length()) { - payload = buffer.getString(index, buffer.length()); - } - - // 2. 构建数据包对象 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addrLength((int) addrLength) - .addr(addr) - .code(code) - .mid(mid) - .payload(payload) - .build(); - - log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", - addr, dataPackage.getCodeDescription(), mid, payload.length()); - return dataPackage; - } catch (Exception e) { - log.error("[decode][解码失败] 数据: {}", buffer.toString(), e); - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); - } - } - - // TODO @haohao:这个要不去掉,暂时没用到; - /** - * 校验数据包格式 - * - * @param buffer 数据缓冲区 - * @return 校验结果 - */ - public static boolean validate(Buffer buffer) { - try { - decode(buffer); - return true; - } catch (Exception e) { - log.warn("[validate][数据包格式校验失败] 数据: {}, 错误: {}", buffer.toString(), e.getMessage()); - return false; - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java deleted file mode 100644 index 62f7bc4848..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java +++ /dev/null @@ -1,159 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; - -/** - * TCP 数据编码器 - *

- * 负责将 TcpDataPackage 对象编码为字节流 - *

- * 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataEncoder { - - /** - * 编码数据包 - * - * @param dataPackage 数据包对象 - * @return 编码后的字节流 - * @throws IllegalArgumentException 如果数据包对象不正确 - */ - public static Buffer encode(TcpDataPackage dataPackage) { - if (dataPackage == null) { - throw new IllegalArgumentException("数据包对象不能为空"); - } - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { - throw new IllegalArgumentException("设备地址不能为空"); - } - if (dataPackage.getPayload() == null) { - dataPackage.setPayload(""); - } - - try { - Buffer buffer = Buffer.buffer(); - - // 1. 计算包体长度(除了包头 4 字节) - int payloadLength = dataPackage.getPayload().getBytes().length; - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; - - // 2.1 写入包头:总长度(4 字节) - buffer.appendInt(totalLength); - // 2.2 写入设备地址长度(2 字节) - buffer.appendShort((short) dataPackage.getAddr().length()); - // 2.3 写入设备地址(不定长) - buffer.appendBytes(dataPackage.getAddr().getBytes()); - // 2.4 写入功能码(2 字节) - buffer.appendShort(dataPackage.getCode()); - // 2.5 写入消息序号(2 字节) - buffer.appendShort(dataPackage.getMid()); - // 2.6 写入包体数据(不定长) - buffer.appendBytes(dataPackage.getPayload().getBytes()); - - log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", - dataPackage.getAddr(), dataPackage.getCodeDescription(), - dataPackage.getMid(), buffer.length()); - return buffer; - } catch (Exception e) { - log.error("[encode][编码失败] 数据包: {}", dataPackage, e); - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); - } - } - - /** - * 创建注册回复数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param success 是否成功 - * @return 编码后的数据包 - */ - public static Buffer createRegisterReply(String addr, short mid, boolean success) { - // TODO @haohao:payload 默认成功、失败,最好讴有个枚举 - String payload = success ? "0" : "1"; // 0 表示成功,1 表示失败 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_REGISTER_REPLY) - .mid(mid) - .payload(payload) - .build(); - return encode(dataPackage); - } - - /** - * 创建数据下发数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param data 下发数据 - * @return 编码后的数据包 - */ - public static Buffer createDataDownPackage(String addr, short mid, String data) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_DATA_DOWN) - .mid(mid) - .payload(data) - .build(); - return encode(dataPackage); - } - - /** - * 创建服务调用数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param serviceData 服务数据 - * @return 编码后的数据包 - */ - public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_SERVICE_INVOKE) - .mid(mid) - .payload(serviceData) - .build(); - return encode(dataPackage); - } - - /** - * 创建属性设置数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param propertyData 属性数据 - * @return 编码后的数据包 - */ - public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_PROPERTY_SET) - .mid(mid) - .payload(propertyData) - .build(); - return encode(dataPackage); - } - - /** - * 创建属性获取数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param propertyNames 属性名称列表 - * @return 编码后的数据包 - */ - public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_PROPERTY_GET) - .mid(mid) - .payload(propertyNames) - .build(); - return encode(dataPackage); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java deleted file mode 100644 index c0a7e7185d..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java +++ /dev/null @@ -1,160 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * TCP 数据包协议定义 - *

- * 数据包格式: - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class TcpDataPackage { - - // ==================== 功能码定义 ==================== - - /** - * 设备注册 - */ - public static final short CODE_REGISTER = 10; - /** - * 注册回复 - */ - public static final short CODE_REGISTER_REPLY = 11; - // TODO @haohao:【重要】一般心跳,服务端会回复一条;回复要搞独立的 code 码,还是继续用原来的,因为 requestId 可以映射; - /** - * 心跳 - */ - public static final short CODE_HEARTBEAT = 20; - // TODO @haohao:【重要】下面的,是不是融合成消息上行(client -> server),消息下行(server -> client);然后把 method 放到 body 里? - /** - * 数据上报 - */ - public static final short CODE_DATA_UP = 30; - /** - * 事件上报 - */ - public static final short CODE_EVENT_UP = 40; - /** - * 数据下发 - */ - public static final short CODE_DATA_DOWN = 50; - /** - * 服务调用 - */ - public static final short CODE_SERVICE_INVOKE = 60; - /** - * 属性设置 - */ - public static final short CODE_PROPERTY_SET = 70; - /** - * 属性获取 - */ - public static final short CODE_PROPERTY_GET = 80; - - // ==================== 数据包字段 ==================== - - // TODO @haohao:设备 addrLength、addr 是不是非必要呀? - - /** - * 设备地址长度 - */ - private Integer addrLength; - - /** - * 设备地址 - */ - private String addr; - - /** - * 功能码 - */ - private short code; - - /** - * 消息序号 - */ - private short mid; - - /** - * 包体数据 - */ - private String payload; - - // ==================== 辅助方法 ==================== - - // TODO @haohao:用不到的方法,可以清理掉哈; - - /** - * 是否为注册消息 - */ - public boolean isRegisterMessage() { - return code == CODE_REGISTER; - } - - /** - * 是否为心跳消息 - */ - public boolean isHeartbeatMessage() { - return code == CODE_HEARTBEAT; - } - - /** - * 是否为数据上报消息 - */ - public boolean isDataUpMessage() { - return code == CODE_DATA_UP; - } - - /** - * 是否为事件上报消息 - */ - public boolean isEventUpMessage() { - return code == CODE_EVENT_UP; - } - - /** - * 是否为下行消息 - */ - public boolean isDownstreamMessage() { - return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE || - code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET; - } - - // TODO @haohao:这个是不是去掉呀?多了一些维护成本; - /** - * 获取功能码描述 - */ - public String getCodeDescription() { - switch (code) { - case CODE_REGISTER: - return "设备注册"; - case CODE_REGISTER_REPLY: - return "注册回复"; - case CODE_HEARTBEAT: - return "心跳"; - case CODE_DATA_UP: - return "数据上报"; - case CODE_EVENT_UP: - return "事件上报"; - case CODE_DATA_DOWN: - return "数据下发"; - case CODE_SERVICE_INVOKE: - return "服务调用"; - case CODE_PROPERTY_SET: - return "属性设置"; - case CODE_PROPERTY_GET: - return "属性获取"; - default: - return "未知功能码"; - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java deleted file mode 100644 index f366418d7e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java +++ /dev/null @@ -1,162 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.parsetools.RecordParser; -import lombok.extern.slf4j.Slf4j; - -import java.util.function.Consumer; - -/** - * TCP 数据读取器 - *

- * 负责从 TCP 流中读取完整的数据包 - *

- * 数据包格式: - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataReader { - - /** - * 创建数据包解析器 - * - * @param receiveHandler 接收处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createParser(Consumer receiveHandler) { - // 首先读取 4 字节的长度信息 - RecordParser parser = RecordParser.newFixed(4); - - // 设置处理器 - parser.setOutput(new Handler() { - // 当前数据包的长度,-1 表示还没有读取到长度信息 - private int dataLength = -1; - - @Override - public void handle(Buffer buffer) { - try { - // 如果还没有读取到长度信息 - if (dataLength == -1) { - // 从包头中读取数据长度 - dataLength = buffer.getInt(0); - - // 校验数据长度(最大 1 MB) - // TODO @haohao:1m 蛮多地方在写死,最好配置管理下。或者有个全局的枚举; - if (dataLength <= 0 || dataLength > 1024 * 1024) { - log.error("[handle][无效的数据包长度: {}]", dataLength); - reset(); - return; - } - - // 切换到读取数据模式 - parser.fixedSizeMode(dataLength); - - log.debug("[handle][读取到数据包长度: {}]", dataLength); - } else { - // 读取到完整的数据包 - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); - - // 处理数据包 - try { - receiveHandler.accept(buffer); - } catch (Exception e) { - log.error("[handle][处理数据包失败]", e); - } - - // 重置状态,准备读取下一个数据包 - reset(); - } - } catch (Exception e) { - log.error("[handle][数据包处理异常]", e); - reset(); - } - } - - /** - * 重置解析器状态 - */ - private void reset() { - dataLength = -1; - parser.fixedSizeMode(4); - } - }); - - return parser; - } - - // TODO @haohao:用不到的方法,可以清理掉哈; - - /** - * 创建带异常处理的数据包解析器 - * - * @param receiveHandler 接收处理器 - * @param exceptionHandler 异常处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createParserWithExceptionHandler( - Consumer receiveHandler, - Consumer exceptionHandler) { - - RecordParser parser = RecordParser.newFixed(4); - - parser.setOutput(new Handler() { - private int dataLength = -1; - - @Override - public void handle(Buffer buffer) { - try { - if (dataLength == -1) { - dataLength = buffer.getInt(0); - - if (dataLength <= 0 || dataLength > 1024 * 1024) { - throw new IllegalArgumentException("无效的数据包长度: " + dataLength); - } - - parser.fixedSizeMode(dataLength); - log.debug("[handle][读取到数据包长度: {}]", dataLength); - } else { - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); - - try { - receiveHandler.accept(buffer); - } catch (Exception e) { - exceptionHandler.accept(e); - } - - reset(); - } - } catch (Exception e) { - exceptionHandler.accept(e); - reset(); - } - } - - private void reset() { - dataLength = -1; - parser.fixedSizeMode(4); - } - }); - - return parser; - } - - /** - * 创建简单的数据包解析器(用于测试) - * - * @param receiveHandler 接收处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createSimpleParser(Consumer receiveHandler) { - return createParser(buffer -> { - try { - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - receiveHandler.accept(dataPackage); - } catch (Exception e) { - log.error("[createSimpleParser][解码数据包失败]", e); - } - }); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 1fcb6a2bb5..919606475b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -1,15 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import com.alibaba.fastjson.JSON; -import io.vertx.core.buffer.Buffer; import lombok.extern.slf4j.Slf4j; /** @@ -21,20 +14,21 @@ import lombok.extern.slf4j.Slf4j; * 3. 属性获取 * 4. 配置下发 * 5. OTA 升级 + *

+ * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录 * * @author 芋道源码 */ @Slf4j public class IotTcpDownstreamHandler { - private final TcpDeviceConnectionManager connectionManager; - private final IotDeviceMessageService messageService; - public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager, - IotDeviceMessageService messageService) { - this.connectionManager = connectionManager; + private final IotTcpDeviceMessageCodec codec; + + public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { this.messageService = messageService; + this.codec = new IotTcpDeviceMessageCodec(); } /** @@ -47,315 +41,19 @@ public class IotTcpDownstreamHandler { log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", message.getDeviceId(), message.getMethod(), message.getId()); - // 1. 获取设备连接 - TcpDeviceClient client = connectionManager.getClientByDeviceId(message.getDeviceId()); - if (client == null || !client.isOnline()) { - log.error("[handle][设备({})不在线,无法发送下行消息]", message.getDeviceId()); - return; - } + // 编码消息用于日志记录和验证 + byte[] encodedMessage = codec.encode(message); + log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", + message.getDeviceId(), encodedMessage.length); + + // 记录下行消息处理 + log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}", + message.getDeviceId(), message.getMethod(), message.getParams()); - // 2. 根据消息方法处理不同类型的下行消息 - // TODO @芋艿、@haohao:看看有没什么办法,减少这样的编码。拓展新消息类型,成本高; - switch (message.getMethod()) { - case "thing.property.set": - handlePropertySet(client, message); - break; - case "thing.property.get": - handlePropertyGet(client, message); - break; - case "thing.service.invoke": - handleServiceInvoke(client, message); - break; - case "thing.config.push": - handleConfigPush(client, message); - break; - case "thing.ota.upgrade": - handleOtaUpgrade(client, message); - break; - default: - log.warn("[handle][未知的下行消息方法: {}]", message.getMethod()); - break; - } } catch (Exception e) { - // TODO @haohao:最好消息的内容,打印下; - log.error("[handle][处理下行消息失败]", e); + log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}", + message.getDeviceId(), message.getMethod(), message.getParams(), e); } } - /** - * 处理属性设置 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}", - client.getDeviceAddr(), message.getParams()); - - // 使用编解码器发送消息,降级处理使用原始编码 - sendMessageWithCodec(client, message, "handlePropertySet", () -> { - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createPropertySetPackage( - client.getDeviceAddr(), mid, payload); - client.sendMessage(buffer); - - log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - }); - } catch (Exception e) { - log.error("[handlePropertySet][属性设置失败]", e); - } - } - - /** - * 处理属性获取 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handlePropertyGet(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handlePropertyGet][属性获取] 设备地址: {}, 属性列表: {}", - client.getDeviceAddr(), message.getParams()); - - // 使用编解码器发送消息,降级处理使用原始编码 - sendMessageWithCodec(client, message, "handlePropertyGet", () -> { - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createPropertyGetPackage( - client.getDeviceAddr(), mid, payload); - client.sendMessage(buffer); - - log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - }); - } catch (Exception e) { - log.error("[handlePropertyGet][属性获取失败]", e); - } - } - - /** - * 处理服务调用 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建服务调用数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createServiceInvokePackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - } catch (Exception e) { - log.error("[handleServiceInvoke][服务调用失败]", e); - } - } - - /** - * 处理配置推送 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建配置推送数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createDataDownPackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - } catch (Exception e) { - log.error("[handleConfigPush][配置推送失败]", e); - } - } - - /** - * 处理 OTA 升级 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建 OTA 升级数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createDataDownPackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - - } catch (Exception e) { - log.error("[handleOtaUpgrade][OTA升级失败]", e); - } - } - - /** - * 处理自定义下行消息 - * - * @param client 设备客户端 - * @param message 设备消息 - * @param code 功能码 - */ - private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) { - try { - log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}", - client.getDeviceAddr(), code, message.getParams()); - - // 1. 构建自定义数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(client.getDeviceAddr()) - .code(code) - .mid(mid) - .payload(payload) - .build(); - - Buffer buffer = TcpDataEncoder.encode(dataPackage); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}", - client.getDeviceAddr(), code, mid); - - } catch (Exception e) { - log.error("[handleCustomMessage][自定义消息发送失败]", e); - } - } - - // TODO @haohao:用不到的,要不暂时不提供; - /** - * 批量发送下行消息 - * - * @param deviceIds 设备ID列表 - * @param message 设备消息 - */ - public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) { - try { - log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}", - deviceIds.length, message.getMethod()); - - for (Long deviceId : deviceIds) { - // 创建副本消息(避免消息ID冲突) - IotDeviceMessage copyMessage = IotDeviceMessage.of( - message.getRequestId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg()); - copyMessage.setDeviceId(deviceId); - - // 处理单个设备消息 - handle(copyMessage); - } - } catch (Exception e) { - log.error("[broadcastMessage][批量发送消息失败]", e); - } - } - - /** - * 检查设备是否支持指定方法 - * - * @param client 设备客户端 - * @param method 消息方法 - * @return 是否支持 - */ - private boolean isMethodSupported(TcpDeviceClient client, String method) { - // TODO: 可以根据设备类型或产品信息判断是否支持特定方法 - return IotDeviceMessageMethodEnum.of(method) != null; - } - - /** - * 生成消息序号 - * - * @return 消息序号 - */ - private short generateMessageId() { - return (short) (System.currentTimeMillis() % Short.MAX_VALUE); - } - - /** - * 使用编解码器发送消息 - * - * @param client 设备客户端 - * @param message 设备消息 - * @param methodName 方法名称 - * @param fallbackAction 降级处理逻辑 - */ - private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message, - String methodName, Runnable fallbackAction) { - try { - // 1. 使用编解码器编码消息 - byte[] messageBytes = messageService.encodeDeviceMessage( - message, client.getProductKey(), client.getDeviceName()); - - // 2. 解析编码后的数据包并设置设备地址和消息序号 - Buffer buffer = Buffer.buffer(messageBytes); - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - dataPackage.setAddr(client.getDeviceAddr()); - dataPackage.setMid(generateMessageId()); - - // 3. 重新编码并发送 - Buffer finalBuffer = TcpDataEncoder.encode(dataPackage); - client.sendMessage(finalBuffer); - - log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}", - methodName, client.getDeviceAddr(), dataPackage.getMid()); - } catch (Exception e) { - log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}", - methodName, e.getMessage()); - - // 执行降级处理 - if (fallbackAction != null) { - fallbackAction.run(); - } - } - } - - // TODO @haohao:看看这个要不要删除掉 - /** - * 获取连接统计信息 - * - * @return 连接统计信息 - */ - public String getHandlerStatistics() { - return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus()); - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index 672de2ad2c..b57cceb9ec 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -1,385 +1,110 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; import cn.hutool.core.util.IdUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataReader; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import io.vertx.core.parsetools.RecordParser; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * IoT 网关 TCP 上行消息处理器 - *

- * 核心负责: - * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信 - * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态 - * 3. 【数据上报】接收设备数据上报和事件上报 - * 4. 【连接管理】管理连接的建立、维护和清理 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotTcpUpstreamHandler implements Handler { - private final IotGatewayProperties.TcpProperties tcpConfig; - - // TODO @haohao:可以把 TcpDeviceConnectionManager 能力放大一点:1)handle 里的 client 初始化,可以拿到 TcpDeviceConnectionManager 里;2)handleDeviceRegister 也是; - private final TcpDeviceConnectionManager connectionManager; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService deviceMessageService; private final String serverId; + private final IotTcpCodecManager codecManager; + + public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, + IotTcpCodecManager codecManager) { + this.deviceMessageService = deviceMessageService; + this.serverId = protocol.getServerId(); + this.codecManager = codecManager; + } + @Override public void handle(NetSocket socket) { - log.info("[handle][收到设备连接: {}]", socket.remoteAddress()); + // 生成客户端ID用于日志标识 + String clientId = IdUtil.simpleUUID(); + log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress()); - // 创建客户端 ID 和设备客户端 - // TODO @haohao:clientid 给 TcpDeviceClient 生成会简洁一点;减少 upsteramhanlder 的非核心逻辑; - String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); - TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs()); + // 设置解析器 + RecordParser parser = RecordParser.newFixed(1024, buffer -> { + try { + handleDataPackage(clientId, buffer); + } catch (Exception e) { + log.error("[handle][处理数据包异常] clientId: {}", clientId, e); + } + }); - try { - // 设置连接异常和关闭处理 - socket.exceptionHandler(ex -> { - // TODO @haohao:这里的日志,可能把 clientid 都打上?因为 address 会重复么? - log.error("[handle][连接({})异常]", socket.remoteAddress(), ex); - handleConnectionClose(client); - }); - socket.closeHandler(v -> { - log.info("[handle][连接({})关闭]", socket.remoteAddress()); - handleConnectionClose(client); - }); - client.setSocket(socket); + // 设置异常处理 + socket.exceptionHandler(ex -> { + log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex); + }); - // 设置解析器 - RecordParser parser = TcpDataReader.createParser(buffer -> { - try { - handleDataPackage(client, buffer); - } catch (Exception e) { - log.error("[handle][处理数据包异常]", e); - } - }); - client.setParser(parser); + socket.closeHandler(v -> { + log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress()); + }); - // TODO @haohao:socket.remoteAddress()) 打印进去 - log.info("[handle][设备连接处理器初始化完成: {}]", clientId); - } catch (Exception e) { - // TODO @haohao:socket.remoteAddress()) 打印进去 - log.error("[handle][初始化连接处理器失败]", e); - client.shutdown(); - } + // 设置数据处理器 + socket.handler(parser); } /** * 处理数据包 - * - * @param client 设备客户端 - * @param buffer 数据缓冲区 */ - private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) { + private void handleDataPackage(String clientId, Buffer buffer) { try { - // 解码数据包 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}", - dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid()); + // 使用编解码器管理器自动检测协议并解码消息 + IotDeviceMessage message = codecManager.decode(buffer.getBytes()); + log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}", + clientId, message.getMethod(), message.getDeviceId()); - // 根据功能码处理不同类型的消息 - switch (dataPackage.getCode()) { - // TODO @haohao:【重要】code 要不要改成 opCode。这样和 data 里的 code 好区分; - case TcpDataPackage.CODE_REGISTER: - handleDeviceRegister(client, dataPackage); - break; - case TcpDataPackage.CODE_HEARTBEAT: - handleHeartbeat(client, dataPackage); - break; - case TcpDataPackage.CODE_DATA_UP: - handleDataUp(client, dataPackage); - break; - case TcpDataPackage.CODE_EVENT_UP: - handleEventUp(client, dataPackage); - break; - default: - log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode()); - break; - } + // 处理上行消息 + handleUpstreamMessage(clientId, message); } catch (Exception e) { - // TODO @haohao:最好有 client 标识; - log.error("[handleDataPackage][处理数据包失败]", e); + log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e); } } /** - * 处理设备注册 - * - * @param client 设备客户端 - * @param dataPackage 数据包 + * 处理上行消息 */ - private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) { + private void handleUpstreamMessage(String clientId, IotDeviceMessage message) { try { - String deviceAddr = dataPackage.getAddr(); - String productKey = dataPackage.getPayload(); - log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey); + log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}", + clientId, message.getMethod(), message.getDeviceId()); - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr); - if (device == null) { - log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr); - sendRegisterReply(client, dataPackage, false); - return; - } + // 解析设备信息(简化处理) + String deviceId = String.valueOf(message.getDeviceId()); + String productKey = extractProductKey(deviceId); + String deviceName = deviceId; - // 更新客户端信息 - // TODO @haohao:一个 set 方法,统一处理掉会好点哈; - client.setProductKey(productKey); - client.setDeviceName(deviceAddr); - client.setDeviceId(device.getId()); - client.setAuthenticated(true); - - // 添加到连接管理器 - connectionManager.addClient(deviceAddr, client); - connectionManager.setDeviceIdMapping(deviceAddr, device.getId()); - - // 发送设备上线消息 - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId); - - // 发送注册成功回复 - sendRegisterReply(client, dataPackage, true); - - log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId()); + // 发送消息到队列 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); } catch (Exception e) { - log.error("[handleDeviceRegister][设备注册失败]", e); - sendRegisterReply(client, dataPackage, false); + log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e); } } /** - * 处理心跳 - * - * @param client 设备客户端 - * @param dataPackage 数据包 + * 从设备ID中提取产品密钥(简化实现) */ - private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr); - - // 更新心跳时间 - client.keepAlive(); - - // 发送心跳回复(可选) - // sendHeartbeatReply(client, dataPackage); - - } catch (Exception e) { - log.error("[handleHeartbeat][处理心跳失败]", e); - } - } - - /** - * 处理数据上报 - * - * @param client 设备客户端 - * @param dataPackage 数据包 - */ - private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - String payload = dataPackage.getPayload(); - - log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload); - - // 检查设备是否已认证 - if (!client.isAuthenticated()) { - log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr); - return; - } - - // 使用 IotDeviceMessageService 解码消息 - try { - // 1. 将 TCP 数据包重新编码为字节数组 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] messageBytes = buffer.getBytes(); - - // 2. 使用 messageService 解码消息 - IotDeviceMessage message = messageService.decodeDeviceMessage( - messageBytes, client.getProductKey(), client.getDeviceName()); - - // 3. 发送解码后的消息 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } catch (Exception e) { - log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); - - // 降级处理:使用原始方式解析数据 - JSONObject dataJson = JSONUtil.parseObj(payload); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson); - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } - - // 发送数据上报回复 - sendDataUpReply(client, dataPackage); - } catch (Exception e) { - log.error("[handleDataUp][处理数据上报失败]", e); - } - } - - /** - * 处理事件上报 - * - * @param client 设备客户端 - * @param dataPackage 数据包 - */ - private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - String payload = dataPackage.getPayload(); - - log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload); - - // 检查设备是否已认证 - if (!client.isAuthenticated()) { - log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr); - return; - } - - // 使用 IotDeviceMessageService 解码消息 - try { - // 1. 将 TCP 数据包重新编码为字节数组 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] messageBytes = buffer.getBytes(); - - // 2. 使用 messageService 解码消息 - IotDeviceMessage message = messageService.decodeDeviceMessage( - messageBytes, client.getProductKey(), client.getDeviceName()); - - // 3. 发送解码后的消息 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } catch (Exception e) { - log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); - - // 降级处理:使用原始方式解析数据 - // TODO @芋艿:降级处理逻辑; - JSONObject eventJson = JSONUtil.parseObj(payload); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson); - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } - - // 发送事件上报回复 - sendEventUpReply(client, dataPackage); - } catch (Exception e) { - log.error("[handleEventUp][处理事件上报失败]", e); - } - } - - /** - * 发送注册回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - * @param success 是否成功 - */ - private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) { - try { - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply( - dataPackage.getAddr(), dataPackage.getMid(), success); - client.sendMessage(replyBuffer); - - log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}", - dataPackage.getAddr(), success ? "成功" : "失败"); - } catch (Exception e) { - log.error("[sendRegisterReply][发送注册回复失败]", e); - } - } - - /** - * 发送数据上报回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - */ - private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - TcpDataPackage replyPackage = TcpDataPackage.builder() - .addr(dataPackage.getAddr()) - .code(TcpDataPackage.CODE_DATA_UP) - .mid(dataPackage.getMid()) - .payload("0") // 0 表示成功 TODO @haohao:最好枚举到 TcpDataPackage 里? - .build(); - - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); - client.sendMessage(replyBuffer); - } catch (Exception e) { - // TODO @haohao:可以有个 client id - log.error("[sendDataUpReply][发送数据上报回复失败]", e); - } - } - - /** - * 发送事件上报回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - */ - private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - TcpDataPackage replyPackage = TcpDataPackage.builder() - .addr(dataPackage.getAddr()) - .code(TcpDataPackage.CODE_EVENT_UP) - .mid(dataPackage.getMid()) - .payload("0") // 0 表示成功 - .build(); - - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); - client.sendMessage(replyBuffer); - } catch (Exception e) { - log.error("[sendEventUpReply][发送事件上报回复失败]", e); - } - } - - /** - * 处理连接关闭 - * - * @param client 设备客户端 - */ - private void handleConnectionClose(TcpDeviceClient client) { - try { - String deviceAddr = client.getDeviceAddr(); - - // 发送设备离线消息 - if (client.isAuthenticated()) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, - client.getProductKey(), client.getDeviceName(), serverId); - } - - // 从连接管理器移除 - if (deviceAddr != null) { - connectionManager.removeClient(deviceAddr); - } - - log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr); - } catch (Exception e) { - log.error("[handleConnectionClose][处理连接关闭失败]", e); + private String extractProductKey(String deviceId) { + // 简化实现:假设设备ID格式为 "productKey_deviceName" + if (deviceId != null && deviceId.contains("_")) { + return deviceId.split("_")[0]; } + return "default_product"; } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java new file mode 100644 index 0000000000..56926569ce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java @@ -0,0 +1,219 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * TCP二进制格式数据包示例 + * + * 演示如何使用二进制协议创建和解析TCP上报数据包和心跳包 + * + * 二进制协议格式: + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) + * + * @author 芋道源码 + */ +@Slf4j +public class TcpBinaryDataPacketExamples { + + public static void main(String[] args) { + IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); + + // 1. 数据上报包示例 + demonstrateDataReport(codec); + + // 2. 心跳包示例 + demonstrateHeartbeat(codec); + + // 3. 复杂数据上报示例 + demonstrateComplexDataReport(codec); + } + + /** + * 演示二进制格式数据上报包 + */ + private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) { + log.info("=== 二进制格式数据上报包示例 ==="); + + // 创建传感器数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + sensorData.put("pressure", 1013.25); + sensorData.put("battery", 85); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + log.info("编码后数据包长度: {} 字节", packet.length); + log.info("编码后数据包(HEX): {}", bytesToHex(packet)); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后上报时间: {}", decoded.getReportTime()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示二进制格式心跳包 + */ + private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) { + log.info("=== 二进制格式心跳包示例 ==="); + + // 创建心跳消息 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); + heartbeat.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(heartbeat); + log.info("心跳包长度: {} 字节", packet.length); + log.info("心跳包(HEX): {}", bytesToHex(packet)); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示二进制格式复杂数据上报 + */ + private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) { + log.info("=== 二进制格式复杂数据上报示例 ==="); + + // 创建复杂设备数据 + Map deviceData = new HashMap<>(); + + // 环境数据 + Map environment = new HashMap<>(); + environment.put("temperature", 23.8); + environment.put("humidity", 55.0); + environment.put("co2", 420); + deviceData.put("environment", environment); + + // GPS数据 + Map location = new HashMap<>(); + location.put("latitude", 39.9042); + location.put("longitude", 116.4074); + location.put("altitude", 43.5); + deviceData.put("location", location); + + // 设备状态 + Map status = new HashMap<>(); + status.put("battery", 78); + status.put("signal", -65); + status.put("online", true); + deviceData.put("status", status); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); + message.setDeviceId(789012L); + + // 编码 + byte[] packet = codec.encode(message); + log.info("复杂数据包长度: {} 字节", packet.length); + log.info("复杂数据包(HEX): {}", bytesToHex(packet)); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 字节数组转十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X ", b)); + } + return result.toString().trim(); + } + + /** + * 演示数据包结构分析 + */ + public static void analyzePacketStructure(byte[] packet) { + if (packet.length < 8) { + log.error("数据包长度不足"); + return; + } + + int index = 0; + + // 解析包头(4字节) - 后续数据长度 + int totalLength = ((packet[index] & 0xFF) << 24) | + ((packet[index + 1] & 0xFF) << 16) | + ((packet[index + 2] & 0xFF) << 8) | + (packet[index + 3] & 0xFF); + index += 4; + log.info("包头 - 后续数据长度: {} 字节", totalLength); + + // 解析设备地址长度(2字节) + int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); + index += 2; + log.info("设备地址长度: {} 字节", addrLength); + + // 解析设备地址 + String deviceAddr = new String(packet, index, addrLength); + index += addrLength; + log.info("设备地址: {}", deviceAddr); + + // 解析功能码(2字节) + int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); + index += 2; + log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); + + // 解析消息序号(2字节) + int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); + index += 2; + log.info("消息序号: {}", messageId); + + // 解析包体数据 + if (index < packet.length) { + String payload = new String(packet, index, packet.length - index); + log.info("包体数据: {}", payload); + } + } + + /** + * 获取功能码名称 + */ + private static String getFunctionCodeName(int code) { + switch (code) { + case 10: return "设备注册"; + case 11: return "注册回复"; + case 20: return "心跳请求"; + case 21: return "心跳回复"; + case 30: return "消息上行"; + case 40: return "消息下行"; + default: return "未知功能码"; + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java new file mode 100644 index 0000000000..d53731fe9a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java @@ -0,0 +1,253 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * TCP JSON格式数据包示例 + * + * 演示如何使用新的JSON格式进行TCP消息编解码 + * + * @author 芋道源码 + */ +@Slf4j +public class TcpJsonDataPacketExamples { + + public static void main(String[] args) { + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + + // 1. 数据上报示例 + demonstrateDataReport(codec); + + // 2. 心跳示例 + demonstrateHeartbeat(codec); + + // 3. 事件上报示例 + demonstrateEventReport(codec); + + // 4. 复杂数据上报示例 + demonstrateComplexDataReport(codec); + + // 5. 便捷方法示例 + demonstrateConvenienceMethods(); + + // 6. EMQX兼容性示例 + demonstrateEmqxCompatibility(); + } + + /** + * 演示数据上报 + */ + private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式数据上报示例 ==="); + + // 创建传感器数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + sensorData.put("pressure", 1013.25); + sensorData.put("battery", 85); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("数据包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示心跳 + */ + private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式心跳示例 ==="); + + // 创建心跳消息 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); + heartbeat.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(heartbeat); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("心跳包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + + System.out.println(); + } + + /** + * 演示事件上报 + */ + private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式事件上报示例 ==="); + + // 创建事件数据 + Map eventData = new HashMap<>(); + eventData.put("eventType", "alarm"); + eventData.put("level", "warning"); + eventData.put("description", "温度过高"); + eventData.put("value", 45.8); + + // 创建事件消息 + IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); + event.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(event); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("事件包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示复杂数据上报 + */ + private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式复杂数据上报示例 ==="); + + // 创建复杂设备数据(类似EMQX格式) + Map deviceData = new HashMap<>(); + + // 环境数据 + Map environment = new HashMap<>(); + environment.put("temperature", 23.8); + environment.put("humidity", 55.0); + environment.put("co2", 420); + environment.put("pm25", 35); + deviceData.put("environment", environment); + + // GPS数据 + Map location = new HashMap<>(); + location.put("latitude", 39.9042); + location.put("longitude", 116.4074); + location.put("altitude", 43.5); + location.put("speed", 0.0); + deviceData.put("location", location); + + // 设备状态 + Map status = new HashMap<>(); + status.put("battery", 78); + status.put("signal", -65); + status.put("online", true); + status.put("version", "1.2.3"); + deviceData.put("status", status); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); + message.setDeviceId(789012L); + + // 编码 + byte[] packet = codec.encode(message); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("复杂数据包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示便捷方法 + */ + private static void demonstrateConvenienceMethods() { + log.info("=== 便捷方法示例 ==="); + + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + + // 使用便捷方法编码数据上报 + Map sensorData = Map.of( + "temperature", 26.5, + "humidity", 58.3 + ); + byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001"); + log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8)); + + // 使用便捷方法编码心跳 + byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001"); + log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8)); + + // 使用便捷方法编码事件 + Map eventData = Map.of( + "eventType", "maintenance", + "description", "定期维护提醒" + ); + byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001"); + log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8)); + + System.out.println(); + } + + /** + * 演示与EMQX格式的兼容性 + */ + private static void demonstrateEmqxCompatibility() { + log.info("=== EMQX格式兼容性示例 ==="); + + // 模拟EMQX风格的消息格式 + String emqxStyleJson = """ + { + "id": "msg_001", + "method": "thing.property.post", + "deviceId": 123456, + "params": { + "temperature": 25.5, + "humidity": 60.2 + }, + "timestamp": 1642781234567 + } + """; + + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + + // 解码EMQX风格的消息 + byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8); + IotDeviceMessage decoded = codec.decode(emqxBytes); + + log.info("EMQX风格消息解码成功:"); + log.info("消息ID: {}", decoded.getId()); + log.info("方法: {}", decoded.getMethod()); + log.info("设备ID: {}", decoded.getDeviceId()); + log.info("参数: {}", decoded.getParams()); + + System.out.println(); + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md new file mode 100644 index 0000000000..7bcf9b084e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -0,0 +1,222 @@ +# TCP二进制协议数据包格式说明和示例 + +## 1. 二进制协议概述 + +TCP二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。 + +## 2. 数据包格式 + +### 2.1 整体结构 +``` ++----------+----------+----------+----------+----------+----------+ +| 包头 | 地址长度 | 设备地址 | 功能码 | 消息序号 | 包体数据 | +| 4字节 | 2字节 | 变长 | 2字节 | 2字节 | 变长 | ++----------+----------+----------+----------+----------+----------+ +``` + +### 2.2 字段说明 + +| 字段 | 长度 | 类型 | 说明 | +|----------|--------|--------|--------------------------------| +| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | +| 地址长度 | 2字节 | short | 设备地址的字节长度 | +| 设备地址 | 变长 | string | 设备标识符 | +| 功能码 | 2字节 | short | 消息类型标识 | +| 消息序号 | 2字节 | short | 消息唯一标识 | +| 包体数据 | 变长 | string | JSON格式的消息内容 | + +### 2.3 功能码定义 + +| 功能码 | 名称 | 说明 | +|--------|----------|--------------------------------| +| 10 | 设备注册 | 设备首次连接时的注册请求 | +| 11 | 注册回复 | 服务器对注册请求的回复 | +| 20 | 心跳请求 | 设备发送的心跳包 | +| 21 | 心跳回复 | 服务器对心跳的回复 | +| 30 | 消息上行 | 设备向服务器发送的数据 | +| 40 | 消息下行 | 服务器向设备发送的指令 | + +## 3. 二进制数据上报包示例 + +### 3.1 温度传感器数据上报 + +**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25 + }, + "timestamp": 1642781234567 +} +``` + +**数据包结构:** +``` +包头: 0x00000045 (69字节) +地址长度: 0x0006 (6字节) +设备地址: "123456" +功能码: 0x001E (30 - 消息上行) +消息序号: 0x1234 (4660) +包体: JSON字符串 +``` + +**完整十六进制数据包:** +``` +00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34 +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 +2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C +22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 +72 61 74 75 72 65 22 3A 32 35 2E 35 2C 22 68 75 +6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 +65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D +2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 +32 37 38 31 32 33 34 35 36 37 7D +``` + +### 2.2 GPS定位数据上报 + +**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "latitude": 39.9042, + "longitude": 116.4074, + "altitude": 43.5, + "speed": 0.0 + }, + "timestamp": 1642781234567 +} +``` + +## 3. 心跳包示例 + +### 3.1 标准心跳包 + +**原始数据:** +```json +{ + "method": "thing.state.online", + "timestamp": 1642781234567 +} +``` + +**数据包结构:** +``` +包头: 0x00000028 (40字节) +地址长度: 0x0006 (6字节) +设备地址: "123456" +功能码: 0x0014 (20 - 心跳请求) +消息序号: 0x5678 (22136) +包体: JSON字符串 +``` + +**完整十六进制数据包:** +``` +00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78 +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 +2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 +74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 32 37 +38 31 32 33 34 35 36 37 7D +``` + +## 4. 复杂数据上报示例 + +### 4.1 多传感器综合数据 + +**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "environment": { + "temperature": 23.8, + "humidity": 55.0, + "co2": 420 + }, + "location": { + "latitude": 39.9042, + "longitude": 116.4074, + "altitude": 43.5 + }, + "status": { + "battery": 78, + "signal": -65, + "online": true + } + }, + "timestamp": 1642781234567 +} +``` + +## 5. 数据包解析步骤 + +### 5.1 解析流程 + +1. **读取包头(4字节)** + - 获取后续数据的总长度 + - 验证数据包完整性 + +2. **读取设备地址长度(2字节)** + - 确定设备地址的字节数 + +3. **读取设备地址(变长)** + - 根据地址长度读取设备标识 + +4. **读取功能码(2字节)** + - 确定消息类型 + +5. **读取消息序号(2字节)** + - 获取消息唯一标识 + +6. **读取包体数据(变长)** + - 解析JSON格式的消息内容 + +### 5.2 Java解析示例 + +```java +public TcpDataPackage parsePacket(byte[] packet) { + int index = 0; + + // 1. 解析包头 + int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); + index += 4; + + // 2. 解析设备地址长度 + short addrLength = ByteBuffer.wrap(packet, index, 2).getShort(); + index += 2; + + // 3. 解析设备地址 + String deviceAddr = new String(packet, index, addrLength); + index += addrLength; + + // 4. 解析功能码 + short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); + index += 2; + + // 5. 解析消息序号 + short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); + index += 2; + + // 6. 解析包体数据 + String payload = new String(packet, index, packet.length - index); + + return TcpDataPackage.builder() + .addr(deviceAddr) + .code(functionCode) + .mid(messageId) + .payload(payload) + .build(); +} +``` + +## 6. 注意事项 + +1. **字节序**:所有多字节数据使用大端序(Big-Endian) +2. **字符编码**:字符串数据使用UTF-8编码 +3. **JSON格式**:包体数据必须是有效的JSON格式 +4. **长度限制**:单个数据包建议不超过1MB +5. **错误处理**:解析失败时应返回相应的错误码 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md new file mode 100644 index 0000000000..45a08d78af --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -0,0 +1,286 @@ +# TCP JSON格式协议说明 + +## 1. 协议概述 + +TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势: + +- **标准化**:使用标准JSON格式,易于解析和处理 +- **可读性**:人类可读,便于调试和维护 +- **扩展性**:可以轻松添加新字段,向后兼容 +- **统一性**:与HTTP模块保持一致的数据格式 + +## 2. 消息格式 + +### 2.1 基础消息结构 + +```json +{ + "id": "消息唯一标识", + "method": "消息方法", + "deviceId": "设备ID", + "params": { + // 消息参数 + }, + "timestamp": 时间戳 +} +``` + +### 2.2 字段说明 + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | String | 是 | 消息唯一标识,UUID格式 | +| method | String | 是 | 消息方法,如 thing.property.post | +| deviceId | Long | 是 | 设备ID | +| params | Object | 否 | 消息参数,具体内容根据method而定 | +| timestamp | Long | 是 | 时间戳(毫秒) | +| code | Integer | 否 | 响应码(下行消息使用) | +| message | String | 否 | 响应消息(下行消息使用) | + +## 3. 消息类型 + +### 3.1 数据上报 (thing.property.post) + +设备向服务器上报属性数据。 + +**示例:** +```json +{ + "id": "8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "thing.property.post", + "deviceId": 123456, + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25, + "battery": 85 + }, + "timestamp": 1753111026437 +} +``` + +### 3.2 心跳 (thing.state.online) + +设备向服务器发送心跳保活。 + +**示例:** +```json +{ + "id": "7db8c4e6408b40f8b2549ddd94f6bb02", + "method": "thing.state.online", + "deviceId": 123456, + "timestamp": 1753111026467 +} +``` + +### 3.3 事件上报 (thing.event.post) + +设备向服务器上报事件信息。 + +**示例:** +```json +{ + "id": "9e7d72731b854916b1baa5088bd6a907", + "method": "thing.event.post", + "deviceId": 123456, + "params": { + "eventType": "alarm", + "level": "warning", + "description": "温度过高", + "value": 45.8 + }, + "timestamp": 1753111026468 +} +``` + +### 3.4 属性设置 (thing.property.set) + +服务器向设备下发属性设置指令。 + +**示例:** +```json +{ + "id": "cmd_001", + "method": "thing.property.set", + "deviceId": 123456, + "params": { + "targetTemperature": 22.0, + "mode": "auto" + }, + "timestamp": 1753111026469 +} +``` + +### 3.5 服务调用 (thing.service.invoke) + +服务器向设备调用服务。 + +**示例:** +```json +{ + "id": "service_001", + "method": "thing.service.invoke", + "deviceId": 123456, + "params": { + "service": "restart", + "args": { + "delay": 5 + } + }, + "timestamp": 1753111026470 +} +``` + +## 4. 复杂数据示例 + +### 4.1 多传感器综合数据 + +```json +{ + "id": "complex_001", + "method": "thing.property.post", + "deviceId": 789012, + "params": { + "environment": { + "temperature": 23.8, + "humidity": 55.0, + "co2": 420, + "pm25": 35 + }, + "location": { + "latitude": 39.9042, + "longitude": 116.4074, + "altitude": 43.5, + "speed": 0.0 + }, + "status": { + "battery": 78, + "signal": -65, + "online": true, + "version": "1.2.3" + } + }, + "timestamp": 1753111026471 +} +``` + +## 5. 与EMQX格式的兼容性 + +本协议设计参考了EMQX的消息格式,具有良好的兼容性: + +### 5.1 EMQX标准格式 + +```json +{ + "id": "msg_001", + "method": "thing.property.post", + "deviceId": 123456, + "params": { + "temperature": 25.5, + "humidity": 60.2 + }, + "timestamp": 1642781234567 +} +``` + +### 5.2 兼容性说明 + +- ✅ **字段名称**:与EMQX保持一致 +- ✅ **数据类型**:完全兼容 +- ✅ **消息结构**:结构相同 +- ✅ **扩展字段**:支持自定义扩展 + +## 6. 使用示例 + +### 6.1 Java编码示例 + +```java +// 创建编解码器 +IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + +// 创建数据上报消息 +Map sensorData = Map.of( + "temperature", 25.5, + "humidity", 60.2 +); +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); +message.setDeviceId(123456L); + +// 编码为字节数组 +byte[] jsonBytes = codec.encode(message); + +// 解码 +IotDeviceMessage decoded = codec.decode(jsonBytes); +``` + +### 6.2 便捷方法示例 + +```java +// 快速编码数据上报 +byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name"); + +// 快速编码心跳 +byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name"); + +// 快速编码事件 +byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name"); +``` + +## 7. 协议优势 + +### 7.1 与原TCP二进制协议对比 + +| 特性 | 二进制协议 | JSON协议 | +|------|------------|----------| +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 差 | 优秀 | +| 解析复杂度 | 高 | 低 | +| 数据大小 | 小 | 稍大 | +| 标准化程度 | 低 | 高 | + +### 7.2 适用场景 + +- ✅ **开发调试**:JSON格式便于查看和调试 +- ✅ **快速集成**:标准JSON格式,集成简单 +- ✅ **协议扩展**:可以轻松添加新字段 +- ✅ **多语言支持**:JSON格式支持所有主流语言 +- ✅ **云平台对接**:与主流IoT云平台格式兼容 + +## 8. 最佳实践 + +### 8.1 消息设计建议 + +1. **保持简洁**:避免过深的嵌套结构 +2. **字段命名**:使用驼峰命名法,保持一致性 +3. **数据类型**:使用合适的数据类型,避免字符串表示数字 +4. **时间戳**:统一使用毫秒级时间戳 + +### 8.2 性能优化 + +1. **批量上报**:可以在params中包含多个数据点 +2. **压缩传输**:对于大数据量可以考虑gzip压缩 +3. **缓存机制**:客户端可以缓存消息,批量发送 + +### 8.3 错误处理 + +1. **格式验证**:确保JSON格式正确 +2. **字段检查**:验证必填字段是否存在 +3. **异常处理**:提供详细的错误信息 + +## 9. 迁移指南 + +### 9.1 从二进制协议迁移 + +1. **保持兼容**:可以同时支持两种协议 +2. **逐步迁移**:按设备类型逐步迁移 +3. **测试验证**:充分测试新协议的稳定性 + +### 9.2 配置变更 + +```java +// 在设备配置中指定编解码器类型 +device.setCodecType("TCP_JSON"); +``` + +这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。