Commit bd8052f56b5254fb073d4b396fcb8326897c79c0

Authored by haohao
1 parent 6a117c9d

feat:【IoT 物联网】新增 TCP 二进制和 JSON 编解码器,重构 TCP 协议处理逻辑

Showing 19 changed files with 1864 additions and 2571 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  8 +import io.vertx.core.buffer.Buffer;
  9 +import lombok.Data;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.stereotype.Component;
  12 +
  13 +/**
  14 + * TCP 二进制格式 {@link IotDeviceMessage} 编解码器
  15 + *
  16 + * 使用自定义二进制协议格式:
  17 + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长)
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +@Component
  22 +@Slf4j
  23 +public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
  24 +
  25 + /**
  26 + * 编解码器类型
  27 + */
  28 + public static final String TYPE = "TCP_BINARY";
  29 +
  30 + // ==================== 常量定义 ====================
  31 +
  32 + @Override
  33 + public byte[] encode(IotDeviceMessage message) {
  34 + if (message == null || StrUtil.isEmpty(message.getMethod())) {
  35 + throw new IllegalArgumentException("消息或方法不能为空");
  36 + }
  37 +
  38 + try {
  39 + // 1. 确定功能码(只支持数据上报和心跳)
  40 + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ?
  41 + TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP;
  42 +
  43 + // 2. 构建简化负载
  44 + String payload = buildSimplePayload(message);
  45 +
  46 + // 3. 构建 TCP 数据包
  47 + String deviceAddr = message.getDeviceId() != null ? String.valueOf(message.getDeviceId()) : "default";
  48 + short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE);
  49 + TcpDataPackage dataPackage = new TcpDataPackage(deviceAddr, code, mid, payload);
  50 +
  51 + // 4. 编码为字节流
  52 + return encodeTcpDataPackage(dataPackage).getBytes();
  53 + } catch (Exception e) {
  54 + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e);
  55 + throw new TcpCodecException("TCP 消息编码失败", e);
  56 + }
  57 + }
  58 +
  59 + @Override
  60 + public IotDeviceMessage decode(byte[] bytes) {
  61 + if (bytes == null || bytes.length == 0) {
  62 + throw new IllegalArgumentException("待解码数据不能为空");
  63 + }
  64 +
  65 + try {
  66 + // 1. 解码 TCP 数据包
  67 + TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes));
  68 +
  69 + // 2. 根据功能码确定方法
  70 + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ?
  71 + MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST;
  72 +
  73 + // 3. 解析负载数据和请求ID
  74 + PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload());
  75 +
  76 + // 4. 构建 IoT 设备消息(设置完整的必要参数)
  77 + IotDeviceMessage message = IotDeviceMessage.requestOf(
  78 + payloadInfo.getRequestId(), method, payloadInfo.getParams());
  79 +
  80 + // 5. 设置设备相关信息
  81 + Long deviceId = parseDeviceId(dataPackage.getAddr());
  82 + message.setDeviceId(deviceId);
  83 +
  84 + // 6. 设置TCP协议相关信息
  85 + message.setServerId(generateServerId(dataPackage));
  86 +
  87 + // 7. 设置租户ID(TODO: 后续可以从设备信息中获取)
  88 + // message.setTenantId(getTenantIdByDeviceId(deviceId));
  89 +
  90 + if (log.isDebugEnabled()) {
  91 + log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}",
  92 + deviceId, method, message.getRequestId(), message.getId());
  93 + }
  94 +
  95 + return message;
  96 + } catch (Exception e) {
  97 + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e);
  98 + throw new TcpCodecException("TCP 消息解码失败", e);
  99 + }
  100 + }
  101 +
  102 + @Override
  103 + public String type() {
  104 + return TYPE;
  105 + }
  106 +
  107 + /**
  108 + * 构建完整负载
  109 + */
  110 + private String buildSimplePayload(IotDeviceMessage message) {
  111 + JSONObject payload = new JSONObject();
  112 +
  113 + // 核心字段
  114 + payload.set(PayloadField.METHOD, message.getMethod());
  115 + if (message.getParams() != null) {
  116 + payload.set(PayloadField.PARAMS, message.getParams());
  117 + }
  118 +
  119 + // 标识字段
  120 + if (StrUtil.isNotEmpty(message.getRequestId())) {
  121 + payload.set(PayloadField.REQUEST_ID, message.getRequestId());
  122 + }
  123 + if (StrUtil.isNotEmpty(message.getId())) {
  124 + payload.set(PayloadField.MESSAGE_ID, message.getId());
  125 + }
  126 +
  127 + // 时间戳
  128 + payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis());
  129 +
  130 + return payload.toString();
  131 + }
  132 +
  133 +
  134 +
  135 + // ==================== 编解码方法 ====================
  136 +
  137 + /**
  138 + * 解析负载信息(包含requestId和params)
  139 + */
  140 + private PayloadInfo parsePayloadInfo(String payload) {
  141 + if (StrUtil.isEmpty(payload)) {
  142 + return new PayloadInfo(null, null);
  143 + }
  144 +
  145 + try {
  146 + JSONObject jsonObject = JSONUtil.parseObj(payload);
  147 + String requestId = jsonObject.getStr(PayloadField.REQUEST_ID);
  148 + if (StrUtil.isEmpty(requestId)) {
  149 + requestId = jsonObject.getStr(PayloadField.MESSAGE_ID);
  150 + }
  151 + Object params = jsonObject.get(PayloadField.PARAMS);
  152 + return new PayloadInfo(requestId, params);
  153 + } catch (Exception e) {
  154 + log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload);
  155 + return new PayloadInfo(null, payload);
  156 + }
  157 + }
  158 +
  159 + /**
  160 + * 从设备地址解析设备ID
  161 + *
  162 + * @param deviceAddr 设备地址字符串
  163 + * @return 设备ID
  164 + */
  165 + private Long parseDeviceId(String deviceAddr) {
  166 + if (StrUtil.isEmpty(deviceAddr)) {
  167 + log.warn("[parseDeviceId][设备地址为空,返回默认ID]");
  168 + return 0L;
  169 + }
  170 +
  171 + try {
  172 + // 尝试直接解析为Long
  173 + return Long.parseLong(deviceAddr);
  174 + } catch (NumberFormatException e) {
  175 + // 如果不是纯数字,可以使用哈希值或其他策略
  176 + log.warn("[parseDeviceId][设备地址不是数字格式: {},使用哈希值]", deviceAddr);
  177 + return (long) deviceAddr.hashCode();
  178 + }
  179 + }
  180 +
  181 + /**
  182 + * 生成服务ID
  183 + *
  184 + * @param dataPackage TCP数据包
  185 + * @return 服务ID
  186 + */
  187 + private String generateServerId(TcpDataPackage dataPackage) {
  188 + // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务ID
  189 + return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid());
  190 + }
  191 +
  192 + // ==================== 内部辅助方法 ====================
  193 +
  194 + /**
  195 + * 编码 TCP 数据包
  196 + *
  197 + * @param dataPackage 数据包对象
  198 + * @return 编码后的字节流
  199 + * @throws IllegalArgumentException 如果数据包对象不正确
  200 + */
  201 + private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) {
  202 + if (dataPackage == null) {
  203 + throw new IllegalArgumentException("数据包对象不能为空");
  204 + }
  205 +
  206 + // 验证数据包
  207 + if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) {
  208 + throw new IllegalArgumentException("设备地址不能为空");
  209 + }
  210 + if (dataPackage.getPayload() == null) {
  211 + throw new IllegalArgumentException("负载不能为空");
  212 + }
  213 +
  214 + try {
  215 + Buffer buffer = Buffer.buffer();
  216 +
  217 + // 1. 计算包体长度(除了包头 4 字节)
  218 + int payloadLength = dataPackage.getPayload().getBytes().length;
  219 + int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength;
  220 +
  221 + // 2.1 写入包头:总长度(4 字节)
  222 + buffer.appendInt(totalLength);
  223 + // 2.2 写入设备地址长度(2 字节)
  224 + buffer.appendShort((short) dataPackage.getAddr().length());
  225 + // 2.3 写入设备地址(不定长)
  226 + buffer.appendBytes(dataPackage.getAddr().getBytes());
  227 + // 2.4 写入功能码(2 字节)
  228 + buffer.appendShort(dataPackage.getCode());
  229 + // 2.5 写入消息序号(2 字节)
  230 + buffer.appendShort(dataPackage.getMid());
  231 + // 2.6 写入包体数据(不定长)
  232 + buffer.appendBytes(dataPackage.getPayload().getBytes());
  233 +
  234 + if (log.isDebugEnabled()) {
  235 + log.debug("[encodeTcpDataPackage][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}",
  236 + dataPackage.getAddr(), dataPackage.getCode(), dataPackage.getMid(), buffer.length());
  237 + }
  238 + return buffer;
  239 + } catch (Exception e) {
  240 + log.error("[encodeTcpDataPackage][编码失败] 数据包: {}", dataPackage, e);
  241 + throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e);
  242 + }
  243 + }
  244 +
  245 + /**
  246 + * 解码 TCP 数据包
  247 + *
  248 + * @param buffer 数据缓冲区
  249 + * @return 解码后的数据包
  250 + * @throws IllegalArgumentException 如果数据包格式不正确
  251 + */
  252 + private TcpDataPackage decodeTcpDataPackage(Buffer buffer) {
  253 + if (buffer == null || buffer.length() < 8) {
  254 + throw new IllegalArgumentException("数据包长度不足");
  255 + }
  256 +
  257 + try {
  258 + int index = 0;
  259 +
  260 + // 1.1 跳过包头(4字节)
  261 + index += 4;
  262 +
  263 + // 1.2 获取设备地址长度(2字节)
  264 + short addrLength = buffer.getShort(index);
  265 + index += 2;
  266 +
  267 + // 1.3 获取设备地址
  268 + String addr = buffer.getBuffer(index, index + addrLength).toString();
  269 + index += addrLength;
  270 +
  271 + // 1.4 获取功能码(2字节)
  272 + short code = buffer.getShort(index);
  273 + index += 2;
  274 +
  275 + // 1.5 获取消息序号(2字节)
  276 + short mid = buffer.getShort(index);
  277 + index += 2;
  278 +
  279 + // 1.6 获取包体数据
  280 + String payload = "";
  281 + if (index < buffer.length()) {
  282 + payload = buffer.getString(index, buffer.length());
  283 + }
  284 +
  285 + // 2. 构建数据包对象
  286 + TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload);
  287 +
  288 + if (log.isDebugEnabled()) {
  289 + log.debug("[decodeTcpDataPackage][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}",
  290 + addr, code, mid, payload.length());
  291 + }
  292 + return dataPackage;
  293 + } catch (Exception e) {
  294 + log.error("[decodeTcpDataPackage][解码失败] 数据长度: {}", buffer.length(), e);
  295 + throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e);
  296 + }
  297 + }
  298 +
  299 + /**
  300 + * 消息方法常量
  301 + */
  302 + public static class MessageMethod {
  303 + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报
  304 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳
  305 + }
  306 +
  307 + /**
  308 + * 负载字段名
  309 + */
  310 + private static class PayloadField {
  311 + public static final String METHOD = "method";
  312 + public static final String PARAMS = "params";
  313 + public static final String TIMESTAMP = "timestamp";
  314 + public static final String REQUEST_ID = "requestId";
  315 + public static final String MESSAGE_ID = "msgId";
  316 + }
  317 +
  318 + // ==================== TCP 数据包编解码方法 ====================
  319 +
  320 + /**
  321 + * 负载信息类
  322 + */
  323 + private static class PayloadInfo {
  324 + private String requestId;
  325 + private Object params;
  326 +
  327 + public PayloadInfo(String requestId, Object params) {
  328 + this.requestId = requestId;
  329 + this.params = params;
  330 + }
  331 +
  332 + public String getRequestId() { return requestId; }
  333 + public Object getParams() { return params; }
  334 + }
  335 +
  336 + /**
  337 + * TCP 数据包内部类
  338 + */
  339 + @Data
  340 + private static class TcpDataPackage {
  341 + // 功能码定义
  342 + public static final short CODE_REGISTER = 10;
  343 + public static final short CODE_REGISTER_REPLY = 11;
  344 + public static final short CODE_HEARTBEAT = 20;
  345 + public static final short CODE_HEARTBEAT_REPLY = 21;
  346 + public static final short CODE_MESSAGE_UP = 30;
  347 + public static final short CODE_MESSAGE_DOWN = 40;
  348 +
  349 + private String addr;
  350 + private short code;
  351 + private short mid;
  352 + private String payload;
  353 +
  354 + public TcpDataPackage(String addr, short code, short mid, String payload) {
  355 + this.addr = addr;
  356 + this.code = code;
  357 + this.mid = mid;
  358 + this.payload = payload;
  359 + }
  360 + }
  361 +
  362 + // ==================== 自定义异常 ====================
  363 +
  364 + /**
  365 + * TCP 编解码异常
  366 + */
  367 + public static class TcpCodecException extends RuntimeException {
  368 +
  369 + public TcpCodecException(String message) {
  370 + super(message);
  371 + }
  372 +
  373 + public TcpCodecException(String message, Throwable cause) {
  374 + super(message, cause);
  375 + }
  376 +
  377 + }
  378 +}
0 \ No newline at end of file 379 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.beans.factory.annotation.Autowired;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +/**
  10 + * TCP编解码器管理器(简化版)
  11 + *
  12 + * 核心功能:
  13 + * - 自动协议检测(二进制 vs JSON)
  14 + * - 统一编解码接口
  15 + * - 默认使用JSON协议
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Slf4j
  20 +@Component
  21 +public class IotTcpCodecManager implements IotDeviceMessageCodec {
  22 +
  23 + public static final String TYPE = "TCP";
  24 +
  25 + @Autowired
  26 + private IotTcpBinaryDeviceMessageCodec binaryCodec;
  27 +
  28 + @Autowired
  29 + private IotTcpJsonDeviceMessageCodec jsonCodec;
  30 +
  31 + /**
  32 + * 当前默认协议(JSON)
  33 + */
  34 + private boolean useJsonByDefault = true;
  35 +
  36 + @Override
  37 + public String type() {
  38 + return TYPE;
  39 + }
  40 +
  41 + @Override
  42 + public byte[] encode(IotDeviceMessage message) {
  43 + // 默认使用JSON协议编码
  44 + return jsonCodec.encode(message);
  45 + }
  46 +
  47 + @Override
  48 + public IotDeviceMessage decode(byte[] bytes) {
  49 + // 自动检测协议类型并解码
  50 + if (isJsonFormat(bytes)) {
  51 + if (log.isDebugEnabled()) {
  52 + log.debug("[decode][检测到JSON协议] 数据长度: {}字节", bytes.length);
  53 + }
  54 + return jsonCodec.decode(bytes);
  55 + } else {
  56 + if (log.isDebugEnabled()) {
  57 + log.debug("[decode][检测到二进制协议] 数据长度: {}字节", bytes.length);
  58 + }
  59 + return binaryCodec.decode(bytes);
  60 + }
  61 + }
  62 +
  63 + // ==================== 便捷方法 ====================
  64 +
  65 + /**
  66 + * 使用JSON协议编码
  67 + */
  68 + public byte[] encodeJson(IotDeviceMessage message) {
  69 + return jsonCodec.encode(message);
  70 + }
  71 +
  72 + /**
  73 + * 使用二进制协议编码
  74 + */
  75 + public byte[] encodeBinary(IotDeviceMessage message) {
  76 + return binaryCodec.encode(message);
  77 + }
  78 +
  79 + /**
  80 + * 获取当前默认协议
  81 + */
  82 + public String getDefaultProtocol() {
  83 + return useJsonByDefault ? "JSON" : "BINARY";
  84 + }
  85 +
  86 + /**
  87 + * 设置默认协议
  88 + */
  89 + public void setDefaultProtocol(boolean useJson) {
  90 + this.useJsonByDefault = useJson;
  91 + log.info("[setDefaultProtocol][设置默认协议] 使用JSON: {}", useJson);
  92 + }
  93 +
  94 + // ==================== 内部方法 ====================
  95 +
  96 + /**
  97 + * 检测是否为JSON格式
  98 + *
  99 + * 检测规则:
  100 + * 1. 数据以 '{' 开头
  101 + * 2. 包含 "method" 或 "id" 字段
  102 + */
  103 + private boolean isJsonFormat(byte[] bytes) {
  104 + if (bytes == null || bytes.length == 0) {
  105 + return useJsonByDefault;
  106 + }
  107 +
  108 + try {
  109 + // 检测JSON格式:以 '{' 开头
  110 + if (bytes[0] == '{') {
  111 + // 进一步验证是否为有效JSON
  112 + String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100));
  113 + return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\"");
  114 + }
  115 +
  116 + // 检测二进制格式:长度 >= 8 且符合二进制协议结构
  117 + if (bytes.length >= 8) {
  118 + // 读取包头(前4字节表示后续数据长度)
  119 + int expectedLength = ((bytes[0] & 0xFF) << 24) |
  120 + ((bytes[1] & 0xFF) << 16) |
  121 + ((bytes[2] & 0xFF) << 8) |
  122 + (bytes[3] & 0xFF);
  123 +
  124 + // 验证长度是否合理
  125 + if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) {
  126 + return false; // 二进制格式
  127 + }
  128 + }
  129 + } catch (Exception e) {
  130 + log.warn("[isJsonFormat][协议检测异常] 使用默认协议: {}", getDefaultProtocol(), e);
  131 + }
  132 +
  133 + // 默认使用当前设置的协议类型
  134 + return useJsonByDefault;
  135 + }
  136 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.codec.tcp;  
