Commit cda59081a38040c9407c9d9cc8f05403a23f8da9
1 parent
6b79cab0
feat:【IoT 物联网】新增 TCP 连接管理器,优化消息编解码逻辑
Showing
15 changed files
with
1023 additions
and
1759 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
| ... | ... | @@ -8,86 +8,72 @@ import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 8 | 8 | import io.vertx.core.buffer.Buffer; |
| 9 | 9 | import lombok.AllArgsConstructor; |
| 10 | 10 | import lombok.Data; |
| 11 | -import lombok.NoArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | 12 | import org.springframework.stereotype.Component; |
| 13 | 13 | |
| 14 | -// TODO @haohao:【重要】是不是二进制更彻底哈? | |
| 15 | -// 包头(4 字节) | |
| 16 | -// 消息 ID string;nvarchar(length + string) | |
| 17 | -// version(可选,不要干脆) | |
| 18 | -// method string;nvarchar;为什么不要 opcode?因为 IotTcpJsonDeviceMessageCodec 里面,实际已经没 opcode 了 | |
| 19 | -// reply bit;0 请求,1 响应 | |
| 20 | -// 请求时: | |
| 21 | -// params;nvarchar;json 处理 | |
| 22 | -// 响应时: | |
| 23 | -// code | |
| 24 | -// msg nvarchar | |
| 25 | -// data;nvarchar;json 处理 | |
| 14 | +import java.nio.charset.StandardCharsets; | |
| 15 | + | |
| 26 | 16 | /** |
| 27 | 17 | * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 |
| 18 | + * <p> | |
| 19 | + * 二进制协议格式(所有数值使用大端序): | |
| 28 | 20 | * |
| 29 | - * 使用自定义二进制协议格式:包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) | |
| 21 | + * <pre> | |
| 22 | + * +--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 23 | + * | 魔术字 | 版本号 | 消息类型| 消息标志| 消息长度(4字节) | | |
| 24 | + * +--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 25 | + * | 消息 ID 长度(2字节) | 消息 ID (变长字符串) | | |
| 26 | + * +--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 27 | + * | 方法名长度(2字节) | 方法名(变长字符串) | | |
| 28 | + * +--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 29 | + * | 消息体数据(变长) | | |
| 30 | + * +--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 31 | + * </pre> | |
| 32 | + * <p> | |
| 33 | + * 消息体格式: | |
| 34 | + * - 请求消息:params 数据(JSON) | |
| 35 | + * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) | |
| 36 | + * <p> | |
| 37 | + * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 | |
| 30 | 38 | * |
| 31 | 39 | * @author 芋道源码 |
| 32 | 40 | */ |
| 41 | +@Slf4j | |
| 33 | 42 | @Component |
| 34 | 43 | public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { |
| 35 | 44 | |
| 36 | - // TODO @haohao:是不是叫 TCP_Binary 好点哈? | |
| 37 | 45 | public static final String TYPE = "TCP_BINARY"; |
| 38 | 46 | |
| 39 | - @Data | |
| 40 | - @NoArgsConstructor | |
| 41 | - @AllArgsConstructor | |
| 42 | - private static class TcpBinaryMessage { | |
| 43 | - | |
| 44 | - /** | |
| 45 | - * 功能码 | |
| 46 | - */ | |
| 47 | - private Short code; | |
| 48 | - | |
| 49 | - // TODO @haohao:这个和 AlinkMessage 里面,是一个东西哇? | |
| 50 | - /** | |
| 51 | - * 消息序号 | |
| 52 | - */ | |
| 53 | - private Short mid; | |
| 54 | - | |
| 55 | - // TODO @haohao:这个字段,是不是没用到呀?感觉应该也不在消息列哈? | |
| 56 | - /** | |
| 57 | - * 设备 ID | |
| 58 | - */ | |
| 59 | - private Long deviceId; | |
| 60 | - | |
| 61 | - /** | |
| 62 | - * 请求方法 | |
| 63 | - */ | |
| 64 | - private String method; | |
| 65 | - | |
| 66 | - /** | |
| 67 | - * 请求参数 | |
| 68 | - */ | |
| 69 | - private Object params; | |
| 70 | - | |
| 71 | - /** | |
| 72 | - * 响应结果 | |
| 73 | - */ | |
| 74 | - private Object data; | |
| 75 | - | |
| 76 | - // TODO @haohao:这个可以改成 code 哇?更好理解一点; | |
| 77 | - /** | |
| 78 | - * 响应错误码 | |
| 79 | - */ | |
| 80 | - private Integer responseCode; | |
| 81 | - | |
| 82 | - /** | |
| 83 | - * 响应提示 | |
| 84 | - */ | |
| 85 | - private String msg; | |
| 86 | - | |
| 87 | - // TODO @haohao:TcpBinaryMessage 和 TcpJsonMessage 保持一致哈? | |
| 47 | + // ==================== 协议常量 ==================== | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 协议魔术字,用于协议识别 | |
| 51 | + */ | |
| 52 | + private static final byte MAGIC_NUMBER = (byte) 0x7E; | |
| 88 | 53 | |
| 54 | + /** | |
| 55 | + * 协议版本号 | |
| 56 | + */ | |
| 57 | + private static final byte PROTOCOL_VERSION = (byte) 0x01; | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * 消息类型常量 | |
| 61 | + */ | |
| 62 | + public static class MessageType { | |
| 63 | + public static final byte REQUEST = 0x01; // 请求消息 | |
| 64 | + public static final byte RESPONSE = 0x02; // 响应消息 | |
| 89 | 65 | } |
| 90 | 66 | |
| 67 | + /** | |
| 68 | + * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息标志 + 消息长度) | |
| 69 | + */ | |
| 70 | + private static final int HEADER_FIXED_LENGTH = 8; | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 最小消息长度(头部 + 消息ID长度 + 方法名长度) | |
| 74 | + */ | |
| 75 | + private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; | |
| 76 | + | |
| 91 | 77 | @Override |
| 92 | 78 | public String type() { |
| 93 | 79 | return TYPE; |
| ... | ... | @@ -99,215 +85,270 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { |
| 99 | 85 | Assert.notBlank(message.getMethod(), "消息方法不能为空"); |
| 100 | 86 | |
| 101 | 87 | try { |
| 102 | - // 1. 确定功能码 | |
| 103 | - short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) | |
| 104 | - ? TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; | |
| 88 | + // 1. 确定消息类型 | |
| 89 | + byte messageType = determineMessageType(message); | |
| 105 | 90 | |
| 106 | - // 2. 构建负载数据 | |
| 107 | - String payload = buildPayload(message); | |
| 91 | + // 2. 构建消息体 | |
| 92 | + byte[] bodyData = buildMessageBody(message, messageType); | |
| 108 | 93 | |
| 109 | - // 3. 构建 TCP 数据包 | |
| 110 | - // TODO @haohao:这个和 AlinkMessage.id 是不是一致的哈? | |
| 111 | - short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); | |
| 112 | - TcpDataPackage dataPackage = new TcpDataPackage(code, mid, payload); | |
| 94 | + // 3. 构建完整消息(不包含deviceId,由连接上下文管理) | |
| 95 | + return buildCompleteMessage(message, messageType, bodyData); | |
| 113 | 96 | |
| 114 | - // 4. 编码为字节流 | |
| 115 | - return encodeTcpDataPackage(dataPackage).getBytes(); | |
| 116 | 97 | } catch (Exception e) { |
| 117 | - throw new TcpCodecException("TCP 消息编码失败", e); | |
| 98 | + log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); | |
| 99 | + throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); | |
| 118 | 100 | } |
| 119 | 101 | } |
| 120 | 102 | |
| 121 | 103 | @Override |
| 122 | 104 | public IotDeviceMessage decode(byte[] bytes) { |
| 123 | 105 | Assert.notNull(bytes, "待解码数据不能为空"); |
| 124 | - Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); | |
| 106 | + Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); | |
| 125 | 107 | |
| 126 | 108 | try { |
| 127 | - // 1. 解码 TCP 数据包 | |
| 128 | - TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); | |
| 129 | - | |
| 130 | - // 2. 根据功能码确定方法 | |
| 131 | - String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? MessageMethod.STATE_ONLINE | |
| 132 | - : MessageMethod.PROPERTY_POST; | |
| 133 | - | |
| 134 | - // 3. 解析负载数据 | |
| 135 | - PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); | |
| 136 | - | |
| 137 | - // 4. 构建 IoT 设备消息 | |
| 138 | - return IotDeviceMessage.of( | |
| 139 | - payloadInfo.getRequestId(), | |
| 140 | - method, | |
| 141 | - payloadInfo.getParams(), | |
| 142 | - null, | |
| 143 | - null, | |
| 144 | - null); | |
| 109 | + Buffer buffer = Buffer.buffer(bytes); | |
| 110 | + | |
| 111 | + // 1. 解析协议头部 | |
| 112 | + ProtocolHeader header = parseProtocolHeader(buffer); | |
| 113 | + | |
| 114 | + // 2. 解析消息内容(不包含deviceId,由上层连接管理器设置) | |
| 115 | + return parseMessageContent(buffer, header); | |
| 116 | + | |
| 145 | 117 | } catch (Exception e) { |
| 146 | - throw new TcpCodecException("TCP 消息解码失败", e); | |
| 118 | + log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); | |
| 119 | + throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); | |
| 147 | 120 | } |
| 148 | 121 | } |
| 149 | 122 | |
| 150 | - // ==================== 内部辅助方法 ==================== | |
| 123 | + // ==================== 编码相关方法 ==================== | |
| 151 | 124 | |
| 152 | 125 | /** |
| 153 | - * 构建负载数据 | |
| 154 | - * | |
| 155 | - * @param message 设备消息 | |
| 156 | - * @return 负载字符串 | |
| 126 | + * 确定消息类型 | |
| 127 | + * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 | |
| 157 | 128 | */ |
| 158 | - private String buildPayload(IotDeviceMessage message) { | |
| 159 | - TcpBinaryMessage tcpBinaryMessage = new TcpBinaryMessage( | |
| 160 | - null, // code 在数据包中单独处理 | |
| 161 | - null, // mid 在数据包中单独处理 | |
| 162 | - message.getDeviceId(), | |
| 163 | - message.getMethod(), | |
| 164 | - message.getParams(), | |
| 165 | - message.getData(), | |
| 166 | - message.getCode(), | |
| 167 | - message.getMsg()); | |
| 168 | - return JsonUtils.toJsonString(tcpBinaryMessage); | |
| 129 | + private byte determineMessageType(IotDeviceMessage message) { | |
| 130 | + // 判断是否为响应消息:有响应码或响应消息时为响应 | |
| 131 | + if (message.getCode() != null || StrUtil.isNotBlank(message.getMsg())) { | |
| 132 | + return MessageType.RESPONSE; | |
| 133 | + } | |
| 134 | + // 默认为请求消息 | |
| 135 | + return MessageType.REQUEST; | |
| 169 | 136 | } |
| 170 | 137 | |
| 171 | 138 | /** |
| 172 | - * 解析负载信息 | |
| 173 | - * | |
| 174 | - * @param payload 负载字符串 | |
| 175 | - * @return 负载信息 | |
| 139 | + * 构建消息体 | |
| 176 | 140 | */ |
| 177 | - private PayloadInfo parsePayloadInfo(String payload) { | |
| 178 | - if (StrUtil.isBlank(payload)) { | |
| 179 | - return new PayloadInfo(null, null); | |
| 180 | - } | |
| 141 | + private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { | |
| 142 | + Buffer bodyBuffer = Buffer.buffer(); | |
| 181 | 143 | |
| 182 | - try { | |
| 183 | - TcpBinaryMessage tcpBinaryMessage = JsonUtils.parseObject(payload, TcpBinaryMessage.class); | |
| 184 | - if (tcpBinaryMessage != null) { | |
| 185 | - return new PayloadInfo( | |
| 186 | - StrUtil.isNotEmpty(tcpBinaryMessage.getMethod()) | |
| 187 | - ? tcpBinaryMessage.getMethod() + "_" + System.currentTimeMillis() | |
| 188 | - : null, | |
| 189 | - tcpBinaryMessage.getParams()); | |
| 144 | + if (messageType == MessageType.RESPONSE) { | |
| 145 | + // 响应消息:code + msg长度 + msg + data | |
| 146 | + bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); | |
| 147 | + | |
| 148 | + String msg = message.getMsg() != null ? message.getMsg() : ""; | |
| 149 | + byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8); | |
| 150 | + bodyBuffer.appendShort((short) msgBytes.length); | |
| 151 | + bodyBuffer.appendBytes(msgBytes); | |
| 152 | + | |
| 153 | + if (message.getData() != null) { | |
| 154 | + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); | |
| 155 | + } | |
| 156 | + } else { | |
| 157 | + // 请求消息:包含 params 或 data | |
| 158 | + Object payload = message.getParams() != null ? message.getParams() : message.getData(); | |
| 159 | + if (payload != null) { | |
| 160 | + bodyBuffer.appendBytes(JsonUtils.toJsonByte(payload)); | |
| 190 | 161 | } |
| 191 | - } catch (Exception e) { | |
| 192 | - // 如果解析失败,返回默认值 | |
| 193 | - return new PayloadInfo("unknown_" + System.currentTimeMillis(), null); | |
| 194 | 162 | } |
| 195 | - return null; | |
| 163 | + | |
| 164 | + return bodyBuffer.getBytes(); | |
| 196 | 165 | } |
| 197 | 166 | |
| 198 | 167 | /** |
| 199 | - * 编码 TCP 数据包 | |
| 200 | - * | |
| 201 | - * @param dataPackage 数据包对象 | |
| 202 | - * @return 编码后的字节流 | |
| 168 | + * 构建完整消息 | |
| 203 | 169 | */ |
| 204 | - private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { | |
| 205 | - Assert.notNull(dataPackage, "数据包对象不能为空"); | |
| 206 | - Assert.notNull(dataPackage.getPayload(), "负载不能为空"); | |
| 170 | + private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { | |
| 171 | + Buffer buffer = Buffer.buffer(); | |
| 207 | 172 | |
| 208 | - // 1. 计算包体长度(除了包头 4 字节) | |
| 209 | - int payloadLength = dataPackage.getPayload().getBytes().length; | |
| 210 | - int totalLength = 2 + 2 + payloadLength; | |
| 173 | + // 1. 写入协议头部 | |
| 174 | + buffer.appendByte(MAGIC_NUMBER); | |
| 175 | + buffer.appendByte(PROTOCOL_VERSION); | |
| 176 | + buffer.appendByte(messageType); | |
| 177 | + buffer.appendByte((byte) 0x00); // 消息标志,预留字段 | |
| 211 | 178 | |
| 212 | - // 2. 写入数据 | |
| 213 | - Buffer buffer = Buffer.buffer(); | |
| 214 | - // 2.1 写入包头:总长度(4 字节) | |
| 215 | - buffer.appendInt(totalLength); | |
| 216 | - // 2.2 写入功能码(2 字节) | |
| 217 | - buffer.appendShort(dataPackage.getCode()); | |
| 218 | - // 2.3 写入消息序号(2 字节) | |
| 219 | - buffer.appendShort(dataPackage.getMid()); | |
| 220 | - // 2.4 写入包体数据(不定长) | |
| 221 | - buffer.appendBytes(dataPackage.getPayload().getBytes()); | |
| 222 | - return buffer; | |
| 179 | + // 2. 预留消息长度位置 | |
| 180 | + int lengthPosition = buffer.length(); | |
| 181 | + buffer.appendInt(0); | |
| 182 | + | |
| 183 | + // 3. 写入消息ID | |
| 184 | + String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() | |
| 185 | + : generateMessageId(message.getMethod()); | |
| 186 | + byte[] messageIdBytes = messageId.getBytes(StandardCharsets.UTF_8); | |
| 187 | + buffer.appendShort((short) messageIdBytes.length); | |
| 188 | + buffer.appendBytes(messageIdBytes); | |
| 189 | + | |
| 190 | + // 4. 写入方法名 | |
| 191 | + byte[] methodBytes = message.getMethod().getBytes(StandardCharsets.UTF_8); | |
| 192 | + buffer.appendShort((short) methodBytes.length); | |
| 193 | + buffer.appendBytes(methodBytes); | |
| 194 | + | |
| 195 | + // 5. 写入消息体 | |
| 196 | + buffer.appendBytes(bodyData); | |
| 197 | + | |
| 198 | + // 6. 更新消息长度 | |
| 199 | + buffer.setInt(lengthPosition, buffer.length()); | |
| 200 | + | |
| 201 | + return buffer.getBytes(); | |
| 223 | 202 | } |
| 224 | 203 | |
| 225 | 204 | /** |
| 226 | - * 解码 TCP 数据包 | |
| 227 | - * | |
| 228 | - * @param buffer 数据缓冲区 | |
| 229 | - * @return 解码后的数据包 | |
| 205 | + * 生成消息 ID | |
| 230 | 206 | */ |
| 231 | - private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { | |
| 232 | - Assert.isTrue(buffer.length() >= 8, "数据包长度不足"); | |
| 207 | + private String generateMessageId(String method) { | |
| 208 | + return method + "_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 1000); | |
| 209 | + } | |
| 210 | + | |
| 211 | + // ==================== 解码相关方法 ==================== | |
| 233 | 212 | |
| 213 | + /** | |
| 214 | + * 解析协议头部 | |
| 215 | + */ | |
| 216 | + private ProtocolHeader parseProtocolHeader(Buffer buffer) { | |
| 234 | 217 | int index = 0; |
| 235 | - // 1. 跳过包头(4 字节) | |
| 218 | + | |
| 219 | + // 1. 验证魔术字 | |
| 220 | + byte magic = buffer.getByte(index++); | |
| 221 | + Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); | |
| 222 | + | |
| 223 | + // 2. 验证版本号 | |
| 224 | + byte version = buffer.getByte(index++); | |
| 225 | + Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); | |
| 226 | + | |
| 227 | + // 3. 读取消息类型 | |
| 228 | + byte messageType = buffer.getByte(index++); | |
| 229 | + Assert.isTrue(isValidMessageType(messageType), "无效的消息类型: " + messageType); | |
| 230 | + | |
| 231 | + // 4. 读取消息标志(暂时跳过) | |
| 232 | + byte messageFlags = buffer.getByte(index++); | |
| 233 | + | |
| 234 | + // 5. 读取消息长度 | |
| 235 | + int messageLength = buffer.getInt(index); | |
| 236 | 236 | index += 4; |
| 237 | - // 2. 获取功能码(2 字节) | |
| 238 | - short code = buffer.getShort(index); | |
| 237 | + | |
| 238 | + Assert.isTrue(messageLength == buffer.length(), "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); | |
| 239 | + | |
| 240 | + return new ProtocolHeader(magic, version, messageType, messageFlags, messageLength, index); | |
| 241 | + } | |
| 242 | + | |
| 243 | + /** | |
| 244 | + * 解析消息内容 | |
| 245 | + */ | |
| 246 | + private IotDeviceMessage parseMessageContent(Buffer buffer, ProtocolHeader header) { | |
| 247 | + int index = header.getNextIndex(); | |
| 248 | + | |
| 249 | + // 1. 读取消息ID | |
| 250 | + short messageIdLength = buffer.getShort(index); | |
| 239 | 251 | index += 2; |
| 240 | - // 3. 获取消息序号(2 字节) | |
| 241 | - short mid = buffer.getShort(index); | |
| 252 | + String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); | |
| 253 | + index += messageIdLength; | |
| 254 | + | |
| 255 | + // 2. 读取方法名 | |
| 256 | + short methodLength = buffer.getShort(index); | |
| 242 | 257 | index += 2; |
| 243 | - // 4. 获取包体数据 | |
| 244 | - String payload = ""; | |
| 245 | - if (index < buffer.length()) { | |
| 246 | - payload = buffer.getString(index, buffer.length()); | |
| 247 | - } | |
| 258 | + String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); | |
| 259 | + index += methodLength; | |
| 248 | 260 | |
| 249 | - return new TcpDataPackage(code, mid, payload); | |
| 261 | + // 3. 解析消息体 | |
| 262 | + return parseMessageBody(buffer, index, header.getMessageType(), messageId, method); | |
| 250 | 263 | } |
| 251 | 264 | |
| 252 | - // ==================== 内部类 ==================== | |
| 253 | - | |
| 254 | - // TODO @haohao:会不会存在 reply 的时候,有 data、msg、code 参数哈。 | |
| 255 | 265 | /** |
| 256 | - * 负载信息类 | |
| 266 | + * 解析消息体 | |
| 257 | 267 | */ |
| 258 | - @Data | |
| 259 | - @AllArgsConstructor | |
| 260 | - private static class PayloadInfo { | |
| 261 | - | |
| 262 | - private String requestId; | |
| 263 | - private Object params; | |
| 268 | + private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, | |
| 269 | + String messageId, String method) { | |
| 270 | + if (startIndex >= buffer.length()) { | |
| 271 | + // 空消息体 | |
| 272 | + return IotDeviceMessage.of(messageId, method, null, null, null, null); | |
| 273 | + } | |
| 264 | 274 | |
| 275 | + if (messageType == MessageType.RESPONSE) { | |
| 276 | + // 响应消息:解析 code + msg + data | |
| 277 | + return parseResponseMessage(buffer, startIndex, messageId, method); | |
| 278 | + } else { | |
| 279 | + // 请求消息:解析 payload(可能是 params 或 data) | |
| 280 | + Object payload = parseJsonData(buffer, startIndex, buffer.length()); | |
| 281 | + return IotDeviceMessage.of(messageId, method, payload, null, null, null); | |
| 282 | + } | |
| 265 | 283 | } |
| 266 | 284 | |
| 267 | 285 | /** |
| 268 | - * TCP 数据包内部类 | |
| 286 | + * 解析响应消息 | |
| 269 | 287 | */ |
| 270 | - @Data | |
| 271 | - @AllArgsConstructor | |
| 272 | - private static class TcpDataPackage { | |
| 288 | + private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { | |
| 289 | + int index = startIndex; | |
| 273 | 290 | |
| 274 | - // 功能码定义 | |
| 275 | - public static final short CODE_REGISTER = 10; | |
| 276 | - public static final short CODE_REGISTER_REPLY = 11; | |
| 277 | - public static final short CODE_HEARTBEAT = 20; | |
| 278 | - public static final short CODE_HEARTBEAT_REPLY = 21; | |
| 279 | - public static final short CODE_MESSAGE_UP = 30; | |
| 280 | - public static final short CODE_MESSAGE_DOWN = 40; | |
| 291 | + // 1. 读取响应码 | |
| 292 | + Integer code = buffer.getInt(index); | |
| 293 | + index += 4; | |
| 281 | 294 | |
| 282 | - // TODO @haohao:要不改成 opCode | |
| 283 | - private short code; | |
| 284 | - private short mid; | |
| 285 | - private String payload; | |
| 295 | + // 2. 读取响应消息 | |
| 296 | + short msgLength = buffer.getShort(index); | |
| 297 | + index += 2; | |
| 298 | + String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; | |
| 299 | + index += msgLength; | |
| 286 | 300 | |
| 287 | - } | |
| 301 | + // 3. 读取响应数据 | |
| 302 | + Object data = null; | |
| 303 | + if (index < buffer.length()) { | |
| 304 | + data = parseJsonData(buffer, index, buffer.length()); | |
| 305 | + } | |
| 288 | 306 | |
| 289 | - // ==================== 常量定义 ==================== | |
| 307 | + return IotDeviceMessage.of(messageId, method, null, data, code, msg); | |
| 308 | + } | |
| 290 | 309 | |
| 291 | 310 | /** |
| 292 | - * 消息方法常量 | |
| 311 | + * 解析JSON数据 | |
| 293 | 312 | */ |
| 294 | - public static class MessageMethod { | |
| 295 | - | |
| 296 | - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 | |
| 297 | - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 313 | + private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { | |
| 314 | + if (startIndex >= endIndex) { | |
| 315 | + return null; | |
| 316 | + } | |
| 298 | 317 | |
| 318 | + try { | |
| 319 | + String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); | |
| 320 | + if (StrUtil.isBlank(jsonStr)) { | |
| 321 | + return null; | |
| 322 | + } | |
| 323 | + return JsonUtils.parseObject(jsonStr, Object.class); | |
| 324 | + } catch (Exception e) { | |
| 325 | + log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); | |
| 326 | + return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); | |
| 327 | + } | |
| 299 | 328 | } |
| 300 | 329 | |
| 301 | - // ==================== 自定义异常 ==================== | |
| 330 | + // ==================== 辅助方法 ==================== | |
| 302 | 331 | |
| 303 | - // TODO @haohao:全局异常搞个。看着可以服用哈。 | |
| 304 | 332 | /** |
| 305 | - * TCP 编解码异常 | |
| 333 | + * 验证消息类型是否有效 | |
| 306 | 334 | */ |
| 307 | - public static class TcpCodecException extends RuntimeException { | |
| 308 | - public TcpCodecException(String message, Throwable cause) { | |
| 309 | - super(message, cause); | |
| 310 | - } | |
| 335 | + private boolean isValidMessageType(byte messageType) { | |
| 336 | + return messageType == MessageType.REQUEST || messageType == MessageType.RESPONSE; | |
| 311 | 337 | } |
| 312 | 338 | |
| 339 | + // ==================== 内部类 ==================== | |
| 340 | + | |
| 341 | + /** | |
| 342 | + * 协议头部信息 | |
| 343 | + */ | |
| 344 | + @Data | |
| 345 | + @AllArgsConstructor | |
| 346 | + private static class ProtocolHeader { | |
| 347 | + private byte magic; | |
| 348 | + private byte version; | |
| 349 | + private byte messageType; | |
| 350 | + private byte messageFlags; | |
| 351 | + private int messageLength; | |
| 352 | + private int nextIndex; // 指向消息内容开始位置 | |
| 353 | + } | |
| 313 | 354 | } |
| 314 | 355 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
| ... | ... | @@ -14,12 +14,13 @@ import org.springframework.stereotype.Component; |
| 14 | 14 | * |
| 15 | 15 | * 采用纯 JSON 格式传输,格式如下: |
| 16 | 16 | * { |
| 17 | - * "id": "消息 ID", | |
| 18 | - * "method": "消息方法", | |
| 19 | - * "deviceId": "设备 ID", | |
| 20 | - * "params": {...}, | |
| 21 | - * "timestamp": 时间戳 | |
| 22 | - * // TODO @haohao:貌似少了 code、msg、timestamp | |
| 17 | + * "id": "消息 ID", | |
| 18 | + * "method": "消息方法", | |
| 19 | + * "params": {...}, // 请求参数 | |
| 20 | + * "data": {...}, // 响应结果 | |
| 21 | + * "code": 200, // 响应错误码 | |
| 22 | + * "msg": "success", // 响应提示 | |
| 23 | + * "timestamp": 时间戳 | |
| 23 | 24 | * } |
| 24 | 25 | * |
| 25 | 26 | * @author 芋道源码 |
| ... | ... | @@ -44,12 +45,6 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { |
| 44 | 45 | */ |
| 45 | 46 | private String method; |
| 46 | 47 | |
| 47 | - // TODO @haohao:这个字段,是不是没用到呀?感觉应该也不在消息列哈? | |
| 48 | - /** | |
| 49 | - * 设备 ID | |
| 50 | - */ | |
| 51 | - private Long deviceId; | |
| 52 | - | |
| 53 | 48 | /** |
| 54 | 49 | * 请求参数 |
| 55 | 50 | */ |
| ... | ... | @@ -84,9 +79,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { |
| 84 | 79 | |
| 85 | 80 | @Override |
| 86 | 81 | public byte[] encode(IotDeviceMessage message) { |
| 87 | - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(message.getRequestId(), message.getMethod(), | |
| 88 | - message.getDeviceId(), | |
| 89 | - message.getParams(), message.getData(), message.getCode(), message.getMsg(), | |
| 82 | + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( | |
| 83 | + message.getRequestId(), | |
| 84 | + message.getMethod(), | |
| 85 | + message.getParams(), | |
| 86 | + message.getData(), | |
| 87 | + message.getCode(), | |
| 88 | + message.getMsg(), | |
| 90 | 89 | System.currentTimeMillis()); |
| 91 | 90 | return JsonUtils.toJsonByte(tcpJsonMessage); |
| 92 | 91 | } |
| ... | ... | @@ -97,11 +96,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { |
| 97 | 96 | TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); |
| 98 | 97 | Assert.notNull(tcpJsonMessage, "消息不能为空"); |
| 99 | 98 | Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); |
| 100 | - // TODO @haohao:这个我已经改了哈。一些属性,可以放在一行,好理解一点~ | |
| 101 | - IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of(tcpJsonMessage.getId(), tcpJsonMessage.getMethod(), | |
| 102 | - tcpJsonMessage.getParams(), tcpJsonMessage.getData(), tcpJsonMessage.getCode(), tcpJsonMessage.getMsg()); | |
| 103 | - iotDeviceMessage.setDeviceId(tcpJsonMessage.getDeviceId()); | |
| 104 | - return iotDeviceMessage; | |
| 99 | + return IotDeviceMessage.of( | |
| 100 | + tcpJsonMessage.getId(), | |
| 101 | + tcpJsonMessage.getMethod(), | |
| 102 | + tcpJsonMessage.getParams(), | |
| 103 | + tcpJsonMessage.getData(), | |
| 104 | + tcpJsonMessage.getCode(), | |
| 105 | + tcpJsonMessage.getMsg()); | |
| 105 | 106 | } |
| 106 | 107 | |
| 107 | 108 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 12 | 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 13 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 14 | 14 | import io.vertx.core.Vertx; |
| ... | ... | @@ -92,19 +92,19 @@ public class IotGatewayConfiguration { |
| 92 | 92 | public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, |
| 93 | 93 | IotDeviceService deviceService, |
| 94 | 94 | IotDeviceMessageService messageService, |
| 95 | - IotTcpSessionManager sessionManager, | |
| 95 | + IotTcpConnectionManager connectionManager, | |
| 96 | 96 | Vertx tcpVertx) { |
| 97 | 97 | return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), |
| 98 | - deviceService, messageService, sessionManager, tcpVertx); | |
| 98 | + deviceService, messageService, connectionManager, tcpVertx); | |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | 101 | @Bean |
| 102 | 102 | public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, |
| 103 | 103 | IotDeviceMessageService messageService, |
| 104 | 104 | IotDeviceService deviceService, |
| 105 | - IotTcpSessionManager sessionManager, | |
| 105 | + IotTcpConnectionManager connectionManager, | |
| 106 | 106 | IotMessageBus messageBus) { |
| 107 | - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, | |
| 107 | + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager, | |
| 108 | 108 | messageBus); |
| 109 | 109 | } |
| 110 | 110 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
| ... | ... | @@ -4,13 +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 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | 11 | import jakarta.annotation.PostConstruct; |
| 12 | +import lombok.RequiredArgsConstructor; | |
| 12 | 13 | import lombok.extern.slf4j.Slf4j; |
| 13 | -import org.springframework.stereotype.Component; | |
| 14 | 14 | |
| 15 | 15 | /** |
| 16 | 16 | * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 |
| ... | ... | @@ -18,37 +18,28 @@ import org.springframework.stereotype.Component; |
| 18 | 18 | * @author 芋道源码 |
| 19 | 19 | */ |
| 20 | 20 | @Slf4j |
| 21 | -@Component | |
| 21 | +@RequiredArgsConstructor | |
| 22 | 22 | public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 23 | 23 | |
| 24 | - private final IotTcpDownstreamHandler downstreamHandler; | |
| 25 | - | |
| 26 | - private final IotMessageBus messageBus; | |
| 27 | - | |
| 28 | 24 | private final IotTcpUpstreamProtocol protocol; |
| 29 | 25 | |
| 30 | - // todo @haohao:不用的变量,可以去掉哈 | |
| 26 | + private final IotDeviceMessageService messageService; | |
| 27 | + | |
| 31 | 28 | private final IotDeviceService deviceService; |
| 32 | 29 | |
| 33 | - private final IotTcpSessionManager sessionManager; | |
| 30 | + private final IotTcpConnectionManager connectionManager; | |
| 34 | 31 | |
| 35 | - // TODO @haohao:lombok 简化 | |
| 36 | - public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, | |
| 37 | - IotDeviceMessageService messageService, | |
| 38 | - IotDeviceService deviceService, | |
| 39 | - IotTcpSessionManager sessionManager, | |
| 40 | - IotMessageBus messageBus) { | |
| 41 | - this.protocol = protocol; | |
| 42 | - this.messageBus = messageBus; | |
| 43 | - this.deviceService = deviceService; | |
| 44 | - this.sessionManager = sessionManager; | |
| 45 | - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, sessionManager); | |
| 46 | - } | |
| 32 | + private final IotMessageBus messageBus; | |
| 33 | + | |
| 34 | + private IotTcpDownstreamHandler downstreamHandler; | |
| 47 | 35 | |
| 48 | 36 | @PostConstruct |
| 49 | 37 | public void init() { |
| 38 | + // 初始化下游处理器 | |
| 39 | + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager); | |
| 40 | + | |
| 50 | 41 | messageBus.register(this); |
| 51 | - log.info("[init][TCP 下游订阅者初始化完成] 服务器 ID: {}, Topic: {}", | |
| 42 | + log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", | |
| 52 | 43 | protocol.getServerId(), getTopic()); |
| 53 | 44 | } |
| 54 | 45 | |
| ... | ... | @@ -68,8 +59,9 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic |
| 68 | 59 | try { |
| 69 | 60 | downstreamHandler.handle(message); |
| 70 | 61 | } catch (Exception e) { |
| 71 | - log.error("[onMessage][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", | |
| 62 | + log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", | |
| 72 | 63 | message.getDeviceId(), message.getMethod(), message.getId(), e); |
| 73 | 64 | } |
| 74 | 65 | } |
| 66 | + | |
| 75 | 67 | } |
| 76 | 68 | \ 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
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| ... | ... | @@ -29,24 +29,24 @@ public class IotTcpUpstreamProtocol { |
| 29 | 29 | |
| 30 | 30 | private final IotDeviceMessageService messageService; |
| 31 | 31 | |
| 32 | - private final IotTcpSessionManager sessionManager; | |
| 32 | + private final IotTcpConnectionManager connectionManager; | |
| 33 | 33 | |
| 34 | 34 | private final Vertx vertx; |
| 35 | 35 | |
| 36 | 36 | @Getter |
| 37 | 37 | private final String serverId; |
| 38 | 38 | |
| 39 | - private NetServer netServer; | |
| 39 | + private NetServer tcpServer; | |
| 40 | 40 | |
| 41 | 41 | public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, |
| 42 | 42 | IotDeviceService deviceService, |
| 43 | 43 | IotDeviceMessageService messageService, |
| 44 | - IotTcpSessionManager sessionManager, | |
| 44 | + IotTcpConnectionManager connectionManager, | |
| 45 | 45 | Vertx vertx) { |
| 46 | 46 | this.tcpProperties = tcpProperties; |
| 47 | 47 | this.deviceService = deviceService; |
| 48 | 48 | this.messageService = messageService; |
| 49 | - this.sessionManager = sessionManager; | |
| 49 | + this.connectionManager = connectionManager; | |
| 50 | 50 | this.vertx = vertx; |
| 51 | 51 | this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); |
| 52 | 52 | } |
| ... | ... | @@ -68,16 +68,16 @@ public class IotTcpUpstreamProtocol { |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | 70 | // 创建服务器并设置连接处理器 |
| 71 | - netServer = vertx.createNetServer(options); | |
| 72 | - netServer.connectHandler(socket -> { | |
| 71 | + tcpServer = vertx.createNetServer(options); | |
| 72 | + tcpServer.connectHandler(socket -> { | |
| 73 | 73 | IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, |
| 74 | - sessionManager); | |
| 74 | + connectionManager); | |
| 75 | 75 | handler.handle(socket); |
| 76 | 76 | }); |
| 77 | 77 | |
| 78 | 78 | // 启动服务器 |
| 79 | 79 | try { |
| 80 | - netServer.listen().result(); | |
| 80 | + tcpServer.listen().result(); | |
| 81 | 81 | log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); |
| 82 | 82 | } catch (Exception e) { |
| 83 | 83 | log.error("[start][IoT 网关 TCP 协议启动失败]", e); |
| ... | ... | @@ -87,9 +87,9 @@ public class IotTcpUpstreamProtocol { |
| 87 | 87 | |
| 88 | 88 | @PreDestroy |
| 89 | 89 | public void stop() { |
| 90 | - if (netServer != null) { | |
| 90 | + if (tcpServer != null) { | |
| 91 | 91 | try { |
| 92 | - netServer.close().result(); | |
| 92 | + tcpServer.close().result(); | |
| 93 | 93 | log.info("[stop][IoT 网关 TCP 协议已停止]"); |
| 94 | 94 | } catch (Exception e) { |
| 95 | 95 | log.error("[stop][IoT 网关 TCP 协议停止失败]", e); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java deleted
| 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 | - // TODO @haohao:得考虑,一个设备连接多次? | |
| 28 | - /** | |
| 29 | - * 设备 ID -> NetSocket 的映射(用于快速查找) | |
| 30 | - */ | |
| 31 | - private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); | |
| 32 | - | |
| 33 | - /** | |
| 34 | - * 注册认证信息 | |
| 35 | - * | |
| 36 | - * @param socket TCP 连接 | |
| 37 | - * @param authInfo 认证信息 | |
| 38 | - */ | |
| 39 | - public void registerAuth(NetSocket socket, AuthInfo authInfo) { | |
| 40 | - // 如果设备已有其他连接,先清理旧连接 | |
| 41 | - // TODO @haohao:是不是允许同时连接?就像 mqtt 应该也允许重复连接哈? | |
| 42 | - NetSocket oldSocket = deviceSocketMap.get(authInfo.getDeviceId()); | |
| 43 | - if (oldSocket != null && oldSocket != socket) { | |
| 44 | - log.info("[registerAuth][设备已有其他连接,清理旧连接] 设备 ID: {}, 旧连接: {}", | |
| 45 | - authInfo.getDeviceId(), oldSocket.remoteAddress()); | |
| 46 | - authStatusMap.remove(oldSocket); | |
| 47 | - } | |
| 48 | - | |
| 49 | - // 注册新认证信息 | |
| 50 | - authStatusMap.put(socket, authInfo); | |
| 51 | - deviceSocketMap.put(authInfo.getDeviceId(), socket); | |
| 52 | - | |
| 53 | - log.info("[registerAuth][注册认证信息] 设备 ID: {}, 连接: {}, productKey: {}, deviceName: {}", | |
| 54 | - authInfo.getDeviceId(), socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); | |
| 55 | - } | |
| 56 | - | |
| 57 | - /** | |
| 58 | - * 注销认证信息 | |
| 59 | - * | |
| 60 | - * @param socket TCP 连接 | |
| 61 | - */ | |
| 62 | - public void unregisterAuth(NetSocket socket) { | |
| 63 | - AuthInfo authInfo = authStatusMap.remove(socket); | |
| 64 | - if (authInfo != null) { | |
| 65 | - deviceSocketMap.remove(authInfo.getDeviceId()); | |
| 66 | - log.info("[unregisterAuth][注销认证信息] 设备 ID: {}, 连接: {}", | |
| 67 | - authInfo.getDeviceId(), socket.remoteAddress()); | |
| 68 | - } | |
| 69 | - } | |
| 70 | - | |
| 71 | - // TODO @haohao:建议暂时没用的方法,可以删除掉;整体聚焦! | |
| 72 | - /** | |
| 73 | - * 注销设备认证信息 | |
| 74 | - * | |
| 75 | - * @param deviceId 设备 ID | |
| 76 | - */ | |
| 77 | - public void unregisterAuth(Long deviceId) { | |
| 78 | - NetSocket socket = deviceSocketMap.remove(deviceId); | |
| 79 | - if (socket != null) { | |
| 80 | - AuthInfo authInfo = authStatusMap.remove(socket); | |
| 81 | - if (authInfo != null) { | |
| 82 | - log.info("[unregisterAuth][注销设备认证信息] 设备 ID: {}, 连接: {}", | |
| 83 | - deviceId, socket.remoteAddress()); | |
| 84 | - } | |
| 85 | - } | |
| 86 | - } | |
| 87 | - | |
| 88 | - /** | |
| 89 | - * 获取认证信息 | |
| 90 | - * | |
| 91 | - * @param socket TCP 连接 | |
| 92 | - * @return 认证信息,如果未认证则返回 null | |
| 93 | - */ | |
| 94 | - public AuthInfo getAuthInfo(NetSocket socket) { | |
| 95 | - return authStatusMap.get(socket); | |
| 96 | - } | |
| 97 | - | |
| 98 | - /** | |
| 99 | - * 获取设备的认证信息 | |
| 100 | - * | |
| 101 | - * @param deviceId 设备 ID | |
| 102 | - * @return 认证信息,如果设备未认证则返回 null | |
| 103 | - */ | |
| 104 | - public AuthInfo getAuthInfo(Long deviceId) { | |
| 105 | - NetSocket socket = deviceSocketMap.get(deviceId); | |
| 106 | - return socket != null ? authStatusMap.get(socket) : null; | |
| 107 | - } | |
| 108 | - | |
| 109 | - /** | |
| 110 | - * 检查连接是否已认证 | |
| 111 | - * | |
| 112 | - * @param socket TCP 连接 | |
| 113 | - * @return 是否已认证 | |
| 114 | - */ | |
| 115 | - public boolean isAuthenticated(NetSocket socket) { | |
| 116 | - return authStatusMap.containsKey(socket); | |
| 117 | - } | |
| 118 | - | |
| 119 | - /** | |
| 120 | - * 检查设备是否已认证 | |
| 121 | - * | |
| 122 | - * @param deviceId 设备 ID | |
| 123 | - * @return 是否已认证 | |
| 124 | - */ | |
| 125 | - public boolean isAuthenticated(Long deviceId) { | |
| 126 | - return deviceSocketMap.containsKey(deviceId); | |
| 127 | - } | |
| 128 | - | |
| 129 | - /** | |
| 130 | - * 获取设备的 TCP 连接 | |
| 131 | - * | |
| 132 | - * @param deviceId 设备 ID | |
| 133 | - * @return TCP 连接,如果设备未认证则返回 null | |
| 134 | - */ | |
| 135 | - public NetSocket getDeviceSocket(Long deviceId) { | |
| 136 | - return deviceSocketMap.get(deviceId); | |
| 137 | - } | |
| 138 | - | |
| 139 | - /** | |
| 140 | - * 获取当前已认证设备数量 | |
| 141 | - * | |
| 142 | - * @return 已认证设备数量 | |
| 143 | - */ | |
| 144 | - public int getAuthenticatedDeviceCount() { | |
| 145 | - return deviceSocketMap.size(); | |
| 146 | - } | |
| 147 | - | |
| 148 | - /** | |
| 149 | - * 获取所有已认证设备 ID | |
| 150 | - * | |
| 151 | - * @return 已认证设备 ID 集合 | |
| 152 | - */ | |
| 153 | - public java.util.Set<Long> getAuthenticatedDeviceIds() { | |
| 154 | - return deviceSocketMap.keySet(); | |
| 155 | - } | |
| 156 | - | |
| 157 | - /** | |
| 158 | - * 清理所有认证信息 | |
| 159 | - */ | |
| 160 | - public void clearAll() { | |
| 161 | - int count = authStatusMap.size(); | |
| 162 | - authStatusMap.clear(); | |
| 163 | - deviceSocketMap.clear(); | |
| 164 | - // TODO @haohao:第一个括号是方法,第二个括号是明细日志;其它日志,也可以检查下哈。 | |
| 165 | - log.info("[clearAll][清理所有认证信息] 清理数量: {}", count); | |
| 166 | - } | |
| 167 | - | |
| 168 | - /** | |
| 169 | - * 认证信息 | |
| 170 | - */ | |
| 171 | - @Data | |
| 172 | - public static class AuthInfo { | |
| 173 | - | |
| 174 | - /** | |
| 175 | - * 设备编号 | |
| 176 | - */ | |
| 177 | - private Long deviceId; | |
| 178 | - | |
| 179 | - /** | |
| 180 | - * 产品标识 | |
| 181 | - */ | |
| 182 | - private String productKey; | |
| 183 | - | |
| 184 | - /** | |
| 185 | - * 设备名称 | |
| 186 | - */ | |
| 187 | - private String deviceName; | |
| 188 | - | |
| 189 | - // TODO @haohao:令牌不要存储,万一有安全问题哈; | |
| 190 | - /** | |
| 191 | - * 认证令牌 | |
| 192 | - */ | |
| 193 | - private String token; | |
| 194 | - | |
| 195 | - /** | |
| 196 | - * 客户端 ID | |
| 197 | - */ | |
| 198 | - private String clientId; | |
| 199 | - | |
| 200 | - } | |
| 201 | - | |
| 202 | -} | |
| 203 | 0 | \ 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/IotTcpConnectionManager.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.experimental.Accessors; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +import java.util.Map; | |
| 10 | +import java.util.concurrent.ConcurrentHashMap; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * IoT 网关 TCP 连接管理器 | |
| 14 | + * <p> | |
| 15 | + * 统一管理 TCP 连接的认证状态、设备会话和消息发送功能: | |
| 16 | + * 1. 管理 TCP 连接的认证状态 | |
| 17 | + * 2. 管理设备会话和在线状态 | |
| 18 | + * 3. 管理消息发送到设备 | |
| 19 | + * | |
| 20 | + * @author 芋道源码 | |
| 21 | + */ | |
| 22 | +@Slf4j | |
| 23 | +@Component | |
| 24 | +public class IotTcpConnectionManager { | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 连接信息映射:NetSocket -> 连接信息 | |
| 28 | + */ | |
| 29 | + private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>(); | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 设备 ID -> NetSocket 的映射(用于快速查找) | |
| 33 | + */ | |
| 34 | + private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * NetSocket -> 设备 ID 的映射(用于连接断开时清理) | |
| 38 | + */ | |
| 39 | + private final Map<NetSocket, Long> socketDeviceMap = new ConcurrentHashMap<>(); | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 注册设备连接(包含认证信息) | |
| 43 | + * | |
| 44 | + * @param socket TCP 连接 | |
| 45 | + * @param deviceId 设备 ID | |
| 46 | + * @param authInfo 认证信息 | |
| 47 | + */ | |
| 48 | + public void registerConnection(NetSocket socket, Long deviceId, AuthInfo authInfo) { | |
| 49 | + // 如果设备已有其他连接,先清理旧连接 | |
| 50 | + NetSocket oldSocket = deviceSocketMap.get(deviceId); | |
| 51 | + if (oldSocket != null && oldSocket != socket) { | |
| 52 | + log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", | |
| 53 | + deviceId, oldSocket.remoteAddress()); | |
| 54 | + oldSocket.close(); | |
| 55 | + // 清理所有相关映射 | |
| 56 | + connectionMap.remove(oldSocket); | |
| 57 | + socketDeviceMap.remove(oldSocket); | |
| 58 | + } | |
| 59 | + | |
| 60 | + // 注册新连接 - 更新所有映射关系 | |
| 61 | + ConnectionInfo connectionInfo = new ConnectionInfo() | |
| 62 | + .setDeviceId(deviceId) | |
| 63 | + .setAuthInfo(authInfo) | |
| 64 | + .setAuthenticated(true); | |
| 65 | + | |
| 66 | + connectionMap.put(socket, connectionInfo); | |
| 67 | + deviceSocketMap.put(deviceId, socket); | |
| 68 | + socketDeviceMap.put(socket, deviceId); | |
| 69 | + | |
| 70 | + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", | |
| 71 | + deviceId, socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); | |
| 72 | + } | |
| 73 | + | |
| 74 | + /** | |
| 75 | + * 注销设备连接 | |
| 76 | + * | |
| 77 | + * @param socket TCP 连接 | |
| 78 | + */ | |
| 79 | + public void unregisterConnection(NetSocket socket) { | |
| 80 | + ConnectionInfo connectionInfo = connectionMap.remove(socket); | |
| 81 | + Long deviceId = socketDeviceMap.remove(socket); | |
| 82 | + | |
| 83 | + if (connectionInfo != null && deviceId != null) { | |
| 84 | + deviceSocketMap.remove(deviceId); | |
| 85 | + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", | |
| 86 | + deviceId, socket.remoteAddress()); | |
| 87 | + } | |
| 88 | + } | |
| 89 | + | |
| 90 | + /** | |
| 91 | + * 注销设备连接(通过设备 ID) | |
| 92 | + * | |
| 93 | + * @param deviceId 设备 ID | |
| 94 | + */ | |
| 95 | + public void unregisterConnection(Long deviceId) { | |
| 96 | + NetSocket socket = deviceSocketMap.remove(deviceId); | |
| 97 | + if (socket != null) { | |
| 98 | + connectionMap.remove(socket); | |
| 99 | + socketDeviceMap.remove(socket); | |
| 100 | + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); | |
| 101 | + } | |
| 102 | + } | |
| 103 | + | |
| 104 | + /** | |
| 105 | + * 检查连接是否已认证 | |
| 106 | + */ | |
| 107 | + public boolean isAuthenticated(NetSocket socket) { | |
| 108 | + ConnectionInfo info = connectionMap.get(socket); | |
| 109 | + return info != null && info.isAuthenticated(); | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 检查连接是否未认证 | |
| 114 | + */ | |
| 115 | + public boolean isNotAuthenticated(NetSocket socket) { | |
| 116 | + return !isAuthenticated(socket); | |
| 117 | + } | |
| 118 | + | |
| 119 | + /** | |
| 120 | + * 获取连接的认证信息 | |
| 121 | + */ | |
| 122 | + public AuthInfo getAuthInfo(NetSocket socket) { | |
| 123 | + ConnectionInfo info = connectionMap.get(socket); | |
| 124 | + return info != null ? info.getAuthInfo() : null; | |
| 125 | + } | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * 检查设备是否在线 | |
| 129 | + */ | |
| 130 | + public boolean isDeviceOnline(Long deviceId) { | |
| 131 | + return deviceSocketMap.containsKey(deviceId); | |
| 132 | + } | |
| 133 | + | |
| 134 | + /** | |
| 135 | + * 检查设备是否离线 | |
| 136 | + */ | |
| 137 | + public boolean isDeviceOffline(Long deviceId) { | |
| 138 | + return !isDeviceOnline(deviceId); | |
| 139 | + } | |
| 140 | + | |
| 141 | + /** | |
| 142 | + * 发送消息到设备 | |
| 143 | + */ | |
| 144 | + public boolean sendToDevice(Long deviceId, byte[] data) { | |
| 145 | + NetSocket socket = deviceSocketMap.get(deviceId); | |
| 146 | + if (socket == null) { | |
| 147 | + log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); | |
| 148 | + return false; | |
| 149 | + } | |
| 150 | + | |
| 151 | + try { | |
| 152 | + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); | |
| 153 | + log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length); | |
| 154 | + return true; | |
| 155 | + } catch (Exception e) { | |
| 156 | + log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); | |
| 157 | + // 发送失败时清理连接 | |
| 158 | + unregisterConnection(socket); | |
| 159 | + return false; | |
| 160 | + } | |
| 161 | + } | |
| 162 | + | |
| 163 | + /** | |
| 164 | + * 连接信息 | |
| 165 | + */ | |
| 166 | + @Data | |
| 167 | + @Accessors(chain = true) | |
| 168 | + public static class ConnectionInfo { | |
| 169 | + private Long deviceId; | |
| 170 | + private AuthInfo authInfo; | |
| 171 | + private boolean authenticated; | |
| 172 | + } | |
| 173 | + | |
| 174 | + /** | |
| 175 | + * 认证信息 | |
| 176 | + */ | |
| 177 | + @Data | |
| 178 | + @Accessors(chain = true) | |
| 179 | + public static class AuthInfo { | |
| 180 | + private Long deviceId; | |
| 181 | + private String productKey; | |
| 182 | + private String deviceName; | |
| 183 | + private String clientId; | |
| 184 | + } | |
| 185 | +} | |
| 0 | 186 | \ 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 deleted
| 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 | -// TODO @haohao:IotTcpSessionManager、IotTcpAuthManager 是不是融合哈? | |
| 11 | -/** | |
| 12 | - * IoT 网关 TCP 会话管理器 | |
| 13 | - * <p> | |
| 14 | - * 维护设备 ID 和 TCP 连接的映射关系,支持下行消息发送 | |
| 15 | - * | |
| 16 | - * @author 芋道源码 | |
| 17 | - */ | |
| 18 | -@Slf4j | |
| 19 | -@Component | |
| 20 | -public class IotTcpSessionManager { | |
| 21 | - | |
| 22 | - /** | |
| 23 | - * 设备 ID -> TCP 连接的映射 | |
| 24 | - */ | |
| 25 | - private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); | |
| 26 | - | |
| 27 | - /** | |
| 28 | - * TCP 连接 -> 设备 ID 的映射(用于连接断开时清理) | |
| 29 | - */ | |
| 30 | - private final Map<NetSocket, Long> socketDeviceMap = new ConcurrentHashMap<>(); | |
| 31 | - | |
| 32 | - /** | |
| 33 | - * 注册设备会话 | |
| 34 | - * | |
| 35 | - * @param deviceId 设备 ID | |
| 36 | - * @param socket TCP 连接 | |
| 37 | - */ | |
| 38 | - public void registerSession(Long deviceId, NetSocket socket) { | |
| 39 | - // 如果设备已有连接,先断开旧连接 | |
| 40 | - NetSocket oldSocket = deviceSocketMap.get(deviceId); | |
| 41 | - if (oldSocket != null && oldSocket != socket) { | |
| 42 | - log.info("[registerSession][设备已有连接,断开旧连接] 设备 ID: {}, 旧连接: {}", deviceId, oldSocket.remoteAddress()); | |
| 43 | - oldSocket.close(); | |
| 44 | - socketDeviceMap.remove(oldSocket); | |
| 45 | - } | |
| 46 | - | |
| 47 | - // 注册新连接 | |
| 48 | - deviceSocketMap.put(deviceId, socket); | |
| 49 | - socketDeviceMap.put(socket, deviceId); | |
| 50 | - | |
| 51 | - log.info("[registerSession][注册设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); | |
| 52 | - } | |
| 53 | - | |
| 54 | - /** | |
| 55 | - * 注销设备会话 | |
| 56 | - * | |
| 57 | - * @param deviceId 设备 ID | |
| 58 | - */ | |
| 59 | - public void unregisterSession(Long deviceId) { | |
| 60 | - NetSocket socket = deviceSocketMap.remove(deviceId); | |
| 61 | - if (socket != null) { | |
| 62 | - socketDeviceMap.remove(socket); | |
| 63 | - log.info("[unregisterSession][注销设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); | |
| 64 | - } | |
| 65 | - } | |
| 66 | - | |
| 67 | - /** | |
| 68 | - * 注销 TCP 连接会话 | |
| 69 | - * | |
| 70 | - * @param socket TCP 连接 | |
| 71 | - */ | |
| 72 | - public void unregisterSession(NetSocket socket) { | |
| 73 | - Long deviceId = socketDeviceMap.remove(socket); | |
| 74 | - if (deviceId != null) { | |
| 75 | - deviceSocketMap.remove(deviceId); | |
| 76 | - log.info("[unregisterSession][注销连接会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); | |
| 77 | - } | |
| 78 | - } | |
| 79 | - | |
| 80 | - /** | |
| 81 | - * 获取设备的 TCP 连接 | |
| 82 | - * | |
| 83 | - * @param deviceId 设备 ID | |
| 84 | - * @return TCP 连接,如果设备未连接则返回 null | |
| 85 | - */ | |
| 86 | - public NetSocket getDeviceSocket(Long deviceId) { | |
| 87 | - return deviceSocketMap.get(deviceId); | |
| 88 | - } | |
| 89 | - | |
| 90 | - /** | |
| 91 | - * 检查设备是否在线 | |
| 92 | - * | |
| 93 | - * @param deviceId 设备 ID | |
| 94 | - * @return 是否在线 | |
| 95 | - */ | |
| 96 | - public boolean isDeviceOnline(Long deviceId) { | |
| 97 | - NetSocket socket = deviceSocketMap.get(deviceId); | |
| 98 | - return socket != null; | |
| 99 | - } | |
| 100 | - | |
| 101 | - /** | |
| 102 | - * 发送消息到设备 | |
| 103 | - * | |
| 104 | - * @param deviceId 设备 ID | |
| 105 | - * @param data 消息数据 | |
| 106 | - * @return 是否发送成功 | |
| 107 | - */ | |
| 108 | - public boolean sendToDevice(Long deviceId, byte[] data) { | |
| 109 | - NetSocket socket = deviceSocketMap.get(deviceId); | |
| 110 | - if (socket == null) { | |
| 111 | - log.warn("[sendToDevice][设备未连接] 设备 ID: {}", deviceId); | |
| 112 | - return false; | |
| 113 | - } | |
| 114 | - | |
| 115 | - try { | |
| 116 | - socket.write(io.vertx.core.buffer.Buffer.buffer(data)); | |
| 117 | - log.debug("[sendToDevice][发送消息成功] 设备 ID: {}, 数据长度: {} 字节", deviceId, data.length); | |
| 118 | - return true; | |
| 119 | - } catch (Exception e) { | |
| 120 | - log.error("[sendToDevice][发送消息失败] 设备 ID: {}", deviceId, e); | |
| 121 | - // 发送失败时清理连接 | |
| 122 | - unregisterSession(deviceId); | |
| 123 | - return false; | |
| 124 | - } | |
| 125 | - } | |
| 126 | - | |
| 127 | - /** | |
| 128 | - * 获取当前在线设备数量 | |
| 129 | - * | |
| 130 | - * @return 在线设备数量 | |
| 131 | - */ | |
| 132 | - public int getOnlineDeviceCount() { | |
| 133 | - return deviceSocketMap.size(); | |
| 134 | - } | |
| 135 | - | |
| 136 | - /** | |
| 137 | - * 获取所有在线设备 ID | |
| 138 | - * | |
| 139 | - * @return 在线设备 ID 集合 | |
| 140 | - */ | |
| 141 | - public java.util.Set<Long> getOnlineDeviceIds() { | |
| 142 | - return deviceSocketMap.keySet(); | |
| 143 | - } | |
| 144 | -} | |
| 145 | 0 | \ 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
| ... | ... | @@ -2,9 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import lombok.RequiredArgsConstructor; | |
| 8 | 9 | import lombok.extern.slf4j.Slf4j; |
| 9 | 10 | |
| 10 | 11 | /** |
| ... | ... | @@ -13,62 +14,50 @@ import lombok.extern.slf4j.Slf4j; |
| 13 | 14 | * @author 芋道源码 |
| 14 | 15 | */ |
| 15 | 16 | @Slf4j |
| 17 | +@RequiredArgsConstructor | |
| 16 | 18 | public class IotTcpDownstreamHandler { |
| 17 | 19 | |
| 18 | - private final IotDeviceMessageService messageService; | |
| 20 | + private final IotDeviceMessageService deviceMessageService; | |
| 19 | 21 | |
| 20 | 22 | private final IotDeviceService deviceService; |
| 21 | 23 | |
| 22 | - private final IotTcpSessionManager sessionManager; | |
| 23 | - | |
| 24 | - // TODO @haohao:这个可以使用 lombok 简化构造方法 | |
| 25 | - public IotTcpDownstreamHandler(IotDeviceMessageService messageService, | |
| 26 | - IotDeviceService deviceService, IotTcpSessionManager sessionManager) { | |
| 27 | - this.messageService = messageService; | |
| 28 | - this.deviceService = deviceService; | |
| 29 | - this.sessionManager = sessionManager; | |
| 30 | - } | |
| 24 | + private final IotTcpConnectionManager connectionManager; | |
| 31 | 25 | |
| 32 | 26 | /** |
| 33 | 27 | * 处理下行消息 |
| 34 | - * | |
| 35 | - * @param message 设备消息 | |
| 36 | 28 | */ |
| 37 | 29 | public void handle(IotDeviceMessage message) { |
| 38 | 30 | try { |
| 39 | - log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", | |
| 31 | + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", | |
| 40 | 32 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 41 | 33 | |
| 42 | - // TODO @haohao 1. 和 2. 可以合成 1.1 1.2 并且中间可以不空行; | |
| 43 | - // 1. 获取设备信息 | |
| 44 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 45 | - if (device == null) { | |
| 46 | - log.error("[handle][设备不存在] 设备 ID: {}", message.getDeviceId()); | |
| 34 | + // 1.1 获取设备信息 | |
| 35 | + IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 36 | + if (deviceInfo == null) { | |
| 37 | + log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId()); | |
| 47 | 38 | return; |
| 48 | 39 | } |
| 49 | - | |
| 50 | - // 2. 检查设备是否在线 | |
| 51 | - if (!sessionManager.isDeviceOnline(message.getDeviceId())) { | |
| 52 | - log.warn("[handle][设备不在线] 设备 ID: {}", message.getDeviceId()); | |
| 40 | + // 1.2 检查设备是否在线 | |
| 41 | + if (connectionManager.isDeviceOffline(message.getDeviceId())) { | |
| 42 | + log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); | |
| 53 | 43 | return; |
| 54 | 44 | } |
| 55 | 45 | |
| 56 | - // 3. 编码消息 | |
| 57 | - byte[] bytes = messageService.encodeDeviceMessage(message, device.getCodecType()); | |
| 46 | + // 2. 根据产品 Key 和设备名称编码消息并发送到设备 | |
| 47 | + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), | |
| 48 | + deviceInfo.getDeviceName()); | |
| 49 | + boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); | |
| 58 | 50 | |
| 59 | - // 4. 发送消息到设备 | |
| 60 | - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes); | |
| 61 | 51 | if (success) { |
| 62 | - log.info("[handle][下行消息发送成功] 设备 ID: {}, 方法: {}, 消息 ID: {}, 数据长度: {} 字节", | |
| 52 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | |
| 63 | 53 | message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); |
| 64 | 54 | } else { |
| 65 | - log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", | |
| 55 | + log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", | |
| 66 | 56 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 67 | - } // TODO @haohao:下面这个空行,可以考虑去掉的哈。 | |
| 68 | - | |
| 57 | + } | |
| 69 | 58 | } catch (Exception e) { |
| 70 | - log.error("[handle][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息内容: {}", | |
| 71 | - message.getDeviceId(), message.getMethod(), message.getParams(), e); | |
| 59 | + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", | |
| 60 | + message.getDeviceId(), message.getMethod(), message, e); | |
| 72 | 61 | } |
| 73 | 62 | } |
| 74 | 63 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
| ... | ... | @@ -12,50 +12,48 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 12 | 12 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 13 | 13 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 14 | 14 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | |
| 15 | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpAuthManager; | |
| 17 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; | |
| 18 | -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 19 | 19 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 20 | 20 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 21 | 21 | import io.vertx.core.Handler; |
| 22 | 22 | import io.vertx.core.buffer.Buffer; |
| 23 | 23 | import io.vertx.core.net.NetSocket; |
| 24 | +import lombok.AllArgsConstructor; | |
| 25 | +import lombok.Data; | |
| 24 | 26 | import lombok.extern.slf4j.Slf4j; |
| 25 | 27 | |
| 28 | +import java.nio.charset.StandardCharsets; | |
| 29 | + | |
| 26 | 30 | /** |
| 27 | 31 | * TCP 上行消息处理器 |
| 32 | + * | |
| 33 | + * @author 芋道源码 | |
| 28 | 34 | */ |
| 29 | 35 | @Slf4j |
| 30 | 36 | public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 31 | 37 | |
| 32 | - // TODO @haohao:这两个变量,可以复用 IotTcpBinaryDeviceMessageCodec 的 TYPE | |
| 33 | - private static final String CODEC_TYPE_JSON = "TCP_JSON"; | |
| 34 | - private static final String CODEC_TYPE_BINARY = "TCP_BINARY"; | |
| 35 | - | |
| 38 | + private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; | |
| 39 | + private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; | |
| 36 | 40 | private static final String AUTH_METHOD = "auth"; |
| 37 | 41 | |
| 38 | 42 | private final IotDeviceMessageService deviceMessageService; |
| 39 | 43 | |
| 40 | 44 | private final IotDeviceService deviceService; |
| 41 | 45 | |
| 42 | - private final IotTcpSessionManager sessionManager; | |
| 43 | - | |
| 44 | - private final IotTcpAuthManager authManager; | |
| 45 | - | |
| 46 | - private final IotDeviceTokenService deviceTokenService; | |
| 46 | + private final IotTcpConnectionManager connectionManager; | |
| 47 | 47 | |
| 48 | 48 | private final IotDeviceCommonApi deviceApi; |
| 49 | 49 | |
| 50 | 50 | private final String serverId; |
| 51 | 51 | |
| 52 | 52 | public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, |
| 53 | - IotDeviceService deviceService, IotTcpSessionManager sessionManager) { | |
| 53 | + IotDeviceService deviceService, IotTcpConnectionManager connectionManager) { | |
| 54 | 54 | this.deviceMessageService = deviceMessageService; |
| 55 | 55 | this.deviceService = deviceService; |
| 56 | - this.sessionManager = sessionManager; | |
| 57 | - this.authManager = SpringUtil.getBean(IotTcpAuthManager.class); | |
| 58 | - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 56 | + this.connectionManager = connectionManager; | |
| 59 | 57 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 60 | 58 | this.serverId = protocol.getServerId(); |
| 61 | 59 | } |
| ... | ... | @@ -63,207 +61,313 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 63 | 61 | @Override |
| 64 | 62 | public void handle(NetSocket socket) { |
| 65 | 63 | String clientId = IdUtil.simpleUUID(); |
| 66 | - log.info("[handle][收到设备连接] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); | |
| 64 | + log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | |
| 67 | 65 | |
| 68 | 66 | // 设置异常和关闭处理器 |
| 69 | 67 | socket.exceptionHandler(ex -> { |
| 70 | - log.error("[handle][连接异常] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress(), ex); | |
| 71 | - cleanupSession(socket); | |
| 68 | + log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | |
| 69 | + cleanupConnection(socket); | |
| 72 | 70 | }); |
| 73 | 71 | |
| 74 | 72 | socket.closeHandler(v -> { |
| 75 | - log.info("[handle][连接关闭] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); | |
| 76 | - cleanupSession(socket); | |
| 73 | + log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | |
| 74 | + cleanupConnection(socket); | |
| 77 | 75 | }); |
| 78 | 76 | |
| 79 | - socket.handler(buffer -> handleDataPackage(clientId, buffer, socket)); | |
| 77 | + socket.handler(buffer -> processMessage(clientId, buffer, socket)); | |
| 80 | 78 | } |
| 81 | 79 | |
| 82 | - private void handleDataPackage(String clientId, Buffer buffer, NetSocket socket) { | |
| 80 | + /** | |
| 81 | + * 处理消息 | |
| 82 | + */ | |
| 83 | + private void processMessage(String clientId, Buffer buffer, NetSocket socket) { | |
| 83 | 84 | try { |
| 85 | + // 1. 数据包基础检查 | |
| 84 | 86 | if (buffer.length() == 0) { |
| 85 | - log.warn("[handleDataPackage][数据包为空] 客户端 ID: {}", clientId); | |
| 86 | 87 | return; |
| 87 | 88 | } |
| 88 | 89 | |
| 89 | - // 1. 解码消息 | |
| 90 | + // 2. 解码消息 | |
| 90 | 91 | MessageInfo messageInfo = decodeMessage(buffer); |
| 91 | 92 | if (messageInfo == null) { |
| 92 | 93 | return; |
| 93 | 94 | } |
| 94 | 95 | |
| 95 | - // TODO @haohao:2. 和 3. 可以合并成 2.1 2.2 ,都是异常的情况。然后 3. 可以 return 直接; | |
| 96 | - // 2. 获取设备信息 | |
| 97 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(messageInfo.message.getDeviceId()); | |
| 98 | - if (device == null) { | |
| 99 | - sendError(socket, messageInfo.message.getRequestId(), "设备不存在", messageInfo.codecType); | |
| 100 | - return; | |
| 101 | - } | |
| 102 | - | |
| 103 | - // 3. 处理消息 | |
| 104 | - if (!authManager.isAuthenticated(socket)) { | |
| 105 | - handleAuthRequest(clientId, messageInfo.message, socket, messageInfo.codecType); | |
| 96 | + // 3. 根据消息类型路由处理 | |
| 97 | + if (isAuthRequest(messageInfo.message)) { | |
| 98 | + // 认证请求:无需检查认证状态 | |
| 99 | + handleAuthenticationRequest(clientId, messageInfo, socket); | |
| 106 | 100 | } else { |
| 107 | - IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); | |
| 108 | - handleBusinessMessage(clientId, messageInfo.message, authInfo); | |
| 101 | + // 业务消息:需要检查认证状态 | |
| 102 | + handleBusinessRequest(clientId, messageInfo, socket); | |
| 109 | 103 | } |
| 104 | + | |
| 110 | 105 | } catch (Exception e) { |
| 111 | - log.error("[handleDataPackage][处理数据包失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); | |
| 106 | + log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); | |
| 112 | 107 | } |
| 113 | 108 | } |
| 114 | 109 | |
| 115 | 110 | /** |
| 116 | 111 | * 处理认证请求 |
| 117 | 112 | */ |
| 118 | - private void handleAuthRequest(String clientId, IotDeviceMessage message, NetSocket socket, String codecType) { | |
| 113 | + private void handleAuthenticationRequest(String clientId, MessageInfo messageInfo, NetSocket socket) { | |
| 119 | 114 | try { |
| 120 | - // 1. 验证认证请求 | |
| 121 | - // TODO @haohao:ObjUtil.notEquals。减少取反 | |
| 122 | - if (!AUTH_METHOD.equals(message.getMethod())) { | |
| 123 | - sendError(socket, message.getRequestId(), "请先进行认证", codecType); | |
| 124 | - return; | |
| 125 | - } | |
| 115 | + IotDeviceMessage message = messageInfo.message; | |
| 126 | 116 | |
| 127 | - // 2. 解析认证参数 // TODO @haohao:1. 和 2. 可以合并成 1.1 1.2 都是参数校验 | |
| 117 | + // 1. 解析认证参数 | |
| 128 | 118 | AuthParams authParams = parseAuthParams(message.getParams()); |
| 129 | 119 | if (authParams == null) { |
| 130 | - sendError(socket, message.getRequestId(), "认证参数不完整", codecType); | |
| 120 | + sendError(socket, message.getRequestId(), "认证参数不完整", messageInfo.codecType); | |
| 131 | 121 | return; |
| 132 | 122 | } |
| 133 | 123 | |
| 134 | - // 3. 执行认证流程 | |
| 135 | - // TODO @haohao:成功失败、都大哥日志,会不会更好哈? | |
| 136 | - if (performAuthentication(authParams, socket, message.getRequestId(), codecType)) { | |
| 137 | - log.info("[handleAuthRequest][认证成功] 客户端 ID: {}, username: {}", clientId, authParams.username); | |
| 124 | + // 2. 执行认证 | |
| 125 | + if (!authenticateDevice(authParams)) { | |
| 126 | + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", | |
| 127 | + clientId, authParams.username); | |
| 128 | + sendError(socket, message.getRequestId(), "认证失败", messageInfo.codecType); | |
| 129 | + return; | |
| 130 | + } | |
| 131 | + | |
| 132 | + // 3. 解析设备信息 | |
| 133 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.username); | |
| 134 | + if (deviceInfo == null) { | |
| 135 | + sendError(socket, message.getRequestId(), "解析设备信息失败", messageInfo.codecType); | |
| 136 | + return; | |
| 137 | + } | |
| 138 | + | |
| 139 | + // 4. 获取设备信息 | |
| 140 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 141 | + deviceInfo.getDeviceName()); | |
| 142 | + if (device == null) { | |
| 143 | + sendError(socket, message.getRequestId(), "设备不存在", messageInfo.codecType); | |
| 144 | + return; | |
| 145 | + } | |
| 146 | + | |
| 147 | + // 5. 注册连接并发送成功响应 | |
| 148 | + registerConnection(socket, device, deviceInfo, authParams.clientId); | |
| 149 | + sendOnlineMessage(deviceInfo); | |
| 150 | + sendSuccess(socket, message.getRequestId(), "认证成功", messageInfo.codecType); | |
| 151 | + | |
| 152 | + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", | |
| 153 | + device.getId(), deviceInfo.getDeviceName()); | |
| 154 | + | |
| 155 | + } catch (Exception e) { | |
| 156 | + log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); | |
| 157 | + sendError(socket, messageInfo.message.getRequestId(), "认证处理异常", messageInfo.codecType); | |
| 158 | + } | |
| 159 | + } | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * 处理业务请求 | |
| 163 | + */ | |
| 164 | + private void handleBusinessRequest(String clientId, MessageInfo messageInfo, NetSocket socket) { | |
| 165 | + try { | |
| 166 | + // 1. 检查认证状态 | |
| 167 | + if (connectionManager.isNotAuthenticated(socket)) { | |
| 168 | + log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); | |
| 169 | + sendError(socket, messageInfo.message.getRequestId(), "请先进行认证", messageInfo.codecType); | |
| 170 | + return; | |
| 138 | 171 | } |
| 172 | + | |
| 173 | + // 2. 获取认证信息并处理业务消息 | |
| 174 | + IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket); | |
| 175 | + processBusinessMessage(clientId, messageInfo.message, authInfo); | |
| 176 | + | |
| 139 | 177 | } catch (Exception e) { |
| 140 | - log.error("[handleAuthRequest][认证处理异常] 客户端 ID: {}", clientId, e); | |
| 141 | - sendError(socket, message.getRequestId(), "认证处理异常: " + e.getMessage(), codecType); | |
| 178 | + log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); | |
| 142 | 179 | } |
| 143 | 180 | } |
| 144 | 181 | |
| 145 | 182 | /** |
| 146 | 183 | * 处理业务消息 |
| 147 | 184 | */ |
| 148 | - private void handleBusinessMessage(String clientId, IotDeviceMessage message, | |
| 149 | - IotTcpAuthManager.AuthInfo authInfo) { | |
| 185 | + private void processBusinessMessage(String clientId, IotDeviceMessage message, | |
| 186 | + IotTcpConnectionManager.AuthInfo authInfo) { | |
| 150 | 187 | try { |
| 151 | 188 | message.setDeviceId(authInfo.getDeviceId()); |
| 152 | 189 | message.setServerId(serverId); |
| 153 | 190 | |
| 154 | - deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), authInfo.getDeviceName(), | |
| 155 | - serverId); | |
| 156 | - log.info("[handleBusinessMessage][业务消息处理完成] 客户端 ID: {}, 消息 ID: {}, 设备 ID: {}, 方法: {}", | |
| 157 | - clientId, message.getId(), message.getDeviceId(), message.getMethod()); | |
| 191 | + // 发送到消息总线 | |
| 192 | + deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), | |
| 193 | + authInfo.getDeviceName(), serverId); | |
| 194 | + | |
| 158 | 195 | } catch (Exception e) { |
| 159 | - log.error("[handleBusinessMessage][处理业务消息失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); | |
| 196 | + log.error("[processBusinessMessage][业务消息处理失败,客户端 ID: {},消息 ID: {}]", | |
| 197 | + clientId, message.getId(), e); | |
| 160 | 198 | } |
| 161 | 199 | } |
| 162 | 200 | |
| 163 | 201 | /** |
| 164 | 202 | * 解码消息 |
| 165 | 203 | */ |
| 166 | - // TODO @haohao:是不是还是直接管理后台配置协议,然后直接使用就好啦。暂时不考虑动态解析哈。保持一致,降低理解成本哈。 | |
| 167 | 204 | private MessageInfo decodeMessage(Buffer buffer) { |
| 205 | + if (buffer == null || buffer.length() == 0) { | |
| 206 | + return null; | |
| 207 | + } | |
| 208 | + | |
| 209 | + // 1. 快速检测消息格式类型 | |
| 210 | + String codecType = detectMessageFormat(buffer); | |
| 211 | + | |
| 168 | 212 | try { |
| 169 | - String rawData = buffer.toString(); | |
| 170 | - String codecType = isJsonFormat(rawData) ? CODEC_TYPE_JSON : CODEC_TYPE_BINARY; | |
| 213 | + // 2. 使用检测到的格式进行解码 | |
| 171 | 214 | IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); |
| 172 | - return message != null ? new MessageInfo(message, codecType) : null; | |
| 215 | + | |
| 216 | + if (message == null) { | |
| 217 | + return null; | |
| 218 | + } | |
| 219 | + | |
| 220 | + return new MessageInfo(message, codecType); | |
| 221 | + | |
| 173 | 222 | } catch (Exception e) { |
| 174 | - log.debug("[decodeMessage][消息解码失败] 错误: {}", e.getMessage()); | |
| 223 | + log.warn("[decodeMessage][消息解码失败,格式: {},数据长度: {},错误: {}]", | |
| 224 | + codecType, buffer.length(), e.getMessage()); | |
| 175 | 225 | return null; |
| 176 | 226 | } |
| 177 | 227 | } |
| 178 | 228 | |
| 179 | 229 | /** |
| 180 | - * 执行认证 | |
| 230 | + * 检测消息格式类型 | |
| 231 | + * 优化性能:避免不必要的字符串转换 | |
| 181 | 232 | */ |
| 182 | - // TODO @haohao:下面的 1. 2. 可以合并下,本质也是校验哈。 | |
| 183 | - private boolean performAuthentication(AuthParams authParams, NetSocket socket, String requestId, String codecType) { | |
| 184 | - // 1. 执行认证 | |
| 185 | - if (!authenticateDevice(authParams)) { | |
| 186 | - sendError(socket, requestId, "认证失败", codecType); | |
| 187 | - return false; | |
| 233 | + private String detectMessageFormat(Buffer buffer) { | |
| 234 | + if (buffer.length() == 0) { | |
| 235 | + return CODEC_TYPE_JSON; // 默认使用 JSON | |
| 188 | 236 | } |
| 189 | 237 | |
| 190 | - // 2. 获取设备信息 | |
| 191 | - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(authParams.username); | |
| 192 | - if (deviceInfo == null) { | |
| 193 | - sendError(socket, requestId, "解析设备信息失败", codecType); | |
| 238 | + // 1. 优先检测二进制格式(检查魔术字节 0x7E) | |
| 239 | + if (isBinaryFormat(buffer)) { | |
| 240 | + return CODEC_TYPE_BINARY; | |
| 241 | + } | |
| 242 | + | |
| 243 | + // 2. 检测 JSON 格式(检查前几个有效字符) | |
| 244 | + if (isJsonFormat(buffer)) { | |
| 245 | + return CODEC_TYPE_JSON; | |
| 246 | + } | |
| 247 | + | |
| 248 | + // 3. 默认尝试 JSON 格式 | |
| 249 | + return CODEC_TYPE_JSON; | |
| 250 | + } | |
| 251 | + | |
| 252 | + /** | |
| 253 | + * 检测二进制格式 | |
| 254 | + * 通过检查魔术字节快速识别,避免完整字符串转换 | |
| 255 | + */ | |
| 256 | + private boolean isBinaryFormat(Buffer buffer) { | |
| 257 | + // 二进制协议最小长度检查 | |
| 258 | + if (buffer.length() < 8) { | |
| 194 | 259 | return false; |
| 195 | 260 | } |
| 196 | 261 | |
| 197 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 198 | - deviceInfo.getDeviceName()); | |
| 199 | - if (device == null) { | |
| 200 | - sendError(socket, requestId, "设备不存在", codecType); | |
| 262 | + try { | |
| 263 | + // 检查魔术字节 0x7E(二进制协议的第一个字节) | |
| 264 | + byte firstByte = buffer.getByte(0); | |
| 265 | + return firstByte == (byte) 0x7E; | |
| 266 | + } catch (Exception e) { | |
| 201 | 267 | return false; |
| 202 | 268 | } |
| 269 | + } | |
| 270 | + | |
| 271 | + /** | |
| 272 | + * 检测 JSON 格式 | |
| 273 | + * 只检查前几个有效字符,避免完整字符串转换 | |
| 274 | + */ | |
| 275 | + private boolean isJsonFormat(Buffer buffer) { | |
| 276 | + try { | |
| 277 | + // 检查前 64 个字节或整个缓冲区(取较小值) | |
| 278 | + int checkLength = Math.min(buffer.length(), 64); | |
| 279 | + String prefix = buffer.getString(0, checkLength, StandardCharsets.UTF_8.name()); | |
| 280 | + | |
| 281 | + if (StrUtil.isBlank(prefix)) { | |
| 282 | + return false; | |
| 283 | + } | |
| 203 | 284 | |
| 204 | - // 3. 注册认证信息 | |
| 205 | - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 206 | - registerAuthInfo(socket, device, deviceInfo, token, authParams.clientId); | |
| 285 | + String trimmed = prefix.trim(); | |
| 286 | + // JSON 格式必须以 { 或 [ 开头 | |
| 287 | + return trimmed.startsWith("{") || trimmed.startsWith("["); | |
| 207 | 288 | |
| 208 | - // 4. 发送上线消息和成功响应 | |
| 209 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 210 | - deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), | |
| 211 | - serverId); | |
| 212 | - sendSuccess(socket, requestId, "认证成功", codecType); | |
| 213 | - return true; | |
| 289 | + } catch (Exception e) { | |
| 290 | + return false; | |
| 291 | + } | |
| 214 | 292 | } |
| 215 | 293 | |
| 216 | 294 | /** |
| 217 | - * 发送响应 | |
| 295 | + * 注册连接信息 | |
| 218 | 296 | */ |
| 219 | - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { | |
| 297 | + private void registerConnection(NetSocket socket, IotDeviceRespDTO device, | |
| 298 | + IotDeviceAuthUtils.DeviceInfo deviceInfo, String clientId) { | |
| 299 | + // 创建认证信息 | |
| 300 | + IotTcpConnectionManager.AuthInfo authInfo = new IotTcpConnectionManager.AuthInfo() | |
| 301 | + .setDeviceId(device.getId()) | |
| 302 | + .setProductKey(deviceInfo.getProductKey()) | |
| 303 | + .setDeviceName(deviceInfo.getDeviceName()) | |
| 304 | + .setClientId(clientId); | |
| 305 | + | |
| 306 | + // 注册连接 | |
| 307 | + connectionManager.registerConnection(socket, device.getId(), authInfo); | |
| 308 | + } | |
| 309 | + | |
| 310 | + /** | |
| 311 | + * 发送设备上线消息 | |
| 312 | + */ | |
| 313 | + private void sendOnlineMessage(IotDeviceAuthUtils.DeviceInfo deviceInfo) { | |
| 220 | 314 | try { |
| 221 | - Object responseData = buildResponseData(success, message); | |
| 222 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, | |
| 223 | - success ? 0 : 401, message); | |
| 224 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 225 | - socket.write(Buffer.buffer(encodedData)); | |
| 226 | - log.debug("[sendResponse][发送响应] success: {}, message: {}, requestId: {}", success, message, requestId); | |
| 315 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 316 | + deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), | |
| 317 | + deviceInfo.getDeviceName(), serverId); | |
| 227 | 318 | } catch (Exception e) { |
| 228 | - log.error("[sendResponse][发送响应失败] requestId: {}", requestId, e); | |
| 319 | + log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", deviceInfo.getDeviceName(), e); | |
| 229 | 320 | } |
| 230 | 321 | } |
| 231 | 322 | |
| 232 | 323 | /** |
| 233 | - * 构建响应数据(不返回 token) | |
| 324 | + * 清理连接 | |
| 234 | 325 | */ |
| 235 | - private Object buildResponseData(boolean success, String message) { | |
| 236 | - return MapUtil.builder() | |
| 237 | - .put("success", success) | |
| 238 | - .put("message", message) | |
| 239 | - .build(); | |
| 326 | + private void cleanupConnection(NetSocket socket) { | |
| 327 | + try { | |
| 328 | + // 发送离线消息(如果已认证) | |
| 329 | + IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket); | |
| 330 | + if (authInfo != null) { | |
| 331 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 332 | + deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), | |
| 333 | + authInfo.getDeviceName(), serverId); | |
| 334 | + } | |
| 335 | + | |
| 336 | + // 注销连接 | |
| 337 | + connectionManager.unregisterConnection(socket); | |
| 338 | + } catch (Exception e) { | |
| 339 | + log.error("[cleanupConnection][清理连接失败]", e); | |
| 340 | + } | |
| 240 | 341 | } |
| 241 | 342 | |
| 242 | 343 | /** |
| 243 | - * 清理会话 | |
| 344 | + * 发送响应消息 | |
| 244 | 345 | */ |
| 245 | - private void cleanupSession(NetSocket socket) { | |
| 246 | - // 如果已认证,发送离线消息 | |
| 247 | - IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); | |
| 248 | - if (authInfo != null) { | |
| 249 | - // 发送离线消息 | |
| 250 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 251 | - deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), authInfo.getDeviceName(), | |
| 252 | - serverId); | |
| 346 | + private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { | |
| 347 | + try { | |
| 348 | + Object responseData = MapUtil.builder() | |
| 349 | + .put("success", success) | |
| 350 | + .put("message", message) | |
| 351 | + .build(); | |
| 352 | + | |
| 353 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, | |
| 354 | + success ? 0 : 401, message); | |
| 355 | + | |
| 356 | + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 357 | + socket.write(Buffer.buffer(encodedData)); | |
| 358 | + | |
| 359 | + } catch (Exception e) { | |
| 360 | + log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); | |
| 253 | 361 | } |
| 254 | - sessionManager.unregisterSession(socket); | |
| 255 | - authManager.unregisterAuth(socket); | |
| 256 | 362 | } |
| 257 | 363 | |
| 364 | + // ==================== 辅助方法 ==================== | |
| 365 | + | |
| 258 | 366 | /** |
| 259 | - * 判断是否为 JSON 格式 | |
| 367 | + * 判断是否为认证请求 | |
| 260 | 368 | */ |
| 261 | - private boolean isJsonFormat(String data) { | |
| 262 | - if (StrUtil.isBlank(data)) { | |
| 263 | - return false; | |
| 264 | - } | |
| 265 | - String trimmed = data.trim(); | |
| 266 | - return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); | |
| 369 | + private boolean isAuthRequest(IotDeviceMessage message) { | |
| 370 | + return AUTH_METHOD.equals(message.getMethod()); | |
| 267 | 371 | } |
| 268 | 372 | |
| 269 | 373 | /** |
| ... | ... | @@ -273,38 +377,37 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 273 | 377 | if (params == null) { |
| 274 | 378 | return null; |
| 275 | 379 | } |
| 276 | - JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params | |
| 277 | - : JSONUtil.parseObj(params.toString()); | |
| 278 | - String clientId = paramsJson.getStr("clientId"); | |
| 279 | - String username = paramsJson.getStr("username"); | |
| 280 | - String password = paramsJson.getStr("password"); | |
| 281 | - return StrUtil.hasBlank(clientId, username, password) ? null : new AuthParams(clientId, username, password); | |
| 380 | + | |
| 381 | + try { | |
| 382 | + JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params | |
| 383 | + : JSONUtil.parseObj(params.toString()); | |
| 384 | + | |
| 385 | + String clientId = paramsJson.getStr("clientId"); | |
| 386 | + String username = paramsJson.getStr("username"); | |
| 387 | + String password = paramsJson.getStr("password"); | |
| 388 | + | |
| 389 | + return StrUtil.hasBlank(clientId, username, password) ? null | |
| 390 | + : new AuthParams(clientId, username, password); | |
| 391 | + } catch (Exception e) { | |
| 392 | + log.warn("[parseAuthParams][解析认证参数失败]", e); | |
| 393 | + return null; | |
| 394 | + } | |
| 282 | 395 | } |
| 283 | 396 | |
| 284 | 397 | /** |
| 285 | 398 | * 认证设备 |
| 286 | 399 | */ |
| 287 | 400 | private boolean authenticateDevice(AuthParams authParams) { |
| 288 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 289 | - .setClientId(authParams.clientId).setUsername(authParams.username).setPassword(authParams.password)); | |
| 290 | - return result.isSuccess() && result.getData(); | |
| 291 | - } | |
| 292 | - | |
| 293 | - /** | |
| 294 | - * 注册认证信息 | |
| 295 | - */ | |
| 296 | - private void registerAuthInfo(NetSocket socket, IotDeviceRespDTO device, IotDeviceAuthUtils.DeviceInfo deviceInfo, | |
| 297 | - String token, String clientId) { | |
| 298 | - // TODO @haohao:可以链式调用; | |
| 299 | - IotTcpAuthManager.AuthInfo auth = new IotTcpAuthManager.AuthInfo(); | |
| 300 | - auth.setDeviceId(device.getId()); | |
| 301 | - auth.setProductKey(deviceInfo.getProductKey()); | |
| 302 | - auth.setDeviceName(deviceInfo.getDeviceName()); | |
| 303 | - auth.setToken(token); | |
| 304 | - auth.setClientId(clientId); | |
| 305 | - | |
| 306 | - authManager.registerAuth(socket, auth); | |
| 307 | - sessionManager.registerSession(device.getId(), socket); | |
| 401 | + try { | |
| 402 | + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 403 | + .setClientId(authParams.clientId) | |
| 404 | + .setUsername(authParams.username) | |
| 405 | + .setPassword(authParams.password)); | |
| 406 | + return result.isSuccess() && Boolean.TRUE.equals(result.getData()); | |
| 407 | + } catch (Exception e) { | |
| 408 | + log.error("[authenticateDevice][设备认证异常,username: {}]", authParams.username, e); | |
| 409 | + return false; | |
| 410 | + } | |
| 308 | 411 | } |
| 309 | 412 | |
| 310 | 413 | /** |
| ... | ... | @@ -315,24 +418,32 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 315 | 418 | } |
| 316 | 419 | |
| 317 | 420 | /** |
| 318 | - * 发送成功响应(不返回 token) | |
| 421 | + * 发送成功响应 | |
| 319 | 422 | */ |
| 320 | 423 | private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { |
| 321 | 424 | sendResponse(socket, true, message, requestId, codecType); |
| 322 | 425 | } |
| 323 | 426 | |
| 324 | - // TODO @haohao:使用 lombok,方便 jdk8 兼容 | |
| 427 | + // ==================== 内部类 ==================== | |
| 325 | 428 | |
| 326 | 429 | /** |
| 327 | 430 | * 认证参数 |
| 328 | 431 | */ |
| 329 | - private record AuthParams(String clientId, String username, String password) { | |
| 432 | + @Data | |
| 433 | + @AllArgsConstructor | |
| 434 | + private static class AuthParams { | |
| 435 | + private final String clientId; | |
| 436 | + private final String username; | |
| 437 | + private final String password; | |
| 330 | 438 | } |
| 331 | 439 | |
| 332 | 440 | /** |
| 333 | 441 | * 消息信息 |
| 334 | 442 | */ |
| 335 | - private record MessageInfo(IotDeviceMessage message, String codecType) { | |
| 443 | + @Data | |
| 444 | + @AllArgsConstructor | |
| 445 | + private static class MessageInfo { | |
| 446 | + private final IotDeviceMessage message; | |
| 447 | + private final String codecType; | |
| 336 | 448 | } |
| 337 | - | |
| 338 | 449 | } |
| 339 | 450 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| ... | ... | @@ -4,6 +4,15 @@ spring: |
| 4 | 4 | profiles: |
| 5 | 5 | active: local # 默认激活本地开发环境 |
| 6 | 6 | |
| 7 | + # Redis 配置 | |
| 8 | + data: | |
| 9 | + redis: | |
| 10 | + host: 127.0.0.1 # Redis 服务器地址 | |
| 11 | + port: 6379 # Redis 服务器端口 | |
| 12 | + database: 0 # Redis 数据库索引 | |
| 13 | + # password: # Redis 密码,如果有的话 | |
| 14 | + timeout: 30000ms # 连接超时时间 | |
| 15 | + | |
| 7 | 16 | --- #################### 消息队列相关 #################### |
| 8 | 17 | |
| 9 | 18 | # rocketmq 配置项,对应 RocketMQProperties 配置类 |
| ... | ... | @@ -45,7 +54,7 @@ yudao: |
| 45 | 54 | # 针对引入的 EMQX 组件的配置 |
| 46 | 55 | # ==================================== |
| 47 | 56 | emqx: |
| 48 | - enabled: false | |
| 57 | + enabled: true | |
| 49 | 58 | http-port: 8090 # MQTT HTTP 服务端口 |
| 50 | 59 | mqtt-host: 127.0.0.1 # MQTT Broker 地址 |
| 51 | 60 | mqtt-port: 1883 # MQTT Broker 端口 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.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 | -import org.junit.jupiter.api.BeforeEach; | |
| 6 | -import org.junit.jupiter.api.Test; | |
| 7 | - | |
| 8 | -import java.util.HashMap; | |
| 9 | -import java.util.Map; | |
| 10 | - | |
| 11 | -import static org.junit.jupiter.api.Assertions.*; | |
| 12 | - | |
| 13 | -/** | |
| 14 | - * TCP 二进制格式数据包单元测试 | |
| 15 | - * | |
| 16 | - * 测试二进制协议创建和解析 TCP 上报数据包和心跳包 | |
| 17 | - * | |
| 18 | - * 二进制协议格式: | |
| 19 | - * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) | |
| 20 | - * | |
| 21 | - * @author 芋道源码 | |
| 22 | - */ | |
| 23 | -@Slf4j | |
| 24 | -class TcpBinaryDataPacketExamplesTest { | |
| 25 | - | |
| 26 | - private IotTcpBinaryDeviceMessageCodec codec; | |
| 27 | - | |
| 28 | - @BeforeEach | |
| 29 | - void setUp() { | |
| 30 | - codec = new IotTcpBinaryDeviceMessageCodec(); | |
| 31 | - } | |
| 32 | - | |
| 33 | - @Test | |
| 34 | - void testDataReport() { | |
| 35 | - log.info("=== 二进制格式数据上报包测试 ==="); | |
| 36 | - | |
| 37 | - // 创建传感器数据 | |
| 38 | - Map<String, Object> sensorData = new HashMap<>(); | |
| 39 | - sensorData.put("temperature", 25.5); | |
| 40 | - sensorData.put("humidity", 60.2); | |
| 41 | - sensorData.put("pressure", 1013.25); | |
| 42 | - sensorData.put("battery", 85); | |
| 43 | - | |
| 44 | - // 创建设备消息 | |
| 45 | - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 46 | - message.setDeviceId(123456L); | |
| 47 | - | |
| 48 | - // 编码 | |
| 49 | - byte[] packet = codec.encode(message); | |
| 50 | - log.info("编码后数据包长度: {} 字节", packet.length); | |
| 51 | - log.info("编码后数据包(HEX): {}", bytesToHex(packet)); | |
| 52 | - | |
| 53 | - // 解码验证 | |
| 54 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 55 | - log.info("解码后消息 ID: {}", decoded.getId()); | |
| 56 | - log.info("解码后请求 ID: {}", decoded.getRequestId()); | |
| 57 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 58 | - log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 59 | - log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 60 | - log.info("解码后参数: {}", decoded.getParams()); | |
| 61 | - | |
| 62 | - // 断言验证 | |
| 63 | - assertNotNull(decoded.getId()); | |
| 64 | - assertEquals("thing.property.post", decoded.getMethod()); | |
| 65 | - assertNotNull(decoded.getParams()); | |
| 66 | - assertTrue(decoded.getParams() instanceof Map); | |
| 67 | - } | |
| 68 | - | |
| 69 | - @Test | |
| 70 | - void testHeartbeat() { | |
| 71 | - log.info("=== 二进制格式心跳包测试 ==="); | |
| 72 | - | |
| 73 | - // 创建心跳消息 | |
| 74 | - IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); | |
| 75 | - heartbeat.setDeviceId(123456L); | |
| 76 | - | |
| 77 | - // 编码 | |
| 78 | - byte[] packet = codec.encode(heartbeat); | |
| 79 | - log.info("心跳包长度: {} 字节", packet.length); | |
| 80 | - log.info("心跳包(HEX): {}", bytesToHex(packet)); | |
| 81 | - | |
| 82 | - // 解码验证 | |
| 83 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 84 | - log.info("解码后消息 ID: {}", decoded.getId()); | |
| 85 | - log.info("解码后请求 ID: {}", decoded.getRequestId()); | |
| 86 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 87 | - log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 88 | - log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 89 | - log.info("解码后参数: {}", decoded.getParams()); | |
| 90 | - | |
| 91 | - // 断言验证 | |
| 92 | - assertNotNull(decoded.getId()); | |
| 93 | - assertEquals("thing.state.online", decoded.getMethod()); | |
| 94 | - } | |
| 95 | - | |
| 96 | - @Test | |
| 97 | - void testComplexDataReport() { | |
| 98 | - log.info("=== 二进制格式复杂数据上报测试 ==="); | |
| 99 | - | |
| 100 | - // 创建复杂设备数据 | |
| 101 | - Map<String, Object> deviceData = new HashMap<>(); | |
| 102 | - | |
| 103 | - // 环境数据 | |
| 104 | - Map<String, Object> environment = new HashMap<>(); | |
| 105 | - environment.put("temperature", 23.8); | |
| 106 | - environment.put("humidity", 55.0); | |
| 107 | - environment.put("co2", 420); | |
| 108 | - deviceData.put("environment", environment); | |
| 109 | - | |
| 110 | - // GPS 数据 | |
| 111 | - Map<String, Object> location = new HashMap<>(); | |
| 112 | - location.put("latitude", 39.9042); | |
| 113 | - location.put("longitude", 116.4074); | |
| 114 | - location.put("altitude", 43.5); | |
| 115 | - deviceData.put("location", location); | |
| 116 | - | |
| 117 | - // 设备状态 | |
| 118 | - Map<String, Object> status = new HashMap<>(); | |
| 119 | - status.put("battery", 78); | |
| 120 | - status.put("signal", -65); | |
| 121 | - status.put("online", true); | |
| 122 | - deviceData.put("status", status); | |
| 123 | - | |
| 124 | - // 创建设备消息 | |
| 125 | - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); | |
| 126 | - message.setDeviceId(789012L); | |
| 127 | - | |
| 128 | - // 编码 | |
| 129 | - byte[] packet = codec.encode(message); | |
| 130 | - log.info("复杂数据包长度: {} 字节", packet.length); | |
| 131 | - log.info("复杂数据包(HEX): {}", bytesToHex(packet)); | |
| 132 | - | |
| 133 | - // 解码验证 | |
| 134 | - IotDeviceMessage decoded = codec.decode(packet); | |
| 135 | - log.info("解码后消息 ID: {}", decoded.getId()); | |
| 136 | - log.info("解码后请求 ID: {}", decoded.getRequestId()); | |
| 137 | - log.info("解码后方法: {}", decoded.getMethod()); | |
| 138 | - log.info("解码后设备 ID: {}", decoded.getDeviceId()); | |
| 139 | - log.info("解码后服务 ID: {}", decoded.getServerId()); | |
| 140 | - log.info("解码后参数: {}", decoded.getParams()); | |
| 141 | - | |
| 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 字节"); | |
| 168 | - } | |
| 169 | - | |
| 170 | - // ==================== 内部辅助方法 ==================== | |
| 171 | - | |
| 172 | - /** | |
| 173 | - * 字节数组转十六进制字符串 | |
| 174 | - * | |
| 175 | - * @param bytes 字节数组 | |
| 176 | - * @return 十六进制字符串 | |
| 177 | - */ | |
| 178 | - private static String bytesToHex(byte[] bytes) { | |
| 179 | - StringBuilder result = new StringBuilder(); | |
| 180 | - for (byte b : bytes) { | |
| 181 | - result.append(String.format("%02X ", b)); | |
| 182 | - } | |
| 183 | - return result.toString().trim(); | |
| 184 | - } | |
| 185 | - | |
| 186 | - /** | |
| 187 | - * 演示数据包结构分析 | |
| 188 | - * | |
| 189 | - * @param packet 数据包 | |
| 190 | - */ | |
| 191 | - private static void analyzePacketStructure(byte[] packet) { | |
| 192 | - if (packet.length < 8) { | |
| 193 | - log.error("数据包长度不足"); | |
| 194 | - return; | |
| 195 | - } | |
| 196 | - | |
| 197 | - int index = 0; | |
| 198 | - | |
| 199 | - // 解析包头(4 字节) - 后续数据长度 | |
| 200 | - int totalLength = ((packet[index] & 0xFF) << 24) | | |
| 201 | - ((packet[index + 1] & 0xFF) << 16) | | |
| 202 | - ((packet[index + 2] & 0xFF) << 8) | | |
| 203 | - (packet[index + 3] & 0xFF); | |
| 204 | - index += 4; | |
| 205 | - log.info("包头 - 后续数据长度: {} 字节", totalLength); | |
| 206 | - | |
| 207 | - // 解析功能码(2 字节) | |
| 208 | - int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); | |
| 209 | - index += 2; | |
| 210 | - log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); | |
| 211 | - | |
| 212 | - // 解析消息序号(2 字节) | |
| 213 | - int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); | |
| 214 | - index += 2; | |
| 215 | - log.info("消息序号: {}", messageId); | |
| 216 | - | |
| 217 | - // 解析包体数据 | |
| 218 | - if (index < packet.length) { | |
| 219 | - String payload = new String(packet, index, packet.length - index); | |
| 220 | - log.info("包体数据: {}", payload); | |
| 221 | - } | |
| 222 | - } | |
| 223 | - | |
| 224 | - /** | |
| 225 | - * 获取功能码名称 | |
| 226 | - * | |
| 227 | - * @param code 功能码 | |
| 228 | - * @return 功能码名称 | |
| 229 | - */ | |
| 230 | - private static String getFunctionCodeName(int code) { | |
| 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 | - }; | |
| 240 | - } | |
| 241 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.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 | -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 | -} | |
| 186 | 0 | \ 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 | -## 1. 二进制协议概述 | |
| 3 | +## 1. 协议概述 | |
| 4 | 4 | |
| 5 | -TCP 二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。该协议采用紧凑的二进制格式,减少数据传输量,提高传输效率。 | |
| 5 | +TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。 | |
| 6 | 6 | |
| 7 | -## 2. 数据包格式 | |
| 7 | +### 1.1 协议特点 | |
| 8 | 8 | |
| 9 | -### 2.1 整体结构 | |
| 9 | +- **高效传输**:完全二进制格式,减少数据传输量 | |
| 10 | +- **版本控制**:内置协议版本号,支持协议升级 | |
| 11 | +- **类型安全**:明确的消息类型标识 | |
| 12 | +- **扩展性**:预留标志位,支持未来功能扩展 | |
| 13 | +- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容 | |
| 14 | + | |
| 15 | +## 2. 协议格式 | |
| 10 | 16 | |
| 11 | -根据代码实现,TCP 二进制协议的数据包格式为: | |
| 17 | +### 2.1 整体结构 | |
| 12 | 18 | |
| 13 | 19 | ``` |
| 14 | -+----------+----------+----------+----------+ | |
| 15 | -| 包头 | 功能码 | 消息序号 | 包体数据 | | |
| 16 | -| 4字节 | 2字节 | 2字节 | 变长 | | |
| 17 | -+----------+----------+----------+----------+ | |
| 20 | ++--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 21 | +| 魔术字 | 版本号 | 消息类型| 消息标志| 消息长度(4字节) | | |
| 22 | ++--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 23 | +| 消息 ID 长度(2字节) | 消息 ID (变长字符串) | | |
| 24 | ++--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 25 | +| 方法名长度(2字节) | 方法名(变长字符串) | | |
| 26 | ++--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 27 | +| 消息体数据(变长) | | |
| 28 | ++--------+--------+--------+--------+--------+--------+--------+--------+ | |
| 18 | 29 | ``` |
| 19 | 30 | |
| 20 | -**注意**:与原始设计相比,实际实现中移除了设备地址字段,简化了协议结构。 | |
| 21 | - | |
| 22 | -### 2.2 字段说明 | |
| 23 | - | |
| 24 | -| 字段 | 长度 | 类型 | 说明 | | |
| 25 | -|------|-----|--------|-----------------| | |
| 26 | -| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | | |
| 27 | -| 功能码 | 2字节 | short | 消息类型标识 | | |
| 28 | -| 消息序号 | 2字节 | short | 消息唯一标识 | | |
| 29 | -| 包体数据 | 变长 | string | JSON 格式的消息内容 | | |
| 31 | +### 2.2 字段详细说明 | |
| 30 | 32 | |
| 31 | -### 2.3 功能码定义 | |
| 33 | +| 字段 | 长度 | 类型 | 说明 | | |
| 34 | +|------|------|------|------| | |
| 35 | +| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 | | |
| 36 | +| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 | | |
| 37 | +| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 | | |
| 38 | +| 消息标志 | 1字节 | byte | 预留字段,用于未来扩展 | | |
| 39 | +| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) | | |
| 40 | +| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 | | |
| 41 | +| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) | | |
| 42 | +| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 | | |
| 43 | +| 方法名 | 变长 | string | 消息方法名(UTF-8编码) | | |
| 44 | +| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 | | |
| 32 | 45 | |
| 33 | -根据代码实现,支持的功能码: | |
| 46 | +**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 | |
| 34 | 47 | |
| 35 | -| 功能码 | 名称 | 说明 | | |
| 36 | -|-----|------|--------------| | |
| 37 | -| 10 | 设备注册 | 设备首次连接时的注册请求 | | |
| 38 | -| 11 | 注册回复 | 服务器对注册请求的回复 | | |
| 39 | -| 20 | 心跳请求 | 设备发送的心跳包 | | |
| 40 | -| 21 | 心跳回复 | 服务器对心跳的回复 | | |
| 41 | -| 30 | 消息上行 | 设备向服务器发送的数据 | | |
| 42 | -| 40 | 消息下行 | 服务器向设备发送的指令 | | |
| 43 | - | |
| 44 | -**常量定义:** | |
| 48 | +### 2.3 协议常量定义 | |
| 45 | 49 | |
| 46 | 50 | ```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" | |
| 51 | +// 协议标识 | |
| 52 | +private static final byte MAGIC_NUMBER = (byte) 0x7E; | |
| 53 | +private static final byte PROTOCOL_VERSION = (byte) 0x01; | |
| 54 | + | |
| 55 | +// 消息类型 | |
| 56 | +public static class MessageType { | |
| 57 | + public static final byte REQUEST = 0x01; // 请求消息 | |
| 58 | + public static final byte RESPONSE = 0x02; // 响应消息 | |
| 70 | 59 | } |
| 71 | -``` | |
| 72 | - | |
| 73 | -### 3.2 字段说明 | |
| 74 | - | |
| 75 | -| 字段名 | 类型 | 必填 | 说明 | | |
| 76 | -|-----------|--------|----|------------------------------| | |
| 77 | -| method | String | 是 | 消息方法,如 `thing.property.post` | | |
| 78 | -| params | Object | 否 | 消息参数 | | |
| 79 | -| timestamp | Long | 是 | 时间戳(毫秒) | | |
| 80 | -| requestId | String | 否 | 请求唯一标识 | | |
| 81 | -| msgId | String | 否 | 消息唯一标识 | | |
| 82 | - | |
| 83 | -**常量定义:** | |
| 84 | 60 | |
| 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"; | |
| 61 | +// 协议长度 | |
| 62 | +private static final int HEADER_FIXED_LENGTH = 8; // 固定头部长度 | |
| 63 | +private static final int MIN_MESSAGE_LENGTH = 12; // 最小消息长度 | |
| 91 | 64 | ``` |
| 92 | 65 | |
| 93 | -## 4. 消息类型 | |
| 94 | - | |
| 95 | -### 4.1 数据上报 (thing.property.post) | |
| 66 | +## 3. 消息类型和格式 | |
| 96 | 67 | |
| 97 | -设备向服务器上报属性数据。 | |
| 68 | +### 3.1 请求消息 (REQUEST - 0x01) | |
| 98 | 69 | |
| 99 | -**功能码:** 30 (CODE_MESSAGE_UP) | |
| 70 | +请求消息用于设备向服务器发送数据或请求。 | |
| 100 | 71 | |
| 101 | -**包体数据示例:** | |
| 102 | - | |
| 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 | -} | |
| 72 | +#### 3.1.1 消息体格式 | |
| 114 | 73 | ``` |
| 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 | -} | |
| 74 | +消息体 = params 数据(JSON格式) | |
| 131 | 75 | ``` |
| 132 | 76 | |
| 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 | -``` | |
| 77 | +#### 3.1.2 示例:设备认证请求 | |
| 139 | 78 | |
| 140 | -## 5. 数据包示例 | |
| 141 | - | |
| 142 | -### 5.1 温度传感器数据上报 | |
| 143 | - | |
| 144 | -**包体数据:** | |
| 145 | -```json | |
| 146 | -{ | |
| 147 | - "method": "thing.property.post", | |
| 148 | - "params": { | |
| 149 | - "temperature": 25.5, | |
| 150 | - "humidity": 60.2, | |
| 151 | - "pressure": 1013.25 | |
| 152 | - }, | |
| 153 | - "timestamp": 1642781234567, | |
| 154 | - "requestId": "req_001" | |
| 155 | -} | |
| 156 | -``` | |
| 79 | +**消息内容:** | |
| 80 | +- 消息 ID: `auth_1704067200000_123` | |
| 81 | +- 方法名: `auth` | |
| 82 | +- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}` | |
| 157 | 83 | |
| 158 | -**数据包结构:** | |
| 84 | +**二进制数据包结构:** | |
| 159 | 85 | ``` |
| 160 | -包头: 0x00000045 (69字节) | |
| 161 | -功能码: 0x001E (30 - 消息上行) | |
| 162 | -消息序号: 0x1234 (4660) | |
| 163 | -包体: JSON字符串 | |
| 86 | +7E // 魔术字 (0x7E) | |
| 87 | +01 // 版本号 (0x01) | |
| 88 | +01 // 消息类型 (REQUEST) | |
| 89 | +00 // 消息标志 (预留) | |
| 90 | +00 00 00 8A // 消息长度 (138字节) | |
| 91 | +00 19 // 消息 ID 长度 (25字节) | |
| 92 | +61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123" | |
| 93 | +36 37 32 30 30 30 30 30 5F 31 | |
| 94 | +32 33 | |
| 95 | +00 04 // 方法名长度 (4字节) | |
| 96 | +61 75 74 68 // 方法名: "auth" | |
| 97 | +7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据 | |
| 98 | +22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001", | |
| 99 | +30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName", | |
| 100 | +6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"} | |
| 101 | +64 75 63 74 4B 65 79 5F 64 65 | |
| 102 | +76 69 63 65 4E 61 6D 65 22 2C | |
| 103 | +22 70 61 73 73 77 6F 72 64 22 | |
| 104 | +3A 22 64 65 76 69 63 65 5F 70 | |
| 105 | +61 73 73 77 6F 72 64 22 7D | |
| 164 | 106 | ``` |
| 165 | 107 | |
| 166 | -**完整十六进制数据包:** | |
| 167 | -``` | |
| 168 | -00 00 00 45 00 1E 12 34 | |
| 169 | -7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 | |
| 170 | -2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C | |
| 171 | -22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 | |
| 172 | -72 61 74 75 72 65 22 3A 32 35 2E 35 2C 22 68 75 | |
| 173 | -6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 | |
| 174 | -65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D | |
| 175 | -2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 | |
| 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 | |
| 178 | -``` | |
| 108 | +#### 3.1.3 示例:属性数据上报 | |
| 179 | 109 | |
| 180 | -### 5.2 心跳包示例 | |
| 110 | +**消息内容:** | |
| 111 | +- 消息 ID: `property_1704067200000_456` | |
| 112 | +- 方法名: `thing.property.post` | |
| 113 | +- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}` | |
| 181 | 114 | |
| 182 | -**包体数据:** | |
| 183 | -```json | |
| 184 | -{ | |
| 185 | - "method": "thing.state.online", | |
| 186 | - "params": {}, | |
| 187 | - "timestamp": 1642781234567, | |
| 188 | - "requestId": "req_002" | |
| 189 | -} | |
| 190 | -``` | |
| 115 | +### 3.2 响应消息 (RESPONSE - 0x02) | |
| 191 | 116 | |
| 192 | -**数据包结构:** | |
| 193 | -``` | |
| 194 | -包头: 0x00000028 (40字节) | |
| 195 | -功能码: 0x0014 (20 - 心跳请求) | |
| 196 | -消息序号: 0x5678 (22136) | |
| 197 | -包体: JSON字符串 | |
| 198 | -``` | |
| 117 | +响应消息用于服务器向设备回复请求结果。 | |
| 199 | 118 | |
| 200 | -**完整十六进制数据包:** | |
| 119 | +#### 3.2.1 消息体格式 | |
| 201 | 120 | ``` |
| 202 | -00 00 00 28 00 14 56 78 | |
| 203 | -7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 | |
| 204 | -2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 | |
| 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 | |
| 121 | +消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON) | |
| 209 | 122 | ``` |
| 210 | 123 | |
| 211 | -## 6. 编解码器实现 | |
| 124 | +#### 3.2.2 字段说明 | |
| 212 | 125 | |
| 213 | -### 6.1 编码器类型 | |
| 126 | +| 字段 | 长度 | 类型 | 说明 | | |
| 127 | +|------|------|------|------| | |
| 128 | +| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 | | |
| 129 | +| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 | | |
| 130 | +| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) | | |
| 131 | +| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) | | |
| 214 | 132 | |
| 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()); | |
| 246 | -} | |
| 247 | -``` | |
| 248 | - | |
| 249 | -## 7. 数据包解析步骤 | |
| 250 | - | |
| 251 | -### 7.1 解析流程 | |
| 252 | - | |
| 253 | -1. **读取包头(4字节)** | |
| 254 | - - 获取后续数据的总长度 | |
| 255 | - - 验证数据包完整性 | |
| 256 | - | |
| 257 | -2. **读取功能码(2字节)** | |
| 258 | - - 确定消息类型 | |
| 259 | - | |
| 260 | -3. **读取消息序号(2字节)** | |
| 261 | - - 获取消息唯一标识 | |
| 262 | - | |
| 263 | -4. **读取包体数据(变长)** | |
| 264 | - - 解析 JSON 格式的消息内容 | |
| 265 | - | |
| 266 | -### 7.2 Java 解析示例 | |
| 133 | +#### 3.2.3 示例:认证成功响应 | |
| 267 | 134 | |
| 268 | -```java | |
| 269 | -public TcpDataPackage parsePacket(byte[] packet) { | |
| 270 | - int index = 0; | |
| 271 | - | |
| 272 | - // 1. 解析包头 | |
| 273 | - int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); | |
| 274 | - index += 4; | |
| 275 | - | |
| 276 | - // 2. 解析功能码 | |
| 277 | - short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); | |
| 278 | - index += 2; | |
| 279 | - | |
| 280 | - // 3. 解析消息序号 | |
| 281 | - short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); | |
| 282 | - index += 2; | |
| 135 | +**消息内容:** | |
| 136 | +- 消息 ID: `auth_response_1704067200000_123` | |
| 137 | +- 方法名: `auth` | |
| 138 | +- 响应码: `0` | |
| 139 | +- 响应消息: `认证成功` | |
| 140 | +- 响应数据: `{"success":true,"message":"认证成功"}` | |
| 283 | 141 | |
| 284 | - // 4. 解析包体数据 | |
| 285 | - String payload = new String(packet, index, packet.length - index); | |
| 286 | - | |
| 287 | - return new TcpDataPackage(functionCode, messageId, payload); | |
| 288 | -} | |
| 142 | +**二进制数据包结构:** | |
| 289 | 143 | ``` |
| 290 | - | |
| 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); | |
| 144 | +7E // 魔术字 (0x7E) | |
| 145 | +01 // 版本号 (0x01) | |
| 146 | +02 // 消息类型 (RESPONSE) | |
| 147 | +00 // 消息标志 (预留) | |
| 148 | +00 00 00 A5 // 消息长度 (165字节) | |
| 149 | +00 22 // 消息 ID 长度 (34字节) | |
| 150 | +61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123" | |
| 151 | +6E 73 65 5F 31 37 30 34 30 36 | |
| 152 | +37 32 30 30 30 30 30 5F 31 32 | |
| 153 | +33 | |
| 154 | +00 04 // 方法名长度 (4字节) | |
| 155 | +61 75 74 68 // 方法名: "auth" | |
| 156 | +00 00 00 00 // 响应码 (0 = 成功) | |
| 157 | +00 0C // 响应消息长度 (12字节) | |
| 158 | +E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8) | |
| 159 | +8A 9F | |
| 160 | +7B 22 73 75 63 63 65 73 73 22 // JSON响应数据 | |
| 161 | +3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"} | |
| 162 | +73 61 67 65 22 3A 22 E8 AE A4 | |
| 163 | +E8 AF 81 E6 88 90 E5 8A 9F 22 | |
| 164 | +7D | |
| 311 | 165 | ``` |
| 312 | 166 | |
| 313 | -### 8.2 错误处理 | |
| 167 | +## 4. 编解码器标识 | |
| 314 | 168 | |
| 315 | 169 | ```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 | - } | |
| 170 | +public static final String TYPE = "TCP_BINARY"; | |
| 332 | 171 | ``` |
| 333 | 172 | |
| 334 | -## 9. 注意事项 | |
| 335 | - | |
| 336 | -1. **字节序**:所有多字节数据使用大端序(Big-Endian) | |
| 337 | -2. **字符编码**:字符串数据使用 UTF-8 编码 | |
| 338 | -3. **JSON 格式**:包体数据必须是有效的 JSON 格式 | |
| 339 | -4. **长度限制**:单个数据包建议不超过 1MB | |
| 340 | -5. **错误处理**:解析失败时会抛出 `TcpCodecException` | |
| 341 | -6. **功能码映射**:目前只支持心跳和数据上报两种消息类型 | |
| 173 | +## 5. 协议优势 | |
| 342 | 174 | |
| 343 | -## 10. 协议特点 | |
| 175 | +- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量 | |
| 176 | +- **解析高效**:直接二进制操作,减少字符串转换开销 | |
| 177 | +- **类型安全**:明确的消息类型和字段定义 | |
| 178 | +- **扩展性强**:预留标志位支持未来功能扩展 | |
| 179 | +- **版本控制**:内置版本号支持协议升级 | |
| 344 | 180 | |
| 345 | -### 10.1 优势 | |
| 181 | +## 6. 与 JSON 协议对比 | |
| 346 | 182 | |
| 347 | -- **高效传输**:二进制格式,数据量小 | |
| 348 | -- **性能优化**:减少解析开销 | |
| 349 | -- **带宽节省**:相比 JSON 格式节省带宽 | |
| 350 | -- **实时性好**:适合高频数据传输 | |
| 351 | - | |
| 352 | -### 10.2 适用场景 | |
| 183 | +| 特性 | 二进制协议 | JSON协议 | | |
| 184 | +|------|------------|----------| | |
| 185 | +| 数据大小 | 小(节省30-50%) | 大 | | |
| 186 | +| 解析性能 | 高 | 中等 | | |
| 187 | +| 网络开销 | 低 | 高 | | |
| 188 | +| 可读性 | 差 | 优秀 | | |
| 189 | +| 调试难度 | 高 | 低 | | |
| 190 | +| 扩展性 | 良好(有预留位) | 优秀 | | |
| 353 | 191 | |
| 192 | +**推荐场景**: | |
| 354 | 193 | - ✅ **高频数据传输**:传感器数据实时上报 |
| 355 | 194 | - ✅ **带宽受限环境**:移动网络、卫星通信 |
| 356 | -- ✅ **性能要求高**:需要低延迟的场景 | |
| 357 | -- ✅ **设备资源有限**:嵌入式设备、IoT 设备 | |
| 358 | - | |
| 359 | -### 10.3 与 JSON 协议对比 | |
| 360 | - | |
| 361 | -| 特性 | 二进制协议 | JSON 协议 | | |
| 362 | -|-------|-------|---------| | |
| 363 | -| 数据大小 | 小 | 稍大 | | |
| 364 | -| 解析性能 | 高 | 中等 | | |
| 365 | -| 可读性 | 差 | 优秀 | | |
| 366 | -| 调试难度 | 高 | 低 | | |
| 367 | -| 扩展性 | 差 | 优秀 | | |
| 368 | -| 实现复杂度 | 高 | 低 | | |
| 369 | - | |
| 370 | -这样就完成了 TCP 二进制协议的完整说明,与实际代码实现完全一致。 | |
| 195 | +- ✅ **性能要求高**:需要低延迟、高吞吐的场景 | |
| 196 | +- ✅ **设备资源有限**:嵌入式设备、低功耗设备 | |
| 197 | +- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议 | |
| 198 | +- ❌ **快速原型开发**:开发效率低 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md
| ... | ... | @@ -2,13 +2,13 @@ |
| 2 | 2 | |
| 3 | 3 | ## 1. å议概述 |
| 4 | 4 | |
| 5 | -TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,å‚考了 EMQX å’Œ HTTP 模å—çš„æ•°æ®æ ¼å¼è®¾è®¡ï¼Œå…·æœ‰ä»¥ä¸‹ä¼˜åŠ¿ï¼š | |
| 5 | +TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,具有以下特点: | |
| 6 | 6 | |
| 7 | 7 | - **æ ‡å‡†åŒ–**ï¼šä½¿ç”¨æ ‡å‡† JSON æ ¼å¼ï¼Œæ˜“于解æžå’Œå¤„ç† |
| 8 | 8 | - **å¯è¯»æ€§**:人类å¯è¯»ï¼Œä¾¿äºŽè°ƒè¯•和维护 |
| 9 | 9 | - **扩展性**:å¯ä»¥è½»æ¾æ·»åŠ æ–°å—æ®µï¼Œå‘åŽå…¼å®¹ |
| 10 | -- **统一性**:与 HTTP 模å—ä¿æŒä¸€è‡´çš„æ•°æ®æ ¼å¼ | |
| 11 | -- **简化性**:相比二进制å议,实现更简å•,调试更容易 | |
| 10 | +- **跨平å°**:JSON æ ¼å¼æ”¯æŒæ‰€æœ‰ä¸»æµç¼–程è¯è¨€ | |
| 11 | +- **安全优化**:移除冗余的 deviceId å—æ®µï¼Œæé«˜å®‰å…¨æ€§ | |
| 12 | 12 | |
| 13 | 13 | ## 2. æ¶ˆæ¯æ ¼å¼ |
| 14 | 14 | |
| ... | ... | @@ -18,294 +18,174 @@ TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,å‚考了 EMQX å’ |
| 18 | 18 | { |
| 19 | 19 | "id": "消æ¯å”¯ä¸€æ ‡è¯†", |
| 20 | 20 | "method": "æ¶ˆæ¯æ–¹æ³•", |
| 21 | - "deviceId": 设备ID, | |
| 22 | 21 | "params": { |
| 23 | - // 消æ¯å‚æ•° | |
| 22 | + // è¯·æ±‚å‚æ•° | |
| 23 | + }, | |
| 24 | + "data": { | |
| 25 | + // å“åº”æ•°æ® | |
| 24 | 26 | }, |
| 25 | - "timestamp": 时间戳, | |
| 26 | 27 | "code": å“应ç , |
| 27 | - "message": "å“应消æ¯" | |
| 28 | + "msg": "å“应消æ¯", | |
| 29 | + "timestamp": 时间戳 | |
| 28 | 30 | } |
| 29 | 31 | ``` |
| 30 | 32 | |
| 31 | -### 2.2 å—æ®µè¯´æ˜Ž | |
| 33 | +**âš ï¸ é‡è¦è¯´æ˜Ž**: | |
| 34 | +- **ä¸åŒ…å« deviceId å—æ®µ**:由æœåŠ¡å™¨é€šè¿‡ TCP 连接上下文自动确定设备 ID | |
| 35 | +- **é¿å…ä¼ªé€ æ”»å‡»**:防æ¢è®¾å¤‡ä¼ªé€ 其他设备的 ID å‘逿¶ˆæ¯ | |
| 32 | 36 | |
| 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 | å¦ | å“应消æ¯ï¼ˆä¸‹è¡Œæ¶ˆæ¯ä½¿ç”¨ï¼‰ | | |
| 37 | +### 2.2 å—æ®µè¯¦ç»†è¯´æ˜Ž | |
| 42 | 38 | |
| 43 | -## 3. 消æ¯ç±»åž‹ | |
| 39 | +| å—æ®µå | 类型 | å¿…å¡« | 用途 | 说明 | | |
| 40 | +|--------|------|------|------|------| | |
| 41 | +| id | String | 是 | æ‰€æœ‰æ¶ˆæ¯ | 消æ¯å”¯ä¸€æ ‡è¯† | | |
| 42 | +| method | String | 是 | æ‰€æœ‰æ¶ˆæ¯ | æ¶ˆæ¯æ–¹æ³•,如 `auth`ã€`thing.property.post` | | |
| 43 | +| params | Object | å¦ | è¯·æ±‚æ¶ˆæ¯ | è¯·æ±‚å‚æ•°ï¼Œå…·ä½“å†…å®¹æ ¹æ®method而定 | | |
| 44 | +| data | Object | å¦ | å“åº”æ¶ˆæ¯ | å“应数æ®ï¼ŒæœåŠ¡å™¨è¿”å›žçš„ç»“æžœæ•°æ® | | |
| 45 | +| code | Integer | å¦ | å“åº”æ¶ˆæ¯ | å“应ç ,0=æˆåŠŸï¼Œå…¶ä»–=错误 | | |
| 46 | +| msg | String | å¦ | å“åº”æ¶ˆæ¯ | å“应æç¤ºä¿¡æ¯ | | |
| 47 | +| timestamp | Long | 是 | æ‰€æœ‰æ¶ˆæ¯ | æ—¶é—´æˆ³ï¼ˆæ¯«ç§’ï¼‰ï¼Œç¼–ç æ—¶è‡ªåŠ¨ç”Ÿæˆ | | |
| 44 | 48 | |
| 45 | -### 3.1 è®¾å¤‡è®¤è¯ (auth) | |
| 49 | +### 2.3 消æ¯åˆ†ç±» | |
| 46 | 50 | |
| 47 | -设备连接åŽé¦–先需è¦è¿›è¡Œè®¤è¯ï¼Œè®¤è¯æˆåŠŸåŽæ‰èƒ½è¿›è¡Œå…¶ä»–æ“作。 | |
| 51 | +#### 2.3.1 请求消æ¯ï¼ˆä¸Šè¡Œï¼‰ | |
| 52 | +- **特å¾**ï¼šåŒ…å« `params` å—æ®µï¼Œä¸åŒ…å« `code`ã€`msg` å—æ®µ | |
| 53 | +- **æ–¹å‘**:设备 → æœåС噍 | |
| 54 | +- **用途**:设备认è¯ã€æ•°æ®ä¸ŠæŠ¥ã€çŠ¶æ€æ›´æ–°ç‰ | |
| 48 | 55 | |
| 49 | -#### 3.1.1 认è¯è¯·æ±‚æ ¼å¼ | |
| 56 | +#### 2.3.2 å“应消æ¯ï¼ˆä¸‹è¡Œï¼‰ | |
| 57 | +- **特å¾**ï¼šåŒ…å« `code`ã€`msg` å—æ®µï¼Œå¯èƒ½åŒ…å« `data` å—æ®µ | |
| 58 | +- **æ–¹å‘**:æœåС噍 → 设备 | |
| 59 | +- **用途**:认è¯ç»“æžœã€æŒ‡ä»¤å“应ã€é”™è¯¯æç¤ºç‰ | |
| 50 | 60 | |
| 51 | -**示例:** | |
| 61 | +## 3. 消æ¯ç¤ºä¾‹ | |
| 62 | + | |
| 63 | +### 3.1 è®¾å¤‡è®¤è¯ (auth) | |
| 64 | + | |
| 65 | +#### 认è¯è¯·æ±‚æ ¼å¼ | |
| 66 | +**æ¶ˆæ¯æ–¹å‘**:设备 → æœåС噍 | |
| 52 | 67 | |
| 53 | 68 | ```json |
| 54 | 69 | { |
| 55 | - "id": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 70 | + "id": "auth_1704067200000_123", | |
| 56 | 71 | "method": "auth", |
| 57 | 72 | "params": { |
| 58 | 73 | "clientId": "device_001", |
| 59 | 74 | "username": "productKey_deviceName", |
| 60 | 75 | "password": "设备密ç " |
| 61 | 76 | }, |
| 62 | - "timestamp": 1753111026437 | |
| 77 | + "timestamp": 1704067200000 | |
| 63 | 78 | } |
| 64 | 79 | ``` |
| 65 | 80 | |
| 66 | -**å—æ®µè¯´æ˜Žï¼š** | |
| 81 | +**认è¯å‚数说明:** | |
| 82 | + | |
| 67 | 83 | | å—æ®µå | 类型 | å¿…å¡« | 说明 | |
| 68 | 84 | |--------|------|------|------| |
| 69 | -| clientId | String | 是 | å®¢æˆ·ç«¯å”¯ä¸€æ ‡è¯† | | |
| 85 | +| clientId | String | 是 | å®¢æˆ·ç«¯å”¯ä¸€æ ‡è¯†ï¼Œç”¨äºŽè¿žæŽ¥ç®¡ç† | | |
| 70 | 86 | | username | String | 是 | 设备用户åï¼Œæ ¼å¼ä¸º `productKey_deviceName` | |
| 71 | -| password | String | 是 | 设备密ç | | |
| 87 | +| password | String | 是 | 设备密ç ,在设备管ç†å¹³å°é…ç½® | | |
| 72 | 88 | |
| 73 | -#### 3.1.2 认è¯å“åº”æ ¼å¼ | |
| 89 | +#### 认è¯å“åº”æ ¼å¼ | |
| 90 | +**æ¶ˆæ¯æ–¹å‘**:æœåС噍 → 设备 | |
| 74 | 91 | |
| 75 | 92 | **è®¤è¯æˆåŠŸå“应:** |
| 76 | - | |
| 77 | 93 | ```json |
| 78 | 94 | { |
| 79 | - "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 80 | - "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 95 | + "id": "response_auth_1704067200000_123", | |
| 81 | 96 | "method": "auth", |
| 82 | 97 | "data": { |
| 83 | 98 | "success": true, |
| 84 | 99 | "message": "è®¤è¯æˆåŠŸ" |
| 85 | 100 | }, |
| 86 | 101 | "code": 0, |
| 87 | - "msg": "è®¤è¯æˆåŠŸ" | |
| 102 | + "msg": "è®¤è¯æˆåŠŸ", | |
| 103 | + "timestamp": 1704067200001 | |
| 88 | 104 | } |
| 89 | 105 | ``` |
| 90 | 106 | |
| 91 | 107 | **认è¯å¤±è´¥å“应:** |
| 92 | - | |
| 93 | 108 | ```json |
| 94 | 109 | { |
| 95 | - "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 96 | - "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 110 | + "id": "response_auth_1704067200000_123", | |
| 97 | 111 | "method": "auth", |
| 98 | 112 | "data": { |
| 99 | 113 | "success": false, |
| 100 | 114 | "message": "认è¯å¤±è´¥ï¼šç”¨æˆ·å或密ç 错误" |
| 101 | 115 | }, |
| 102 | 116 | "code": 401, |
| 103 | - "msg": "认è¯å¤±è´¥ï¼šç”¨æˆ·å或密ç 错误" | |
| 117 | + "msg": "认è¯å¤±è´¥", | |
| 118 | + "timestamp": 1704067200001 | |
| 104 | 119 | } |
| 105 | 120 | ``` |
| 106 | 121 | |
| 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. **ä¼šè¯æ³¨å†Œ** → 注册设备会è¯ï¼Œå…许åŽç»ä¸šåŠ¡æ“作 | |
| 116 | - | |
| 117 | -#### 3.1.4 认è¯é”™è¯¯ç | |
| 122 | +### 3.2 属性数æ®ä¸ŠæŠ¥ (thing.property.post) | |
| 118 | 123 | |
| 119 | -| 错误ç | 说明 | 处ç†å»ºè®® | | |
| 120 | -|-----|-------|--------------| | |
| 121 | -| 401 | 认è¯å¤±è´¥ | 检查用户åã€å¯†ç æ˜¯å¦æ£ç¡® | | |
| 122 | -| 400 | 傿•°é”™è¯¯ | 检查认è¯å‚数是å¦å®Œæ•´ | | |
| 123 | -| 404 | 设备ä¸å˜åœ¨ | 检查设备是å¦å·²æ³¨å†Œ | | |
| 124 | -| 500 | æœåŠ¡å™¨é”™è¯¯ | è”系管ç†å‘˜ | | |
| 124 | +**æ¶ˆæ¯æ–¹å‘**:设备 → æœåС噍 | |
| 125 | 125 | |
| 126 | -### 3.2 æ•°æ®ä¸ŠæŠ¥ (thing.property.post) | |
| 127 | - | |
| 128 | -è®¾å¤‡å‘æœåŠ¡å™¨ä¸ŠæŠ¥å±žæ€§æ•°æ®ã€‚ | |
| 129 | - | |
| 130 | -**示例:** | |
| 126 | +**ç¤ºä¾‹ï¼šæ¸©åº¦ä¼ æ„Ÿå™¨æ•°æ®ä¸ŠæŠ¥** | |
| 131 | 127 | ```json |
| 132 | 128 | { |
| 133 | - "id": "8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 129 | + "id": "property_1704067200000_456", | |
| 134 | 130 | "method": "thing.property.post", |
| 135 | - "deviceId": 8, | |
| 136 | 131 | "params": { |
| 137 | 132 | "temperature": 25.5, |
| 138 | 133 | "humidity": 60.2, |
| 139 | 134 | "pressure": 1013.25, |
| 140 | - "battery": 85 | |
| 135 | + "battery": 85, | |
| 136 | + "signal_strength": -65 | |
| 141 | 137 | }, |
| 142 | - "timestamp": 1753111026437 | |
| 138 | + "timestamp": 1704067200000 | |
| 143 | 139 | } |
| 144 | 140 | ``` |
| 145 | 141 | |
| 146 | -### 3.3 心跳 (thing.state.update) | |
| 142 | +### 3.3 è®¾å¤‡çŠ¶æ€æ›´æ–° (thing.state.update) | |
| 147 | 143 | |
| 148 | -è®¾å¤‡å‘æœåС噍å‘é€å¿ƒè·³ä¿æ´»ã€‚ | |
| 144 | +**æ¶ˆæ¯æ–¹å‘**:设备 → æœåС噍 | |
| 149 | 145 | |
| 150 | -**示例:** | |
| 146 | +**示例:心跳请求** | |
| 151 | 147 | ```json |
| 152 | 148 | { |
| 153 | - "id": "7db8c4e6408b40f8b2549ddd94f6bb02", | |
| 149 | + "id": "heartbeat_1704067200000_321", | |
| 154 | 150 | "method": "thing.state.update", |
| 155 | - "deviceId": 8, | |
| 156 | 151 | "params": { |
| 157 | - "state": "1" | |
| 152 | + "state": "online", | |
| 153 | + "uptime": 86400, | |
| 154 | + "memory_usage": 65.2, | |
| 155 | + "cpu_usage": 12.8 | |
| 158 | 156 | }, |
| 159 | - "timestamp": 1753111026467 | |
| 157 | + "timestamp": 1704067200000 | |
| 160 | 158 | } |
| 161 | 159 | ``` |
| 162 | 160 | |
| 163 | -### 3.4 æ¶ˆæ¯æ–¹æ³•å¸¸é‡ | |
| 164 | - | |
| 165 | -支æŒçš„æ¶ˆæ¯æ–¹æ³•: | |
| 166 | - | |
| 167 | -- `auth` - è®¾å¤‡è®¤è¯ | |
| 168 | -- `thing.property.post` - æ•°æ®ä¸ŠæŠ¥ | |
| 169 | -- `thing.state.update` - 心跳 | |
| 170 | - | |
| 171 | -## 4. å议特点 | |
| 172 | - | |
| 173 | -### 4.1 优势 | |
| 174 | - | |
| 175 | -- **ç®€å•æ˜“用**:纯 JSON æ ¼å¼ï¼Œæ— éœ€å¤æ‚çš„äºŒè¿›åˆ¶è§£æž | |
| 176 | -- **调试å‹å¥½**:å¯ä»¥ç›´æŽ¥æŸ¥çœ‹æ¶ˆæ¯å†…容 | |
| 177 | -- **扩展性强**:å¯ä»¥è½»æ¾æ·»åŠ æ–°å—æ®µ | |
| 178 | -- **æ ‡å‡†åŒ–**:与 EMQX ç‰ä¸»æµå¹³å°æ ¼å¼å…¼å®¹ | |
| 179 | -- **错误处ç†**:æä¾›è¯¦ç»†çš„错误信æ¯å’Œå¼‚å¸¸å¤„ç† | |
| 180 | -- **安全性**:支æŒè®¾å¤‡è®¤è¯æœºåˆ¶ | |
| 181 | - | |
| 182 | -### 4.2 与二进制å议对比 | |
| 183 | - | |
| 184 | -| 特性 | 二进制åè®® | JSON åè®® | | |
| 185 | -|-------|-------|----------| | |
| 186 | -| å¯è¯»æ€§ | å·® | 优秀 | | |
| 187 | -| 调试难度 | 高 | 低 | | |
| 188 | -| 扩展性 | 差 | 优秀 | | |
| 189 | -| è§£æžå¤æ‚度 | 高 | 低 | | |
| 190 | -| æ•°æ®å¤§å° | å° | ç¨å¤§ | | |
| 191 | -| æ ‡å‡†åŒ–ç¨‹åº¦ | 低 | 高 | | |
| 192 | -| å®žçŽ°å¤æ‚度 | 高 | 低 | | |
| 193 | -| 安全性 | 一般 | 优秀(支æŒè®¤è¯ï¼‰ | | |
| 194 | - | |
| 195 | -### 4.3 适用场景 | |
| 196 | - | |
| 197 | -- ✅ **å¼€å‘调试**:JSON æ ¼å¼ä¾¿äºŽæŸ¥çœ‹å’Œè°ƒè¯• | |
| 198 | -- ✅ **快速集æˆ**ï¼šæ ‡å‡† JSON æ ¼å¼ï¼Œé›†æˆç®€å• | |
| 199 | -- ✅ **å议扩展**:å¯ä»¥è½»æ¾æ·»åŠ æ–°å—æ®µ | |
| 200 | -- ✅ **多è¯è¨€æ”¯æŒ**:JSON æ ¼å¼æ”¯æŒæ‰€æœ‰ä¸»æµè¯è¨€ | |
| 201 | -- ✅ **云平å°å¯¹æŽ¥**ï¼šä¸Žä¸»æµ IoT äº‘å¹³å°æ ¼å¼å…¼å®¹ | |
| 202 | -- ✅ **å®‰å…¨è¦æ±‚**:支æŒè®¾å¤‡è®¤è¯å’Œè®¿é—®æŽ§åˆ¶ | |
| 203 | - | |
| 204 | -## 5. 最佳实践 | |
| 205 | - | |
| 206 | -### 5.1 è®¤è¯æœ€ä½³å®žè·µ | |
| 207 | - | |
| 208 | -1. **连接å³è®¤è¯**:设备连接åŽç«‹å³è¿›è¡Œè®¤è¯ | |
| 209 | -2. **é‡è¿žæœºåˆ¶**:连接æ–å¼€åŽé‡æ–°è®¤è¯ | |
| 210 | -3. **错误é‡è¯•**:认è¯å¤±è´¥æ—¶é€‚当é‡è¯• | |
| 211 | -4. **å®‰å…¨ä¼ è¾“**:使用 TLS åŠ å¯†ä¼ è¾“æ•æ„Ÿä¿¡æ¯ | |
| 212 | - | |
| 213 | -### 5.2 消æ¯è®¾è®¡ | |
| 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. **心跳间隔**:åˆç†è®¾ç½®å¿ƒè·³é—´éš”,é¿å…è¿‡äºŽé¢‘ç¹ | |
| 233 | - | |
| 234 | -## 6. é…置说明 | |
| 235 | - | |
| 236 | -### 6.1 å¯ç”¨ JSON åè®® | |
| 237 | - | |
| 238 | -在é…置文件ä¸è®¾ç½®ï¼š | |
| 239 | - | |
| 240 | -```yaml | |
| 241 | -yudao: | |
| 242 | - iot: | |
| 243 | - gateway: | |
| 244 | - protocol: | |
| 245 | - tcp: | |
| 246 | - enabled: true | |
| 247 | - port: 8091 | |
| 248 | - default-protocol: "JSON" # 使用 JSON åè®® | |
| 249 | -``` | |
| 250 | - | |
| 251 | -### 6.2 认è¯é…ç½® | |
| 161 | +## 4. ç¼–è§£ç å™¨æ ‡è¯† | |
| 252 | 162 | |
| 253 | -```yaml | |
| 254 | -yudao: | |
| 255 | - iot: | |
| 256 | - gateway: | |
| 257 | - token: | |
| 258 | - secret: "your-secret-key" # JWT 密钥 | |
| 259 | - expiration: "24h" # Token 过期时间 | |
| 163 | +```java | |
| 164 | +public static final String TYPE = "TCP_JSON"; | |
| 260 | 165 | ``` |
| 261 | 166 | |
| 262 | -## 7. 调试和监控 | |
| 263 | - | |
| 264 | -### 7.1 日志级别 | |
| 265 | - | |
| 266 | -```yaml | |
| 267 | -logging: | |
| 268 | - level: | |
| 269 | - cn.iocoder.yudao.module.iot.gateway.protocol.tcp: DEBUG | |
| 270 | -``` | |
| 167 | +## 5. å议优势 | |
| 271 | 168 | |
| 272 | -### 7.2 è°ƒè¯•ä¿¡æ¯ | |
| 273 | - | |
| 274 | -ç¼–è§£ç 器会输出详细的调试日志: | |
| 275 | - | |
| 276 | -- 认è¯è¿‡ç¨‹ï¼šæ˜¾ç¤ºè®¤è¯è¯·æ±‚å’Œå“应 | |
| 277 | -- ç¼–ç æˆåŠŸï¼šæ˜¾ç¤ºæ–¹æ³•ã€é•¿åº¦ã€å†…容 | |
| 278 | -- è§£ç 过程:显示原始数æ®ã€è§£æžç»“æžœ | |
| 279 | -- 错误信æ¯ï¼šè¯¦ç»†çš„å¼‚å¸¸å †æ ˆ | |
| 280 | - | |
| 281 | -### 7.3 ç›‘æŽ§æŒ‡æ ‡ | |
| 282 | - | |
| 283 | -- è®¤è¯æˆåŠŸçŽ‡ | |
| 284 | -- 消æ¯å¤„ç†æ•°é‡ | |
| 285 | -- ç¼–è§£ç æˆåŠŸçŽ‡ | |
| 286 | -- 处ç†å»¶è¿Ÿ | |
| 287 | -- 错误率 | |
| 288 | -- åœ¨çº¿è®¾å¤‡æ•°é‡ | |
| 289 | - | |
| 290 | -## 8. 安全考虑 | |
| 291 | - | |
| 292 | -### 8.1 认è¯å®‰å…¨ | |
| 293 | - | |
| 294 | -1. **密ç 强度**:使用强密ç ç–ç•¥ | |
| 295 | -2. **Token 过期**:设置åˆç†çš„ Token 过期时间 | |
| 296 | -3. **连接é™åˆ¶**:é™åˆ¶å•个设备的并å‘连接数 | |
| 297 | -4. **IP 白åå•**:å¯é€‰çš„ IP 访问控制 | |
| 298 | - | |
| 299 | -### 8.2 ä¼ è¾“å®‰å…¨ | |
| 300 | - | |
| 301 | -1. **TLS åŠ å¯†**:使用 TLS 1.2+ åŠ å¯†ä¼ è¾“ | |
| 302 | -2. **è¯ä¹¦éªŒè¯**ï¼šéªŒè¯æœåС噍è¯ä¹¦ | |
| 303 | -3. **密钥管ç†**:安全å˜å‚¨å’Œç®¡ç†å¯†é’¥ | |
| 304 | - | |
| 305 | -### 8.3 æ•°æ®å®‰å…¨ | |
| 306 | - | |
| 307 | -1. **æ•æ„Ÿä¿¡æ¯**:ä¸åœ¨æ—¥å¿—ä¸è®°å½•密ç ç‰æ•æ„Ÿä¿¡æ¯ | |
| 308 | -2. **æ•°æ®éªŒè¯**ï¼šéªŒè¯æ‰€æœ‰è¾“å…¥æ•°æ® | |
| 309 | -3. **访问控制**:基于 Token 的访问控制 | |
| 310 | - | |
| 311 | -è¿™æ ·å°±å®Œæˆäº† TCP JSON æ ¼å¼åè®®çš„å®Œæ•´è¯´æ˜Žï¼ŒåŒ…æ‹¬è®¤è¯æµç¨‹çš„详细说明,与实际代ç 实现完全一致。 | |
| 169 | +- **开呿•ˆçŽ‡é«˜**:JSON æ ¼å¼ï¼Œå¼€å‘å’Œè°ƒè¯•ç®€å• | |
| 170 | +- **è·¨è¯è¨€æ”¯æŒ**:所有主æµè¯è¨€éƒ½æ”¯æŒ JSON | |
| 171 | +- **å¯è¯»æ€§ä¼˜ç§€**:å¯ä»¥ç›´æŽ¥æŸ¥çœ‹æ¶ˆæ¯å†…容 | |
| 172 | +- **扩展性强**:å¯ä»¥è½»æ¾æ·»åŠ æ–°å—æ®µ | |
| 173 | +- **安全性高**:移除 deviceId å—æ®µï¼Œé˜²æ¢ä¼ªé€ 攻击 | |
| 174 | + | |
| 175 | +## 6. 与二进制å议对比 | |
| 176 | + | |
| 177 | +| 特性 | JSONåè®® | 二进制åè®® | | |
| 178 | +|------|----------|------------| | |
| 179 | +| å¼€å‘难度 | 低 | 高 | | |
| 180 | +| 调试难度 | 低 | 高 | | |
| 181 | +| å¯è¯»æ€§ | 优秀 | å·® | | |
| 182 | +| æ•°æ®å¤§å° | ä¸ç‰ | å°ï¼ˆèŠ‚çœ30-50%) | | |
| 183 | +| è§£æžæ€§èƒ½ | ä¸ç‰ | 高 | | |
| 184 | +| å¦ä¹ æˆæœ¬ | 低 | 高 | | |
| 185 | + | |
| 186 | +**推è场景**: | |
| 187 | +- ✅ **å¼€å‘调试阶段**:调试å‹å¥½ï¼Œå¼€å‘效率高 | |
| 188 | +- ✅ **快速原型开å‘**:实现简å•,快速è¿ä»£ | |
| 189 | +- ✅ **多è¯è¨€é›†æˆ**:广泛的è¯è¨€æ”¯æŒ | |
| 190 | +- ⌠**高频数æ®ä¼ 输**:建议使用二进制åè®® | |
| 191 | +- ⌠**带宽å—é™çŽ¯å¢ƒ**:建议使用二进制åè®® | |
| 312 | 192 | \ No newline at end of file | ... | ... |