Commit c9b9fc1f31242804e594638d8d64d6a6705fa967
1 parent
f70f578a
feat:【IoT 物联网】重构 TCP 协议处理,新增 TCP 会话和认证管理
Showing
19 changed files
with
1717 additions
and
1351 deletions
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 { |
| 236 | 236 | |
| 237 | 237 | @Override |
| 238 | 238 | public Long getDeviceMessageCount(LocalDateTime createTime) { |
| 239 | - return deviceMessageMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); | |
| 239 | + return deviceMessageMapper | |
| 240 | + .selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); | |
| 240 | 241 | } |
| 241 | 242 | |
| 242 | 243 | @Override |
| ... | ... | @@ -244,10 +245,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 244 | 245 | IotStatisticsDeviceMessageReqVO reqVO) { |
| 245 | 246 | // 1. 按小时统计,获取分项统计数据 |
| 246 | 247 | List<Map<String, Object>> countList = deviceMessageMapper.selectDeviceMessageCountGroupByDate( |
| 247 | - LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); | |
| 248 | + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), | |
| 249 | + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); | |
| 248 | 250 | |
| 249 | 251 | // 2. 按照日期间隔,合并数据 |
| 250 | - List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval()); | |
| 252 | + List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], | |
| 253 | + reqVO.getInterval()); | |
| 251 | 254 | return convertList(timeRanges, times -> { |
| 252 | 255 | Integer upstreamCount = countList.stream() |
| 253 | 256 | .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], (Timestamp) vo.get("time"))) | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/pom.xml
| ... | ... | @@ -47,6 +47,13 @@ |
| 47 | 47 | <groupId>io.vertx</groupId> |
| 48 | 48 | <artifactId>vertx-mqtt</artifactId> |
| 49 | 49 | </dependency> |
| 50 | + | |
| 51 | + <!-- 测试相关 --> | |
| 52 | + <dependency> | |
| 53 | + <groupId>cn.iocoder.boot</groupId> | |
| 54 | + <artifactId>yudao-spring-boot-starter-test</artifactId> | |
| 55 | + <scope>test</scope> | |
| 56 | + </dependency> | |
| 50 | 57 | </dependencies> |
| 51 | 58 | |
| 52 | 59 | <build> | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.codec.tcp; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 3 | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | -import cn.hutool.json.JSONObject; | |
| 5 | -import cn.hutool.json.JSONUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 8 | 8 | import io.vertx.core.buffer.Buffer; |
| 9 | +import lombok.AllArgsConstructor; | |
| 9 | 10 | import lombok.Data; |
| 10 | -import lombok.extern.slf4j.Slf4j; | |
| 11 | +import lombok.NoArgsConstructor; | |
| 11 | 12 | import org.springframework.stereotype.Component; |
| 12 | 13 | |
| 13 | -// TODO @haohao:设备地址(变长) 是不是非必要哈?因为认证后,不需要每次都带呀。 | |
| 14 | 14 | /** |
| 15 | 15 | * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 |
| 16 | 16 | * |
| 17 | 17 | * 使用自定义二进制协议格式: |
| 18 | - * 包头(4 字节) | 地址长度(2 字节) | 设备地址(变长) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) | |
| 18 | + * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) | |
| 19 | 19 | * |
| 20 | 20 | * @author 芋道源码 |
| 21 | 21 | */ |
| 22 | 22 | @Component |
| 23 | -@Slf4j | |
| 24 | 23 | public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { |
| 25 | 24 | |
| 26 | - /** | |
| 27 | - * 编解码器类型 | |
| 28 | - */ | |
| 29 | 25 | public static final String TYPE = "TCP_BINARY"; |
| 30 | 26 | |
| 31 | - // TODO @haohao:这个注释不太对。 | |
| 32 | - // ==================== 常量定义 ==================== | |
| 27 | + @Data | |
| 28 | + @NoArgsConstructor | |
| 29 | + @AllArgsConstructor | |
| 30 | + private static class TcpBinaryMessage { | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 功能码 | |
| 34 | + */ | |
| 35 | + private Short code; | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * 消息序号 | |
| 39 | + */ | |
| 40 | + private Short mid; | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 设备 ID | |
| 44 | + */ | |
| 45 | + private Long deviceId; | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * 请求方法 | |
| 49 | + */ | |
| 50 | + private String method; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * 请求参数 | |
| 54 | + */ | |
| 55 | + private Object params; | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * 响应结果 | |
| 59 | + */ | |
| 60 | + private Object data; | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * 响应错误码 | |
| 64 | + */ | |
| 65 | + private Integer responseCode; | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 响应提示 | |
| 69 | + */ | |
| 70 | + private String msg; | |
| 71 | + | |
| 72 | + } | |
| 73 | + | |
| 74 | + @Override | |
| 75 | + public String type() { | |
| 76 | + return TYPE; | |
| 77 | + } | |
| 33 | 78 | |
| 34 | 79 | @Override |
| 35 | 80 | public byte[] encode(IotDeviceMessage message) { |
| 36 | - if (message == null || StrUtil.isEmpty(message.getMethod())) { | |
| 37 | - throw new IllegalArgumentException("消息或方法不能为空"); | |
| 38 | - } | |
| 81 | + Assert.notNull(message, "消息不能为空"); | |
| 82 | + Assert.notBlank(message.getMethod(), "消息方法不能为空"); | |
| 39 | 83 | |
| 40 | 84 | try { |
| 41 | - // 1. 确定功能码(只支持数据上报和心跳) | |
| 42 | - short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? | |
| 43 | - TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; | |
| 85 | + // 1. 确定功能码 | |
| 86 | + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? TcpDataPackage.CODE_HEARTBEAT | |
| 87 | + : TcpDataPackage.CODE_MESSAGE_UP; | |
| 44 | 88 | |
| 45 | - // 2. 构建简化负载 | |
| 46 | - String payload = buildSimplePayload(message); | |
| 89 | + // 2. 构建负载数据 | |
| 90 | + String payload = buildPayload(message); | |
| 47 | 91 | |
| 48 | 92 | // 3. 构建 TCP 数据包 |
| 49 | - String deviceAddr = message.getDeviceId() != null ? String.valueOf(message.getDeviceId()) : "default"; | |
| 50 | 93 | short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); |
| 51 | - TcpDataPackage dataPackage = new TcpDataPackage(deviceAddr, code, mid, payload); | |
| 94 | + TcpDataPackage dataPackage = new TcpDataPackage(code, mid, payload); | |
| 52 | 95 | |
| 53 | 96 | // 4. 编码为字节流 |
| 54 | 97 | return encodeTcpDataPackage(dataPackage).getBytes(); |
| 55 | 98 | } catch (Exception e) { |
| 56 | - log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); | |
| 57 | 99 | throw new TcpCodecException("TCP 消息编码失败", e); |
| 58 | 100 | } |
| 59 | 101 | } |
| 60 | 102 | |
| 61 | 103 | @Override |
| 104 | + @SuppressWarnings("DataFlowIssue") | |
| 62 | 105 | public IotDeviceMessage decode(byte[] bytes) { |
| 63 | - if (bytes == null || bytes.length == 0) { | |
| 64 | - throw new IllegalArgumentException("待解码数据不能为空"); | |
| 65 | - } | |
| 106 | + Assert.notNull(bytes, "待解码数据不能为空"); | |
| 107 | + Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); | |
| 66 | 108 | |
| 67 | 109 | try { |
| 68 | 110 | // 1. 解码 TCP 数据包 |
| 69 | 111 | TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); |
| 70 | 112 | |
| 71 | 113 | // 2. 根据功能码确定方法 |
| 72 | - // TODO @haohao:会不会有事件上报哈。 | |
| 73 | - String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? | |
| 74 | - MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; | |
| 114 | + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? MessageMethod.STATE_ONLINE | |
| 115 | + : MessageMethod.PROPERTY_POST; | |
| 75 | 116 | |
| 76 | - // 3. 解析负载数据和请求 ID | |
| 117 | + // 3. 解析负载数据 | |
| 77 | 118 | PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); |
| 78 | 119 | |
| 79 | - // 4. 构建 IoT 设备消息(设置完整的必要参数) | |
| 80 | - IotDeviceMessage message = IotDeviceMessage.requestOf( | |
| 81 | - payloadInfo.getRequestId(), method, payloadInfo.getParams()); | |
| 82 | - | |
| 83 | - // 5. 设置设备相关信息 | |
| 84 | - // TODO @haohao:serverId 不是这里解析的哈。 | |
| 85 | - Long deviceId = parseDeviceId(dataPackage.getAddr()); | |
| 86 | - message.setDeviceId(deviceId); | |
| 87 | - | |
| 88 | - // 6. 设置 TCP 协议相关信息 | |
| 89 | - // TODO @haohao:serverId 不是这里解析的哈。 | |
| 90 | - message.setServerId(generateServerId(dataPackage)); | |
| 91 | - | |
| 92 | - // 7. 设置租户 ID(TODO: 后续可以从设备信息中获取) | |
| 93 | - // TODO @haohao:租户 id 不是这里解析的哈。 | |
| 94 | - // message.setTenantId(getTenantIdByDeviceId(deviceId)); | |
| 95 | - | |
| 96 | - if (log.isDebugEnabled()) { | |
| 97 | - log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}", | |
| 98 | - deviceId, method, message.getRequestId(), message.getId()); | |
| 99 | - } | |
| 100 | - | |
| 101 | - return message; | |
| 120 | + // 4. 构建 IoT 设备消息 | |
| 121 | + return IotDeviceMessage.of( | |
| 122 | + payloadInfo.getRequestId(), | |
| 123 | + method, | |
| 124 | + payloadInfo.getParams(), | |
| 125 | + null, | |
| 126 | + null, | |
| 127 | + null); | |
| 102 | 128 | } catch (Exception e) { |
| 103 | - log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); | |
| 104 | 129 | throw new TcpCodecException("TCP 消息解码失败", e); |
| 105 | 130 | } |
| 106 | 131 | } |
| 107 | 132 | |
| 108 | - @Override | |
| 109 | - public String type() { | |
| 110 | - return TYPE; | |
| 111 | - } | |
| 133 | + // ==================== 内部辅助方法 ==================== | |
| 112 | 134 | |
| 113 | - // TODO @haohao:这种简单解析,中间不用空格哈。 | |
| 114 | 135 | /** |
| 115 | - * 构建完整负载 | |
| 136 | + * 构建负载数据 | |
| 137 | + * | |
| 138 | + * @param message 设备消息 | |
| 139 | + * @return 负载字符串 | |
| 116 | 140 | */ |
| 117 | - private String buildSimplePayload(IotDeviceMessage message) { | |
| 118 | - JSONObject payload = new JSONObject(); | |
| 119 | - | |
| 120 | - // 核心字段 | |
| 121 | - payload.set(PayloadField.METHOD, message.getMethod()); | |
| 122 | - if (message.getParams() != null) { | |
| 123 | - payload.set(PayloadField.PARAMS, message.getParams()); | |
| 124 | - } | |
| 125 | - | |
| 126 | - // 标识字段 | |
| 127 | - if (StrUtil.isNotEmpty(message.getRequestId())) { | |
| 128 | - payload.set(PayloadField.REQUEST_ID, message.getRequestId()); | |
| 129 | - } | |
| 130 | - if (StrUtil.isNotEmpty(message.getId())) { | |
| 131 | - payload.set(PayloadField.MESSAGE_ID, message.getId()); | |
| 132 | - } | |
| 133 | - | |
| 134 | - // 时间戳 | |
| 135 | - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); | |
| 136 | - | |
| 137 | - return payload.toString(); | |
| 141 | + private String buildPayload(IotDeviceMessage message) { | |
| 142 | + TcpBinaryMessage tcpBinaryMessage = new TcpBinaryMessage( | |
| 143 | + null, // code 在数据包中单独处理 | |
| 144 | + null, // mid 在数据包中单独处理 | |
| 145 | + message.getDeviceId(), | |
| 146 | + message.getMethod(), | |
| 147 | + message.getParams(), | |
| 148 | + message.getData(), | |
| 149 | + message.getCode(), | |
| 150 | + message.getMsg()); | |
| 151 | + return JsonUtils.toJsonString(tcpBinaryMessage); | |
| 138 | 152 | } |
| 139 | 153 | |
| 140 | - // ==================== 编解码方法 ==================== | |
| 141 | - | |
| 142 | 154 | /** |
| 143 | - * 解析负载信息(包含 requestId 和 params) | |
| 155 | + * 解析负载信息 | |
| 156 | + * | |
| 157 | + * @param payload 负载字符串 | |
| 158 | + * @return 负载信息 | |
| 144 | 159 | */ |
| 145 | 160 | private PayloadInfo parsePayloadInfo(String payload) { |
| 146 | - if (StrUtil.isEmpty(payload)) { | |
| 161 | + if (StrUtil.isBlank(payload)) { | |
| 147 | 162 | return new PayloadInfo(null, null); |
| 148 | 163 | } |
| 149 | 164 | |
| 150 | 165 | try { |
| 151 | - // TODO @haohao:使用 jsonUtils | |
| 152 | - JSONObject jsonObject = JSONUtil.parseObj(payload); | |
| 153 | - String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); | |
| 154 | - if (StrUtil.isEmpty(requestId)) { | |
| 155 | - requestId = jsonObject.getStr(PayloadField.MESSAGE_ID); | |
| 166 | + TcpBinaryMessage tcpBinaryMessage = JsonUtils.parseObject(payload, TcpBinaryMessage.class); | |
| 167 | + if (tcpBinaryMessage != null) { | |
| 168 | + return new PayloadInfo( | |
| 169 | + StrUtil.isNotEmpty(tcpBinaryMessage.getMethod()) | |
| 170 | + ? tcpBinaryMessage.getMethod() + "_" + System.currentTimeMillis() | |
| 171 | + : null, | |
| 172 | + tcpBinaryMessage.getParams()); | |
| 156 | 173 | } |
| 157 | - Object params = jsonObject.get(PayloadField.PARAMS); | |
| 158 | - return new PayloadInfo(requestId, params); | |
| 159 | 174 | } catch (Exception e) { |
| 160 | - log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload); | |
| 161 | - return new PayloadInfo(null, payload); | |
| 175 | + // 如果解析失败,返回默认值 | |
| 176 | + return new PayloadInfo("unknown_" + System.currentTimeMillis(), null); | |
| 162 | 177 | } |
| 178 | + return null; | |
| 163 | 179 | } |
| 164 | 180 | |
| 165 | 181 | /** |
| 166 | - * 从设备地址解析设备ID | |
| 167 | - * | |
| 168 | - * @param deviceAddr 设备地址字符串 | |
| 169 | - * @return 设备ID | |
| 170 | - */ | |
| 171 | - private Long parseDeviceId(String deviceAddr) { | |
| 172 | - if (StrUtil.isEmpty(deviceAddr)) { | |
| 173 | - log.warn("[parseDeviceId][设备地址为空,返回默认ID]"); | |
| 174 | - return 0L; | |
| 175 | - } | |
| 176 | - | |
| 177 | - try { | |
| 178 | - // 尝试直接解析为Long | |
| 179 | - return Long.parseLong(deviceAddr); | |
| 180 | - } catch (NumberFormatException e) { | |
| 181 | - // 如果不是纯数字,可以使用哈希值或其他策略 | |
| 182 | - log.warn("[parseDeviceId][设备地址不是数字格式: {},使用哈希值]", deviceAddr); | |
| 183 | - return (long) deviceAddr.hashCode(); | |
| 184 | - } | |
| 185 | - } | |
| 186 | - | |
| 187 | - /** | |
| 188 | - * 生成服务ID | |
| 189 | - * | |
| 190 | - * @param dataPackage TCP数据包 | |
| 191 | - * @return 服务ID | |
| 192 | - */ | |
| 193 | - private String generateServerId(TcpDataPackage dataPackage) { | |
| 194 | - // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务 ID | |
| 195 | - return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid()); | |
| 196 | - } | |
| 197 | - | |
| 198 | - // ==================== 内部辅助方法 ==================== | |
| 199 | - | |
| 200 | - /** | |
| 201 | 182 | * 编码 TCP 数据包 |
| 202 | 183 | * |
| 203 | 184 | * @param dataPackage 数据包对象 |
| 204 | 185 | * @return 编码后的字节流 |
| 205 | - * @throws IllegalArgumentException 如果数据包对象不正确 | |
| 206 | 186 | */ |
| 207 | 187 | private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { |
| 208 | - if (dataPackage == null) { | |
| 209 | - throw new IllegalArgumentException("数据包对象不能为空"); | |
| 210 | - } | |
| 188 | + Assert.notNull(dataPackage, "数据包对象不能为空"); | |
| 189 | + Assert.notNull(dataPackage.getPayload(), "负载不能为空"); | |
| 211 | 190 | |
| 212 | - // 验证数据包 | |
| 213 | - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { | |
| 214 | - throw new IllegalArgumentException("设备地址不能为空"); | |
| 215 | - } | |
| 216 | - if (dataPackage.getPayload() == null) { | |
| 217 | - throw new IllegalArgumentException("负载不能为空"); | |
| 218 | - } | |
| 191 | + Buffer buffer = Buffer.buffer(); | |
| 219 | 192 | |
| 220 | - try { | |
| 221 | - Buffer buffer = Buffer.buffer(); | |
| 222 | - | |
| 223 | - // 1. 计算包体长度(除了包头 4 字节) | |
| 224 | - int payloadLength = dataPackage.getPayload().getBytes().length; | |
| 225 | - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; | |
| 226 | - | |
| 227 | - // 2.1 写入包头:总长度(4 字节) | |
| 228 | - buffer.appendInt(totalLength); | |
| 229 | - // 2.2 写入设备地址长度(2 字节) | |
| 230 | - buffer.appendShort((short) dataPackage.getAddr().length()); | |
| 231 | - // 2.3 写入设备地址(不定长) | |
| 232 | - buffer.appendBytes(dataPackage.getAddr().getBytes()); | |
| 233 | - // 2.4 写入功能码(2 字节) | |
| 234 | - buffer.appendShort(dataPackage.getCode()); | |
| 235 | - // 2.5 写入消息序号(2 字节) | |
| 236 | - buffer.appendShort(dataPackage.getMid()); | |
| 237 | - // 2.6 写入包体数据(不定长) | |
| 238 | - buffer.appendBytes(dataPackage.getPayload().getBytes()); | |
| 239 | - | |
| 240 | - if (log.isDebugEnabled()) { | |
| 241 | - log.debug("[encodeTcpDataPackage][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", | |
| 242 | - dataPackage.getAddr(), dataPackage.getCode(), dataPackage.getMid(), buffer.length()); | |
| 243 | - } | |
| 244 | - return buffer; | |
| 245 | - } catch (Exception e) { | |
| 246 | - log.error("[encodeTcpDataPackage][编码失败] 数据包: {}", dataPackage, e); | |
| 247 | - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); | |
| 248 | - } | |
| 193 | + // 1. 计算包体长度(除了包头 4 字节) | |
| 194 | + int payloadLength = dataPackage.getPayload().getBytes().length; | |
| 195 | + int totalLength = 2 + 2 + payloadLength; | |
| 196 | + | |
| 197 | + // 2. 写入包头:总长度(4 字节) | |
| 198 | + buffer.appendInt(totalLength); | |
| 199 | + // 3. 写入功能码(2 字节) | |
| 200 | + buffer.appendShort(dataPackage.getCode()); | |
| 201 | + // 4. 写入消息序号(2 字节) | |
| 202 | + buffer.appendShort(dataPackage.getMid()); | |
| 203 | + // 5. 写入包体数据(不定长) | |
| 204 | + buffer.appendBytes(dataPackage.getPayload().getBytes()); | |
| 205 | + | |
| 206 | + return buffer; | |
| 249 | 207 | } |
| 250 | 208 | |
| 251 | 209 | /** |
| ... | ... | @@ -253,101 +211,49 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { |
| 253 | 211 | * |
| 254 | 212 | * @param buffer 数据缓冲区 |
| 255 | 213 | * @return 解码后的数据包 |
| 256 | - * @throws IllegalArgumentException 如果数据包格式不正确 | |
| 257 | 214 | */ |
| 258 | 215 | private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { |
| 259 | - if (buffer == null || buffer.length() < 8) { | |
| 260 | - throw new IllegalArgumentException("数据包长度不足"); | |
| 261 | - } | |
| 216 | + Assert.isTrue(buffer.length() >= 8, "数据包长度不足"); | |
| 262 | 217 | |
| 263 | - try { | |
| 264 | - int index = 0; | |
| 265 | - | |
| 266 | - // 1.1 跳过包头(4字节) | |
| 267 | - index += 4; | |
| 268 | - | |
| 269 | - // 1.2 获取设备地址长度(2字节) | |
| 270 | - short addrLength = buffer.getShort(index); | |
| 271 | - index += 2; | |
| 218 | + int index = 0; | |
| 272 | 219 | |
| 273 | - // 1.3 获取设备地址 | |
| 274 | - String addr = buffer.getBuffer(index, index + addrLength).toString(); | |
| 275 | - index += addrLength; | |
| 220 | + // 1. 跳过包头(4 字节) | |
| 221 | + index += 4; | |
| 276 | 222 | |
| 277 | - // 1.4 获取功能码(2字节) | |
| 278 | - short code = buffer.getShort(index); | |
| 279 | - index += 2; | |
| 280 | - | |
| 281 | - // 1.5 获取消息序号(2字节) | |
| 282 | - short mid = buffer.getShort(index); | |
| 283 | - index += 2; | |
| 284 | - | |
| 285 | - // 1.6 获取包体数据 | |
| 286 | - String payload = ""; | |
| 287 | - if (index < buffer.length()) { | |
| 288 | - payload = buffer.getString(index, buffer.length()); | |
| 289 | - } | |
| 223 | + // 2. 获取功能码(2 字节) | |
| 224 | + short code = buffer.getShort(index); | |
| 225 | + index += 2; | |
| 290 | 226 | |
| 291 | - // 2. 构建数据包对象 | |
| 292 | - TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload); | |
| 227 | + // 3. 获取消息序号(2 字节) | |
| 228 | + short mid = buffer.getShort(index); | |
| 229 | + index += 2; | |
| 293 | 230 | |
| 294 | - if (log.isDebugEnabled()) { | |
| 295 | - log.debug("[decodeTcpDataPackage][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", | |
| 296 | - addr, code, mid, payload.length()); | |
| 297 | - } | |
| 298 | - return dataPackage; | |
| 299 | - } catch (Exception e) { | |
| 300 | - log.error("[decodeTcpDataPackage][解码失败] 数据长度: {}", buffer.length(), e); | |
| 301 | - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); | |
| 231 | + // 4. 获取包体数据 | |
| 232 | + String payload = ""; | |
| 233 | + if (index < buffer.length()) { | |
| 234 | + payload = buffer.getString(index, buffer.length()); | |
| 302 | 235 | } |
| 303 | - } | |
| 304 | - | |
| 305 | - /** | |
| 306 | - * 消息方法常量 | |
| 307 | - */ | |
| 308 | - public static class MessageMethod { | |
| 309 | - | |
| 310 | - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 | |
| 311 | - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 312 | 236 | |
| 237 | + return new TcpDataPackage(code, mid, payload); | |
| 313 | 238 | } |
| 314 | 239 | |
| 315 | - /** | |
| 316 | - * 负载字段名 | |
| 317 | - */ | |
| 318 | - private static class PayloadField { | |
| 319 | - | |
| 320 | - public static final String METHOD = "method"; | |
| 321 | - public static final String PARAMS = "params"; | |
| 322 | - public static final String TIMESTAMP = "timestamp"; | |
| 323 | - public static final String REQUEST_ID = "requestId"; | |
| 324 | - public static final String MESSAGE_ID = "msgId"; | |
| 240 | + // ==================== 内部类 ==================== | |
| 325 | 241 | |
| 326 | - } | |
| 327 | - | |
| 328 | - // ==================== TCP 数据包编解码方法 ==================== | |
| 329 | - | |
| 330 | - // TODO @haohao:lombok 简化 | |
| 331 | 242 | /** |
| 332 | 243 | * 负载信息类 |
| 333 | 244 | */ |
| 245 | + @Data | |
| 246 | + @AllArgsConstructor | |
| 334 | 247 | private static class PayloadInfo { |
| 335 | 248 | private String requestId; |
| 336 | 249 | private Object params; |
| 337 | - | |
| 338 | - public PayloadInfo(String requestId, Object params) { | |
| 339 | - this.requestId = requestId; | |
| 340 | - this.params = params; | |
| 341 | - } | |
| 342 | - | |
| 343 | - public String getRequestId() { return requestId; } | |
| 344 | - public Object getParams() { return params; } | |
| 345 | 250 | } |
| 346 | 251 | |
| 347 | 252 | /** |
| 348 | 253 | * TCP 数据包内部类 |
| 349 | 254 | */ |
| 350 | 255 | @Data |
| 256 | + @AllArgsConstructor | |
| 351 | 257 | private static class TcpDataPackage { |
| 352 | 258 | // 功能码定义 |
| 353 | 259 | public static final short CODE_REGISTER = 10; |
| ... | ... | @@ -357,35 +263,29 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { |
| 357 | 263 | public static final short CODE_MESSAGE_UP = 30; |
| 358 | 264 | public static final short CODE_MESSAGE_DOWN = 40; |
| 359 | 265 | |
| 360 | - private String addr; | |
| 361 | 266 | private short code; |
| 362 | 267 | private short mid; |
| 363 | 268 | private String payload; |
| 269 | + } | |
| 364 | 270 | |
| 365 | - public TcpDataPackage(String addr, short code, short mid, String payload) { | |
| 366 | - this.addr = addr; | |
| 367 | - this.code = code; | |
| 368 | - this.mid = mid; | |
| 369 | - this.payload = payload; | |
| 370 | - } | |
| 271 | + // ==================== 常量定义 ==================== | |
| 272 | + | |
| 273 | + /** | |
| 274 | + * 消息方法常量 | |
| 275 | + */ | |
| 276 | + public static class MessageMethod { | |
| 277 | + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 | |
| 278 | + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 371 | 279 | } |
| 372 | 280 | |
| 373 | 281 | // ==================== 自定义异常 ==================== |
| 374 | 282 | |
| 375 | - // TODO @haohao:可以搞个全局的; | |
| 376 | 283 | /** |
| 377 | 284 | * TCP 编解码异常 |
| 378 | 285 | */ |
| 379 | 286 | public static class TcpCodecException extends RuntimeException { |
| 380 | - | |
| 381 | - // TODO @haohao:非必要构造方法,可以去掉哈。 | |
| 382 | - public TcpCodecException(String message) { | |
| 383 | - super(message); | |
| 384 | - } | |
| 385 | - | |
| 386 | 287 | public TcpCodecException(String message, Throwable cause) { |
| 387 | 288 | super(message, cause); |
| 388 | 289 | } |
| 389 | - | |
| 390 | 290 | } |
| 391 | 291 | } |
| 392 | 292 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | |
| 5 | -import lombok.extern.slf4j.Slf4j; | |
| 6 | -import org.springframework.beans.factory.annotation.Autowired; | |
| 7 | -import org.springframework.stereotype.Component; | |
| 8 | - | |
| 9 | -/** | |
| 10 | - * TCP编解码器管理器(简化版) | |
| 11 | - * | |
| 12 | - * 核心功能: | |
| 13 | - * - 自动协议检测(二进制 vs JSON) | |
| 14 | - * - 统一编解码接口 | |
| 15 | - * - 默认使用 JSON 协议 | |
| 16 | - * | |
| 17 | - * @author 芋道源码 | |
| 18 | - */ | |
| 19 | -@Slf4j | |
| 20 | -@Component | |
| 21 | -public class IotTcpCodecManager implements IotDeviceMessageCodec { | |
| 22 | - | |
| 23 | - public static final String TYPE = "TCP"; | |
| 24 | - | |
| 25 | - // TODO @haohao:@Resource | |
| 26 | - | |
| 27 | - @Autowired | |
| 28 | - private IotTcpBinaryDeviceMessageCodec binaryCodec; | |
| 29 | - | |
| 30 | - @Autowired | |
| 31 | - private IotTcpJsonDeviceMessageCodec jsonCodec; | |
| 32 | - | |
| 33 | - /** | |
| 34 | - * 当前默认协议(JSON) | |
| 35 | - */ | |
| 36 | - private boolean useJsonByDefault = true; | |
| 37 | - | |
| 38 | - @Override | |
| 39 | - public String type() { | |
| 40 | - return TYPE; | |
| 41 | - } | |
| 42 | - | |
| 43 | - @Override | |
| 44 | - public byte[] encode(IotDeviceMessage message) { | |
| 45 | - // 默认使用 JSON 协议编码 | |
| 46 | - return jsonCodec.encode(message); | |
| 47 | - } | |
| 48 | - | |
| 49 | - // TODO @haohao:要不还是不自动检测,用户手动配置哈。简化一些。。。 | |
| 50 | - @Override | |
| 51 | - public IotDeviceMessage decode(byte[] bytes) { | |
| 52 | - // 自动检测协议类型并解码 | |
| 53 | - if (isJsonFormat(bytes)) { | |
| 54 | - if (log.isDebugEnabled()) { | |
| 55 | - log.debug("[decode][检测到 JSON 协议,数据长度: {} 字节]", bytes.length); | |
| 56 | - } | |
| 57 | - return jsonCodec.decode(bytes); | |
| 58 | - } else { | |
| 59 | - if (log.isDebugEnabled()) { | |
| 60 | - log.debug("[decode][检测到二进制协议,数据长度: {} 字节]", bytes.length); | |
| 61 | - } | |
| 62 | - return binaryCodec.decode(bytes); | |
| 63 | - } | |
| 64 | - } | |
| 65 | - | |
| 66 | - // ==================== 便捷方法 ==================== | |
| 67 | - | |
| 68 | - /** | |
| 69 | - * 使用 JSON 协议编码 | |
| 70 | - */ | |
| 71 | - public byte[] encodeJson(IotDeviceMessage message) { | |
| 72 | - return jsonCodec.encode(message); | |
| 73 | - } | |
| 74 | - | |
| 75 | - /** | |
| 76 | - * 使用二进制协议编码 | |
| 77 | - */ | |
| 78 | - public byte[] encodeBinary(IotDeviceMessage message) { | |
| 79 | - return binaryCodec.encode(message); | |
| 80 | - } | |
| 81 | - | |
| 82 | - /** | |
| 83 | - * 获取当前默认协议 | |
| 84 | - */ | |
| 85 | - public String getDefaultProtocol() { | |
| 86 | - return useJsonByDefault ? "JSON" : "BINARY"; | |
| 87 | - } | |
| 88 | - | |
| 89 | - /** | |
| 90 | - * 设置默认协议 | |
| 91 | - */ | |
| 92 | - public void setDefaultProtocol(boolean useJson) { | |
| 93 | - this.useJsonByDefault = useJson; | |
| 94 | - log.info("[setDefaultProtocol][设置默认协议] 使用JSON: {}", useJson); | |
| 95 | - } | |
| 96 | - | |
| 97 | - // ==================== 内部方法 ==================== | |
| 98 | - | |
| 99 | - /** | |
| 100 | - * 检测是否为JSON格式 | |
| 101 | - * | |
| 102 | - * 检测规则: | |
| 103 | - * 1. 数据以 '{' 开头 | |
| 104 | - * 2. 包含 "method" 或 "id" 字段 | |
| 105 | - */ | |
| 106 | - private boolean isJsonFormat(byte[] bytes) { | |
| 107 | - // TODO @haohao:ArrayUtil.isEmpty(bytes) 可以简化下 | |
| 108 | - if (bytes == null || bytes.length == 0) { | |
| 109 | - return useJsonByDefault; | |
| 110 | - } | |
| 111 | - | |
| 112 | - try { | |
| 113 | - // 检测 JSON 格式:以 '{' 开头 | |
| 114 | - if (bytes[0] == '{') { | |
| 115 | - // TODO @haohao:不一定按照顺序写,这个可能要看下。 | |
| 116 | - // 进一步验证是否为有效 JSON | |
| 117 | - String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100)); | |
| 118 | - return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\""); | |
| 119 | - } | |
| 120 | - | |
| 121 | - // 检测二进制格式:长度 >= 8 且符合二进制协议结构 | |
| 122 | - if (bytes.length >= 8) { | |
| 123 | - // 读取包头(前 4 字节表示后续数据长度) | |
| 124 | - int expectedLength = ((bytes[0] & 0xFF) << 24) | | |
| 125 | - ((bytes[1] & 0xFF) << 16) | | |
| 126 | - ((bytes[2] & 0xFF) << 8) | | |
| 127 | - (bytes[3] & 0xFF); | |
| 128 | - | |
| 129 | - // 验证长度是否合理 | |
| 130 | - // TODO @haohao:expectedLength > 0 多余的貌似; | |
| 131 | - if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) { | |
| 132 | - return false; // 二进制格式 | |
| 133 | - } | |
| 134 | - } | |
| 135 | - } catch (Exception e) { | |
| 136 | - log.warn("[isJsonFormat][协议检测异常,使用默认协议: {}]", getDefaultProtocol(), e); | |
| 137 | - } | |
| 138 | - | |
| 139 | - // 默认使用当前设置的协议类型 | |
| 140 | - return useJsonByDefault; | |
| 141 | - } | |
| 142 | - | |
| 143 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.codec.tcp; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.util.IdUtil; | |
| 4 | -import cn.hutool.core.util.StrUtil; | |
| 5 | -import cn.hutool.json.JSONObject; | |
| 6 | -import cn.hutool.json.JSONUtil; | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 7 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | 6 | import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 9 | -import lombok.extern.slf4j.Slf4j; | |
| 7 | +import lombok.AllArgsConstructor; | |
| 8 | +import lombok.Data; | |
| 9 | +import lombok.NoArgsConstructor; | |
| 10 | 10 | import org.springframework.stereotype.Component; |
| 11 | 11 | |
| 12 | -import java.nio.charset.StandardCharsets; | |
| 13 | - | |
| 14 | 12 | /** |
| 15 | 13 | * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 |
| 16 | 14 | * |
| 17 | - * 采用纯 JSON 格式传输,参考 EMQX 和 HTTP 模块的数据格式 | |
| 15 | + * 采用纯 JSON 格式传输 | |
| 18 | 16 | * |
| 19 | - * JSON消息格式: | |
| 17 | + * JSON 消息格式: | |
| 20 | 18 | * { |
| 21 | - * "id": "消息 ID", | |
| 22 | - * "method": "消息方法", | |
| 23 | - * "deviceId": "设备 ID", | |
| 24 | - * "productKey": "产品 Key", | |
| 25 | - * "deviceName": "设备名称", | |
| 26 | - * "params": {...}, | |
| 27 | - * "timestamp": 时间戳 | |
| 19 | + * "id": "消息 ID", | |
| 20 | + * "method": "消息方法", | |
| 21 | + * "deviceId": "设备 ID", | |
| 22 | + * "params": {...}, | |
| 23 | + * "timestamp": 时间戳 | |
| 28 | 24 | * } |
| 29 | 25 | * |
| 30 | 26 | * @author 芋道源码 |
| 31 | 27 | */ |
| 32 | -@Slf4j | |
| 33 | 28 | @Component |
| 34 | 29 | public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { |
| 35 | 30 | |
| 36 | 31 | public static final String TYPE = "TCP_JSON"; |
| 37 | 32 | |
| 38 | - // TODO @haohao:变量不太对; | |
| 39 | - // ==================== 常量定义 ==================== | |
| 33 | + @Data | |
| 34 | + @NoArgsConstructor | |
| 35 | + @AllArgsConstructor | |
| 36 | + private static class TcpJsonMessage { | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * 消息 ID,且每个消息 ID 在当前设备具有唯一性 | |
| 40 | + */ | |
| 41 | + private String id; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 请求方法 | |
| 45 | + */ | |
| 46 | + private String method; | |
| 47 | + | |
| 48 | + /** | |
| 49 | + * 设备 ID | |
| 50 | + */ | |
| 51 | + private Long deviceId; | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 请求参数 | |
| 55 | + */ | |
| 56 | + private Object params; | |
| 57 | + | |
| 58 | + /** | |
| 59 | + * 响应结果 | |
| 60 | + */ | |
| 61 | + private Object data; | |
| 62 | + | |
| 63 | + /** | |
| 64 | + * 响应错误码 | |
| 65 | + */ | |
| 66 | + private Integer code; | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * 响应提示 | |
| 70 | + */ | |
| 71 | + private String msg; | |
| 72 | + | |
| 73 | + /** | |
| 74 | + * 时间戳 | |
| 75 | + */ | |
| 76 | + private Long timestamp; | |
| 77 | + | |
| 78 | + } | |
| 40 | 79 | |
| 41 | 80 | @Override |
| 42 | 81 | public String type() { |
| ... | ... | @@ -45,208 +84,33 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { |
| 45 | 84 | |
| 46 | 85 | @Override |
| 47 | 86 | public byte[] encode(IotDeviceMessage message) { |
| 48 | - if (message == null || StrUtil.isEmpty(message.getMethod())) { | |
| 49 | - throw new IllegalArgumentException("消息或方法不能为空"); | |
| 50 | - } | |
| 51 | - | |
| 52 | - try { | |
| 53 | - // 构建JSON消息 | |
| 54 | - JSONObject jsonMessage = buildJsonMessage(message); | |
| 55 | - | |
| 56 | - // 转换为字节数组 | |
| 57 | - String jsonString = jsonMessage.toString(); | |
| 58 | - byte[] result = jsonString.getBytes(StandardCharsets.UTF_8); | |
| 59 | - | |
| 60 | - if (log.isDebugEnabled()) { | |
| 61 | - log.debug("[encode][编码成功] 方法: {}, JSON长度: {}字节, 内容: {}", | |
| 62 | - message.getMethod(), result.length, jsonString); | |
| 63 | - } | |
| 64 | - | |
| 65 | - return result; | |
| 66 | - } catch (Exception e) { | |
| 67 | - log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); | |
| 68 | - throw new RuntimeException("JSON消息编码失败", e); | |
| 69 | - } | |
| 87 | + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( | |
| 88 | + message.getRequestId(), | |
| 89 | + message.getMethod(), | |
| 90 | + message.getDeviceId(), | |
| 91 | + message.getParams(), | |
| 92 | + message.getData(), | |
| 93 | + message.getCode(), | |
| 94 | + message.getMsg(), | |
| 95 | + System.currentTimeMillis()); | |
| 96 | + return JsonUtils.toJsonByte(tcpJsonMessage); | |
| 70 | 97 | } |
| 71 | 98 | |
| 72 | - // ==================== 编解码方法 ==================== | |
| 73 | - | |
| 74 | 99 | @Override |
| 100 | + @SuppressWarnings("DataFlowIssue") | |
| 75 | 101 | public IotDeviceMessage decode(byte[] bytes) { |
| 76 | - if (bytes == null || bytes.length == 0) { | |
| 77 | - throw new IllegalArgumentException("待解码数据不能为空"); | |
| 78 | - } | |
| 79 | - | |
| 80 | - try { | |
| 81 | - // 转换为 JSON 字符串 | |
| 82 | - String jsonString = new String(bytes, StandardCharsets.UTF_8); | |
| 83 | - | |
| 84 | - if (log.isDebugEnabled()) { | |
| 85 | - log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString); | |
| 86 | - } | |
| 87 | - | |
| 88 | - // 解析 JSON 消息 | |
| 89 | - // TODO @haohao:JsonUtils | |
| 90 | - JSONObject jsonMessage = JSONUtil.parseObj(jsonString); | |
| 91 | - | |
| 92 | - // 构建IoT设备消息 | |
| 93 | - IotDeviceMessage message = parseJsonMessage(jsonMessage); | |
| 94 | - | |
| 95 | - if (log.isDebugEnabled()) { | |
| 96 | - log.debug("[decode][解码成功] 消息ID: {}, 方法: {}, 设备ID: {}", | |
| 97 | - message.getId(), message.getMethod(), message.getDeviceId()); | |
| 98 | - } | |
| 99 | - | |
| 100 | - return message; | |
| 101 | - } catch (Exception e) { | |
| 102 | - log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); | |
| 103 | - throw new RuntimeException("JSON消息解码失败", e); | |
| 104 | - } | |
| 105 | - } | |
| 106 | - | |
| 107 | - /** | |
| 108 | - * 编码数据上报消息 | |
| 109 | - */ | |
| 110 | - public byte[] encodeDataReport(Object params, Long deviceId, String productKey, String deviceName) { | |
| 111 | - IotDeviceMessage message = createMessage(MessageMethod.PROPERTY_POST, params, deviceId, productKey, deviceName); | |
| 112 | - return encode(message); | |
| 113 | - } | |
| 114 | - | |
| 115 | - /** | |
| 116 | - * 编码心跳消息 | |
| 117 | - */ | |
| 118 | - public byte[] encodeHeartbeat(Long deviceId, String productKey, String deviceName) { | |
| 119 | - IotDeviceMessage message = createMessage(MessageMethod.STATE_ONLINE, null, deviceId, productKey, deviceName); | |
| 120 | - return encode(message); | |
| 121 | - } | |
| 122 | - | |
| 123 | - // ==================== 便捷方法 ==================== | |
| 124 | - | |
| 125 | - /** | |
| 126 | - * 编码事件上报消息 | |
| 127 | - */ | |
| 128 | - public byte[] encodeEventReport(Object params, Long deviceId, String productKey, String deviceName) { | |
| 129 | - IotDeviceMessage message = createMessage(MessageMethod.EVENT_POST, params, deviceId, productKey, deviceName); | |
| 130 | - return encode(message); | |
| 131 | - } | |
| 132 | - | |
| 133 | - /** | |
| 134 | - * 构建 JSON 消息 | |
| 135 | - */ | |
| 136 | - private JSONObject buildJsonMessage(IotDeviceMessage message) { | |
| 137 | - JSONObject jsonMessage = new JSONObject(); | |
| 138 | - | |
| 139 | - // 基础字段 | |
| 140 | - jsonMessage.set(JsonField.ID, StrUtil.isNotEmpty(message.getId()) ? message.getId() : IdUtil.fastSimpleUUID()); | |
| 141 | - jsonMessage.set(JsonField.METHOD, message.getMethod()); | |
| 142 | - jsonMessage.set(JsonField.TIMESTAMP, System.currentTimeMillis()); | |
| 143 | - | |
| 144 | - // 设备信息 | |
| 145 | - if (message.getDeviceId() != null) { | |
| 146 | - jsonMessage.set(JsonField.DEVICE_ID, message.getDeviceId()); | |
| 147 | - } | |
| 148 | - | |
| 149 | - // 参数 | |
| 150 | - if (message.getParams() != null) { | |
| 151 | - jsonMessage.set(JsonField.PARAMS, message.getParams()); | |
| 152 | - } | |
| 153 | - | |
| 154 | - // 响应码和消息(用于下行消息) | |
| 155 | - if (message.getCode() != null) { | |
| 156 | - jsonMessage.set(JsonField.CODE, message.getCode()); | |
| 157 | - } | |
| 158 | - if (StrUtil.isNotEmpty(message.getMsg())) { | |
| 159 | - jsonMessage.set(JsonField.MESSAGE, message.getMsg()); | |
| 160 | - } | |
| 161 | - | |
| 162 | - return jsonMessage; | |
| 102 | + TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); | |
| 103 | + Assert.notNull(tcpJsonMessage, "消息不能为空"); | |
| 104 | + Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); | |
| 105 | + IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of( | |
| 106 | + tcpJsonMessage.getId(), | |
| 107 | + tcpJsonMessage.getMethod(), | |
| 108 | + tcpJsonMessage.getParams(), | |
| 109 | + tcpJsonMessage.getData(), | |
| 110 | + tcpJsonMessage.getCode(), | |
| 111 | + tcpJsonMessage.getMsg()); | |
| 112 | + iotDeviceMessage.setDeviceId(tcpJsonMessage.getDeviceId()); | |
| 113 | + return iotDeviceMessage; | |
| 163 | 114 | } |
| 164 | 115 | |
| 165 | - /** | |
| 166 | - * 解析JSON消息 | |
| 167 | - */ | |
| 168 | - private IotDeviceMessage parseJsonMessage(JSONObject jsonMessage) { | |
| 169 | - // 提取基础字段 | |
| 170 | - String id = jsonMessage.getStr(JsonField.ID); | |
| 171 | - String method = jsonMessage.getStr(JsonField.METHOD); | |
| 172 | - Object params = jsonMessage.get(JsonField.PARAMS); | |
| 173 | - | |
| 174 | - // 创建消息对象 | |
| 175 | - IotDeviceMessage message = IotDeviceMessage.requestOf(id, method, params); | |
| 176 | - | |
| 177 | - // 设置设备信息 | |
| 178 | - Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); | |
| 179 | - if (deviceId != null) { | |
| 180 | - message.setDeviceId(deviceId); | |
| 181 | - } | |
| 182 | - | |
| 183 | - // 设置响应信息 | |
| 184 | - Integer code = jsonMessage.getInt(JsonField.CODE); | |
| 185 | - if (code != null) { | |
| 186 | - message.setCode(code); | |
| 187 | - } | |
| 188 | - | |
| 189 | - String msg = jsonMessage.getStr(JsonField.MESSAGE); | |
| 190 | - if (StrUtil.isNotEmpty(msg)) { | |
| 191 | - message.setMsg(msg); | |
| 192 | - } | |
| 193 | - | |
| 194 | - // 设置服务 ID(基于 JSON 格式) | |
| 195 | - message.setServerId(generateServerId(jsonMessage)); | |
| 196 | - | |
| 197 | - return message; | |
| 198 | - } | |
| 199 | - | |
| 200 | - // ==================== 内部辅助方法 ==================== | |
| 201 | - | |
| 202 | - /** | |
| 203 | - * 创建消息对象 | |
| 204 | - */ | |
| 205 | - private IotDeviceMessage createMessage(String method, Object params, Long deviceId, String productKey, String deviceName) { | |
| 206 | - IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); | |
| 207 | - message.setDeviceId(deviceId); | |
| 208 | - return message; | |
| 209 | - } | |
| 210 | - | |
| 211 | - /** | |
| 212 | - * 生成服务ID | |
| 213 | - */ | |
| 214 | - private String generateServerId(JSONObject jsonMessage) { | |
| 215 | - String id = jsonMessage.getStr(JsonField.ID); | |
| 216 | - Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); | |
| 217 | - return String.format("tcp_json_%s_%s", deviceId != null ? deviceId : "unknown", | |
| 218 | - StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId"); | |
| 219 | - } | |
| 220 | - | |
| 221 | - // TODO @haohao:注释格式不对; | |
| 222 | - /** | |
| 223 | - * 消息方法常量 | |
| 224 | - */ | |
| 225 | - public static class MessageMethod { | |
| 226 | - | |
| 227 | - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 | |
| 228 | - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 229 | - public static final String EVENT_POST = "thing.event.post"; // 事件上报 | |
| 230 | - public static final String PROPERTY_SET = "thing.property.set"; // 属性设置 | |
| 231 | - public static final String PROPERTY_GET = "thing.property.get"; // 属性获取 | |
| 232 | - public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用 | |
| 233 | - | |
| 234 | - } | |
| 235 | - | |
| 236 | - /** | |
| 237 | - * JSON字段名(参考EMQX和HTTP模块格式) | |
| 238 | - */ | |
| 239 | - private static class JsonField { | |
| 240 | - | |
| 241 | - public static final String ID = "id"; | |
| 242 | - public static final String METHOD = "method"; | |
| 243 | - public static final String DEVICE_ID = "deviceId"; | |
| 244 | - public static final String PRODUCT_KEY = "productKey"; | |
| 245 | - public static final String DEVICE_NAME = "deviceName"; | |
| 246 | - public static final String PARAMS = "params"; | |
| 247 | - public static final String TIMESTAMP = "timestamp"; | |
| 248 | - public static final String CODE = "code"; | |
| 249 | - public static final String MESSAGE = "message"; | |
| 250 | - | |
| 251 | - } | |
| 252 | 116 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; | |
| 6 | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; |
| 7 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; |
| 8 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| ... | ... | @@ -10,6 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr |
| 10 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 11 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; |
| 12 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 13 | 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 14 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 15 | 14 | import io.vertx.core.Vertx; |
| ... | ... | @@ -93,18 +92,20 @@ public class IotGatewayConfiguration { |
| 93 | 92 | public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, |
| 94 | 93 | IotDeviceService deviceService, |
| 95 | 94 | IotDeviceMessageService messageService, |
| 96 | - IotDeviceCommonApi deviceApi, | |
| 97 | - IotTcpCodecManager codecManager, | |
| 95 | + IotTcpSessionManager sessionManager, | |
| 98 | 96 | Vertx tcpVertx) { |
| 99 | 97 | return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), |
| 100 | - deviceService, messageService, deviceApi, codecManager, tcpVertx); | |
| 98 | + deviceService, messageService, sessionManager, tcpVertx); | |
| 101 | 99 | } |
| 102 | 100 | |
| 103 | 101 | @Bean |
| 104 | 102 | public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, |
| 105 | 103 | IotDeviceMessageService messageService, |
| 104 | + IotDeviceService deviceService, | |
| 105 | + IotTcpSessionManager sessionManager, | |
| 106 | 106 | IotMessageBus messageBus) { |
| 107 | - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus); | |
| 107 | + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, | |
| 108 | + messageBus); | |
| 108 | 109 | } |
| 109 | 110 | |
| 110 | 111 | } | ... | ... |
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; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 7 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 8 | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 9 | 11 | import jakarta.annotation.PostConstruct; |
| 10 | 12 | import lombok.extern.slf4j.Slf4j; |
| 13 | +import org.springframework.stereotype.Component; | |
| 11 | 14 | |
| 12 | 15 | /** |
| 13 | 16 | * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 |
| ... | ... | @@ -15,6 +18,7 @@ import lombok.extern.slf4j.Slf4j; |
| 15 | 18 | * @author 芋道源码 |
| 16 | 19 | */ |
| 17 | 20 | @Slf4j |
| 21 | +@Component | |
| 18 | 22 | public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 19 | 23 | |
| 20 | 24 | private final IotTcpDownstreamHandler downstreamHandler; |
| ... | ... | @@ -23,17 +27,27 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic |
| 23 | 27 | |
| 24 | 28 | private final IotTcpUpstreamProtocol protocol; |
| 25 | 29 | |
| 30 | + private final IotDeviceService deviceService; | |
| 31 | + | |
| 32 | + private final IotTcpSessionManager sessionManager; | |
| 33 | + | |
| 26 | 34 | public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, |
| 27 | 35 | IotDeviceMessageService messageService, |
| 36 | + IotDeviceService deviceService, | |
| 37 | + IotTcpSessionManager sessionManager, | |
| 28 | 38 | IotMessageBus messageBus) { |
| 29 | 39 | this.protocol = protocol; |
| 30 | 40 | this.messageBus = messageBus; |
| 31 | - this.downstreamHandler = new IotTcpDownstreamHandler(messageService); | |
| 41 | + this.deviceService = deviceService; | |
| 42 | + this.sessionManager = sessionManager; | |
| 43 | + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, sessionManager); | |
| 32 | 44 | } |
| 33 | 45 | |
| 34 | 46 | @PostConstruct |
| 35 | 47 | public void init() { |
| 36 | 48 | messageBus.register(this); |
| 49 | + log.info("[init][TCP 下游订阅者初始化完成] 服务器 ID: {}, Topic: {}", | |
| 50 | + protocol.getServerId(), getTopic()); | |
| 37 | 51 | } |
| 38 | 52 | |
| 39 | 53 | @Override |
| ... | ... | @@ -49,22 +63,11 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic |
| 49 | 63 | |
| 50 | 64 | @Override |
| 51 | 65 | public void onMessage(IotDeviceMessage message) { |
| 52 | - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", | |
| 53 | - message.getId(), message.getMethod(), message.getDeviceId()); | |
| 54 | 66 | try { |
| 55 | - // 1. 校验 | |
| 56 | - String method = message.getMethod(); | |
| 57 | - if (method == null) { | |
| 58 | - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", | |
| 59 | - message.getId(), message.getDeviceId()); | |
| 60 | - return; | |
| 61 | - } | |
| 62 | - | |
| 63 | - // 2. 处理下行消息 | |
| 64 | 67 | downstreamHandler.handle(message); |
| 65 | 68 | } catch (Exception e) { |
| 66 | - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", | |
| 67 | - message.getId(), message.getMethod(), message.getDeviceId(), e); | |
| 69 | + log.error("[onMessage][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", | |
| 70 | + message.getDeviceId(), message.getMethod(), message.getId(), e); | |
| 68 | 71 | } |
| 69 | 72 | } |
| 70 | 73 | } |
| 71 | 74 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; | |
| 6 | 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 7 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 9 | 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| ... | ... | @@ -30,10 +29,7 @@ public class IotTcpUpstreamProtocol { |
| 30 | 29 | |
| 31 | 30 | private final IotDeviceMessageService messageService; |
| 32 | 31 | |
| 33 | - // TODO @haohao:不用的变量,可以删除; | |
| 34 | - private final IotDeviceCommonApi deviceApi; | |
| 35 | - | |
| 36 | - private final IotTcpCodecManager codecManager; | |
| 32 | + private final IotTcpSessionManager sessionManager; | |
| 37 | 33 | |
| 38 | 34 | private final Vertx vertx; |
| 39 | 35 | |
| ... | ... | @@ -45,28 +41,24 @@ public class IotTcpUpstreamProtocol { |
| 45 | 41 | public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, |
| 46 | 42 | IotDeviceService deviceService, |
| 47 | 43 | IotDeviceMessageService messageService, |
| 48 | - IotDeviceCommonApi deviceApi, | |
| 49 | - IotTcpCodecManager codecManager, | |
| 44 | + IotTcpSessionManager sessionManager, | |
| 50 | 45 | Vertx vertx) { |
| 51 | 46 | this.tcpProperties = tcpProperties; |
| 52 | 47 | this.deviceService = deviceService; |
| 53 | 48 | this.messageService = messageService; |
| 54 | - this.deviceApi = deviceApi; | |
| 55 | - this.codecManager = codecManager; | |
| 49 | + this.sessionManager = sessionManager; | |
| 56 | 50 | this.vertx = vertx; |
| 57 | 51 | this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); |
| 58 | 52 | } |
| 59 | 53 | |
| 60 | 54 | @PostConstruct |
| 61 | 55 | public void start() { |
| 62 | - // TODO @haohao:类似下面 62 到 75 是处理 options 的,因为中间写了注释,其实可以不用空行;然后 77 到 91 可以中间空喊去掉,更紧凑一点; | |
| 63 | 56 | // 创建服务器选项 |
| 64 | 57 | NetServerOptions options = new NetServerOptions() |
| 65 | 58 | .setPort(tcpProperties.getPort()) |
| 66 | 59 | .setTcpKeepAlive(true) |
| 67 | 60 | .setTcpNoDelay(true) |
| 68 | 61 | .setReuseAddress(true); |
| 69 | - | |
| 70 | 62 | // 配置 SSL(如果启用) |
| 71 | 63 | if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { |
| 72 | 64 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| ... | ... | @@ -78,7 +70,8 @@ public class IotTcpUpstreamProtocol { |
| 78 | 70 | // 创建服务器并设置连接处理器 |
| 79 | 71 | netServer = vertx.createNetServer(options); |
| 80 | 72 | netServer.connectHandler(socket -> { |
| 81 | - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager); | |
| 73 | + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, | |
| 74 | + sessionManager); | |
| 82 | 75 | handler.handle(socket); |
| 83 | 76 | }); |
| 84 | 77 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; | |
| 2 | + | |
| 3 | +import io.vertx.core.net.NetSocket; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.extern.slf4j.Slf4j; | |
| 6 | +import org.springframework.stereotype.Component; | |
| 7 | + | |
| 8 | +import java.util.Map; | |
| 9 | +import java.util.concurrent.ConcurrentHashMap; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * IoT 网关 TCP 认证信息管理器 | |
| 13 | + * <p> | |
| 14 | + * 维护 TCP 连接的认证状态,支持认证信息的存储、查询和清理 | |
| 15 | + * | |
| 16 | + * @author 芋道源码 | |
| 17 | + */ | |
| 18 | +@Slf4j | |
| 19 | +@Component | |
| 20 | +public class IotTcpAuthManager { | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * 连接认证状态映射:NetSocket -> 认证信息 | |
| 24 | + */ | |
| 25 | + private final Map<NetSocket, AuthInfo> authStatusMap = new ConcurrentHashMap<>(); | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 设备 ID -> NetSocket 的映射(用于快速查找) | |
| 29 | + */ | |
| 30 | + private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 注册认证信息 | |
| 34 | + * | |
| 35 | + * @param socket TCP 连接 | |
| 36 | + * @param authInfo 认证信息 | |
| 37 | + */ | |
| 38 | + public void registerAuth(NetSocket socket, AuthInfo authInfo) { | |
| 39 | + // 如果设备已有其他连接,先清理旧连接 | |
| 40 | + NetSocket oldSocket = deviceSocketMap.get(authInfo.getDeviceId()); | |
| 41 | + if (oldSocket != null && oldSocket != socket) { | |
| 42 | + log.info("[registerAuth][设备已有其他连接,清理旧连接] 设备 ID: {}, 旧连接: {}", | |
| 43 | + authInfo.getDeviceId(), oldSocket.remoteAddress()); | |
| 44 | + authStatusMap.remove(oldSocket); | |
| 45 | + } | |
| 46 | + | |
| 47 | + // 注册新认证信息 | |
| 48 | + authStatusMap.put(socket, authInfo); | |
| 49 | + deviceSocketMap.put(authInfo.getDeviceId(), socket); | |
| 50 | + | |
| 51 | + log.info("[registerAuth][注册认证信息] 设备 ID: {}, 连接: {}, productKey: {}, deviceName: {}", | |
| 52 | + authInfo.getDeviceId(), socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); | |
| 53 | + } | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * 注销认证信息 | |
| 57 | + * | |
| 58 | + * @param socket TCP 连接 | |
| 59 | + */ | |
| 60 | + public void unregisterAuth(NetSocket socket) { | |
| 61 | + AuthInfo authInfo = authStatusMap.remove(socket); | |
| 62 | + if (authInfo != null) { | |
| 63 | + deviceSocketMap.remove(authInfo.getDeviceId()); | |
| 64 | + log.info("[unregisterAuth][注销认证信息] 设备 ID: {}, 连接: {}", | |
| 65 | + authInfo.getDeviceId(), socket.remoteAddress()); | |
| 66 | + } | |
| 67 | + } | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * 注销设备认证信息 | |
| 71 | + * | |
| 72 | + * @param deviceId 设备 ID | |
| 73 | + */ | |
| 74 | + public void unregisterAuth(Long deviceId) { | |
| 75 | + NetSocket socket = deviceSocketMap.remove(deviceId); | |
| 76 | + if (socket != null) { | |
| 77 | + AuthInfo authInfo = authStatusMap.remove(socket); | |
| 78 | + if (authInfo != null) { | |
| 79 | + log.info("[unregisterAuth][注销设备认证信息] 设备 ID: {}, 连接: {}", | |
| 80 | + deviceId, socket.remoteAddress()); | |
| 81 | + } | |
| 82 | + } | |
| 83 | + } | |
| 84 | + | |
| 85 | + /** | |
| 86 | + * 获取认证信息 | |
| 87 | + * | |
| 88 | + * @param socket TCP 连接 | |
| 89 | + * @return 认证信息,如果未认证则返回 null | |
| 90 | + */ | |
| 91 | + public AuthInfo getAuthInfo(NetSocket socket) { | |
| 92 | + return authStatusMap.get(socket); | |
| 93 | + } | |
| 94 | + | |
| 95 | + /** | |
| 96 | + * 获取设备的认证信息 | |
| 97 | + * | |
| 98 | + * @param deviceId 设备 ID | |
| 99 | + * @return 认证信息,如果设备未认证则返回 null | |
| 100 | + */ | |
| 101 | + public AuthInfo getAuthInfo(Long deviceId) { | |
| 102 | + NetSocket socket = deviceSocketMap.get(deviceId); | |
| 103 | + return socket != null ? authStatusMap.get(socket) : null; | |
| 104 | + } | |
| 105 | + | |
| 106 | + /** | |
| 107 | + * 检查连接是否已认证 | |
| 108 | + * | |
| 109 | + * @param socket TCP 连接 | |
| 110 | + * @return 是否已认证 | |
| 111 | + */ | |
| 112 | + public boolean isAuthenticated(NetSocket socket) { | |
| 113 | + return authStatusMap.containsKey(socket); | |
| 114 | + } | |
| 115 | + | |
| 116 | + /** | |
| 117 | + * 检查设备是否已认证 | |
| 118 | + * | |
| 119 | + * @param deviceId 设备 ID | |
| 120 | + * @return 是否已认证 | |
| 121 | + */ | |
| 122 | + public boolean isAuthenticated(Long deviceId) { | |
| 123 | + return deviceSocketMap.containsKey(deviceId); | |
| 124 | + } | |
| 125 | + | |
| 126 | + /** | |
| 127 | + * 获取设备的 TCP 连接 | |
| 128 | + * | |
| 129 | + * @param deviceId 设备 ID | |
| 130 | + * @return TCP 连接,如果设备未认证则返回 null | |
| 131 | + */ | |
| 132 | + public NetSocket getDeviceSocket(Long deviceId) { | |
| 133 | + return deviceSocketMap.get(deviceId); | |
| 134 | + } | |
| 135 | + | |
| 136 | + /** | |
| 137 | + * 获取当前已认证设备数量 | |
| 138 | + * | |
| 139 | + * @return 已认证设备数量 | |
| 140 | + */ | |
| 141 | + public int getAuthenticatedDeviceCount() { | |
| 142 | + return deviceSocketMap.size(); | |
| 143 | + } | |
| 144 | + | |
| 145 | + /** | |
| 146 | + * 获取所有已认证设备 ID | |
| 147 | + * | |
| 148 | + * @return 已认证设备 ID 集合 | |
| 149 | + */ | |
| 150 | + public java.util.Set<Long> getAuthenticatedDeviceIds() { | |
| 151 | + return deviceSocketMap.keySet(); | |
| 152 | + } | |
| 153 | + | |
| 154 | + /** | |
| 155 | + * 清理所有认证信息 | |
| 156 | + */ | |
| 157 | + public void clearAll() { | |
| 158 | + int count = authStatusMap.size(); | |
| 159 | + authStatusMap.clear(); | |
| 160 | + deviceSocketMap.clear(); | |
| 161 | + log.info("[clearAll][清理所有认证信息] 清理数量: {}", count); | |
| 162 | + } | |
| 163 | + | |
| 164 | + /** | |
| 165 | + * 认证信息 | |
| 166 | + */ | |
| 167 | + @Data | |
| 168 | + public static class AuthInfo { | |
| 169 | + /** | |
| 170 | + * 设备编号 | |
| 171 | + */ | |
| 172 | + private Long deviceId; | |
| 173 | + | |
| 174 | + /** | |
| 175 | + * 产品标识 | |
| 176 | + */ | |
| 177 | + private String productKey; | |
| 178 | + | |
| 179 | + /** | |
| 180 | + * 设备名称 | |
| 181 | + */ | |
| 182 | + private String deviceName; | |
| 183 | + | |
| 184 | + /** | |
| 185 | + * 认证令牌 | |
| 186 | + */ | |
| 187 | + private String token; | |
| 188 | + | |
| 189 | + /** | |
| 190 | + * 客户端 ID | |
| 191 | + */ | |
| 192 | + private String clientId; | |
| 193 | + } | |
| 194 | +} | |
| 0 | 195 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; | |
| 2 | + | |
| 3 | +import io.vertx.core.net.NetSocket; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | +import org.springframework.stereotype.Component; | |
| 6 | + | |
| 7 | +import java.util.Map; | |
| 8 | +import java.util.concurrent.ConcurrentHashMap; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT 网关 TCP 会话管理器 | |
| 12 | + * <p> | |
| 13 | + * 维护设备 ID 和 TCP 连接的映射关系,支持下行消息发送 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@Slf4j | |
| 18 | +@Component | |
| 19 | +public class IotTcpSessionManager { | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 设备 ID -> TCP 连接的映射 | |
| 23 | + */ | |
| 24 | + private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * TCP 连接 -> 设备 ID 的映射(用于连接断开时清理) | |
| 28 | + */ | |
| 29 | + private final Map<NetSocket, Long> socketDeviceMap = new ConcurrentHashMap<>(); | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 注册设备会话 | |
| 33 | + * | |
| 34 | + * @param deviceId 设备 ID | |
| 35 | + * @param socket TCP 连接 | |
| 36 | + */ | |
| 37 | + public void registerSession(Long deviceId, NetSocket socket) { | |
| 38 | + // 如果设备已有连接,先断开旧连接 | |
| 39 | + NetSocket oldSocket = deviceSocketMap.get(deviceId); | |
| 40 | + if (oldSocket != null && oldSocket != socket) { | |
| 41 | + log.info("[registerSession][设备已有连接,断开旧连接] 设备 ID: {}, 旧连接: {}", deviceId, oldSocket.remoteAddress()); | |
| 42 | + oldSocket.close(); | |
| 43 | + socketDeviceMap.remove(oldSocket); | |
| 44 | + } | |
| 45 | + | |
| 46 | + // 注册新连接 | |
| 47 | + deviceSocketMap.put(deviceId, socket); | |
| 48 | + socketDeviceMap.put(socket, deviceId); | |
| 49 | + | |
| 50 | + log.info("[registerSession][注册设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); | |
| 51 | + } | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 注销设备会话 | |
| 55 | + * | |
| 56 | + * @param deviceId 设备 ID | |
| 57 | + */ | |
| 58 | + public void unregisterSession(Long deviceId) { | |
| 59 | + NetSocket socket = deviceSocketMap.remove(deviceId); | |
| 60 | + if (socket != null) { | |
| 61 | + socketDeviceMap.remove(socket); | |
| 62 | + log.info("[unregisterSession][注销设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); | |
| 63 | + } | |
| 64 | + } | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * 注销 TCP 连接会话 | |
| 68 | + * | |
| 69 | + * @param socket TCP 连接 | |
| 70 | + */ | |
| 71 | + public void unregisterSession(NetSocket socket) { | |
| 72 | + Long deviceId = socketDeviceMap.remove(socket); | |
| 73 | + if (deviceId != null) { | |
| 74 | + deviceSocketMap.remove(deviceId); | |
| 75 | + log.info("[unregisterSession][注销连接会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); | |
| 76 | + } | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 80 | + * 获取设备的 TCP 连接 | |
| 81 | + * | |
| 82 | + * @param deviceId 设备 ID | |
| 83 | + * @return TCP 连接,如果设备未连接则返回 null | |
| 84 | + */ | |
| 85 | + public NetSocket getDeviceSocket(Long deviceId) { | |
| 86 | + return deviceSocketMap.get(deviceId); | |
| 87 | + } | |
| 88 | + | |
| 89 | + /** | |
| 90 | + * 检查设备是否在线 | |
| 91 | + * | |
| 92 | + * @param deviceId 设备 ID | |
| 93 | + * @return 是否在线 | |
| 94 | + */ | |
| 95 | + public boolean isDeviceOnline(Long deviceId) { | |
| 96 | + NetSocket socket = deviceSocketMap.get(deviceId); | |
| 97 | + return socket != null; | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** | |
| 101 | + * 发送消息到设备 | |
| 102 | + * | |
| 103 | + * @param deviceId 设备 ID | |
| 104 | + * @param data 消息数据 | |
| 105 | + * @return 是否发送成功 | |
| 106 | + */ | |
| 107 | + public boolean sendToDevice(Long deviceId, byte[] data) { | |
| 108 | + NetSocket socket = deviceSocketMap.get(deviceId); | |
| 109 | + if (socket == null) { | |
| 110 | + log.warn("[sendToDevice][设备未连接] 设备 ID: {}", deviceId); | |
| 111 | + return false; | |
| 112 | + } | |
| 113 | + | |
| 114 | + try { | |
| 115 | + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); | |
| 116 | + log.debug("[sendToDevice][发送消息成功] 设备 ID: {}, 数据长度: {} 字节", deviceId, data.length); | |
| 117 | + return true; | |
| 118 | + } catch (Exception e) { | |
| 119 | + log.error("[sendToDevice][发送消息失败] 设备 ID: {}", deviceId, e); | |
| 120 | + // 发送失败时清理连接 | |
| 121 | + unregisterSession(deviceId); | |
| 122 | + return false; | |
| 123 | + } | |
| 124 | + } | |
| 125 | + | |
| 126 | + /** | |
| 127 | + * 获取当前在线设备数量 | |
| 128 | + * | |
| 129 | + * @return 在线设备数量 | |
| 130 | + */ | |
| 131 | + public int getOnlineDeviceCount() { | |
| 132 | + return deviceSocketMap.size(); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 获取所有在线设备 ID | |
| 137 | + * | |
| 138 | + * @return 在线设备 ID 集合 | |
| 139 | + */ | |
| 140 | + public java.util.Set<Long> getOnlineDeviceIds() { | |
| 141 | + return deviceSocketMap.keySet(); | |
| 142 | + } | |
| 143 | +} | |
| 0 | 144 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 4 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 5 | 8 | import lombok.extern.slf4j.Slf4j; |
| 6 | 9 | |
| 7 | 10 | /** |
| 8 | 11 | * IoT 网关 TCP 下行消息处理器 |
| 9 | - * <p> | |
| 10 | - * 负责处理从业务系统发送到设备的下行消息,包括: | |
| 11 | - * 1. 属性设置 | |
| 12 | - * 2. 服务调用 | |
| 13 | - * 3. 属性获取 | |
| 14 | - * 4. 配置下发 | |
| 15 | - * 5. OTA 升级 | |
| 16 | - * <p> | |
| 17 | - * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录 | |
| 18 | 12 | * |
| 19 | 13 | * @author 芋道源码 |
| 20 | 14 | */ |
| ... | ... | @@ -23,12 +17,15 @@ public class IotTcpDownstreamHandler { |
| 23 | 17 | |
| 24 | 18 | private final IotDeviceMessageService messageService; |
| 25 | 19 | |
| 26 | - // TODO @haohao:代码没提交全,有报错。 | |
| 27 | -// private final IotTcpDeviceMessageCodec codec; | |
| 20 | + private final IotDeviceService deviceService; | |
| 28 | 21 | |
| 29 | - public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { | |
| 22 | + private final IotTcpSessionManager sessionManager; | |
| 23 | + | |
| 24 | + public IotTcpDownstreamHandler(IotDeviceMessageService messageService, | |
| 25 | + IotDeviceService deviceService, IotTcpSessionManager sessionManager) { | |
| 30 | 26 | this.messageService = messageService; |
| 31 | -// this.codec = new IotTcpDeviceMessageCodec(); | |
| 27 | + this.deviceService = deviceService; | |
| 28 | + this.sessionManager = sessionManager; | |
| 32 | 29 | } |
| 33 | 30 | |
| 34 | 31 | /** |
| ... | ... | @@ -38,23 +35,38 @@ public class IotTcpDownstreamHandler { |
| 38 | 35 | */ |
| 39 | 36 | public void handle(IotDeviceMessage message) { |
| 40 | 37 | try { |
| 41 | - log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", | |
| 38 | + log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", | |
| 42 | 39 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 43 | 40 | |
| 44 | - // 编码消息用于日志记录和验证 | |
| 45 | - byte[] encodedMessage = null; | |
| 46 | -// codec.encode(message); | |
| 47 | - log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", | |
| 48 | - message.getDeviceId(), encodedMessage.length); | |
| 41 | + // 1. 获取设备信息 | |
| 42 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 43 | + if (device == null) { | |
| 44 | + log.error("[handle][设备不存在] 设备 ID: {}", message.getDeviceId()); | |
| 45 | + return; | |
| 46 | + } | |
| 47 | + | |
| 48 | + // 2. 检查设备是否在线 | |
| 49 | + if (!sessionManager.isDeviceOnline(message.getDeviceId())) { | |
| 50 | + log.warn("[handle][设备不在线] 设备 ID: {}", message.getDeviceId()); | |
| 51 | + return; | |
| 52 | + } | |
| 49 | 53 | |
| 50 | - // 记录下行消息处理 | |
| 51 | - log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}", | |
| 52 | - message.getDeviceId(), message.getMethod(), message.getParams()); | |
| 54 | + // 3. 编码消息 | |
| 55 | + byte[] bytes = messageService.encodeDeviceMessage(message, device.getCodecType()); | |
| 56 | + | |
| 57 | + // 4. 发送消息到设备 | |
| 58 | + boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes); | |
| 59 | + if (success) { | |
| 60 | + log.info("[handle][下行消息发送成功] 设备 ID: {}, 方法: {}, 消息 ID: {}, 数据长度: {} 字节", | |
| 61 | + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); | |
| 62 | + } else { | |
| 63 | + log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", | |
| 64 | + message.getDeviceId(), message.getMethod(), message.getId()); | |
| 65 | + } | |
| 53 | 66 | |
| 54 | 67 | } catch (Exception e) { |
| 55 | - log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}", | |
| 68 | + log.error("[handle][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息内容: {}", | |
| 56 | 69 | message.getDeviceId(), message.getMethod(), message.getParams(), e); |
| 57 | 70 | } |
| 58 | 71 | } |
| 59 | - | |
| 60 | 72 | } |
| 61 | 73 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.map.MapUtil; | |
| 3 | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 6 | +import cn.hutool.extra.spring.SpringUtil; | |
| 7 | +import cn.hutool.json.JSONObject; | |
| 8 | +import cn.hutool.json.JSONUtil; | |
| 9 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 4 | 12 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; | |
| 13 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 6 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpAuthManager; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 7 | 19 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | 20 | import io.vertx.core.Handler; |
| 9 | 21 | import io.vertx.core.buffer.Buffer; |
| 10 | 22 | import io.vertx.core.net.NetSocket; |
| 11 | -import io.vertx.core.parsetools.RecordParser; | |
| 12 | 23 | import lombok.extern.slf4j.Slf4j; |
| 13 | 24 | |
| 14 | 25 | /** |
| 15 | - * IoT 网关 TCP 上行消息处理器 | |
| 16 | - * | |
| 17 | - * @author 芋道源码 | |
| 26 | + * TCP 上行消息处理器 | |
| 18 | 27 | */ |
| 19 | 28 | @Slf4j |
| 20 | 29 | public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 21 | 30 | |
| 31 | + private static final String CODEC_TYPE_JSON = "TCP_JSON"; | |
| 32 | + private static final String CODEC_TYPE_BINARY = "TCP_BINARY"; | |
| 33 | + private static final String AUTH_METHOD = "auth"; | |
| 34 | + | |
| 22 | 35 | private final IotDeviceMessageService deviceMessageService; |
| 23 | 36 | |
| 24 | - private final String serverId; | |
| 37 | + private final IotDeviceService deviceService; | |
| 38 | + | |
| 39 | + private final IotTcpSessionManager sessionManager; | |
| 40 | + | |
| 41 | + private final IotTcpAuthManager authManager; | |
| 42 | + | |
| 43 | + private final IotDeviceTokenService deviceTokenService; | |
| 44 | + | |
| 45 | + private final IotDeviceCommonApi deviceApi; | |
| 25 | 46 | |
| 26 | - private final IotTcpCodecManager codecManager; | |
| 47 | + private final String serverId; | |
| 27 | 48 | |
| 28 | 49 | public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, |
| 29 | - IotTcpCodecManager codecManager) { | |
| 50 | + IotDeviceService deviceService, IotTcpSessionManager sessionManager) { | |
| 30 | 51 | this.deviceMessageService = deviceMessageService; |
| 52 | + this.deviceService = deviceService; | |
| 53 | + this.sessionManager = sessionManager; | |
| 54 | + this.authManager = SpringUtil.getBean(IotTcpAuthManager.class); | |
| 55 | + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 56 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 31 | 57 | this.serverId = protocol.getServerId(); |
| 32 | - this.codecManager = codecManager; | |
| 33 | 58 | } |
| 34 | 59 | |
| 35 | 60 | @Override |
| 36 | 61 | public void handle(NetSocket socket) { |
| 37 | - // 生成客户端ID用于日志标识 | |
| 38 | 62 | String clientId = IdUtil.simpleUUID(); |
| 39 | - log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress()); | |
| 40 | - | |
| 41 | - // 设置解析器 | |
| 42 | - RecordParser parser = RecordParser.newFixed(1024, buffer -> { | |
| 43 | - try { | |
| 44 | - handleDataPackage(clientId, buffer); | |
| 45 | - } catch (Exception e) { | |
| 46 | - log.error("[handle][处理数据包异常] clientId: {}", clientId, e); | |
| 47 | - } | |
| 48 | - }); | |
| 63 | + log.info("[handle][收到设备连接] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); | |
| 49 | 64 | |
| 50 | - // 设置异常处理 | |
| 65 | + // 设置异常和关闭处理器 | |
| 51 | 66 | socket.exceptionHandler(ex -> { |
| 52 | - log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex); | |
| 67 | + log.error("[handle][连接异常] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress(), ex); | |
| 68 | + cleanupSession(socket); | |
| 53 | 69 | }); |
| 54 | 70 | |
| 55 | 71 | socket.closeHandler(v -> { |
| 56 | - log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress()); | |
| 72 | + log.info("[handle][连接关闭] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); | |
| 73 | + cleanupSession(socket); | |
| 57 | 74 | }); |
| 58 | 75 | |
| 59 | - // 设置数据处理器 | |
| 60 | - socket.handler(parser); | |
| 76 | + socket.handler(buffer -> handleDataPackage(clientId, buffer, socket)); | |
| 77 | + } | |
| 78 | + | |
| 79 | + private void handleDataPackage(String clientId, Buffer buffer, NetSocket socket) { | |
| 80 | + try { | |
| 81 | + if (buffer.length() == 0) { | |
| 82 | + log.warn("[handleDataPackage][数据包为空] 客户端 ID: {}", clientId); | |
| 83 | + return; | |
| 84 | + } | |
| 85 | + | |
| 86 | + // 1. 解码消息 | |
| 87 | + MessageInfo messageInfo = decodeMessage(buffer); | |
| 88 | + if (messageInfo == null) { | |
| 89 | + return; | |
| 90 | + } | |
| 91 | + | |
| 92 | + // 2. 获取设备信息 | |
| 93 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(messageInfo.message.getDeviceId()); | |
| 94 | + if (device == null) { | |
| 95 | + sendError(socket, messageInfo.message.getRequestId(), "设备不存在", messageInfo.codecType); | |
| 96 | + return; | |
| 97 | + } | |
| 98 | + | |
| 99 | + // 3. 处理消息 | |
| 100 | + if (!authManager.isAuthenticated(socket)) { | |
| 101 | + handleAuthRequest(clientId, messageInfo.message, socket, messageInfo.codecType); | |
| 102 | + } else { | |
| 103 | + IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); | |
| 104 | + handleBusinessMessage(clientId, messageInfo.message, authInfo); | |
| 105 | + } | |
| 106 | + } catch (Exception e) { | |
| 107 | + log.error("[handleDataPackage][处理数据包失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); | |
| 108 | + } | |
| 61 | 109 | } |
| 62 | 110 | |
| 63 | 111 | /** |
| 64 | - * 处理数据包 | |
| 112 | + * 处理认证请求 | |
| 65 | 113 | */ |
| 66 | - private void handleDataPackage(String clientId, Buffer buffer) { | |
| 114 | + private void handleAuthRequest(String clientId, IotDeviceMessage message, NetSocket socket, String codecType) { | |
| 67 | 115 | try { |
| 68 | - // 使用编解码器管理器自动检测协议并解码消息 | |
| 69 | - IotDeviceMessage message = codecManager.decode(buffer.getBytes()); | |
| 70 | - log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}", | |
| 71 | - clientId, message.getMethod(), message.getDeviceId()); | |
| 116 | + // 1. 验证认证请求 | |
| 117 | + if (!AUTH_METHOD.equals(message.getMethod())) { | |
| 118 | + sendError(socket, message.getRequestId(), "请先进行认证", codecType); | |
| 119 | + return; | |
| 120 | + } | |
| 72 | 121 | |
| 73 | - // 处理上行消息 | |
| 74 | - handleUpstreamMessage(clientId, message); | |
| 122 | + // 2. 解析认证参数 | |
| 123 | + AuthParams authParams = parseAuthParams(message.getParams()); | |
| 124 | + if (authParams == null) { | |
| 125 | + sendError(socket, message.getRequestId(), "认证参数不完整", codecType); | |
| 126 | + return; | |
| 127 | + } | |
| 128 | + | |
| 129 | + // 3. 执行认证流程 | |
| 130 | + if (performAuthentication(authParams, socket, message.getRequestId(), codecType)) { | |
| 131 | + log.info("[handleAuthRequest][认证成功] 客户端 ID: {}, username: {}", clientId, authParams.username); | |
| 132 | + } | |
| 75 | 133 | } catch (Exception e) { |
| 76 | - log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e); | |
| 134 | + log.error("[handleAuthRequest][认证处理异常] 客户端 ID: {}", clientId, e); | |
| 135 | + sendError(socket, message.getRequestId(), "认证处理异常: " + e.getMessage(), codecType); | |
| 77 | 136 | } |
| 78 | 137 | } |
| 79 | 138 | |
| 80 | 139 | /** |
| 81 | - * 处理上行消息 | |
| 140 | + * 处理业务消息 | |
| 82 | 141 | */ |
| 83 | - private void handleUpstreamMessage(String clientId, IotDeviceMessage message) { | |
| 142 | + private void handleBusinessMessage(String clientId, IotDeviceMessage message, | |
| 143 | + IotTcpAuthManager.AuthInfo authInfo) { | |
| 84 | 144 | try { |
| 85 | - log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}", | |
| 86 | - clientId, message.getMethod(), message.getDeviceId()); | |
| 145 | + message.setDeviceId(authInfo.getDeviceId()); | |
| 146 | + message.setServerId(serverId); | |
| 87 | 147 | |
| 88 | - // 解析设备信息(简化处理) | |
| 89 | - String deviceId = String.valueOf(message.getDeviceId()); | |
| 90 | - String productKey = extractProductKey(deviceId); | |
| 91 | - String deviceName = deviceId; | |
| 148 | + deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), authInfo.getDeviceName(), | |
| 149 | + serverId); | |
| 150 | + log.info("[handleBusinessMessage][业务消息处理完成] 客户端 ID: {}, 消息 ID: {}, 设备 ID: {}, 方法: {}", | |
| 151 | + clientId, message.getId(), message.getDeviceId(), message.getMethod()); | |
| 152 | + } catch (Exception e) { | |
| 153 | + log.error("[handleBusinessMessage][处理业务消息失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); | |
| 154 | + } | |
| 155 | + } | |
| 92 | 156 | |
| 93 | - // 发送消息到队列 | |
| 94 | - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 157 | + /** | |
| 158 | + * 解码消息 | |
| 159 | + */ | |
| 160 | + private MessageInfo decodeMessage(Buffer buffer) { | |
| 161 | + try { | |
| 162 | + String rawData = buffer.toString(); | |
| 163 | + String codecType = isJsonFormat(rawData) ? CODEC_TYPE_JSON : CODEC_TYPE_BINARY; | |
| 164 | + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); | |
| 165 | + return message != null ? new MessageInfo(message, codecType) : null; | |
| 95 | 166 | } catch (Exception e) { |
| 96 | - log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e); | |
| 167 | + log.debug("[decodeMessage][消息解码失败] 错误: {}", e.getMessage()); | |
| 168 | + return null; | |
| 169 | + } | |
| 170 | + } | |
| 171 | + | |
| 172 | + /** | |
| 173 | + * 执行认证 | |
| 174 | + */ | |
| 175 | + private boolean performAuthentication(AuthParams authParams, NetSocket socket, String requestId, String codecType) { | |
| 176 | + // 1. 执行认证 | |
| 177 | + if (!authenticateDevice(authParams)) { | |
| 178 | + sendError(socket, requestId, "认证失败", codecType); | |
| 179 | + return false; | |
| 180 | + } | |
| 181 | + | |
| 182 | + // 2. 获取设备信息 | |
| 183 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(authParams.username); | |
| 184 | + if (deviceInfo == null) { | |
| 185 | + sendError(socket, requestId, "解析设备信息失败", codecType); | |
| 186 | + return false; | |
| 187 | + } | |
| 188 | + | |
| 189 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 190 | + deviceInfo.getDeviceName()); | |
| 191 | + if (device == null) { | |
| 192 | + sendError(socket, requestId, "设备不存在", codecType); | |
| 193 | + return false; | |
| 97 | 194 | } |
| 195 | + | |
| 196 | + // 3. 注册认证信息 | |
| 197 | + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 198 | + registerAuthInfo(socket, device, deviceInfo, token, authParams.clientId); | |
| 199 | + | |
| 200 | + // 4. 发送上线消息和成功响应 | |
| 201 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 202 | + deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), | |
| 203 | + serverId); | |
| 204 | + sendSuccess(socket, requestId, "认证成功", codecType); | |
| 205 | + | |
| 206 | + return true; | |
| 98 | 207 | } |
| 99 | 208 | |
| 100 | 209 | /** |
| 101 | - * 从设备ID中提取产品密钥(简化实现) | |
| 210 | + * 发送响应 | |
| 102 | 211 | */ |
| 103 | - private String extractProductKey(String deviceId) { | |
| 104 | - // 简化实现:假设设备ID格式为 "productKey_deviceName" | |
| 105 | - if (deviceId != null && deviceId.contains("_")) { | |
| 106 | - return deviceId.split("_")[0]; | |
| 212 | + private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { | |
| 213 | + try { | |
| 214 | + Object responseData = buildResponseData(success, message); | |
| 215 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, | |
| 216 | + success ? 0 : 401, message); | |
| 217 | + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 218 | + socket.write(Buffer.buffer(encodedData)); | |
| 219 | + log.debug("[sendResponse][发送响应] success: {}, message: {}, requestId: {}", success, message, requestId); | |
| 220 | + } catch (Exception e) { | |
| 221 | + log.error("[sendResponse][发送响应失败] requestId: {}", requestId, e); | |
| 107 | 222 | } |
| 108 | - return "default_product"; | |
| 223 | + } | |
| 224 | + | |
| 225 | + /** | |
| 226 | + * 构建响应数据(不返回 token) | |
| 227 | + */ | |
| 228 | + private Object buildResponseData(boolean success, String message) { | |
| 229 | + return MapUtil.builder() | |
| 230 | + .put("success", success) | |
| 231 | + .put("message", message) | |
| 232 | + .build(); | |
| 233 | + } | |
| 234 | + | |
| 235 | + /** | |
| 236 | + * 清理会话 | |
| 237 | + */ | |
| 238 | + private void cleanupSession(NetSocket socket) { | |
| 239 | + // 如果已认证,发送离线消息 | |
| 240 | + IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); | |
| 241 | + if (authInfo != null) { | |
| 242 | + // 发送离线消息 | |
| 243 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 244 | + deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), authInfo.getDeviceName(), | |
| 245 | + serverId); | |
| 246 | + } | |
| 247 | + sessionManager.unregisterSession(socket); | |
| 248 | + authManager.unregisterAuth(socket); | |
| 249 | + } | |
| 250 | + | |
| 251 | + /** | |
| 252 | + * 判断是否为 JSON 格式 | |
| 253 | + */ | |
| 254 | + private boolean isJsonFormat(String data) { | |
| 255 | + if (StrUtil.isBlank(data)) | |
| 256 | + return false; | |
| 257 | + String trimmed = data.trim(); | |
| 258 | + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); | |
| 259 | + } | |
| 260 | + | |
| 261 | + /** | |
| 262 | + * 解析认证参数 | |
| 263 | + */ | |
| 264 | + private AuthParams parseAuthParams(Object params) { | |
| 265 | + if (params == null) | |
| 266 | + return null; | |
| 267 | + | |
| 268 | + JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params | |
| 269 | + : JSONUtil.parseObj(params.toString()); | |
| 270 | + String clientId = paramsJson.getStr("clientId"); | |
| 271 | + String username = paramsJson.getStr("username"); | |
| 272 | + String password = paramsJson.getStr("password"); | |
| 273 | + | |
| 274 | + return StrUtil.hasBlank(clientId, username, password) ? null : new AuthParams(clientId, username, password); | |
| 275 | + } | |
| 276 | + | |
| 277 | + /** | |
| 278 | + * 认证设备 | |
| 279 | + */ | |
| 280 | + private boolean authenticateDevice(AuthParams authParams) { | |
| 281 | + CommonResult<Boolean> result = deviceApi | |
| 282 | + .authDevice(new cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO() | |
| 283 | + .setClientId(authParams.clientId) | |
| 284 | + .setUsername(authParams.username) | |
| 285 | + .setPassword(authParams.password)); | |
| 286 | + return result.isSuccess() && result.getData(); | |
| 287 | + } | |
| 288 | + | |
| 289 | + /** | |
| 290 | + * 注册认证信息 | |
| 291 | + */ | |
| 292 | + private void registerAuthInfo(NetSocket socket, IotDeviceRespDTO device, IotDeviceAuthUtils.DeviceInfo deviceInfo, | |
| 293 | + String token, String clientId) { | |
| 294 | + IotTcpAuthManager.AuthInfo auth = new IotTcpAuthManager.AuthInfo(); | |
| 295 | + auth.setDeviceId(device.getId()); | |
| 296 | + auth.setProductKey(deviceInfo.getProductKey()); | |
| 297 | + auth.setDeviceName(deviceInfo.getDeviceName()); | |
| 298 | + auth.setToken(token); | |
| 299 | + auth.setClientId(clientId); | |
| 300 | + | |
| 301 | + authManager.registerAuth(socket, auth); | |
| 302 | + sessionManager.registerSession(device.getId(), socket); | |
| 303 | + } | |
| 304 | + | |
| 305 | + /** | |
| 306 | + * 发送错误响应 | |
| 307 | + */ | |
| 308 | + private void sendError(NetSocket socket, String requestId, String errorMessage, String codecType) { | |
| 309 | + sendResponse(socket, false, errorMessage, requestId, codecType); | |
| 310 | + } | |
| 311 | + | |
| 312 | + /** | |
| 313 | + * 发送成功响应(不返回 token) | |
| 314 | + */ | |
| 315 | + private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { | |
| 316 | + sendResponse(socket, true, message, requestId, codecType); | |
| 317 | + } | |
| 318 | + | |
| 319 | + /** | |
| 320 | + * 认证参数 | |
| 321 | + */ | |
| 322 | + private record AuthParams(String clientId, String username, String password) { | |
| 323 | + } | |
| 324 | + | |
| 325 | + /** | |
| 326 | + * 消息信息 | |
| 327 | + */ | |
| 328 | + private record MessageInfo(IotDeviceMessage message, String codecType) { | |
| 109 | 329 | } |
| 110 | 330 | } |
| 111 | 331 | \ No newline at end of file | ... | ... |
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 { |
| 21 | 21 | String productKey, String deviceName); |
| 22 | 22 | |
| 23 | 23 | /** |
| 24 | + * 编码消息 | |
| 25 | + * | |
| 26 | + * @param message 消息 | |
| 27 | + * @param codecType 编解码器类型 | |
| 28 | + * @return 编码后的消息内容 | |
| 29 | + */ | |
| 30 | + byte[] encodeDeviceMessage(IotDeviceMessage message, | |
| 31 | + String codecType); | |
| 32 | + | |
| 33 | + /** | |
| 24 | 34 | * 解码消息 |
| 25 | 35 | * |
| 26 | 36 | * @param bytes 消息内容 |
| ... | ... | @@ -32,12 +42,21 @@ public interface IotDeviceMessageService { |
| 32 | 42 | String productKey, String deviceName); |
| 33 | 43 | |
| 34 | 44 | /** |
| 45 | + * 解码消息 | |
| 46 | + * | |
| 47 | + * @param bytes 消息内容 | |
| 48 | + * @param codecType 编解码器类型 | |
| 49 | + * @return 解码后的消息内容 | |
| 50 | + */ | |
| 51 | + IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); | |
| 52 | + | |
| 53 | + /** | |
| 35 | 54 | * 发送消息 |
| 36 | 55 | * |
| 37 | - * @param message 消息 | |
| 56 | + * @param message 消息 | |
| 38 | 57 | * @param productKey 产品 Key |
| 39 | 58 | * @param deviceName 设备名称 |
| 40 | - * @param serverId 设备连接的 serverId | |
| 59 | + * @param serverId 设备连接的 serverId | |
| 41 | 60 | */ |
| 42 | 61 | void sendDeviceMessage(IotDeviceMessage message, |
| 43 | 62 | String productKey, String deviceName, String serverId); | ... | ... |
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 { |
| 62 | 62 | } |
| 63 | 63 | |
| 64 | 64 | @Override |
| 65 | + public byte[] encodeDeviceMessage(IotDeviceMessage message, | |
| 66 | + String codecType) { | |
| 67 | + // 1. 获取编解码器 | |
| 68 | + IotDeviceMessageCodec codec = codes.get(codecType); | |
| 69 | + if (codec == null) { | |
| 70 | + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); | |
| 71 | + } | |
| 72 | + | |
| 73 | + // 2. 编码消息 | |
| 74 | + return codec.encode(message); | |
| 75 | + } | |
| 76 | + | |
| 77 | + @Override | |
| 65 | 78 | public IotDeviceMessage decodeDeviceMessage(byte[] bytes, |
| 66 | 79 | String productKey, String deviceName) { |
| 67 | 80 | // 1.1 获取设备信息 |
| ... | ... | @@ -80,6 +93,18 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 80 | 93 | } |
| 81 | 94 | |
| 82 | 95 | @Override |
| 96 | + public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { | |
| 97 | + // 1. 获取编解码器 | |
| 98 | + IotDeviceMessageCodec codec = codes.get(codecType); | |
| 99 | + if (codec == null) { | |
| 100 | + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); | |
| 101 | + } | |
| 102 | + | |
| 103 | + // 2. 解码消息 | |
| 104 | + return codec.decode(bytes); | |
| 105 | + } | |
| 106 | + | |
| 107 | + @Override | |
| 83 | 108 | public void sendDeviceMessage(IotDeviceMessage message, |
| 84 | 109 | String productKey, String deviceName, String serverId) { |
| 85 | 110 | // 1. 获取设备信息 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java
| ... | ... | @@ -2,42 +2,37 @@ package cn.iocoder.yudao.module.iot.gateway.codec.tcp; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | 4 | import lombok.extern.slf4j.Slf4j; |
| 5 | +import org.junit.jupiter.api.BeforeEach; | |
| 6 | +import org.junit.jupiter.api.Test; | |
| 5 | 7 | |
| 6 | 8 | import java.util.HashMap; |
| 7 | 9 | import java.util.Map; |
| 8 | 10 | |
| 9 | -// TODO @haohao:这种写成单测,会好点 | |
| 11 | +import static org.junit.jupiter.api.Assertions.*; | |
| 12 | + | |
| 10 | 13 | /** |
| 11 | - * TCP二进制格式数据包示例 | |
| 14 | + * TCP 二进制格式数据包单元测试 | |
| 12 | 15 | * |
| 13 | - * 演示如何使用二进制协议创建和解析TCP上报数据包和心跳包 | |
| 16 | + * 测试二进制协议创建和解析 TCP 上报数据包和心跳包 | |
| 14 | 17 | * |
| 15 | 18 | * 二进制协议格式: |
| 16 | - * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) | |
| 19 | + * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) | |
| 17 | 20 | * |
| 18 | 21 | * @author 芋道源码 |
| 19 | 22 | */ |
| 20 | 23 | @Slf4j |
| 21 | -public class TcpBinaryDataPacketExamples { | |
| 22 | - | |
| 23 | - public static void main(String[] args) { | |
| 24 | - IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); | |
| 24 | +class TcpBinaryDataPacketExamplesTest { | |
| 25 | 25 | |
| 26 | - // 1. 数据上报包示例 | |
| 27 | - demonstrateDataReport(codec); | |
| 26 | + private IotTcpBinaryDeviceMessageCodec codec; | |
| 28 | 27 | |
| 29 | - // 2. 心跳包示例 | |
| 30 | - demonstrateHeartbeat(codec); | |
| 31 | - | |
| 32 | - // 3. 复杂数据上报示例 | |
| 33 | - demonstrateComplexDataReport(codec); | |
| 28 | + @BeforeEach | |
| 29 | + void setUp() { | |
| 30 | + codec = new IotTcpBinaryDeviceMessageCodec(); | |
| 34 | 31 | } |
| 35 | 32 | |
| 36 | - /** | |
| 37 | - * 演示二进制格式数据上报包 | |
| 38 | - */ | |
| 39 | - private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) { | |
| 40 | - log.info("=== 二进制格式数据上报包示例 ==="); | |
| 33 | + @Test | |
| 34 | + void testDataReport() { | |
| 35 | + log.info("=== 二进制格式数据上报包测试 ==="); | |
| 41 | 36 | |
| 42 | 37 | // 创建传感器数据 |
| 43 | 38 | Map<String, Object> sensorData = new HashMap<>(); |
| ... | ... | @@ -57,22 +52,23 @@ public class TcpBinaryDataPacketExamples { |
| 57 | 52 | |
| 58 | 53 | // 解码验证 |
| 59 | 54 | IotDeviceMessage decoded = codec.decode(packet); |
| 60 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 61 | - log.info("解码后请求ID: {}", decoded.getRequestId()); | |
| 55 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 56 | + log.info("解码后请求 ID: {}", decoded.getRequestId()); | |
| 62 | 57 | log.info("解码后方法: {}", decoded.getMethod()); |
| 63 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 64 | - log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 65 | - log.info("解码后上报时间: {}", decoded.getReportTime()); | |
| 58 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 59 | + log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 66 | 60 | log.info("解码后参数: {}", decoded.getParams()); |
| 67 | 61 | |
| 68 | - System.out.println(); | |
| 62 | + // 断言验证 | |
| 63 | + assertNotNull(decoded.getId()); | |
| 64 | + assertEquals("thing.property.post", decoded.getMethod()); | |
| 65 | + assertNotNull(decoded.getParams()); | |
| 66 | + assertTrue(decoded.getParams() instanceof Map); | |
| 69 | 67 | } |
| 70 | 68 | |
| 71 | - /** | |
| 72 | - * 演示二进制格式心跳包 | |
| 73 | - */ | |
| 74 | - private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) { | |
| 75 | - log.info("=== 二进制格式心跳包示例 ==="); | |
| 69 | + @Test | |
| 70 | + void testHeartbeat() { | |
| 71 | + log.info("=== 二进制格式心跳包测试 ==="); | |
| 76 | 72 | |
| 77 | 73 | // 创建心跳消息 |
| 78 | 74 | IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); |
| ... | ... | @@ -85,21 +81,21 @@ public class TcpBinaryDataPacketExamples { |
| 85 | 81 | |
| 86 | 82 | // 解码验证 |
| 87 | 83 | IotDeviceMessage decoded = codec.decode(packet); |
| 88 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 89 | - log.info("解码后请求ID: {}", decoded.getRequestId()); | |
| 84 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 85 | + log.info("解码后请求 ID: {}", decoded.getRequestId()); | |
| 90 | 86 | log.info("解码后方法: {}", decoded.getMethod()); |
| 91 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 92 | - log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 87 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 88 | + log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 93 | 89 | log.info("解码后参数: {}", decoded.getParams()); |
| 94 | 90 | |
| 95 | - System.out.println(); | |
| 91 | + // 断言验证 | |
| 92 | + assertNotNull(decoded.getId()); | |
| 93 | + assertEquals("thing.state.online", decoded.getMethod()); | |
| 96 | 94 | } |
| 97 | 95 | |
| 98 | - /** | |
| 99 | - * 演示二进制格式复杂数据上报 | |
| 100 | - */ | |
| 101 | - private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) { | |
| 102 | - log.info("=== 二进制格式复杂数据上报示例 ==="); | |
| 96 | + @Test | |
| 97 | + void testComplexDataReport() { | |
| 98 | + log.info("=== 二进制格式复杂数据上报测试 ==="); | |
| 103 | 99 | |
| 104 | 100 | // 创建复杂设备数据 |
| 105 | 101 | Map<String, Object> deviceData = new HashMap<>(); |
| ... | ... | @@ -111,7 +107,7 @@ public class TcpBinaryDataPacketExamples { |
| 111 | 107 | environment.put("co2", 420); |
| 112 | 108 | deviceData.put("environment", environment); |
| 113 | 109 | |
| 114 | - // GPS数据 | |
| 110 | + // GPS 数据 | |
| 115 | 111 | Map<String, Object> location = new HashMap<>(); |
| 116 | 112 | location.put("latitude", 39.9042); |
| 117 | 113 | location.put("longitude", 116.4074); |
| ... | ... | @@ -136,18 +132,48 @@ public class TcpBinaryDataPacketExamples { |
| 136 | 132 | |
| 137 | 133 | // 解码验证 |
| 138 | 134 | IotDeviceMessage decoded = codec.decode(packet); |
| 139 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 140 | - log.info("解码后请求ID: {}", decoded.getRequestId()); | |
| 135 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 136 | + log.info("解码后请求 ID: {}", decoded.getRequestId()); | |
| 141 | 137 | log.info("解码后方法: {}", decoded.getMethod()); |
| 142 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 143 | - log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 138 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 139 | + log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 144 | 140 | log.info("解码后参数: {}", decoded.getParams()); |
| 145 | 141 | |
| 146 | - System.out.println(); | |
| 142 | + // 断言验证 | |
| 143 | + assertNotNull(decoded.getId()); | |
| 144 | + assertEquals("thing.property.post", decoded.getMethod()); | |
| 145 | + assertNotNull(decoded.getParams()); | |
| 146 | + } | |
| 147 | + | |
| 148 | + @Test | |
| 149 | + void testPacketStructureAnalysis() { | |
| 150 | + log.info("=== 数据包结构分析测试 ==="); | |
| 151 | + | |
| 152 | + // 创建测试数据 | |
| 153 | + Map<String, Object> sensorData = new HashMap<>(); | |
| 154 | + sensorData.put("temperature", 25.5); | |
| 155 | + sensorData.put("humidity", 60.2); | |
| 156 | + | |
| 157 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 158 | + message.setDeviceId(123456L); | |
| 159 | + | |
| 160 | + // 编码 | |
| 161 | + byte[] packet = codec.encode(message); | |
| 162 | + | |
| 163 | + // 分析数据包结构 | |
| 164 | + analyzePacketStructure(packet); | |
| 165 | + | |
| 166 | + // 断言验证 | |
| 167 | + assertTrue(packet.length >= 8, "数据包长度应该至少为 8 字节"); | |
| 147 | 168 | } |
| 148 | 169 | |
| 170 | + // ==================== 内部辅助方法 ==================== | |
| 171 | + | |
| 149 | 172 | /** |
| 150 | 173 | * 字节数组转十六进制字符串 |
| 174 | + * | |
| 175 | + * @param bytes 字节数组 | |
| 176 | + * @return 十六进制字符串 | |
| 151 | 177 | */ |
| 152 | 178 | private static String bytesToHex(byte[] bytes) { |
| 153 | 179 | StringBuilder result = new StringBuilder(); |
| ... | ... | @@ -159,8 +185,10 @@ public class TcpBinaryDataPacketExamples { |
| 159 | 185 | |
| 160 | 186 | /** |
| 161 | 187 | * 演示数据包结构分析 |
| 188 | + * | |
| 189 | + * @param packet 数据包 | |
| 162 | 190 | */ |
| 163 | - public static void analyzePacketStructure(byte[] packet) { | |
| 191 | + private static void analyzePacketStructure(byte[] packet) { | |
| 164 | 192 | if (packet.length < 8) { |
| 165 | 193 | log.error("数据包长度不足"); |
| 166 | 194 | return; |
| ... | ... | @@ -168,30 +196,20 @@ public class TcpBinaryDataPacketExamples { |
| 168 | 196 | |
| 169 | 197 | int index = 0; |
| 170 | 198 | |
| 171 | - // 解析包头(4字节) - 后续数据长度 | |
| 199 | + // 解析包头(4 字节) - 后续数据长度 | |
| 172 | 200 | int totalLength = ((packet[index] & 0xFF) << 24) | |
| 173 | - ((packet[index + 1] & 0xFF) << 16) | | |
| 174 | - ((packet[index + 2] & 0xFF) << 8) | | |
| 175 | - (packet[index + 3] & 0xFF); | |
| 201 | + ((packet[index + 1] & 0xFF) << 16) | | |
| 202 | + ((packet[index + 2] & 0xFF) << 8) | | |
| 203 | + (packet[index + 3] & 0xFF); | |
| 176 | 204 | index += 4; |
| 177 | 205 | log.info("包头 - 后续数据长度: {} 字节", totalLength); |
| 178 | 206 | |
| 179 | - // 解析设备地址长度(2字节) | |
| 180 | - int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); | |
| 181 | - index += 2; | |
| 182 | - log.info("设备地址长度: {} 字节", addrLength); | |
| 183 | - | |
| 184 | - // 解析设备地址 | |
| 185 | - String deviceAddr = new String(packet, index, addrLength); | |
| 186 | - index += addrLength; | |
| 187 | - log.info("设备地址: {}", deviceAddr); | |
| 188 | - | |
| 189 | - // 解析功能码(2字节) | |
| 207 | + // 解析功能码(2 字节) | |
| 190 | 208 | int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); |
| 191 | 209 | index += 2; |
| 192 | 210 | log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); |
| 193 | 211 | |
| 194 | - // 解析消息序号(2字节) | |
| 212 | + // 解析消息序号(2 字节) | |
| 195 | 213 | int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); |
| 196 | 214 | index += 2; |
| 197 | 215 | log.info("消息序号: {}", messageId); |
| ... | ... | @@ -205,16 +223,19 @@ public class TcpBinaryDataPacketExamples { |
| 205 | 223 | |
| 206 | 224 | /** |
| 207 | 225 | * 获取功能码名称 |
| 226 | + * | |
| 227 | + * @param code 功能码 | |
| 228 | + * @return 功能码名称 | |
| 208 | 229 | */ |
| 209 | 230 | private static String getFunctionCodeName(int code) { |
| 210 | - switch (code) { | |
| 211 | - case 10: return "设备注册"; | |
| 212 | - case 11: return "注册回复"; | |
| 213 | - case 20: return "心跳请求"; | |
| 214 | - case 21: return "心跳回复"; | |
| 215 | - case 30: return "消息上行"; | |
| 216 | - case 40: return "消息下行"; | |
| 217 | - default: return "未知功能码"; | |
| 218 | - } | |
| 231 | + return switch (code) { | |
| 232 | + case 10 -> "设备注册"; | |
| 233 | + case 11 -> "注册回复"; | |
| 234 | + case 20 -> "心跳请求"; | |
| 235 | + case 21 -> "心跳回复"; | |
| 236 | + case 30 -> "消息上行"; | |
| 237 | + case 40 -> "消息下行"; | |
| 238 | + default -> "未知功能码"; | |
| 239 | + }; | |
| 219 | 240 | } |
| 220 | 241 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | -import lombok.extern.slf4j.Slf4j; | |
| 5 | - | |
| 6 | -import java.nio.charset.StandardCharsets; | |
| 7 | -import java.util.HashMap; | |
| 8 | -import java.util.Map; | |
| 9 | - | |
| 10 | -// TODO @haohao:这种写成单测,会好点 | |
| 11 | -/** | |
| 12 | - * TCP JSON格式数据包示例 | |
| 13 | - * | |
| 14 | - * 演示如何使用新的JSON格式进行TCP消息编解码 | |
| 15 | - * | |
| 16 | - * @author 芋道源码 | |
| 17 | - */ | |
| 18 | -@Slf4j | |
| 19 | -public class TcpJsonDataPacketExamples { | |
| 20 | - | |
| 21 | - public static void main(String[] args) { | |
| 22 | - IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 23 | - | |
| 24 | - // 1. 数据上报示例 | |
| 25 | - demonstrateDataReport(codec); | |
| 26 | - | |
| 27 | - // 2. 心跳示例 | |
| 28 | - demonstrateHeartbeat(codec); | |
| 29 | - | |
| 30 | - // 3. 事件上报示例 | |
| 31 | - demonstrateEventReport(codec); | |
| 32 | - | |
| 33 | - // 4. 复杂数据上报示例 | |
| 34 | - demonstrateComplexDataReport(codec); | |
| 35 | - | |
| 36 | - // 5. 便捷方法示例 | |
| 37 | - demonstrateConvenienceMethods(); | |
| 38 | - | |
| 39 | - // 6. EMQX兼容性示例 | |
| 40 | - demonstrateEmqxCompatibility(); | |
| 41 | - } | |
| 42 | - | |
| 43 | - /** | |
| 44 | - * 演示数据上报 | |
| 45 | - */ | |
| 46 | - private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) { | |
| 47 | - log.info("=== JSON格式数据上报示例 ==="); | |
| 48 | - | |
| 49 | - // 创建传感器数据 | |
| 50 | - Map<String, Object> sensorData = new HashMap<>(); | |
| 51 | - sensorData.put("temperature", 25.5); | |
| 52 | - sensorData.put("humidity", 60.2); | |
| 53 | - sensorData.put("pressure", 1013.25); | |
| 54 | - sensorData.put("battery", 85); | |
| 55 | - | |
| 56 | - // 创建设备消息 | |
| 57 | - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 58 | - message.setDeviceId(123456L); | |
| 59 | - | |
| 60 | - // 编码 | |
| 61 | - byte[] packet = codec.encode(message); | |
| 62 | - String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 63 | - log.info("编码后JSON: {}", jsonString); | |
| 64 | - log.info("数据包长度: {} 字节", packet.length); | |
| 65 | - | |
| 66 | - // 解码验证 | |
| 67 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 68 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 69 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 70 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 71 | - log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 72 | - log.info("解码后参数: {}", decoded.getParams()); | |
| 73 | - | |
| 74 | - System.out.println(); | |
| 75 | - } | |
| 76 | - | |
| 77 | - /** | |
| 78 | - * 演示心跳 | |
| 79 | - */ | |
| 80 | - private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) { | |
| 81 | - log.info("=== JSON格式心跳示例 ==="); | |
| 82 | - | |
| 83 | - // 创建心跳消息 | |
| 84 | - IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); | |
| 85 | - heartbeat.setDeviceId(123456L); | |
| 86 | - | |
| 87 | - // 编码 | |
| 88 | - byte[] packet = codec.encode(heartbeat); | |
| 89 | - String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 90 | - log.info("编码后JSON: {}", jsonString); | |
| 91 | - log.info("心跳包长度: {} 字节", packet.length); | |
| 92 | - | |
| 93 | - // 解码验证 | |
| 94 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 95 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 96 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 97 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 98 | - log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 99 | - | |
| 100 | - System.out.println(); | |
| 101 | - } | |
| 102 | - | |
| 103 | - /** | |
| 104 | - * 演示事件上报 | |
| 105 | - */ | |
| 106 | - private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) { | |
| 107 | - log.info("=== JSON格式事件上报示例 ==="); | |
| 108 | - | |
| 109 | - // 创建事件数据 | |
| 110 | - Map<String, Object> eventData = new HashMap<>(); | |
| 111 | - eventData.put("eventType", "alarm"); | |
| 112 | - eventData.put("level", "warning"); | |
| 113 | - eventData.put("description", "温度过高"); | |
| 114 | - eventData.put("value", 45.8); | |
| 115 | - | |
| 116 | - // 创建事件消息 | |
| 117 | - IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); | |
| 118 | - event.setDeviceId(123456L); | |
| 119 | - | |
| 120 | - // 编码 | |
| 121 | - byte[] packet = codec.encode(event); | |
| 122 | - String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 123 | - log.info("编码后JSON: {}", jsonString); | |
| 124 | - log.info("事件包长度: {} 字节", packet.length); | |
| 125 | - | |
| 126 | - // 解码验证 | |
| 127 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 128 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 129 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 130 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 131 | - log.info("解码后参数: {}", decoded.getParams()); | |
| 132 | - | |
| 133 | - System.out.println(); | |
| 134 | - } | |
| 135 | - | |
| 136 | - /** | |
| 137 | - * 演示复杂数据上报 | |
| 138 | - */ | |
| 139 | - private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) { | |
| 140 | - log.info("=== JSON格式复杂数据上报示例 ==="); | |
| 141 | - | |
| 142 | - // 创建复杂设备数据(类似EMQX格式) | |
| 143 | - Map<String, Object> deviceData = new HashMap<>(); | |
| 144 | - | |
| 145 | - // 环境数据 | |
| 146 | - Map<String, Object> environment = new HashMap<>(); | |
| 147 | - environment.put("temperature", 23.8); | |
| 148 | - environment.put("humidity", 55.0); | |
| 149 | - environment.put("co2", 420); | |
| 150 | - environment.put("pm25", 35); | |
| 151 | - deviceData.put("environment", environment); | |
| 152 | - | |
| 153 | - // GPS数据 | |
| 154 | - Map<String, Object> location = new HashMap<>(); | |
| 155 | - location.put("latitude", 39.9042); | |
| 156 | - location.put("longitude", 116.4074); | |
| 157 | - location.put("altitude", 43.5); | |
| 158 | - location.put("speed", 0.0); | |
| 159 | - deviceData.put("location", location); | |
| 160 | - | |
| 161 | - // 设备状态 | |
| 162 | - Map<String, Object> status = new HashMap<>(); | |
| 163 | - status.put("battery", 78); | |
| 164 | - status.put("signal", -65); | |
| 165 | - status.put("online", true); | |
| 166 | - status.put("version", "1.2.3"); | |
| 167 | - deviceData.put("status", status); | |
| 168 | - | |
| 169 | - // 创建设备消息 | |
| 170 | - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); | |
| 171 | - message.setDeviceId(789012L); | |
| 172 | - | |
| 173 | - // 编码 | |
| 174 | - byte[] packet = codec.encode(message); | |
| 175 | - String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 176 | - log.info("编码后JSON: {}", jsonString); | |
| 177 | - log.info("复杂数据包长度: {} 字节", packet.length); | |
| 178 | - | |
| 179 | - // 解码验证 | |
| 180 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 181 | - log.info("解码后消息ID: {}", decoded.getId()); | |
| 182 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 183 | - log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 184 | - log.info("解码后参数: {}", decoded.getParams()); | |
| 185 | - | |
| 186 | - System.out.println(); | |
| 187 | - } | |
| 188 | - | |
| 189 | - /** | |
| 190 | - * 演示便捷方法 | |
| 191 | - */ | |
| 192 | - private static void demonstrateConvenienceMethods() { | |
| 193 | - log.info("=== 便捷方法示例 ==="); | |
| 194 | - | |
| 195 | - IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 196 | - | |
| 197 | - // 使用便捷方法编码数据上报 | |
| 198 | - Map<String, Object> sensorData = Map.of( | |
| 199 | - "temperature", 26.5, | |
| 200 | - "humidity", 58.3 | |
| 201 | - ); | |
| 202 | - byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001"); | |
| 203 | - log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8)); | |
| 204 | - | |
| 205 | - // 使用便捷方法编码心跳 | |
| 206 | - byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001"); | |
| 207 | - log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8)); | |
| 208 | - | |
| 209 | - // 使用便捷方法编码事件 | |
| 210 | - Map<String, Object> eventData = Map.of( | |
| 211 | - "eventType", "maintenance", | |
| 212 | - "description", "定期维护提醒" | |
| 213 | - ); | |
| 214 | - byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001"); | |
| 215 | - log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8)); | |
| 216 | - | |
| 217 | - System.out.println(); | |
| 218 | - } | |
| 219 | - | |
| 220 | - /** | |
| 221 | - * 演示与EMQX格式的兼容性 | |
| 222 | - */ | |
| 223 | - private static void demonstrateEmqxCompatibility() { | |
| 224 | - log.info("=== EMQX格式兼容性示例 ==="); | |
| 225 | - | |
| 226 | - // 模拟EMQX风格的消息格式 | |
| 227 | - String emqxStyleJson = """ | |
| 228 | - { | |
| 229 | - "id": "msg_001", | |
| 230 | - "method": "thing.property.post", | |
| 231 | - "deviceId": 123456, | |
| 232 | - "params": { | |
| 233 | - "temperature": 25.5, | |
| 234 | - "humidity": 60.2 | |
| 235 | - }, | |
| 236 | - "timestamp": 1642781234567 | |
| 237 | - } | |
| 238 | - """; | |
| 239 | - | |
| 240 | - IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 241 | - | |
| 242 | - // 解码EMQX风格的消息 | |
| 243 | - byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8); | |
| 244 | - IotDeviceMessage decoded = codec.decode(emqxBytes); | |
| 245 | - | |
| 246 | - log.info("EMQX风格消息解码成功:"); | |
| 247 | - log.info("消息ID: {}", decoded.getId()); | |
| 248 | - log.info("方法: {}", decoded.getMethod()); | |
| 249 | - log.info("设备ID: {}", decoded.getDeviceId()); | |
| 250 | - log.info("参数: {}", decoded.getParams()); | |
| 251 | - | |
| 252 | - System.out.println(); | |
| 253 | - } | |
| 254 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | +import org.junit.jupiter.api.BeforeEach; | |
| 6 | +import org.junit.jupiter.api.Test; | |
| 7 | + | |
| 8 | +import java.nio.charset.StandardCharsets; | |
| 9 | +import java.util.HashMap; | |
| 10 | +import java.util.Map; | |
| 11 | + | |
| 12 | +import static org.junit.jupiter.api.Assertions.*; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * TCP JSON 格式数据包单元测试 | |
| 16 | + * <p> | |
| 17 | + * 测试 JSON 格式的 TCP 消息编解码功能 | |
| 18 | + * | |
| 19 | + * @author 芋道源码 | |
| 20 | + */ | |
| 21 | +@Slf4j | |
| 22 | +class TcpJsonDataPacketExamplesTest { | |
| 23 | + | |
| 24 | + private IotTcpJsonDeviceMessageCodec codec; | |
| 25 | + | |
| 26 | + @BeforeEach | |
| 27 | + void setUp() { | |
| 28 | + codec = new IotTcpJsonDeviceMessageCodec(); | |
| 29 | + } | |
| 30 | + | |
| 31 | + @Test | |
| 32 | + void testDataReport() { | |
| 33 | + log.info("=== JSON 格式数据上报测试 ==="); | |
| 34 | + | |
| 35 | + // 创建传感器数据 | |
| 36 | + Map<String, Object> sensorData = new HashMap<>(); | |
| 37 | + sensorData.put("temperature", 25.5); | |
| 38 | + sensorData.put("humidity", 60.2); | |
| 39 | + sensorData.put("pressure", 1013.25); | |
| 40 | + sensorData.put("battery", 85); | |
| 41 | + | |
| 42 | + // 创建设备消息 | |
| 43 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 44 | + message.setDeviceId(123456L); | |
| 45 | + | |
| 46 | + // 编码 | |
| 47 | + byte[] packet = codec.encode(message); | |
| 48 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 49 | + log.info("编码后 JSON: {}", jsonString); | |
| 50 | + log.info("数据包长度: {} 字节", packet.length); | |
| 51 | + | |
| 52 | + // 解码验证 | |
| 53 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 54 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 55 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 56 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 57 | + log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 58 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 59 | + | |
| 60 | + // 断言验证 | |
| 61 | + assertNotNull(decoded.getId()); | |
| 62 | + assertEquals("thing.property.post", decoded.getMethod()); | |
| 63 | + assertEquals(123456L, decoded.getDeviceId()); | |
| 64 | + assertNotNull(decoded.getParams()); | |
| 65 | + assertTrue(decoded.getParams() instanceof Map); | |
| 66 | + } | |
| 67 | + | |
| 68 | + @Test | |
| 69 | + void testHeartbeat() { | |
| 70 | + log.info("=== JSON 格式心跳测试 ==="); | |
| 71 | + | |
| 72 | + // 创建心跳消息 | |
| 73 | + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); | |
| 74 | + heartbeat.setDeviceId(123456L); | |
| 75 | + | |
| 76 | + // 编码 | |
| 77 | + byte[] packet = codec.encode(heartbeat); | |
| 78 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 79 | + log.info("编码后 JSON: {}", jsonString); | |
| 80 | + log.info("心跳包长度: {} 字节", packet.length); | |
| 81 | + | |
| 82 | + // 解码验证 | |
| 83 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 84 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 85 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 86 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 87 | + log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 88 | + | |
| 89 | + // 断言验证 | |
| 90 | + assertNotNull(decoded.getId()); | |
| 91 | + assertEquals("thing.state.online", decoded.getMethod()); | |
| 92 | + assertEquals(123456L, decoded.getDeviceId()); | |
| 93 | + } | |
| 94 | + | |
| 95 | + @Test | |
| 96 | + void testEventReport() { | |
| 97 | + log.info("=== JSON 格式事件上报测试 ==="); | |
| 98 | + | |
| 99 | + // 创建事件数据 | |
| 100 | + Map<String, Object> eventData = new HashMap<>(); | |
| 101 | + eventData.put("eventType", "alarm"); | |
| 102 | + eventData.put("level", "warning"); | |
| 103 | + eventData.put("description", "温度过高"); | |
| 104 | + eventData.put("value", 45.8); | |
| 105 | + | |
| 106 | + // 创建事件消息 | |
| 107 | + IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); | |
| 108 | + event.setDeviceId(123456L); | |
| 109 | + | |
| 110 | + // 编码 | |
| 111 | + byte[] packet = codec.encode(event); | |
| 112 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 113 | + log.info("编码后 JSON: {}", jsonString); | |
| 114 | + log.info("事件包长度: {} 字节", packet.length); | |
| 115 | + | |
| 116 | + // 解码验证 | |
| 117 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 118 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 119 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 120 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 121 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 122 | + | |
| 123 | + // 断言验证 | |
| 124 | + assertNotNull(decoded.getId()); | |
| 125 | + assertEquals("thing.event.post", decoded.getMethod()); | |
| 126 | + assertEquals(123456L, decoded.getDeviceId()); | |
| 127 | + assertNotNull(decoded.getParams()); | |
| 128 | + } | |
| 129 | + | |
| 130 | + @Test | |
| 131 | + void testComplexDataReport() { | |
| 132 | + log.info("=== JSON 格式复杂数据上报测试 ==="); | |
| 133 | + | |
| 134 | + // 创建复杂设备数据(类似 EMQX 格式) | |
| 135 | + Map<String, Object> deviceData = new HashMap<>(); | |
| 136 | + | |
| 137 | + // 环境数据 | |
| 138 | + Map<String, Object> environment = new HashMap<>(); | |
| 139 | + environment.put("temperature", 23.8); | |
| 140 | + environment.put("humidity", 55.0); | |
| 141 | + environment.put("co2", 420); | |
| 142 | + environment.put("pm25", 35); | |
| 143 | + deviceData.put("environment", environment); | |
| 144 | + | |
| 145 | + // GPS 数据 | |
| 146 | + Map<String, Object> location = new HashMap<>(); | |
| 147 | + location.put("latitude", 39.9042); | |
| 148 | + location.put("longitude", 116.4074); | |
| 149 | + location.put("altitude", 43.5); | |
| 150 | + location.put("speed", 0.0); | |
| 151 | + deviceData.put("location", location); | |
| 152 | + | |
| 153 | + // 设备状态 | |
| 154 | + Map<String, Object> status = new HashMap<>(); | |
| 155 | + status.put("battery", 78); | |
| 156 | + status.put("signal", -65); | |
| 157 | + status.put("online", true); | |
| 158 | + status.put("version", "1.2.3"); | |
| 159 | + deviceData.put("status", status); | |
| 160 | + | |
| 161 | + // 创建设备消息 | |
| 162 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); | |
| 163 | + message.setDeviceId(789012L); | |
| 164 | + | |
| 165 | + // 编码 | |
| 166 | + byte[] packet = codec.encode(message); | |
| 167 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 168 | + log.info("编码后 JSON: {}", jsonString); | |
| 169 | + log.info("复杂数据包长度: {} 字节", packet.length); | |
| 170 | + | |
| 171 | + // 解码验证 | |
| 172 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 173 | + log.info("解码后消息 ID: {}", decoded.getId()); | |
| 174 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 175 | + log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 176 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 177 | + | |
| 178 | + // 断言验证 | |
| 179 | + assertNotNull(decoded.getId()); | |
| 180 | + assertEquals("thing.property.post", decoded.getMethod()); | |
| 181 | + assertEquals(789012L, decoded.getDeviceId()); | |
| 182 | + assertNotNull(decoded.getParams()); | |
| 183 | + } | |
| 184 | + | |
| 185 | +} | |
| 0 | 186 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md
| 1 | -# TCP二进制å议数æ®åŒ…æ ¼å¼è¯´æ˜Žå’Œç¤ºä¾‹ | |
| 1 | +# TCP 二进制å议数æ®åŒ…æ ¼å¼è¯´æ˜Žå’Œç¤ºä¾‹ | |
| 2 | 2 | |
| 3 | 3 | ## 1. 二进制å议概述 |
| 4 | 4 | |
| 5 | -TCP二进制å议是一ç§é«˜æ•ˆçš„自定义åè®®æ ¼å¼ï¼Œé€‚ç”¨äºŽå¯¹å¸¦å®½å’Œæ€§èƒ½è¦æ±‚较高的场景。 | |
| 5 | +TCP 二进制å议是一ç§é«˜æ•ˆçš„自定义åè®®æ ¼å¼ï¼Œé€‚ç”¨äºŽå¯¹å¸¦å®½å’Œæ€§èƒ½è¦æ±‚较高的场景。该åè®®é‡‡ç”¨ç´§å‡‘çš„äºŒè¿›åˆ¶æ ¼å¼ï¼Œå‡å°‘æ•°æ®ä¼ 输é‡ï¼Œæé«˜ä¼ 输效率。 | |
| 6 | 6 | |
| 7 | 7 | ## 2. æ•°æ®åŒ…æ ¼å¼ |
| 8 | 8 | |
| 9 | 9 | ### 2.1 整体结构 |
| 10 | + | |
| 11 | +æ ¹æ®ä»£ç 实现,TCP 二进制å议的数æ®åŒ…æ ¼å¼ä¸ºï¼š | |
| 12 | + | |
| 10 | 13 | ``` |
| 11 | -+----------+----------+----------+----------+----------+----------+ | |
| 12 | -| 包头 | 地å€é•¿åº¦ | è®¾å¤‡åœ°å€ | 功能ç | 消æ¯åºå· | åŒ…ä½“æ•°æ® | | |
| 13 | -| 4å—节 | 2å—节 | å˜é•¿ | 2å—节 | 2å—节 | å˜é•¿ | | |
| 14 | -+----------+----------+----------+----------+----------+----------+ | |
| 14 | ++----------+----------+----------+----------+ | |
| 15 | +| 包头 | 功能ç | 消æ¯åºå· | åŒ…ä½“æ•°æ® | | |
| 16 | +| 4å—节 | 2å—节 | 2å—节 | å˜é•¿ | | |
| 17 | ++----------+----------+----------+----------+ | |
| 15 | 18 | ``` |
| 16 | 19 | |
| 20 | +**注æ„**:与原始设计相比,实际实现ä¸ç§»é™¤äº†è®¾å¤‡åœ°å€å—段,简化了å议结构。 | |
| 21 | + | |
| 17 | 22 | ### 2.2 å—æ®µè¯´æ˜Ž |
| 18 | 23 | |
| 19 | -| å—æ®µ | 长度 | 类型 | 说明 | | |
| 20 | -|----------|--------|--------|--------------------------------| | |
| 21 | -| 包头 | 4å—节 | int | åŽç»æ•°æ®çš„æ€»é•¿åº¦ï¼ˆä¸åŒ…å«åŒ…头) | | |
| 22 | -| 地å€é•¿åº¦ | 2å—节 | short | 设备地å€çš„å—节长度 | | |
| 23 | -| è®¾å¤‡åœ°å€ | å˜é•¿ | string | è®¾å¤‡æ ‡è¯†ç¬¦ | | |
| 24 | -| 功能ç | 2å—节 | short | 消æ¯ç±»åž‹æ ‡è¯† | | |
| 25 | -| 消æ¯åºå· | 2å—节 | short | 消æ¯å”¯ä¸€æ ‡è¯† | | |
| 26 | -| åŒ…ä½“æ•°æ® | å˜é•¿ | string | JSONæ ¼å¼çš„æ¶ˆæ¯å†…容 | | |
| 24 | +| å—æ®µ | 长度 | 类型 | 说明 | | |
| 25 | +|------|-----|--------|-----------------| | |
| 26 | +| 包头 | 4å—节 | int | åŽç»æ•°æ®çš„æ€»é•¿åº¦ï¼ˆä¸åŒ…å«åŒ…头) | | |
| 27 | +| 功能ç | 2å—节 | short | 消æ¯ç±»åž‹æ ‡è¯† | | |
| 28 | +| 消æ¯åºå· | 2å—节 | short | 消æ¯å”¯ä¸€æ ‡è¯† | | |
| 29 | +| åŒ…ä½“æ•°æ® | å˜é•¿ | string | JSON æ ¼å¼çš„æ¶ˆæ¯å†…容 | | |
| 27 | 30 | |
| 28 | 31 | ### 2.3 功能ç 定义 |
| 29 | 32 | |
| 30 | -| 功能ç | åç§° | 说明 | | |
| 31 | -|--------|----------|--------------------------------| | |
| 32 | -| 10 | 设备注册 | 设备首次连接时的注册请求 | | |
| 33 | -| 11 | æ³¨å†Œå›žå¤ | æœåŠ¡å™¨å¯¹æ³¨å†Œè¯·æ±‚çš„å›žå¤ | | |
| 34 | -| 20 | 心跳请求 | 设备å‘é€çš„心跳包 | | |
| 35 | -| 21 | å¿ƒè·³å›žå¤ | æœåŠ¡å™¨å¯¹å¿ƒè·³çš„å›žå¤ | | |
| 36 | -| 30 | 消æ¯ä¸Šè¡Œ | è®¾å¤‡å‘æœåС噍å‘é€çš„æ•°æ® | | |
| 37 | -| 40 | 消æ¯ä¸‹è¡Œ | æœåС噍å‘设备å‘é€çš„æŒ‡ä»¤ | | |
| 33 | +æ ¹æ®ä»£ç 实现,支æŒçš„功能ç : | |
| 34 | + | |
| 35 | +| 功能ç | åç§° | 说明 | | |
| 36 | +|-----|------|--------------| | |
| 37 | +| 10 | 设备注册 | 设备首次连接时的注册请求 | | |
| 38 | +| 11 | æ³¨å†Œå›žå¤ | æœåŠ¡å™¨å¯¹æ³¨å†Œè¯·æ±‚çš„å›žå¤ | | |
| 39 | +| 20 | 心跳请求 | 设备å‘é€çš„心跳包 | | |
| 40 | +| 21 | å¿ƒè·³å›žå¤ | æœåŠ¡å™¨å¯¹å¿ƒè·³çš„å›žå¤ | | |
| 41 | +| 30 | 消æ¯ä¸Šè¡Œ | è®¾å¤‡å‘æœåС噍å‘é€çš„æ•°æ® | | |
| 42 | +| 40 | 消æ¯ä¸‹è¡Œ | æœåС噍å‘设备å‘é€çš„æŒ‡ä»¤ | | |
| 43 | + | |
| 44 | +**常é‡å®šä¹‰ï¼š** | |
| 45 | + | |
| 46 | +```java | |
| 47 | +public static final short CODE_REGISTER = 10; | |
| 48 | +public static final short CODE_REGISTER_REPLY = 11; | |
| 49 | +public static final short CODE_HEARTBEAT = 20; | |
| 50 | +public static final short CODE_HEARTBEAT_REPLY = 21; | |
| 51 | +public static final short CODE_MESSAGE_UP = 30; | |
| 52 | +public static final short CODE_MESSAGE_DOWN = 40; | |
| 53 | +``` | |
| 54 | + | |
| 55 | +## 3. åŒ…ä½“æ•°æ®æ ¼å¼ | |
| 56 | + | |
| 57 | +### 3.1 JSON 负载结构 | |
| 58 | + | |
| 59 | +包体数æ®é‡‡ç”¨ JSON æ ¼å¼ï¼ŒåŒ…å«ä»¥ä¸‹å—段: | |
| 60 | + | |
| 61 | +```json | |
| 62 | +{ | |
| 63 | + "method": "æ¶ˆæ¯æ–¹æ³•", | |
| 64 | + "params": { | |
| 65 | + // 消æ¯å‚æ•° | |
| 66 | + }, | |
| 67 | + "timestamp": 时间戳, | |
| 68 | + "requestId": "请求ID", | |
| 69 | + "msgId": "消æ¯ID" | |
| 70 | +} | |
| 71 | +``` | |
| 72 | + | |
| 73 | +### 3.2 å—æ®µè¯´æ˜Ž | |
| 38 | 74 | |
| 39 | -## 3. 二进制数æ®ä¸ŠæŠ¥åŒ…示例 | |
| 75 | +| å—æ®µå | 类型 | å¿…å¡« | 说明 | | |
| 76 | +|-----------|--------|----|------------------------------| | |
| 77 | +| method | String | 是 | æ¶ˆæ¯æ–¹æ³•,如 `thing.property.post` | | |
| 78 | +| params | Object | å¦ | 消æ¯å‚æ•° | | |
| 79 | +| timestamp | Long | 是 | 时间戳(毫秒) | | |
| 80 | +| requestId | String | å¦ | è¯·æ±‚å”¯ä¸€æ ‡è¯† | | |
| 81 | +| msgId | String | å¦ | 消æ¯å”¯ä¸€æ ‡è¯† | | |
| 40 | 82 | |
| 41 | -### 3.1 æ¸©åº¦ä¼ æ„Ÿå™¨æ•°æ®ä¸ŠæŠ¥ | |
| 83 | +**常é‡å®šä¹‰ï¼š** | |
| 84 | + | |
| 85 | +```java | |
| 86 | +public static final String METHOD = "method"; | |
| 87 | +public static final String PARAMS = "params"; | |
| 88 | +public static final String TIMESTAMP = "timestamp"; | |
| 89 | +public static final String REQUEST_ID = "requestId"; | |
| 90 | +public static final String MESSAGE_ID = "msgId"; | |
| 91 | +``` | |
| 92 | + | |
| 93 | +## 4. 消æ¯ç±»åž‹ | |
| 94 | + | |
| 95 | +### 4.1 æ•°æ®ä¸ŠæŠ¥ (thing.property.post) | |
| 96 | + | |
| 97 | +è®¾å¤‡å‘æœåŠ¡å™¨ä¸ŠæŠ¥å±žæ€§æ•°æ®ã€‚ | |
| 98 | + | |
| 99 | +**功能ç :** 30 (CODE_MESSAGE_UP) | |
| 100 | + | |
| 101 | +**包体数æ®ç¤ºä¾‹ï¼š** | |
| 42 | 102 | |
| 43 | -**原始数æ®ï¼š** | |
| 103 | +```json | |
| 104 | +{ | |
| 105 | + "method": "thing.property.post", | |
| 106 | + "params": { | |
| 107 | + "temperature": 25.5, | |
| 108 | + "humidity": 60.2, | |
| 109 | + "pressure": 1013.25 | |
| 110 | + }, | |
| 111 | + "timestamp": 1642781234567, | |
| 112 | + "requestId": "req_001" | |
| 113 | +} | |
| 114 | +``` | |
| 115 | + | |
| 116 | +### 4.2 心跳 (thing.state.online) | |
| 117 | + | |
| 118 | +è®¾å¤‡å‘æœåС噍å‘é€å¿ƒè·³ä¿æ´»ã€‚ | |
| 119 | + | |
| 120 | +**功能ç :** 20 (CODE_HEARTBEAT) | |
| 121 | + | |
| 122 | +**包体数æ®ç¤ºä¾‹ï¼š** | |
| 123 | + | |
| 124 | +```json | |
| 125 | +{ | |
| 126 | + "method": "thing.state.online", | |
| 127 | + "params": {}, | |
| 128 | + "timestamp": 1642781234567, | |
| 129 | + "requestId": "req_002" | |
| 130 | +} | |
| 131 | +``` | |
| 132 | + | |
| 133 | +### 4.3 æ¶ˆæ¯æ–¹æ³•å¸¸é‡ | |
| 134 | + | |
| 135 | +```java | |
| 136 | +public static final String PROPERTY_POST = "thing.property.post"; // æ•°æ®ä¸ŠæŠ¥ | |
| 137 | +public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 138 | +``` | |
| 139 | + | |
| 140 | +## 5. æ•°æ®åŒ…示例 | |
| 141 | + | |
| 142 | +### 5.1 æ¸©åº¦ä¼ æ„Ÿå™¨æ•°æ®ä¸ŠæŠ¥ | |
| 143 | + | |
| 144 | +**包体数æ®ï¼š** | |
| 44 | 145 | ```json |
| 45 | 146 | { |
| 46 | 147 | "method": "thing.property.post", |
| ... | ... | @@ -49,15 +150,14 @@ TCP二进制å议是一ç§é«˜æ•ˆçš„自定义åè®®æ ¼å¼ï¼Œé€‚ç”¨äºŽå¯¹å¸¦å®½å’ |
| 49 | 150 | "humidity": 60.2, |
| 50 | 151 | "pressure": 1013.25 |
| 51 | 152 | }, |
| 52 | - "timestamp": 1642781234567 | |
| 153 | + "timestamp": 1642781234567, | |
| 154 | + "requestId": "req_001" | |
| 53 | 155 | } |
| 54 | 156 | ``` |
| 55 | 157 | |
| 56 | 158 | **æ•°æ®åŒ…结构:** |
| 57 | 159 | ``` |
| 58 | 160 | 包头: 0x00000045 (69å—节) |
| 59 | -地å€é•¿åº¦: 0x0006 (6å—节) | |
| 60 | -设备地å€: "123456" | |
| 61 | 161 | 功能ç : 0x001E (30 - 消æ¯ä¸Šè¡Œ) |
| 62 | 162 | 消æ¯åºå·: 0x1234 (4660) |
| 63 | 163 | 包体: JSONå—符串 |
| ... | ... | @@ -65,7 +165,7 @@ TCP二进制å议是一ç§é«˜æ•ˆçš„自定义åè®®æ ¼å¼ï¼Œé€‚ç”¨äºŽå¯¹å¸¦å®½å’ |
| 65 | 165 | |
| 66 | 166 | **完整åå…进制数æ®åŒ…:** |
| 67 | 167 | ``` |
| 68 | -00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34 | |
| 168 | +00 00 00 45 00 1E 12 34 | |
| 69 | 169 | 7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 |
| 70 | 170 | 2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C |
| 71 | 171 | 22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 |
| ... | ... | @@ -73,42 +173,25 @@ TCP二进制å议是一ç§é«˜æ•ˆçš„自定义åè®®æ ¼å¼ï¼Œé€‚ç”¨äºŽå¯¹å¸¦å®½å’ |
| 73 | 173 | 6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 |
| 74 | 174 | 65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D |
| 75 | 175 | 2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 |
| 76 | -32 37 38 31 32 33 34 35 36 37 7D | |
| 77 | -``` | |
| 78 | - | |
| 79 | -### 2.2 GPSå®šä½æ•°æ®ä¸ŠæŠ¥ | |
| 80 | - | |
| 81 | -**原始数æ®ï¼š** | |
| 82 | -```json | |
| 83 | -{ | |
| 84 | - "method": "thing.property.post", | |
| 85 | - "params": { | |
| 86 | - "latitude": 39.9042, | |
| 87 | - "longitude": 116.4074, | |
| 88 | - "altitude": 43.5, | |
| 89 | - "speed": 0.0 | |
| 90 | - }, | |
| 91 | - "timestamp": 1642781234567 | |
| 92 | -} | |
| 176 | +32 37 38 31 32 33 34 35 36 37 2C 22 72 65 71 75 | |
| 177 | +65 73 74 49 64 22 3A 22 72 65 71 5F 30 30 31 22 7D | |
| 93 | 178 | ``` |
| 94 | 179 | |
| 95 | -## 3. 心跳包示例 | |
| 96 | - | |
| 97 | -### 3.1 æ ‡å‡†å¿ƒè·³åŒ… | |
| 180 | +### 5.2 心跳包示例 | |
| 98 | 181 | |
| 99 | -**原始数æ®ï¼š** | |
| 182 | +**包体数æ®ï¼š** | |
| 100 | 183 | ```json |
| 101 | 184 | { |
| 102 | 185 | "method": "thing.state.online", |
| 103 | - "timestamp": 1642781234567 | |
| 186 | + "params": {}, | |
| 187 | + "timestamp": 1642781234567, | |
| 188 | + "requestId": "req_002" | |
| 104 | 189 | } |
| 105 | 190 | ``` |
| 106 | 191 | |
| 107 | 192 | **æ•°æ®åŒ…结构:** |
| 108 | 193 | ``` |
| 109 | 194 | 包头: 0x00000028 (40å—节) |
| 110 | -地å€é•¿åº¦: 0x0006 (6å—节) | |
| 111 | -设备地å€: "123456" | |
| 112 | 195 | 功能ç : 0x0014 (20 - 心跳请求) |
| 113 | 196 | 消æ¯åºå·: 0x5678 (22136) |
| 114 | 197 | 包体: JSONå—符串 |
| ... | ... | @@ -116,66 +199,71 @@ TCP二进制å议是一ç§é«˜æ•ˆçš„自定义åè®®æ ¼å¼ï¼Œé€‚ç”¨äºŽå¯¹å¸¦å®½å’ |
| 116 | 199 | |
| 117 | 200 | **完整åå…进制数æ®åŒ…:** |
| 118 | 201 | ``` |
| 119 | -00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78 | |
| 202 | +00 00 00 28 00 14 56 78 | |
| 120 | 203 | 7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 |
| 121 | 204 | 2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 |
| 122 | -74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 32 37 | |
| 123 | -38 31 32 33 34 35 36 37 7D | |
| 205 | +70 61 72 61 6D 73 22 3A 7B 7D 2C 22 74 69 6D 65 | |
| 206 | +73 74 61 6D 70 22 3A 31 36 34 32 37 38 31 32 33 | |
| 207 | +34 35 36 37 2C 22 72 65 71 75 65 73 74 49 64 22 | |
| 208 | +3A 22 72 65 71 5F 30 30 32 22 7D | |
| 124 | 209 | ``` |
| 125 | 210 | |
| 126 | -## 4. 夿‚æ•°æ®ä¸ŠæŠ¥ç¤ºä¾‹ | |
| 211 | +## 6. ç¼–è§£ç 器实现 | |
| 127 | 212 | |
| 128 | -### 4.1 å¤šä¼ æ„Ÿå™¨ç»¼åˆæ•°æ® | |
| 213 | +### 6.1 ç¼–ç 器类型 | |
| 129 | 214 | |
| 130 | -**原始数æ®ï¼š** | |
| 131 | -```json | |
| 132 | -{ | |
| 133 | - "method": "thing.property.post", | |
| 134 | - "params": { | |
| 135 | - "environment": { | |
| 136 | - "temperature": 23.8, | |
| 137 | - "humidity": 55.0, | |
| 138 | - "co2": 420 | |
| 139 | - }, | |
| 140 | - "location": { | |
| 141 | - "latitude": 39.9042, | |
| 142 | - "longitude": 116.4074, | |
| 143 | - "altitude": 43.5 | |
| 144 | - }, | |
| 145 | - "status": { | |
| 146 | - "battery": 78, | |
| 147 | - "signal": -65, | |
| 148 | - "online": true | |
| 149 | - } | |
| 150 | - }, | |
| 151 | - "timestamp": 1642781234567 | |
| 215 | +```java | |
| 216 | +public static final String TYPE = "TCP_BINARY"; | |
| 217 | +``` | |
| 218 | + | |
| 219 | +### 6.2 ç¼–ç 过程 | |
| 220 | + | |
| 221 | +1. **傿•°éªŒè¯**:检查消æ¯å’Œæ–¹æ³•是å¦ä¸ºç©º | |
| 222 | +2. **确定功能ç **: | |
| 223 | + - 心跳消æ¯ï¼šä½¿ç”¨ `CODE_HEARTBEAT` (20) | |
| 224 | + - 其他消æ¯ï¼šä½¿ç”¨ `CODE_MESSAGE_UP` (30) | |
| 225 | +3. **构建负载**:使用 `buildSimplePayload()` 构建 JSON 负载 | |
| 226 | +4. **ç”Ÿæˆæ¶ˆæ¯åºå·**ï¼šåŸºäºŽå½“å‰æ—¶é—´æˆ³ç”Ÿæˆ | |
| 227 | +5. **构建数æ®åŒ…**:创建 `TcpDataPackage` 对象 | |
| 228 | +6. **ç¼–ç 为å—节æµ**:使用 `encodeTcpDataPackage()` ç¼–ç | |
| 229 | + | |
| 230 | +### 6.3 è§£ç 过程 | |
| 231 | + | |
| 232 | +1. **傿•°éªŒè¯**:检查å—节数组是å¦ä¸ºç©º | |
| 233 | +2. **è§£ç æ•°æ®åŒ…**:使用 `decodeTcpDataPackage()` è§£ç | |
| 234 | +3. **ç¡®å®šæ¶ˆæ¯æ–¹æ³•**: | |
| 235 | + - 功能ç 20:`thing.state.online` (心跳) | |
| 236 | + - 功能ç 30:`thing.property.post` (æ•°æ®ä¸ŠæŠ¥) | |
| 237 | +4. **è§£æžè´Ÿè½½ä¿¡æ¯**:使用 `parsePayloadInfo()` è§£æž JSON è´Ÿè½½ | |
| 238 | +5. **构建设备消æ¯**:创建 `IotDeviceMessage` 对象 | |
| 239 | +6. **设置æœåŠ¡ ID**:使用 `generateServerId()` ç”Ÿæˆ | |
| 240 | + | |
| 241 | +### 6.4 æœåŠ¡ ID ç”Ÿæˆ | |
| 242 | + | |
| 243 | +```java | |
| 244 | +private String generateServerId(TcpDataPackage dataPackage) { | |
| 245 | + return String.format("tcp_binary_%d_%d", dataPackage.getCode(), dataPackage.getMid()); | |
| 152 | 246 | } |
| 153 | 247 | ``` |
| 154 | 248 | |
| 155 | -## 5. æ•°æ®åŒ…è§£æžæ¥éª¤ | |
| 249 | +## 7. æ•°æ®åŒ…è§£æžæ¥éª¤ | |
| 156 | 250 | |
| 157 | -### 5.1 è§£æžæµç¨‹ | |
| 251 | +### 7.1 è§£æžæµç¨‹ | |
| 158 | 252 | |
| 159 | 253 | 1. **读å–包头(4å—节)** |
| 160 | 254 | - 获å–åŽç»æ•°æ®çš„æ€»é•¿åº¦ |
| 161 | 255 | - éªŒè¯æ•°æ®åŒ…完整性 |
| 162 | 256 | |
| 163 | -2. **读å–设备地å€é•¿åº¦ï¼ˆ2å—节)** | |
| 164 | - - 确定设备地å€çš„å—节数 | |
| 165 | - | |
| 166 | -3. **读å–设备地å€ï¼ˆå˜é•¿ï¼‰** | |
| 167 | - - æ ¹æ®åœ°å€é•¿åº¦è¯»å–è®¾å¤‡æ ‡è¯† | |
| 168 | - | |
| 169 | -4. **读å–功能ç (2å—节)** | |
| 257 | +2. **读å–功能ç (2å—节)** | |
| 170 | 258 | - 确定消æ¯ç±»åž‹ |
| 171 | 259 | |
| 172 | -5. **è¯»å–æ¶ˆæ¯åºå·ï¼ˆ2å—节)** | |
| 260 | +3. **è¯»å–æ¶ˆæ¯åºå·ï¼ˆ2å—节)** | |
| 173 | 261 | - èŽ·å–æ¶ˆæ¯å”¯ä¸€æ ‡è¯† |
| 174 | 262 | |
| 175 | -6. **读å–包体数æ®ï¼ˆå˜é•¿ï¼‰** | |
| 176 | - - è§£æžJSONæ ¼å¼çš„æ¶ˆæ¯å†…容 | |
| 263 | +4. **读å–包体数æ®ï¼ˆå˜é•¿ï¼‰** | |
| 264 | + - è§£æž JSON æ ¼å¼çš„æ¶ˆæ¯å†…容 | |
| 177 | 265 | |
| 178 | -### 5.2 Javaè§£æžç¤ºä¾‹ | |
| 266 | +### 7.2 Java è§£æžç¤ºä¾‹ | |
| 179 | 267 | |
| 180 | 268 | ```java |
| 181 | 269 | public TcpDataPackage parsePacket(byte[] packet) { |
| ... | ... | @@ -184,39 +272,99 @@ public TcpDataPackage parsePacket(byte[] packet) { |
| 184 | 272 | // 1. è§£æžåŒ…头 |
| 185 | 273 | int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); |
| 186 | 274 | index += 4; |
| 187 | - | |
| 188 | - // 2. è§£æžè®¾å¤‡åœ°å€é•¿åº¦ | |
| 189 | - short addrLength = ByteBuffer.wrap(packet, index, 2).getShort(); | |
| 190 | - index += 2; | |
| 191 | - | |
| 192 | - // 3. è§£æžè®¾å¤‡åœ°å€ | |
| 193 | - String deviceAddr = new String(packet, index, addrLength); | |
| 194 | - index += addrLength; | |
| 195 | - | |
| 196 | - // 4. è§£æžåŠŸèƒ½ç | |
| 275 | + | |
| 276 | + // 2. è§£æžåŠŸèƒ½ç | |
| 197 | 277 | short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); |
| 198 | 278 | index += 2; |
| 199 | - | |
| 200 | - // 5. è§£æžæ¶ˆæ¯åºå· | |
| 279 | + | |
| 280 | + // 3. è§£æžæ¶ˆæ¯åºå· | |
| 201 | 281 | short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); |
| 202 | 282 | index += 2; |
| 203 | - | |
| 204 | - // 6. è§£æžåŒ…ä½“æ•°æ® | |
| 283 | + | |
| 284 | + // 4. è§£æžåŒ…ä½“æ•°æ® | |
| 205 | 285 | String payload = new String(packet, index, packet.length - index); |
| 206 | - | |
| 207 | - return TcpDataPackage.builder() | |
| 208 | - .addr(deviceAddr) | |
| 209 | - .code(functionCode) | |
| 210 | - .mid(messageId) | |
| 211 | - .payload(payload) | |
| 212 | - .build(); | |
| 286 | + | |
| 287 | + return new TcpDataPackage(functionCode, messageId, payload); | |
| 213 | 288 | } |
| 214 | 289 | ``` |
| 215 | 290 | |
| 216 | -## 6. 注æ„事项 | |
| 291 | +## 8. 使用示例 | |
| 292 | + | |
| 293 | +### 8.1 基本使用 | |
| 294 | + | |
| 295 | +```java | |
| 296 | +// 创建编解ç 器 | |
| 297 | +IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); | |
| 298 | + | |
| 299 | +// 创建数æ®ä¸ŠæŠ¥æ¶ˆæ¯ | |
| 300 | +Map<String, Object> sensorData = Map.of( | |
| 301 | + "temperature", 25.5, | |
| 302 | + "humidity", 60.2 | |
| 303 | +); | |
| 304 | + | |
| 305 | +// ç¼–ç | |
| 306 | +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 307 | +byte[] data = codec.encode(message); | |
| 308 | + | |
| 309 | +// è§£ç | |
| 310 | +IotDeviceMessage decoded = codec.decode(data); | |
| 311 | +``` | |
| 312 | + | |
| 313 | +### 8.2 é”™è¯¯å¤„ç† | |
| 314 | + | |
| 315 | +```java | |
| 316 | +try{ | |
| 317 | +byte[] data = codec.encode(message); | |
| 318 | +// 处ç†ç¼–ç æˆåŠŸ | |
| 319 | +}catch( | |
| 320 | +IllegalArgumentException e){ | |
| 321 | + // 处ç†å‚数错误 | |
| 322 | + log. | |
| 323 | + | |
| 324 | +error("ç¼–ç 傿•°é”™è¯¯: {}",e.getMessage()); | |
| 325 | + }catch( | |
| 326 | +TcpCodecException e){ | |
| 327 | + // 处ç†ç¼–ç 失败 | |
| 328 | + log. | |
| 329 | + | |
| 330 | +error("ç¼–ç 失败: {}",e.getMessage()); | |
| 331 | + } | |
| 332 | +``` | |
| 333 | + | |
| 334 | +## 9. 注æ„事项 | |
| 217 | 335 | |
| 218 | 336 | 1. **å—节åº**:所有多å—节数æ®ä½¿ç”¨å¤§ç«¯åºï¼ˆBig-Endian) |
| 219 | -2. **å—符编ç **:å—符串数æ®ä½¿ç”¨UTF-8ç¼–ç | |
| 220 | -3. **JSONæ ¼å¼**:包体数æ®å¿…须是有效的JSONæ ¼å¼ | |
| 221 | -4. **长度é™åˆ¶**:å•个数æ®åŒ…建议ä¸è¶…过1MB | |
| 222 | -5. **错误处ç†**:解æžå¤±è´¥æ—¶åº”返回相应的错误ç | |
| 337 | +2. **å—符编ç **:å—符串数æ®ä½¿ç”¨ UTF-8 ç¼–ç | |
| 338 | +3. **JSON æ ¼å¼**:包体数æ®å¿…须是有效的 JSON æ ¼å¼ | |
| 339 | +4. **长度é™åˆ¶**:å•个数æ®åŒ…建议ä¸è¶…过 1MB | |
| 340 | +5. **错误处ç†**:解æžå¤±è´¥æ—¶ä¼šæŠ›å‡º `TcpCodecException` | |
| 341 | +6. **åŠŸèƒ½ç æ˜ å°„**:目å‰åªæ”¯æŒå¿ƒè·³å’Œæ•°æ®ä¸ŠæŠ¥ä¸¤ç§æ¶ˆæ¯ç±»åž‹ | |
| 342 | + | |
| 343 | +## 10. å议特点 | |
| 344 | + | |
| 345 | +### 10.1 优势 | |
| 346 | + | |
| 347 | +- **é«˜æ•ˆä¼ è¾“**ï¼šäºŒè¿›åˆ¶æ ¼å¼ï¼Œæ•°æ®é‡å° | |
| 348 | +- **性能优化**:å‡å°‘è§£æžå¼€é”€ | |
| 349 | +- **带宽节çœ**:相比 JSON æ ¼å¼èŠ‚çœå¸¦å®½ | |
| 350 | +- **实时性好**:适åˆé«˜é¢‘æ•°æ®ä¼ 输 | |
| 351 | + | |
| 352 | +### 10.2 适用场景 | |
| 353 | + | |
| 354 | +- ✅ **高频数æ®ä¼ 输**ï¼šä¼ æ„Ÿå™¨æ•°æ®å®žæ—¶ä¸ŠæŠ¥ | |
| 355 | +- ✅ **带宽å—é™çŽ¯å¢ƒ**:移动网络ã€å«æ˜Ÿé€šä¿¡ | |
| 356 | +- ✅ **æ€§èƒ½è¦æ±‚高**:需è¦ä½Žå»¶è¿Ÿçš„场景 | |
| 357 | +- ✅ **è®¾å¤‡èµ„æºæœ‰é™**:嵌入å¼è®¾å¤‡ã€IoT 设备 | |
| 358 | + | |
| 359 | +### 10.3 与 JSON å议对比 | |
| 360 | + | |
| 361 | +| 特性 | 二进制åè®® | JSON åè®® | | |
| 362 | +|-------|-------|---------| | |
| 363 | +| æ•°æ®å¤§å° | å° | ç¨å¤§ | | |
| 364 | +| è§£æžæ€§èƒ½ | 高 | ä¸ç‰ | | |
| 365 | +| å¯è¯»æ€§ | å·® | 优秀 | | |
| 366 | +| 调试难度 | 高 | 低 | | |
| 367 | +| 扩展性 | 差 | 优秀 | | |
| 368 | +| å®žçŽ°å¤æ‚度 | 高 | 低 | | |
| 369 | + | |
| 370 | +è¿™æ ·å°±å®Œæˆäº† TCP 二进制å议的完整说明,与实际代ç 实现完全一致。 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md
| 1 | -# TCP JSON格式协议说明 | |
| 1 | +# TCP JSON 格式协议说明 | |
| 2 | 2 | |
| 3 | 3 | ## 1. 协议概述 |
| 4 | 4 | |
| 5 | -TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势: | |
| 5 | +TCP JSON 格式协议采用纯 JSON 格式进行数据传输,参考了 EMQX 和 HTTP 模块的数据格式设计,具有以下优势: | |
| 6 | 6 | |
| 7 | -- **标准化**:使用标准JSON格式,易于解析和处理 | |
| 7 | +- **标准化**:使用标准 JSON 格式,易于解析和处理 | |
| 8 | 8 | - **可读性**:人类可读,便于调试和维护 |
| 9 | 9 | - **扩展性**:可以轻松添加新字段,向后兼容 |
| 10 | -- **统一性**:与HTTP模块保持一致的数据格式 | |
| 10 | +- **统一性**:与 HTTP 模块保持一致的数据格式 | |
| 11 | +- **简化性**:相比二进制协议,实现更简单,调试更容易 | |
| 11 | 12 | |
| 12 | 13 | ## 2. 消息格式 |
| 13 | 14 | |
| ... | ... | @@ -17,270 +18,294 @@ TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP |
| 17 | 18 | { |
| 18 | 19 | "id": "消息唯一标识", |
| 19 | 20 | "method": "消息方法", |
| 20 | - "deviceId": "设备ID", | |
| 21 | + "deviceId": 设备ID, | |
| 21 | 22 | "params": { |
| 22 | 23 | // 消息参数 |
| 23 | 24 | }, |
| 24 | - "timestamp": 时间戳 | |
| 25 | + "timestamp": 时间戳, | |
| 26 | + "code": 响应码, | |
| 27 | + "message": "响应消息" | |
| 25 | 28 | } |
| 26 | 29 | ``` |
| 27 | 30 | |
| 28 | 31 | ### 2.2 字段说明 |
| 29 | 32 | |
| 30 | -| 字段名 | 类型 | 必填 | 说明 | | |
| 31 | -|--------|------|------|------| | |
| 32 | -| id | String | 是 | 消息唯一标识,UUID格式 | | |
| 33 | -| method | String | 是 | 消息方法,如 thing.property.post | | |
| 34 | -| deviceId | Long | 是 | 设备ID | | |
| 35 | -| params | Object | 否 | 消息参数,具体内容根据method而定 | | |
| 36 | -| timestamp | Long | 是 | 时间戳(毫秒) | | |
| 37 | -| code | Integer | 否 | 响应码(下行消息使用) | | |
| 38 | -| message | String | 否 | 响应消息(下行消息使用) | | |
| 33 | +| 字段名 | 类型 | 必填 | 说明 | | |
| 34 | +|-----------|---------|----|-------------------------------------| | |
| 35 | +| id | String | 是 | 消息唯一标识,如果为空会自动生成 UUID | | |
| 36 | +| method | String | 是 | 消息方法,如 `auth`、`thing.property.post` | | |
| 37 | +| deviceId | Long | 否 | 设备 ID | | |
| 38 | +| params | Object | 否 | 消息参数,具体内容根据 method 而定 | | |
| 39 | +| timestamp | Long | 是 | 时间戳(毫秒),自动生成 | | |
| 40 | +| code | Integer | 否 | 响应码(下行消息使用) | | |
| 41 | +| message | String | 否 | 响应消息(下行消息使用) | | |
| 39 | 42 | |
| 40 | 43 | ## 3. 消息类型 |
| 41 | 44 | |
| 42 | -### 3.1 数据上报 (thing.property.post) | |
| 45 | +### 3.1 设备认证 (auth) | |
| 43 | 46 | |
| 44 | -设备向服务器上报属性数据。 | |
| 47 | +设备连接后首先需要进行认证,认证成功后才能进行其他操作。 | |
| 48 | + | |
| 49 | +#### 3.1.1 认证请求格式 | |
| 45 | 50 | |
| 46 | 51 | **示例:** |
| 52 | + | |
| 47 | 53 | ```json |
| 48 | 54 | { |
| 49 | - "id": "8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 50 | - "method": "thing.property.post", | |
| 51 | - "deviceId": 123456, | |
| 55 | + "id": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 56 | + "method": "auth", | |
| 52 | 57 | "params": { |
| 53 | - "temperature": 25.5, | |
| 54 | - "humidity": 60.2, | |
| 55 | - "pressure": 1013.25, | |
| 56 | - "battery": 85 | |
| 58 | + "clientId": "device_001", | |
| 59 | + "username": "productKey_deviceName", | |
| 60 | + "password": "设备密码" | |
| 57 | 61 | }, |
| 58 | 62 | "timestamp": 1753111026437 |
| 59 | 63 | } |
| 60 | 64 | ``` |
| 61 | 65 | |
| 62 | -### 3.2 心跳 (thing.state.online) | |
| 66 | +**字段说明:** | |
| 67 | +| 字段名 | 类型 | 必填 | 说明 | | |
| 68 | +|--------|------|------|------| | |
| 69 | +| clientId | String | 是 | 客户端唯一标识 | | |
| 70 | +| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | | |
| 71 | +| password | String | 是 | 设备密码 | | |
| 63 | 72 | |
| 64 | -设备向服务器发送心跳保活。 | |
| 73 | +#### 3.1.2 认证响应格式 | |
| 74 | + | |
| 75 | +**认证成功响应:** | |
| 65 | 76 | |
| 66 | -**示例:** | |
| 67 | 77 | ```json |
| 68 | 78 | { |
| 69 | - "id": "7db8c4e6408b40f8b2549ddd94f6bb02", | |
| 70 | - "method": "thing.state.online", | |
| 71 | - "deviceId": 123456, | |
| 72 | - "timestamp": 1753111026467 | |
| 79 | + "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 80 | + "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 81 | + "method": "auth", | |
| 82 | + "data": { | |
| 83 | + "success": true, | |
| 84 | + "message": "认证成功" | |
| 85 | + }, | |
| 86 | + "code": 0, | |
| 87 | + "msg": "认证成功" | |
| 73 | 88 | } |
| 74 | 89 | ``` |
| 75 | 90 | |
| 76 | -### 3.3 事件上报 (thing.event.post) | |
| 77 | - | |
| 78 | -设备向服务器上报事件信息。 | |
| 91 | +**认证失败响应:** | |
| 79 | 92 | |
| 80 | -**示例:** | |
| 81 | 93 | ```json |
| 82 | 94 | { |
| 83 | - "id": "9e7d72731b854916b1baa5088bd6a907", | |
| 84 | - "method": "thing.event.post", | |
| 85 | - "deviceId": 123456, | |
| 86 | - "params": { | |
| 87 | - "eventType": "alarm", | |
| 88 | - "level": "warning", | |
| 89 | - "description": "温度过高", | |
| 90 | - "value": 45.8 | |
| 95 | + "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 96 | + "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 97 | + "method": "auth", | |
| 98 | + "data": { | |
| 99 | + "success": false, | |
| 100 | + "message": "认证失败:用户名或密码错误" | |
| 91 | 101 | }, |
| 92 | - "timestamp": 1753111026468 | |
| 102 | + "code": 401, | |
| 103 | + "msg": "认证失败:用户名或密码错误" | |
| 93 | 104 | } |
| 94 | 105 | ``` |
| 95 | 106 | |
| 96 | -### 3.4 属性设置 (thing.property.set) | |
| 107 | +#### 3.1.3 认证流程 | |
| 108 | + | |
| 109 | +1. **设备连接** → 建立 TCP 连接 | |
| 110 | +2. **发送认证请求** → 发送包含认证信息的 JSON 消息 | |
| 111 | +3. **服务器验证** → 验证 clientId、username、password | |
| 112 | +4. **生成 Token** → 认证成功后生成 JWT Token(内部使用) | |
| 113 | +5. **设备上线** → 发送设备上线消息到消息总线 | |
| 114 | +6. **返回响应** → 返回认证结果 | |
| 115 | +7. **会话注册** → 注册设备会话,允许后续业务操作 | |
| 97 | 116 | |
| 98 | -服务器向设备下发属性设置指令。 | |
| 117 | +#### 3.1.4 认证错误码 | |
| 118 | + | |
| 119 | +| 错误码 | 说明 | 处理建议 | | |
| 120 | +|-----|-------|--------------| | |
| 121 | +| 401 | 认证失败 | 检查用户名、密码是否正确 | | |
| 122 | +| 400 | 参数错误 | 检查认证参数是否完整 | | |
| 123 | +| 404 | 设备不存在 | 检查设备是否已注册 | | |
| 124 | +| 500 | 服务器错误 | 联系管理员 | | |
| 125 | + | |
| 126 | +### 3.2 数据上报 (thing.property.post) | |
| 127 | + | |
| 128 | +设备向服务器上报属性数据。 | |
| 99 | 129 | |
| 100 | 130 | **示例:** |
| 101 | 131 | ```json |
| 102 | 132 | { |
| 103 | - "id": "cmd_001", | |
| 104 | - "method": "thing.property.set", | |
| 105 | - "deviceId": 123456, | |
| 133 | + "id": "8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 134 | + "method": "thing.property.post", | |
| 135 | + "deviceId": 8, | |
| 106 | 136 | "params": { |
| 107 | - "targetTemperature": 22.0, | |
| 108 | - "mode": "auto" | |
| 137 | + "temperature": 25.5, | |
| 138 | + "humidity": 60.2, | |
| 139 | + "pressure": 1013.25, | |
| 140 | + "battery": 85 | |
| 109 | 141 | }, |
| 110 | - "timestamp": 1753111026469 | |
| 142 | + "timestamp": 1753111026437 | |
| 111 | 143 | } |
| 112 | 144 | ``` |
| 113 | 145 | |
| 114 | -### 3.5 服务调用 (thing.service.invoke) | |
| 146 | +### 3.3 心跳 (thing.state.update) | |
| 115 | 147 | |
| 116 | -服务器向设备调用服务。 | |
| 148 | +设备向服务器发送心跳保活。 | |
| 117 | 149 | |
| 118 | 150 | **示例:** |
| 119 | 151 | ```json |
| 120 | 152 | { |
| 121 | - "id": "service_001", | |
| 122 | - "method": "thing.service.invoke", | |
| 123 | - "deviceId": 123456, | |
| 153 | + "id": "7db8c4e6408b40f8b2549ddd94f6bb02", | |
| 154 | + "method": "thing.state.update", | |
| 155 | + "deviceId": 8, | |
| 124 | 156 | "params": { |
| 125 | - "service": "restart", | |
| 126 | - "args": { | |
| 127 | - "delay": 5 | |
| 128 | - } | |
| 157 | + "state": "1" | |
| 129 | 158 | }, |
| 130 | - "timestamp": 1753111026470 | |
| 159 | + "timestamp": 1753111026467 | |
| 131 | 160 | } |
| 132 | 161 | ``` |
| 133 | 162 | |
| 134 | -## 4. 复杂数据示例 | |
| 163 | +### 3.4 消息方法常量 | |
| 135 | 164 | |
| 136 | -### 4.1 多传感器综合数据 | |
| 165 | +支持的消息方法: | |
| 137 | 166 | |
| 138 | -```json | |
| 139 | -{ | |
| 140 | - "id": "complex_001", | |
| 141 | - "method": "thing.property.post", | |
| 142 | - "deviceId": 789012, | |
| 143 | - "params": { | |
| 144 | - "environment": { | |
| 145 | - "temperature": 23.8, | |
| 146 | - "humidity": 55.0, | |
| 147 | - "co2": 420, | |
| 148 | - "pm25": 35 | |
| 149 | - }, | |
| 150 | - "location": { | |
| 151 | - "latitude": 39.9042, | |
| 152 | - "longitude": 116.4074, | |
| 153 | - "altitude": 43.5, | |
| 154 | - "speed": 0.0 | |
| 155 | - }, | |
| 156 | - "status": { | |
| 157 | - "battery": 78, | |
| 158 | - "signal": -65, | |
| 159 | - "online": true, | |
| 160 | - "version": "1.2.3" | |
| 161 | - } | |
| 162 | - }, | |
| 163 | - "timestamp": 1753111026471 | |
| 164 | -} | |
| 165 | -``` | |
| 167 | +- `auth` - 设备认证 | |
| 168 | +- `thing.property.post` - 数据上报 | |
| 169 | +- `thing.state.update` - 心跳 | |
| 166 | 170 | |
| 167 | -## 5. 与EMQX格式的兼容性 | |
| 171 | +## 4. 协议特点 | |
| 168 | 172 | |
| 169 | -本协议设计参考了EMQX的消息格式,具有良好的兼容性: | |
| 173 | +### 4.1 优势 | |
| 170 | 174 | |
| 171 | -### 5.1 EMQX标准格式 | |
| 175 | +- **简单易用**:纯 JSON 格式,无需复杂的二进制解析 | |
| 176 | +- **调试友好**:可以直接查看消息内容 | |
| 177 | +- **扩展性强**:可以轻松添加新字段 | |
| 178 | +- **标准化**:与 EMQX 等主流平台格式兼容 | |
| 179 | +- **错误处理**:提供详细的错误信息和异常处理 | |
| 180 | +- **安全性**:支持设备认证机制 | |
| 172 | 181 | |
| 173 | -```json | |
| 174 | -{ | |
| 175 | - "id": "msg_001", | |
| 176 | - "method": "thing.property.post", | |
| 177 | - "deviceId": 123456, | |
| 178 | - "params": { | |
| 179 | - "temperature": 25.5, | |
| 180 | - "humidity": 60.2 | |
| 181 | - }, | |
| 182 | - "timestamp": 1642781234567 | |
| 183 | -} | |
| 184 | -``` | |
| 182 | +### 4.2 与二进制协议对比 | |
| 185 | 183 | |
| 186 | -### 5.2 兼容性说明 | |
| 184 | +| 特性 | 二进制协议 | JSON 协议 | | |
| 185 | +|-------|-------|----------| | |
| 186 | +| 可读性 | 差 | 优秀 | | |
| 187 | +| 调试难度 | 高 | 低 | | |
| 188 | +| 扩展性 | 差 | 优秀 | | |
| 189 | +| 解析复杂度 | 高 | 低 | | |
| 190 | +| 数据大小 | 小 | 稍大 | | |
| 191 | +| 标准化程度 | 低 | 高 | | |
| 192 | +| 实现复杂度 | 高 | 低 | | |
| 193 | +| 安全性 | 一般 | 优秀(支持认证) | | |
| 187 | 194 | |
| 188 | -- ✅ **字段名称**:与EMQX保持一致 | |
| 189 | -- ✅ **数据类型**:完全兼容 | |
| 190 | -- ✅ **消息结构**:结构相同 | |
| 191 | -- ✅ **扩展字段**:支持自定义扩展 | |
| 195 | +### 4.3 适用场景 | |
| 192 | 196 | |
| 193 | -## 6. 使用示例 | |
| 197 | +- ✅ **开发调试**:JSON 格式便于查看和调试 | |
| 198 | +- ✅ **快速集成**:标准 JSON 格式,集成简单 | |
| 199 | +- ✅ **协议扩展**:可以轻松添加新字段 | |
| 200 | +- ✅ **多语言支持**:JSON 格式支持所有主流语言 | |
| 201 | +- ✅ **云平台对接**:与主流 IoT 云平台格式兼容 | |
| 202 | +- ✅ **安全要求**:支持设备认证和访问控制 | |
| 194 | 203 | |
| 195 | -### 6.1 Java编码示例 | |
| 204 | +## 5. 最佳实践 | |
| 196 | 205 | |
| 197 | -```java | |
| 198 | -// 创建编解码器 | |
| 199 | -IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 206 | +### 5.1 认证最佳实践 | |
| 200 | 207 | |
| 201 | -// 创建数据上报消息 | |
| 202 | -Map<String, Object> sensorData = Map.of( | |
| 203 | - "temperature", 25.5, | |
| 204 | - "humidity", 60.2 | |
| 205 | -); | |
| 206 | -IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 207 | -message.setDeviceId(123456L); | |
| 208 | +1. **连接即认证**:设备连接后立即进行认证 | |
| 209 | +2. **重连机制**:连接断开后重新认证 | |
| 210 | +3. **错误重试**:认证失败时适当重试 | |
| 211 | +4. **安全传输**:使用 TLS 加密传输敏感信息 | |
| 208 | 212 | |
| 209 | -// 编码为字节数组 | |
| 210 | -byte[] jsonBytes = codec.encode(message); | |
| 213 | +### 5.2 消息设计 | |
| 211 | 214 | |
| 212 | -// 解码 | |
| 213 | -IotDeviceMessage decoded = codec.decode(jsonBytes); | |
| 214 | -``` | |
| 215 | +1. **保持简洁**:避免过深的嵌套结构 | |
| 216 | +2. **字段命名**:使用驼峰命名法,保持一致性 | |
| 217 | +3. **数据类型**:使用合适的数据类型,避免字符串表示数字 | |
| 218 | +4. **时间戳**:统一使用毫秒级时间戳 | |
| 219 | + | |
| 220 | +### 5.3 错误处理 | |
| 221 | + | |
| 222 | +1. **参数验证**:确保必要字段存在且有效 | |
| 223 | +2. **异常捕获**:正确处理编码解码异常 | |
| 224 | +3. **日志记录**:记录详细的调试信息 | |
| 225 | +4. **认证失败**:认证失败时及时关闭连接 | |
| 226 | + | |
| 227 | +### 5.4 性能优化 | |
| 228 | + | |
| 229 | +1. **批量上报**:可以在 params 中包含多个数据点 | |
| 230 | +2. **连接复用**:保持 TCP 连接,避免频繁建立连接 | |
| 231 | +3. **消息缓存**:客户端可以缓存消息,批量发送 | |
| 232 | +4. **心跳间隔**:合理设置心跳间隔,避免过于频繁 | |
| 215 | 233 | |
| 216 | -### 6.2 便捷方法示例 | |
| 234 | +## 6. 配置说明 | |
| 217 | 235 | |
| 218 | -```java | |
| 219 | -// 快速编码数据上报 | |
| 220 | -byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name"); | |
| 236 | +### 6.1 启用 JSON 协议 | |
| 221 | 237 | |
| 222 | -// 快速编码心跳 | |
| 223 | -byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name"); | |
| 238 | +在配置文件中设置: | |
| 224 | 239 | |
| 225 | -// 快速编码事件 | |
| 226 | -byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name"); | |
| 240 | +```yaml | |
| 241 | +yudao: | |
| 242 | + iot: | |
| 243 | + gateway: | |
| 244 | + protocol: | |
| 245 | + tcp: | |
| 246 | + enabled: true | |
| 247 | + port: 8091 | |
| 248 | + default-protocol: "JSON" # 使用 JSON 协议 | |
| 227 | 249 | ``` |
| 228 | 250 | |
| 229 | -## 7. 协议优势 | |
| 251 | +### 6.2 认证配置 | |
| 230 | 252 | |
| 231 | -### 7.1 与原TCP二进制协议对比 | |
| 253 | +```yaml | |
| 254 | +yudao: | |
| 255 | + iot: | |
| 256 | + gateway: | |
| 257 | + token: | |
| 258 | + secret: "your-secret-key" # JWT 密钥 | |
| 259 | + expiration: "24h" # Token 过期时间 | |
| 260 | +``` | |
| 232 | 261 | |
| 233 | -| 特性 | 二进制协议 | JSON协议 | | |
| 234 | -|------|------------|----------| | |
| 235 | -| 可读性 | 差 | 优秀 | | |
| 236 | -| 调试难度 | 高 | 低 | | |
| 237 | -| 扩展性 | 差 | 优秀 | | |
| 238 | -| 解析复杂度 | 高 | 低 | | |
| 239 | -| 数据大小 | 小 | 稍大 | | |
| 240 | -| 标准化程度 | 低 | 高 | | |
| 262 | +## 7. 调试和监控 | |
| 241 | 263 | |
| 242 | -### 7.2 适用场景 | |
| 264 | +### 7.1 日志级别 | |
| 243 | 265 | |
| 244 | -- ✅ **开发调试**:JSON格式便于查看和调试 | |
| 245 | -- ✅ **快速集成**:标准JSON格式,集成简单 | |
| 246 | -- ✅ **协议扩展**:可以轻松添加新字段 | |
| 247 | -- ✅ **多语言支持**:JSON格式支持所有主流语言 | |
| 248 | -- ✅ **云平台对接**:与主流IoT云平台格式兼容 | |
| 266 | +```yaml | |
| 267 | +logging: | |
| 268 | + level: | |
| 269 | + cn.iocoder.yudao.module.iot.gateway.protocol.tcp: DEBUG | |
| 270 | +``` | |
| 249 | 271 | |
| 250 | -## 8. 最佳实践 | |
| 272 | +### 7.2 调试信息 | |
| 251 | 273 | |
| 252 | -### 8.1 消息设计建议 | |
| 274 | +编解码器会输出详细的调试日志: | |
| 253 | 275 | |
| 254 | -1. **保持简洁**:避免过深的嵌套结构 | |
| 255 | -2. **字段命名**:使用驼峰命名法,保持一致性 | |
| 256 | -3. **数据类型**:使用合适的数据类型,避免字符串表示数字 | |
| 257 | -4. **时间戳**:统一使用毫秒级时间戳 | |
| 276 | +- 认证过程:显示认证请求和响应 | |
| 277 | +- 编码成功:显示方法、长度、内容 | |
| 278 | +- 解码过程:显示原始数据、解析结果 | |
| 279 | +- 错误信息:详细的异常堆栈 | |
| 258 | 280 | |
| 259 | -### 8.2 性能优化 | |
| 281 | +### 7.3 监控指标 | |
| 260 | 282 | |
| 261 | -1. **批量上报**:可以在params中包含多个数据点 | |
| 262 | -2. **压缩传输**:对于大数据量可以考虑gzip压缩 | |
| 263 | -3. **缓存机制**:客户端可以缓存消息,批量发送 | |
| 283 | +- 认证成功率 | |
| 284 | +- 消息处理数量 | |
| 285 | +- 编解码成功率 | |
| 286 | +- 处理延迟 | |
| 287 | +- 错误率 | |
| 288 | +- 在线设备数量 | |
| 264 | 289 | |
| 265 | -### 8.3 错误处理 | |
| 290 | +## 8. 安全考虑 | |
| 266 | 291 | |
| 267 | -1. **格式验证**:确保JSON格式正确 | |
| 268 | -2. **字段检查**:验证必填字段是否存在 | |
| 269 | -3. **异常处理**:提供详细的错误信息 | |
| 292 | +### 8.1 认证安全 | |
| 270 | 293 | |
| 271 | -## 9. 迁移指南 | |
| 294 | +1. **密码强度**:使用强密码策略 | |
| 295 | +2. **Token 过期**:设置合理的 Token 过期时间 | |
| 296 | +3. **连接限制**:限制单个设备的并发连接数 | |
| 297 | +4. **IP 白名单**:可选的 IP 访问控制 | |
| 272 | 298 | |
| 273 | -### 9.1 从二进制协议迁移 | |
| 299 | +### 8.2 传输安全 | |
| 274 | 300 | |
| 275 | -1. **保持兼容**:可以同时支持两种协议 | |
| 276 | -2. **逐步迁移**:按设备类型逐步迁移 | |
| 277 | -3. **测试验证**:充分测试新协议的稳定性 | |
| 301 | +1. **TLS 加密**:使用 TLS 1.2+ 加密传输 | |
| 302 | +2. **证书验证**:验证服务器证书 | |
| 303 | +3. **密钥管理**:安全存储和管理密钥 | |
| 278 | 304 | |
| 279 | -### 9.2 配置变更 | |
| 305 | +### 8.3 数据安全 | |
| 280 | 306 | |
| 281 | -```java | |
| 282 | -// 在设备配置中指定编解码器类型 | |
| 283 | -device.setCodecType("TCP_JSON"); | |
| 284 | -``` | |
| 307 | +1. **敏感信息**:不在日志中记录密码等敏感信息 | |
| 308 | +2. **数据验证**:验证所有输入数据 | |
| 309 | +3. **访问控制**:基于 Token 的访问控制 | |
| 285 | 310 | |
| 286 | -这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。 | |
| 311 | +这样就完成了 TCP JSON 格式协议的完整说明,包括认证流程的详细说明,与实际代码实现完全一致。 | ... | ... |