2 -  
3 -import cn.hutool.core.util.ObjectUtil;  
4 -import cn.hutool.core.util.StrUtil;  
5 -import cn.hutool.json.JSONException;  
6 -import cn.hutool.json.JSONObject;  
7 -import cn.hutool.json.JSONUtil;  
8 -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;  
9 -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;  
10 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder;  
11 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder;  
12 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage;  
13 -import io.vertx.core.buffer.Buffer;  
14 -import lombok.extern.slf4j.Slf4j;  
15 -import org.springframework.stereotype.Component;  
16 -  
17 -import java.nio.charset.StandardCharsets;  
18 -import java.util.Map;  
19 -import java.util.Objects;  
20 -import java.util.concurrent.ConcurrentHashMap;  
21 -  
22 -/**  
23 - * TCP {@link IotDeviceMessage} 编解码器  
24 - * <p>  
25 - * 参考 EMQX 设计理念:  
26 - * 1. 高性能编解码  
27 - * 2. 容错机制  
28 - * 3. 缓存优化  
29 - * 4. 监控统计  
30 - * 5. 资源管理  
31 - *  
32 - * @author 芋道源码  
33 - */  
34 -@Component  
35 -@Slf4j  
36 -public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec {  
37 -  
38 - /**  
39 - * 编解码器类型  
40 - */  
41 - public static final String TYPE = "tcp";  
42 -  
43 - // ==================== 方法映射 ====================  
44 -  
45 - /**  
46 - * 消息方法到功能码的映射  
47 - */  
48 - private static final Map<String, Short> METHOD_TO_CODE_MAP = new ConcurrentHashMap<>();  
49 -  
50 - /**  
51 - * 功能码到消息方法的映射  
52 - */  
53 - private static final Map<Short, String> CODE_TO_METHOD_MAP = new ConcurrentHashMap<>();  
54 -  
55 - static {  
56 - // 初始化方法映射  
57 - // TODO @haohao:有没可能去掉这个 code 到 method 的映射哈?  
58 - initializeMethodMappings();  
59 - }  
60 -  
61 - // ==================== 缓存管理 ====================  
62 -  
63 - /**  
64 - * JSON 缓存,提升编解码性能  
65 - */  
66 - private final Map<String, JSONObject> jsonCache = new ConcurrentHashMap<>();  
67 -  
68 - /**  
69 - * 缓存最大大小  
70 - */  
71 - private static final int MAX_CACHE_SIZE = 1000;  
72 -  
73 - // ==================== 常量定义 ====================  
74 -  
75 - /**  
76 - * 负载字段名  
77 - */  
78 - public static class PayloadField {  
79 -  
80 - public static final String TIMESTAMP = "timestamp";  
81 - public static final String MESSAGE_ID = "msgId";  
82 - public static final String DEVICE_ID = "deviceId";  
83 - public static final String PARAMS = "params";  
84 - public static final String DATA = "data";  
85 - public static final String CODE = "code";  
86 - public static final String MESSAGE = "message";  
87 -  
88 - }  
89 -  
90 - /**  
91 - * 消息方法映射  
92 - */  
93 - public static class MessageMethod {  
94 -  
95 - public static final String PROPERTY_POST = "thing.property.post";  
96 - public static final String PROPERTY_SET = "thing.property.set";  
97 - public static final String PROPERTY_GET = "thing.property.get";  
98 - public static final String EVENT_POST = "thing.event.post";  
99 - public static final String SERVICE_INVOKE = "thing.service.invoke";  
100 - public static final String CONFIG_PUSH = "thing.config.push";  
101 - public static final String OTA_UPGRADE = "thing.ota.upgrade";  
102 - public static final String STATE_ONLINE = "thing.state.online";  
103 - public static final String STATE_OFFLINE = "thing.state.offline";  
104 -  
105 - }  
106 -  
107 - // ==================== 初始化方法 ====================  
108 -  
109 - /**  
110 - * 初始化方法映射  
111 - */  
112 - private static void initializeMethodMappings() {  
113 - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP);  
114 - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET);  
115 - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET);  
116 - METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP);  
117 - METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE);  
118 - METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN);  
119 - METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN);  
120 - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT);  
121 - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT);  
122 -  
123 - // 反向映射  
124 - METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method));  
125 - }  
126 -  
127 - // ==================== 编解码方法 ====================  
128 -  
129 - @Override  
130 - public byte[] encode(IotDeviceMessage message) {  
131 - validateEncodeParams(message);  
132 -  
133 - try {  
134 - if (log.isDebugEnabled()) {  
135 - log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}",  
136 - message.getMethod(), message.getRequestId());  
137 - }  
138 -  
139 - // 1. 获取功能码  
140 - short code = getCodeByMethodSafely(message.getMethod());  
141 -  
142 - // 2. 构建负载  
143 - String payload = buildPayloadOptimized(message);  
144 -  
145 - // 3. 构建 TCP 数据包  
146 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
147 - .addr("")  
148 - .code(code)  
149 - .mid((short) 0)  
150 - .payload(payload)  
151 - .build();  
152 -  
153 - // 4. 编码为字节流  
154 - Buffer buffer = TcpDataEncoder.encode(dataPackage);  
155 - byte[] result = buffer.getBytes();  
156 -  
157 - // 5. 统计信息  
158 - if (log.isDebugEnabled()) {  
159 - log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}",  
160 - message.getMethod(), result.length);  
161 - }  
162 - return result;  
163 - } catch (Exception e) {  
164 - log.error("[encode][TCP 消息编码失败] 消息: {}", message, e);  
165 - throw new TcpCodecException("TCP 消息编码失败", e);  
166 - }  
167 - }  
168 -  
169 - @Override  
170 - public IotDeviceMessage decode(byte[] bytes) {  
171 - validateDecodeParams(bytes);  
172 -  
173 - try {  
174 - if (log.isDebugEnabled()) {  
175 - log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length);  
176 - }  
177 -  
178 - // 1. 解码 TCP 数据包  
179 - Buffer buffer = Buffer.buffer(bytes);  
180 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);  
181 - // 2. 获取消息方法  
182 - String method = getMethodByCodeSafely(dataPackage.getCode());  
183 - // 3. 解析负载数据  
184 - Object params = parsePayloadOptimized(dataPackage.getPayload());  
185 - // 4. 构建 IoT 设备消息  
186 - IotDeviceMessage message = IotDeviceMessage.requestOf(method, params);  
187 -  
188 - // 5. 统计信息  
189 - if (log.isDebugEnabled()) {  
190 - log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}",  
191 - method, dataPackage.getCode());  
192 - }  
193 - return message;  
194 - } catch (Exception e) {  
195 - log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}",  
196 - bytes.length, truncateData(bytes, 100), e);  
197 - throw new TcpCodecException("TCP 消息解码失败", e);  
198 - }  
199 - }  
200 -  
201 - @Override  
202 - public String type() {  
203 - return TYPE;  
204 - }  
205 -  
206 - // ==================== 内部辅助方法 ====================  
207 -  
208 - /**  
209 - * 验证编码参数  
210 - */  
211 - private void validateEncodeParams(IotDeviceMessage message) {  
212 - if (Objects.isNull(message)) {  
213 - throw new IllegalArgumentException("IoT 设备消息不能为空");  
214 - }  
215 - if (StrUtil.isEmpty(message.getMethod())) {  
216 - throw new IllegalArgumentException("消息方法不能为空");  
217 - }  
218 - }  
219 -  
220 - /**  
221 - * 验证解码参数  
222 - */  
223 - private void validateDecodeParams(byte[] bytes) {  
224 - if (Objects.isNull(bytes) || bytes.length == 0) {  
225 - throw new IllegalArgumentException("待解码数据不能为空");  
226 - }  
227 - if (bytes.length > 1024 * 1024) {  
228 - throw new IllegalArgumentException("数据包过大,超过 1MB 限制");  
229 - }  
230 - }  
231 -  
232 - /**  
233 - * 安全获取功能码  
234 - */  
235 - private short getCodeByMethodSafely(String method) {  
236 - Short code = METHOD_TO_CODE_MAP.get(method);  
237 - // 默认为数据上报  
238 - if (code == null) {  
239 - log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method);  
240 - return TcpDataPackage.CODE_DATA_UP;  
241 - }  
242 - return code;  
243 - }  
244 -  
245 - /**  
246 - * 安全获取消息方法  
247 - */  
248 - private String getMethodByCodeSafely(short code) {  
249 - String method = CODE_TO_METHOD_MAP.get(code);  
250 - if (method == null) {  
251 - log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code);  
252 - return MessageMethod.PROPERTY_POST; // 默认为属性上报  
253 - }  
254 - return method;  
255 - }  
256 -  
257 - /**  
258 - * 优化的负载构建  
259 - */  
260 - private String buildPayloadOptimized(IotDeviceMessage message) {  
261 - // 使用缓存键  
262 - // TODO @haohao:是不是不用缓存哈?  
263 - String cacheKey = message.getMethod() + "_" + message.getRequestId();  
264 - JSONObject cachedPayload = jsonCache.get(cacheKey);  
265 -  
266 - if (cachedPayload != null) {  
267 - // 更新时间戳  
268 - cachedPayload.set(PayloadField.TIMESTAMP, System.currentTimeMillis());  
269 - return cachedPayload.toString();  
270 - }  
271 -  
272 - // 创建新的负载  
273 - JSONObject payload = new JSONObject();  
274 - // 添加基础字段  
275 - addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId());  
276 - addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId());  
277 - addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams());  
278 - addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData());  
279 - addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode());  
280 - addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg());  
281 - // 添加时间戳  
282 - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis());  
283 -  
284 - // 缓存管理  
285 - if (jsonCache.size() < MAX_CACHE_SIZE) {  
286 - jsonCache.put(cacheKey, payload);  
287 - } else {  
288 - cleanJsonCacheIfNeeded();  
289 - }  
290 -  
291 - return payload.toString();  
292 - }  
293 -  
294 - /**  
295 - * 优化的负载解析  
296 - */  
297 - private Object parsePayloadOptimized(String payload) {  
298 - if (StrUtil.isEmpty(payload)) {  
299 - return null;  
300 - }  
301 -  
302 - try {  
303 - // 尝试从缓存获取  
304 - JSONObject cachedJson = jsonCache.get(payload);  
305 - if (cachedJson != null) {  
306 - return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson;  
307 - }  
308 -  
309 - // 解析 JSON 对象  
310 - JSONObject jsonObject = JSONUtil.parseObj(payload);  
311 -  
312 - // 缓存解析结果  
313 - if (jsonCache.size() < MAX_CACHE_SIZE) {  
314 - jsonCache.put(payload, jsonObject);  
315 - }  
316 -  
317 - return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject;  
318 - } catch (JSONException e) {  
319 - log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload);  
320 - return payload;  
321 - } catch (Exception e) {  
322 - log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e);  
323 - return payload;  
324 - }  
325 - }  
326 -  
327 - /**  
328 - * 添加非空值到负载  
329 - */  
330 - private void addToPayloadIfNotNull(JSONObject json, String key, Object value) {  
331 - if (ObjectUtil.isNotNull(value)) {  
332 - json.set(key, value);  
333 - }  
334 - }  
335 -  
336 - /**  
337 - * 添加非空字符串到负载  
338 - */  
339 - private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) {  
340 - if (StrUtil.isNotEmpty(value)) {  
341 - json.set(key, value);  
342 - }  
343 - }  
344 -  
345 - /**  
346 - * 清理JSON缓存  
347 - */  
348 - private void cleanJsonCacheIfNeeded() {  
349 - if (jsonCache.size() > MAX_CACHE_SIZE) {  
350 - // 清理一半的缓存  
351 - int clearCount = jsonCache.size() / 2;  
352 - jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5);  
353 -  
354 - if (log.isDebugEnabled()) {  
355 - log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size());  
356 - }  
357 - }  
358 - }  
359 -  
360 - /**  
361 - * 截断数据用于日志输出  
362 - */  
363 - private String truncateData(byte[] data, int maxLength) {  
364 - if (data.length <= maxLength) {  
365 - return new String(data, StandardCharsets.UTF_8);  
366 - }  
367 -  
368 - byte[] truncated = new byte[maxLength];  
369 - System.arraycopy(data, 0, truncated, 0, maxLength);  
370 - return new String(truncated, StandardCharsets.UTF_8) + "...(截断)";  
371 - }  
372 -  
373 - // ==================== 自定义异常 ====================  
374 -  
375 - /**  
376 - * TCP 编解码异常  
377 - */  
378 - public static class TcpCodecException extends RuntimeException {  
379 -  
380 - public TcpCodecException(String message) {  
381 - super(message);  
382 - }  
383 -  
384 - public TcpCodecException(String message, Throwable cause) {  
385 - super(message, cause);  
386 - }  
387 -  
388 - }  
389 -}  
390 \ 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/codec/tcp/IotTcpJsonDeviceMessageCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.hutool.json.JSONObject;
  6 +import cn.hutool.json.JSONUtil;
  7 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.stereotype.Component;
  11 +
  12 +import java.nio.charset.StandardCharsets;
  13 +
  14 +/**
  15 + * TCP JSON格式 {@link IotDeviceMessage} 编解码器
  16 + *
  17 + * 采用纯JSON格式传输,参考EMQX和HTTP模块的数据格式
  18 + *
  19 + * JSON消息格式:
  20 + * {
  21 + * "id": "消息ID",
  22 + * "method": "消息方法",
  23 + * "deviceId": "设备ID",
  24 + * "productKey": "产品Key",
  25 + * "deviceName": "设备名称",
  26 + * "params": {...},
  27 + * "timestamp": 时间戳
  28 + * }
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Slf4j
  33 +@Component
  34 +public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
  35 +
  36 + public static final String TYPE = "TCP_JSON";
  37 +
  38 + // ==================== 常量定义 ====================
  39 +
  40 + @Override
  41 + public String type() {
  42 + return TYPE;
  43 + }
  44 +
  45 + @Override
  46 + public byte[] encode(IotDeviceMessage message) {
  47 + if (message == null || StrUtil.isEmpty(message.getMethod())) {
  48 + throw new IllegalArgumentException("消息或方法不能为空");
  49 + }
  50 +
  51 + try {
  52 + // 构建JSON消息
  53 + JSONObject jsonMessage = buildJsonMessage(message);
  54 +
  55 + // 转换为字节数组
  56 + String jsonString = jsonMessage.toString();
  57 + byte[] result = jsonString.getBytes(StandardCharsets.UTF_8);
  58 +
  59 + if (log.isDebugEnabled()) {
  60 + log.debug("[encode][编码成功] 方法: {}, JSON长度: {}字节, 内容: {}",
  61 + message.getMethod(), result.length, jsonString);
  62 + }
  63 +
  64 + return result;
  65 + } catch (Exception e) {
  66 + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e);
  67 + throw new RuntimeException("JSON消息编码失败", e);
  68 + }
  69 + }
  70 +
  71 + // ==================== 编解码方法 ====================
  72 +
  73 + @Override
  74 + public IotDeviceMessage decode(byte[] bytes) {
  75 + if (bytes == null || bytes.length == 0) {
  76 + throw new IllegalArgumentException("待解码数据不能为空");
  77 + }
  78 +
  79 + try {
  80 + // 转换为JSON字符串
  81 + String jsonString = new String(bytes, StandardCharsets.UTF_8);
  82 +
  83 + if (log.isDebugEnabled()) {
  84 + log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString);
  85 + }
  86 +
  87 + // 解析JSON消息
  88 + JSONObject jsonMessage = JSONUtil.parseObj(jsonString);
  89 +
  90 + // 构建IoT设备消息
  91 + IotDeviceMessage message = parseJsonMessage(jsonMessage);
  92 +
  93 + if (log.isDebugEnabled()) {
  94 + log.debug("[decode][解码成功] 消息ID: {}, 方法: {}, 设备ID: {}",
  95 + message.getId(), message.getMethod(), message.getDeviceId());
  96 + }
  97 +
  98 + return message;
  99 + } catch (Exception e) {
  100 + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e);
  101 + throw new RuntimeException("JSON消息解码失败", e);
  102 + }
  103 + }
  104 +
  105 + /**
  106 + * 编码数据上报消息
  107 + */
  108 + public byte[] encodeDataReport(Object params, Long deviceId, String productKey, String deviceName) {
  109 + IotDeviceMessage message = createMessage(MessageMethod.PROPERTY_POST, params, deviceId, productKey, deviceName);
  110 + return encode(message);
  111 + }
  112 +
  113 + /**
  114 + * 编码心跳消息
  115 + */
  116 + public byte[] encodeHeartbeat(Long deviceId, String productKey, String deviceName) {
  117 + IotDeviceMessage message = createMessage(MessageMethod.STATE_ONLINE, null, deviceId, productKey, deviceName);
  118 + return encode(message);
  119 + }
  120 +
  121 + // ==================== 便捷方法 ====================
  122 +
  123 + /**
  124 + * 编码事件上报消息
  125 + */
  126 + public byte[] encodeEventReport(Object params, Long deviceId, String productKey, String deviceName) {
  127 + IotDeviceMessage message = createMessage(MessageMethod.EVENT_POST, params, deviceId, productKey, deviceName);
  128 + return encode(message);
  129 + }
  130 +
  131 + /**
  132 + * 构建JSON消息
  133 + */
  134 + private JSONObject buildJsonMessage(IotDeviceMessage message) {
  135 + JSONObject jsonMessage = new JSONObject();
  136 +
  137 + // 基础字段
  138 + jsonMessage.set(JsonField.ID, StrUtil.isNotEmpty(message.getId()) ? message.getId() : IdUtil.fastSimpleUUID());
  139 + jsonMessage.set(JsonField.METHOD, message.getMethod());
  140 + jsonMessage.set(JsonField.TIMESTAMP, System.currentTimeMillis());
  141 +
  142 + // 设备信息
  143 + if (message.getDeviceId() != null) {
  144 + jsonMessage.set(JsonField.DEVICE_ID, message.getDeviceId());
  145 + }
  146 +
  147 + // 参数
  148 + if (message.getParams() != null) {
  149 + jsonMessage.set(JsonField.PARAMS, message.getParams());
  150 + }
  151 +
  152 + // 响应码和消息(用于下行消息)
  153 + if (message.getCode() != null) {
  154 + jsonMessage.set(JsonField.CODE, message.getCode());
  155 + }
  156 + if (StrUtil.isNotEmpty(message.getMsg())) {
  157 + jsonMessage.set(JsonField.MESSAGE, message.getMsg());
  158 + }
  159 +
  160 + return jsonMessage;
  161 + }
  162 +
  163 + /**
  164 + * 解析JSON消息
  165 + */
  166 + private IotDeviceMessage parseJsonMessage(JSONObject jsonMessage) {
  167 + // 提取基础字段
  168 + String id = jsonMessage.getStr(JsonField.ID);
  169 + String method = jsonMessage.getStr(JsonField.METHOD);
  170 + Object params = jsonMessage.get(JsonField.PARAMS);
  171 +
  172 + // 创建消息对象
  173 + IotDeviceMessage message = IotDeviceMessage.requestOf(id, method, params);
  174 +
  175 + // 设置设备信息
  176 + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID);
  177 + if (deviceId != null) {
  178 + message.setDeviceId(deviceId);
  179 + }
  180 +
  181 + // 设置响应信息
  182 + Integer code = jsonMessage.getInt(JsonField.CODE);
  183 + if (code != null) {
  184 + message.setCode(code);
  185 + }
  186 +
  187 + String msg = jsonMessage.getStr(JsonField.MESSAGE);
  188 + if (StrUtil.isNotEmpty(msg)) {
  189 + message.setMsg(msg);
  190 + }
  191 +
  192 + // 设置服务ID(基于JSON格式)
  193 + message.setServerId(generateServerId(jsonMessage));
  194 +
  195 + return message;
  196 + }
  197 +
  198 + // ==================== 内部辅助方法 ====================
  199 +
  200 + /**
  201 + * 创建消息对象
  202 + */
  203 + private IotDeviceMessage createMessage(String method, Object params, Long deviceId, String productKey, String deviceName) {
  204 + IotDeviceMessage message = IotDeviceMessage.requestOf(method, params);
  205 + message.setDeviceId(deviceId);
  206 + return message;
  207 + }
  208 +
  209 + /**
  210 + * 生成服务ID
  211 + */
  212 + private String generateServerId(JSONObject jsonMessage) {
  213 + String id = jsonMessage.getStr(JsonField.ID);
  214 + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID);
  215 + return String.format("tcp_json_%s_%s", deviceId != null ? deviceId : "unknown",
  216 + StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId");
  217 + }
  218 +
  219 + /**
  220 + * 消息方法常量
  221 + */
  222 + public static class MessageMethod {
  223 + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报
  224 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳
  225 + public static final String EVENT_POST = "thing.event.post"; // 事件上报
  226 + public static final String PROPERTY_SET = "thing.property.set"; // 属性设置
  227 + public static final String PROPERTY_GET = "thing.property.get"; // 属性获取
  228 + public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用
  229 + }
  230 +
  231 + /**
  232 + * JSON字段名(参考EMQX和HTTP模块格式)
  233 + */
  234 + private static class JsonField {
  235 + public static final String ID = "id";
  236 + public static final String METHOD = "method";
  237 + public static final String DEVICE_ID = "deviceId";
  238 + public static final String PRODUCT_KEY = "productKey";
  239 + public static final String DEVICE_NAME = "deviceName";
  240 + public static final String PARAMS = "params";
  241 + public static final String TIMESTAMP = "timestamp";
  242 + public static final String CODE = "code";
  243 + public static final String MESSAGE = "message";
  244 + }
  245 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.config;
2 2
3 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 3 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
4 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 4 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  5 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
@@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr @@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr
9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
12 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;  
13 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 13 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
15 import io.vertx.core.Vertx; 15 import io.vertx.core.Vertx;
@@ -90,27 +90,21 @@ public class IotGatewayConfiguration { @@ -90,27 +90,21 @@ public class IotGatewayConfiguration {
90 } 90 }
91 91
92 @Bean 92 @Bean
93 - public TcpDeviceConnectionManager tcpDeviceConnectionManager() {  
94 - return new TcpDeviceConnectionManager();  
95 - }  
96 -  
97 - @Bean  
98 public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, 93 public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
99 - TcpDeviceConnectionManager connectionManager,  
100 IotDeviceService deviceService, 94 IotDeviceService deviceService,
101 IotDeviceMessageService messageService, 95 IotDeviceMessageService messageService,
102 IotDeviceCommonApi deviceApi, 96 IotDeviceCommonApi deviceApi,
  97 + IotTcpCodecManager codecManager,
103 Vertx tcpVertx) { 98 Vertx tcpVertx) {
104 - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager,  
105 - deviceService, messageService, deviceApi, tcpVertx); 99 + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
  100 + deviceService, messageService, deviceApi, codecManager, tcpVertx);
