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,86 +8,72 @@ import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 8 | import io.vertx.core.buffer.Buffer; | 8 | import io.vertx.core.buffer.Buffer; |
| 9 | import lombok.AllArgsConstructor; | 9 | import lombok.AllArgsConstructor; |
| 10 | import lombok.Data; | 10 | import lombok.Data; |
| 11 | -import lombok.NoArgsConstructor; | 11 | +import lombok.extern.slf4j.Slf4j; |
| 12 | import org.springframework.stereotype.Component; | 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 | * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 | 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 | * @author 芋道源码 | 39 | * @author 芋道源码 |
| 32 | */ | 40 | */ |
| 41 | +@Slf4j | ||
| 33 | @Component | 42 | @Component |
| 34 | public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { | 43 | public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { |
| 35 | 44 | ||
| 36 | - // TODO @haohao:是不是叫 TCP_Binary 好点哈? | ||
| 37 | public static final String TYPE = "TCP_BINARY"; | 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 | @Override | 77 | @Override |
| 92 | public String type() { | 78 | public String type() { |
| 93 | return TYPE; | 79 | return TYPE; |
| @@ -99,215 +85,270 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { | @@ -99,215 +85,270 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { | ||
| 99 | Assert.notBlank(message.getMethod(), "消息方法不能为空"); | 85 | Assert.notBlank(message.getMethod(), "消息方法不能为空"); |
| 100 | 86 | ||
| 101 | try { | 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 | } catch (Exception e) { | 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 | @Override | 103 | @Override |
| 122 | public IotDeviceMessage decode(byte[] bytes) { | 104 | public IotDeviceMessage decode(byte[] bytes) { |
| 123 | Assert.notNull(bytes, "待解码数据不能为空"); | 105 | Assert.notNull(bytes, "待解码数据不能为空"); |
| 124 | - Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); | 106 | + Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); |
| 125 | 107 | ||
| 126 | try { | 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 | } catch (Exception e) { | 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 | int index = 0; | 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 | index += 4; | 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 | index += 2; | 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 | index += 2; | 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 | \ No newline at end of file | 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,12 +14,13 @@ import org.springframework.stereotype.Component; | ||
| 14 | * | 14 | * |
| 15 | * 采用纯 JSON 格式传输,格式如下: | 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 | * @author 芋道源码 | 26 | * @author 芋道源码 |
| @@ -44,12 +45,6 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | @@ -44,12 +45,6 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | ||
| 44 | */ | 45 | */ |
| 45 | private String method; | 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,9 +79,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | ||
| 84 | 79 | ||
| 85 | @Override | 80 | @Override |
| 86 | public byte[] encode(IotDeviceMessage message) { | 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 | System.currentTimeMillis()); | 89 | System.currentTimeMillis()); |
| 91 | return JsonUtils.toJsonByte(tcpJsonMessage); | 90 | return JsonUtils.toJsonByte(tcpJsonMessage); |
| 92 | } | 91 | } |
| @@ -97,11 +96,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | @@ -97,11 +96,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | ||
| 97 | TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); | 96 | TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); |
| 98 | Assert.notNull(tcpJsonMessage, "消息不能为空"); | 97 | Assert.notNull(tcpJsonMessage, "消息不能为空"); |
| 99 | Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); | 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,7 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr | ||
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | 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 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 14 | import io.vertx.core.Vertx; | 14 | import io.vertx.core.Vertx; |
| @@ -92,19 +92,19 @@ public class IotGatewayConfiguration { | @@ -92,19 +92,19 @@ public class IotGatewayConfiguration { | ||
| 92 | public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, | 92 | public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, |
| 93 | IotDeviceService deviceService, | 93 | IotDeviceService deviceService, |
| 94 | IotDeviceMessageService messageService, | 94 | IotDeviceMessageService messageService, |
| 95 | - IotTcpSessionManager sessionManager, | 95 | + IotTcpConnectionManager connectionManager, |
| 96 | Vertx tcpVertx) { | 96 | Vertx tcpVertx) { |
| 97 | return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), | 97 | return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), |
| 98 | - deviceService, messageService, sessionManager, tcpVertx); | 98 | + deviceService, messageService, connectionManager, tcpVertx); |
| 99 | } | 99 | } |
| 100 | 100 | ||
| 101 | @Bean | 101 | @Bean |
| 102 | public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, | 102 | public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, |
| 103 | IotDeviceMessageService messageService, | 103 | IotDeviceMessageService messageService, |
| 104 | IotDeviceService deviceService, | 104 | IotDeviceService deviceService, |
| 105 | - IotTcpSessionManager sessionManager, | 105 | + IotTcpConnectionManager connectionManager, |
| 106 | IotMessageBus messageBus) { | 106 | IotMessageBus messageBus) { |
| 107 | - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, | 107 | + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager, |
| 108 | messageBus); | 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,13 +4,13 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | import jakarta.annotation.PostConstruct; | 11 | import jakarta.annotation.PostConstruct; |
| 12 | +import lombok.RequiredArgsConstructor; | ||
| 12 | import lombok.extern.slf4j.Slf4j; | 13 | import lombok.extern.slf4j.Slf4j; |
| 13 | -import org.springframework.stereotype.Component; | ||
| 14 | 14 | ||
| 15 | /** | 15 | /** |
| 16 | * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 | 16 | * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 |
| @@ -18,37 +18,28 @@ import org.springframework.stereotype.Component; | @@ -18,37 +18,28 @@ import org.springframework.stereotype.Component; | ||
| 18 | * @author 芋道源码 | 18 | * @author 芋道源码 |
| 19 | */ | 19 | */ |
| 20 | @Slf4j | 20 | @Slf4j |
| 21 | -@Component | 21 | +@RequiredArgsConstructor |
| 22 | public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | 22 | public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 23 | 23 | ||
| 24 | - private final IotTcpDownstreamHandler downstreamHandler; | ||
| 25 | - | ||
| 26 | - private final IotMessageBus messageBus; | ||
| 27 | - | ||
| 28 | private final IotTcpUpstreamProtocol protocol; | 24 | private final IotTcpUpstreamProtocol protocol; |
| 29 | 25 | ||
| 30 | - // todo @haohao:不用的变量,可以去掉哈 | 26 | + private final IotDeviceMessageService messageService; |
| 27 | + | ||
| 31 | private final IotDeviceService deviceService; | 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 | @PostConstruct | 36 | @PostConstruct |
| 49 | public void init() { | 37 | public void init() { |
| 38 | + // 初始化下游处理器 | ||
| 39 | + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager); | ||
| 40 | + | ||
| 50 | messageBus.register(this); | 41 | messageBus.register(this); |
| 51 | - log.info("[init][TCP 下游订阅者初始化完成] 服务器 ID: {}, Topic: {}", | 42 | + log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", |
| 52 | protocol.getServerId(), getTopic()); | 43 | protocol.getServerId(), getTopic()); |
| 53 | } | 44 | } |
| 54 | 45 | ||
| @@ -68,8 +59,9 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic | @@ -68,8 +59,9 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic | ||
| 68 | try { | 59 | try { |
| 69 | downstreamHandler.handle(message); | 60 | downstreamHandler.handle(message); |
| 70 | } catch (Exception e) { | 61 | } catch (Exception e) { |
| 71 | - log.error("[onMessage][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", | 62 | + log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", |
| 72 | message.getDeviceId(), message.getMethod(), message.getId(), e); | 63 | message.getDeviceId(), message.getMethod(), message.getId(), e); |
| 73 | } | 64 | } |
| 74 | } | 65 | } |
| 66 | + | ||
| 75 | } | 67 | } |
| 76 | \ No newline at end of file | 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,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| @@ -29,24 +29,24 @@ public class IotTcpUpstreamProtocol { | @@ -29,24 +29,24 @@ public class IotTcpUpstreamProtocol { | ||
| 29 | 29 | ||
| 30 | private final IotDeviceMessageService messageService; | 30 | private final IotDeviceMessageService messageService; |
| 31 | 31 | ||
| 32 | - private final IotTcpSessionManager sessionManager; | 32 | + private final IotTcpConnectionManager connectionManager; |
| 33 | 33 | ||
| 34 | private final Vertx vertx; | 34 | private final Vertx vertx; |
| 35 | 35 | ||
| 36 | @Getter | 36 | @Getter |
| 37 | private final String serverId; | 37 | private final String serverId; |
| 38 | 38 | ||
| 39 | - private NetServer netServer; | 39 | + private NetServer tcpServer; |
| 40 | 40 | ||
| 41 | public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, | 41 | public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, |
| 42 | IotDeviceService deviceService, | 42 | IotDeviceService deviceService, |
| 43 | IotDeviceMessageService messageService, | 43 | IotDeviceMessageService messageService, |
| 44 | - IotTcpSessionManager sessionManager, | 44 | + IotTcpConnectionManager connectionManager, |
| 45 | Vertx vertx) { | 45 | Vertx vertx) { |
| 46 | this.tcpProperties = tcpProperties; | 46 | this.tcpProperties = tcpProperties; |
| 47 | this.deviceService = deviceService; | 47 | this.deviceService = deviceService; |
| 48 | this.messageService = messageService; | 48 | this.messageService = messageService; |
| 49 | - this.sessionManager = sessionManager; | 49 | + this.connectionManager = connectionManager; |
| 50 | this.vertx = vertx; | 50 | this.vertx = vertx; |
| 51 | this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); | 51 | this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); |
| 52 | } | 52 | } |
| @@ -68,16 +68,16 @@ public class IotTcpUpstreamProtocol { | @@ -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 | IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, | 73 | IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, |
| 74 | - sessionManager); | 74 | + connectionManager); |
| 75 | handler.handle(socket); | 75 | handler.handle(socket); |
| 76 | }); | 76 | }); |
| 77 | 77 | ||
| 78 | // 启动服务器 | 78 | // 启动服务器 |
| 79 | try { | 79 | try { |
| 80 | - netServer.listen().result(); | 80 | + tcpServer.listen().result(); |
| 81 | log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); | 81 | log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); |
| 82 | } catch (Exception e) { | 82 | } catch (Exception e) { |
| 83 | log.error("[start][IoT 网关 TCP 协议启动失败]", e); | 83 | log.error("[start][IoT 网关 TCP 协议启动失败]", e); |
| @@ -87,9 +87,9 @@ public class IotTcpUpstreamProtocol { | @@ -87,9 +87,9 @@ public class IotTcpUpstreamProtocol { | ||
| 87 | 87 | ||
| 88 | @PreDestroy | 88 | @PreDestroy |
| 89 | public void stop() { | 89 | public void stop() { |
| 90 | - if (netServer != null) { | 90 | + if (tcpServer != null) { |
| 91 | try { | 91 | try { |
| 92 | - netServer.close().result(); | 92 | + tcpServer.close().result(); |
| 93 | log.info("[stop][IoT 网关 TCP 协议已停止]"); | 93 | log.info("[stop][IoT 网关 TCP 协议已停止]"); |
| 94 | } catch (Exception e) { | 94 | } catch (Exception e) { |
| 95 | log.error("[stop][IoT 网关 TCP 协议停止失败]", e); | 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 | \ No newline at end of file | 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 | \ No newline at end of file | 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 | \ No newline at end of file | 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,9 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 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 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import lombok.RequiredArgsConstructor; | ||
| 8 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 9 | 10 | ||
| 10 | /** | 11 | /** |
| @@ -13,62 +14,50 @@ import lombok.extern.slf4j.Slf4j; | @@ -13,62 +14,50 @@ import lombok.extern.slf4j.Slf4j; | ||
| 13 | * @author 芋道源码 | 14 | * @author 芋道源码 |
| 14 | */ | 15 | */ |
| 15 | @Slf4j | 16 | @Slf4j |
| 17 | +@RequiredArgsConstructor | ||
| 16 | public class IotTcpDownstreamHandler { | 18 | public class IotTcpDownstreamHandler { |
| 17 | 19 | ||
| 18 | - private final IotDeviceMessageService messageService; | 20 | + private final IotDeviceMessageService deviceMessageService; |
| 19 | 21 | ||
| 20 | private final IotDeviceService deviceService; | 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 | public void handle(IotDeviceMessage message) { | 29 | public void handle(IotDeviceMessage message) { |
| 38 | try { | 30 | try { |
| 39 | - log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", | 31 | + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 40 | message.getDeviceId(), message.getMethod(), message.getId()); | 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 | return; | 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 | return; | 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 | if (success) { | 51 | if (success) { |
| 62 | - log.info("[handle][下行消息发送成功] 设备 ID: {}, 方法: {}, 消息 ID: {}, 数据长度: {} 字节", | 52 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", |
| 63 | message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); | 53 | message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); |
| 64 | } else { | 54 | } else { |
| 65 | - log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", | 55 | + log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", |
| 66 | message.getDeviceId(), message.getMethod(), message.getId()); | 56 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 67 | - } // TODO @haohao:下面这个空行,可以考虑去掉的哈。 | ||
| 68 | - | 57 | + } |
| 69 | } catch (Exception e) { | 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,50 +12,48 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 12 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 12 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 13 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 13 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 14 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | 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 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 19 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 20 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 20 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 21 | import io.vertx.core.Handler; | 21 | import io.vertx.core.Handler; |
| 22 | import io.vertx.core.buffer.Buffer; | 22 | import io.vertx.core.buffer.Buffer; |
| 23 | import io.vertx.core.net.NetSocket; | 23 | import io.vertx.core.net.NetSocket; |
| 24 | +import lombok.AllArgsConstructor; | ||
| 25 | +import lombok.Data; | ||
| 24 | import lombok.extern.slf4j.Slf4j; | 26 | import lombok.extern.slf4j.Slf4j; |
| 25 | 27 | ||
| 28 | +import java.nio.charset.StandardCharsets; | ||
| 29 | + | ||
| 26 | /** | 30 | /** |
| 27 | * TCP 上行消息处理器 | 31 | * TCP 上行消息处理器 |
| 32 | + * | ||
| 33 | + * @author 芋道源码 | ||
| 28 | */ | 34 | */ |
| 29 | @Slf4j | 35 | @Slf4j |
| 30 | public class IotTcpUpstreamHandler implements Handler<NetSocket> { | 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 | private static final String AUTH_METHOD = "auth"; | 40 | private static final String AUTH_METHOD = "auth"; |
| 37 | 41 | ||
| 38 | private final IotDeviceMessageService deviceMessageService; | 42 | private final IotDeviceMessageService deviceMessageService; |
| 39 | 43 | ||
| 40 | private final IotDeviceService deviceService; | 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 | private final IotDeviceCommonApi deviceApi; | 48 | private final IotDeviceCommonApi deviceApi; |
| 49 | 49 | ||
| 50 | private final String serverId; | 50 | private final String serverId; |
| 51 | 51 | ||
| 52 | public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, | 52 | public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, |
| 53 | - IotDeviceService deviceService, IotTcpSessionManager sessionManager) { | 53 | + IotDeviceService deviceService, IotTcpConnectionManager connectionManager) { |
| 54 | this.deviceMessageService = deviceMessageService; | 54 | this.deviceMessageService = deviceMessageService; |
| 55 | this.deviceService = deviceService; | 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 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | 57 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 60 | this.serverId = protocol.getServerId(); | 58 | this.serverId = protocol.getServerId(); |
| 61 | } | 59 | } |
| @@ -63,207 +61,313 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -63,207 +61,313 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 63 | @Override | 61 | @Override |
| 64 | public void handle(NetSocket socket) { | 62 | public void handle(NetSocket socket) { |
| 65 | String clientId = IdUtil.simpleUUID(); | 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 | socket.exceptionHandler(ex -> { | 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 | socket.closeHandler(v -> { | 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 | try { | 84 | try { |
| 85 | + // 1. 数据包基础检查 | ||
| 84 | if (buffer.length() == 0) { | 86 | if (buffer.length() == 0) { |
| 85 | - log.warn("[handleDataPackage][数据包为空] 客户端 ID: {}", clientId); | ||
| 86 | return; | 87 | return; |
| 87 | } | 88 | } |
| 88 | 89 | ||
| 89 | - // 1. 解码消息 | 90 | + // 2. 解码消息 |
| 90 | MessageInfo messageInfo = decodeMessage(buffer); | 91 | MessageInfo messageInfo = decodeMessage(buffer); |
| 91 | if (messageInfo == null) { | 92 | if (messageInfo == null) { |
| 92 | return; | 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 | } else { | 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 | } catch (Exception e) { | 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 | try { | 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 | AuthParams authParams = parseAuthParams(message.getParams()); | 118 | AuthParams authParams = parseAuthParams(message.getParams()); |
| 129 | if (authParams == null) { | 119 | if (authParams == null) { |
| 130 | - sendError(socket, message.getRequestId(), "认证参数不完整", codecType); | 120 | + sendError(socket, message.getRequestId(), "认证参数不完整", messageInfo.codecType); |
| 131 | return; | 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 | } catch (Exception e) { | 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 | try { | 187 | try { |
| 151 | message.setDeviceId(authInfo.getDeviceId()); | 188 | message.setDeviceId(authInfo.getDeviceId()); |
| 152 | message.setServerId(serverId); | 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 | } catch (Exception e) { | 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 | private MessageInfo decodeMessage(Buffer buffer) { | 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 | try { | 212 | try { |
| 169 | - String rawData = buffer.toString(); | ||
| 170 | - String codecType = isJsonFormat(rawData) ? CODEC_TYPE_JSON : CODEC_TYPE_BINARY; | 213 | + // 2. 使用检测到的格式进行解码 |
| 171 | IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); | 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 | } catch (Exception e) { | 222 | } catch (Exception e) { |
| 174 | - log.debug("[decodeMessage][消息解码失败] 错误: {}", e.getMessage()); | 223 | + log.warn("[decodeMessage][消息解码失败,格式: {},数据长度: {},错误: {}]", |
| 224 | + codecType, buffer.length(), e.getMessage()); | ||
| 175 | return null; | 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 | return false; | 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 | return false; | 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 | try { | 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 | } catch (Exception e) { | 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,38 +377,37 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 273 | if (params == null) { | 377 | if (params == null) { |
| 274 | return null; | 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 | private boolean authenticateDevice(AuthParams authParams) { | 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,24 +418,32 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 315 | } | 418 | } |
| 316 | 419 | ||
| 317 | /** | 420 | /** |
| 318 | - * 发送成功响应(不返回 token) | 421 | + * 发送成功响应 |
| 319 | */ | 422 | */ |
| 320 | private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { | 423 | private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { |
| 321 | sendResponse(socket, true, message, requestId, codecType); | 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 | \ No newline at end of file | 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,6 +4,15 @@ spring: | ||
| 4 | profiles: | 4 | profiles: |
| 5 | active: local # 默认激活本地开发环境 | 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 | # rocketmq 配置项,对应 RocketMQProperties 配置类 | 18 | # rocketmq 配置项,对应 RocketMQProperties 配置类 |
| @@ -45,7 +54,7 @@ yudao: | @@ -45,7 +54,7 @@ yudao: | ||
| 45 | # 针对引入的 EMQX 组件的配置 | 54 | # 针对引入的 EMQX 组件的配置 |
| 46 | # ==================================== | 55 | # ==================================== |
| 47 | emqx: | 56 | emqx: |
| 48 | - enabled: false | 57 | + enabled: true |
| 49 | http-port: 8090 # MQTT HTTP 服务端口 | 58 | http-port: 8090 # MQTT HTTP 服务端口 |
| 50 | mqtt-host: 127.0.0.1 # MQTT Broker 地址 | 59 | mqtt-host: 127.0.0.1 # MQTT Broker 地址 |
| 51 | mqtt-port: 1883 # MQTT Broker 端口 | 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 | \ No newline at end of file | 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 | ```java | 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 | ```java | 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,13 +2,13 @@ | ||
| 2 | 2 | ||
| 3 | ## 1. å议概述 | 3 | ## 1. å议概述 |
| 4 | 4 | ||
| 5 | -TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,å‚考了 EMQX å’Œ HTTP 模å—çš„æ•°æ®æ ¼å¼è®¾è®¡ï¼Œå…·æœ‰ä»¥ä¸‹ä¼˜åŠ¿ï¼š | 5 | +TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,具有以下特点: |
| 6 | 6 | ||
| 7 | - **æ ‡å‡†åŒ–**ï¼šä½¿ç”¨æ ‡å‡† JSON æ ¼å¼ï¼Œæ˜“于解æžå’Œå¤„ç† | 7 | - **æ ‡å‡†åŒ–**ï¼šä½¿ç”¨æ ‡å‡† JSON æ ¼å¼ï¼Œæ˜“于解æžå’Œå¤„ç† |
| 8 | - **å¯è¯»æ€§**:人类å¯è¯»ï¼Œä¾¿äºŽè°ƒè¯•和维护 | 8 | - **å¯è¯»æ€§**:人类å¯è¯»ï¼Œä¾¿äºŽè°ƒè¯•和维护 |
| 9 | - **扩展性**:å¯ä»¥è½»æ¾æ·»åŠ æ–°å—æ®µï¼Œå‘åŽå…¼å®¹ | 9 | - **扩展性**:å¯ä»¥è½»æ¾æ·»åŠ æ–°å—æ®µï¼Œå‘åŽå…¼å®¹ |
| 10 | -- **统一性**:与 HTTP 模å—ä¿æŒä¸€è‡´çš„æ•°æ®æ ¼å¼ | ||
| 11 | -- **简化性**:相比二进制å议,实现更简å•,调试更容易 | 10 | +- **跨平å°**:JSON æ ¼å¼æ”¯æŒæ‰€æœ‰ä¸»æµç¼–程è¯è¨€ |
| 11 | +- **安全优化**:移除冗余的 deviceId å—æ®µï¼Œæé«˜å®‰å…¨æ€§ | ||
| 12 | 12 | ||
| 13 | ## 2. æ¶ˆæ¯æ ¼å¼ | 13 | ## 2. æ¶ˆæ¯æ ¼å¼ |
| 14 | 14 | ||
| @@ -18,294 +18,174 @@ TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,å‚考了 EMQX å’ | @@ -18,294 +18,174 @@ TCP JSON æ ¼å¼å议采用纯 JSON æ ¼å¼è¿›è¡Œæ•°æ®ä¼ 输,å‚考了 EMQX å’ | ||
| 18 | { | 18 | { |
| 19 | "id": "消æ¯å”¯ä¸€æ ‡è¯†", | 19 | "id": "消æ¯å”¯ä¸€æ ‡è¯†", |
| 20 | "method": "æ¶ˆæ¯æ–¹æ³•", | 20 | "method": "æ¶ˆæ¯æ–¹æ³•", |
| 21 | - "deviceId": 设备ID, | ||
| 22 | "params": { | 21 | "params": { |
| 23 | - // 消æ¯å‚æ•° | 22 | + // è¯·æ±‚å‚æ•° |
| 23 | + }, | ||
| 24 | + "data": { | ||
| 25 | + // å“åº”æ•°æ® | ||
| 24 | }, | 26 | }, |
| 25 | - "timestamp": 时间戳, | ||
| 26 | "code": å“应ç , | 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 | ```json | 68 | ```json |
| 54 | { | 69 | { |
| 55 | - "id": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | 70 | + "id": "auth_1704067200000_123", |
| 56 | "method": "auth", | 71 | "method": "auth", |
| 57 | "params": { | 72 | "params": { |
| 58 | "clientId": "device_001", | 73 | "clientId": "device_001", |
| 59 | "username": "productKey_deviceName", | 74 | "username": "productKey_deviceName", |
| 60 | "password": "设备密ç " | 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 | | username | String | 是 | 设备用户åï¼Œæ ¼å¼ä¸º `productKey_deviceName` | | 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 | ```json | 93 | ```json |
| 78 | { | 94 | { |
| 79 | - "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", | ||
| 80 | - "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | 95 | + "id": "response_auth_1704067200000_123", |
| 81 | "method": "auth", | 96 | "method": "auth", |
| 82 | "data": { | 97 | "data": { |
| 83 | "success": true, | 98 | "success": true, |
| 84 | "message": "è®¤è¯æˆåŠŸ" | 99 | "message": "è®¤è¯æˆåŠŸ" |
| 85 | }, | 100 | }, |
| 86 | "code": 0, | 101 | "code": 0, |
| 87 | - "msg": "è®¤è¯æˆåŠŸ" | 102 | + "msg": "è®¤è¯æˆåŠŸ", |
| 103 | + "timestamp": 1704067200001 | ||
| 88 | } | 104 | } |
| 89 | ``` | 105 | ``` |
| 90 | 106 | ||
| 91 | **认è¯å¤±è´¥å“应:** | 107 | **认è¯å¤±è´¥å“应:** |
| 92 | - | ||
| 93 | ```json | 108 | ```json |
| 94 | { | 109 | { |
| 95 | - "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", | ||
| 96 | - "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", | 110 | + "id": "response_auth_1704067200000_123", |
| 97 | "method": "auth", | 111 | "method": "auth", |
| 98 | "data": { | 112 | "data": { |
| 99 | "success": false, | 113 | "success": false, |
| 100 | "message": "认è¯å¤±è´¥ï¼šç”¨æˆ·å或密ç 错误" | 114 | "message": "认è¯å¤±è´¥ï¼šç”¨æˆ·å或密ç 错误" |
| 101 | }, | 115 | }, |
| 102 | "code": 401, | 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 | ```json | 127 | ```json |
| 132 | { | 128 | { |
| 133 | - "id": "8ac6a1db91e64aa9996143fdbac2cbfe", | 129 | + "id": "property_1704067200000_456", |
| 134 | "method": "thing.property.post", | 130 | "method": "thing.property.post", |
| 135 | - "deviceId": 8, | ||
| 136 | "params": { | 131 | "params": { |
| 137 | "temperature": 25.5, | 132 | "temperature": 25.5, |
| 138 | "humidity": 60.2, | 133 | "humidity": 60.2, |
| 139 | "pressure": 1013.25, | 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 | ```json | 147 | ```json |
| 152 | { | 148 | { |
| 153 | - "id": "7db8c4e6408b40f8b2549ddd94f6bb02", | 149 | + "id": "heartbeat_1704067200000_321", |
| 154 | "method": "thing.state.update", | 150 | "method": "thing.state.update", |
| 155 | - "deviceId": 8, | ||
| 156 | "params": { | 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 | \ No newline at end of file | 192 | \ No newline at end of file |