Commit bb1210a17a3a9b584fefbfee0b52a8ce56b9962c

Authored by haohao
1 parent a912be64

feat:【IoT 物联网】重构 TCP 协议处理

Showing 18 changed files with 2860 additions and 433 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java 0 → 100644
  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 + initializeMethodMappings();
  58 + }
  59 +
  60 + // ==================== 缓存管理 ====================
  61 +
  62 + /**
  63 + * JSON 缓存,提升编解码性能
  64 + */
  65 + private final Map<String, JSONObject> jsonCache = new ConcurrentHashMap<>();
  66 +
  67 + /**
  68 + * 缓存最大大小
  69 + */
  70 + private static final int MAX_CACHE_SIZE = 1000;
  71 +
  72 + // ==================== 常量定义 ====================
  73 +
  74 + /**
  75 + * 负载字段名
  76 + */
  77 + public static class PayloadField {
  78 + public static final String TIMESTAMP = "timestamp";
  79 + public static final String MESSAGE_ID = "msgId";
  80 + public static final String DEVICE_ID = "deviceId";
  81 + public static final String PARAMS = "params";
  82 + public static final String DATA = "data";
  83 + public static final String CODE = "code";
  84 + public static final String MESSAGE = "message";
  85 + }
  86 +
  87 + /**
  88 + * 消息方法映射
  89 + */
  90 + public static class MessageMethod {
  91 + public static final String PROPERTY_POST = "thing.property.post";
  92 + public static final String PROPERTY_SET = "thing.property.set";
  93 + public static final String PROPERTY_GET = "thing.property.get";
  94 + public static final String EVENT_POST = "thing.event.post";
  95 + public static final String SERVICE_INVOKE = "thing.service.invoke";
  96 + public static final String CONFIG_PUSH = "thing.config.push";
  97 + public static final String OTA_UPGRADE = "thing.ota.upgrade";
  98 + public static final String STATE_ONLINE = "thing.state.online";
  99 + public static final String STATE_OFFLINE = "thing.state.offline";
  100 + }
  101 +
  102 + // ==================== 初始化方法 ====================
  103 +
  104 + /**
  105 + * 初始化方法映射
  106 + */
  107 + private static void initializeMethodMappings() {
  108 + METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP);
  109 + METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET);
  110 + METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET);
  111 + METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP);
  112 + METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE);
  113 + METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN);
  114 + METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN);
  115 + METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT);
  116 + METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT);
  117 +
  118 + // 反向映射
  119 + METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method));
  120 + }
  121 +
  122 + // ==================== 编解码方法 ====================
  123 +
  124 + @Override
  125 + public byte[] encode(IotDeviceMessage message) {
  126 + validateEncodeParams(message);
  127 +
  128 + try {
  129 + if (log.isDebugEnabled()) {
  130 + log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}",
  131 + message.getMethod(), message.getRequestId());
  132 + }
  133 +
  134 + // 1. 获取功能码
  135 + short code = getCodeByMethodSafely(message.getMethod());
  136 +
  137 + // 2. 构建负载
  138 + String payload = buildPayloadOptimized(message);
  139 +
  140 + // 3. 构建 TCP 数据包
  141 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  142 + .addr("") // 地址在发送时由调用方设置
  143 + .code(code)
  144 + .mid((short) 0) // 消息序号在发送时由调用方设置
  145 + .payload(payload)
  146 + .build();
  147 +
  148 + // 4. 编码为字节流
  149 + Buffer buffer = TcpDataEncoder.encode(dataPackage);
  150 + byte[] result = buffer.getBytes();
  151 +
  152 + // 5. 统计信息
  153 + if (log.isDebugEnabled()) {
  154 + log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}",
  155 + message.getMethod(), result.length);
  156 + }
  157 +
  158 + return result;
  159 +
  160 + } catch (Exception e) {
  161 + log.error("[encode][TCP 消息编码失败] 消息: {}", message, e);
  162 + throw new TcpCodecException("TCP 消息编码失败", e);
  163 + }
  164 + }
  165 +
  166 + @Override
  167 + public IotDeviceMessage decode(byte[] bytes) {
  168 + validateDecodeParams(bytes);
  169 +
  170 + try {
  171 + if (log.isDebugEnabled()) {
  172 + log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length);
  173 + }
  174 +
  175 + // 1. 解码 TCP 数据包
  176 + Buffer buffer = Buffer.buffer(bytes);
  177 + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
  178 +
  179 + // 2. 获取消息方法
  180 + String method = getMethodByCodeSafely(dataPackage.getCode());
  181 +
  182 + // 3. 解析负载数据
  183 + Object params = parsePayloadOptimized(dataPackage.getPayload());
  184 +
  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 +
  194 + return message;
  195 +
  196 + } catch (Exception e) {
  197 + log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}",
  198 + bytes.length, truncateData(bytes, 100), e);
  199 + throw new TcpCodecException("TCP 消息解码失败", e);
  200 + }
  201 + }
  202 +
  203 + @Override
  204 + public String type() {
  205 + return TYPE;
  206 + }
  207 +
  208 + // ==================== 内部辅助方法 ====================
  209 +
  210 + /**
  211 + * 验证编码参数
  212 + */
  213 + private void validateEncodeParams(IotDeviceMessage message) {
  214 + if (Objects.isNull(message)) {
  215 + throw new IllegalArgumentException("IoT 设备消息不能为空");
  216 + }
  217 + if (StrUtil.isEmpty(message.getMethod())) {
  218 + throw new IllegalArgumentException("消息方法不能为空");
  219 + }
  220 + }
  221 +
  222 + /**
  223 + * 验证解码参数
  224 + */
  225 + private void validateDecodeParams(byte[] bytes) {
  226 + if (Objects.isNull(bytes) || bytes.length == 0) {
  227 + throw new IllegalArgumentException("待解码数据不能为空");
  228 + }
  229 + if (bytes.length > 1024 * 1024) { // 1MB 限制
  230 + throw new IllegalArgumentException("数据包过大,超过1MB限制");
  231 + }
  232 + }
  233 +
  234 + /**
  235 + * 安全获取功能码
  236 + */
  237 + private short getCodeByMethodSafely(String method) {
  238 + Short code = METHOD_TO_CODE_MAP.get(method);
  239 + if (code == null) {
  240 + log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method);
  241 + return TcpDataPackage.CODE_DATA_UP; // 默认为数据上报
  242 + }
  243 + return code;
  244 + }
  245 +
  246 + /**
  247 + * 安全获取消息方法
  248 + */
  249 + private String getMethodByCodeSafely(short code) {
  250 + String method = CODE_TO_METHOD_MAP.get(code);
  251 + if (method == null) {
  252 + log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code);
  253 + return MessageMethod.PROPERTY_POST; // 默认为属性上报
  254 + }
  255 + return method;
  256 + }
  257 +
  258 + /**
  259 + * 优化的负载构建
  260 + */
  261 + private String buildPayloadOptimized(IotDeviceMessage message) {
  262 + // 使用缓存键
  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 + // 添加基础字段
  276 + addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId());
  277 + addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId());
  278 + addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams());
  279 + addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData());
  280 + addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode());
  281 + addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg());
  282 +
  283 + // 添加时间戳
  284 + payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis());
  285 +
  286 + // 缓存管理
  287 + if (jsonCache.size() < MAX_CACHE_SIZE) {
  288 + jsonCache.put(cacheKey, payload);
  289 + } else {
  290 + cleanJsonCacheIfNeeded();
  291 + }
  292 +
  293 + return payload.toString();
  294 + }
  295 +
  296 + /**
  297 + * 优化的负载解析
  298 + */
  299 + private Object parsePayloadOptimized(String payload) {
  300 + if (StrUtil.isEmpty(payload)) {
  301 + return null;
  302 + }
  303 +
  304 + try {
  305 + // 尝试从缓存获取
  306 + JSONObject cachedJson = jsonCache.get(payload);
  307 + if (cachedJson != null) {
  308 + return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson;
  309 + }
  310 +
  311 + // 解析 JSON 对象
  312 + JSONObject jsonObject = JSONUtil.parseObj(payload);
  313 +
  314 + // 缓存解析结果
  315 + if (jsonCache.size() < MAX_CACHE_SIZE) {
  316 + jsonCache.put(payload, jsonObject);
  317 + }
  318 +
  319 + return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject;
  320 +
  321 + } catch (JSONException e) {
  322 + log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload);
  323 + return payload;
  324 + } catch (Exception e) {
  325 + log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e);
  326 + return payload;
  327 + }
  328 + }
  329 +
  330 + /**
  331 + * 添加非空值到负载
  332 + */
  333 + private void addToPayloadIfNotNull(JSONObject json, String key, Object value) {
  334 + if (ObjectUtil.isNotNull(value)) {
  335 + json.set(key, value);
  336 + }
  337 + }
  338 +
  339 + /**
  340 + * 添加非空字符串到负载
  341 + */
  342 + private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) {
  343 + if (StrUtil.isNotEmpty(value)) {
  344 + json.set(key, value);
  345 + }
  346 + }
  347 +
  348 + /**
  349 + * 清理JSON缓存
  350 + */
  351 + private void cleanJsonCacheIfNeeded() {
  352 + if (jsonCache.size() > MAX_CACHE_SIZE) {
  353 + // 清理一半的缓存
  354 + int clearCount = jsonCache.size() / 2;
  355 + jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5);
  356 +
  357 + if (log.isDebugEnabled()) {
  358 + log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size());
  359 + }
  360 + }
  361 + }
  362 +
  363 + /**
  364 + * 截断数据用于日志输出
  365 + */
  366 + private String truncateData(byte[] data, int maxLength) {
  367 + if (data.length <= maxLength) {
  368 + return new String(data, StandardCharsets.UTF_8);
  369 + }
  370 +
  371 + byte[] truncated = new byte[maxLength];
  372 + System.arraycopy(data, 0, truncated, 0, maxLength);
  373 + return new String(truncated, StandardCharsets.UTF_8) + "...(截断)";
  374 + }
  375 +
  376 + // ==================== 自定义异常 ====================
  377 +
  378 + /**
  379 + * TCP 编解码异常
  380 + */
  381 + public static class TcpCodecException extends RuntimeException {
  382 + public TcpCodecException(String message) {
  383 + super(message);
  384 + }
  385 +
  386 + public TcpCodecException(String message, Throwable cause) {
  387 + super(message, cause);
  388 + }
  389 + }
  390 +}