106 } 101 }
107 102
108 @Bean 103 @Bean
109 public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, 104 public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
110 - TcpDeviceConnectionManager connectionManager,  
111 IotDeviceMessageService messageService, 105 IotDeviceMessageService messageService,
112 IotMessageBus messageBus) { 106 IotMessageBus messageBus) {
113 - return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus); 107 + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus);
114 } 108 }
115 109
116 } 110 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
@@ -4,161 +4,67 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; @@ -4,161 +4,67 @@ 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.TcpDeviceConnectionManager;  
8 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10 import jakarta.annotation.PostConstruct; 9 import jakarta.annotation.PostConstruct;
11 -import jakarta.annotation.PreDestroy;  
12 -import lombok.RequiredArgsConstructor;  
13 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
14 11
15 -import java.util.concurrent.atomic.AtomicBoolean;  
16 -import java.util.concurrent.atomic.AtomicLong;  
17 -  
18 /** 12 /**
19 * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 13 * IoT 网关 TCP 下游订阅者:接收下行给设备的消息
20 - * <p>  
21 - * 参考 EMQX 设计理念:  
22 - * 1. 高性能消息路由  
23 - * 2. 容错机制  
24 - * 3. 状态监控  
25 - * 4. 资源管理  
26 * 14 *
27 * @author 芋道源码 15 * @author 芋道源码
28 */ 16 */
29 -@RequiredArgsConstructor  
30 @Slf4j 17 @Slf4j
31 public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { 18 public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
32 19
33 - private final IotTcpUpstreamProtocol protocolHandler;  
34 -  
35 - private final TcpDeviceConnectionManager connectionManager;  
36 -  
37 - private final IotDeviceMessageService messageService; 20 + private final IotTcpDownstreamHandler downstreamHandler;
38 21
39 private final IotMessageBus messageBus; 22 private final IotMessageBus messageBus;
40 23
41 - private volatile IotTcpDownstreamHandler downstreamHandler;  
42 -  
43 - private final AtomicBoolean initialized = new AtomicBoolean(false);  
44 -  
45 - private final AtomicLong processedMessages = new AtomicLong(0); 24 + private final IotTcpUpstreamProtocol protocol;
46 25
47 - private final AtomicLong failedMessages = new AtomicLong(0); 26 + public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol,
  27 + IotDeviceMessageService messageService,
  28 + IotMessageBus messageBus) {
  29 + this.protocol = protocol;
  30 + this.messageBus = messageBus;
  31 + this.downstreamHandler = new IotTcpDownstreamHandler(messageService);
  32 + }
48 33
49 @PostConstruct 34 @PostConstruct
50 public void init() { 35 public void init() {
51 - if (!initialized.compareAndSet(false, true)) {  
52 - log.warn("[init][TCP 下游消息订阅者已初始化,跳过重复初始化]");  
53 - return;  
54 - }  
55 -  
56 - try {  
57 - // 初始化下游处理器  
58 - downstreamHandler = new IotTcpDownstreamHandler(connectionManager, messageService);  
59 -  
60 - // 注册到消息总线  
61 - messageBus.register(this);  
62 -  
63 - log.info("[init][TCP 下游消息订阅者初始化完成] Topic: {}, Group: {}",  
64 - getTopic(), getGroup());  
65 - } catch (Exception e) {  
66 - initialized.set(false);  
67 - log.error("[init][TCP 下游消息订阅者初始化失败]", e);  
68 - throw new RuntimeException("TCP 下游消息订阅者初始化失败", e);  
69 - }  
70 - }  
71 -  
72 - @PreDestroy  
73 - public void destroy() {  
74 - if (!initialized.get()) {  
75 - return;  
76 - }  
77 -  
78 - try {  
79 - log.info("[destroy][TCP 下游消息订阅者已关闭] 处理消息数: {}, 失败消息数: {}",  
80 - processedMessages.get(), failedMessages.get());  
81 - } catch (Exception e) {  
82 - log.error("[destroy][TCP 下游消息订阅者关闭失败]", e);  
83 - } finally {  
84 - initialized.set(false);  
85 - } 36 + messageBus.register(this);
86 } 37 }
87 38
88 @Override 39 @Override
89 public String getTopic() { 40 public String getTopic() {
90 - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId()); 41 + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
91 } 42 }
92 43
93 @Override 44 @Override
94 public String getGroup() { 45 public String getGroup() {
95 - return "tcp-downstream-" + protocolHandler.getServerId(); 46 + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
  47 + return getTopic();
96 } 48 }
97 49
98 @Override 50 @Override
99 public void onMessage(IotDeviceMessage message) { 51 public void onMessage(IotDeviceMessage message) {
100 - if (!initialized.get()) {  
101 - log.warn("[onMessage][订阅者未初始化,跳过消息处理]");  
102 - return;  
103 - }  
104 -  
105 - long startTime = System.currentTimeMillis();  
106 - 52 + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
  53 + message.getId(), message.getMethod(), message.getDeviceId());
107 try { 54 try {
108 - processedMessages.incrementAndGet();  
109 -  
110 - if (log.isDebugEnabled()) {  
111 - log.debug("[onMessage][收到下行消息] 设备 ID: {}, 方法: {}, 消息ID: {}",  
112 - message.getDeviceId(), message.getMethod(), message.getId());  
113 - }  
114 - // 参数校验  
115 - if (message.getDeviceId() == null) {  
116 - log.warn("[onMessage][下行消息设备 ID 为空,跳过处理] 消息: {}", message);  
117 - return;  
118 - }  
119 - // 检查连接状态  
120 - if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) {  
121 - log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}",  
122 - message.getDeviceId(), message.getMethod()); 55 + // 1. 校验
  56 + String method = message.getMethod();
  57 + if (method == null) {
  58 + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
  59 + message.getId(), message.getDeviceId());
123 return; 60 return;
124 } 61 }
125 62
126 - // 处理下行消息 63 + // 2. 处理下行消息
127 downstreamHandler.handle(message); 64 downstreamHandler.handle(message);
128 -  
129 - // 性能监控  
130 - long processTime = System.currentTimeMillis() - startTime;  
131 - // TODO @haohao:1000 搞成静态变量;  
132 - if (processTime > 1000) { // 超过 1 秒的慢消息  
133 - log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms",  
134 - message.getDeviceId(), message.getMethod(), processTime);  
135 - }  
136 } catch (Exception e) { 65 } catch (Exception e) {
137 - failedMessages.incrementAndGet();  
138 - log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}",  
139 - message.getDeviceId(), message.getMethod(), message, e); 66 + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
  67 + message.getId(), message.getMethod(), message.getDeviceId(), e);
