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