0 \ No newline at end of file 391 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
@@ -7,10 +7,9 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr @@ -7,10 +7,9 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr
7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
10 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager;  
11 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;  
12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
14 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 13 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
16 import io.vertx.core.Vertx; 15 import io.vertx.core.Vertx;
@@ -85,26 +84,33 @@ public class IotGatewayConfiguration { @@ -85,26 +84,33 @@ public class IotGatewayConfiguration {
85 @Slf4j 84 @Slf4j
86 public static class TcpProtocolConfiguration { 85 public static class TcpProtocolConfiguration {
87 86
88 - // TODO @haohao:close  
89 - @Bean 87 + @Bean(destroyMethod = "close")
90 public Vertx tcpVertx() { 88 public Vertx tcpVertx() {
91 return Vertx.vertx(); 89 return Vertx.vertx();
92 } 90 }
93 91
94 @Bean 92 @Bean
95 - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(Vertx tcpVertx, IotGatewayProperties gatewayProperties,  
96 - IotTcpConnectionManager connectionManager, 93 + public TcpDeviceConnectionManager tcpDeviceConnectionManager() {
  94 + return new TcpDeviceConnectionManager();
  95 + }
  96 +
  97 + @Bean
  98 + public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
  99 + TcpDeviceConnectionManager connectionManager,
  100 + IotDeviceService deviceService,
97 IotDeviceMessageService messageService, 101 IotDeviceMessageService messageService,
98 - IotDeviceService deviceService, IotDeviceCommonApi deviceApi) {  
99 - return new IotTcpUpstreamProtocol(tcpVertx, gatewayProperties, connectionManager,  
100 - messageService, deviceService, deviceApi); 102 + IotDeviceCommonApi deviceApi,
  103 + Vertx tcpVertx) {
  104 + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager,
  105 + deviceService, messageService, deviceApi, tcpVertx);
101 } 106 }
102 107
103 @Bean 108 @Bean
104 - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol tcpUpstreamProtocol,  
105 - IotMessageBus messageBus,  
106 - IotTcpDownstreamHandler downstreamHandler) {  
107 - return new IotTcpDownstreamSubscriber(tcpUpstreamProtocol, messageBus, downstreamHandler); 109 + public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
  110 + TcpDeviceConnectionManager connectionManager,
  111 + IotDeviceMessageService messageService,
  112 + IotMessageBus messageBus) {
  113 + return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus);
108 } 114 }
109 115
110 } 116 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
@@ -116,22 +116,6 @@ public class IotGatewayProperties { @@ -116,22 +116,6 @@ public class IotGatewayProperties {
116 } 116 }
117 117
118 @Data 118 @Data
119 - public static class TcpProperties {  
120 -  
121 - /**  
122 - * 是否开启  
123 - */  
124 - @NotNull(message = "是否开启不能为空")  
125 - private Boolean enabled;  
126 -  
127 - /**  
128 - * 服务端口(默认:8093)  
129 - */  
130 - private Integer serverPort = 8093;  
131 -  
132 - }  
133 -  
134 - @Data  
135 public static class EmqxProperties { 119 public static class EmqxProperties {
136 120
137 /** 121 /**
@@ -300,4 +284,45 @@ public class IotGatewayProperties { @@ -300,4 +284,45 @@ public class IotGatewayProperties {
300 284
301 } 285 }
302 286
  287 + @Data
  288 + public static class TcpProperties {
  289 +
  290 + /**
  291 + * 是否开启
  292 + */
  293 + @NotNull(message = "是否开启不能为空")
  294 + private Boolean enabled;
  295 +
  296 + /**
  297 + * 服务器端口
  298 + */
  299 + private Integer port = 8091;
  300 +
  301 + /**
  302 + * 心跳超时时间(毫秒)
  303 + */
  304 + private Long keepAliveTimeoutMs = 30000L;
  305 +
  306 + /**
  307 + * 最大连接数
  308 + */
  309 + private Integer maxConnections = 1000;
  310 +
  311 + /**
  312 + * 是否启用SSL
  313 + */
  314 + private Boolean sslEnabled = false;
  315 +
  316 + /**
  317 + * SSL证书路径
  318 + */
  319 + private String sslCertPath;
  320 +
  321 + /**
  322 + * SSL私钥路径
  323 + */
  324 + private String sslKeyPath;
  325 +
  326 + }
  327 +
303 } 328 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConnectionManager.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;  
2 -  
3 -import io.vertx.core.net.NetSocket;  
4 -import lombok.extern.slf4j.Slf4j;  
5 -import org.springframework.stereotype.Component;  
6 -  
7 -import java.util.concurrent.ConcurrentHashMap;  
8 -import java.util.concurrent.ConcurrentMap;  
9 -  
10 -/**  
11 - * IoT TCP 连接管理器  
12 - *  
13 - * @author 芋道源码  
14 - */  
15 -@Component  
16 -@Slf4j  
17 -public class IotTcpConnectionManager {  
18 -  
19 - // TODO @haohao:要考虑,相同设备,多次连接的情况哇?  
20 - /**  
21 - * 连接集合  
22 - *  
23 - * key:设备唯一标识  
24 - */  
25 - private final ConcurrentMap<String, NetSocket> connectionMap = new ConcurrentHashMap<>();  
26 -  
27 - /**  
28 - * 添加一个新连接  
29 - *  
30 - * @param deviceId 设备唯一标识  
31 - * @param socket Netty Channel  
32 - */  
33 - public void addConnection(String deviceId, NetSocket socket) {  
34 - log.info("[addConnection][设备({}) 连接({})]", deviceId, socket.remoteAddress());  
35 - connectionMap.put(deviceId, socket);  
36 - }  
37 -  
38 - /**  
39 - * 根据设备 ID 获取连接  
40 - *  
41 - * @param deviceId 设备 ID  
42 - * @return 连接  
43 - */  
44 - public NetSocket getConnection(String deviceId) {  
45 - return connectionMap.get(deviceId);  
46 - }  
47 -  
48 - /**  
49 - * 移除指定连接  
50 - *  
51 - * @param socket Netty Channel  
52 - */  
53 - public void removeConnection(NetSocket socket) {  
54 - // TODO @haohao:vertx 的 socket,有没办法设置一些属性,类似 netty 的;目的是,避免遍历 connectionMap 去操作哈;  
55 - connectionMap.entrySet().stream()  
56 - .filter(entry -> entry.getValue().equals(socket))  
57 - .findFirst()  
58 - .ifPresent(entry -> {  
59 - log.info("[removeConnection][设备({}) 断开连接({})]", entry.getKey(), socket.remoteAddress());  
60 - connectionMap.remove(entry.getKey());  
61 - });  
62 - }  
63 -  
64 -}  
65 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;  
2 -  
3 -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;  
4 -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;  
5 -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;  
6 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;  
7 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;  
8 -import jakarta.annotation.PostConstruct;  
9 -import lombok.RequiredArgsConstructor;  
10 -import lombok.extern.slf4j.Slf4j;  
11 -  
12 -/**  
13 - * IoT 网关 TCP 订阅者:接收下行给设备的消息  
14 - *  
15 - * @author 芋道源码  
16 - */  
17 -@RequiredArgsConstructor  
18 -@Slf4j  
19 -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {  
20 -  
21 - private final IotTcpUpstreamProtocol protocol;  
22 -  
23 - private final IotMessageBus messageBus;  
24 -  
25 - private final IotTcpDownstreamHandler downstreamHandler;  
26 -  
27 - @PostConstruct  
28 - public void init() {  
29 - messageBus.register(this);  
30 - }  
31 -  
32 - @Override  
33 - public String getTopic() {  
34 - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());  
35 - }  
36 -  
37 - @Override  
38 - public String getGroup() {  
39 - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group  
40 - return getTopic();  
41 - }  
42 -  
43 - @Override  
44 - public void onMessage(IotDeviceMessage message) {  
45 - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",  
46 - message.getId(), message.getMethod(), message.getDeviceId());  
47 - try {  
48 - // 1. 校验  
49 - String method = message.getMethod();  
50 - if (method == null) {  
51 - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",  
52 - message.getId(), message.getDeviceId());  
53 - return;  
54 - }  
55 -  
56 - // 2. 处理下行消息  
57 - downstreamHandler.handle(message);  
58 - } catch (Exception e) {  
59 - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",  
60 - message.getId(), message.getMethod(), message.getDeviceId(), e);  
61 - }  
62 - }  
63 - 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
  9 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  10 +import jakarta.annotation.PostConstruct;
  11 +import jakarta.annotation.PreDestroy;
  12 +import lombok.RequiredArgsConstructor;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +import java.util.concurrent.atomic.AtomicBoolean;
  16 +import java.util.concurrent.atomic.AtomicLong;
  17 +
  18 +/**
  19 + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息
  20 + * <p>
  21 + * 参考 EMQX 设计理念:
  22 + * 1. 高性能消息路由
  23 + * 2. 容错机制
  24 + * 3. 状态监控
  25 + * 4. 资源管理
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@RequiredArgsConstructor
  30 +@Slf4j
  31 +public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
  32 +
  33 + private final IotTcpUpstreamProtocol protocolHandler;
  34 +
  35 + private final TcpDeviceConnectionManager connectionManager;
  36 +
  37 + private final IotDeviceMessageService messageService;
  38 +
  39 + private final IotMessageBus messageBus;
  40 +
  41 + private volatile IotTcpDownstreamHandler downstreamHandler;
  42 +
  43 + private final AtomicBoolean initialized = new AtomicBoolean(false);
  44 +
  45 + private final AtomicLong processedMessages = new AtomicLong(0);
  46 +
  47 + private final AtomicLong failedMessages = new AtomicLong(0);
  48 +
  49 + @PostConstruct
  50 + 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 + }
  86 + }
  87 +
  88 + @Override
  89 + public String getTopic() {
  90 + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId());
  91 + }
  92 +
  93 + @Override
  94 + public String getGroup() {
  95 + return "tcp-downstream-" + protocolHandler.getServerId();
  96 + }
  97 +
  98 + @Override
  99 + public void onMessage(IotDeviceMessage message) {
  100 + if (!initialized.get()) {
  101 + log.warn("[onMessage][订阅者未初始化,跳过消息处理]");
  102 + return;
  103 + }
  104 +
  105 + long startTime = System.currentTimeMillis();
  106 +
  107 + 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 + // 参数校验
  116 + if (message.getDeviceId() == null) {
  117 + log.warn("[onMessage][下行消息设备ID为空,跳过处理] 消息: {}", message);
  118 + return;
  119 + }
  120 +
  121 + // 检查连接状态
  122 + if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) {
  123 + log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}",
  124 + message.getDeviceId(), message.getMethod());
  125 + return;
  126 + }
  127 +
  128 + // 处理下行消息
  129 + downstreamHandler.handle(message);
  130 +
  131 + // 性能监控
  132 + long processTime = System.currentTimeMillis() - startTime;
  133 + if (processTime > 1000) { // 超过1秒的慢消息
  134 + log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms",
  135 + message.getDeviceId(), message.getMethod(), processTime);
  136 + }
  137 +
  138 + } catch (Exception e) {
  139 + failedMessages.incrementAndGet();
  140 + log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}",
  141 + message.getDeviceId(), message.getMethod(), message, e);
  142 + }
  143 + }
  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 + }
64 } 163 }
65 \ No newline at end of file 164 \ 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
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;  
2 -  
3 -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;  
4 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;  
5 -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;  
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpConnectionHandler;  
7 -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;  
8 -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;  
9 -import io.vertx.core.Vertx;  
10 -import io.vertx.core.net.NetServer;  
11 -import jakarta.annotation.PostConstruct;  
12 -import jakarta.annotation.PreDestroy;  
13 -import lombok.Getter;  
14 -import lombok.RequiredArgsConstructor;  
15 -import lombok.extern.slf4j.Slf4j;  
16 -  
17 -/**  
18 - * IoT 网关 TCP 协议:接收设备上行消息  
19 - *  
20 - * @author 芋道源码  
21 - */  
22 -@Slf4j  
23 -@RequiredArgsConstructor  
24 -public class IotTcpUpstreamProtocol {  
25 -  
26 - private final Vertx vertx;  
27 -  
28 - private final IotGatewayProperties gatewayProperties;  
29 -  
30 - private final IotTcpConnectionManager connectionManager;  
31 -  
32 - private final IotDeviceMessageService messageService;  
33 -  
34 - private final IotDeviceService deviceService;  
35 -  
36 - private final IotDeviceCommonApi deviceApi;  
37 -  
38 - @Getter  
39 - private String serverId;  
40 -  
41 - private NetServer netServer;  
42 -  
43 - @PostConstruct  
44 - public void start() {  
45 - // 1. 初始化参数  
46 - IotGatewayProperties.TcpProperties tcpProperties = gatewayProperties.getProtocol().getTcp();  
47 - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getServerPort());  
48 -  
49 - // 2. 创建 TCP 服务器  
50 - netServer = vertx.createNetServer();  
51 - netServer.connectHandler(socket -> {  
52 - new IotTcpConnectionHandler(socket, connectionManager,  
53 - messageService, deviceService, deviceApi, serverId).start();  
54 - });  
55 -  
56 - // 3. 启动 TCP 服务器  
57 - netServer.listen(tcpProperties.getServerPort())  
58 - .onSuccess(server -> log.info("[start][IoT 网关 TCP 服务启动成功,端口:{}]", server.actualPort()))  
59 - .onFailure(e -> log.error("[start][IoT 网关 TCP 服务启动失败]", e));  
60 - }  
61 -  
62 - @PreDestroy  
63 - public void stop() {  
64 - if (netServer != null) {  
65 - netServer.close()  
66 - .onSuccess(v -> log.info("[stop][IoT 网关 TCP 服务已停止]"))  
67 - .onFailure(e -> log.error("[stop][IoT 网关 TCP 服务停止失败]", e));  
68 - }  
69 - }  
70 - 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  4 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  5 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
  8 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  9 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  10 +import io.vertx.core.Vertx;
  11 +import io.vertx.core.net.NetServer;
  12 +import io.vertx.core.net.NetServerOptions;
  13 +import io.vertx.core.net.PemKeyCertOptions;
  14 +import jakarta.annotation.PostConstruct;
  15 +import jakarta.annotation.PreDestroy;
  16 +import lombok.Getter;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +
  19 +import java.util.concurrent.CountDownLatch;
  20 +import java.util.concurrent.TimeUnit;
  21 +import java.util.concurrent.atomic.AtomicReference;
  22 +
  23 +/**
  24 + * IoT 网关 TCP 协议:接收设备上行消息
  25 + * <p>
  26 + * 负责接收设备上行消息,支持:
  27 + * 1. 设备注册
  28 + * 2. 心跳保活
  29 + * 3. 属性上报
  30 + * 4. 事件上报
  31 + * 5. 设备连接管理
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Slf4j
  36 +public class IotTcpUpstreamProtocol {
  37 +
  38 + private final IotGatewayProperties.TcpProperties tcpProperties;
  39 +
  40 + private final TcpDeviceConnectionManager connectionManager;
  41 +
  42 + private final IotDeviceService deviceService;
  43 +
  44 + private final IotDeviceMessageService messageService;
  45 +
  46 + private final IotDeviceCommonApi deviceApi;
  47 +
  48 + private final Vertx vertx;
  49 +
  50 + @Getter
  51 + private final String serverId;
  52 +
  53 + private NetServer netServer;
  54 +
  55 + public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
  56 + TcpDeviceConnectionManager connectionManager,
  57 + IotDeviceService deviceService,
  58 + IotDeviceMessageService messageService,
  59 + IotDeviceCommonApi deviceApi,
  60 + Vertx vertx) {
  61 + this.tcpProperties = tcpProperties;
  62 + this.connectionManager = connectionManager;
  63 + this.deviceService = deviceService;
  64 + this.messageService = messageService;
  65 + this.deviceApi = deviceApi;
  66 + this.vertx = vertx;
  67 + this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
  68 + }
  69 +
  70 + @PostConstruct
  71 + 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 + // 1. 创建服务器选项
  96 + NetServerOptions options = new NetServerOptions()
  97 + .setPort(tcpProperties.getPort())
  98 + .setTcpKeepAlive(true)
  99 + .setTcpNoDelay(true)
  100 + .setReuseAddress(true);
  101 +
  102 + // 2. 配置 SSL(如果启用)
  103 + if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
  104 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  105 + .setKeyPath(tcpProperties.getSslKeyPath())
  106 + .setCertPath(tcpProperties.getSslCertPath());
  107 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  108 + }
  109 +
  110 + // 3. 创建 TCP 服务器
  111 + netServer = vertx.createNetServer(options);
  112 +
  113 + // 4. 设置连接处理器
  114 + netServer.connectHandler(socket -> {
  115 + log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress());
  116 + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(
  117 + tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId);
  118 + handler.handle(socket);
  119 + });
  120 +
  121 + // 5. 同步启动服务器,等待结果
  122 + CountDownLatch latch = new CountDownLatch(1);
  123 + AtomicReference<Throwable> failure = new AtomicReference<>();
  124 + netServer.listen(result -> {
  125 + if (result.succeeded()) {
  126 + log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}",
  127 + result.result().actualPort(), serverId);
  128 + } else {
  129 + log.error("[startTcpServer][TCP 服务器启动失败]", result.cause());
  130 + failure.set(result.cause());
  131 + }
  132 + latch.countDown();
  133 + });
  134 +
  135 + // 6. 等待启动结果,设置超时
  136 + try {
  137 + if (!latch.await(10, TimeUnit.SECONDS)) {
  138 + throw new RuntimeException("TCP 服务器启动超时");
  139 + }
  140 + if (failure.get() != null) {
  141 + throw new RuntimeException("TCP 服务器启动失败", failure.get());
  142 + }
  143 + } catch (InterruptedException e) {
  144 + Thread.currentThread().interrupt();
  145 + throw new RuntimeException("TCP 服务器启动被中断", e);
  146 + }
  147 + }
  148 +
  149 + /**
  150 + * 停止 TCP 服务器
  151 + */
  152 + private void stopTcpServer() {
  153 + if (netServer == null) {
  154 + return;
  155 + }
  156 + log.info("[stopTcpServer][准备关闭 TCP 服务器]");
  157 + CountDownLatch latch = new CountDownLatch(1);
  158 + // 异步关闭,并使用 Latch 等待结果
  159 + netServer.close(result -> {
  160 + if (result.succeeded()) {
  161 + log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]");
  162 + } else {
  163 + log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause());
  164 + }
  165 + latch.countDown();
  166 + });
  167 +
  168 + try {
  169 + // 等待关闭完成,设置超时
  170 + if (!latch.await(10, TimeUnit.SECONDS)) {
  171 + log.warn("[stopTcpServer][关闭 TCP 服务器超时]");
  172 + }
  173 + } catch (InterruptedException e) {
  174 + Thread.currentThread().interrupt();
  175 + log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e);
  176 + }
  177 + }