140 } 68 }
141 } 69 }
142 -  
143 - // TODO @haohao:多余的要不先清理掉;  
144 -  
145 - /**  
146 - * 获取订阅者统计信息  
147 - */  
148 - public String getSubscriberStatistics() {  
149 - return String.format("TCP下游订阅者 - 已处理: %d, 失败: %d, 成功率: %.2f%%",  
150 - processedMessages.get(),  
151 - failedMessages.get(),  
152 - processedMessages.get() > 0  
153 - ? (double) (processedMessages.get() - failedMessages.get()) / processedMessages.get() * 100  
154 - : 0.0);  
155 - }  
156 -  
157 - /**  
158 - * 检查订阅者健康状态  
159 - */  
160 - public boolean isHealthy() {  
161 - return initialized.get() && downstreamHandler != null;  
162 - }  
163 -  
164 } 70 }
165 \ No newline at end of file 71 \ 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,8 +2,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
2 2
3 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 3 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  5 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager;
5 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;  
7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
8 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 8 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -16,19 +16,8 @@ import jakarta.annotation.PreDestroy; @@ -16,19 +16,8 @@ import jakarta.annotation.PreDestroy;
16 import lombok.Getter; 16 import lombok.Getter;
17 import lombok.extern.slf4j.Slf4j; 17 import lombok.extern.slf4j.Slf4j;
18 18
19 -import java.util.concurrent.CountDownLatch;  
20 -import java.util.concurrent.TimeUnit;  
21 -import java.util.concurrent.atomic.AtomicReference;  
22 -  
23 /** 19 /**
24 * IoT 网关 TCP 协议:接收设备上行消息 20 * IoT 网关 TCP 协议:接收设备上行消息
25 - * <p>  
26 - * 负责接收设备上行消息,支持:  
27 - * 1. 设备注册  
28 - * 2. 心跳保活  
29 - * 3. 属性上报  
30 - * 4. 事件上报  
31 - * 5. 设备连接管理  
32 * 21 *
33 * @author 芋道源码 22 * @author 芋道源码
34 */ 23 */
@@ -37,14 +26,14 @@ public class IotTcpUpstreamProtocol { @@ -37,14 +26,14 @@ public class IotTcpUpstreamProtocol {
37 26
38 private final IotGatewayProperties.TcpProperties tcpProperties; 27 private final IotGatewayProperties.TcpProperties tcpProperties;
39 28
40 - private final TcpDeviceConnectionManager connectionManager;  
41 -  
42 private final IotDeviceService deviceService; 29 private final IotDeviceService deviceService;
43 30
44 private final IotDeviceMessageService messageService; 31 private final IotDeviceMessageService messageService;
45 32
46 private final IotDeviceCommonApi deviceApi; 33 private final IotDeviceCommonApi deviceApi;
47 34
  35 + private final IotTcpCodecManager codecManager;
  36 +
48 private final Vertx vertx; 37 private final Vertx vertx;
49 38
50 @Getter 39 @Getter
@@ -53,54 +42,30 @@ public class IotTcpUpstreamProtocol { @@ -53,54 +42,30 @@ public class IotTcpUpstreamProtocol {
53 private NetServer netServer; 42 private NetServer netServer;
54 43
55 public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, 44 public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
56 - TcpDeviceConnectionManager connectionManager,  
57 - IotDeviceService deviceService,  
58 - IotDeviceMessageService messageService,  
59 - IotDeviceCommonApi deviceApi,  
60 - Vertx vertx) { 45 + IotDeviceService deviceService,
  46 + IotDeviceMessageService messageService,
  47 + IotDeviceCommonApi deviceApi,
  48 + IotTcpCodecManager codecManager,
  49 + Vertx vertx) {
61 this.tcpProperties = tcpProperties; 50 this.tcpProperties = tcpProperties;
62 - this.connectionManager = connectionManager;  
63 this.deviceService = deviceService; 51 this.deviceService = deviceService;
64 this.messageService = messageService; 52 this.messageService = messageService;
65 this.deviceApi = deviceApi; 53 this.deviceApi = deviceApi;
  54 + this.codecManager = codecManager;
66 this.vertx = vertx; 55 this.vertx = vertx;
67 this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); 56 this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
68 } 57 }
69 58
70 @PostConstruct 59 @PostConstruct
71 public void start() { 60 public void start() {
72 - // 1. 启动 TCP 服务器  
73 - try {  
74 - startTcpServer();  
75 - log.info("[start][IoT 网关 TCP 协议处理器启动完成,服务器ID: {}]", serverId);  
76 - } catch (Exception e) {  
77 - log.error("[start][IoT 网关 TCP 协议处理器启动失败]", e);  
78 - // 抛出异常,中断 Spring 容器启动  
79 - throw new RuntimeException("IoT 网关 TCP 协议处理器启动失败", e);  
80 - }  
81 - }  
82 -  
83 - @PreDestroy  
84 - public void stop() {  
85 - if (netServer != null) {  
86 - stopTcpServer();  
87 - log.info("[stop][IoT 网关 TCP 协议处理器已停止]");  
88 - }  
89 - }  
90 -  
91 - /**  
92 - * 启动 TCP 服务器  
93 - */  
94 - private void startTcpServer() {  
95 - // TODO @haohao:同类的,最好使用相同序号前缀,一个方法看起来有段落感。包括同类可以去掉之间的空格。例如说这里的,1. 2. 3. 4. 是初始化;5. 6. 是管理启动  
96 - // 1. 创建服务器选项 61 + // 创建服务器选项
97 NetServerOptions options = new NetServerOptions() 62 NetServerOptions options = new NetServerOptions()
98 .setPort(tcpProperties.getPort()) 63 .setPort(tcpProperties.getPort())
99 .setTcpKeepAlive(true) 64 .setTcpKeepAlive(true)
100 .setTcpNoDelay(true) 65 .setTcpNoDelay(true)
101 .setReuseAddress(true); 66 .setReuseAddress(true);
102 67
103 - // 2. 配置 SSL(如果启用) 68 + // 配置 SSL(如果启用)
104 if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { 69 if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
105 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() 70 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
106 .setKeyPath(tcpProperties.getSslKeyPath()) 71 .setKeyPath(tcpProperties.getSslKeyPath())
@@ -108,72 +73,32 @@ public class IotTcpUpstreamProtocol { @@ -108,72 +73,32 @@ public class IotTcpUpstreamProtocol {
108 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); 73 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
109 } 74 }
110 75
111 - // 3. 创建 TCP 服务器 76 + // 创建服务器并设置连接处理器
112 netServer = vertx.createNetServer(options); 77 netServer = vertx.createNetServer(options);
113 -  
114 - // 4. 设置连接处理器  
115 netServer.connectHandler(socket -> { 78 netServer.connectHandler(socket -> {
116 - log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress());  
117 - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(  
118 - tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId); 79 + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager);
119 handler.handle(socket); 80 handler.handle(socket);
120 }); 81 });
121 82
122 - // 5. 同步启动服务器,等待结果  
123 - CountDownLatch latch = new CountDownLatch(1);  
124 - AtomicReference<Throwable> failure = new AtomicReference<>();  
125 - netServer.listen(result -> {  
126 - if (result.succeeded()) {  
127 - log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}",  
128 - result.result().actualPort(), serverId);  
129 - } else {  
130 - log.error("[startTcpServer][TCP 服务器启动失败]", result.cause());  
131 - failure.set(result.cause());  
132 - }  
133 - latch.countDown();  
134 - });  
135 -  
136 - // 6. 等待启动结果,设置超时 83 + // 启动服务器
137 try { 84 try {
138 - if (!latch.await(10, TimeUnit.SECONDS)) {  
139 - throw new RuntimeException("TCP 服务器启动超时");  
140 - }  
141 - if (failure.get() != null) {  
142 - throw new RuntimeException("TCP 服务器启动失败", failure.get());  
143 - }  
144 - } catch (InterruptedException e) {  
145 - Thread.currentThread().interrupt();  
146 - throw new RuntimeException("TCP 服务器启动被中断", e); 85 + netServer.listen().result();
  86 + log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort());
  87 + } catch (Exception e) {
  88 + log.error("[start][IoT 网关 TCP 协议启动失败]", e);
  89 + throw e;
147 } 90 }
148 } 91 }
149 92
150 - /**  
151 - * 停止 TCP 服务器  
152 - */  
153 - private void stopTcpServer() {  
154 - if (netServer == null) {  
155 - return;  
156 - }  
157 - log.info("[stopTcpServer][准备关闭 TCP 服务器]");  
158 - CountDownLatch latch = new CountDownLatch(1);  
159 - // 异步关闭,并使用 Latch 等待结果  
160 - netServer.close(result -> {  
161 - if (result.succeeded()) {  
162 - log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]");  
163 - } else {  
164 - log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause());  
165 - }  
166 - latch.countDown();  
167 - });  
168 -  
169 - try {  
170 - // 等待关闭完成,设置超时  
171 - if (!latch.await(10, TimeUnit.SECONDS)) {  
172 - log.warn("[stopTcpServer][关闭 TCP 服务器超时]"); 93 + @PreDestroy
  94 + public void stop() {
  95 + if (netServer != null) {
  96 + try {
  97 + netServer.close().result();
  98 + log.info("[stop][IoT 网关 TCP 协议已停止]");
  99 + } catch (Exception e) {
  100 + log.error("[stop][IoT 网关 TCP 协议停止失败]", e);
173 } 101 }
174 - } catch (InterruptedException e) {  
175 - Thread.currentThread().interrupt();  
176 - log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e);  
177 } 102 }
178 } 103 }
179 } 104 }
180 \ No newline at end of file 105 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client;  
2 -  
3 -import io.vertx.core.buffer.Buffer;  
4 -import io.vertx.core.net.NetSocket;  
5 -import io.vertx.core.parsetools.RecordParser;  
6 -import lombok.Getter;  
7 -import lombok.Setter;  
8 -import lombok.extern.slf4j.Slf4j;  
9 -  
10 -import java.util.concurrent.atomic.AtomicBoolean;  
11 -  
12 -/**  
13 - * TCP 设备客户端:封装设备连接的基本信息和操作  
14 - * <p>  
15 - * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。  
16 - * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。  
17 - *  
18 - * @author 芋道源码  
19 - */  
20 -@Slf4j  
21 -public class TcpDeviceClient {  
22 -  
23 - @Getter  
24 - private final String clientId;  
25 -  
26 - @Getter  
27 - @Setter  
28 - private String deviceAddr; // 从 final 移除,因为在注册后才设置  
29 -  
30 - @Getter  
31 - @Setter  
32 - private String productKey;  
33 -  
34 - @Getter  
35 - @Setter  
36 - private String deviceName;  
37 -  
38 - @Getter  
39 - @Setter  
40 - private Long deviceId;  
41 -  
42 - @Getter  
43 - private NetSocket socket;  
44 -  
45 - @Getter  
46 - @Setter  
47 - private RecordParser parser;  
48 -  
49 - @Getter  
50 - private final long keepAliveTimeoutMs;  
51 -  
52 - private volatile long lastKeepAliveTime;  
53 -  
54 - private final AtomicBoolean authenticated = new AtomicBoolean(false);  
55 - private final AtomicBoolean closed = new AtomicBoolean(false);  
56 -  
57 - /**  
58 - * 构造函数  
59 - *  
60 - * @param clientId 客户端 ID,全局唯一  
61 - * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取  
62 - */  
63 - public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) {  
64 - this.clientId = clientId;  
65 - this.keepAliveTimeoutMs = keepAliveTimeoutMs;  
66 - this.lastKeepAliveTime = System.currentTimeMillis();  
67 - }  
68 -  
69 - /**  
70 - * 绑定网络套接字,并设置相关处理器。  
71 - * 此方法应在 Vert.x Event Loop 线程中调用  
72 - *  
73 - * @param socket 网络套接字  
74 - */  
75 - public void setSocket(NetSocket socket) {  
76 - // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理  
77 - if (this.socket != null && this.socket != socket) {  
78 - log.warn("[setSocket][客户端({}) 正在用新的 socket 替换旧的,旧 socket 将被关闭]", clientId);  
79 - this.socket.close();  
80 - }  
81 - this.socket = socket;  
82 -  
83 - // 注册处理器  
84 - if (socket != null) {  
85 - // 1. 设置关闭处理器  
86 - socket.closeHandler(v -> {  
87 - log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId);  
88 - shutdown(); // 统一调用 shutdown 进行资源清理  
89 - });  
90 -  
91 - // 2. 设置异常处理器  
92 - socket.exceptionHandler(e -> {  
93 - log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e);  
94 - shutdown(); // 出现异常时也关闭连接  
95 - });  
96 -  
97 - // 3. 设置数据处理器  
98 - socket.handler(buffer -> {  
99 - // 任何数据往来都表示连接是活跃的  
100 - keepAlive();  
101 -  
102 - if (parser != null) {  
103 - parser.handle(buffer);  
104 - } else {  
105 - log.warn("[setSocket][设备客户端({}) 未设置解析器(parser),原始数据被忽略: {}]", clientId, buffer.toString());  
106 - }  
107 - });  
108 - }  
109 - }  
110 -  
111 - /**  
112 - * 更新心跳时间,表示设备仍然活跃  
113 - */  
114 - public void keepAlive() {  
115 - this.lastKeepAliveTime = System.currentTimeMillis();  
116 - }  
117 -  
118 - /**  
119 - * 检查连接是否在线  
120 - * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内  
121 - *  
122 - * @return 是否在线  
123 - */  
124 - public boolean isOnline() {  
125 - if (closed.get() || socket == null) {  
126 - return false;  
127 - }  
128 - long idleTime = System.currentTimeMillis() - lastKeepAliveTime;  
129 - return idleTime < keepAliveTimeoutMs;  
130 - }  
131 -  
132 - // TODO @haohao:1)是不是简化下:productKey 和 deviceName 非空,就认为是已认证;2)如果是的话,productKey 和 deviceName 搞成一个设置方法?setAuthenticated(productKey、deviceName)  
133 -  
134 - public boolean isAuthenticated() {  
135 - return authenticated.get();  
136 - }  
137 -  
138 - public void setAuthenticated(boolean authenticated) {  
139 - this.authenticated.set(authenticated);  
140 - }  
141 -  
142 - /**  
143 - * 向设备发送消息  
144 - *  
145 - * @param buffer 消息内容  
146 - */  
147 - public void sendMessage(Buffer buffer) {  
148 - if (closed.get() || socket == null) {  
149 - log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId);  
150 - return;  
151 - }  
152 -  
153 - // Vert.x 的 write 是异步的,不会阻塞  
154 - socket.write(buffer, result -> {  
155 - // 发送失败可能意味着连接已断开,主动关闭  
156 - if (!result.succeeded()) {  
157 - log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause());  
158 - shutdown();  
159 - return;  
160 - }  
161 -  
162 - // 发送成功也更新心跳,表示连接活跃  
163 - if (log.isDebugEnabled()) {  
164 - log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId);  
165 - }  
166 - keepAlive();  
167 - });  
168 - }  
169 -  
170 - // TODO @haohao:是不是叫 close 好点?或者问问大模型  
171 - /**  
172 - * 关闭客户端连接并清理资源。  
173 - * 这是一个幂等操作,可以被多次安全调用。  
174 - */  
175 - public void shutdown() {  
176 - // 使用原子操作保证只执行一次关闭逻辑  
177 - if (closed.getAndSet(true)) {  
178 - return;  
179 - }  
180 -  
181 - log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId);  
182 -  
183 - // 先将 socket 引用置空,再关闭,避免并发问题  
184 - NetSocket socketToClose = this.socket;  
185 - this.socket = null;  
186 -  
187 - if (socketToClose != null) {  
188 - try {  
189 - // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭  
190 - socketToClose.close();  
191 - } catch (Exception e) {  
192 - log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e);  
193 - }  
194 - }  
195 -  
196 - // 重置认证状态  
197 - authenticated.set(false);  
198 - }  
199 -  
200 - public String getConnectionInfo() {  
201 - NetSocket currentSocket = this.socket;  
202 - if (currentSocket != null && currentSocket.remoteAddress() != null) {  
203 - return currentSocket.remoteAddress().toString();  
204 - }  
205 - return "disconnected";  
206 - }  
207 -  
208 - @Override  
209 - public String toString() {  
210 - return "TcpDeviceClient{" +  
211 - "clientId='" + clientId + '\'' +  
212 - ", deviceAddr='" + deviceAddr + '\'' +  
213 - ", deviceId=" + deviceId +  
214 - ", authenticated=" + authenticated.get() +  
215 - ", online=" + isOnline() +  
216 - ", connection=" + getConnectionInfo() +  
217 - '}';  
218 - }  
219 -  
220 -}  
221 \ 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/TcpDeviceConnectionManager.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;  
2 -  
3 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient;  
4 -import io.vertx.core.buffer.Buffer;  
5 -import io.vertx.core.net.NetSocket;  
6 -import lombok.extern.slf4j.Slf4j;  
7 -import org.springframework.stereotype.Component;  
8 -  
9 -import java.util.concurrent.*;  
10 -import java.util.concurrent.atomic.AtomicLong;  
11 -import java.util.concurrent.locks.ReentrantReadWriteLock;  
12 -  
13 -/**  
14 - * TCP 设备连接管理器  
15 - * <p>  
16 - * 参考 EMQX 设计理念:  
17 - * 1. 高性能连接管理  
18 - * 2. 连接池和资源管理  
19 - * 3. 流量控制 TODO @haohao:这个要不先去掉  
20 - * 4. 监控统计 TODO @haohao:这个要不先去掉  
21 - * 5. 自动清理和容错  
22 - *  
23 - * @author 芋道源码  
24 - */  
25 -@Component  
26 -@Slf4j  
27 -public class TcpDeviceConnectionManager {  
28 -  
29 - // ==================== 连接存储 ====================  
30 -  
31 - /**  
32 - * 设备客户端映射  
33 - * Key: 设备地址, Value: 设备客户端  
34 - */  
35 - private final ConcurrentMap<String, TcpDeviceClient> clientMap = new ConcurrentHashMap<>();  
36 -  
37 - /**  
38 - * 设备ID到设备地址的映射  
39 - * Key: 设备ID, Value: 设备地址  
40 - */  
41 - private final ConcurrentMap<Long, String> deviceIdToAddrMap = new ConcurrentHashMap<>();  
42 -  
43 - /**  
44 - * 套接字到客户端的映射,用于快速查找  
45 - * Key: NetSocket, Value: 设备地址  
46 - */  
47 - private final ConcurrentMap<NetSocket, String> socketToAddrMap = new ConcurrentHashMap<>();  
48 -  
49 - // ==================== 读写锁 ====================  
50 -  
51 - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  
52 - private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();  
53 - private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();  
54 -  
55 - // ==================== 定时任务 ====================  
56 -  
57 - /**  
58 - * 定时任务执行器  
59 - */  
60 - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> {  
61 - Thread t = new Thread(r, "tcp-connection-manager");  
62 - t.setDaemon(true);  
63 - return t;  
64 - });  
65 -  
66 - // ==================== 统计信息 ====================  
67 -  
68 - private final AtomicLong totalConnections = new AtomicLong(0);  
69 - private final AtomicLong totalDisconnections = new AtomicLong(0);  
70 - private final AtomicLong totalMessages = new AtomicLong(0);  
71 - private final AtomicLong totalFailedMessages = new AtomicLong(0);  
72 - private final AtomicLong totalBytes = new AtomicLong(0);  
73 -  
74 - // ==================== 配置参数 ====================  
75 -  
76 - private static final int MAX_CONNECTIONS = 10000;  
77 - private static final int HEARTBEAT_CHECK_INTERVAL = 30; // 秒  
78 - private static final int CONNECTION_CLEANUP_INTERVAL = 60; // 秒  
79 - private static final int STATS_LOG_INTERVAL = 300; // 秒  
80 -  
81 - /**  
82 - * 构造函数,启动定时任务  
83 - */  
84 - public TcpDeviceConnectionManager() {  
85 - startScheduledTasks();  
86 - }  
87 -  
88 - /**  
89 - * 启动定时任务  
90 - */  
91 - private void startScheduledTasks() {  
92 - // 心跳检查任务  
93 - scheduler.scheduleAtFixedRate(this::checkHeartbeat,  
94 - HEARTBEAT_CHECK_INTERVAL, HEARTBEAT_CHECK_INTERVAL, TimeUnit.SECONDS);  
95 -  
96 - // 连接清理任务  
97 - scheduler.scheduleAtFixedRate(this::cleanupConnections,  
98 - CONNECTION_CLEANUP_INTERVAL, CONNECTION_CLEANUP_INTERVAL, TimeUnit.SECONDS);  
99 -  
100 - // 统计日志任务  
101 - scheduler.scheduleAtFixedRate(this::logStatistics,  
102 - STATS_LOG_INTERVAL, STATS_LOG_INTERVAL, TimeUnit.SECONDS);  
103 - }  
104 -  
105 - /**  
106 - * 添加设备客户端  
107 - */  
108 - public boolean addClient(String deviceAddr, TcpDeviceClient client) {  
109 - // TODO @haohao:这个要不去掉;目前看着没做 result 的处理;  
110 - if (clientMap.size() >= MAX_CONNECTIONS) {  
111 - log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr);  
112 - return false;  
113 - }  
114 -  
115 - writeLock.lock();  
116 - try {  
117 - log.info("[addClient][添加设备客户端: {}]", deviceAddr);  
118 -  
119 - // 关闭之前的连接(如果存在)  
120 - TcpDeviceClient existingClient = clientMap.get(deviceAddr);  
121 - if (existingClient != null) {  
122 - log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr);  
123 - removeClientInternal(deviceAddr, existingClient);  
124 - }  
125 -  
126 - // 添加新连接  
127 - clientMap.put(deviceAddr, client);  
128 -  
129 - // 添加套接字映射  
130 - if (client.getSocket() != null) {  
131 - socketToAddrMap.put(client.getSocket(), deviceAddr);  
132 - }  
133 -  
134 - // 如果客户端已设置设备 ID,更新映射  
135 - if (client.getDeviceId() != null) {  
136 - deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr);  
137 - }  
138 -  
139 - totalConnections.incrementAndGet();  
140 - return true;  
141 - } finally {  
142 - writeLock.unlock();  
143 - }  
144 - }  
145 -  
146 - /**  
147 - * 移除设备客户端  
148 - */  
149 - public void removeClient(String deviceAddr) {  
150 - writeLock.lock();  
151 - try {  
152 - TcpDeviceClient client = clientMap.get(deviceAddr);  
153 - if (client != null) {  
154 - removeClientInternal(deviceAddr, client);  
155 - }  
156 - } finally {  
157 - writeLock.unlock();  
158 - }  
159 - }  
160 -  
161 - /**  
162 - * 内部移除客户端方法(无锁)  
163 - */  
164 - private void removeClientInternal(String deviceAddr, TcpDeviceClient client) {  
165 - log.info("[removeClient][移除设备客户端: {}]", deviceAddr);  
166 -  
167 - // 从映射中移除  
168 - clientMap.remove(deviceAddr);  
169 -  
170 - // 移除套接字映射  
171 - if (client.getSocket() != null) {  
172 - socketToAddrMap.remove(client.getSocket());  
173 - }  
174 -  
175 - // 移除设备ID映射  
176 - if (client.getDeviceId() != null) {  
177 - deviceIdToAddrMap.remove(client.getDeviceId());  
178 - }  
179 -  
180 - // 关闭连接  
181 - client.shutdown();  
182 -  
183 - totalDisconnections.incrementAndGet();  
184 - }  
185 -  
186 - /**  
187 - * 通过设备地址获取客户端  
188 - */  
189 - public TcpDeviceClient getClient(String deviceAddr) {  
190 - readLock.lock();  
191 - try {  
192 - return clientMap.get(deviceAddr);  
193 - } finally {  
194 - readLock.unlock();  
195 - }  
196 - }  
197 -  
198 - /**  
199 - * 通过设备 ID 获取客户端  
200 - */  
201 - public TcpDeviceClient getClientByDeviceId(Long deviceId) {  
202 - readLock.lock();  
203 - try {  
204 - String deviceAddr = deviceIdToAddrMap.get(deviceId);  
205 - return deviceAddr != null ? clientMap.get(deviceAddr) : null;  
206 - } finally {  
207 - readLock.unlock();  
208 - }  
209 - }  
210 -  
211 - // TODO @haohao:getClientBySocket、isDeviceOnline、sendMessage、sendMessageByDeviceId、broadcastMessage 用不到的方法,要不先暂时不提供?保持简洁、更容易理解哈。  
212 -  
213 - /**  
214 - * 通过网络连接获取客户端  
215 - */  
216 - public TcpDeviceClient getClientBySocket(NetSocket socket) {  
217 - readLock.lock();  
218 - try {  
219 - String deviceAddr = socketToAddrMap.get(socket);  
220 - return deviceAddr != null ? clientMap.get(deviceAddr) : null;  
221 - } finally {  
222 - readLock.unlock();  
223 - }  
224 - }  
225 -  
226 - /**  
227 - * 检查设备是否在线  
228 - */  
229 - public boolean isDeviceOnline(Long deviceId) {  
230 - TcpDeviceClient client = getClientByDeviceId(deviceId);  
231 - return client != null && client.isOnline();  
232 - }  
233 -  
234 - /**  
235 - * 设置设备 ID 映射  
236 - */  
237 - public void setDeviceIdMapping(String deviceAddr, Long deviceId) {  
238 - writeLock.lock();  
239 - try {  
240 - TcpDeviceClient client = clientMap.get(deviceAddr);  
241 - if (client != null) {  
242 - client.setDeviceId(deviceId);  
243 - deviceIdToAddrMap.put(deviceId, deviceAddr);  
244 - log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId);  
245 - }  
246 - } finally {  
247 - writeLock.unlock();  
248 - }  
249 - }  
250 -  
251 - /**  
252 - * 发送消息给设备  
253 - */  
254 - public boolean sendMessage(String deviceAddr, Buffer buffer) {  
255 - TcpDeviceClient client = getClient(deviceAddr);  
256 - if (client != null && client.isOnline()) {  
257 - try {  
258 - client.sendMessage(buffer);  
259 - totalMessages.incrementAndGet();  
260 - totalBytes.addAndGet(buffer.length());  
261 - return true;  
262 - } catch (Exception e) {  
263 - totalFailedMessages.incrementAndGet();  
264 - log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e);  
265 - return false;  
266 - }  
267 - }  
268 - log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr);  
269 - return false;  
270 - }  
271 -  
272 - /**  
273 - * 通过设备ID发送消息  
274 - */  
275 - public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) {  
276 - TcpDeviceClient client = getClientByDeviceId(deviceId);  
277 - if (client != null && client.isOnline()) {  
278 - try {  
279 - client.sendMessage(buffer);  
280 - totalMessages.incrementAndGet();  
281 - totalBytes.addAndGet(buffer.length());  
282 - return true;  
283 - } catch (Exception e) {  
284 - totalFailedMessages.incrementAndGet();  
285 - log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e);  
286 - return false;  
287 - }  
288 - }  
289 - log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId);  
290 - return false;  
291 - }  
292 -  
293 - /**  
294 - * 广播消息给所有在线设备  
295 - */  
296 - public int broadcastMessage(Buffer buffer) {  
297 - int successCount = 0;  
298 - readLock.lock();  
299 - try {  
300 - for (TcpDeviceClient client : clientMap.values()) {  
301 - if (client.isOnline()) {  
302 - try {  
303 - client.sendMessage(buffer);  
304 - successCount++;  
305 - } catch (Exception e) {  
306 - log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e);  
307 - }  
308 - }  
309 - }  
310 - } finally {  
311 - readLock.unlock();  
312 - }  
313 -  
314 - totalMessages.addAndGet(successCount);  
315 - totalBytes.addAndGet((long) successCount * buffer.length());  
316 - return successCount;  
317 - }  
318 -  
319 - /**  
320 - * 获取在线设备数量  
321 - */  
322 - public int getOnlineCount() {  
323 - readLock.lock();  
324 - try {  
325 - return (int) clientMap.values().stream()  
326 - .filter(TcpDeviceClient::isOnline)  
327 - .count();  
328 - } finally {  
329 - readLock.unlock();  
330 - }  
331 - }  
332 -  
333 - /**  
334 - * 获取总连接数  
335 - */  
336 - public int getTotalCount() {  
337 - return clientMap.size();  
338 - }  
339 -  
340 - /**  
341 - * 获取认证设备数量  
342 - */  
343 - public int getAuthenticatedCount() {  
344 - readLock.lock();  
345 - try {  
346 - return (int) clientMap.values().stream()  
347 - .filter(TcpDeviceClient::isAuthenticated)  
348 - .count();  
349 - } finally {  
350 - readLock.unlock();  
351 - }  
352 - }  
353 -  
354 - // TODO @haohao:心跳超时,需要 close 么?  
355 - /**  
356 - * 心跳检查任务  
357 - */  
358 - private void checkHeartbeat() {  
359 - try {  
360 - int offlineCount = 0;  
361 -  
362 - readLock.lock();  
363 - try {  
364 - for (TcpDeviceClient client : clientMap.values()) {  
365 - if (!client.isOnline()) {  
366 - offlineCount++;  
367 - }  
368 - }  
369 - } finally {  
370 - readLock.unlock();  
371 - }  
372 -  
373 - if (offlineCount > 0) {  
374 - log.info("[checkHeartbeat][发现 {} 个离线设备,将在清理任务中处理]", offlineCount);  
375 - }  
376 - } catch (Exception e) {  
377 - log.error("[checkHeartbeat][心跳检查任务异常]", e);  
378 - }  
379 - }  
380 -  
381 - /**  
382 - * 连接清理任务  
383 - */  
384 - private void cleanupConnections() {  
385 - try {  
386 - int beforeSize = clientMap.size();  
387 -  
388 - writeLock.lock();  
389 - try {  
390 - clientMap.entrySet().removeIf(entry -> {  
391 - TcpDeviceClient client = entry.getValue();  
392 - if (!client.isOnline()) {  
393 - log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey());  
394 -  
395 - // 清理相关映射  
396 - if (client.getSocket() != null) {  
397 - socketToAddrMap.remove(client.getSocket());  
398 - }  
399 - if (client.getDeviceId() != null) {  
400 - deviceIdToAddrMap.remove(client.getDeviceId());  
401 - }  
402 -  
403 - client.shutdown();  
404 - totalDisconnections.incrementAndGet();  
405 - return true;  
406 - }  
407 - return false;  
408 - });  
409 - } finally {  
410 - writeLock.unlock();  
411 - }  
412 -  
413 - int afterSize = clientMap.size();  
414 - if (beforeSize != afterSize) {  
415 - log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}",  
416 - beforeSize, afterSize, beforeSize - afterSize);  
417 - }  
418 - } catch (Exception e) {  
419 - log.error("[cleanupConnections][连接清理任务异常]", e);  
420 - }  
421 - }  
422 -  
423 - /**  
424 - * 统计日志任务  
425 - */  
426 - private void logStatistics() {  
427 - try {  
428 - long totalConn = totalConnections.get();  
429 - long totalDisconnections = this.totalDisconnections.get();  
430 - long totalMsg = totalMessages.get();  
431 - long totalFailedMsg = totalFailedMessages.get();  
432 - long totalBytesValue = totalBytes.get();  
433 -  
434 - log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " +  
435 - "总消息: {}, 失败消息: {}, 总字节: {}",  
436 - totalConn, totalDisconnections, getOnlineCount(), getAuthenticatedCount(),  
437 - totalMsg, totalFailedMsg, totalBytesValue);  
438 - } catch (Exception e) {  
439 - log.error("[logStatistics][统计日志任务异常]", e);  
440 - }  
441 - }  
442 -  
443 - /**  
444 - * 关闭连接管理器  
445 - */  
446 - public void shutdown() {  
447 - log.info("[shutdown][关闭TCP连接管理器]");  
448 -  
449 - // 关闭定时任务  
450 - scheduler.shutdown();  
451 - try {  
452 - if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {  
453 - scheduler.shutdownNow();  
454 - }  
455 - } catch (InterruptedException e) {  
456 - scheduler.shutdownNow();  
457 - Thread.currentThread().interrupt();  
458 - }  
459 -  
460 - // 关闭所有连接  
461 - writeLock.lock();  
462 - try {  
463 - clientMap.values().forEach(TcpDeviceClient::shutdown);  
464 - clientMap.clear();  
465 - deviceIdToAddrMap.clear();  
466 - socketToAddrMap.clear();  
467 - } finally {  
468 - writeLock.unlock();  
469 - }  
470 - }  
471 -  
472 - /**  
473 - * 获取连接状态信息  
474 - */  
475 - public String getConnectionStatus() {  
476 - return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%",  
477 - getTotalCount(), getOnlineCount(), getAuthenticatedCount(),  
478 - totalMessages.get() > 0  
479 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100  
480 - : 0.0);  
481 - }  
482 -  
483 - /**  
484 - * 获取详细统计信息  
485 - */  
486 - public String getDetailedStatistics() {  
487 - return String.format(  
488 - "TCP连接管理器统计:\n" +  
489 - "- 当前连接数: %d\n" +  
490 - "- 在线设备数: %d\n" +  
491 - "- 认证设备数: %d\n" +  
492 - "- 历史总连接: %d\n" +  
493 - "- 历史总断开: %d\n" +  
494 - "- 总消息数: %d\n" +  
495 - "- 失败消息数: %d\n" +  
496 - "- 总字节数: %d\n" +  
497 - "- 消息成功率: %.2f%%",  
498 - getTotalCount(), getOnlineCount(), getAuthenticatedCount(),  
499 - totalConnections.get(), totalDisconnections.get(),  
500 - totalMessages.get(), totalFailedMessages.get(), totalBytes.get(),  
501 - totalMessages.get() > 0  
502 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100  
503 - : 0.0);  
504 - }  
505 -  
506 -}  
507 \ 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/protocol/TcpDataDecoder.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;  
2 -  
3 -import io.vertx.core.buffer.Buffer;  
4 -import lombok.extern.slf4j.Slf4j;  
5 -  
6 -// TODO @haohao:“设备地址长度”是不是不需要。  
7 -/**  
8 - * TCP 数据解码器  
9 - * <p>  
10 - * 负责将字节流解码为 TcpDataPackage 对象  
11 - * <p>  
12 - * 数据包格式:  
13 - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长)  
14 - *  
15 - * @author 芋道源码  
16 - */  
17 -@Slf4j  
18 -public class TcpDataDecoder {  
19 -  
20 - /**  
21 - * 解码数据包  
22 - *  
23 - * @param buffer 数据缓冲区  
24 - * @return 解码后的数据包  
25 - * @throws IllegalArgumentException 如果数据包格式不正确  
26 - */  
27 - public static TcpDataPackage decode(Buffer buffer) {  
28 - if (buffer == null || buffer.length() < 8) {  
29 - throw new IllegalArgumentException("数据包长度不足");  
30 - }  
31 -  
32 - try {  
33 - int index = 0;  
34 -  
35 - // 1.1 获取设备地址长度(2字节)  
36 - short addrLength = buffer.getShort(index);  
37 - index += 2;  
38 -  
39 - // 1.2 校验数据包长度  
40 - int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号  
41 - if (buffer.length() < expectedLength) {  
42 - throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节");  
43 - }  
44 -  
45 - // 1.3 获取设备地址  
46 - String addr = buffer.getBuffer(index, index + addrLength).toString();  
47 - index += addrLength;  
48 -  
49 - // 1.4 获取功能码(2字节)  
50 - short code = buffer.getShort(index);  
51 - index += 2;  
52 -  
53 - // 1.5 获取消息序号(2字节)  
54 - short mid = buffer.getShort(index);  
55 - index += 2;  
56 -  
57 - // 1.6 获取包体数据  
58 - String payload = "";  
59 - if (index < buffer.length()) {  
60 - payload = buffer.getString(index, buffer.length());  
61 - }  
62 -  
63 - // 2. 构建数据包对象  
64 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
65 - .addrLength((int) addrLength)  
66 - .addr(addr)  
67 - .code(code)  
68 - .mid(mid)  
69 - .payload(payload)  
70 - .build();  
71 -  
72 - log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}",  
73 - addr, dataPackage.getCodeDescription(), mid, payload.length());  
74 - return dataPackage;  
75 - } catch (Exception e) {  
76 - log.error("[decode][解码失败] 数据: {}", buffer.toString(), e);  
77 - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e);  
78 - }  
79 - }  
80 -  
81 - // TODO @haohao:这个要不去掉,暂时没用到;  
82 - /**  
83 - * 校验数据包格式  
84 - *  
85 - * @param buffer 数据缓冲区  
86 - * @return 校验结果  
87 - */  
88 - public static boolean validate(Buffer buffer) {  
89 - try {  
90 - decode(buffer);  
91 - return true;  
92 - } catch (Exception e) {  
93 - log.warn("[validate][数据包格式校验失败] 数据: {}, 错误: {}", buffer.toString(), e.getMessage());  
94 - return false;  
95 - }  
96 - }  
97 -  
98 -}  
99 \ 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/protocol/TcpDataEncoder.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;  
2 -  
3 -import io.vertx.core.buffer.Buffer;  
4 -import lombok.extern.slf4j.Slf4j;  
5 -  
6 -/**  
7 - * TCP 数据编码器  
8 - * <p>  
9 - * 负责将 TcpDataPackage 对象编码为字节流  
10 - * <p>  
11 - * 数据包格式:  
12 - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长)  
13 - *  
14 - * @author 芋道源码  
15 - */  
16 -@Slf4j  
17 -public class TcpDataEncoder {  
18 -  
19 - /**  
20 - * 编码数据包  
21 - *  
22 - * @param dataPackage 数据包对象  
23 - * @return 编码后的字节流  
24 - * @throws IllegalArgumentException 如果数据包对象不正确  
25 - */  
26 - public static Buffer encode(TcpDataPackage dataPackage) {  
27 - if (dataPackage == null) {  
28 - throw new IllegalArgumentException("数据包对象不能为空");  
29 - }  
30 - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) {  
31 - throw new IllegalArgumentException("设备地址不能为空");  
32 - }  
33 - if (dataPackage.getPayload() == null) {  
34 - dataPackage.setPayload("");  
35 - }  
36 -  
37 - try {  
38 - Buffer buffer = Buffer.buffer();  
39 -  
40 - // 1. 计算包体长度(除了包头 4 字节)  
41 - int payloadLength = dataPackage.getPayload().getBytes().length;  
42 - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength;  
43 -  
44 - // 2.1 写入包头:总长度(4 字节)  
45 - buffer.appendInt(totalLength);  
46 - // 2.2 写入设备地址长度(2 字节)  
47 - buffer.appendShort((short) dataPackage.getAddr().length());  
48 - // 2.3 写入设备地址(不定长)  
49 - buffer.appendBytes(dataPackage.getAddr().getBytes());  
50 - // 2.4 写入功能码(2 字节)  
51 - buffer.appendShort(dataPackage.getCode());  
52 - // 2.5 写入消息序号(2 字节)  
53 - buffer.appendShort(dataPackage.getMid());  
54 - // 2.6 写入包体数据(不定长)  
55 - buffer.appendBytes(dataPackage.getPayload().getBytes());  
56 -  
57 - log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}",  
58 - dataPackage.getAddr(), dataPackage.getCodeDescription(),  
59 - dataPackage.getMid(), buffer.length());  
60 - return buffer;  
61 - } catch (Exception e) {  
62 - log.error("[encode][编码失败] 数据包: {}", dataPackage, e);  
63 - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e);  
64 - }  
65 - }  
66 -  
67 - /**  
68 - * 创建注册回复数据包  
69 - *  
70 - * @param addr 设备地址  
71 - * @param mid 消息序号  
72 - * @param success 是否成功  
73 - * @return 编码后的数据包  
74 - */  
75 - public static Buffer createRegisterReply(String addr, short mid, boolean success) {  
76 - // TODO @haohao:payload 默认成功、失败,最好讴有个枚举  
77 - String payload = success ? "0" : "1"; // 0 表示成功,1 表示失败  
78 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
79 - .addr(addr)  
80 - .code(TcpDataPackage.CODE_REGISTER_REPLY)  
81 - .mid(mid)  
82 - .payload(payload)  
83 - .build();  
84 - return encode(dataPackage);  
85 - }  
86 -  
87 - /**  
88 - * 创建数据下发数据包  
89 - *  
90 - * @param addr 设备地址  
91 - * @param mid 消息序号  
92 - * @param data 下发数据  
93 - * @return 编码后的数据包  
94 - */  
95 - public static Buffer createDataDownPackage(String addr, short mid, String data) {  
96 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
97 - .addr(addr)  
98 - .code(TcpDataPackage.CODE_DATA_DOWN)  
99 - .mid(mid)  
100 - .payload(data)  
101 - .build();  
102 - return encode(dataPackage);  
103 - }  
104 -  
105 - /**  
106 - * 创建服务调用数据包  
107 - *  
108 - * @param addr 设备地址  
109 - * @param mid 消息序号  
110 - * @param serviceData 服务数据  
111 - * @return 编码后的数据包  
112 - */  
113 - public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) {  
114 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
115 - .addr(addr)  
116 - .code(TcpDataPackage.CODE_SERVICE_INVOKE)  
117 - .mid(mid)  
118 - .payload(serviceData)  
119 - .build();  
120 - return encode(dataPackage);  
121 - }  
122 -  
123 - /**  
124 - * 创建属性设置数据包  
125 - *  
126 - * @param addr 设备地址  
127 - * @param mid 消息序号  
128 - * @param propertyData 属性数据  
129 - * @return 编码后的数据包  
130 - */  
131 - public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) {  
132 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
133 - .addr(addr)  
134 - .code(TcpDataPackage.CODE_PROPERTY_SET)  
135 - .mid(mid)  
136 - .payload(propertyData)  
137 - .build();  
138 - return encode(dataPackage);  
139 - }  
140 -  
141 - /**  
142 - * 创建属性获取数据包  
143 - *  
144 - * @param addr 设备地址  
145 - * @param mid 消息序号  
146 - * @param propertyNames 属性名称列表  
147 - * @return 编码后的数据包  
148 - */  
149 - public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) {  
150 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
151 - .addr(addr)  
152 - .code(TcpDataPackage.CODE_PROPERTY_GET)  
153 - .mid(mid)  
154 - .payload(propertyNames)  
155 - .build();  
156 - return encode(dataPackage);  
157 - }  
158 -  
159 -}  
160 \ 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/protocol/TcpDataPackage.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;  
2 -  
3 -import lombok.AllArgsConstructor;  
4 -import lombok.Builder;  
5 -import lombok.Data;  
6 -import lombok.NoArgsConstructor;  
7 -  
8 -/**  
9 - * TCP 数据包协议定义  
10 - * <p>  
11 - * 数据包格式:  
12 - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长)  
13 - *  
14 - * @author 芋道源码  
15 - */  
16 -@Data  
17 -@AllArgsConstructor  
18 -@NoArgsConstructor  
19 -@Builder  
20 -public class TcpDataPackage {  
21 -  
22 - // ==================== 功能码定义 ====================  
23 -  
24 - /**  
25 - * 设备注册  
26 - */  
27 - public static final short CODE_REGISTER = 10;  
28 - /**  
29 - * 注册回复  
30 - */  
31 - public static final short CODE_REGISTER_REPLY = 11;  
32 - // TODO @haohao:【重要】一般心跳,服务端会回复一条;回复要搞独立的 code 码,还是继续用原来的,因为 requestId 可以映射;  
33 - /**  
34 - * 心跳  
35 - */  
36 - public static final short CODE_HEARTBEAT = 20;  
37 - // TODO @haohao:【重要】下面的,是不是融合成消息上行(client -> server),消息下行(server -> client);然后把 method 放到 body 里?  
38 - /**  
39 - * 数据上报  
40 - */  
41 - public static final short CODE_DATA_UP = 30;  
42 - /**  
43 - * 事件上报  
44 - */  
45 - public static final short CODE_EVENT_UP = 40;  
46 - /**  
47 - * 数据下发  
48 - */  
49 - public static final short CODE_DATA_DOWN = 50;  
50 - /**  
51 - * 服务调用  
52 - */  
53 - public static final short CODE_SERVICE_INVOKE = 60;  
54 - /**  
55 - * 属性设置  
56 - */  
57 - public static final short CODE_PROPERTY_SET = 70;  
58 - /**  
59 - * 属性获取  
60 - */  
61 - public static final short CODE_PROPERTY_GET = 80;  
62 -  
63 - // ==================== 数据包字段 ====================  
64 -  
65 - // TODO @haohao:设备 addrLength、addr 是不是非必要呀?  
66 -  
67 - /**  
68 - * 设备地址长度  
69 - */  
70 - private Integer addrLength;  
71 -  
72 - /**  
73 - * 设备地址  
74 - */  
75 - private String addr;  
76 -  
77 - /**  
78 - * 功能码  
79 - */  
80 - private short code;  
81 -  
82 - /**  
83 - * 消息序号  
84 - */  
85 - private short mid;  
86 -  
87 - /**  
88 - * 包体数据  
89 - */  
90 - private String payload;  
91 -  
92 - // ==================== 辅助方法 ====================  
93 -  
94 - // TODO @haohao:用不到的方法,可以清理掉哈;  
95 -  
96 - /**  
97 - * 是否为注册消息  
98 - */  
99 - public boolean isRegisterMessage() {  
100 - return code == CODE_REGISTER;  
101 - }  
102 -  
103 - /**  
104 - * 是否为心跳消息  
105 - */  
106 - public boolean isHeartbeatMessage() {  
107 - return code == CODE_HEARTBEAT;  
108 - }  
109 -  
110 - /**  
111 - * 是否为数据上报消息  
112 - */  
113 - public boolean isDataUpMessage() {  
114 - return code == CODE_DATA_UP;  
115 - }  
116 -  
117 - /**  
118 - * 是否为事件上报消息  
119 - */  
120 - public boolean isEventUpMessage() {  
121 - return code == CODE_EVENT_UP;  
122 - }  
123 -  
124 - /**  
125 - * 是否为下行消息  
126 - */  
127 - public boolean isDownstreamMessage() {  
128 - return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE ||  
129 - code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET;  
130 - }  
131 -  
132 - // TODO @haohao:这个是不是去掉呀?多了一些维护成本;  
133 - /**  
134 - * 获取功能码描述  
135 - */  
136 - public String getCodeDescription() {  
137 - switch (code) {  
138 - case CODE_REGISTER:  
139 - return "设备注册";  
140 - case CODE_REGISTER_REPLY:  
141 - return "注册回复";  
142 - case CODE_HEARTBEAT:  
143 - return "心跳";  
144 - case CODE_DATA_UP:  
145 - return "数据上报";  
146 - case CODE_EVENT_UP:  
147 - return "事件上报";  
148 - case CODE_DATA_DOWN:  
149 - return "数据下发";  
150 - case CODE_SERVICE_INVOKE:  
151 - return "服务调用";  
152 - case CODE_PROPERTY_SET:  
153 - return "属性设置";  
154 - case CODE_PROPERTY_GET:  
155 - return "属性获取";  
156 - default:  
157 - return "未知功能码";  
158 - }  
159 - }  
160 -}  
161 \ 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/protocol/TcpDataReader.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;  
2 -  
3 -import io.vertx.core.Handler;  
4 -import io.vertx.core.buffer.Buffer;  
5 -import io.vertx.core.parsetools.RecordParser;  
6 -import lombok.extern.slf4j.Slf4j;  
7 -  
8 -import java.util.function.Consumer;  
9 -  
10 -/**  
11 - * TCP 数据读取器  
12 - * <p>  
13 - * 负责从 TCP 流中读取完整的数据包  
14 - * <p>  
15 - * 数据包格式:  
16 - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长)  
17 - *  
18 - * @author 芋道源码  
19 - */  
20 -@Slf4j  
21 -public class TcpDataReader {  
22 -  
23 - /**  
24 - * 创建数据包解析器  
25 - *  
26 - * @param receiveHandler 接收处理器  
27 - * @return RecordParser 解析器  
28 - */  
29 - public static RecordParser createParser(Consumer<Buffer> receiveHandler) {  
30 - // 首先读取 4 字节的长度信息  
31 - RecordParser parser = RecordParser.newFixed(4);  
32 -  
33 - // 设置处理器  
34 - parser.setOutput(new Handler<Buffer>() {  
35 - // 当前数据包的长度,-1 表示还没有读取到长度信息  
36 - private int dataLength = -1;  
37 -  
38 - @Override  
39 - public void handle(Buffer buffer) {  
40 - try {  
41 - // 如果还没有读取到长度信息  
42 - if (dataLength == -1) {  
43 - // 从包头中读取数据长度  
44 - dataLength = buffer.getInt(0);  
45 -  
46 - // 校验数据长度(最大 1 MB)  
47 - // TODO @haohao:1m 蛮多地方在写死,最好配置管理下。或者有个全局的枚举;  
48 - if (dataLength <= 0 || dataLength > 1024 * 1024) {  
49 - log.error("[handle][无效的数据包长度: {}]", dataLength);  
50 - reset();  
51 - return;  
52 - }  
53 -  
54 - // 切换到读取数据模式  
55 - parser.fixedSizeMode(dataLength);  
56 -  
57 - log.debug("[handle][读取到数据包长度: {}]", dataLength);  
58 - } else {  
59 - // 读取到完整的数据包  
60 - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length());  
61 -  
62 - // 处理数据包  
63 - try {  
64 - receiveHandler.accept(buffer);  
65 - } catch (Exception e) {  
66 - log.error("[handle][处理数据包失败]", e);  
67 - }  
68 -  
69 - // 重置状态,准备读取下一个数据包  
70 - reset();  
71 - }  
72 - } catch (Exception e) {  
73 - log.error("[handle][数据包处理异常]", e);  
74 - reset();  
75 - }  
76 - }  
77 -  
78 - /**  
79 - * 重置解析器状态  
80 - */  
81 - private void reset() {  
82 - dataLength = -1;  
83 - parser.fixedSizeMode(4);  
84 - }  
85 - });  
86 -  
87 - return parser;  
88 - }  
89 -  
90 - // TODO @haohao:用不到的方法,可以清理掉哈;  
91 -  
92 - /**  
93 - * 创建带异常处理的数据包解析器  
94 - *  
95 - * @param receiveHandler 接收处理器  
96 - * @param exceptionHandler 异常处理器  
97 - * @return RecordParser 解析器  
98 - */  
99 - public static RecordParser createParserWithExceptionHandler(  
100 - Consumer<Buffer> receiveHandler,  
101 - Consumer<Throwable> exceptionHandler) {  
102 -  
103 - RecordParser parser = RecordParser.newFixed(4);  
104 -  
105 - parser.setOutput(new Handler<Buffer>() {  
106 - private int dataLength = -1;  
107 -  
108 - @Override  
109 - public void handle(Buffer buffer) {  
110 - try {  
111 - if (dataLength == -1) {  
112 - dataLength = buffer.getInt(0);  
113 -  
114 - if (dataLength <= 0 || dataLength > 1024 * 1024) {  
115 - throw new IllegalArgumentException("无效的数据包长度: " + dataLength);  
116 - }  
117 -  
118 - parser.fixedSizeMode(dataLength);  
119 - log.debug("[handle][读取到数据包长度: {}]", dataLength);  
120 - } else {  
121 - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length());  
122 -  
123 - try {  
124 - receiveHandler.accept(buffer);  
125 - } catch (Exception e) {  
126 - exceptionHandler.accept(e);  
127 - }  
128 -  
129 - reset();  
130 - }  
131 - } catch (Exception e) {  
132 - exceptionHandler.accept(e);  
133 - reset();  
134 - }  
135 - }  
136 -  
137 - private void reset() {  
138 - dataLength = -1;  
139 - parser.fixedSizeMode(4);  
140 - }  
141 - });  
142 -  
143 - return parser;  
144 - }  
145 -  
146 - /**  
147 - * 创建简单的数据包解析器(用于测试)  
148 - *  
149 - * @param receiveHandler 接收处理器  
150 - * @return RecordParser 解析器  
151 - */  
152 - public static RecordParser createSimpleParser(Consumer<TcpDataPackage> receiveHandler) {  
153 - return createParser(buffer -> {  
154 - try {  
155 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);  
156 - receiveHandler.accept(dataPackage);  
157 - } catch (Exception e) {  
158 - log.error("[createSimpleParser][解码数据包失败]", e);  
159 - }  
160 - });  
161 - }  
162 -}  
163 \ 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
1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
2 2
3 -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient;  
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;  
7 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder;  
8 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder;  
9 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; 4 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpDeviceMessageCodec;
10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 5 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11 -import com.alibaba.fastjson.JSON;  
12 -import io.vertx.core.buffer.Buffer;  
13 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
14 7
15 /** 8 /**
@@ -21,20 +14,21 @@ import lombok.extern.slf4j.Slf4j; @@ -21,20 +14,21 @@ import lombok.extern.slf4j.Slf4j;
21 * 3. 属性获取 14 * 3. 属性获取
22 * 4. 配置下发 15 * 4. 配置下发
23 * 5. OTA 升级 16 * 5. OTA 升级
  17 + * <p>
  18 + * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录
24 * 19 *
25 * @author 芋道源码 20 * @author 芋道源码
26 */ 21 */
27 @Slf4j 22 @Slf4j
28 public class IotTcpDownstreamHandler { 23 public class IotTcpDownstreamHandler {
29 24
30 - private final TcpDeviceConnectionManager connectionManager;  
31 -  
32 private final IotDeviceMessageService messageService; 25 private final IotDeviceMessageService messageService;
33 26
34 - public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager,  
35 - IotDeviceMessageService messageService) {  
36 - this.connectionManager = connectionManager; 27 + private final IotTcpDeviceMessageCodec codec;
  28 +
  29 + public IotTcpDownstreamHandler(IotDeviceMessageService messageService) {
37 this.messageService = messageService; 30 this.messageService = messageService;
  31 + this.codec = new IotTcpDeviceMessageCodec();
38 } 32 }
39 33
40 /** 34 /**
@@ -47,315 +41,19 @@ public class IotTcpDownstreamHandler { @@ -47,315 +41,19 @@ public class IotTcpDownstreamHandler {
47 log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", 41 log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}",
48 message.getDeviceId(), message.getMethod(), message.getId()); 42 message.getDeviceId(), message.getMethod(), message.getId());
49 43
50 - // 1. 获取设备连接  
51 - TcpDeviceClient client = connectionManager.getClientByDeviceId(message.getDeviceId());  
52 - if (client == null || !client.isOnline()) {  
53 - log.error("[handle][设备({})不在线,无法发送下行消息]", message.getDeviceId());  
54 - return;  
55 - }  
56 -  
57 - // 2. 根据消息方法处理不同类型的下行消息  
58 - // TODO @芋艿、@haohao:看看有没什么办法,减少这样的编码。拓展新消息类型,成本高;  
59 - switch (message.getMethod()) {  
60 - case "thing.property.set":  
61 - handlePropertySet(client, message);  
62 - break;  
63 - case "thing.property.get":  
64 - handlePropertyGet(client, message);  
65 - break;  
66 - case "thing.service.invoke":  
67 - handleServiceInvoke(client, message);  
68 - break;  
69 - case "thing.config.push":  
70 - handleConfigPush(client, message);  
71 - break;  
72 - case "thing.ota.upgrade":  
73 - handleOtaUpgrade(client, message);  
74 - break;  
75 - default:  
76 - log.warn("[handle][未知的下行消息方法: {}]", message.getMethod());  
77 - break;  
78 - }  
79 - } catch (Exception e) {  
80 - // TODO @haohao:最好消息的内容,打印下;  
81 - log.error("[handle][处理下行消息失败]", e);  
82 - }  
83 - }  
84 -  
85 - /**  
86 - * 处理属性设置  
87 - *  
88 - * @param client 设备客户端  
89 - * @param message 设备消息  
90 - */  
91 - private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) {  
92 - try {  
93 - log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}",  
94 - client.getDeviceAddr(), message.getParams());  
95 -  
96 - // 使用编解码器发送消息,降级处理使用原始编码  
97 - sendMessageWithCodec(client, message, "handlePropertySet", () -> {  
98 - String payload = JSON.toJSONString(message.getParams());  
99 - short mid = generateMessageId();  
100 -  
101 - Buffer buffer = TcpDataEncoder.createPropertySetPackage(  
102 - client.getDeviceAddr(), mid, payload);  
103 - client.sendMessage(buffer); 44 + // 编码消息用于日志记录和验证
  45 + byte[] encodedMessage = codec.encode(message);
  46 + log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节",
  47 + message.getDeviceId(), encodedMessage.length);
104 48
105 - log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}",  
106 - client.getDeviceAddr(), mid);  
107 - });  
108 - } catch (Exception e) {  
109 - log.error("[handlePropertySet][属性设置失败]", e);  
110 - }  
111 - } 49 + // 记录下行消息处理
  50 + log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}",
  51 + message.getDeviceId(), message.getMethod(), message.getParams());
