From c9b9fc1f31242804e594638d8d64d6a6705fa967 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Sat, 26 Jul 2025 22:15:37 +0800 Subject: [PATCH] feat:【IoT 物联网】重构 TCP 协议处理,新增 TCP 会话和认证管理 --- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java | 9 ++++++--- yudao-module-iot/yudao-module-iot-gateway/pom.xml | 7 +++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java | 408 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java | 143 ----------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java | 298 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java | 13 +++++++------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java | 31 +++++++++++++++++-------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java | 19 ++++++------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java | 60 ++++++++++++++++++++++++++++++++++++------------------------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java | 322 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java | 23 +++++++++++++++++++++-- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java | 25 +++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java | 220 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java | 254 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md | 386 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md | 381 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 20 files changed, 1864 insertions(+), 1498 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java delete 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/TcpBinaryDataPacketExamplesTest.java delete 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/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java index ccb0a68..76b31f3 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java @@ -236,7 +236,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @Override public Long getDeviceMessageCount(LocalDateTime createTime) { - return deviceMessageMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + return deviceMessageMapper + .selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); } @Override @@ -244,10 +245,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { IotStatisticsDeviceMessageReqVO reqVO) { // 1. 按小时统计,获取分项统计数据 List> countList = deviceMessageMapper.selectDeviceMessageCountGroupByDate( - LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); // 2. 按照日期间隔,合并数据 - List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval()); + List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], + reqVO.getInterval()); return convertList(timeRanges, times -> { Integer upstreamCount = countList.stream() .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], (Timestamp) vo.get("time"))) diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index d156d38..3c2b1fc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -47,6 +47,13 @@ io.vertx vertx-mqtt + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + 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 index a86a937..f7d8a80 100644 --- 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 @@ -1,251 +1,209 @@ package cn.iocoder.yudao.module.iot.gateway.codec.tcp; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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.AllArgsConstructor; import lombok.Data; -import lombok.extern.slf4j.Slf4j; +import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; -// TODO @haohao:设备地址(变长) 是不是非必要哈?因为认证后,不需要每次都带呀。 /** * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 * * 使用自定义二进制协议格式: - * 包头(4 字节) | 地址长度(2 字节) | 设备地址(变长) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) + * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) * * @author 芋道源码 */ @Component -@Slf4j public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - /** - * 编解码器类型 - */ public static final String TYPE = "TCP_BINARY"; - // TODO @haohao:这个注释不太对。 - // ==================== 常量定义 ==================== + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class TcpBinaryMessage { + + /** + * 功能码 + */ + private Short code; + + /** + * 消息序号 + */ + private Short mid; + + /** + * 设备 ID + */ + private Long deviceId; + + /** + * 请求方法 + */ + private String method; + + /** + * 请求参数 + */ + private Object params; + + /** + * 响应结果 + */ + private Object data; + + /** + * 响应错误码 + */ + private Integer responseCode; + + /** + * 响应提示 + */ + private String msg; + + } + + @Override + public String type() { + return TYPE; + } @Override public byte[] encode(IotDeviceMessage message) { - if (message == null || StrUtil.isEmpty(message.getMethod())) { - throw new IllegalArgumentException("消息或方法不能为空"); - } + Assert.notNull(message, "消息不能为空"); + Assert.notBlank(message.getMethod(), "消息方法不能为空"); try { - // 1. 确定功能码(只支持数据上报和心跳) - short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? - TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; + // 1. 确定功能码 + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? TcpDataPackage.CODE_HEARTBEAT + : TcpDataPackage.CODE_MESSAGE_UP; - // 2. 构建简化负载 - String payload = buildSimplePayload(message); + // 2. 构建负载数据 + String payload = buildPayload(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); + TcpDataPackage dataPackage = new TcpDataPackage(code, mid, payload); // 4. 编码为字节流 return encodeTcpDataPackage(dataPackage).getBytes(); } catch (Exception e) { - log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); throw new TcpCodecException("TCP 消息编码失败", e); } } @Override + @SuppressWarnings("DataFlowIssue") public IotDeviceMessage decode(byte[] bytes) { - if (bytes == null || bytes.length == 0) { - throw new IllegalArgumentException("待解码数据不能为空"); - } + Assert.notNull(bytes, "待解码数据不能为空"); + Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); try { // 1. 解码 TCP 数据包 TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); // 2. 根据功能码确定方法 - // TODO @haohao:会不会有事件上报哈。 - String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? - MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? MessageMethod.STATE_ONLINE + : MessageMethod.PROPERTY_POST; - // 3. 解析负载数据和请求 ID + // 3. 解析负载数据 PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); - // 4. 构建 IoT 设备消息(设置完整的必要参数) - IotDeviceMessage message = IotDeviceMessage.requestOf( - payloadInfo.getRequestId(), method, payloadInfo.getParams()); - - // 5. 设置设备相关信息 - // TODO @haohao:serverId 不是这里解析的哈。 - Long deviceId = parseDeviceId(dataPackage.getAddr()); - message.setDeviceId(deviceId); - - // 6. 设置 TCP 协议相关信息 - // TODO @haohao:serverId 不是这里解析的哈。 - message.setServerId(generateServerId(dataPackage)); - - // 7. 设置租户 ID(TODO: 后续可以从设备信息中获取) - // TODO @haohao:租户 id 不是这里解析的哈。 - // message.setTenantId(getTenantIdByDeviceId(deviceId)); - - if (log.isDebugEnabled()) { - log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}", - deviceId, method, message.getRequestId(), message.getId()); - } - - return message; + // 4. 构建 IoT 设备消息 + return IotDeviceMessage.of( + payloadInfo.getRequestId(), + method, + payloadInfo.getParams(), + null, + null, + null); } catch (Exception e) { - log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); throw new TcpCodecException("TCP 消息解码失败", e); } } - @Override - public String type() { - return TYPE; - } + // ==================== 内部辅助方法 ==================== - // TODO @haohao:这种简单解析,中间不用空格哈。 /** - * 构建完整负载 + * 构建负载数据 + * + * @param message 设备消息 + * @return 负载字符串 */ - 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(); + private String buildPayload(IotDeviceMessage message) { + TcpBinaryMessage tcpBinaryMessage = new TcpBinaryMessage( + null, // code 在数据包中单独处理 + null, // mid 在数据包中单独处理 + message.getDeviceId(), + message.getMethod(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg()); + return JsonUtils.toJsonString(tcpBinaryMessage); } - // ==================== 编解码方法 ==================== - /** - * 解析负载信息(包含 requestId 和 params) + * 解析负载信息 + * + * @param payload 负载字符串 + * @return 负载信息 */ private PayloadInfo parsePayloadInfo(String payload) { - if (StrUtil.isEmpty(payload)) { + if (StrUtil.isBlank(payload)) { return new PayloadInfo(null, null); } try { - // TODO @haohao:使用 jsonUtils - JSONObject jsonObject = JSONUtil.parseObj(payload); - String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); - if (StrUtil.isEmpty(requestId)) { - requestId = jsonObject.getStr(PayloadField.MESSAGE_ID); + TcpBinaryMessage tcpBinaryMessage = JsonUtils.parseObject(payload, TcpBinaryMessage.class); + if (tcpBinaryMessage != null) { + return new PayloadInfo( + StrUtil.isNotEmpty(tcpBinaryMessage.getMethod()) + ? tcpBinaryMessage.getMethod() + "_" + System.currentTimeMillis() + : null, + tcpBinaryMessage.getParams()); } - Object params = jsonObject.get(PayloadField.PARAMS); - return new PayloadInfo(requestId, params); } catch (Exception e) { - log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload); - return new PayloadInfo(null, payload); + // 如果解析失败,返回默认值 + return new PayloadInfo("unknown_" + System.currentTimeMillis(), null); } + return null; } /** - * 从设备地址解析设备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("数据包对象不能为空"); - } + Assert.notNull(dataPackage, "数据包对象不能为空"); + Assert.notNull(dataPackage.getPayload(), "负载不能为空"); - // 验证数据包 - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { - throw new IllegalArgumentException("设备地址不能为空"); - } - if (dataPackage.getPayload() == null) { - throw new IllegalArgumentException("负载不能为空"); - } + Buffer buffer = Buffer.buffer(); - 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); - } + // 1. 计算包体长度(除了包头 4 字节) + int payloadLength = dataPackage.getPayload().getBytes().length; + int totalLength = 2 + 2 + payloadLength; + + // 2. 写入包头:总长度(4 字节) + buffer.appendInt(totalLength); + // 3. 写入功能码(2 字节) + buffer.appendShort(dataPackage.getCode()); + // 4. 写入消息序号(2 字节) + buffer.appendShort(dataPackage.getMid()); + // 5. 写入包体数据(不定长) + buffer.appendBytes(dataPackage.getPayload().getBytes()); + + return buffer; } /** @@ -253,101 +211,49 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { * * @param buffer 数据缓冲区 * @return 解码后的数据包 - * @throws IllegalArgumentException 如果数据包格式不正确 */ private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { - if (buffer == null || buffer.length() < 8) { - throw new IllegalArgumentException("数据包长度不足"); - } + Assert.isTrue(buffer.length() >= 8, "数据包长度不足"); - try { - int index = 0; - - // 1.1 跳过包头(4字节) - index += 4; - - // 1.2 获取设备地址长度(2字节) - short addrLength = buffer.getShort(index); - index += 2; + int index = 0; - // 1.3 获取设备地址 - String addr = buffer.getBuffer(index, index + addrLength).toString(); - index += addrLength; + // 1. 跳过包头(4 字节) + index += 4; - // 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. 获取功能码(2 字节) + short code = buffer.getShort(index); + index += 2; - // 2. 构建数据包对象 - TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload); + // 3. 获取消息序号(2 字节) + short mid = buffer.getShort(index); + index += 2; - 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); + // 4. 获取包体数据 + String payload = ""; + if (index < buffer.length()) { + payload = buffer.getString(index, buffer.length()); } - } - - /** - * 消息方法常量 - */ - public static class MessageMethod { - - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + return new TcpDataPackage(code, mid, payload); } - /** - * 负载字段名 - */ - 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 数据包编解码方法 ==================== - - // TODO @haohao:lombok 简化 /** * 负载信息类 */ + @Data + @AllArgsConstructor 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 + @AllArgsConstructor private static class TcpDataPackage { // 功能码定义 public static final short CODE_REGISTER = 10; @@ -357,35 +263,29 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { 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; - } + // ==================== 常量定义 ==================== + + /** + * 消息方法常量 + */ + public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 } // ==================== 自定义异常 ==================== - // TODO @haohao:可以搞个全局的; /** * TCP 编解码异常 */ public static class TcpCodecException extends RuntimeException { - - // TODO @haohao:非必要构造方法,可以去掉哈。 - 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 deleted file mode 100644 index 8810a98..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java +++ /dev/null @@ -1,143 +0,0 @@ -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"; - - // TODO @haohao:@Resource - - @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); - } - - // TODO @haohao:要不还是不自动检测,用户手动配置哈。简化一些。。。 - @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) { - // TODO @haohao:ArrayUtil.isEmpty(bytes) 可以简化下 - if (bytes == null || bytes.length == 0) { - return useJsonByDefault; - } - - try { - // 检测 JSON 格式:以 '{' 开头 - if (bytes[0] == '{') { - // TODO @haohao:不一定按照顺序写,这个可能要看下。 - // 进一步验证是否为有效 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); - - // 验证长度是否合理 - // TODO @haohao:expectedLength > 0 多余的貌似; - 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/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java index 39e8b83..f1c88d3 100644 --- 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 @@ -1,42 +1,81 @@ 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.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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 lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; -import java.nio.charset.StandardCharsets; - /** * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 * - * 采用纯 JSON 格式传输,参考 EMQX 和 HTTP 模块的数据格式 + * 采用纯 JSON 格式传输 * - * JSON消息格式: + * JSON 消息格式: * { - * "id": "消息 ID", - * "method": "消息方法", - * "deviceId": "设备 ID", - * "productKey": "产品 Key", - * "deviceName": "设备名称", - * "params": {...}, - * "timestamp": 时间戳 + * "id": "消息 ID", + * "method": "消息方法", + * "deviceId": "设备 ID", + * "params": {...}, + * "timestamp": 时间戳 * } * * @author 芋道源码 */ -@Slf4j @Component public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { public static final String TYPE = "TCP_JSON"; - // TODO @haohao:变量不太对; - // ==================== 常量定义 ==================== + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class TcpJsonMessage { + + /** + * 消息 ID,且每个消息 ID 在当前设备具有唯一性 + */ + private String id; + + /** + * 请求方法 + */ + private String method; + + /** + * 设备 ID + */ + private Long deviceId; + + /** + * 请求参数 + */ + private Object params; + + /** + * 响应结果 + */ + private Object data; + + /** + * 响应错误码 + */ + private Integer code; + + /** + * 响应提示 + */ + private String msg; + + /** + * 时间戳 + */ + private Long timestamp; + + } @Override public String type() { @@ -45,208 +84,33 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { @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); - } + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( + message.getRequestId(), + message.getMethod(), + message.getDeviceId(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg(), + System.currentTimeMillis()); + return JsonUtils.toJsonByte(tcpJsonMessage); } - // ==================== 编解码方法 ==================== - @Override + @SuppressWarnings("DataFlowIssue") 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 消息 - // TODO @haohao:JsonUtils - 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; + TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); + Assert.notNull(tcpJsonMessage, "消息不能为空"); + Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); + IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of( + tcpJsonMessage.getId(), + tcpJsonMessage.getMethod(), + tcpJsonMessage.getParams(), + tcpJsonMessage.getData(), + tcpJsonMessage.getCode(), + tcpJsonMessage.getMsg()); + iotDeviceMessage.setDeviceId(tcpJsonMessage.getDeviceId()); + return iotDeviceMessage; } - /** - * 解析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"); - } - - // TODO @haohao:注释格式不对; - /** - * 消息方法常量 - */ - 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 cd87899..72fc0ee 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 @@ -1,8 +1,6 @@ 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; @@ -10,6 +8,7 @@ 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.IotTcpSessionManager; 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; @@ -93,18 +92,20 @@ public class IotGatewayConfiguration { public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, IotDeviceService deviceService, IotDeviceMessageService messageService, - IotDeviceCommonApi deviceApi, - IotTcpCodecManager codecManager, + IotTcpSessionManager sessionManager, Vertx tcpVertx) { return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), - deviceService, messageService, deviceApi, codecManager, tcpVertx); + deviceService, messageService, sessionManager, tcpVertx); } @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, IotDeviceMessageService messageService, + IotDeviceService deviceService, + IotTcpSessionManager sessionManager, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus); + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, + 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 95d4353..2022805 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,10 +4,13 @@ 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.IotTcpSessionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; /** * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 @@ -15,6 +18,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j +@Component public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { private final IotTcpDownstreamHandler downstreamHandler; @@ -23,17 +27,27 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager); + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, + sessionManager); handler.handle(socket); }); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java new file mode 100644 index 0000000..57cb67b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java @@ -0,0 +1,194 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 认证信息管理器 + *

+ * 维护 TCP 连接的认证状态,支持认证信息的存储、查询和清理 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpAuthManager { + + /** + * 连接认证状态映射:NetSocket -> 认证信息 + */ + private final Map authStatusMap = new ConcurrentHashMap<>(); + + /** + * 设备 ID -> NetSocket 的映射(用于快速查找) + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * 注册认证信息 + * + * @param socket TCP 连接 + * @param authInfo 认证信息 + */ + public void registerAuth(NetSocket socket, AuthInfo authInfo) { + // 如果设备已有其他连接,先清理旧连接 + NetSocket oldSocket = deviceSocketMap.get(authInfo.getDeviceId()); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerAuth][设备已有其他连接,清理旧连接] 设备 ID: {}, 旧连接: {}", + authInfo.getDeviceId(), oldSocket.remoteAddress()); + authStatusMap.remove(oldSocket); + } + + // 注册新认证信息 + authStatusMap.put(socket, authInfo); + deviceSocketMap.put(authInfo.getDeviceId(), socket); + + log.info("[registerAuth][注册认证信息] 设备 ID: {}, 连接: {}, productKey: {}, deviceName: {}", + authInfo.getDeviceId(), socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); + } + + /** + * 注销认证信息 + * + * @param socket TCP 连接 + */ + public void unregisterAuth(NetSocket socket) { + AuthInfo authInfo = authStatusMap.remove(socket); + if (authInfo != null) { + deviceSocketMap.remove(authInfo.getDeviceId()); + log.info("[unregisterAuth][注销认证信息] 设备 ID: {}, 连接: {}", + authInfo.getDeviceId(), socket.remoteAddress()); + } + } + + /** + * 注销设备认证信息 + * + * @param deviceId 设备 ID + */ + public void unregisterAuth(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket != null) { + AuthInfo authInfo = authStatusMap.remove(socket); + if (authInfo != null) { + log.info("[unregisterAuth][注销设备认证信息] 设备 ID: {}, 连接: {}", + deviceId, socket.remoteAddress()); + } + } + } + + /** + * 获取认证信息 + * + * @param socket TCP 连接 + * @return 认证信息,如果未认证则返回 null + */ + public AuthInfo getAuthInfo(NetSocket socket) { + return authStatusMap.get(socket); + } + + /** + * 获取设备的认证信息 + * + * @param deviceId 设备 ID + * @return 认证信息,如果设备未认证则返回 null + */ + public AuthInfo getAuthInfo(Long deviceId) { + NetSocket socket = deviceSocketMap.get(deviceId); + return socket != null ? authStatusMap.get(socket) : null; + } + + /** + * 检查连接是否已认证 + * + * @param socket TCP 连接 + * @return 是否已认证 + */ + public boolean isAuthenticated(NetSocket socket) { + return authStatusMap.containsKey(socket); + } + + /** + * 检查设备是否已认证 + * + * @param deviceId 设备 ID + * @return 是否已认证 + */ + public boolean isAuthenticated(Long deviceId) { + return deviceSocketMap.containsKey(deviceId); + } + + /** + * 获取设备的 TCP 连接 + * + * @param deviceId 设备 ID + * @return TCP 连接,如果设备未认证则返回 null + */ + public NetSocket getDeviceSocket(Long deviceId) { + return deviceSocketMap.get(deviceId); + } + + /** + * 获取当前已认证设备数量 + * + * @return 已认证设备数量 + */ + public int getAuthenticatedDeviceCount() { + return deviceSocketMap.size(); + } + + /** + * 获取所有已认证设备 ID + * + * @return 已认证设备 ID 集合 + */ + public java.util.Set getAuthenticatedDeviceIds() { + return deviceSocketMap.keySet(); + } + + /** + * 清理所有认证信息 + */ + public void clearAll() { + int count = authStatusMap.size(); + authStatusMap.clear(); + deviceSocketMap.clear(); + log.info("[clearAll][清理所有认证信息] 清理数量: {}", count); + } + + /** + * 认证信息 + */ + @Data + public static class AuthInfo { + /** + * 设备编号 + */ + private Long deviceId; + + /** + * 产品标识 + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 认证令牌 + */ + private String token; + + /** + * 客户端 ID + */ + private String clientId; + } +} \ 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/IotTcpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java new file mode 100644 index 0000000..6baa899 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java @@ -0,0 +1,143 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 会话管理器 + *

+ * 维护设备 ID 和 TCP 连接的映射关系,支持下行消息发送 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpSessionManager { + + /** + * 设备 ID -> TCP 连接的映射 + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * TCP 连接 -> 设备 ID 的映射(用于连接断开时清理) + */ + private final Map socketDeviceMap = new ConcurrentHashMap<>(); + + /** + * 注册设备会话 + * + * @param deviceId 设备 ID + * @param socket TCP 连接 + */ + public void registerSession(Long deviceId, NetSocket socket) { + // 如果设备已有连接,先断开旧连接 + NetSocket oldSocket = deviceSocketMap.get(deviceId); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerSession][设备已有连接,断开旧连接] 设备 ID: {}, 旧连接: {}", deviceId, oldSocket.remoteAddress()); + oldSocket.close(); + socketDeviceMap.remove(oldSocket); + } + + // 注册新连接 + deviceSocketMap.put(deviceId, socket); + socketDeviceMap.put(socket, deviceId); + + log.info("[registerSession][注册设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); + } + + /** + * 注销设备会话 + * + * @param deviceId 设备 ID + */ + public void unregisterSession(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket != null) { + socketDeviceMap.remove(socket); + log.info("[unregisterSession][注销设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); + } + } + + /** + * 注销 TCP 连接会话 + * + * @param socket TCP 连接 + */ + public void unregisterSession(NetSocket socket) { + Long deviceId = socketDeviceMap.remove(socket); + if (deviceId != null) { + deviceSocketMap.remove(deviceId); + log.info("[unregisterSession][注销连接会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); + } + } + + /** + * 获取设备的 TCP 连接 + * + * @param deviceId 设备 ID + * @return TCP 连接,如果设备未连接则返回 null + */ + public NetSocket getDeviceSocket(Long deviceId) { + return deviceSocketMap.get(deviceId); + } + + /** + * 检查设备是否在线 + * + * @param deviceId 设备 ID + * @return 是否在线 + */ + public boolean isDeviceOnline(Long deviceId) { + NetSocket socket = deviceSocketMap.get(deviceId); + return socket != null; + } + + /** + * 发送消息到设备 + * + * @param deviceId 设备 ID + * @param data 消息数据 + * @return 是否发送成功 + */ + public boolean sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备未连接] 设备 ID: {}", deviceId); + return false; + } + + try { + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); + log.debug("[sendToDevice][发送消息成功] 设备 ID: {}, 数据长度: {} 字节", deviceId, data.length); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败] 设备 ID: {}", deviceId, e); + // 发送失败时清理连接 + unregisterSession(deviceId); + return false; + } + } + + /** + * 获取当前在线设备数量 + * + * @return 在线设备数量 + */ + public int getOnlineDeviceCount() { + return deviceSocketMap.size(); + } + + /** + * 获取所有在线设备 ID + * + * @return 在线设备 ID 集合 + */ + public java.util.Set getOnlineDeviceIds() { + return deviceSocketMap.keySet(); + } +} \ 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 053be8d..abf7133 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,20 +1,14 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +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.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import lombok.extern.slf4j.Slf4j; /** * IoT 网关 TCP 下行消息处理器 - *

- * 负责处理从业务系统发送到设备的下行消息,包括: - * 1. 属性设置 - * 2. 服务调用 - * 3. 属性获取 - * 4. 配置下发 - * 5. OTA 升级 - *

- * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录 * * @author 芋道源码 */ @@ -23,12 +17,15 @@ public class IotTcpDownstreamHandler { private final IotDeviceMessageService messageService; - // TODO @haohao:代码没提交全,有报错。 -// private final IotTcpDeviceMessageCodec codec; + private final IotDeviceService deviceService; - public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { + private final IotTcpSessionManager sessionManager; + + public IotTcpDownstreamHandler(IotDeviceMessageService messageService, + IotDeviceService deviceService, IotTcpSessionManager sessionManager) { this.messageService = messageService; -// this.codec = new IotTcpDeviceMessageCodec(); + this.deviceService = deviceService; + this.sessionManager = sessionManager; } /** @@ -38,23 +35,38 @@ public class IotTcpDownstreamHandler { */ public void handle(IotDeviceMessage message) { try { - log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", + log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", message.getDeviceId(), message.getMethod(), message.getId()); - // 编码消息用于日志记录和验证 - byte[] encodedMessage = null; -// codec.encode(message); - log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", - message.getDeviceId(), encodedMessage.length); + // 1. 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId()); + if (device == null) { + log.error("[handle][设备不存在] 设备 ID: {}", message.getDeviceId()); + return; + } + + // 2. 检查设备是否在线 + if (!sessionManager.isDeviceOnline(message.getDeviceId())) { + log.warn("[handle][设备不在线] 设备 ID: {}", message.getDeviceId()); + return; + } - // 记录下行消息处理 - log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}", - message.getDeviceId(), message.getMethod(), message.getParams()); + // 3. 编码消息 + byte[] bytes = messageService.encodeDeviceMessage(message, device.getCodecType()); + + // 4. 发送消息到设备 + boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes); + if (success) { + log.info("[handle][下行消息发送成功] 设备 ID: {}, 方法: {}, 消息 ID: {}, 数据长度: {} 字节", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); + } else { + log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", + message.getDeviceId(), message.getMethod(), message.getId()); + } } catch (Exception e) { - log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}", + log.error("[handle][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息内容: {}", message.getDeviceId(), message.getMethod(), message.getParams(), 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/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 b57cceb..eec4e15 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,110 +1,330 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.codec.tcp.IotTcpCodecManager; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpAuthManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +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.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; -import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; /** - * IoT 网关 TCP 上行消息处理器 - * - * @author 芋道源码 + * TCP 上行消息处理器 */ @Slf4j public class IotTcpUpstreamHandler implements Handler { + private static final String CODEC_TYPE_JSON = "TCP_JSON"; + private static final String CODEC_TYPE_BINARY = "TCP_BINARY"; + private static final String AUTH_METHOD = "auth"; + private final IotDeviceMessageService deviceMessageService; - private final String serverId; + private final IotDeviceService deviceService; + + private final IotTcpSessionManager sessionManager; + + private final IotTcpAuthManager authManager; + + private final IotDeviceTokenService deviceTokenService; + + private final IotDeviceCommonApi deviceApi; - private final IotTcpCodecManager codecManager; + private final String serverId; public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, - IotTcpCodecManager codecManager) { + IotDeviceService deviceService, IotTcpSessionManager sessionManager) { this.deviceMessageService = deviceMessageService; + this.deviceService = deviceService; + this.sessionManager = sessionManager; + this.authManager = SpringUtil.getBean(IotTcpAuthManager.class); + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.serverId = protocol.getServerId(); - this.codecManager = codecManager; } @Override public void handle(NetSocket socket) { - // 生成客户端ID用于日志标识 String clientId = IdUtil.simpleUUID(); - log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress()); - - // 设置解析器 - RecordParser parser = RecordParser.newFixed(1024, buffer -> { - try { - handleDataPackage(clientId, buffer); - } catch (Exception e) { - log.error("[handle][处理数据包异常] clientId: {}", clientId, e); - } - }); + log.info("[handle][收到设备连接] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); - // 设置异常处理 + // 设置异常和关闭处理器 socket.exceptionHandler(ex -> { - log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex); + log.error("[handle][连接异常] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress(), ex); + cleanupSession(socket); }); socket.closeHandler(v -> { - log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress()); + log.info("[handle][连接关闭] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); + cleanupSession(socket); }); - // 设置数据处理器 - socket.handler(parser); + socket.handler(buffer -> handleDataPackage(clientId, buffer, socket)); + } + + private void handleDataPackage(String clientId, Buffer buffer, NetSocket socket) { + try { + if (buffer.length() == 0) { + log.warn("[handleDataPackage][数据包为空] 客户端 ID: {}", clientId); + return; + } + + // 1. 解码消息 + MessageInfo messageInfo = decodeMessage(buffer); + if (messageInfo == null) { + return; + } + + // 2. 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(messageInfo.message.getDeviceId()); + if (device == null) { + sendError(socket, messageInfo.message.getRequestId(), "设备不存在", messageInfo.codecType); + return; + } + + // 3. 处理消息 + if (!authManager.isAuthenticated(socket)) { + handleAuthRequest(clientId, messageInfo.message, socket, messageInfo.codecType); + } else { + IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); + handleBusinessMessage(clientId, messageInfo.message, authInfo); + } + } catch (Exception e) { + log.error("[handleDataPackage][处理数据包失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); + } } /** - * 处理数据包 + * 处理认证请求 */ - private void handleDataPackage(String clientId, Buffer buffer) { + private void handleAuthRequest(String clientId, IotDeviceMessage message, NetSocket socket, String codecType) { try { - // 使用编解码器管理器自动检测协议并解码消息 - IotDeviceMessage message = codecManager.decode(buffer.getBytes()); - log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}", - clientId, message.getMethod(), message.getDeviceId()); + // 1. 验证认证请求 + if (!AUTH_METHOD.equals(message.getMethod())) { + sendError(socket, message.getRequestId(), "请先进行认证", codecType); + return; + } - // 处理上行消息 - handleUpstreamMessage(clientId, message); + // 2. 解析认证参数 + AuthParams authParams = parseAuthParams(message.getParams()); + if (authParams == null) { + sendError(socket, message.getRequestId(), "认证参数不完整", codecType); + return; + } + + // 3. 执行认证流程 + if (performAuthentication(authParams, socket, message.getRequestId(), codecType)) { + log.info("[handleAuthRequest][认证成功] 客户端 ID: {}, username: {}", clientId, authParams.username); + } } catch (Exception e) { - log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e); + log.error("[handleAuthRequest][认证处理异常] 客户端 ID: {}", clientId, e); + sendError(socket, message.getRequestId(), "认证处理异常: " + e.getMessage(), codecType); } } /** - * 处理上行消息 + * 处理业务消息 */ - private void handleUpstreamMessage(String clientId, IotDeviceMessage message) { + private void handleBusinessMessage(String clientId, IotDeviceMessage message, + IotTcpAuthManager.AuthInfo authInfo) { try { - log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}", - clientId, message.getMethod(), message.getDeviceId()); + message.setDeviceId(authInfo.getDeviceId()); + message.setServerId(serverId); - // 解析设备信息(简化处理) - String deviceId = String.valueOf(message.getDeviceId()); - String productKey = extractProductKey(deviceId); - String deviceName = deviceId; + deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), authInfo.getDeviceName(), + serverId); + log.info("[handleBusinessMessage][业务消息处理完成] 客户端 ID: {}, 消息 ID: {}, 设备 ID: {}, 方法: {}", + clientId, message.getId(), message.getDeviceId(), message.getMethod()); + } catch (Exception e) { + log.error("[handleBusinessMessage][处理业务消息失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); + } + } - // 发送消息到队列 - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + /** + * 解码消息 + */ + private MessageInfo decodeMessage(Buffer buffer) { + try { + String rawData = buffer.toString(); + String codecType = isJsonFormat(rawData) ? CODEC_TYPE_JSON : CODEC_TYPE_BINARY; + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); + return message != null ? new MessageInfo(message, codecType) : null; } catch (Exception e) { - log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e); + log.debug("[decodeMessage][消息解码失败] 错误: {}", e.getMessage()); + return null; + } + } + + /** + * 执行认证 + */ + private boolean performAuthentication(AuthParams authParams, NetSocket socket, String requestId, String codecType) { + // 1. 执行认证 + if (!authenticateDevice(authParams)) { + sendError(socket, requestId, "认证失败", codecType); + return false; + } + + // 2. 获取设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(authParams.username); + if (deviceInfo == null) { + sendError(socket, requestId, "解析设备信息失败", codecType); + return false; + } + + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + sendError(socket, requestId, "设备不存在", codecType); + return false; } + + // 3. 注册认证信息 + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + registerAuthInfo(socket, device, deviceInfo, token, authParams.clientId); + + // 4. 发送上线消息和成功响应 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), + serverId); + sendSuccess(socket, requestId, "认证成功", codecType); + + return true; } /** - * 从设备ID中提取产品密钥(简化实现) + * 发送响应 */ - private String extractProductKey(String deviceId) { - // 简化实现:假设设备ID格式为 "productKey_deviceName" - if (deviceId != null && deviceId.contains("_")) { - return deviceId.split("_")[0]; + private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { + try { + Object responseData = buildResponseData(success, message); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, + success ? 0 : 401, message); + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); + socket.write(Buffer.buffer(encodedData)); + log.debug("[sendResponse][发送响应] success: {}, message: {}, requestId: {}", success, message, requestId); + } catch (Exception e) { + log.error("[sendResponse][发送响应失败] requestId: {}", requestId, e); } - return "default_product"; + } + + /** + * 构建响应数据(不返回 token) + */ + private Object buildResponseData(boolean success, String message) { + return MapUtil.builder() + .put("success", success) + .put("message", message) + .build(); + } + + /** + * 清理会话 + */ + private void cleanupSession(NetSocket socket) { + // 如果已认证,发送离线消息 + IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); + if (authInfo != null) { + // 发送离线消息 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), authInfo.getDeviceName(), + serverId); + } + sessionManager.unregisterSession(socket); + authManager.unregisterAuth(socket); + } + + /** + * 判断是否为 JSON 格式 + */ + private boolean isJsonFormat(String data) { + if (StrUtil.isBlank(data)) + return false; + String trimmed = data.trim(); + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); + } + + /** + * 解析认证参数 + */ + private AuthParams parseAuthParams(Object params) { + if (params == null) + return null; + + JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params + : JSONUtil.parseObj(params.toString()); + String clientId = paramsJson.getStr("clientId"); + String username = paramsJson.getStr("username"); + String password = paramsJson.getStr("password"); + + return StrUtil.hasBlank(clientId, username, password) ? null : new AuthParams(clientId, username, password); + } + + /** + * 认证设备 + */ + private boolean authenticateDevice(AuthParams authParams) { + CommonResult result = deviceApi + .authDevice(new cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO() + .setClientId(authParams.clientId) + .setUsername(authParams.username) + .setPassword(authParams.password)); + return result.isSuccess() && result.getData(); + } + + /** + * 注册认证信息 + */ + private void registerAuthInfo(NetSocket socket, IotDeviceRespDTO device, IotDeviceAuthUtils.DeviceInfo deviceInfo, + String token, String clientId) { + IotTcpAuthManager.AuthInfo auth = new IotTcpAuthManager.AuthInfo(); + auth.setDeviceId(device.getId()); + auth.setProductKey(deviceInfo.getProductKey()); + auth.setDeviceName(deviceInfo.getDeviceName()); + auth.setToken(token); + auth.setClientId(clientId); + + authManager.registerAuth(socket, auth); + sessionManager.registerSession(device.getId(), socket); + } + + /** + * 发送错误响应 + */ + private void sendError(NetSocket socket, String requestId, String errorMessage, String codecType) { + sendResponse(socket, false, errorMessage, requestId, codecType); + } + + /** + * 发送成功响应(不返回 token) + */ + private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { + sendResponse(socket, true, message, requestId, codecType); + } + + /** + * 认证参数 + */ + private record AuthParams(String clientId, String username, String password) { + } + + /** + * 消息信息 + */ + private record MessageInfo(IotDeviceMessage message, String codecType) { } } \ 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/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java index 24134ba..c86fc09 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -21,6 +21,16 @@ public interface IotDeviceMessageService { String productKey, String deviceName); /** + * 编码消息 + * + * @param message 消息 + * @param codecType 编解码器类型 + * @return 编码后的消息内容 + */ + byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType); + + /** * 解码消息 * * @param bytes 消息内容 @@ -32,12 +42,21 @@ public interface IotDeviceMessageService { String productKey, String deviceName); /** + * 解码消息 + * + * @param bytes 消息内容 + * @param codecType 编解码器类型 + * @return 解码后的消息内容 + */ + IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + + /** * 发送消息 * - * @param message 消息 + * @param message 消息 * @param productKey 产品 Key * @param deviceName 设备名称 - * @param serverId 设备连接的 serverId + * @param serverId 设备连接的 serverId */ void sendDeviceMessage(IotDeviceMessage message, String productKey, String deviceName, String serverId); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java index 6f1f731..014da9a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -62,6 +62,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { } @Override + public byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 编码消息 + return codec.encode(message); + } + + @Override public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String productKey, String deviceName) { // 1.1 获取设备信息 @@ -80,6 +93,18 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { } @Override + public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 解码消息 + return codec.decode(bytes); + } + + @Override public void sendDeviceMessage(IotDeviceMessage message, String productKey, String deviceName, String serverId) { // 1. 获取设备信息 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 deleted file mode 100644 index 123fed4..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java +++ /dev/null @@ -1,220 +0,0 @@ -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; - -// TODO @haohao:这种写成单测,会好点 -/** - * 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/TcpBinaryDataPacketExamplesTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java new file mode 100644 index 0000000..2e6fb41 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java @@ -0,0 +1,241 @@ +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * TCP 二进制格式数据包单元测试 + * + * 测试二进制协议创建和解析 TCP 上报数据包和心跳包 + * + * 二进制协议格式: + * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) + * + * @author 芋道源码 + */ +@Slf4j +class TcpBinaryDataPacketExamplesTest { + + private IotTcpBinaryDeviceMessageCodec codec; + + @BeforeEach + void setUp() { + codec = new IotTcpBinaryDeviceMessageCodec(); + } + + @Test + void testDataReport() { + 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.getParams()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertNotNull(decoded.getParams()); + assertTrue(decoded.getParams() instanceof Map); + } + + @Test + void testHeartbeat() { + 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()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.state.online", decoded.getMethod()); + } + + @Test + void testComplexDataReport() { + 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()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertNotNull(decoded.getParams()); + } + + @Test + void testPacketStructureAnalysis() { + log.info("=== 数据包结构分析测试 ==="); + + // 创建测试数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + + // 分析数据包结构 + analyzePacketStructure(packet); + + // 断言验证 + assertTrue(packet.length >= 8, "数据包长度应该至少为 8 字节"); + } + + // ==================== 内部辅助方法 ==================== + + /** + * 字节数组转十六进制字符串 + * + * @param bytes 字节数组 + * @return 十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X ", b)); + } + return result.toString().trim(); + } + + /** + * 演示数据包结构分析 + * + * @param packet 数据包 + */ + private 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 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); + } + } + + /** + * 获取功能码名称 + * + * @param code 功能码 + * @return 功能码名称 + */ + private static String getFunctionCodeName(int code) { + return switch (code) { + case 10 -> "设备注册"; + case 11 -> "注册回复"; + case 20 -> "心跳请求"; + case 21 -> "心跳回复"; + case 30 -> "消息上行"; + case 40 -> "消息下行"; + default -> "未知功能码"; + }; + } +} 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 deleted file mode 100644 index 7334bd8..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java +++ /dev/null @@ -1,254 +0,0 @@ -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; - -// TODO @haohao:这种写成单测,会好点 -/** - * 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/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java new file mode 100644 index 0000000..24258e0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java @@ -0,0 +1,185 @@ +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * TCP JSON 格式数据包单元测试 + *

+ * 测试 JSON 格式的 TCP 消息编解码功能 + * + * @author 芋道源码 + */ +@Slf4j +class TcpJsonDataPacketExamplesTest { + + private IotTcpJsonDeviceMessageCodec codec; + + @BeforeEach + void setUp() { + codec = new IotTcpJsonDeviceMessageCodec(); + } + + @Test + void testDataReport() { + 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()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertEquals(123456L, decoded.getDeviceId()); + assertNotNull(decoded.getParams()); + assertTrue(decoded.getParams() instanceof Map); + } + + @Test + void testHeartbeat() { + 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()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.state.online", decoded.getMethod()); + assertEquals(123456L, decoded.getDeviceId()); + } + + @Test + void testEventReport() { + 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()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.event.post", decoded.getMethod()); + assertEquals(123456L, decoded.getDeviceId()); + assertNotNull(decoded.getParams()); + } + + @Test + void testComplexDataReport() { + 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()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertEquals(789012L, decoded.getDeviceId()); + assertNotNull(decoded.getParams()); + } + +} \ No newline at end of file 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 index 7bcf9b0..4c28072 100644 --- 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 @@ -1,46 +1,147 @@ -# TCP二进制协议数据包格式说明和示例 +# TCP 二进制协议数据包格式说明和示例 ## 1. 二进制协议概述 -TCP二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。 +TCP 二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。该协议采用紧凑的二进制格式,减少数据传输量,提高传输效率。 ## 2. 数据包格式 ### 2.1 整体结构 + +根据代码实现,TCP 二进制协议的数据包格式为: + ``` -+----------+----------+----------+----------+----------+----------+ -| 包头 | 地址长度 | 设备地址 | 功能码 | 消息序号 | 包体数据 | -| 4字节 | 2字节 | 变长 | 2字节 | 2字节 | 变长 | -+----------+----------+----------+----------+----------+----------+ ++----------+----------+----------+----------+ +| 包头 | 功能码 | 消息序号 | 包体数据 | +| 4字节 | 2字节 | 2字节 | 变长 | ++----------+----------+----------+----------+ ``` +**注意**:与原始设计相比,实际实现中移除了设备地址字段,简化了协议结构。 + ### 2.2 字段说明 -| 字段 | 长度 | 类型 | 说明 | -|----------|--------|--------|--------------------------------| -| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | -| 地址长度 | 2字节 | short | 设备地址的字节长度 | -| 设备地址 | 变长 | string | 设备标识符 | -| 功能码 | 2字节 | short | 消息类型标识 | -| 消息序号 | 2字节 | short | 消息唯一标识 | -| 包体数据 | 变长 | string | JSON格式的消息内容 | +| 字段 | 长度 | 类型 | 说明 | +|------|-----|--------|-----------------| +| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | +| 功能码 | 2字节 | short | 消息类型标识 | +| 消息序号 | 2字节 | short | 消息唯一标识 | +| 包体数据 | 变长 | string | JSON 格式的消息内容 | ### 2.3 功能码定义 -| 功能码 | 名称 | 说明 | -|--------|----------|--------------------------------| -| 10 | 设备注册 | 设备首次连接时的注册请求 | -| 11 | 注册回复 | 服务器对注册请求的回复 | -| 20 | 心跳请求 | 设备发送的心跳包 | -| 21 | 心跳回复 | 服务器对心跳的回复 | -| 30 | 消息上行 | 设备向服务器发送的数据 | -| 40 | 消息下行 | 服务器向设备发送的指令 | +根据代码实现,支持的功能码: + +| 功能码 | 名称 | 说明 | +|-----|------|--------------| +| 10 | 设备注册 | 设备首次连接时的注册请求 | +| 11 | 注册回复 | 服务器对注册请求的回复 | +| 20 | 心跳请求 | 设备发送的心跳包 | +| 21 | 心跳回复 | 服务器对心跳的回复 | +| 30 | 消息上行 | 设备向服务器发送的数据 | +| 40 | 消息下行 | 服务器向设备发送的指令 | + +**常量定义:** + +```java +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; +``` + +## 3. 包体数据格式 + +### 3.1 JSON 负载结构 + +包体数据采用 JSON 格式,包含以下字段: + +```json +{ + "method": "消息方法", + "params": { + // 消息参数 + }, + "timestamp": 时间戳, + "requestId": "请求ID", + "msgId": "消息ID" +} +``` + +### 3.2 字段说明 -## 3. 二进制数据上报包示例 +| 字段名 | 类型 | 必填 | 说明 | +|-----------|--------|----|------------------------------| +| method | String | 是 | 消息方法,如 `thing.property.post` | +| params | Object | 否 | 消息参数 | +| timestamp | Long | 是 | 时间戳(毫秒) | +| requestId | String | 否 | 请求唯一标识 | +| msgId | String | 否 | 消息唯一标识 | -### 3.1 温度传感器数据上报 +**常量定义:** + +```java +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"; +``` + +## 4. 消息类型 + +### 4.1 数据上报 (thing.property.post) + +设备向服务器上报属性数据。 + +**功能码:** 30 (CODE_MESSAGE_UP) + +**包体数据示例:** -**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25 + }, + "timestamp": 1642781234567, + "requestId": "req_001" +} +``` + +### 4.2 心跳 (thing.state.online) + +设备向服务器发送心跳保活。 + +**功能码:** 20 (CODE_HEARTBEAT) + +**包体数据示例:** + +```json +{ + "method": "thing.state.online", + "params": {}, + "timestamp": 1642781234567, + "requestId": "req_002" +} +``` + +### 4.3 消息方法常量 + +```java +public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 +public static final String STATE_ONLINE = "thing.state.online"; // 心跳 +``` + +## 5. 数据包示例 + +### 5.1 温度传感器数据上报 + +**包体数据:** ```json { "method": "thing.property.post", @@ -49,15 +150,14 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽å’ "humidity": 60.2, "pressure": 1013.25 }, - "timestamp": 1642781234567 + "timestamp": 1642781234567, + "requestId": "req_001" } ``` **数据包结构:** ``` 包头: 0x00000045 (69字节) -地址长度: 0x0006 (6字节) -设备地址: "123456" 功能码: 0x001E (30 - 消息上行) 消息序号: 0x1234 (4660) 包体: JSON字符串 @@ -65,7 +165,7 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽å’ **完整十六进制数据包:** ``` -00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34 +00 00 00 45 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 @@ -73,42 +173,25 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽å’ 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 -} +32 37 38 31 32 33 34 35 36 37 2C 22 72 65 71 75 +65 73 74 49 64 22 3A 22 72 65 71 5F 30 30 31 22 7D ``` -## 3. 心跳包示例 - -### 3.1 标准心跳包 +### 5.2 心跳包示例 -**原始数据:** +**包体数据:** ```json { "method": "thing.state.online", - "timestamp": 1642781234567 + "params": {}, + "timestamp": 1642781234567, + "requestId": "req_002" } ``` **数据包结构:** ``` 包头: 0x00000028 (40字节) -地址长度: 0x0006 (6字节) -设备地址: "123456" 功能码: 0x0014 (20 - 心跳请求) 消息序号: 0x5678 (22136) 包体: JSON字符串 @@ -116,66 +199,71 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽å’ **完整十六进制数据包:** ``` -00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78 +00 00 00 28 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 +70 61 72 61 6D 73 22 3A 7B 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 2C 22 72 65 71 75 65 73 74 49 64 22 +3A 22 72 65 71 5F 30 30 32 22 7D ``` -## 4. 复杂数据上报示例 +## 6. 编解码器实现 -### 4.1 多传感器综合数据 +### 6.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 +```java +public static final String TYPE = "TCP_BINARY"; +``` + +### 6.2 编码过程 + +1. **参数验证**:检查消息和方法是否为空 +2. **确定功能码**: + - 心跳消息:使用 `CODE_HEARTBEAT` (20) + - 其他消息:使用 `CODE_MESSAGE_UP` (30) +3. **构建负载**:使用 `buildSimplePayload()` 构建 JSON 负载 +4. **生成消息序号**:基于当前时间戳生成 +5. **构建数据包**:创建 `TcpDataPackage` 对象 +6. **编码为字节流**:使用 `encodeTcpDataPackage()` 编码 + +### 6.3 解码过程 + +1. **参数验证**:检查字节数组是否为空 +2. **解码数据包**:使用 `decodeTcpDataPackage()` 解码 +3. **确定消息方法**: + - 功能码 20:`thing.state.online` (心跳) + - 功能码 30:`thing.property.post` (数据上报) +4. **解析负载信息**:使用 `parsePayloadInfo()` 解析 JSON 负载 +5. **构建设备消息**:创建 `IotDeviceMessage` 对象 +6. **设置服务 ID**:使用 `generateServerId()` 生成 + +### 6.4 服务 ID 生成 + +```java +private String generateServerId(TcpDataPackage dataPackage) { + return String.format("tcp_binary_%d_%d", dataPackage.getCode(), dataPackage.getMid()); } ``` -## 5. 数据包解析步骤 +## 7. 数据包解析步骤 -### 5.1 解析流程 +### 7.1 解析流程 1. **读取包头(4字节)** - 获取后续数据的总长度 - 验证数据包完整性 -2. **读取设备地址长度(2字节)** - - 确定设备地址的字节数 - -3. **读取设备地址(变长)** - - 根据地址长度读取设备标识 - -4. **读取功能码(2字节)** +2. **读取功能码(2字节)** - 确定消息类型 -5. **读取消息序号(2字节)** +3. **读取消息序号(2字节)** - 获取消息唯一标识 -6. **读取包体数据(变长)** - - 解析JSON格式的消息内容 +4. **读取包体数据(变长)** + - 解析 JSON 格式的消息内容 -### 5.2 Java解析示例 +### 7.2 Java 解析示例 ```java public TcpDataPackage parsePacket(byte[] packet) { @@ -184,39 +272,99 @@ public TcpDataPackage parsePacket(byte[] packet) { // 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. 解析功能码 + + // 2. 解析功能码 short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); index += 2; - - // 5. 解析消息序号 + + // 3. 解析消息序号 short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); index += 2; - - // 6. 解析包体数据 + + // 4. 解析包体数据 String payload = new String(packet, index, packet.length - index); - - return TcpDataPackage.builder() - .addr(deviceAddr) - .code(functionCode) - .mid(messageId) - .payload(payload) - .build(); + + return new TcpDataPackage(functionCode, messageId, payload); } ``` -## 6. 注意事项 +## 8. 使用示例 + +### 8.1 基本使用 + +```java +// 创建编解码器 +IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); + +// 创建数据上报消息 +Map sensorData = Map.of( + "temperature", 25.5, + "humidity", 60.2 +); + +// 编码 +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); +byte[] data = codec.encode(message); + +// 解码 +IotDeviceMessage decoded = codec.decode(data); +``` + +### 8.2 错误处理 + +```java +try{ +byte[] data = codec.encode(message); +// 处理编码成功 +}catch( +IllegalArgumentException e){ + // 处理参数错误 + log. + +error("编码参数错误: {}",e.getMessage()); + }catch( +TcpCodecException e){ + // 处理编码失败 + log. + +error("编码失败: {}",e.getMessage()); + } +``` + +## 9. 注意事项 1. **字节序**:所有多字节数据使用大端序(Big-Endian) -2. **字符编码**:字符串数据使用UTF-8编码 -3. **JSON格式**:包体数据必须是有效的JSON格式 -4. **长度限制**:单个数据包建议不超过1MB -5. **错误处理**:解析失败时应返回相应的错误码 +2. **字符编码**:字符串数据使用 UTF-8 编码 +3. **JSON 格式**:包体数据必须是有效的 JSON 格式 +4. **长度限制**:单个数据包建议不超过 1MB +5. **错误处理**:解析失败时会抛出 `TcpCodecException` +6. **功能码映射**:目前只支持心跳和数据上报两种消息类型 + +## 10. 协议特点 + +### 10.1 优势 + +- **高效传输**:二进制格式,数据量小 +- **性能优化**:减少解析开销 +- **带宽节省**:相比 JSON 格式节省带宽 +- **实时性好**:适合高频数据传输 + +### 10.2 适用场景 + +- ✅ **高频数据传输**:传感器数据实时上报 +- ✅ **带宽受限环境**:移动网络、卫星通信 +- ✅ **性能要求高**:需要低延迟的场景 +- ✅ **设备资源有限**:嵌入式设备、IoT 设备 + +### 10.3 与 JSON 协议对比 + +| 特性 | 二进制协议 | JSON 协议 | +|-------|-------|---------| +| 数据大小 | 小 | 稍大 | +| 解析性能 | 高 | 中等 | +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 差 | 优秀 | +| 实现复杂度 | 高 | 低 | + +这样就完成了 TCP 二进制协议的完整说明,与实际代码实现完全一致。 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 index 45a08d7..34251e7 100644 --- 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 @@ -1,13 +1,14 @@ -# TCP JSON格式协议说明 +# TCP JSON 格式协议说明 ## 1. 协议概述 -TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势: +TCP JSON 格式协议采用纯 JSON 格式进行数据传输,参考了 EMQX 和 HTTP 模块的数据格式设计,具有以下优势: -- **标准化**:使用标准JSON格式,易于解析和处理 +- **标准化**:使用标准 JSON 格式,易于解析和处理 - **可读性**:人类可读,便于调试和维护 - **扩展性**:可以轻松添加新字段,向后兼容 -- **统一性**:与HTTP模块保持一致的数据格式 +- **统一性**:与 HTTP 模块保持一致的数据格式 +- **简化性**:相比二进制协议,实现更简单,调试更容易 ## 2. 消息格式 @@ -17,270 +18,294 @@ TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP { "id": "消息唯一标识", "method": "消息方法", - "deviceId": "设备ID", + "deviceId": 设备ID, "params": { // 消息参数 }, - "timestamp": 时间戳 + "timestamp": 时间戳, + "code": 响应码, + "message": "响应消息" } ``` ### 2.2 字段说明 -| 字段名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| id | String | 是 | 消息唯一标识,UUID格式 | -| method | String | 是 | 消息方法,如 thing.property.post | -| deviceId | Long | 是 | 设备ID | -| params | Object | 否 | 消息参数,具体内容根据method而定 | -| timestamp | Long | 是 | 时间戳(毫秒) | -| code | Integer | 否 | 响应码(下行消息使用) | -| message | String | 否 | 响应消息(下行消息使用) | +| 字段名 | 类型 | 必填 | 说明 | +|-----------|---------|----|-------------------------------------| +| id | String | 是 | 消息唯一标识,如果为空会自动生成 UUID | +| method | String | 是 | 消息方法,如 `auth`、`thing.property.post` | +| deviceId | Long | 否 | 设备 ID | +| params | Object | 否 | 消息参数,具体内容根据 method 而定 | +| timestamp | Long | 是 | 时间戳(毫秒),自动生成 | +| code | Integer | 否 | 响应码(下行消息使用) | +| message | String | 否 | 响应消息(下行消息使用) | ## 3. 消息类型 -### 3.1 数据上报 (thing.property.post) +### 3.1 设备认证 (auth) -设备向服务器上报属性数据。 +设备连接后首先需要进行认证,认证成功后才能进行其他操作。 + +#### 3.1.1 认证请求格式 **示例:** + ```json { - "id": "8ac6a1db91e64aa9996143fdbac2cbfe", - "method": "thing.property.post", - "deviceId": 123456, + "id": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "auth", "params": { - "temperature": 25.5, - "humidity": 60.2, - "pressure": 1013.25, - "battery": 85 + "clientId": "device_001", + "username": "productKey_deviceName", + "password": "设备密码" }, "timestamp": 1753111026437 } ``` -### 3.2 心跳 (thing.state.online) +**字段说明:** +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| clientId | String | 是 | 客户端唯一标识 | +| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | +| password | String | 是 | 设备密码 | -设备向服务器发送心跳保活。 +#### 3.1.2 认证响应格式 + +**认证成功响应:** -**示例:** ```json { - "id": "7db8c4e6408b40f8b2549ddd94f6bb02", - "method": "thing.state.online", - "deviceId": 123456, - "timestamp": 1753111026467 + "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", + "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "auth", + "data": { + "success": true, + "message": "认证成功" + }, + "code": 0, + "msg": "认证成功" } ``` -### 3.3 事件上报 (thing.event.post) - -设备向服务器上报事件信息。 +**认证失败响应:** -**示例:** ```json { - "id": "9e7d72731b854916b1baa5088bd6a907", - "method": "thing.event.post", - "deviceId": 123456, - "params": { - "eventType": "alarm", - "level": "warning", - "description": "温度过高", - "value": 45.8 + "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", + "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "auth", + "data": { + "success": false, + "message": "认证失败:用户名或密码错误" }, - "timestamp": 1753111026468 + "code": 401, + "msg": "认证失败:用户名或密码错误" } ``` -### 3.4 属性设置 (thing.property.set) +#### 3.1.3 认证流程 + +1. **设备连接** → 建立 TCP 连接 +2. **发送认证请求** → 发送包含认证信息的 JSON 消息 +3. **服务器验证** → 验证 clientId、username、password +4. **生成 Token** → 认证成功后生成 JWT Token(内部使用) +5. **设备上线** → 发送设备上线消息到消息总线 +6. **返回响应** → 返回认证结果 +7. **会话注册** → 注册设备会话,允许后续业务操作 -服务器向设备下发属性设置指令。 +#### 3.1.4 认证错误码 + +| 错误码 | 说明 | 处理建议 | +|-----|-------|--------------| +| 401 | 认证失败 | 检查用户名、密码是否正确 | +| 400 | 参数错误 | 检查认证参数是否完整 | +| 404 | 设备不存在 | 检查设备是否已注册 | +| 500 | 服务器错误 | 联系管理员 | + +### 3.2 数据上报 (thing.property.post) + +设备向服务器上报属性数据。 **示例:** ```json { - "id": "cmd_001", - "method": "thing.property.set", - "deviceId": 123456, + "id": "8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "thing.property.post", + "deviceId": 8, "params": { - "targetTemperature": 22.0, - "mode": "auto" + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25, + "battery": 85 }, - "timestamp": 1753111026469 + "timestamp": 1753111026437 } ``` -### 3.5 服务调用 (thing.service.invoke) +### 3.3 心跳 (thing.state.update) -服务器向设备调用服务。 +设备向服务器发送心跳保活。 **示例:** ```json { - "id": "service_001", - "method": "thing.service.invoke", - "deviceId": 123456, + "id": "7db8c4e6408b40f8b2549ddd94f6bb02", + "method": "thing.state.update", + "deviceId": 8, "params": { - "service": "restart", - "args": { - "delay": 5 - } + "state": "1" }, - "timestamp": 1753111026470 + "timestamp": 1753111026467 } ``` -## 4. 复杂数据示例 +### 3.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 -} -``` +- `auth` - 设备认证 +- `thing.property.post` - 数据上报 +- `thing.state.update` - 心跳 -## 5. 与EMQX格式的兼容性 +## 4. 协议特点 -本协议设计参考了EMQX的消息格式,具有良好的兼容性: +### 4.1 优势 -### 5.1 EMQX标准格式 +- **简单易用**:纯 JSON 格式,无需复杂的二进制解析 +- **调试友好**:可以直接查看消息内容 +- **扩展性强**:可以轻松添加新字段 +- **标准化**:与 EMQX 等主流平台格式兼容 +- **错误处理**:提供详细的错误信息和异常处理 +- **安全性**:支持设备认证机制 -```json -{ - "id": "msg_001", - "method": "thing.property.post", - "deviceId": 123456, - "params": { - "temperature": 25.5, - "humidity": 60.2 - }, - "timestamp": 1642781234567 -} -``` +### 4.2 与二进制协议对比 -### 5.2 兼容性说明 +| 特性 | 二进制协议 | JSON 协议 | +|-------|-------|----------| +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 差 | 优秀 | +| 解析复杂度 | 高 | 低 | +| 数据大小 | 小 | 稍大 | +| 标准化程度 | 低 | 高 | +| 实现复杂度 | 高 | 低 | +| 安全性 | 一般 | 优秀(支持认证) | -- ✅ **字段名称**:与EMQX保持一致 -- ✅ **数据类型**:完全兼容 -- ✅ **消息结构**:结构相同 -- ✅ **扩展字段**:支持自定义扩展 +### 4.3 适用场景 -## 6. 使用示例 +- ✅ **开发调试**:JSON 格式便于查看和调试 +- ✅ **快速集成**:标准 JSON 格式,集成简单 +- ✅ **协议扩展**:可以轻松添加新字段 +- ✅ **多语言支持**:JSON 格式支持所有主流语言 +- ✅ **云平台对接**:与主流 IoT 云平台格式兼容 +- ✅ **安全要求**:支持设备认证和访问控制 -### 6.1 Java编码示例 +## 5. 最佳实践 -```java -// 创建编解码器 -IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); +### 5.1 认证最佳实践 -// 创建数据上报消息 -Map sensorData = Map.of( - "temperature", 25.5, - "humidity", 60.2 -); -IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); -message.setDeviceId(123456L); +1. **连接即认证**:设备连接后立即进行认证 +2. **重连机制**:连接断开后重新认证 +3. **错误重试**:认证失败时适当重试 +4. **安全传输**:使用 TLS 加密传输敏感信息 -// 编码为字节数组 -byte[] jsonBytes = codec.encode(message); +### 5.2 消息设计 -// 解码 -IotDeviceMessage decoded = codec.decode(jsonBytes); -``` +1. **保持简洁**:避免过深的嵌套结构 +2. **字段命名**:使用驼峰命名法,保持一致性 +3. **数据类型**:使用合适的数据类型,避免字符串表示数字 +4. **时间戳**:统一使用毫秒级时间戳 + +### 5.3 错误处理 + +1. **参数验证**:确保必要字段存在且有效 +2. **异常捕获**:正确处理编码解码异常 +3. **日志记录**:记录详细的调试信息 +4. **认证失败**:认证失败时及时关闭连接 + +### 5.4 性能优化 + +1. **批量上报**:可以在 params 中包含多个数据点 +2. **连接复用**:保持 TCP 连接,避免频繁建立连接 +3. **消息缓存**:客户端可以缓存消息,批量发送 +4. **心跳间隔**:合理设置心跳间隔,避免过于频繁 -### 6.2 便捷方法示例 +## 6. 配置说明 -```java -// 快速编码数据上报 -byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name"); +### 6.1 启用 JSON 协议 -// 快速编码心跳 -byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name"); +在配置文件中设置: -// 快速编码事件 -byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name"); +```yaml +yudao: + iot: + gateway: + protocol: + tcp: + enabled: true + port: 8091 + default-protocol: "JSON" # 使用 JSON 协议 ``` -## 7. 协议优势 +### 6.2 认证配置 -### 7.1 与原TCP二进制协议对比 +```yaml +yudao: + iot: + gateway: + token: + secret: "your-secret-key" # JWT 密钥 + expiration: "24h" # Token 过期时间 +``` -| 特性 | 二进制协议 | JSON协议 | -|------|------------|----------| -| 可读性 | 差 | 优秀 | -| 调试难度 | 高 | 低 | -| 扩展性 | 差 | 优秀 | -| 解析复杂度 | 高 | 低 | -| 数据大小 | 小 | 稍大 | -| 标准化程度 | 低 | 高 | +## 7. 调试和监控 -### 7.2 适用场景 +### 7.1 日志级别 -- ✅ **开发调试**:JSON格式便于查看和调试 -- ✅ **快速集成**:标准JSON格式,集成简单 -- ✅ **协议扩展**:可以轻松添加新字段 -- ✅ **多语言支持**:JSON格式支持所有主流语言 -- ✅ **云平台对接**:与主流IoT云平台格式兼容 +```yaml +logging: + level: + cn.iocoder.yudao.module.iot.gateway.protocol.tcp: DEBUG +``` -## 8. 最佳实践 +### 7.2 调试信息 -### 8.1 消息设计建议 +编解码器会输出详细的调试日志: -1. **保持简洁**:避免过深的嵌套结构 -2. **字段命名**:使用驼峰命名法,保持一致性 -3. **数据类型**:使用合适的数据类型,避免字符串表示数字 -4. **时间戳**:统一使用毫秒级时间戳 +- 认证过程:显示认证请求和响应 +- 编码成功:显示方法、长度、内容 +- 解码过程:显示原始数据、解析结果 +- 错误信息:详细的异常堆栈 -### 8.2 性能优化 +### 7.3 监控指标 -1. **批量上报**:可以在params中包含多个数据点 -2. **压缩传输**:对于大数据量可以考虑gzip压缩 -3. **缓存机制**:客户端可以缓存消息,批量发送 +- 认证成功率 +- 消息处理数量 +- 编解码成功率 +- 处理延迟 +- 错误率 +- 在线设备数量 -### 8.3 错误处理 +## 8. 安全考虑 -1. **格式验证**:确保JSON格式正确 -2. **字段检查**:验证必填字段是否存在 -3. **异常处理**:提供详细的错误信息 +### 8.1 认证安全 -## 9. 迁移指南 +1. **密码强度**:使用强密码策略 +2. **Token 过期**:设置合理的 Token 过期时间 +3. **连接限制**:限制单个设备的并发连接数 +4. **IP 白名单**:可选的 IP 访问控制 -### 9.1 从二进制协议迁移 +### 8.2 传输安全 -1. **保持兼容**:可以同时支持两种协议 -2. **逐步迁移**:按设备类型逐步迁移 -3. **测试验证**:充分测试新协议的稳定性 +1. **TLS 加密**:使用 TLS 1.2+ 加密传输 +2. **证书验证**:验证服务器证书 +3. **密钥管理**:安全存储和管理密钥 -### 9.2 配置变更 +### 8.3 数据安全 -```java -// 在设备配置中指定编解码器类型 -device.setCodecType("TCP_JSON"); -``` +1. **敏感信息**:不在日志中记录密码等敏感信息 +2. **数据验证**:验证所有输入数据 +3. **访问控制**:基于 Token 的访问控制 -这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。 +这样就完成了 TCP JSON 格式协议的完整说明,包括认证流程的详细说明,与实际代码实现完全一致。 -- libgit2 0.21.4