71 } 178 }
72 \ No newline at end of file 179 \ 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 0 → 100644
  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 + * 封装设备连接的基本信息和操作。
  16 + * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。
  17 + * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +@Slf4j
  22 +public class TcpDeviceClient {
  23 +
  24 + @Getter
  25 + private final String clientId;
  26 +
  27 + @Getter
  28 + @Setter
  29 + private String deviceAddr; // 从 final 移除,因为在注册后才设置
  30 +
  31 + @Getter
  32 + @Setter
  33 + private String productKey;
  34 +
  35 + @Getter
  36 + @Setter
  37 + private String deviceName;
  38 +
  39 + @Getter
  40 + @Setter
  41 + private Long deviceId;
  42 +
  43 + @Getter
  44 + private NetSocket socket;
  45 +
  46 + @Getter
  47 + @Setter
  48 + private RecordParser parser;
  49 +
  50 + @Getter
  51 + private final long keepAliveTimeoutMs; // 改为 final,通过构造函数注入
  52 +
  53 + private volatile long lastKeepAliveTime;
  54 +
  55 + private final AtomicBoolean authenticated = new AtomicBoolean(false);
  56 + private final AtomicBoolean closed = new AtomicBoolean(false);
  57 +
  58 + /**
  59 + * 构造函数
  60 + *
  61 + * @param clientId 客户端ID,全局唯一
  62 + * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取
  63 + */
  64 + public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) {
  65 + this.clientId = clientId;
  66 + this.keepAliveTimeoutMs = keepAliveTimeoutMs;
  67 + this.lastKeepAliveTime = System.currentTimeMillis();
  68 + }
  69 +
  70 + /**
  71 + * 绑定网络套接字,并设置相关处理器。
  72 + * 此方法应在 Vert.x Event Loop 线程中调用。
  73 + *
  74 + * @param socket 网络套接字
  75 + */
  76 + public void setSocket(NetSocket socket) {
  77 + // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理
  78 + if (this.socket != null && this.socket != socket) {
  79 + log.warn("[setSocket][客户端({})] 正在用新的 socket 替换旧的,旧 socket 将被关闭。", clientId);
  80 + this.socket.close();
  81 + }
  82 +
  83 + this.socket = socket;
  84 +
  85 + if (socket != null) {
  86 + // 1. 设置关闭处理器
  87 + socket.closeHandler(v -> {
  88 + log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId);
  89 + shutdown(); // 统一调用 shutdown 进行资源清理
  90 + });
  91 +
  92 + // 2. 设置异常处理器
  93 + socket.exceptionHandler(e -> {
  94 + log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e);
  95 + shutdown(); // 出现异常时也关闭连接
  96 + });
  97 +
  98 + // 3. 设置数据处理器
  99 + socket.handler(buffer -> {
  100 + // 任何数据往来都表示连接是活跃的
  101 + keepAlive();
  102 +
  103 + if (parser != null) {
  104 + parser.handle(buffer);
  105 + } else {
  106 + log.warn("[setSocket][设备客户端({})] 未设置解析器(parser),原始数据被忽略: {}", clientId, buffer.toString());
  107 + }
  108 + });
  109 + }
  110 + }
  111 +
  112 + /**
  113 + * 更新心跳时间,表示设备仍然活跃。
  114 + */
  115 + public void keepAlive() {
  116 + this.lastKeepAliveTime = System.currentTimeMillis();
  117 + }
  118 +
  119 + /**
  120 + * 检查连接是否在线。
  121 + * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内。
  122 + *
  123 + * @return 是否在线
  124 + */
  125 + public boolean isOnline() {
  126 + if (closed.get() || socket == null) {
  127 + return false;
  128 + }
  129 + long idleTime = System.currentTimeMillis() - lastKeepAliveTime;
  130 + return idleTime < keepAliveTimeoutMs;
  131 + }
  132 +
  133 + public boolean isAuthenticated() {
  134 + return authenticated.get();
  135 + }
  136 +
  137 + public void setAuthenticated(boolean authenticated) {
  138 + this.authenticated.set(authenticated);
  139 + }
  140 +
  141 + /**
  142 + * 向设备发送消息。
  143 + *
  144 + * @param buffer 消息内容
  145 + */
  146 + public void sendMessage(Buffer buffer) {
  147 + if (closed.get() || socket == null) {
  148 + log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId);
  149 + return;
  150 + }
  151 +
  152 + // Vert.x 的 write 是异步的,不会阻塞
  153 + socket.write(buffer, result -> {
  154 + if (result.succeeded()) {
  155 + log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId);
  156 + // 发送成功也更新心跳,表示连接活跃
  157 + keepAlive();
  158 + } else {
  159 + log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause());
  160 + // 发送失败可能意味着连接已断开,主动关闭
  161 + shutdown();
  162 + }
  163 + });
  164 + }
  165 +
  166 + /**
  167 + * 关闭客户端连接并清理资源。
  168 + * 这是一个幂等操作,可以被多次安全调用。
  169 + */
  170 + public void shutdown() {
  171 + // 使用原子操作保证只执行一次关闭逻辑
  172 + if (closed.getAndSet(true)) {
  173 + return;
  174 + }
  175 +
  176 + log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId);
  177 +
  178 + // 先将 socket 引用置空,再关闭,避免并发问题
  179 + NetSocket socketToClose = this.socket;
  180 + this.socket = null;
  181 +
  182 + if (socketToClose != null) {
  183 + try {
  184 + // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭
  185 + socketToClose.close();
  186 + } catch (Exception e) {
  187 + log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e);
  188 + }
  189 + }
  190 +
  191 + // 重置认证状态
  192 + authenticated.set(false);
  193 + }
  194 +
  195 + public String getConnectionInfo() {
  196 + NetSocket currentSocket = this.socket;
  197 + if (currentSocket != null && currentSocket.remoteAddress() != null) {
  198 + return currentSocket.remoteAddress().toString();
  199 + }
  200 + return "disconnected";
  201 + }
  202 +
  203 + public long getLastKeepAliveTime() {
  204 + return lastKeepAliveTime;
  205 + }
  206 +
  207 + @Override
  208 + public String toString() {
  209 + return "TcpDeviceClient{" +
  210 + "clientId='" + clientId + '\'' +
  211 + ", deviceAddr='" + deviceAddr + '\'' +
  212 + ", deviceId=" + deviceId +
  213 + ", authenticated=" + authenticated.get() +
  214 + ", online=" + isOnline() +
  215 + ", connection=" + getConnectionInfo() +
  216 + '}';
  217 + }
  218 +}