112 52
113 - /**  
114 - * 处理属性获取  
115 - *  
116 - * @param client 设备客户端  
117 - * @param message 设备消息  
118 - */  
119 - private void handlePropertyGet(TcpDeviceClient client, IotDeviceMessage message) {  
120 - try {  
121 - log.info("[handlePropertyGet][属性获取] 设备地址: {}, 属性列表: {}",  
122 - client.getDeviceAddr(), message.getParams());  
123 -  
124 - // 使用编解码器发送消息,降级处理使用原始编码  
125 - sendMessageWithCodec(client, message, "handlePropertyGet", () -> {  
126 - String payload = JSON.toJSONString(message.getParams());  
127 - short mid = generateMessageId();  
128 -  
129 - Buffer buffer = TcpDataEncoder.createPropertyGetPackage(  
130 - client.getDeviceAddr(), mid, payload);  
131 - client.sendMessage(buffer);  
132 -  
133 - log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}",  
134 - client.getDeviceAddr(), mid);  
135 - });  
136 } catch (Exception e) { 53 } catch (Exception e) {
137 - log.error("[handlePropertyGet][属性获取失败]", e); 54 + log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}",
  55 + message.getDeviceId(), message.getMethod(), message.getParams(), e);
138 } 56 }
139 } 57 }
140 58
141 - /**  
142 - * 处理服务调用  
143 - *  
144 - * @param client 设备客户端  
145 - * @param message 设备消息  
146 - */  
147 - private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) {  
148 - try {  
149 - log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}",  
150 - client.getDeviceAddr(), message.getParams());  
151 -  
152 - // 1. 构建服务调用数据包  
153 - String payload = JSON.toJSONString(message.getParams());  
154 - short mid = generateMessageId();  
155 -  
156 - Buffer buffer = TcpDataEncoder.createServiceInvokePackage(  
157 - client.getDeviceAddr(), mid, payload);  
158 -  
159 - // 2. 发送消息  
160 - client.sendMessage(buffer);  
161 -  
162 - log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}",  
163 - client.getDeviceAddr(), mid);  
164 - } catch (Exception e) {  
165 - log.error("[handleServiceInvoke][服务调用失败]", e);  
166 - }  
167 - }  
168 -  
169 - /**  
170 - * 处理配置推送  
171 - *  
172 - * @param client 设备客户端  
173 - * @param message 设备消息  
174 - */  
175 - private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) {  
176 - try {  
177 - log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}",  
178 - client.getDeviceAddr(), message.getParams());  
179 -  
180 - // 1. 构建配置推送数据包  
181 - String payload = JSON.toJSONString(message.getParams());  
182 - short mid = generateMessageId();  
183 -  
184 - Buffer buffer = TcpDataEncoder.createDataDownPackage(  
185 - client.getDeviceAddr(), mid, payload);  
186 -  
187 - // 2. 发送消息  
188 - client.sendMessage(buffer);  
189 -  
190 - log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}",  
191 - client.getDeviceAddr(), mid);  
192 - } catch (Exception e) {  
193 - log.error("[handleConfigPush][配置推送失败]", e);  
194 - }  
195 - }  
196 -  
197 - /**  
198 - * 处理 OTA 升级  
199 - *  
200 - * @param client 设备客户端  
201 - * @param message 设备消息  
202 - */  
203 - private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) {  
204 - try {  
205 - log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}",  
206 - client.getDeviceAddr(), message.getParams());  
207 -  
208 - // 1. 构建 OTA 升级数据包  
209 - String payload = JSON.toJSONString(message.getParams());  
210 - short mid = generateMessageId();  
211 -  
212 - Buffer buffer = TcpDataEncoder.createDataDownPackage(  
213 - client.getDeviceAddr(), mid, payload);  
214 -  
215 - // 2. 发送消息  
216 - client.sendMessage(buffer);  
217 -  
218 - log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}",  
219 - client.getDeviceAddr(), mid);  
220 -  
221 - } catch (Exception e) {  
222 - log.error("[handleOtaUpgrade][OTA升级失败]", e);  
223 - }  
224 - }  
225 -  
226 - /**  
227 - * 处理自定义下行消息  
228 - *  
229 - * @param client 设备客户端  
230 - * @param message 设备消息  
231 - * @param code 功能码  
232 - */  
233 - private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) {  
234 - try {  
235 - log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}",  
236 - client.getDeviceAddr(), code, message.getParams());  
237 -  
238 - // 1. 构建自定义数据包  
239 - String payload = JSON.toJSONString(message.getParams());  
240 - short mid = generateMessageId();  
241 -  
242 - TcpDataPackage dataPackage = TcpDataPackage.builder()  
243 - .addr(client.getDeviceAddr())  
244 - .code(code)  
245 - .mid(mid)  
246 - .payload(payload)  
247 - .build();  
248 -  
249 - Buffer buffer = TcpDataEncoder.encode(dataPackage);  
250 -  
251 - // 2. 发送消息  
252 - client.sendMessage(buffer);  
253 -  
254 - log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}",  
255 - client.getDeviceAddr(), code, mid);  
256 -  
257 - } catch (Exception e) {  
258 - log.error("[handleCustomMessage][自定义消息发送失败]", e);  
259 - }  
260 - }  
261 -  
262 - // TODO @haohao:用不到的,要不暂时不提供;  
263 - /**  
264 - * 批量发送下行消息  
265 - *  
266 - * @param deviceIds 设备ID列表  
267 - * @param message 设备消息  
268 - */  
269 - public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) {  
270 - try {  
271 - log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}",  
272 - deviceIds.length, message.getMethod());  
273 -  
274 - for (Long deviceId : deviceIds) {  
275 - // 创建副本消息(避免消息ID冲突)  
276 - IotDeviceMessage copyMessage = IotDeviceMessage.of(  
277 - message.getRequestId(),  
278 - message.getMethod(),  
279 - message.getParams(),  
280 - message.getData(),  
281 - message.getCode(),  
282 - message.getMsg());  
283 - copyMessage.setDeviceId(deviceId);  
284 -  
285 - // 处理单个设备消息  
286 - handle(copyMessage);  
287 - }  
288 - } catch (Exception e) {  
289 - log.error("[broadcastMessage][批量发送消息失败]", e);  
290 - }  
291 - }  
292 -  
293 - /**  
294 - * 检查设备是否支持指定方法  
295 - *  
296 - * @param client 设备客户端  
297 - * @param method 消息方法  
298 - * @return 是否支持  
299 - */  
300 - private boolean isMethodSupported(TcpDeviceClient client, String method) {  
301 - // TODO: 可以根据设备类型或产品信息判断是否支持特定方法  
302 - return IotDeviceMessageMethodEnum.of(method) != null;  
303 - }  
304 -  
305 - /**  
306 - * 生成消息序号  
307 - *  
308 - * @return 消息序号  
309 - */  
310 - private short generateMessageId() {  
311 - return (short) (System.currentTimeMillis() % Short.MAX_VALUE);  
312 - }  
313 -  
314 - /**  
315 - * 使用编解码器发送消息  
316 - *  
317 - * @param client 设备客户端  
318 - * @param message 设备消息  
319 - * @param methodName 方法名称  
320 - * @param fallbackAction 降级处理逻辑  
321 - */  
322 - private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message,  
323 - String methodName, Runnable fallbackAction) {  
324 - try {  
325 - // 1. 使用编解码器编码消息  
326 - byte[] messageBytes = messageService.encodeDeviceMessage(  
327 - message, client.getProductKey(), client.getDeviceName());  
328 -  
329 - // 2. 解析编码后的数据包并设置设备地址和消息序号  
330 - Buffer buffer = Buffer.buffer(messageBytes);  
331 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);  
332 - dataPackage.setAddr(client.getDeviceAddr());  
333 - dataPackage.setMid(generateMessageId());  
334 -  
335 - // 3. 重新编码并发送  
336 - Buffer finalBuffer = TcpDataEncoder.encode(dataPackage);  
337 - client.sendMessage(finalBuffer);  
338 -  
339 - log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}",  
340 - methodName, client.getDeviceAddr(), dataPackage.getMid());  
341 - } catch (Exception e) {  
342 - log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}",  
343 - methodName, e.getMessage());  
344 -  
345 - // 执行降级处理  
346 - if (fallbackAction != null) {  
347 - fallbackAction.run();  
348 - }  
349 - }  
350 - }  
351 -  
352 - // TODO @haohao:看看这个要不要删除掉  
353 - /**  
354 - * 获取连接统计信息  
355 - *  
356 - * @return 连接统计信息  
357 - */  
358 - public String getHandlerStatistics() {  
359 - return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus());  
360 - }  
361 } 59 }
362 \ No newline at end of file 60 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
2 2
3 import cn.hutool.core.util.IdUtil; 3 import cn.hutool.core.util.IdUtil;
4 -import cn.hutool.json.JSONObject;  
5 -import cn.hutool.json.JSONUtil;  
6 -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;  
7 -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;  
8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;  
10 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient;  
11 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;  
12 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder;  
13 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder;  
14 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage;  
15 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataReader;  
16 -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 5 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
17 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
18 import io.vertx.core.Handler; 8 import io.vertx.core.Handler;
19 import io.vertx.core.buffer.Buffer; 9 import io.vertx.core.buffer.Buffer;
20 import io.vertx.core.net.NetSocket; 10 import io.vertx.core.net.NetSocket;
21 import io.vertx.core.parsetools.RecordParser; 11 import io.vertx.core.parsetools.RecordParser;
22 -import lombok.RequiredArgsConstructor;  
23 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
24 13
25 /** 14 /**
26 * IoT 网关 TCP 上行消息处理器 15 * IoT 网关 TCP 上行消息处理器
27 - * <p>  
28 - * 核心负责:  
29 - * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信  
30 - * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态  
31 - * 3. 【数据上报】接收设备数据上报和事件上报  
32 - * 4. 【连接管理】管理连接的建立、维护和清理  
33 * 16 *
34 * @author 芋道源码 17 * @author 芋道源码
35 */ 18 */
36 -@RequiredArgsConstructor  
37 @Slf4j 19 @Slf4j
38 public class IotTcpUpstreamHandler implements Handler<NetSocket> { 20 public class IotTcpUpstreamHandler implements Handler<NetSocket> {
39 21
40 - private final IotGatewayProperties.TcpProperties tcpConfig;  
41 -  
42 - // TODO @haohao:可以把 TcpDeviceConnectionManager 能力放大一点:1)handle 里的 client 初始化,可以拿到 TcpDeviceConnectionManager 里;2)handleDeviceRegister 也是;  
43 - private final TcpDeviceConnectionManager connectionManager;  
44 -  
45 - private final IotDeviceService deviceService;  
46 -  
47 - private final IotDeviceMessageService messageService;  
48 -  
49 - private final IotDeviceCommonApi deviceApi; 22 + private final IotDeviceMessageService deviceMessageService;
50 23
51 private final String serverId; 24 private final String serverId;
52 25
53 - @Override  
54 - public void handle(NetSocket socket) {  
55 - log.info("[handle][收到设备连接: {}]", socket.remoteAddress());  
56 -  
57 - // 创建客户端 ID 和设备客户端  
58 - // TODO @haohao:clientid 给 TcpDeviceClient 生成会简洁一点;减少 upsteramhanlder 的非核心逻辑;  
59 - String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress();  
60 - TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs());  
61 -  
62 - try {  
63 - // 设置连接异常和关闭处理  
64 - socket.exceptionHandler(ex -> {  
65 - // TODO @haohao:这里的日志,可能把 clientid 都打上?因为 address 会重复么?  
66 - log.error("[handle][连接({})异常]", socket.remoteAddress(), ex);  
67 - handleConnectionClose(client);  
68 - });  
69 - socket.closeHandler(v -> {  
70 - log.info("[handle][连接({})关闭]", socket.remoteAddress());  
71 - handleConnectionClose(client);  
72 - });  
73 - client.setSocket(socket);  
74 -  
75 - // 设置解析器  
76 - RecordParser parser = TcpDataReader.createParser(buffer -> {  
77 - try {  
78 - handleDataPackage(client, buffer);  
79 - } catch (Exception e) {  
80 - log.error("[handle][处理数据包异常]", e);  
81 - }  
82 - });  
83 - client.setParser(parser); 26 + private final IotTcpCodecManager codecManager;
84 27
85 - // TODO @haohao:socket.remoteAddress()) 打印进去  
86 - log.info("[handle][设备连接处理器初始化完成: {}]", clientId);  
87 - } catch (Exception e) {  
88 - // TODO @haohao:socket.remoteAddress()) 打印进去  
89 - log.error("[handle][初始化连接处理器失败]", e);  
90 - client.shutdown();  
91 - }  
92 - }  
93 -  
94 - /**  
95 - * 处理数据包  
96 - *  
97 - * @param client 设备客户端  
98 - * @param buffer 数据缓冲区  
99 - */  
100 - private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) {  
101 - try {  
102 - // 解码数据包  
103 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);  
104 - log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}",  
105 - dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid());  
106 -  
107 - // 根据功能码处理不同类型的消息  
108 - switch (dataPackage.getCode()) {  
109 - // TODO @haohao:【重要】code 要不要改成 opCode。这样和 data 里的 code 好区分;  
110 - case TcpDataPackage.CODE_REGISTER:  
111 - handleDeviceRegister(client, dataPackage);  
112 - break;  
113 - case TcpDataPackage.CODE_HEARTBEAT:  
114 - handleHeartbeat(client, dataPackage);  
115 - break;  
116 - case TcpDataPackage.CODE_DATA_UP:  
117 - handleDataUp(client, dataPackage);  
118 - break;  
119 - case TcpDataPackage.CODE_EVENT_UP:  
120 - handleEventUp(client, dataPackage);  
121 - break;  
122 - default:  
123 - log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode());  
124 - break;  
125 - }  
126 - } catch (Exception e) {  
127 - // TODO @haohao:最好有 client 标识;  
128 - log.error("[handleDataPackage][处理数据包失败]", e);  
129 - }  
130 - }  
131 -  
132 - /**  
133 - * 处理设备注册  
134 - *  
135 - * @param client 设备客户端  
136 - * @param dataPackage 数据包  
137 - */  
138 - private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) {  
139 - try {  
140 - String deviceAddr = dataPackage.getAddr();  
141 - String productKey = dataPackage.getPayload();  
142 - log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey);  
143 -  
144 - // 获取设备信息  
145 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr);  
146 - if (device == null) {  
147 - log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr);  
148 - sendRegisterReply(client, dataPackage, false);  
149 - return;  
150 - }  
151 -  
152 - // 更新客户端信息  
153 - // TODO @haohao:一个 set 方法,统一处理掉会好点哈;  
154 - client.setProductKey(productKey);  
155 - client.setDeviceName(deviceAddr);  
156 - client.setDeviceId(device.getId());  
157 - client.setAuthenticated(true);  
158 -  
159 - // 添加到连接管理器  
160 - connectionManager.addClient(deviceAddr, client);  
161 - connectionManager.setDeviceIdMapping(deviceAddr, device.getId());  
162 -  
163 - // 发送设备上线消息  
164 - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();  
165 - messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId);  
166 -  
167 - // 发送注册成功回复  
168 - sendRegisterReply(client, dataPackage, true);  
169 -  
170 - log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId());  
171 - } catch (Exception e) {  
172 - log.error("[handleDeviceRegister][设备注册失败]", e);  
173 - sendRegisterReply(client, dataPackage, false);  
174 - }  
175 - }  
176 -  
177 - /**  
178 - * 处理心跳  
179 - *  
180 - * @param client 设备客户端  
181 - * @param dataPackage 数据包  
182 - */  
183 - private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) {  
184 - try {  
185 - String deviceAddr = dataPackage.getAddr();  
186 - log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr);  
187 -  
188 - // 更新心跳时间  
189 - client.keepAlive();  
190 -  
191 - // 发送心跳回复(可选)  
192 - // sendHeartbeatReply(client, dataPackage);  
193 -  
194 - } catch (Exception e) {  
195 - log.error("[handleHeartbeat][处理心跳失败]", e);  
196 - } 28 + public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService,
  29 + IotTcpCodecManager codecManager) {
  30 + this.deviceMessageService = deviceMessageService;
  31 + this.serverId = protocol.getServerId();
  32 + this.codecManager = codecManager;
197 } 33 }
198 34
199 - /**  
200 - * 处理数据上报  
201 - *  
202 - * @param client 设备客户端  
203 - * @param dataPackage 数据包  
204 - */  
205 - private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) {  
206 - try {  
207 - String deviceAddr = dataPackage.getAddr();  
208 - String payload = dataPackage.getPayload();  
209 -  
210 - log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload);  
211 -  
212 - // 检查设备是否已认证  
213 - if (!client.isAuthenticated()) {  
214 - log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr);  
215 - return;  
216 - } 35 + @Override
  36 + public void handle(NetSocket socket) {
  37 + // 生成客户端ID用于日志标识
  38 + String clientId = IdUtil.simpleUUID();
  39 + log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress());
