Commit cda59081a38040c9407c9d9cc8f05403a23f8da9

Authored by haohao
1 parent 6b79cab0

feat:【IoT 物联网】新增 TCP 连接管理器,优化消息编解码逻辑

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