0 \ No newline at end of file 219 \ 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 0 → 100644
  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. 流量控制
  20 + * 4. 监控统计
  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 + if (clientMap.size() >= MAX_CONNECTIONS) {
  110 + log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr);
  111 + return false;
  112 + }
  113 +
  114 + writeLock.lock();
  115 + try {
  116 + log.info("[addClient][添加设备客户端: {}]", deviceAddr);
  117 +
  118 + // 关闭之前的连接(如果存在)
  119 + TcpDeviceClient existingClient = clientMap.get(deviceAddr);
  120 + if (existingClient != null) {
  121 + log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr);
  122 + removeClientInternal(deviceAddr, existingClient);
  123 + }
  124 +
  125 + // 添加新连接
  126 + clientMap.put(deviceAddr, client);
  127 +
  128 + // 添加套接字映射
  129 + if (client.getSocket() != null) {
  130 + socketToAddrMap.put(client.getSocket(), deviceAddr);
  131 + }
  132 +
  133 + // 如果客户端已设置设备ID,更新映射
  134 + if (client.getDeviceId() != null) {
  135 + deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr);
  136 + }
  137 +
  138 + totalConnections.incrementAndGet();
  139 + return true;
  140 +
  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 + /**
  212 + * 通过网络连接获取客户端
  213 + */
  214 + public TcpDeviceClient getClientBySocket(NetSocket socket) {
  215 + readLock.lock();
  216 + try {
  217 + String deviceAddr = socketToAddrMap.get(socket);
  218 + return deviceAddr != null ? clientMap.get(deviceAddr) : null;
  219 + } finally {
  220 + readLock.unlock();
  221 + }
  222 + }
  223 +
  224 + /**
  225 + * 检查设备是否在线
  226 + */
  227 + public boolean isDeviceOnline(Long deviceId) {
  228 + TcpDeviceClient client = getClientByDeviceId(deviceId);
  229 + return client != null && client.isOnline();
  230 + }
  231 +
  232 + /**
  233 + * 设置设备ID映射
  234 + */
  235 + public void setDeviceIdMapping(String deviceAddr, Long deviceId) {
  236 + writeLock.lock();
  237 + try {
  238 + TcpDeviceClient client = clientMap.get(deviceAddr);
  239 + if (client != null) {
  240 + client.setDeviceId(deviceId);
  241 + deviceIdToAddrMap.put(deviceId, deviceAddr);
  242 + log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId);
  243 + }
  244 + } finally {
  245 + writeLock.unlock();
  246 + }
  247 + }
  248 +
  249 + /**
  250 + * 发送消息给设备
  251 + */
  252 + public boolean sendMessage(String deviceAddr, Buffer buffer) {
  253 + TcpDeviceClient client = getClient(deviceAddr);
  254 + if (client != null && client.isOnline()) {
  255 + try {
  256 + client.sendMessage(buffer);
  257 + totalMessages.incrementAndGet();
  258 + totalBytes.addAndGet(buffer.length());
  259 + return true;
  260 + } catch (Exception e) {
  261 + totalFailedMessages.incrementAndGet();
  262 + log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e);
  263 + return false;
  264 + }
  265 + }
  266 + log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr);
  267 + return false;
  268 + }
  269 +
  270 + /**
  271 + * 通过设备ID发送消息
  272 + */
  273 + public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) {
  274 + TcpDeviceClient client = getClientByDeviceId(deviceId);
  275 + if (client != null && client.isOnline()) {
  276 + try {
  277 + client.sendMessage(buffer);
  278 + totalMessages.incrementAndGet();
  279 + totalBytes.addAndGet(buffer.length());
  280 + return true;
  281 + } catch (Exception e) {
  282 + totalFailedMessages.incrementAndGet();
  283 + log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e);
  284 + return false;
  285 + }
  286 + }
  287 + log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId);
  288 + return false;
  289 + }
  290 +
  291 + /**
  292 + * 广播消息给所有在线设备
  293 + */
  294 + public int broadcastMessage(Buffer buffer) {
  295 + int successCount = 0;
  296 + readLock.lock();
  297 + try {
  298 + for (TcpDeviceClient client : clientMap.values()) {
  299 + if (client.isOnline()) {
  300 + try {
  301 + client.sendMessage(buffer);
  302 + successCount++;
  303 + } catch (Exception e) {
  304 + log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e);
  305 + }
  306 + }
  307 + }
  308 + } finally {
  309 + readLock.unlock();
  310 + }
  311 +
  312 + totalMessages.addAndGet(successCount);
  313 + totalBytes.addAndGet((long) successCount * buffer.length());
  314 + return successCount;
  315 + }
  316 +
  317 + /**
  318 + * 获取在线设备数量
  319 + */
  320 + public int getOnlineCount() {
  321 + readLock.lock();
  322 + try {
  323 + return (int) clientMap.values().stream()
  324 + .filter(TcpDeviceClient::isOnline)
  325 + .count();
  326 + } finally {
  327 + readLock.unlock();
  328 + }
  329 + }
  330 +
  331 + /**
  332 + * 获取总连接数
  333 + */
  334 + public int getTotalCount() {
  335 + return clientMap.size();
  336 + }
  337 +
  338 + /**
  339 + * 获取认证设备数量
  340 + */
  341 + public int getAuthenticatedCount() {
  342 + readLock.lock();
  343 + try {
  344 + return (int) clientMap.values().stream()
  345 + .filter(TcpDeviceClient::isAuthenticated)
  346 + .count();
  347 + } finally {
  348 + readLock.unlock();
  349 + }
  350 + }
  351 +
  352 + /**
  353 + * 心跳检查任务
  354 + */
  355 + private void checkHeartbeat() {
  356 + try {
  357 + long currentTime = System.currentTimeMillis();
  358 + int offlineCount = 0;
  359 +
  360 + readLock.lock();
  361 + try {
  362 + for (TcpDeviceClient client : clientMap.values()) {
  363 + if (!client.isOnline()) {
  364 + offlineCount++;
  365 + }
  366 + }
  367 + } finally {
  368 + readLock.unlock();
  369 + }
  370 +
  371 + if (offlineCount > 0) {
  372 + log.info("[checkHeartbeat][发现{}个离线设备,将在清理任务中处理]", offlineCount);
  373 + }
  374 + } catch (Exception e) {
  375 + log.error("[checkHeartbeat][心跳检查任务异常]", e);
  376 + }
  377 + }
  378 +
  379 + /**
  380 + * 连接清理任务
  381 + */
  382 + private void cleanupConnections() {
  383 + try {
  384 + int beforeSize = clientMap.size();
  385 +
  386 + writeLock.lock();
  387 + try {
  388 + clientMap.entrySet().removeIf(entry -> {
  389 + TcpDeviceClient client = entry.getValue();
  390 + if (!client.isOnline()) {
  391 + log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey());
  392 +
  393 + // 清理相关映射
  394 + if (client.getSocket() != null) {
  395 + socketToAddrMap.remove(client.getSocket());
  396 + }
  397 + if (client.getDeviceId() != null) {
  398 + deviceIdToAddrMap.remove(client.getDeviceId());
  399 + }
  400 +
  401 + client.shutdown();
  402 + totalDisconnections.incrementAndGet();
  403 + return true;
  404 + }
  405 + return false;
  406 + });
  407 + } finally {
  408 + writeLock.unlock();
  409 + }
  410 +
  411 + int afterSize = clientMap.size();
  412 + if (beforeSize != afterSize) {
  413 + log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}",
  414 + beforeSize, afterSize, beforeSize - afterSize);
  415 + }
  416 + } catch (Exception e) {
  417 + log.error("[cleanupConnections][连接清理任务异常]", e);
  418 + }
  419 + }
  420 +
  421 + /**
  422 + * 统计日志任务
  423 + */
  424 + private void logStatistics() {
  425 + try {
  426 + long totalConn = totalConnections.get();
  427 + long totalDisconn = totalDisconnections.get();
  428 + long totalMsg = totalMessages.get();
  429 + long totalFailedMsg = totalFailedMessages.get();
  430 + long totalBytesValue = totalBytes.get();
  431 +
  432 + log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " +
  433 + "总消息: {}, 失败消息: {}, 总字节: {}",
  434 + totalConn, totalDisconn, getOnlineCount(), getAuthenticatedCount(),
  435 + totalMsg, totalFailedMsg, totalBytesValue);
  436 + } catch (Exception e) {
  437 + log.error("[logStatistics][统计日志任务异常]", e);
  438 + }
  439 + }
  440 +
  441 + /**
  442 + * 关闭连接管理器
  443 + */
  444 + public void shutdown() {
  445 + log.info("[shutdown][关闭TCP连接管理器]");
  446 +
  447 + // 关闭定时任务
  448 + scheduler.shutdown();
  449 + try {
  450 + if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
  451 + scheduler.shutdownNow();
  452 + }
  453 + } catch (InterruptedException e) {
  454 + scheduler.shutdownNow();
  455 + Thread.currentThread().interrupt();
  456 + }
  457 +
  458 + // 关闭所有连接
  459 + writeLock.lock();
  460 + try {
  461 + clientMap.values().forEach(TcpDeviceClient::shutdown);
  462 + clientMap.clear();
  463 + deviceIdToAddrMap.clear();
  464 + socketToAddrMap.clear();
  465 + } finally {
  466 + writeLock.unlock();
  467 + }
  468 + }
  469 +
  470 + /**
  471 + * 获取连接状态信息
  472 + */
  473 + public String getConnectionStatus() {
  474 + return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%",
  475 + getTotalCount(), getOnlineCount(), getAuthenticatedCount(),
  476 + totalMessages.get() > 0
  477 + ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100
  478 + : 0.0);
  479 + }
  480 +
  481 + /**
  482 + * 获取详细统计信息
  483 + */
  484 + public String getDetailedStatistics() {
  485 + return String.format(
  486 + "TCP连接管理器统计:\n" +
  487 + "- 当前连接数: %d\n" +
  488 + "- 在线设备数: %d\n" +
  489 + "- 认证设备数: %d\n" +
  490 + "- 历史总连接: %d\n" +
  491 + "- 历史总断开: %d\n" +
  492 + "- 总消息数: %d\n" +
  493 + "- 失败消息数: %d\n" +
  494 + "- 总字节数: %d\n" +
  495 + "- 消息成功率: %.2f%%",
  496 + getTotalCount(), getOnlineCount(), getAuthenticatedCount(),
  497 + totalConnections.get(), totalDisconnections.get(),
  498 + totalMessages.get(), totalFailedMessages.get(), totalBytes.get(),
  499 + totalMessages.get() > 0
  500 + ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100
  501 + : 0.0);
  502 + }
  503 +}