217 40
218 - // 使用 IotDeviceMessageService 解码消息 41 + // 设置解析器
  42 + RecordParser parser = RecordParser.newFixed(1024, buffer -> {
219 try { 43 try {
220 - // 1. 将 TCP 数据包重新编码为字节数组  
221 - Buffer buffer = TcpDataEncoder.encode(dataPackage);  
222 - byte[] messageBytes = buffer.getBytes();  
223 -  
224 - // 2. 使用 messageService 解码消息  
225 - IotDeviceMessage message = messageService.decodeDeviceMessage(  
226 - messageBytes, client.getProductKey(), client.getDeviceName());  
227 -  
228 - // 3. 发送解码后的消息  
229 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); 44 + handleDataPackage(clientId, buffer);
230 } catch (Exception e) { 45 } catch (Exception e) {
231 - log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage());  
232 -  
233 - // 降级处理:使用原始方式解析数据  
234 - JSONObject dataJson = JSONUtil.parseObj(payload);  
235 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson);  
236 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); 46 + log.error("[handle][处理数据包异常] clientId: {}", clientId, e);
237 } 47 }
  48 + });
238 49
239 - // 发送数据上报回复  
240 - sendDataUpReply(client, dataPackage);  
241 - } catch (Exception e) {  
242 - log.error("[handleDataUp][处理数据上报失败]", e);  
243 - }  
244 - } 50 + // 设置异常处理
  51 + socket.exceptionHandler(ex -> {
  52 + log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex);
  53 + });
