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 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 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 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
4 4 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  5 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager;
5 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
6 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
7 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 10 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
10 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
13 13 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
14 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
15 15 import io.vertx.core.Vertx;
... ... @@ -90,27 +90,21 @@ public class IotGatewayConfiguration {
90 90 }
91 91  
92 92 @Bean
93   - public TcpDeviceConnectionManager tcpDeviceConnectionManager() {
94   - return new TcpDeviceConnectionManager();
95   - }
96   -
97   - @Bean
98 93 public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
99   - TcpDeviceConnectionManager connectionManager,
100 94 IotDeviceService deviceService,
101 95 IotDeviceMessageService messageService,
102 96 IotDeviceCommonApi deviceApi,
  97 + IotTcpCodecManager codecManager,
103 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 103 @Bean
109 104 public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
110   - TcpDeviceConnectionManager connectionManager,
111 105 IotDeviceMessageService messageService,
112 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 4 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
8 7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
9 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10 9 import jakarta.annotation.PostConstruct;
11   -import jakarta.annotation.PreDestroy;
12   -import lombok.RequiredArgsConstructor;
13 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 13 * IoT 网关 TCP 下游订阅者:接收下行给设备的消息
20   - * <p>
21   - * 参考 EMQX 设计理念:
22   - * 1. 高性能消息路由
23   - * 2. 容错机制
24   - * 3. 状态监控
25   - * 4. 资源管理
26 14 *
27 15 * @author 芋道源码
28 16 */
29   -@RequiredArgsConstructor
30 17 @Slf4j
31 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 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 34 @PostConstruct
50 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 39 @Override
89 40 public String getTopic() {
90   - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId());
  41 + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
91 42 }
92 43  
93 44 @Override
94 45 public String getGroup() {
95   - return "tcp-downstream-" + protocolHandler.getServerId();
  46 + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
  47 + return getTopic();
96 48 }
97 49  
98 50 @Override
99 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 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 60 return;
124 61 }
125 62  
126   - // 处理下行消息
  63 + // 2. 处理下行消息
127 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 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 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 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
4 4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  5 +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager;
5 6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
8 8 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
9 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
... ... @@ -16,19 +16,8 @@ import jakarta.annotation.PreDestroy;
16 16 import lombok.Getter;
17 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 20 * IoT 网关 TCP 协议:接收设备上行消息
25   - * <p>
26   - * 负责接收设备上行消息,支持:
27   - * 1. 设备注册
28   - * 2. 心跳保活
29   - * 3. 属性上报
30   - * 4. 事件上报
31   - * 5. 设备连接管理
32 21 *
33 22 * @author 芋道源码
34 23 */
... ... @@ -37,14 +26,14 @@ public class IotTcpUpstreamProtocol {
37 26  
38 27 private final IotGatewayProperties.TcpProperties tcpProperties;
39 28  
40   - private final TcpDeviceConnectionManager connectionManager;
41   -
42 29 private final IotDeviceService deviceService;
43 30  
44 31 private final IotDeviceMessageService messageService;
45 32  
46 33 private final IotDeviceCommonApi deviceApi;
47 34  
  35 + private final IotTcpCodecManager codecManager;
  36 +
48 37 private final Vertx vertx;
49 38  
50 39 @Getter
... ... @@ -53,54 +42,30 @@ public class IotTcpUpstreamProtocol {
53 42 private NetServer netServer;
54 43  
55 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 50 this.tcpProperties = tcpProperties;
62   - this.connectionManager = connectionManager;
63 51 this.deviceService = deviceService;
64 52 this.messageService = messageService;
65 53 this.deviceApi = deviceApi;
  54 + this.codecManager = codecManager;
66 55 this.vertx = vertx;
67 56 this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
68 57 }
69 58  
70 59 @PostConstruct
71 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 62 NetServerOptions options = new NetServerOptions()
98 63 .setPort(tcpProperties.getPort())
99 64 .setTcpKeepAlive(true)
100 65 .setTcpNoDelay(true)
101 66 .setReuseAddress(true);
102 67  
103   - // 2. 配置 SSL(如果启用)
  68 + // 配置 SSL(如果启用)
104 69 if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
105 70 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
106 71 .setKeyPath(tcpProperties.getSslKeyPath())
... ... @@ -108,72 +73,32 @@ public class IotTcpUpstreamProtocol {
108 73 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
109 74 }
110 75  
111   - // 3. 创建 TCP 服务器
  76 + // 创建服务器并设置连接处理器
112 77 netServer = vertx.createNetServer(options);
113   -
114   - // 4. 设置连接处理器
115 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 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 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 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 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 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 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 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 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 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 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
2 2  
3   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 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 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 6 import lombok.extern.slf4j.Slf4j;
14 7  
15 8 /**
... ... @@ -21,20 +14,21 @@ import lombok.extern.slf4j.Slf4j;
21 14 * 3. 属性获取
22 15 * 4. 配置下发
23 16 * 5. OTA 升级
  17 + * <p>
  18 + * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录
24 19 *
25 20 * @author 芋道源码
26 21 */
27 22 @Slf4j
28 23 public class IotTcpDownstreamHandler {
29 24  
30   - private final TcpDeviceConnectionManager connectionManager;
31   -
32 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 30 this.messageService = messageService;
  31 + this.codec = new IotTcpDeviceMessageCodec();
38 32 }
39 33  
40 34 /**
... ... @@ -47,315 +41,19 @@ public class IotTcpDownstreamHandler {
47 41 log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}",
48 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 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 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 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
2 2  
3 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 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 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
18 8 import io.vertx.core.Handler;
19 9 import io.vertx.core.buffer.Buffer;
20 10 import io.vertx.core.net.NetSocket;
21 11 import io.vertx.core.parsetools.RecordParser;
22   -import lombok.RequiredArgsConstructor;
23 12 import lombok.extern.slf4j.Slf4j;
24 13  
25 14 /**
26 15 * IoT 网关 TCP 上行消息处理器
27   - * <p>
28   - * 核心负责:
29   - * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信
30   - * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态
31   - * 3. 【数据上报】接收设备数据上报和事件上报
32   - * 4. 【连接管理】管理连接的建立、维护和清理
33 16 *
34 17 * @author 芋道源码
35 18 */
36   -@RequiredArgsConstructor
37 19 @Slf4j
38 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 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 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 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 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 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 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 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 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格式的升级,提供了更好的可读性、扩展性和兼容性。
... ...