0 \ No newline at end of file 504 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;  
2 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java 0 → 100644
  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 TcpDataDecoder {
  18 +
  19 + /**
  20 + * 解码数据包
  21 + *
  22 + * @param buffer 数据缓冲区
  23 + * @return 解码后的数据包
  24 + * @throws IllegalArgumentException 如果数据包格式不正确
  25 + */
  26 + public static TcpDataPackage decode(Buffer buffer) {
  27 + if (buffer == null || buffer.length() < 8) {
  28 + throw new IllegalArgumentException("数据包长度不足");
  29 + }
  30 +
  31 + try {
  32 + int index = 0;
  33 +
  34 + // 1. 获取设备地址长度(2字节)
  35 + short addrLength = buffer.getShort(index);
  36 + index += 2;
  37 +
  38 + // 2. 校验数据包长度
  39 + int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号
  40 + if (buffer.length() < expectedLength) {
  41 + throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节");
  42 + }
  43 +
  44 + // 3. 获取设备地址
  45 + String addr = buffer.getBuffer(index, index + addrLength).toString();
  46 + index += addrLength;
  47 +
  48 + // 4. 获取功能码(2字节)
  49 + short code = buffer.getShort(index);
  50 + index += 2;
  51 +
  52 + // 5. 获取消息序号(2字节)
  53 + short mid = buffer.getShort(index);
  54 + index += 2;
  55 +
  56 + // 6. 获取包体数据
  57 + String payload = "";
  58 + if (index < buffer.length()) {
  59 + payload = buffer.getString(index, buffer.length());
  60 + }
  61 +
  62 + // 7. 构建数据包对象
  63 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  64 + .addrLength((int) addrLength)
  65 + .addr(addr)
  66 + .code(code)
  67 + .mid(mid)
  68 + .payload(payload)
  69 + .build();
  70 +
  71 + log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}",
  72 + addr, dataPackage.getCodeDescription(), mid, payload.length());
  73 +
  74 + return dataPackage;
  75 +
  76 + } catch (Exception e) {
  77 + log.error("[decode][解码失败] 数据: {}", buffer.toString(), e);
  78 + throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e);
  79 + }
  80 + }
  81 +
  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 +}
0 \ No newline at end of file 98 \ 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 0 → 100644
  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 +
  31 + if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) {
  32 + throw new IllegalArgumentException("设备地址不能为空");
  33 + }
  34 +
  35 + if (dataPackage.getPayload() == null) {
  36 + dataPackage.setPayload("");
  37 + }
  38 +
  39 + try {
  40 + Buffer buffer = Buffer.buffer();
  41 +
  42 + // 1. 计算包体长度(除了包头4字节)
  43 + int payloadLength = dataPackage.getPayload().getBytes().length;
  44 + int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength;
  45 +
  46 + // 2. 写入包头:总长度(4字节)
  47 + buffer.appendInt(totalLength);
  48 +
  49 + // 3. 写入设备地址长度(2字节)
  50 + buffer.appendShort((short) dataPackage.getAddr().length());
  51 +
  52 + // 4. 写入设备地址(不定长)
  53 + buffer.appendBytes(dataPackage.getAddr().getBytes());
  54 +
  55 + // 5. 写入功能码(2字节)
  56 + buffer.appendShort(dataPackage.getCode());
  57 +
  58 + // 6. 写入消息序号(2字节)
  59 + buffer.appendShort(dataPackage.getMid());
  60 +
  61 + // 7. 写入包体数据(不定长)
  62 + buffer.appendBytes(dataPackage.getPayload().getBytes());
  63 +
  64 + log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}",
  65 + dataPackage.getAddr(), dataPackage.getCodeDescription(),
  66 + dataPackage.getMid(), buffer.length());
  67 +
  68 + return buffer;
  69 +
  70 + } catch (Exception e) {
  71 + log.error("[encode][编码失败] 数据包: {}", dataPackage, e);
  72 + throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e);
  73 + }
  74 + }
  75 +
  76 + /**
  77 + * 创建注册回复数据包
  78 + *
  79 + * @param addr 设备地址
  80 + * @param mid 消息序号
  81 + * @param success 是否成功
  82 + * @return 编码后的数据包
  83 + */
  84 + public static Buffer createRegisterReply(String addr, short mid, boolean success) {
  85 + String payload = success ? "0" : "1"; // 0表示成功,1表示失败
  86 +
  87 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  88 + .addr(addr)
  89 + .code(TcpDataPackage.CODE_REGISTER_REPLY)
  90 + .mid(mid)
  91 + .payload(payload)
  92 + .build();
  93 +
  94 + return encode(dataPackage);
  95 + }
  96 +
  97 + /**
  98 + * 创建数据下发数据包
  99 + *
  100 + * @param addr 设备地址
  101 + * @param mid 消息序号
  102 + * @param data 下发数据
  103 + * @return 编码后的数据包
  104 + */
  105 + public static Buffer createDataDownPackage(String addr, short mid, String data) {
  106 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  107 + .addr(addr)
  108 + .code(TcpDataPackage.CODE_DATA_DOWN)
  109 + .mid(mid)
  110 + .payload(data)
  111 + .build();
  112 +
  113 + return encode(dataPackage);
  114 + }
  115 +
  116 + /**
  117 + * 创建服务调用数据包
  118 + *
  119 + * @param addr 设备地址
  120 + * @param mid 消息序号
  121 + * @param serviceData 服务数据
  122 + * @return 编码后的数据包
  123 + */
  124 + public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) {
  125 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  126 + .addr(addr)
  127 + .code(TcpDataPackage.CODE_SERVICE_INVOKE)
  128 + .mid(mid)
  129 + .payload(serviceData)
  130 + .build();
  131 +
  132 + return encode(dataPackage);
  133 + }
  134 +
  135 + /**
  136 + * 创建属性设置数据包
  137 + *
  138 + * @param addr 设备地址
  139 + * @param mid 消息序号
  140 + * @param propertyData 属性数据
  141 + * @return 编码后的数据包
  142 + */
  143 + public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) {
  144 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  145 + .addr(addr)
  146 + .code(TcpDataPackage.CODE_PROPERTY_SET)
  147 + .mid(mid)
  148 + .payload(propertyData)
  149 + .build();
  150 +
  151 + return encode(dataPackage);
  152 + }
  153 +
  154 + /**
  155 + * 创建属性获取数据包
  156 + *
  157 + * @param addr 设备地址
  158 + * @param mid 消息序号
  159 + * @param propertyNames 属性名称列表
  160 + * @return 编码后的数据包
  161 + */
  162 + public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) {
  163 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  164 + .addr(addr)
  165 + .code(TcpDataPackage.CODE_PROPERTY_GET)
  166 + .mid(mid)
  167 + .payload(propertyNames)
  168 + .build();
  169 +
  170 + return encode(dataPackage);
  171 + }
  172 +}
0 \ No newline at end of file 173 \ 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 0 → 100644
  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 + /**
  33 + * 心跳
  34 + */
  35 + public static final short CODE_HEARTBEAT = 20;
  36 + /**
  37 + * 数据上报
  38 + */
  39 + public static final short CODE_DATA_UP = 30;
  40 + /**
  41 + * 事件上报
  42 + */
  43 + public static final short CODE_EVENT_UP = 40;
  44 + /**
  45 + * 数据下发
  46 + */
  47 + public static final short CODE_DATA_DOWN = 50;
  48 + /**
  49 + * 服务调用
  50 + */
  51 + public static final short CODE_SERVICE_INVOKE = 60;
  52 + /**
  53 + * 属性设置
  54 + */
  55 + public static final short CODE_PROPERTY_SET = 70;
  56 + /**
  57 + * 属性获取
  58 + */
  59 + public static final short CODE_PROPERTY_GET = 80;
  60 +
  61 + // ==================== 数据包字段 ====================
  62 +
  63 + /**
  64 + * 设备地址长度
  65 + */
  66 + private Integer addrLength;
  67 +
  68 + /**
  69 + * 设备地址
  70 + */
  71 + private String addr;
  72 +
  73 + /**
  74 + * 功能码
  75 + */
  76 + private short code;
  77 +
  78 + /**
  79 + * 消息序号
  80 + */
  81 + private short mid;
  82 +
  83 + /**
  84 + * 包体数据
  85 + */
  86 + private String payload;
  87 +
  88 + // ==================== 辅助方法 ====================
  89 +
  90 + /**
  91 + * 是否为注册消息
  92 + */
  93 + public boolean isRegisterMessage() {
  94 + return code == CODE_REGISTER;
  95 + }
  96 +
  97 + /**
  98 + * 是否为心跳消息
  99 + */
  100 + public boolean isHeartbeatMessage() {
  101 + return code == CODE_HEARTBEAT;
  102 + }
  103 +
  104 + /**
  105 + * 是否为数据上报消息
  106 + */
  107 + public boolean isDataUpMessage() {
  108 + return code == CODE_DATA_UP;
  109 + }
  110 +
  111 + /**
  112 + * 是否为事件上报消息
  113 + */
  114 + public boolean isEventUpMessage() {
  115 + return code == CODE_EVENT_UP;
  116 + }
  117 +
  118 + /**
  119 + * 是否为下行消息
  120 + */
  121 + public boolean isDownstreamMessage() {
  122 + return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE ||
  123 + code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET;
  124 + }
  125 +
  126 + /**
  127 + * 获取功能码描述
  128 + */
  129 + public String getCodeDescription() {
  130 + switch (code) {
  131 + case CODE_REGISTER:
  132 + return "设备注册";
  133 + case CODE_REGISTER_REPLY:
  134 + return "注册回复";
  135 + case CODE_HEARTBEAT:
  136 + return "心跳";
  137 + case CODE_DATA_UP:
  138 + return "数据上报";
  139 + case CODE_EVENT_UP:
  140 + return "事件上报";
  141 + case CODE_DATA_DOWN:
  142 + return "数据下发";
  143 + case CODE_SERVICE_INVOKE:
  144 + return "服务调用";
  145 + case CODE_PROPERTY_SET:
  146 + return "属性设置";
  147 + case CODE_PROPERTY_GET:
  148 + return "属性获取";
  149 + default:
  150 + return "未知功能码";
  151 + }
  152 + }
  153 +}