245 54
246 - /**  
247 - * 处理事件上报  
248 - *  
249 - * @param client 设备客户端  
250 - * @param dataPackage 数据包  
251 - */  
252 - private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) {  
253 - try {  
254 - String deviceAddr = dataPackage.getAddr();  
255 - String payload = dataPackage.getPayload();  
256 -  
257 - log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload);  
258 -  
259 - // 检查设备是否已认证  
260 - if (!client.isAuthenticated()) {  
261 - log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr);  
262 - return;  
263 - }  
264 -  
265 - // 使用 IotDeviceMessageService 解码消息  
266 - try {  
267 - // 1. 将 TCP 数据包重新编码为字节数组  
268 - Buffer buffer = TcpDataEncoder.encode(dataPackage);  
269 - byte[] messageBytes = buffer.getBytes(); 55 + socket.closeHandler(v -> {
  56 + log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress());
  57 + });
270 58
271 - // 2. 使用 messageService 解码消息  
272 - IotDeviceMessage message = messageService.decodeDeviceMessage(  
273 - messageBytes, client.getProductKey(), client.getDeviceName());  
274 -  
275 - // 3. 发送解码后的消息  
276 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);  
277 - } catch (Exception e) {  
278 - log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage());  
279 -  
280 - // 降级处理:使用原始方式解析数据  
281 - // TODO @芋艿:降级处理逻辑;  
282 - JSONObject eventJson = JSONUtil.parseObj(payload);  
283 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson);  
284 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);  
285 - }  
286 -  
287 - // 发送事件上报回复  
288 - sendEventUpReply(client, dataPackage);  
289 - } catch (Exception e) {  
290 - log.error("[handleEventUp][处理事件上报失败]", e);  
291 - } 59 + // 设置数据处理器
  60 + socket.handler(parser);
292 } 61 }
293 62
294 /** 63 /**
295 - * 发送注册回复  
296 - *  
297 - * @param client 设备客户端  
298 - * @param dataPackage 原始数据包  
299 - * @param success 是否成功 64 + * 处理数据包
300 */ 65 */
301 - private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) { 66 + private void handleDataPackage(String clientId, Buffer buffer) {
302 try { 67 try {
303 - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply(  
304 - dataPackage.getAddr(), dataPackage.getMid(), success);  
305 - client.sendMessage(replyBuffer); 68 + // 使用编解码器管理器自动检测协议并解码消息
  69 + IotDeviceMessage message = codecManager.decode(buffer.getBytes());
  70 + log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}",
  71 + clientId, message.getMethod(), message.getDeviceId());
306 72
307 - log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}",  
308 - dataPackage.getAddr(), success ? "成功" : "失败"); 73 + // 处理上行消息
  74 + handleUpstreamMessage(clientId, message);
309 } catch (Exception e) { 75 } catch (Exception e) {
310 - log.error("[sendRegisterReply][发送注册回复失败]", e); 76 + log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e);
311 } 77 }
312 } 78 }
313 79
314 /** 80 /**
315 - * 发送数据上报回复  
316 - *  
317 - * @param client 设备客户端  
318 - * @param dataPackage 原始数据包 81 + * 处理上行消息
319 */ 82 */
320 - private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { 83 + private void handleUpstreamMessage(String clientId, IotDeviceMessage message) {
321 try { 84 try {
322 - TcpDataPackage replyPackage = TcpDataPackage.builder()  
323 - .addr(dataPackage.getAddr())  
324 - .code(TcpDataPackage.CODE_DATA_UP)  
325 - .mid(dataPackage.getMid())  
326 - .payload("0") // 0 表示成功 TODO @haohao:最好枚举到 TcpDataPackage 里?  
327 - .build();  
328 -  
329 - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage);  
330 - client.sendMessage(replyBuffer);  
331 - } catch (Exception e) {  
332 - // TODO @haohao:可以有个 client id  
333 - log.error("[sendDataUpReply][发送数据上报回复失败]", e);  
334 - }  
335 - } 85 + log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}",
  86 + clientId, message.getMethod(), message.getDeviceId());
336 87
337 - /**  
338 - * 发送事件上报回复  
339 - *  
340 - * @param client 设备客户端  
341 - * @param dataPackage 原始数据包  
342 - */  
343 - private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) {  
344 - try {  
345 - TcpDataPackage replyPackage = TcpDataPackage.builder()  
346 - .addr(dataPackage.getAddr())  
347 - .code(TcpDataPackage.CODE_EVENT_UP)  
348 - .mid(dataPackage.getMid())  
349 - .payload("0") // 0 表示成功  
350 - .build(); 88 + // 解析设备信息(简化处理)
  89 + String deviceId = String.valueOf(message.getDeviceId());
  90 + String productKey = extractProductKey(deviceId);
  91 + String deviceName = deviceId;
351 92
352 - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage);  
353 - client.sendMessage(replyBuffer); 93 + // 发送消息到队列
  94 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
354 } catch (Exception e) { 95 } catch (Exception e) {
355 - log.error("[sendEventUpReply][发送事件上报回复失败]", e); 96 + log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e);
356 } 97 }
357 } 98 }
358 99
359 /** 100 /**
360 - * 处理连接关闭  
361 - *  
362 - * @param client 设备客户端 101 + * 从设备ID中提取产品密钥(简化实现)
363 */ 102 */
364 - private void handleConnectionClose(TcpDeviceClient client) {  
365 - try {  
366 - String deviceAddr = client.getDeviceAddr();  
367 -  
368 - // 发送设备离线消息  
369 - if (client.isAuthenticated()) {  
370 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();  
371 - messageService.sendDeviceMessage(offlineMessage,  
372 - client.getProductKey(), client.getDeviceName(), serverId);  
373 - }  
374 -  
375 - // 从连接管理器移除  
376 - if (deviceAddr != null) {  
377 - connectionManager.removeClient(deviceAddr);  
378 - }  
379 -  
380 - log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr);  
381 - } catch (Exception e) {  
382 - log.error("[handleConnectionClose][处理连接关闭失败]", e); 103 + private String extractProductKey(String deviceId) {
  104 + // 简化实现:假设设备ID格式为 "productKey_deviceName"
  105 + if (deviceId != null && deviceId.contains("_")) {
  106 + return deviceId.split("_")[0];
383 } 107 }
  108 + return "default_product";
384 } 109 }
385 } 110 }
386 \ No newline at end of file 111 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +
  6 +import java.util.HashMap;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * TCP二进制格式数据包示例
  11 + *
  12 + * 演示如何使用二进制协议创建和解析TCP上报数据包和心跳包
  13 + *
  14 + * 二进制协议格式:
  15 + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长)
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Slf4j
  20 +public class TcpBinaryDataPacketExamples {
  21 +
  22 + public static void main(String[] args) {
  23 + IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec();
  24 +
  25 + // 1. 数据上报包示例
  26 + demonstrateDataReport(codec);
  27 +
  28 + // 2. 心跳包示例
  29 + demonstrateHeartbeat(codec);
  30 +
  31 + // 3. 复杂数据上报示例
  32 + demonstrateComplexDataReport(codec);
  33 + }
  34 +
  35 + /**
  36 + * 演示二进制格式数据上报包
  37 + */
  38 + private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) {
  39 + log.info("=== 二进制格式数据上报包示例 ===");
  40 +
  41 + // 创建传感器数据
  42 + Map<String, Object> sensorData = new HashMap<>();
  43 + sensorData.put("temperature", 25.5);
  44 + sensorData.put("humidity", 60.2);
  45 + sensorData.put("pressure", 1013.25);
  46 + sensorData.put("battery", 85);
  47 +
  48 + // 创建设备消息
  49 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData);
  50 + message.setDeviceId(123456L);
  51 +
  52 + // 编码
  53 + byte[] packet = codec.encode(message);
  54 + log.info("编码后数据包长度: {} 字节", packet.length);
  55 + log.info("编码后数据包(HEX): {}", bytesToHex(packet));
  56 +
  57 + // 解码验证
  58 + IotDeviceMessage decoded = codec.decode(packet);
  59 + log.info("解码后消息ID: {}", decoded.getId());
  60 + log.info("解码后请求ID: {}", decoded.getRequestId());
  61 + log.info("解码后方法: {}", decoded.getMethod());
  62 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  63 + log.info("解码后服务ID: {}", decoded.getServerId());
  64 + log.info("解码后上报时间: {}", decoded.getReportTime());
  65 + log.info("解码后参数: {}", decoded.getParams());
  66 +
  67 + System.out.println();
  68 + }
  69 +
  70 + /**
  71 + * 演示二进制格式心跳包
  72 + */
  73 + private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) {
  74 + log.info("=== 二进制格式心跳包示例 ===");
  75 +
  76 + // 创建心跳消息
  77 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null);
  78 + heartbeat.setDeviceId(123456L);
  79 +
  80 + // 编码
  81 + byte[] packet = codec.encode(heartbeat);
  82 + log.info("心跳包长度: {} 字节", packet.length);
  83 + log.info("心跳包(HEX): {}", bytesToHex(packet));
  84 +
  85 + // 解码验证
  86 + IotDeviceMessage decoded = codec.decode(packet);
  87 + log.info("解码后消息ID: {}", decoded.getId());
  88 + log.info("解码后请求ID: {}", decoded.getRequestId());
  89 + log.info("解码后方法: {}", decoded.getMethod());
  90 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  91 + log.info("解码后服务ID: {}", decoded.getServerId());
  92 + log.info("解码后参数: {}", decoded.getParams());
  93 +
  94 + System.out.println();
  95 + }
  96 +
  97 + /**
  98 + * 演示二进制格式复杂数据上报
  99 + */
  100 + private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) {
  101 + log.info("=== 二进制格式复杂数据上报示例 ===");
  102 +
  103 + // 创建复杂设备数据
  104 + Map<String, Object> deviceData = new HashMap<>();
  105 +
  106 + // 环境数据
  107 + Map<String, Object> environment = new HashMap<>();
  108 + environment.put("temperature", 23.8);
  109 + environment.put("humidity", 55.0);
  110 + environment.put("co2", 420);
  111 + deviceData.put("environment", environment);
  112 +
  113 + // GPS数据
  114 + Map<String, Object> location = new HashMap<>();
  115 + location.put("latitude", 39.9042);
  116 + location.put("longitude", 116.4074);
  117 + location.put("altitude", 43.5);
  118 + deviceData.put("location", location);
  119 +
  120 + // 设备状态
  121 + Map<String, Object> status = new HashMap<>();
  122 + status.put("battery", 78);
  123 + status.put("signal", -65);
  124 + status.put("online", true);
  125 + deviceData.put("status", status);
  126 +
  127 + // 创建设备消息
  128 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData);
  129 + message.setDeviceId(789012L);
  130 +
  131 + // 编码
  132 + byte[] packet = codec.encode(message);
  133 + log.info("复杂数据包长度: {} 字节", packet.length);
  134 + log.info("复杂数据包(HEX): {}", bytesToHex(packet));
  135 +
  136 + // 解码验证
  137 + IotDeviceMessage decoded = codec.decode(packet);
  138 + log.info("解码后消息ID: {}", decoded.getId());
  139 + log.info("解码后请求ID: {}", decoded.getRequestId());
  140 + log.info("解码后方法: {}", decoded.getMethod());
  141 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  142 + log.info("解码后服务ID: {}", decoded.getServerId());
  143 + log.info("解码后参数: {}", decoded.getParams());
  144 +
  145 + System.out.println();
  146 + }
  147 +
  148 + /**
  149 + * 字节数组转十六进制字符串
  150 + */
  151 + private static String bytesToHex(byte[] bytes) {
  152 + StringBuilder result = new StringBuilder();
  153 + for (byte b : bytes) {
  154 + result.append(String.format("%02X ", b));
  155 + }
  156 + return result.toString().trim();
  157 + }
  158 +
  159 + /**
  160 + * 演示数据包结构分析
  161 + */
  162 + public static void analyzePacketStructure(byte[] packet) {
  163 + if (packet.length < 8) {
  164 + log.error("数据包长度不足");
  165 + return;
  166 + }
  167 +
  168 + int index = 0;
  169 +
  170 + // 解析包头(4字节) - 后续数据长度
  171 + int totalLength = ((packet[index] & 0xFF) << 24) |
  172 + ((packet[index + 1] & 0xFF) << 16) |
  173 + ((packet[index + 2] & 0xFF) << 8) |
  174 + (packet[index + 3] & 0xFF);
  175 + index += 4;
  176 + log.info("包头 - 后续数据长度: {} 字节", totalLength);
  177 +
  178 + // 解析设备地址长度(2字节)
  179 + int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF);
  180 + index += 2;
  181 + log.info("设备地址长度: {} 字节", addrLength);
  182 +
  183 + // 解析设备地址
  184 + String deviceAddr = new String(packet, index, addrLength);
  185 + index += addrLength;
  186 + log.info("设备地址: {}", deviceAddr);
  187 +
  188 + // 解析功能码(2字节)
  189 + int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF);
  190 + index += 2;
  191 + log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode));
  192 +
  193 + // 解析消息序号(2字节)
  194 + int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF);
  195 + index += 2;
  196 + log.info("消息序号: {}", messageId);
  197 +
  198 + // 解析包体数据
  199 + if (index < packet.length) {
  200 + String payload = new String(packet, index, packet.length - index);
  201 + log.info("包体数据: {}", payload);
  202 + }
  203 + }
  204 +
  205 + /**
  206 + * 获取功能码名称
  207 + */
  208 + private static String getFunctionCodeName(int code) {
  209 + switch (code) {
  210 + case 10: return "设备注册";
  211 + case 11: return "注册回复";
  212 + case 20: return "心跳请求";
  213 + case 21: return "心跳回复";
  214 + case 30: return "消息上行";
  215 + case 40: return "消息下行";
  216 + default: return "未知功能码";
  217 + }
  218 + }
  219 +}
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +
  6 +import java.nio.charset.StandardCharsets;
  7 +import java.util.HashMap;
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * TCP JSON格式数据包示例
  12 + *
  13 + * 演示如何使用新的JSON格式进行TCP消息编解码
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +public class TcpJsonDataPacketExamples {
  19 +
  20 + public static void main(String[] args) {
  21 + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec();
  22 +
  23 + // 1. 数据上报示例
  24 + demonstrateDataReport(codec);
  25 +
  26 + // 2. 心跳示例
  27 + demonstrateHeartbeat(codec);
  28 +
  29 + // 3. 事件上报示例
  30 + demonstrateEventReport(codec);
  31 +
  32 + // 4. 复杂数据上报示例
  33 + demonstrateComplexDataReport(codec);
  34 +
  35 + // 5. 便捷方法示例
  36 + demonstrateConvenienceMethods();
  37 +
  38 + // 6. EMQX兼容性示例
  39 + demonstrateEmqxCompatibility();
  40 + }
  41 +
  42 + /**
  43 + * 演示数据上报
  44 + */
  45 + private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) {
  46 + log.info("=== JSON格式数据上报示例 ===");
  47 +
  48 + // 创建传感器数据
  49 + Map<String, Object> sensorData = new HashMap<>();
  50 + sensorData.put("temperature", 25.5);
  51 + sensorData.put("humidity", 60.2);
  52 + sensorData.put("pressure", 1013.25);
  53 + sensorData.put("battery", 85);
  54 +
  55 + // 创建设备消息
  56 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData);
  57 + message.setDeviceId(123456L);
  58 +
  59 + // 编码
  60 + byte[] packet = codec.encode(message);
  61 + String jsonString = new String(packet, StandardCharsets.UTF_8);
  62 + log.info("编码后JSON: {}", jsonString);
  63 + log.info("数据包长度: {} 字节", packet.length);
  64 +
  65 + // 解码验证
  66 + IotDeviceMessage decoded = codec.decode(packet);
  67 + log.info("解码后消息ID: {}", decoded.getId());
  68 + log.info("解码后方法: {}", decoded.getMethod());
  69 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  70 + log.info("解码后服务ID: {}", decoded.getServerId());
  71 + log.info("解码后参数: {}", decoded.getParams());
  72 +
  73 + System.out.println();
  74 + }
  75 +
  76 + /**
  77 + * 演示心跳
  78 + */
  79 + private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) {
  80 + log.info("=== JSON格式心跳示例 ===");
  81 +
  82 + // 创建心跳消息
  83 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null);
  84 + heartbeat.setDeviceId(123456L);
  85 +
  86 + // 编码
  87 + byte[] packet = codec.encode(heartbeat);
  88 + String jsonString = new String(packet, StandardCharsets.UTF_8);
  89 + log.info("编码后JSON: {}", jsonString);
  90 + log.info("心跳包长度: {} 字节", packet.length);
  91 +
  92 + // 解码验证
  93 + IotDeviceMessage decoded = codec.decode(packet);
  94 + log.info("解码后消息ID: {}", decoded.getId());
  95 + log.info("解码后方法: {}", decoded.getMethod());
  96 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  97 + log.info("解码后服务ID: {}", decoded.getServerId());
  98 +
  99 + System.out.println();
  100 + }
  101 +
  102 + /**
  103 + * 演示事件上报
  104 + */
  105 + private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) {
  106 + log.info("=== JSON格式事件上报示例 ===");
  107 +
  108 + // 创建事件数据
  109 + Map<String, Object> eventData = new HashMap<>();
  110 + eventData.put("eventType", "alarm");
  111 + eventData.put("level", "warning");
  112 + eventData.put("description", "温度过高");
  113 + eventData.put("value", 45.8);
  114 +
  115 + // 创建事件消息
  116 + IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData);
  117 + event.setDeviceId(123456L);
  118 +
  119 + // 编码
  120 + byte[] packet = codec.encode(event);
  121 + String jsonString = new String(packet, StandardCharsets.UTF_8);
  122 + log.info("编码后JSON: {}", jsonString);
  123 + log.info("事件包长度: {} 字节", packet.length);
  124 +
  125 + // 解码验证
  126 + IotDeviceMessage decoded = codec.decode(packet);
  127 + log.info("解码后消息ID: {}", decoded.getId());
  128 + log.info("解码后方法: {}", decoded.getMethod());
  129 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  130 + log.info("解码后参数: {}", decoded.getParams());
  131 +
  132 + System.out.println();
  133 + }
  134 +
  135 + /**
  136 + * 演示复杂数据上报
  137 + */
  138 + private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) {
  139 + log.info("=== JSON格式复杂数据上报示例 ===");
  140 +
  141 + // 创建复杂设备数据(类似EMQX格式)
  142 + Map<String, Object> deviceData = new HashMap<>();
  143 +
  144 + // 环境数据
  145 + Map<String, Object> environment = new HashMap<>();
  146 + environment.put("temperature", 23.8);
  147 + environment.put("humidity", 55.0);
  148 + environment.put("co2", 420);
  149 + environment.put("pm25", 35);
  150 + deviceData.put("environment", environment);
  151 +
  152 + // GPS数据
  153 + Map<String, Object> location = new HashMap<>();
  154 + location.put("latitude", 39.9042);
  155 + location.put("longitude", 116.4074);
  156 + location.put("altitude", 43.5);
  157 + location.put("speed", 0.0);
  158 + deviceData.put("location", location);
  159 +
  160 + // 设备状态
  161 + Map<String, Object> status = new HashMap<>();
  162 + status.put("battery", 78);
  163 + status.put("signal", -65);
  164 + status.put("online", true);
  165 + status.put("version", "1.2.3");
  166 + deviceData.put("status", status);
  167 +
  168 + // 创建设备消息
  169 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData);
  170 + message.setDeviceId(789012L);
  171 +
  172 + // 编码
  173 + byte[] packet = codec.encode(message);
  174 + String jsonString = new String(packet, StandardCharsets.UTF_8);
  175 + log.info("编码后JSON: {}", jsonString);
  176 + log.info("复杂数据包长度: {} 字节", packet.length);
  177 +
  178 + // 解码验证
  179 + IotDeviceMessage decoded = codec.decode(packet);
  180 + log.info("解码后消息ID: {}", decoded.getId());
  181 + log.info("解码后方法: {}", decoded.getMethod());
  182 + log.info("解码后设备ID: {}", decoded.getDeviceId());
  183 + log.info("解码后参数: {}", decoded.getParams());
  184 +
  185 + System.out.println();
  186 + }
  187 +
  188 + /**
  189 + * 演示便捷方法
  190 + */
  191 + private static void demonstrateConvenienceMethods() {
  192 + log.info("=== 便捷方法示例 ===");
  193 +
  194 + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec();
  195 +
  196 + // 使用便捷方法编码数据上报
  197 + Map<String, Object> sensorData = Map.of(
  198 + "temperature", 26.5,
  199 + "humidity", 58.3
  200 + );
  201 + byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001");
  202 + log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8));
  203 +
  204 + // 使用便捷方法编码心跳
  205 + byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001");
  206 + log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8));
  207 +
  208 + // 使用便捷方法编码事件
  209 + Map<String, Object> eventData = Map.of(
  210 + "eventType", "maintenance",
  211 + "description", "定期维护提醒"
  212 + );
  213 + byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001");
  214 + log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8));
  215 +
  216 + System.out.println();
  217 + }
  218 +
  219 + /**
  220 + * 演示与EMQX格式的兼容性
  221 + */
  222 + private static void demonstrateEmqxCompatibility() {
  223 + log.info("=== EMQX格式兼容性示例 ===");
  224 +
  225 + // 模拟EMQX风格的消息格式
  226 + String emqxStyleJson = """
  227 + {
  228 + "id": "msg_001",
  229 + "method": "thing.property.post",
  230 + "deviceId": 123456,
  231 + "params": {
  232 + "temperature": 25.5,
  233 + "humidity": 60.2
  234 + },
  235 + "timestamp": 1642781234567
  236 + }
  237 + """;
  238 +
  239 + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec();
  240 +
  241 + // 解码EMQX风格的消息
  242 + byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8);
  243 + IotDeviceMessage decoded = codec.decode(emqxBytes);
  244 +
  245 + log.info("EMQX风格消息解码成功:");
  246 + log.info("消息ID: {}", decoded.getId());
  247 + log.info("方法: {}", decoded.getMethod());
  248 + log.info("设备ID: {}", decoded.getDeviceId());
  249 + log.info("参数: {}", decoded.getParams());
  250 +
  251 + System.out.println();
  252 + }
  253 +}
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md 0 → 100644
  1 +# TCP二进制协议数据包格式说明和示例
  2 +
  3 +## 1. 二进制协议概述
  4 +
  5 +TCP二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。
  6 +
  7 +## 2. 数据包格式
  8 +
  9 +### 2.1 整体结构
  10 +```
  11 ++----------+----------+----------+----------+----------+----------+
  12 +| 包头 | 地址长度 | 设备地址 | 功能码 | 消息序号 | 包体数据 |
  13 +| 4字节 | 2字节 | 变长 | 2字节 | 2字节 | 变长 |
  14 ++----------+----------+----------+----------+----------+----------+
  15 +```
  16 +
  17 +### 2.2 字段说明
  18 +
  19 +| 字段 | 长度 | 类型 | 说明 |
  20 +|----------|--------|--------|--------------------------------|
  21 +| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) |
  22 +| 地址长度 | 2字节 | short | 设备地址的字节长度 |
  23 +| 设备地址 | 变长 | string | 设备标识符 |
  24 +| 功能码 | 2字节 | short | 消息类型标识 |
  25 +| 消息序号 | 2字节 | short | 消息唯一标识 |
  26 +| 包体数据 | 变长 | string | JSON格式的消息内容 |
  27 +
  28 +### 2.3 功能码定义
  29 +
  30 +| 功能码 | 名称 | 说明 |
  31 +|--------|----------|--------------------------------|
  32 +| 10 | 设备注册 | 设备首次连接时的注册请求 |
  33 +| 11 | 注册回复 | 服务器对注册请求的回复 |
  34 +| 20 | 心跳请求 | 设备发送的心跳包 |
  35 +| 21 | 心跳回复 | 服务器对心跳的回复 |
  36 +| 30 | 消息上行 | 设备向服务器发送的数据 |
  37 +| 40 | 消息下行 | 服务器向设备发送的指令 |
  38 +
  39 +## 3. 二进制数据上报包示例
  40 +
  41 +### 3.1 温度传感器数据上报
  42 +
  43 +**原始数据:**
  44 +```json
  45 +{
  46 + "method": "thing.property.post",
  47 + "params": {
  48 + "temperature": 25.5,
  49 + "humidity": 60.2,
  50 + "pressure": 1013.25
  51 + },
  52 + "timestamp": 1642781234567
  53 +}
  54 +```
  55 +
  56 +**数据包结构:**
  57 +```
  58 +包头: 0x00000045 (69字节)
  59 +地址长度: 0x0006 (6字节)
  60 +设备地址: "123456"
  61 +功能码: 0x001E (30 - 消息上行)
  62 +消息序号: 0x1234 (4660)
  63 +包体: JSON字符串
  64 +```
  65 +
  66 +**完整十六进制数据包:**
  67 +```
  68 +00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34
  69 +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67
  70 +2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C
  71 +22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65
  72 +72 61 74 75 72 65 22 3A 32 35 2E 35 2C 22 68 75
  73 +6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72
  74 +65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D
  75 +2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34
  76 +32 37 38 31 32 33 34 35 36 37 7D
  77 +```
  78 +
  79 +### 2.2 GPS定位数据上报
  80 +
  81 +**原始数据:**
  82 +```json
  83 +{
  84 + "method": "thing.property.post",
  85 + "params": {
  86 + "latitude": 39.9042,
  87 + "longitude": 116.4074,
  88 + "altitude": 43.5,
  89 + "speed": 0.0
  90 + },
  91 + "timestamp": 1642781234567
  92 +}
  93 +```
  94 +
  95 +## 3. 心跳包示例
  96 +
  97 +### 3.1 标准心跳包
  98 +
  99 +**原始数据:**
  100 +```json
  101 +{
  102 + "method": "thing.state.online",
  103 + "timestamp": 1642781234567
  104 +}
  105 +```
  106 +
  107 +**数据包结构:**
  108 +```
  109 +包头: 0x00000028 (40字节)
  110 +地址长度: 0x0006 (6字节)
  111 +设备地址: "123456"
  112 +功能码: 0x0014 (20 - 心跳请求)
  113 +消息序号: 0x5678 (22136)
  114 +包体: JSON字符串
  115 +```
  116 +
  117 +**完整十六进制数据包:**
  118 +```
  119 +00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78
  120 +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67
  121 +2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22
  122 +74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 32 37
  123 +38 31 32 33 34 35 36 37 7D
  124 +```
  125 +
  126 +## 4. 复杂数据上报示例
  127 +
  128 +### 4.1 多传感器综合数据
  129 +
  130 +**原始数据:**
  131 +```json
  132 +{
  133 + "method": "thing.property.post",
  134 + "params": {
  135 + "environment": {
  136 + "temperature": 23.8,
  137 + "humidity": 55.0,
  138 + "co2": 420
  139 + },
  140 + "location": {
  141 + "latitude": 39.9042,
  142 + "longitude": 116.4074,
  143 + "altitude": 43.5
  144 + },
  145 + "status": {
  146 + "battery": 78,
  147 + "signal": -65,
  148 + "online": true
  149 + }
  150 + },
  151 + "timestamp": 1642781234567
  152 +}
  153 +```
  154 +
  155 +## 5. 数据包解析步骤
  156 +
  157 +### 5.1 解析流程
  158 +
  159 +1. **读取包头(4字节)**
  160 + - 获取后续数据的总长度
  161 + - 验证数据包完整性
  162 +
  163 +2. **读取设备地址长度(2字节)**
  164 + - 确定设备地址的字节数
  165 +
  166 +3. **读取设备地址(变长)**
  167 + - 根据地址长度读取设备标识
  168 +
  169 +4. **读取功能码(2字节)**
  170 + - 确定消息类型
  171 +
  172 +5. **读取消息序号(2字节)**
  173 + - 获取消息唯一标识
  174 +
  175 +6. **读取包体数据(变长)**
  176 + - 解析JSON格式的消息内容
  177 +
  178 +### 5.2 Java解析示例
  179 +
  180 +```java
  181 +public TcpDataPackage parsePacket(byte[] packet) {
  182 + int index = 0;
  183 +
  184 + // 1. 解析包头
  185 + int totalLength = ByteBuffer.wrap(packet, index, 4).getInt();
  186 + index += 4;
  187 +
  188 + // 2. 解析设备地址长度
  189 + short addrLength = ByteBuffer.wrap(packet, index, 2).getShort();
  190 + index += 2;
  191 +
  192 + // 3. 解析设备地址
  193 + String deviceAddr = new String(packet, index, addrLength);
  194 + index += addrLength;
  195 +
  196 + // 4. 解析功能码
  197 + short functionCode = ByteBuffer.wrap(packet, index, 2).getShort();
  198 + index += 2;
  199 +
  200 + // 5. 解析消息序号
  201 + short messageId = ByteBuffer.wrap(packet, index, 2).getShort();
  202 + index += 2;
  203 +
  204 + // 6. 解析包体数据
  205 + String payload = new String(packet, index, packet.length - index);
  206 +
  207 + return TcpDataPackage.builder()
  208 + .addr(deviceAddr)
  209 + .code(functionCode)
  210 + .mid(messageId)
  211 + .payload(payload)
  212 + .build();
  213 +}
  214 +```
  215 +
  216 +## 6. 注意事项
  217 +
  218 +1. **字节序**:所有多字节数据使用大端序(Big-Endian)
  219 +2. **字符编码**:字符串数据使用UTF-8编码
  220 +3. **JSON格式**:包体数据必须是有效的JSON格式
  221 +4. **长度限制**:单个数据包建议不超过1MB
  222 +5. **错误处理**:解析失败时应返回相应的错误码
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md 0 → 100644
  1 +# TCP JSON格式协议说明
  2 +
  3 +## 1. 协议概述
  4 +
  5 +TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势:
  6 +
  7 +- **标准化**:使用标准JSON格式,易于解析和处理
  8 +- **可读性**:人类可读,便于调试和维护
  9 +- **扩展性**:可以轻松添加新字段,向后兼容
  10 +- **统一性**:与HTTP模块保持一致的数据格式
  11 +
  12 +## 2. 消息格式
  13 +
  14 +### 2.1 基础消息结构
  15 +
  16 +```json
  17 +{
  18 + "id": "消息唯一标识",
  19 + "method": "消息方法",
  20 + "deviceId": "设备ID",
  21 + "params": {
  22 + // 消息参数
  23 + },
  24 + "timestamp": 时间戳
  25 +}
  26 +```
  27 +
  28 +### 2.2 字段说明
  29 +
  30 +| 字段名 | 类型 | 必填 | 说明 |
  31 +|--------|------|------|------|
  32 +| id | String | 是 | 消息唯一标识,UUID格式 |
  33 +| method | String | 是 | 消息方法,如 thing.property.post |
  34 +| deviceId | Long | 是 | 设备ID |
  35 +| params | Object | 否 | 消息参数,具体内容根据method而定 |
  36 +| timestamp | Long | 是 | 时间戳(毫秒) |
  37 +| code | Integer | 否 | 响应码(下行消息使用) |
  38 +| message | String | 否 | 响应消息(下行消息使用) |
  39 +
  40 +## 3. 消息类型
  41 +
  42 +### 3.1 数据上报 (thing.property.post)
  43 +
  44 +设备向服务器上报属性数据。
  45 +
  46 +**示例:**
  47 +```json
  48 +{
  49 + "id": "8ac6a1db91e64aa9996143fdbac2cbfe",
  50 + "method": "thing.property.post",
  51 + "deviceId": 123456,
  52 + "params": {
  53 + "temperature": 25.5,
  54 + "humidity": 60.2,
  55 + "pressure": 1013.25,
  56 + "battery": 85
  57 + },
  58 + "timestamp": 1753111026437
  59 +}
  60 +```
  61 +
  62 +### 3.2 心跳 (thing.state.online)
  63 +
  64 +设备向服务器发送心跳保活。
  65 +
  66 +**示例:**
  67 +```json
  68 +{
  69 + "id": "7db8c4e6408b40f8b2549ddd94f6bb02",
  70 + "method": "thing.state.online",
  71 + "deviceId": 123456,
  72 + "timestamp": 1753111026467
  73 +}
  74 +```
  75 +
  76 +### 3.3 事件上报 (thing.event.post)
  77 +
  78 +设备向服务器上报事件信息。
  79 +
  80 +**示例:**
  81 +```json
  82 +{
  83 + "id": "9e7d72731b854916b1baa5088bd6a907",
  84 + "method": "thing.event.post",
  85 + "deviceId": 123456,
  86 + "params": {
  87 + "eventType": "alarm",
  88 + "level": "warning",
  89 + "description": "温度过高",
  90 + "value": 45.8
  91 + },
  92 + "timestamp": 1753111026468
  93 +}
  94 +```
  95 +
  96 +### 3.4 属性设置 (thing.property.set)
  97 +
  98 +服务器向设备下发属性设置指令。
  99 +
  100 +**示例:**
  101 +```json
  102 +{
  103 + "id": "cmd_001",
  104 + "method": "thing.property.set",
  105 + "deviceId": 123456,
  106 + "params": {
  107 + "targetTemperature": 22.0,
  108 + "mode": "auto"
  109 + },
  110 + "timestamp": 1753111026469
  111 +}
  112 +```
  113 +
  114 +### 3.5 服务调用 (thing.service.invoke)
  115 +
  116 +服务器向设备调用服务。
  117 +
  118 +**示例:**
  119 +```json
  120 +{
  121 + "id": "service_001",
  122 + "method": "thing.service.invoke",
  123 + "deviceId": 123456,
  124 + "params": {
  125 + "service": "restart",
  126 + "args": {
  127 + "delay": 5
  128 + }
  129 + },
  130 + "timestamp": 1753111026470
  131 +}
  132 +```
  133 +
  134 +## 4. 复杂数据示例
  135 +
  136 +### 4.1 多传感器综合数据
  137 +
  138 +```json
  139 +{
  140 + "id": "complex_001",
  141 + "method": "thing.property.post",
  142 + "deviceId": 789012,
  143 + "params": {
  144 + "environment": {
  145 + "temperature": 23.8,
  146 + "humidity": 55.0,
  147 + "co2": 420,
  148 + "pm25": 35
  149 + },
  150 + "location": {
  151 + "latitude": 39.9042,
  152 + "longitude": 116.4074,
  153 + "altitude": 43.5,
  154 + "speed": 0.0
  155 + },
  156 + "status": {
  157 + "battery": 78,
  158 + "signal": -65,
  159 + "online": true,
  160 + "version": "1.2.3"
  161 + }
  162 + },
  163 + "timestamp": 1753111026471
  164 +}
  165 +```
  166 +
  167 +## 5. 与EMQX格式的兼容性
  168 +
  169 +本协议设计参考了EMQX的消息格式,具有良好的兼容性:
  170 +
  171 +### 5.1 EMQX标准格式
  172 +
  173 +```json
  174 +{
  175 + "id": "msg_001",
  176 + "method": "thing.property.post",
  177 + "deviceId": 123456,
  178 + "params": {
  179 + "temperature": 25.5,
  180 + "humidity": 60.2
  181 + },
  182 + "timestamp": 1642781234567
  183 +}
  184 +```
  185 +
  186 +### 5.2 兼容性说明
  187 +
  188 +- ✅ **字段名称**:与EMQX保持一致
  189 +- ✅ **数据类型**:完全兼容
  190 +- ✅ **消息结构**:结构相同
  191 +- ✅ **扩展字段**:支持自定义扩展
  192 +
  193 +## 6. 使用示例
  194 +
  195 +### 6.1 Java编码示例
  196 +
  197 +```java
  198 +// 创建编解码器
  199 +IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec();
  200 +
  201 +// 创建数据上报消息
  202 +Map<String, Object> sensorData = Map.of(
  203 + "temperature", 25.5,
  204 + "humidity", 60.2
  205 +);
  206 +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData);
  207 +message.setDeviceId(123456L);
  208 +
  209 +// 编码为字节数组
  210 +byte[] jsonBytes = codec.encode(message);
  211 +
  212 +// 解码
  213 +IotDeviceMessage decoded = codec.decode(jsonBytes);
  214 +```
  215 +
  216 +### 6.2 便捷方法示例
  217 +
  218 +```java
  219 +// 快速编码数据上报
  220 +byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name");
  221 +
  222 +// 快速编码心跳
  223 +byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name");
  224 +
  225 +// 快速编码事件
  226 +byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name");
  227 +```
  228 +
  229 +## 7. 协议优势
  230 +
  231 +### 7.1 与原TCP二进制协议对比
  232 +
  233 +| 特性 | 二进制协议 | JSON协议 |
  234 +|------|------------|----------|
  235 +| 可读性 | 差 | 优秀 |
  236 +| 调试难度 | 高 | 低 |
  237 +| 扩展性 | 差 | 优秀 |
  238 +| 解析复杂度 | 高 | 低 |
  239 +| 数据大小 | 小 | 稍大 |
  240 +| 标准化程度 | 低 | 高 |
  241 +
  242 +### 7.2 适用场景
  243 +
  244 +- ✅ **开发调试**:JSON格式便于查看和调试
  245 +- ✅ **快速集成**:标准JSON格式,集成简单
  246 +- ✅ **协议扩展**:可以轻松添加新字段
  247 +- ✅ **多语言支持**:JSON格式支持所有主流语言
  248 +- ✅ **云平台对接**:与主流IoT云平台格式兼容
  249 +
  250 +## 8. 最佳实践
  251 +
  252 +### 8.1 消息设计建议
  253 +
  254 +1. **保持简洁**:避免过深的嵌套结构
  255 +2. **字段命名**:使用驼峰命名法,保持一致性
  256 +3. **数据类型**:使用合适的数据类型,避免字符串表示数字
  257 +4. **时间戳**:统一使用毫秒级时间戳
  258 +
  259 +### 8.2 性能优化
  260 +
  261 +1. **批量上报**:可以在params中包含多个数据点
  262 +2. **压缩传输**:对于大数据量可以考虑gzip压缩
  263 +3. **缓存机制**:客户端可以缓存消息,批量发送
  264 +
  265 +### 8.3 错误处理
  266 +
  267 +1. **格式验证**:确保JSON格式正确
  268 +2. **字段检查**:验证必填字段是否存在
  269 +3. **异常处理**:提供详细的错误信息
  270 +
  271 +## 9. 迁移指南
  272 +
  273 +### 9.1 从二进制协议迁移
  274 +
  275 +1. **保持兼容**:可以同时支持两种协议
  276 +2. **逐步迁移**:按设备类型逐步迁移
  277 +3. **测试验证**:充分测试新协议的稳定性
  278 +
  279 +### 9.2 配置变更
  280 +
  281 +```java
  282 +// 在设备配置中指定编解码器类型
  283 +device.setCodecType("TCP_JSON");
  284 +```
  285 +
  286 +这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。