0 \ No newline at end of file 154 \ 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 0 → 100644
  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 + // 校验数据长度
  47 + if (dataLength <= 0 || dataLength > 1024 * 1024) { // 最大1MB
  48 + log.error("[handle][无效的数据包长度: {}]", dataLength);
  49 + reset();
  50 + return;
  51 + }
  52 +
  53 + // 切换到读取数据模式
  54 + parser.fixedSizeMode(dataLength);
  55 +
  56 + log.debug("[handle][读取到数据包长度: {}]", dataLength);
  57 + } else {
  58 + // 读取到完整的数据包
  59 + log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length());
  60 +
  61 + // 处理数据包
  62 + try {
  63 + receiveHandler.accept(buffer);
  64 + } catch (Exception e) {
  65 + log.error("[handle][处理数据包失败]", e);
  66 + }
  67 +
  68 + // 重置状态,准备读取下一个数据包
  69 + reset();
  70 + }
  71 + } catch (Exception e) {
  72 + log.error("[handle][数据包处理异常]", e);
  73 + reset();
  74 + }
  75 + }
  76 +
  77 + /**
  78 + * 重置解析器状态
  79 + */
  80 + private void reset() {
  81 + dataLength = -1;
  82 + parser.fixedSizeMode(4);
  83 + }
  84 + });
  85 +
  86 + return parser;
  87 + }
  88 +
  89 + /**
  90 + * 创建带异常处理的数据包解析器
  91 + *
  92 + * @param receiveHandler 接收处理器
  93 + * @param exceptionHandler 异常处理器
  94 + * @return RecordParser 解析器
  95 + */
  96 + public static RecordParser createParserWithExceptionHandler(
  97 + Consumer<Buffer> receiveHandler,
  98 + Consumer<Throwable> exceptionHandler) {
  99 +
  100 + RecordParser parser = RecordParser.newFixed(4);
  101 +
  102 + parser.setOutput(new Handler<Buffer>() {
  103 + private int dataLength = -1;
  104 +
  105 + @Override
  106 + public void handle(Buffer buffer) {
  107 + try {
  108 + if (dataLength == -1) {
  109 + dataLength = buffer.getInt(0);
  110 +
  111 + if (dataLength <= 0 || dataLength > 1024 * 1024) {
  112 + throw new IllegalArgumentException("无效的数据包长度: " + dataLength);
  113 + }
  114 +
  115 + parser.fixedSizeMode(dataLength);
  116 + log.debug("[handle][读取到数据包长度: {}]", dataLength);
  117 + } else {
  118 + log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length());
  119 +
  120 + try {
  121 + receiveHandler.accept(buffer);
  122 + } catch (Exception e) {
  123 + exceptionHandler.accept(e);
  124 + }
  125 +
  126 + reset();
  127 + }
  128 + } catch (Exception e) {
  129 + exceptionHandler.accept(e);
  130 + reset();
  131 + }
  132 + }
  133 +
  134 + private void reset() {
  135 + dataLength = -1;
  136 + parser.fixedSizeMode(4);
  137 + }
  138 + });
  139 +
  140 + return parser;
  141 + }
  142 +
  143 + /**
  144 + * 创建简单的数据包解析器(用于测试)
  145 + *
  146 + * @param receiveHandler 接收处理器
  147 + * @return RecordParser 解析器
  148 + */
  149 + public static RecordParser createSimpleParser(Consumer<TcpDataPackage> receiveHandler) {
  150 + return createParser(buffer -> {
  151 + try {
  152 + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
  153 + receiveHandler.accept(dataPackage);
  154 + } catch (Exception e) {
  155 + log.error("[createSimpleParser][解码数据包失败]", e);
  156 + }
  157 + });
  158 + }
  159 +}
0 \ No newline at end of file 160 \ 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/IotTcpConnectionHandler.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;  
2 -  
3 -import cn.hutool.core.util.BooleanUtil;  
4 -import cn.iocoder.yudao.framework.common.pojo.CommonResult;  
5 -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;  
6 -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;  
7 -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;  
8 -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;  
9 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager;  
10 -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;  
11 -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;  
12 -import io.vertx.core.Handler;  
13 -import io.vertx.core.buffer.Buffer;  
14 -import io.vertx.core.net.NetSocket;  
15 -import io.vertx.core.parsetools.RecordParser;  
16 -import lombok.RequiredArgsConstructor;  
17 -import lombok.extern.slf4j.Slf4j;  
18 -  
19 -/**  
20 - * IoT TCP 连接处理器  
21 - * <p>  
22 - * 核心负责:  
23 - * 1. 【认证】创建连接后,设备需要发送认证消息,认证通过后,才能进行后续的通信  
24 - * 2. 【消息处理】接收设备发送的消息,解码后,发送到消息队列  
25 - * 3. 【断开】设备断开连接后,清理资源  
26 - *  
27 - * @author 芋道源码  
28 - */  
29 -@RequiredArgsConstructor  
30 -@Slf4j  
31 -public class IotTcpConnectionHandler implements Handler<Buffer> {  
32 -  
33 - private final NetSocket socket;  
34 - /**  
35 - * 是否已认证  
36 - */  
37 - private boolean authenticated = false;  
38 - /**  
39 - * 设备信息  
40 - */  
41 - private IotDeviceRespDTO device;  
42 -  
43 - private final IotTcpConnectionManager connectionManager;  
44 -  
45 - private final IotDeviceMessageService messageService;  
46 -  
47 - private final IotDeviceService deviceService;  
48 -  
49 - private final IotDeviceCommonApi deviceApi;  
50 -  
51 - private final String serverId;  
52 -  
53 - public void start() {  
54 - // 1. 设置解析器  
55 - final RecordParser parser = RecordParser.newDelimited("\n", this);  
56 - socket.handler(parser);  
57 -  
58 - // 2. 设置处理器  
59 - socket.closeHandler(v -> handleConnectionClose());  
60 - socket.exceptionHandler(this::handleException);  
61 - }  
62 -  
63 - @Override  
64 - public void handle(Buffer buffer) {  
65 - log.info("[handle][接收到数据: {}]", buffer);  
66 - try {  
67 - // TODO @haohao:可以调研下,做个对比表格哈;  
68 - // 1. 处理认证  
69 - if (!authenticated) {  
70 - handleAuthentication(buffer);  
71 - return;  
72 - }  
73 - // 2. 处理消息  
74 - handleMessage(buffer);  
75 - } catch (Exception e) {  
76 - log.error("[handle][处理异常]", e);  
77 - socket.close();  
78 - }  
79 - }  
80 -  
81 - private void handleAuthentication(Buffer buffer) {  
82 - // 1. 解析认证信息  
83 - // TODO @芋艿:这里的认证协议,需要和设备端约定。默认为 productKey,deviceName,password  
84 - // TODO @haohao:这里,要不也 json 解析?类似 http 是 {  
85 - // "clientId": "4aymZgOTOOCrDKRT.small",  
86 - // "username": "small&4aymZgOTOOCrDKRT",  
87 - // "password": "509e2b08f7598eb139d276388c600435913ba4c94cd0d50aebc5c0d1855bcb75"  
88 - //}  
89 - String[] parts = buffer.toString().split(",");  
90 - if (parts.length != 3) {  
91 - log.error("[handleAuthentication][认证信息({})格式不正确]", buffer);  
92 - socket.close();  
93 - return;  
94 - }  
95 - String productKey = parts[0];  
96 - String deviceName = parts[1];  
97 - String password = parts[2];  
98 -  
99 - // 2. 执行认证  
100 - CommonResult<Boolean> authResult = deviceApi.authDevice(new IotDeviceAuthReqDTO()  
101 - .setClientId(socket.remoteAddress().toString()).setUsername(productKey + "/" + deviceName)  
102 - .setPassword(password));  
103 - if (authResult.isError() || !BooleanUtil.isTrue(authResult.getData())) {  
104 - log.error("[handleAuthentication][认证失败,productKey({}) deviceName({}) password({})]", productKey, deviceName,  
105 - password);  
106 - socket.close();  
107 - return;  
108 - }  
109 -  
110 - // 3. 认证成功  
111 - this.authenticated = true;  
112 - this.device = deviceService.getDeviceFromCache(productKey, deviceName);  
113 - connectionManager.addConnection(String.valueOf(device.getId()), socket);  
114 -  
115 - // 4. 发送上线消息  
116 - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();  
117 - messageService.sendDeviceMessage(message, productKey, deviceName, serverId);  
118 - log.info("[handleAuthentication][认证成功]");  
119 - }  
120 -  
121 - private void handleMessage(Buffer buffer) {  
122 - // 1. 解码消息  
123 - IotDeviceMessage message = messageService.decodeDeviceMessage(buffer.getBytes(),  
124 - device.getProductKey(), device.getDeviceName());  
125 - if (message == null) {  
126 - log.warn("[handleMessage][解码消息失败]");  
127 - return;  
128 - }  
129 - // 2. 发送消息到队列  
130 - messageService.sendDeviceMessage(message, device.getProductKey(), device.getDeviceName(), serverId);  
131 - }  
132 -  
133 - private void handleConnectionClose() {  
134 - // 1. 移除连接  
135 - connectionManager.removeConnection(socket);  
136 - // 2. 发送离线消息  
137 - if (device != null) {  
138 - IotDeviceMessage message = IotDeviceMessage.buildStateOffline();  
139 - messageService.sendDeviceMessage(message, device.getProductKey(), device.getDeviceName(), serverId);  
140 - }  
141 - }  
142 -  
143 - private void handleException(Throwable e) {  
144 - log.error("[handleException][连接({}) 发生异常]", socket.remoteAddress(), e);  
145 - socket.close();  
146 - }  
147 -  
148 -}  
149 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;  
2 -  
3 -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;  
4 -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager;  
5 -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;  
6 -import io.vertx.core.buffer.Buffer;  
7 -import io.vertx.core.net.NetSocket;  
8 -import lombok.RequiredArgsConstructor;  
9 -import lombok.extern.slf4j.Slf4j;  
10 -import org.springframework.stereotype.Component;  
11 -  
12 -/**  
13 - * IoT 网关 TCP 下行消息处理器  
14 - * <p>  
15 - * 从消息总线接收到下行消息,然后发布到 TCP 连接,从而被设备所接收  
16 - *  
17 - * @author 芋道源码  
18 - */  
19 -@Slf4j  
20 -@Component  
21 -@RequiredArgsConstructor  
22 -public class IotTcpDownstreamHandler {  
23 -  
24 - private final IotTcpConnectionManager connectionManager;  
25 -  
26 - private final IotDeviceMessageService messageService;  
27 -  
28 - /**  
29 - * 处理下行消息  
30 - *  
31 - * @param message 设备消息  
32 - */  
33 - public void handle(IotDeviceMessage message) {  
34 - // 1. 获取设备对应的连接  
35 - NetSocket socket = connectionManager.getConnection(String.valueOf(message.getDeviceId()));  
36 - if (socket == null) {  
37 - log.error("[handle][设备({})的连接不存在]", message.getDeviceId());  
38 - return;  
39 - }  
40 -  
41 - // 2. 编码消息  
42 - byte[] bytes = messageService.encodeDeviceMessage(message, null, null);  
43 -  
44 - // 3. 发送消息  
45 - socket.write(Buffer.buffer(bytes));  
46 - // TODO @芋艿:这里的换行符,需要和设备端约定  
47 - // TODO @haohao:tcp 要不定长?很少 \n 哈。然后有个 magic number;可以参考 dubbo rpc;  
48 - socket.write("\n");  
49 - }  
50 - 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +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;
  10 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  11 +import com.alibaba.fastjson.JSON;
  12 +import io.vertx.core.buffer.Buffer;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +/**
  16 + * IoT 网关 TCP 下行消息处理器
  17 + * <p>
  18 + * 负责处理从业务系统发送到设备的下行消息,包括:
  19 + * 1. 属性设置
  20 + * 2. 服务调用
  21 + * 3. 属性获取
  22 + * 4. 配置下发
  23 + * 5. OTA 升级
  24 + *
  25 + * @author 芋道源码
  26 + */
  27 +@Slf4j
  28 +public class IotTcpDownstreamHandler {
  29 +
  30 + private final TcpDeviceConnectionManager connectionManager;
  31 +
  32 + private final IotDeviceMessageService messageService;
  33 +
  34 + public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager,
  35 + IotDeviceMessageService messageService) {
  36 + this.connectionManager = connectionManager;
  37 + this.messageService = messageService;
  38 + }
  39 +
  40 + /**
  41 + * 处理下行消息
  42 + *
  43 + * @param message 设备消息
  44 + */
  45 + public void handle(IotDeviceMessage message) {
  46 + try {
  47 + log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}",
  48 + message.getDeviceId(), message.getMethod(), message.getId());
  49 +
  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 + switch (message.getMethod()) {
  59 + case "thing.property.set":
  60 + handlePropertySet(client, message);
  61 + break;
  62 + case "thing.property.get":
  63 + handlePropertyGet(client, message);
  64 + break;
  65 + case "thing.service.invoke":
  66 + handleServiceInvoke(client, message);
  67 + break;
  68 + case "thing.config.push":
  69 + handleConfigPush(client, message);
  70 + break;
  71 + case "thing.ota.upgrade":
  72 + handleOtaUpgrade(client, message);
  73 + break;
  74 + default:
  75 + log.warn("[handle][未知的下行消息方法: {}]", message.getMethod());
  76 + break;
  77 + }
  78 +
  79 + } catch (Exception e) {
  80 + log.error("[handle][处理下行消息失败]", e);
  81 + }
  82 + }
  83 +
  84 + /**
  85 + * 处理属性设置
  86 + *
  87 + * @param client 设备客户端
  88 + * @param message 设备消息
  89 + */
  90 + private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) {
  91 + try {
  92 + log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}",
  93 + client.getDeviceAddr(), message.getParams());
  94 +
  95 + // 使用编解码器发送消息,降级处理使用原始编码
  96 + sendMessageWithCodec(client, message, "handlePropertySet", () -> {
  97 + String payload = JSON.toJSONString(message.getParams());
  98 + short mid = generateMessageId();
  99 +
  100 + Buffer buffer = TcpDataEncoder.createPropertySetPackage(
  101 + client.getDeviceAddr(), mid, payload);
  102 + client.sendMessage(buffer);
  103 +
  104 + log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}",
  105 + client.getDeviceAddr(), mid);
  106 + });
  107 +
  108 + } catch (Exception e) {
  109 + log.error("[handlePropertySet][属性设置失败]", e);
  110 + }
  111 + }
  112 +
  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 +
  137 + } catch (Exception e) {
  138 + log.error("[handlePropertyGet][属性获取失败]", e);
  139 + }
  140 + }
  141 +
  142 + /**
  143 + * 处理服务调用
  144 + *
  145 + * @param client 设备客户端
  146 + * @param message 设备消息
  147 + */
  148 + private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) {
  149 + try {
  150 + log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}",
  151 + client.getDeviceAddr(), message.getParams());
  152 +
  153 + // 1. 构建服务调用数据包
  154 + String payload = JSON.toJSONString(message.getParams());
  155 + short mid = generateMessageId();
  156 +
  157 + Buffer buffer = TcpDataEncoder.createServiceInvokePackage(
  158 + client.getDeviceAddr(), mid, payload);
  159 +
  160 + // 2. 发送消息
  161 + client.sendMessage(buffer);
  162 +
  163 + log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}",
  164 + client.getDeviceAddr(), mid);
  165 +
  166 + } catch (Exception e) {
  167 + log.error("[handleServiceInvoke][服务调用失败]", e);
  168 + }
  169 + }
  170 +
  171 + /**
  172 + * 处理配置推送
  173 + *
  174 + * @param client 设备客户端
  175 + * @param message 设备消息
  176 + */
  177 + private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) {
  178 + try {
  179 + log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}",
  180 + client.getDeviceAddr(), message.getParams());
  181 +
  182 + // 1. 构建配置推送数据包
  183 + String payload = JSON.toJSONString(message.getParams());
  184 + short mid = generateMessageId();
  185 +
  186 + Buffer buffer = TcpDataEncoder.createDataDownPackage(
  187 + client.getDeviceAddr(), mid, payload);
  188 +
  189 + // 2. 发送消息
  190 + client.sendMessage(buffer);
  191 +
  192 + log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}",
  193 + client.getDeviceAddr(), mid);
  194 +
  195 + } catch (Exception e) {
  196 + log.error("[handleConfigPush][配置推送失败]", e);
  197 + }
  198 + }
  199 +
  200 + /**
  201 + * 处理 OTA 升级
  202 + *
  203 + * @param client 设备客户端
  204 + * @param message 设备消息
  205 + */
  206 + private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) {
  207 + try {
  208 + log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}",
  209 + client.getDeviceAddr(), message.getParams());
  210 +
  211 + // 1. 构建 OTA 升级数据包
  212 + String payload = JSON.toJSONString(message.getParams());
  213 + short mid = generateMessageId();
  214 +
  215 + Buffer buffer = TcpDataEncoder.createDataDownPackage(
  216 + client.getDeviceAddr(), mid, payload);
  217 +
  218 + // 2. 发送消息
  219 + client.sendMessage(buffer);
  220 +
  221 + log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}",
  222 + client.getDeviceAddr(), mid);
  223 +
  224 + } catch (Exception e) {
  225 + log.error("[handleOtaUpgrade][OTA升级失败]", e);
  226 + }
  227 + }
  228 +
  229 + /**
  230 + * 处理自定义下行消息
  231 + *
  232 + * @param client 设备客户端
  233 + * @param message 设备消息
  234 + * @param code 功能码
  235 + */
  236 + private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) {
  237 + try {
  238 + log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}",
  239 + client.getDeviceAddr(), code, message.getParams());
  240 +
  241 + // 1. 构建自定义数据包
  242 + String payload = JSON.toJSONString(message.getParams());
  243 + short mid = generateMessageId();
  244 +
  245 + TcpDataPackage dataPackage = TcpDataPackage.builder()
  246 + .addr(client.getDeviceAddr())
  247 + .code(code)
  248 + .mid(mid)
  249 + .payload(payload)
  250 + .build();
  251 +
  252 + Buffer buffer = TcpDataEncoder.encode(dataPackage);
  253 +
  254 + // 2. 发送消息
  255 + client.sendMessage(buffer);
  256 +
  257 + log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}",
  258 + client.getDeviceAddr(), code, mid);
  259 +
  260 + } catch (Exception e) {
  261 + log.error("[handleCustomMessage][自定义消息发送失败]", e);
  262 + }
  263 + }
  264 +
  265 + /**
  266 + * 批量发送下行消息
  267 + *
  268 + * @param deviceIds 设备ID列表
  269 + * @param message 设备消息
  270 + */
  271 + public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) {
  272 + try {
  273 + log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}",
  274 + deviceIds.length, message.getMethod());
  275 +
  276 + for (Long deviceId : deviceIds) {
  277 + // 创建副本消息(避免消息ID冲突)
  278 + IotDeviceMessage copyMessage = IotDeviceMessage.of(
  279 + message.getRequestId(),
  280 + message.getMethod(),
  281 + message.getParams(),
  282 + message.getData(),
  283 + message.getCode(),
  284 + message.getMsg());
  285 + copyMessage.setDeviceId(deviceId);
  286 +
  287 + // 处理单个设备消息
  288 + handle(copyMessage);
  289 + }
  290 +
  291 + } catch (Exception e) {
  292 + log.error("[broadcastMessage][批量发送消息失败]", e);
  293 + }
  294 + }
  295 +
  296 + /**
  297 + * 检查设备是否支持指定方法
  298 + *
  299 + * @param client 设备客户端
  300 + * @param method 消息方法
  301 + * @return 是否支持
  302 + */
  303 + private boolean isMethodSupported(TcpDeviceClient client, String method) {
  304 + // TODO: 可以根据设备类型或产品信息判断是否支持特定方法
  305 + return IotDeviceMessageMethodEnum.of(method) != null;
  306 + }
  307 +
  308 + /**
  309 + * 生成消息序号
  310 + *
  311 + * @return 消息序号
  312 + */
  313 + private short generateMessageId() {
  314 + return (short) (System.currentTimeMillis() % Short.MAX_VALUE);
  315 + }
  316 +
  317 + /**
  318 + * 使用编解码器发送消息
  319 + *
  320 + * @param client 设备客户端
  321 + * @param message 设备消息
  322 + * @param methodName 方法名称
  323 + * @param fallbackAction 降级处理逻辑
  324 + */
  325 + private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message,
  326 + String methodName, Runnable fallbackAction) {
  327 + try {
  328 + // 1. 使用编解码器编码消息
  329 + byte[] messageBytes = messageService.encodeDeviceMessage(
  330 + message, client.getProductKey(), client.getDeviceName());
  331 +
  332 + // 2. 解析编码后的数据包并设置设备地址和消息序号
  333 + Buffer buffer = Buffer.buffer(messageBytes);
  334 + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
  335 + dataPackage.setAddr(client.getDeviceAddr());
  336 + dataPackage.setMid(generateMessageId());
  337 +
  338 + // 3. 重新编码并发送
  339 + Buffer finalBuffer = TcpDataEncoder.encode(dataPackage);
  340 + client.sendMessage(finalBuffer);
  341 +
  342 + log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}",
  343 + methodName, client.getDeviceAddr(), dataPackage.getMid());
  344 +
  345 + } catch (Exception e) {
  346 + log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}",
  347 + methodName, e.getMessage());
  348 +
  349 + // 执行降级处理
  350 + if (fallbackAction != null) {
  351 + fallbackAction.run();
  352 + }
  353 + }
  354 + }
  355 +
  356 + /**
  357 + * 获取连接统计信息
  358 + *
  359 + * @return 连接统计信息
  360 + */
  361 + public String getHandlerStatistics() {
  362 + return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus());
  363 + }
51 } 364 }
52 \ No newline at end of file 365 \ 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 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  8 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  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;
  17 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  18 +import io.vertx.core.Handler;
  19 +import io.vertx.core.buffer.Buffer;
  20 +import io.vertx.core.net.NetSocket;
  21 +import io.vertx.core.parsetools.RecordParser;
  22 +import lombok.RequiredArgsConstructor;
  23 +import lombok.extern.slf4j.Slf4j;
  24 +
  25 +/**
  26 + * IoT 网关 TCP 上行消息处理器
  27 + * <p>
  28 + * 核心负责:
  29 + * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信
  30 + * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态
  31 + * 3. 【数据上报】接收设备数据上报和事件上报
  32 + * 4. 【连接管理】管理连接的建立、维护和清理
  33 + *
  34 + * @author 芋道源码
  35 + */
  36 +@RequiredArgsConstructor
  37 +@Slf4j
  38 +public class IotTcpUpstreamHandler implements Handler<NetSocket> {
  39 +
  40 + private final IotGatewayProperties.TcpProperties tcpConfig;
  41 +
  42 + private final TcpDeviceConnectionManager connectionManager;
  43 +
  44 + private final IotDeviceService deviceService;
  45 +
  46 + private final IotDeviceMessageService messageService;
  47 +
  48 + private final IotDeviceCommonApi deviceApi;
  49 +
  50 + private final String serverId;
  51 +
  52 + @Override
  53 + public void handle(NetSocket socket) {
  54 + log.info("[handle][收到设备连接: {}]", socket.remoteAddress());
  55 +
  56 + // 创建客户端ID和设备客户端
  57 + String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress();
  58 + TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs());
  59 +
  60 + try {
  61 + // 设置连接异常和关闭处理
  62 + socket.exceptionHandler(ex -> {
  63 + log.error("[handle][连接({})异常]", socket.remoteAddress(), ex);
  64 + handleConnectionClose(client);
  65 + });
  66 +
  67 + socket.closeHandler(v -> {
  68 + log.info("[handle][连接({})关闭]", socket.remoteAddress());
  69 + handleConnectionClose(client);
  70 + });
  71 +
  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 +
  84 + // 设置解析器
  85 + client.setParser(parser);
  86 +
  87 + log.info("[handle][设备连接处理器初始化完成: {}]", clientId);
  88 +
  89 + } catch (Exception e) {
  90 + log.error("[handle][初始化连接处理器失败]", e);
  91 + client.shutdown();
  92 + }
  93 + }
  94 +
  95 + /**
  96 + * 处理数据包
  97 + *
  98 + * @param client 设备客户端
  99 + * @param buffer 数据缓冲区
  100 + */
  101 + private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) {
  102 + try {
  103 + // 解码数据包
  104 + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
  105 +
  106 + log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}",
  107 + dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid());
  108 +
  109 + // 根据功能码处理不同类型的消息
  110 + switch (dataPackage.getCode()) {
  111 + case TcpDataPackage.CODE_REGISTER:
  112 + handleDeviceRegister(client, dataPackage);
  113 + break;
  114 + case TcpDataPackage.CODE_HEARTBEAT:
  115 + handleHeartbeat(client, dataPackage);
  116 + break;
  117 + case TcpDataPackage.CODE_DATA_UP:
  118 + handleDataUp(client, dataPackage);
  119 + break;
  120 + case TcpDataPackage.CODE_EVENT_UP:
  121 + handleEventUp(client, dataPackage);
  122 + break;
  123 + default:
  124 + log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode());
  125 + break;
  126 + }
  127 +
  128 + } catch (Exception e) {
  129 + log.error("[handleDataPackage][处理数据包失败]", e);
  130 + }
  131 + }
  132 +
  133 + /**
  134 + * 处理设备注册
  135 + *
  136 + * @param client 设备客户端
  137 + * @param dataPackage 数据包
  138 + */
  139 + private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) {
  140 + try {
  141 + String deviceAddr = dataPackage.getAddr();
  142 + String productKey = dataPackage.getPayload();
  143 +
  144 + log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey);
  145 +
  146 + // 获取设备信息
  147 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr);
  148 + if (device == null) {
  149 + log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr);
  150 + sendRegisterReply(client, dataPackage, false);
  151 + return;
  152 + }
  153 +
  154 + // 更新客户端信息
  155 + client.setProductKey(productKey);
  156 + client.setDeviceName(deviceAddr);
  157 + client.setDeviceId(device.getId());
  158 + client.setAuthenticated(true);
  159 +
  160 + // 添加到连接管理器
  161 + connectionManager.addClient(deviceAddr, client);
  162 + connectionManager.setDeviceIdMapping(deviceAddr, device.getId());
  163 +
  164 + // 发送设备上线消息
  165 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  166 + messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId);
  167 +
  168 + // 发送注册成功回复
  169 + sendRegisterReply(client, dataPackage, true);
  170 +
  171 + log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId());
  172 +
  173 + } catch (Exception e) {
  174 + log.error("[handleDeviceRegister][设备注册失败]", e);
  175 + sendRegisterReply(client, dataPackage, false);
  176 + }
  177 + }
  178 +
  179 + /**
  180 + * 处理心跳
  181 + *
  182 + * @param client 设备客户端
  183 + * @param dataPackage 数据包
  184 + */
  185 + private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) {
  186 + try {
  187 + String deviceAddr = dataPackage.getAddr();
  188 +
  189 + log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr);
  190 +
  191 + // 更新心跳时间
  192 + client.keepAlive();
  193 +
  194 + // 发送心跳回复(可选)
  195 + // sendHeartbeatReply(client, dataPackage);
  196 +
  197 + } catch (Exception e) {
  198 + log.error("[handleHeartbeat][处理心跳失败]", e);
  199 + }
  200 + }
  201 +
  202 + /**
  203 + * 处理数据上报
  204 + *
  205 + * @param client 设备客户端
  206 + * @param dataPackage 数据包
  207 + */
  208 + private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) {
  209 + try {
  210 + String deviceAddr = dataPackage.getAddr();
  211 + String payload = dataPackage.getPayload();
  212 +
  213 + log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload);
  214 +
  215 + // 检查设备是否已认证
  216 + if (!client.isAuthenticated()) {
  217 + log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr);
  218 + return;
  219 + }
  220 +
  221 + // 使用 IotDeviceMessageService 解码消息
  222 + try {
  223 + // 1. 将 TCP 数据包重新编码为字节数组
  224 + Buffer buffer = TcpDataEncoder.encode(dataPackage);
  225 + byte[] messageBytes = buffer.getBytes();
  226 +
  227 + // 2. 使用 messageService 解码消息
  228 + IotDeviceMessage message = messageService.decodeDeviceMessage(
  229 + messageBytes, client.getProductKey(), client.getDeviceName());
  230 +
  231 + // 3. 发送解码后的消息
  232 + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
  233 +
  234 + } catch (Exception e) {
  235 + log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage());
  236 +
  237 + // 降级处理:使用原始方式解析数据
  238 + JSONObject dataJson = JSONUtil.parseObj(payload);
  239 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson);
  240 + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
  241 + }
  242 +
  243 + // 发送数据上报回复
  244 + sendDataUpReply(client, dataPackage);
  245 +
  246 + } catch (Exception e) {
  247 + log.error("[handleDataUp][处理数据上报失败]", e);
  248 + }
  249 + }
  250 +
  251 + /**
  252 + * 处理事件上报
  253 + *
  254 + * @param client 设备客户端
  255 + * @param dataPackage 数据包
  256 + */
  257 + private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) {
  258 + try {
  259 + String deviceAddr = dataPackage.getAddr();
  260 + String payload = dataPackage.getPayload();
  261 +
  262 + log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload);
  263 +
  264 + // 检查设备是否已认证
  265 + if (!client.isAuthenticated()) {
  266 + log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr);
  267 + return;
  268 + }
  269 +
  270 + // 使用 IotDeviceMessageService 解码消息
  271 + try {
  272 + // 1. 将 TCP 数据包重新编码为字节数组
  273 + Buffer buffer = TcpDataEncoder.encode(dataPackage);
  274 + byte[] messageBytes = buffer.getBytes();
  275 +
  276 + // 2. 使用 messageService 解码消息
  277 + IotDeviceMessage message = messageService.decodeDeviceMessage(
  278 + messageBytes, client.getProductKey(), client.getDeviceName());
  279 +
  280 + // 3. 发送解码后的消息
  281 + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
  282 +
  283 + } catch (Exception e) {
  284 + log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage());
  285 +
  286 + // 降级处理:使用原始方式解析数据
  287 + JSONObject eventJson = JSONUtil.parseObj(payload);
  288 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson);
  289 + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
  290 + }
  291 +
  292 + // 发送事件上报回复
  293 + sendEventUpReply(client, dataPackage);
  294 +
  295 + } catch (Exception e) {
  296 + log.error("[handleEventUp][处理事件上报失败]", e);
  297 + }
  298 + }
  299 +
  300 + /**
  301 + * 发送注册回复
  302 + *
  303 + * @param client 设备客户端
  304 + * @param dataPackage 原始数据包
  305 + * @param success 是否成功
  306 + */
  307 + private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) {
  308 + try {
  309 + io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply(
  310 + dataPackage.getAddr(), dataPackage.getMid(), success);
  311 + client.sendMessage(replyBuffer);
  312 +
  313 + log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}",
  314 + dataPackage.getAddr(), success ? "成功" : "失败");
  315 + } catch (Exception e) {
  316 + log.error("[sendRegisterReply][发送注册回复失败]", e);
  317 + }
  318 + }
  319 +
  320 + /**
  321 + * 发送数据上报回复
  322 + *
  323 + * @param client 设备客户端
  324 + * @param dataPackage 原始数据包
  325 + */
  326 + private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) {
  327 + try {
  328 + TcpDataPackage replyPackage = TcpDataPackage.builder()
  329 + .addr(dataPackage.getAddr())
  330 + .code(TcpDataPackage.CODE_DATA_UP)
  331 + .mid(dataPackage.getMid())
  332 + .payload("0") // 0表示成功
  333 + .build();
  334 +
  335 + io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage);
  336 + client.sendMessage(replyBuffer);
  337 +
  338 + } catch (Exception e) {
  339 + log.error("[sendDataUpReply][发送数据上报回复失败]", e);
  340 + }
  341 + }
  342 +
  343 + /**
  344 + * 发送事件上报回复
  345 + *
  346 + * @param client 设备客户端
  347 + * @param dataPackage 原始数据包
  348 + */
  349 + private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) {
  350 + try {
  351 + TcpDataPackage replyPackage = TcpDataPackage.builder()
  352 + .addr(dataPackage.getAddr())
  353 + .code(TcpDataPackage.CODE_EVENT_UP)
  354 + .mid(dataPackage.getMid())
  355 + .payload("0") // 0表示成功
  356 + .build();
  357 +
  358 + io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage);
  359 + client.sendMessage(replyBuffer);
  360 +
  361 + } catch (Exception e) {
  362 + log.error("[sendEventUpReply][发送事件上报回复失败]", e);
  363 + }
  364 + }
  365 +
  366 + /**
  367 + * 处理连接关闭
  368 + *
  369 + * @param client 设备客户端
  370 + */
  371 + private void handleConnectionClose(TcpDeviceClient client) {
  372 + try {
  373 + String deviceAddr = client.getDeviceAddr();
  374 +
  375 + // 发送设备离线消息
  376 + if (client.isAuthenticated()) {
  377 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  378 + messageService.sendDeviceMessage(offlineMessage,
  379 + client.getProductKey(), client.getDeviceName(), serverId);
  380 + }
  381 +
  382 + // 从连接管理器移除
  383 + if (deviceAddr != null) {
  384 + connectionManager.removeClient(deviceAddr);
  385 + }
  386 +
  387 + log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr);
  388 +
  389 + } catch (Exception e) {
  390 + log.error("[handleConnectionClose][处理连接关闭失败]", e);
  391 + }
  392 + }
  393 +}
0 \ No newline at end of file 394 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
@@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; 7 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
9 -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; 9 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
10 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 10 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
11 import jakarta.annotation.Resource; 11 import jakarta.annotation.Resource;
12 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
@@ -31,7 +31,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -31,7 +31,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
31 /** 31 /**
32 * 编解码器 32 * 编解码器
33 */ 33 */
34 - private final Map<String, IotAlinkDeviceMessageCodec> codes; 34 + private final Map<String, IotDeviceMessageCodec> codes;
35 35
36 @Resource 36 @Resource
37 private IotDeviceService deviceService; 37 private IotDeviceService deviceService;
@@ -39,8 +39,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -39,8 +39,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
39 @Resource 39 @Resource
40 private IotDeviceMessageProducer deviceMessageProducer; 40 private IotDeviceMessageProducer deviceMessageProducer;
41 41
42 - public IotDeviceMessageServiceImpl(List<IotAlinkDeviceMessageCodec> codes) {  
43 - this.codes = CollectionUtils.convertMap(codes, IotAlinkDeviceMessageCodec::type); 42 + public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) {
  43 + this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type);
44 } 44 }
45 45
46 @Override 46 @Override
@@ -52,7 +52,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -52,7 +52,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
52 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); 52 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
53 } 53 }
54 // 1.2 获取编解码器 54 // 1.2 获取编解码器
55 - IotAlinkDeviceMessageCodec codec = codes.get(device.getCodecType()); 55 + IotDeviceMessageCodec codec = codes.get(device.getCodecType());
56 if (codec == null) { 56 if (codec == null) {
57 throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); 57 throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
58 } 58 }
@@ -70,7 +70,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -70,7 +70,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
70 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); 70 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
71 } 71 }
72 // 1.2 获取编解码器 72 // 1.2 获取编解码器
73 - IotAlinkDeviceMessageCodec codec = codes.get(device.getCodecType()); 73 + IotDeviceMessageCodec codec = codes.get(device.getCodecType());
74 if (codec == null) { 74 if (codec == null) {
75 throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); 75 throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
76 } 76 }
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -79,8 +79,13 @@ yudao: @@ -79,8 +79,13 @@ yudao:
79 # 针对引入的 TCP 组件的配置 79 # 针对引入的 TCP 组件的配置
80 # ==================================== 80 # ====================================
81 tcp: 81 tcp:
82 - enabled: true  
83 - server-port: 8093 82 + enabled: false
  83 + port: 8091
  84 + keep-alive-timeout-ms: 30000
  85 + max-connections: 1000
  86 + ssl-enabled: false
  87 + ssl-cert-path: "classpath:certs/client.jks"
  88 + ssl-key-path: "classpath:certs/client.jks"
84 89
85 --- #################### 日志相关配置 #################### 90 --- #################### 日志相关配置 ####################
86 91