Commit 44b1950e4a4825bbe8488d257ee0cb99cd568ea2

Authored by YunaiV
1 parent e89fc2bf

feat(iot):【协议改造】tcp 初步改造(50%)

Showing 20 changed files with 1037 additions and 905 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
5   -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
6 5 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
7 6 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
8 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
... ... @@ -12,10 +11,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
12 11 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
13 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
14 13 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
16   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
17   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
18   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
19 14 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
20 15 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
21 16 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
... ... @@ -24,6 +19,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownst
24 19 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
25 20 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
26 21 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler;
  22 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
27 23 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
28 24 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
29 25 import io.vertx.core.Vertx;
... ... @@ -85,44 +81,6 @@ public class IotGatewayConfiguration {
85 81 }
86 82  
87 83 /**
88   - * IoT 网关 TCP 协议配置类
89   - */
90   - @Configuration
91   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true")
92   - @Slf4j
93   - public static class TcpProtocolConfiguration {
94   -
95   - @Bean(name = "tcpVertx", destroyMethod = "close")
96   - public Vertx tcpVertx() {
97   - return Vertx.vertx();
98   - }
99   -
100   - @Bean
101   - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
102   - IotDeviceService deviceService,
103   - IotDeviceMessageService messageService,
104   - IotTcpConnectionManager connectionManager,
105   - @Qualifier("tcpVertx") Vertx tcpVertx) {
106   - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
107   - deviceService, messageService, connectionManager, tcpVertx);
108   - }
109   -
110   - @Bean
111   - public IotTcpDownstreamHandler iotTcpDownstreamHandler(IotDeviceMessageService messageService,
112   - IotTcpConnectionManager connectionManager) {
113   - return new IotTcpDownstreamHandler(messageService, connectionManager);
114   - }
115   -
116   - @Bean
117   - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
118   - IotTcpDownstreamHandler downstreamHandler,
119   - IotMessageBus messageBus) {
120   - return new IotTcpDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus);
121   - }
122   -
123   - }
124   -
125   - /**
126 84 * IoT 网关 MQTT 协议配置类
127 85 */
128 86 @Configuration
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
... ... @@ -2,6 +2,9 @@ package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  6 +import io.vertx.core.net.KeyCertOptions;
  7 +import io.vertx.core.net.TrustOptions;
5 8 import jakarta.validation.Valid;
6 9 import jakarta.validation.constraints.NotEmpty;
7 10 import jakarta.validation.constraints.NotNull;
... ... @@ -77,21 +80,11 @@ public class IotGatewayProperties {
77 80 public static class ProtocolProperties {
78 81  
79 82 /**
80   - * HTTP 组件配置
81   - */
82   - private HttpProperties http;
83   -
84   - /**
85 83 * EMQX 组件配置
86 84 */
87 85 private EmqxProperties emqx;
88 86  
89 87 /**
90   - * TCP 组件配置
91   - */
92   - private TcpProperties tcp;
93   -
94   - /**
95 88 * MQTT 组件配置
96 89 */
97 90 private MqttProperties mqtt;
... ... @@ -114,36 +107,6 @@ public class IotGatewayProperties {
114 107 }
115 108  
116 109 @Data
117   - public static class HttpProperties {
118   -
119   - /**
120   - * 是否开启
121   - */
122   - @NotNull(message = "是否开启不能为空")
123   - private Boolean enabled;
124   - /**
125   - * 服务端口
126   - */
127   - private Integer serverPort;
128   -
129   - /**
130   - * 是否开启 SSL
131   - */
132   - @NotNull(message = "是否开启 SSL 不能为空")
133   - private Boolean sslEnabled = false;
134   -
135   - /**
136   - * SSL 证书路径
137   - */
138   - private String sslKeyPath;
139   - /**
140   - * SSL 证书路径
141   - */
142   - private String sslCertPath;
143   -
144   - }
145   -
146   - @Data
147 110 public static class EmqxProperties {
148 111  
149 112 /**
... ... @@ -313,47 +276,6 @@ public class IotGatewayProperties {
313 276 }
314 277  
315 278 @Data
316   - public static class TcpProperties {
317   -
318   - /**
319   - * 是否开启
320   - */
321   - @NotNull(message = "是否开启不能为空")
322   - private Boolean enabled;
323   -
324   - /**
325   - * 服务器端口
326   - */
327   - private Integer port = 8091;
328   -
329   - /**
330   - * 心跳超时时间(毫秒)
331   - */
332   - private Long keepAliveTimeoutMs = 30000L;
333   -
334   - /**
335   - * 最大连接数
336   - */
337   - private Integer maxConnections = 1000;
338   -
339   - /**
340   - * 是否启用SSL
341   - */
342   - private Boolean sslEnabled = false;
343   -
344   - /**
345   - * SSL证书路径
346   - */
347   - private String sslCertPath;
348   -
349   - /**
350   - * SSL私钥路径
351   - */
352   - private String sslKeyPath;
353   -
354   - }
355   -
356   - @Data
357 279 public static class MqttProperties {
358 280  
359 281 /**
... ... @@ -381,6 +303,7 @@ public class IotGatewayProperties {
381 303 */
382 304 private Integer keepAliveTimeoutSeconds = 300;
383 305  
  306 + // TODO @AI:所有跟 ssl 相关的参数,是不是可以统一?放到 protocol 层级?ProtocolInstanceProperties【优先级:低】暂时不用规划;
384 307 /**
385 308 * 是否启用 SSL
386 309 */
... ... @@ -399,11 +322,11 @@ public class IotGatewayProperties {
399 322 /**
400 323 * 密钥证书选项
401 324 */
402   - private io.vertx.core.net.KeyCertOptions keyCertOptions;
  325 + private KeyCertOptions keyCertOptions;
403 326 /**
404 327 * 信任选项
405 328 */
406   - private io.vertx.core.net.TrustOptions trustOptions;
  329 + private TrustOptions trustOptions;
407 330 /**
408 331 * SSL 证书路径
409 332 */
... ... @@ -596,78 +519,11 @@ public class IotGatewayProperties {
596 519 @Valid
597 520 private IotHttpConfig http;
598 521  
599   - // TODO @AI:后续改下;
600 522 /**
601   - * TCP 协议配置(后续扩展)
  523 + * TCP 协议配置
602 524 */
603 525 @Valid
604   - private TcpInstanceConfig tcp;
605   -
606   - }
607   -
608   - /**
609   - * TCP 协议实例配置(后续扩展)
610   - */
611   - @Data
612   - public static class TcpInstanceConfig {
613   -
614   - /**
615   - * 最大连接数
616   - */
617   - private Integer maxConnections = 1000;
618   -
619   - /**
620   - * 心跳超时时间(毫秒)
621   - */
622   - private Long keepAliveTimeoutMs = 30000L;
623   -
624   - /**
625   - * 是否启用 SSL
626   - */
627   - private Boolean sslEnabled = false;
628   -
629   - /**
630   - * SSL 证书路径
631   - */
632   - private String sslCertPath;
633   -
634   - /**
635   - * SSL 私钥路径
636   - */
637   - private String sslKeyPath;
638   -
639   - /**
640   - * 拆包配置
641   - */
642   - private CodecConfig codec;
643   -
644   - /**
645   - * TCP 拆包配置
646   - */
647   - @Data
648   - public static class CodecConfig {
649   -
650   - /**
651   - * 拆包类型:LENGTH_FIELD / DELIMITER
652   - */
653   - private String type;
654   -
655   - /**
656   - * LENGTH_FIELD: 偏移量
657   - */
658   - private Integer lengthFieldOffset;
659   -
660   - /**
661   - * LENGTH_FIELD: 长度字段长度
662   - */
663   - private Integer lengthFieldLength;
664   -
665   - /**
666   - * DELIMITER: 分隔符
667   - */
668   - private String delimiter;
669   -
670   - }
  526 + private IotTcpConfig tcp;
671 527  
672 528 }
673 529  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
7 7 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
9 10 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
10 11 import lombok.extern.slf4j.Slf4j;
11 12 import org.springframework.context.SmartLifecycle;
... ... @@ -14,9 +15,7 @@ import java.util.ArrayList;
14 15 import java.util.List;
15 16  
16 17 /**
17   - * IoT 协议管理器
18   - *
19   - * 负责根据配置创建和管理协议实例
  18 + * IoT 协议管理器:负责根据配置创建和管理协议实例
20 19 *
21 20 * @author 芋道源码
22 21 */
... ... @@ -96,7 +95,7 @@ public class IotProtocolManager implements SmartLifecycle {
96 95 * @param config 协议实例配置
97 96 * @return 协议实例
98 97 */
99   - @SuppressWarnings({"SwitchStatementWithTooFewBranches", "EnhancedSwitchMigration"})
  98 + @SuppressWarnings({"EnhancedSwitchMigration"})
100 99 private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
101 100 IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType());
102 101 if (protocolType == null) {
... ... @@ -106,6 +105,8 @@ public class IotProtocolManager implements SmartLifecycle {
106 105 switch (protocolType) {
107 106 case HTTP:
108 107 return createHttpProtocol(config);
  108 + case TCP:
  109 + return createTcpProtocol(config);
109 110 // TODO 后续添加其他协议类型
110 111 default:
111 112 throw new IllegalArgumentException(String.format(
... ... @@ -123,4 +124,14 @@ public class IotProtocolManager implements SmartLifecycle {
123 124 return new IotHttpProtocol(config, messageBus);
124 125 }
125 126  
  127 + /**
  128 + * 创建 TCP 协议实例
  129 + *
  130 + * @param config 协议实例配置
  131 + * @return TCP 协议实例
  132 + */
  133 + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  134 + return new IotTcpProtocol(config, messageBus, serializerManager);
  135 + }
  136 +
126 137 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
... ... @@ -34,36 +34,32 @@ public class IotHttpProtocol implements IotProtocol {
34 34 */
35 35 private final ProtocolInstanceProperties properties;
36 36 /**
37   - * 消息总线
38   - */
39   - private final IotMessageBus messageBus;
40   - /**
41 37 * 服务器 ID(用于消息追踪,全局唯一)
42 38 */
43 39 @Getter
44 40 private final String serverId;
45 41  
46 42 /**
47   - * Vert.x 实例(每个 Protocol 自己管理)
  43 + * 运行状态
  44 + */
  45 + private volatile boolean running = false;
  46 +
  47 + /**
  48 + * Vert.x 实例
48 49 */
49 50 private Vertx vertx;
50 51 /**
51 52 * HTTP 服务器
52 53 */
53 54 private HttpServer httpServer;
  55 +
54 56 /**
55 57 * 下行消息订阅者
56 58 */
57 59 private IotHttpDownstreamSubscriber downstreamSubscriber;
58 60  
59   - /**
60   - * 运行状态
61   - */
62   - private volatile boolean running = false;
63   -
64 61 public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) {
65 62 this.properties = properties;
66   - this.messageBus = messageBus;
67 63 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
68 64 this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
69 65 }
... ... @@ -121,7 +117,6 @@ public class IotHttpProtocol implements IotProtocol {
121 117 getId(), properties.getPort(), serverId);
122 118  
123 119 // 2. 启动下行消息订阅者
124   - this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
125 120 this.downstreamSubscriber.start();
126 121 } catch (Exception e) {
127 122 log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
... ... @@ -132,9 +127,6 @@ public class IotHttpProtocol implements IotProtocol {
132 127 }
133 128 throw e;
134 129 }
135   -
136   - // 2. 启动下行消息订阅者
137   - this.downstreamSubscriber.start();
138 130 }
139 131  
140 132 @Override
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  4 +import jakarta.validation.Valid;
  5 +import jakarta.validation.constraints.Min;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +/**
  10 + * IoT TCP 协议配置
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +public class IotTcpConfig {
  16 +
  17 + /**
  18 + * 最大连接数
  19 + */
  20 + @NotNull(message = "最大连接数不能为空")
  21 + @Min(value = 1, message = "最大连接数必须大于 0")
  22 + private Integer maxConnections = 1000;
  23 + /**
  24 + * 心跳超时时间(毫秒)
  25 + */
  26 + @NotNull(message = "心跳超时时间不能为空")
  27 + @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒")
  28 + private Long keepAliveTimeoutMs = 30000L;
  29 +
  30 + /**
  31 + * 是否启用 SSL
  32 + */
  33 + @NotNull(message = "是否启用 SSL 不能为空")
  34 + private Boolean sslEnabled = false;
  35 + /**
  36 + * SSL 证书路径
  37 + */
  38 + private String sslCertPath;
  39 + /**
  40 + * SSL 私钥路径
  41 + */
  42 + private String sslKeyPath;
  43 +
  44 + /**
  45 + * 拆包配置
  46 + */
  47 + @Valid
  48 + private CodecConfig codec;
  49 +
  50 + /**
  51 + * TCP 拆包配置
  52 + */
  53 + @Data
  54 + public static class CodecConfig {
  55 +
  56 + /**
  57 + * 拆包类型
  58 + *
  59 + * @see IotTcpCodecTypeEnum
  60 + */
  61 + @NotNull(message = "拆包类型不能为空")
  62 + private String type;
  63 +
  64 + /**
  65 + * LENGTH_FIELD: 长度字段偏移量
  66 + * <p>
  67 + * 表示长度字段在消息中的起始位置(从0开始)
  68 + */
  69 + private Integer lengthFieldOffset;
  70 + /**
  71 + * LENGTH_FIELD: 长度字段长度(字节数)
  72 + * <p>
  73 + * 常见值:1(最大255)、2(最大65535)、4(最大2GB)
  74 + */
  75 + private Integer lengthFieldLength;
  76 + /**
  77 + * LENGTH_FIELD: 长度调整值
  78 + * <p>
  79 + * 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度
  80 + */
  81 + private Integer lengthAdjustment = 0;
  82 + /**
  83 + * LENGTH_FIELD: 跳过的初始字节数
  84 + * <p>
  85 + * 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength
  86 + */
  87 + private Integer initialBytesToStrip = 0;
  88 +
  89 + /**
  90 + * DELIMITER: 分隔符
  91 + * <p>
  92 + * 支持转义字符:\n(换行)、\r(回车)、\r\n(回车换行)
  93 + */
  94 + private String delimiter;
  95 +
  96 + /**
  97 + * FIXED_LENGTH: 固定消息长度(字节)
  98 + * <p>
  99 + * 每条消息的固定长度
  100 + */
  101 + private Integer fixedLength;
  102 +
  103 + /**
  104 + * 最大帧长度(字节)
  105 + * <p>
  106 + * 防止内存溢出,默认 1MB
  107 + */
  108 + @NotNull(message = "最大帧长度不能为空")
  109 + @Min(value = 1, message = "最大帧长度必须大于 0")
  110 + private Integer maxFrameLength = 1048576;
  111 +
  112 + }
  113 +
  114 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  7 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  8 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
  15 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  16 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
  17 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  18 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  19 +import io.vertx.core.Vertx;
  20 +import io.vertx.core.net.NetServer;
  21 +import io.vertx.core.net.NetServerOptions;
  22 +import io.vertx.core.net.PemKeyCertOptions;
  23 +import lombok.Getter;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +
  26 +/**
  27 + * IoT TCP 协议实现
  28 + * <p>
  29 + * 基于 Vert.x 实现 TCP 服务器,接收设备上行消息
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Slf4j
  34 +public class IotTcpProtocol implements IotProtocol {
  35 +
  36 + /**
  37 + * 协议配置
  38 + */
  39 + private final ProtocolInstanceProperties properties;
  40 + /**
  41 + * 服务器 ID(用于消息追踪,全局唯一)
  42 + */
  43 + @Getter
  44 + private final String serverId;
  45 +
  46 + /**
  47 + * 运行状态
  48 + */
  49 + private volatile boolean running = false;
  50 +
  51 + /**
  52 + * Vert.x 实例
  53 + */
  54 + private Vertx vertx;
  55 + /**
  56 + * TCP 服务器
  57 + */
  58 + private NetServer tcpServer;
  59 +
  60 + /**
  61 + * 下行消息订阅者
  62 + */
  63 + private final IotTcpDownstreamSubscriber downstreamSubscriber;
  64 +
  65 + /**
  66 + * 消息序列化器
  67 + */
  68 + private final IotMessageSerializer serializer;
  69 +
  70 + /**
  71 + * TCP 帧编解码器
  72 + */
  73 + private final IotTcpFrameCodec frameCodec;
  74 +
  75 + public IotTcpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus,
  76 + IotMessageSerializerManager serializerManager) {
  77 + this.properties = properties;
  78 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  79 +
  80 + // 初始化序列化器
  81 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize());
  82 + if (serializeType == null) {
  83 + serializeType = IotSerializeTypeEnum.JSON; // 默认 JSON
  84 + }
  85 + this.serializer = serializerManager.get(serializeType);
  86 +
  87 + // 初始化帧编解码器
  88 + IotTcpConfig tcpConfig = properties.getTcp();
  89 + IotTcpConfig.CodecConfig codecConfig = tcpConfig != null ? tcpConfig.getCodec() : null;
  90 + this.frameCodec = IotTcpFrameCodec.create(codecConfig);
  91 +
  92 + // 初始化下行消息订阅者
  93 + IotTcpConnectionManager connectionManager = SpringUtil.getBean(IotTcpConnectionManager.class);
  94 + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer);
  95 + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus);
  96 + }
  97 +
  98 + @Override
  99 + public String getId() {
  100 + return properties.getId();
  101 + }
  102 +
  103 + @Override
  104 + public IotProtocolTypeEnum getType() {
  105 + return IotProtocolTypeEnum.TCP;
  106 + }
  107 +
  108 + @Override
  109 + public void start() {
  110 + if (running) {
  111 + log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId());
  112 + return;
  113 + }
  114 +
  115 + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理)
  116 + this.vertx = Vertx.vertx();
  117 +
  118 + // 1.2 创建服务器选项
  119 + IotTcpConfig tcpConfig = properties.getTcp();
  120 + NetServerOptions options = new NetServerOptions()
  121 + .setPort(properties.getPort())
  122 + .setTcpKeepAlive(true)
  123 + .setTcpNoDelay(true)
  124 + .setReuseAddress(true);
  125 + if (tcpConfig != null && Boolean.TRUE.equals(tcpConfig.getSslEnabled())) {
  126 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  127 + .setKeyPath(tcpConfig.getSslKeyPath())
  128 + .setCertPath(tcpConfig.getSslCertPath());
  129 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  130 + }
  131 +
  132 + // 1.3 创建服务器并设置连接处理器
  133 + tcpServer = vertx.createNetServer(options);
  134 + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
  135 + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
  136 + IotTcpConnectionManager connectionManager = SpringUtil.getBean(IotTcpConnectionManager.class);
  137 + tcpServer.connectHandler(socket -> {
  138 + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService,
  139 + connectionManager, frameCodec, serializer);
  140 + handler.handle(socket);
  141 + });
  142 +
  143 + // 1.4 启动 TCP 服务器
  144 + try {
  145 + tcpServer.listen().result();
  146 + running = true;
  147 + log.info("[start][IoT TCP 协议 {} 启动成功,端口:{},serverId:{}]",
  148 + getId(), properties.getPort(), serverId);
  149 +
  150 + // 2. 启动下行消息订阅者
  151 + this.downstreamSubscriber.start();
  152 + } catch (Exception e) {
  153 + log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e);
  154 + // 启动失败时关闭 Vertx
  155 + if (vertx != null) {
  156 + vertx.close();
  157 + vertx = null;
  158 + }
  159 + throw e;
  160 + }
  161 + }
  162 +
  163 + @Override
  164 + public void stop() {
  165 + if (!running) {
  166 + return;
  167 + }
  168 + // 1. 停止下行消息订阅者
  169 + try {
  170 + downstreamSubscriber.stop();
  171 + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId());
  172 + } catch (Exception e) {
  173 + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  174 + }
  175 +
  176 + // 2.1 关闭 TCP 服务器
  177 + if (tcpServer != null) {
  178 + try {
  179 + tcpServer.close().result();
  180 + log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId());
  181 + } catch (Exception e) {
  182 + log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e);
  183 + }
  184 + tcpServer = null;
  185 + }
  186 + // 2.2 关闭 Vertx 实例
  187 + if (vertx != null) {
  188 + try {
  189 + vertx.close().result();
  190 + log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId());
  191 + } catch (Exception e) {
  192 + log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e);
  193 + }
  194 + vertx = null;
  195 + }
  196 + running = false;
  197 + log.info("[stop][IoT TCP 协议 {} 已停止]", getId());
  198 + }
  199 +
  200 + @Override
  201 + public boolean isRunning() {
  202 + return running;
  203 + }
  204 +
  205 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
2   -
3   -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
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.IotProtocol;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
9   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
10   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11   -import io.vertx.core.Vertx;
12   -import io.vertx.core.net.NetServer;
13   -import io.vertx.core.net.NetServerOptions;
14   -import io.vertx.core.net.PemKeyCertOptions;
15   -import jakarta.annotation.PostConstruct;
16   -import jakarta.annotation.PreDestroy;
17   -import lombok.Getter;
18   -import lombok.extern.slf4j.Slf4j;
19   -
20   -/**
21   - * IoT 网关 TCP 协议:接收设备上行消息
22   - *
23   - * @author 芋道源码
24   - */
25   -@Slf4j
26   -public class IotTcpUpstreamProtocol implements IotProtocol {
27   -
28   - private static final String ID = "tcp";
29   -
30   - private final IotGatewayProperties.TcpProperties tcpProperties;
31   -
32   - private final IotDeviceService deviceService;
33   -
34   - private final IotDeviceMessageService messageService;
35   -
36   - private final IotTcpConnectionManager connectionManager;
37   -
38   - private final Vertx vertx;
39   -
40   - @Getter
41   - private final String serverId;
42   -
43   - private NetServer tcpServer;
44   -
45   - private volatile boolean running = false;
46   -
47   - public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
48   - IotDeviceService deviceService,
49   - IotDeviceMessageService messageService,
50   - IotTcpConnectionManager connectionManager,
51   - Vertx vertx) {
52   - this.tcpProperties = tcpProperties;
53   - this.deviceService = deviceService;
54   - this.messageService = messageService;
55   - this.connectionManager = connectionManager;
56   - this.vertx = vertx;
57   - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
58   - }
59   -
60   - @Override
61   - public String getId() {
62   - return ID;
63   - }
64   -
65   - @Override
66   - public IotProtocolTypeEnum getType() {
67   - return IotProtocolTypeEnum.TCP;
68   - }
69   -
70   - @Override
71   - @PostConstruct
72   - public void start() {
73   - // 创建服务器选项
74   - NetServerOptions options = new NetServerOptions()
75   - .setPort(tcpProperties.getPort())
76   - .setTcpKeepAlive(true)
77   - .setTcpNoDelay(true)
78   - .setReuseAddress(true);
79   - // 配置 SSL(如果启用)
80   - if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
81   - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
82   - .setKeyPath(tcpProperties.getSslKeyPath())
83   - .setCertPath(tcpProperties.getSslCertPath());
84   - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
85   - }
86   -
87   - // 创建服务器并设置连接处理器
88   - tcpServer = vertx.createNetServer(options);
89   - tcpServer.connectHandler(socket -> {
90   - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService,
91   - connectionManager);
92   - handler.handle(socket);
93   - });
94   -
95   - // 启动服务器
96   - try {
97   - tcpServer.listen().result();
98   - running = true;
99   - log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort());
100   - } catch (Exception e) {
101   - log.error("[start][IoT 网关 TCP 协议启动失败]", e);
102   - throw e;
103   - }
104   - }
105   -
106   - @Override
107   - @PreDestroy
108   - public void stop() {
109   - if (tcpServer != null) {
110   - try {
111   - tcpServer.close().result();
112   - running = false;
113   - log.info("[stop][IoT 网关 TCP 协议已停止]");
114   - } catch (Exception e) {
115   - log.error("[stop][IoT 网关 TCP 协议停止失败]", e);
116   - }
117   - }
118   - }
119   -
120   - @Override
121   - public boolean isRunning() {
122   - return running;
123   - }
124   -
125   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec;
  8 +import lombok.AllArgsConstructor;
  9 +import lombok.Getter;
  10 +
  11 +import java.util.function.Function;
  12 +
  13 +/**
  14 + * IoT TCP 拆包类型枚举
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@AllArgsConstructor
  19 +@Getter
  20 +public enum IotTcpCodecTypeEnum {
  21 +
  22 + /**
  23 + * 基于固定长度的拆包
  24 + * <p>
  25 + * 消息格式:固定长度的消息体
  26 + * 需要配置:fixedLength(固定长度)
  27 + */
  28 + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec::new),
  29 +
  30 + /**
  31 + * 基于分隔符的拆包
  32 + * <p>
  33 + * 消息格式:消息内容 + 分隔符
  34 + * 需要配置:delimiter(分隔符)
  35 + */
  36 + DELIMITER("delimiter", IotTcpDelimiterFrameCodec::new),
  37 +
  38 + /**
  39 + * 基于长度字段的拆包
  40 + * <p>
  41 + * 消息格式:[长度字段][消息体]
  42 + * 需要配置:lengthFieldOffset(长度字段偏移量)、lengthFieldLength(长度字段长度)
  43 + */
  44 + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec::new),
  45 + ;
  46 +
  47 + /**
  48 + * 类型标识
  49 + */
  50 + private final String type;
  51 +
  52 + /**
  53 + * Codec 创建工厂
  54 + */
  55 + private final Function<IotTcpConfig.CodecConfig, IotTcpFrameCodec> codecFactory;
  56 +
  57 + /**
  58 + * 根据类型获取枚举
  59 + *
  60 + * @param type 类型标识
  61 + * @return 枚举值
  62 + */
  63 + public static IotTcpCodecTypeEnum of(String type) {
  64 + return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values());
  65 + }
  66 +
  67 + /**
  68 + * 创建 Codec 实例
  69 + *
  70 + * @param config 拆包配置
  71 + * @return Codec 实例
  72 + */
  73 + public IotTcpFrameCodec createCodec(IotTcpConfig.CodecConfig config) {
  74 + return codecFactory.apply(config);
  75 + }
  76 +
  77 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  4 +import io.vertx.core.Handler;
  5 +import io.vertx.core.buffer.Buffer;
  6 +import io.vertx.core.parsetools.RecordParser;
  7 +
  8 +/**
  9 + * IoT TCP 帧编解码器接口
  10 + * <p>
  11 + * 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +public interface IotTcpFrameCodec {
  16 +
  17 + /**
  18 + * 获取编解码器类型
  19 + *
  20 + * @return 编解码器类型
  21 + */
  22 + IotTcpCodecTypeEnum getType();
  23 +
  24 + /**
  25 + * 创建解码器(RecordParser)
  26 + * <p>
  27 + * 每个连接调用一次,返回的 parser 需绑定到 socket.handler()
  28 + *
  29 + * @param handler 消息处理器,当收到完整的消息帧后回调
  30 + * @return RecordParser 实例
  31 + */
  32 + RecordParser createDecodeParser(Handler<Buffer> handler);
  33 +
  34 + /**
  35 + * 编码消息(加帧)
  36 + * <p>
  37 + * 根据不同的编解码类型添加帧头/分隔符
  38 + *
  39 + * @param data 原始数据
  40 + * @return 编码后的数据(带帧头/分隔符)
  41 + */
  42 + Buffer encode(byte[] data);
  43 +
  44 + // TODO @AI:还是搞个 facory 类 ,更好理解;
  45 + // ========== 静态工厂方法 ==========
  46 +
  47 + /**
  48 + * 根据配置创建编解码器
  49 + *
  50 + * @param config 拆包配置
  51 + * @return 编解码器实例,如果配置为空则返回 null
  52 + */
  53 + static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) {
  54 + if (config == null) {
  55 + return null;
  56 + }
  57 + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType());
  58 + if (type == null) {
  59 + return null;
  60 + }
  61 + return type.createCodec(config);
  62 + }
  63 +
  64 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  7 +import io.vertx.core.Handler;
  8 +import io.vertx.core.buffer.Buffer;
  9 +import io.vertx.core.parsetools.RecordParser;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +
  12 +/**
  13 + * IoT TCP 分隔符帧编解码器
  14 + * <p>
  15 + * 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符
  16 + * <p>
  17 + * 支持的分隔符:
  18 + * <ul>
  19 + * <li>\n - 换行符</li>
  20 + * <li>\r - 回车符</li>
  21 + * <li>\r\n - 回车换行</li>
  22 + * <li>自定义字符串</li>
  23 + * </ul>
  24 + *
  25 + * @author 芋道源码
  26 + */
  27 +@Slf4j
  28 +public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
  29 +
  30 + private final IotTcpConfig.CodecConfig config;
  31 +
  32 + /**
  33 + * 解析后的分隔符字节数组
  34 + */
  35 + private final byte[] delimiterBytes;
  36 +
  37 + /**
  38 + * 最大帧长度
  39 + */
  40 + // TODO @AI:最大帧数要不去掉;简洁一点;包括其他地方的配置项;
  41 + private final int maxFrameLength;
  42 +
  43 + public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
  44 + this.config = config;
  45 + // TODO @AI:禁止为空;
  46 + this.delimiterBytes = parseDelimiter(config.getDelimiter());
  47 + this.maxFrameLength = config.getMaxFrameLength() != null ? config.getMaxFrameLength() : 1048576;
  48 + }
  49 +
  50 + @Override
  51 + public IotTcpCodecTypeEnum getType() {
  52 + return IotTcpCodecTypeEnum.DELIMITER;
  53 + }
  54 +
  55 + @Override
  56 + public RecordParser createDecodeParser(Handler<Buffer> handler) {
  57 + RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes));
  58 +
  59 + parser.handler(buffer -> {
  60 + // 检查帧长度是否超过限制
  61 + if (buffer.length() > maxFrameLength) {
  62 + log.warn("[createDecodeParser][帧长度超过限制,length: {}, maxFrameLength: {}]",
  63 + buffer.length(), maxFrameLength);
  64 + return;
  65 + }
  66 + // 处理完整消息(不包含分隔符)
  67 + handler.handle(buffer);
  68 + });
  69 +
  70 + // TODO @AI:异常处理;
  71 + parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex));
  72 + return parser;
  73 + }
  74 +
  75 + @Override
  76 + public Buffer encode(byte[] data) {
  77 + Buffer buffer = Buffer.buffer();
  78 + buffer.appendBytes(data);
  79 + buffer.appendBytes(delimiterBytes);
  80 + return buffer;
  81 + }
  82 +
  83 + /**
  84 + * 解析分隔符字符串为字节数组
  85 + * <p>
  86 + * 支持转义字符:\n、\r、\r\n、\t
  87 + *
  88 + * @param delimiter 分隔符字符串
  89 + * @return 分隔符字节数组
  90 + */
  91 + private byte[] parseDelimiter(String delimiter) {
  92 + if (StrUtil.isBlank(delimiter)) {
  93 + // 默认使用换行符
  94 + return new byte[]{'\n'};
  95 + }
  96 +
  97 + // 处理转义字符
  98 + // TODO @AI:是否必要?不调整感觉也没问题?用户自己写对就 ok 了是哇?
  99 + String parsed = delimiter
  100 + .replace("\\r\\n", "\r\n")
  101 + .replace("\\r", "\r")
  102 + .replace("\\n", "\n")
  103 + .replace("\\t", "\t");
  104 + return parsed.getBytes();
  105 + }
  106 +
  107 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  6 +import io.vertx.core.Handler;
  7 +import io.vertx.core.buffer.Buffer;
  8 +import io.vertx.core.parsetools.RecordParser;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +/**
  12 + * IoT TCP 定长帧编解码器
  13 + * <p>
  14 + * 基于固定长度的拆包策略,每条消息固定字节数
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Slf4j
  19 +public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec {
  20 +
  21 + /**
  22 + * 固定消息长度
  23 + */
  24 + private final int fixedLength;
  25 +
  26 + public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) {
  27 + // TODO @AI:config.getFixedLength() 禁止为空;
  28 + this.fixedLength = config.getFixedLength() != null ? config.getFixedLength() : 1024;
  29 + }
  30 +
  31 + @Override
  32 + public IotTcpCodecTypeEnum getType() {
  33 + return IotTcpCodecTypeEnum.FIXED_LENGTH;
  34 + }
  35 +
  36 + @Override
  37 + public RecordParser createDecodeParser(Handler<Buffer> handler) {
  38 + RecordParser parser = RecordParser.newFixed(fixedLength);
  39 + parser.handler(handler);
  40 + // TODO @AI:解析失败,是不是要抛出异常?因为要 close 掉连接;
  41 + parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex));
  42 + return parser;
  43 + }
  44 +
  45 + @Override
  46 + public Buffer encode(byte[] data) {
  47 + Buffer buffer = Buffer.buffer(fixedLength);
  48 + buffer.appendBytes(data);
  49 + // 如果数据不足固定长度,填充 0
  50 + // TODO @AI:这里的填充是合理的么?RecordParser.newFixed(fixedLength) 有填充的逻辑么?
  51 + if (data.length < fixedLength) {
  52 + byte[] padding = new byte[fixedLength - data.length];
  53 + buffer.appendBytes(padding);
  54 + }
  55 + return buffer;
  56 + }
  57 +
  58 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  6 +import io.vertx.core.Handler;
  7 +import io.vertx.core.buffer.Buffer;
  8 +import io.vertx.core.parsetools.RecordParser;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +/**
  12 + * IoT TCP 长度字段帧编解码器
  13 + * <p>
  14 + * 基于长度字段的拆包策略,消息格式:[长度字段][消息体]
  15 + * <p>
  16 + * 参数说明:
  17 + * <ul>
  18 + * <li>lengthFieldOffset: 长度字段在消息中的偏移量</li>
  19 + * <li>lengthFieldLength: 长度字段的字节数(1/2/4)</li>
  20 + * <li>lengthAdjustment: 长度调整值,用于调整长度字段的实际含义</li>
  21 + * <li>initialBytesToStrip: 解码后跳过的字节数</li>
  22 + * </ul>
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@Slf4j
  27 +public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec {
  28 +
  29 + private final int lengthFieldOffset;
  30 + private final int lengthFieldLength;
  31 + private final int lengthAdjustment;
  32 + private final int initialBytesToStrip;
  33 + // TODO @AI:去掉 maxFrameLength 相关字段;
  34 + private final int maxFrameLength;
  35 +
  36 + /**
  37 + * 头部长度 = 长度字段偏移量 + 长度字段长度
  38 + */
  39 + private final int headerLength;
  40 +
  41 + public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) {
  42 + // TODO @AI: 增加参数校验;不要 default 逻辑;
  43 + this.lengthFieldOffset = config.getLengthFieldOffset() != null ? config.getLengthFieldOffset() : 0;
  44 + this.lengthFieldLength = config.getLengthFieldLength() != null ? config.getLengthFieldLength() : 4;
  45 + this.lengthAdjustment = config.getLengthAdjustment() != null ? config.getLengthAdjustment() : 0;
  46 + this.initialBytesToStrip = config.getInitialBytesToStrip() != null ? config.getInitialBytesToStrip() : 0;
  47 + this.maxFrameLength = config.getMaxFrameLength() != null ? config.getMaxFrameLength() : 1048576;
  48 + this.headerLength = lengthFieldOffset + lengthFieldLength;
  49 + }
  50 +
  51 + @Override
  52 + public IotTcpCodecTypeEnum getType() {
  53 + return IotTcpCodecTypeEnum.LENGTH_FIELD;
  54 + }
  55 +
  56 + @Override
  57 + public RecordParser createDecodeParser(Handler<Buffer> handler) {
  58 + // 创建状态机:先读取头部,再读取消息体
  59 + RecordParser parser = RecordParser.newFixed(headerLength);
  60 + // 使用数组保存状态和头部数据
  61 + // TODO @AI:bodyLength 只使用第 0 位,是不是 atomicInteger 更合适?
  62 + final int[] bodyLength = {-1};
  63 + final Buffer[] headerBuffer = {null};
  64 +
  65 + // 处理读取到的数据
  66 + parser.handler(buffer -> {
  67 + if (bodyLength[0] == -1) {
  68 + // 阶段 1: 读取头部,解析长度字段
  69 + headerBuffer[0] = buffer.copy();
  70 + int length = readLength(buffer, lengthFieldOffset, lengthFieldLength);
  71 + int frameBodyLength = length + lengthAdjustment;
  72 + // 检查帧长度是否超过限制
  73 + if (frameBodyLength < 0 || frameBodyLength > maxFrameLength - headerLength) {
  74 + log.warn("[createDecodeParser][帧长度异常,length: {}, frameBodyLength: {}, maxFrameLength: {}]",
  75 + length, frameBodyLength, maxFrameLength);
  76 + return;
  77 + }
  78 +
  79 + if (frameBodyLength == 0) {
  80 + // 消息体为空,直接处理
  81 + // TODO @AI:消息体为空,是不是不合理哈?应该抛出异常?
  82 + Buffer frame = processFrame(headerBuffer[0], null);
  83 + handler.handle(frame);
  84 + } else {
  85 + // 切换到读取消息体模式
  86 + bodyLength[0] = frameBodyLength;
  87 + parser.fixedSizeMode(frameBodyLength);
  88 + }
  89 + } else {
  90 + // 阶段 2: 读取消息体,组装完整帧
  91 + Buffer frame = processFrame(headerBuffer[0], buffer);
  92 + // 重置状态,准备读取下一帧
  93 + bodyLength[0] = -1;
  94 + headerBuffer[0] = null;
  95 + parser.fixedSizeMode(headerLength);
  96 +
  97 + // 处理完整消息
  98 + handler.handle(frame);
  99 + }
  100 + });
  101 +
  102 + parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex));
  103 + return parser;
  104 + }
  105 +
  106 + @Override
  107 + public Buffer encode(byte[] data) {
  108 + Buffer buffer = Buffer.buffer();
  109 + // 计算要写入的长度值
  110 + int lengthValue = data.length - lengthAdjustment;
  111 + // 写入偏移量前的填充字节(如果有)
  112 + for (int i = 0; i < lengthFieldOffset; i++) {
  113 + buffer.appendByte((byte) 0);
  114 + }
  115 + // 写入长度字段
  116 + writeLength(buffer, lengthValue, lengthFieldLength);
  117 + // 写入消息体
  118 + buffer.appendBytes(data);
  119 + return buffer;
  120 + }
  121 +
  122 + /**
  123 + * 从 Buffer 中读取长度字段
  124 + */
  125 + // TODO @AI:兼容 JDK8
  126 + private int readLength(Buffer buffer, int offset, int length) {
  127 + return switch (length) {
  128 + case 1 -> buffer.getUnsignedByte(offset);
  129 + case 2 -> buffer.getUnsignedShort(offset);
  130 + case 4 -> buffer.getInt(offset);
  131 + default -> throw new IllegalArgumentException("不支持的长度字段长度: " + length);
  132 + };
  133 + }
  134 +
  135 + /**
  136 + * 向 Buffer 中写入长度字段
  137 + */
  138 + // TODO @AI:兼容 JDK8
  139 + private void writeLength(Buffer buffer, int length, int fieldLength) {
  140 + switch (fieldLength) {
  141 + case 1 -> buffer.appendByte((byte) length);
  142 + case 2 -> buffer.appendShort((short) length);
  143 + case 4 -> buffer.appendInt(length);
  144 + default -> throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength);
  145 + }
  146 + }
  147 +
  148 + /**
  149 + * 处理帧数据(根据 initialBytesToStrip 跳过指定字节)
  150 + */
  151 + private Buffer processFrame(Buffer header, Buffer body) {
  152 + Buffer fullFrame = Buffer.buffer();
  153 + if (header != null) {
  154 + fullFrame.appendBuffer(header);
  155 + }
  156 + if (body != null) {
  157 + fullFrame.appendBuffer(body);
  158 + }
  159 + // 根据 initialBytesToStrip 跳过指定字节
  160 + if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) {
  161 + return fullFrame.slice(initialBytesToStrip, fullFrame.length());
  162 + }
  163 + return fullFrame;
  164 + }
  165 +
  166 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
4 5 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
5   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  6 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  7 +import io.vertx.core.buffer.Buffer;
6 8 import lombok.RequiredArgsConstructor;
7 9 import lombok.extern.slf4j.Slf4j;
8 10  
... ... @@ -15,32 +17,43 @@ import lombok.extern.slf4j.Slf4j;
15 17 @RequiredArgsConstructor
16 18 public class IotTcpDownstreamHandler {
17 19  
18   - private final IotDeviceMessageService deviceMessageService;
19   -
20 20 private final IotTcpConnectionManager connectionManager;
21 21  
22 22 /**
  23 + * TCP 帧编解码器(处理粘包/拆包)
  24 + */
  25 + private final IotTcpFrameCodec codec;
  26 + /**
  27 + * 消息序列化器(处理业务消息序列化/反序列化)
  28 + */
  29 + private final IotMessageSerializer serializer;
  30 +
  31 + /**
23 32 * 处理下行消息
24 33 */
25 34 public void handle(IotDeviceMessage message) {
26 35 try {
27 36 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
28 37 message.getDeviceId(), message.getMethod(), message.getId());
29   -
30   - // 1. 获取连接信息(包含 codecType)
  38 + // 1. 检查设备连接
31 39 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
32 40 message.getDeviceId());
33 41 if (connectionInfo == null) {
34   - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
  42 + // TODO @AI:是不是把消息 id 也打印进去?类似上面的日志
  43 + log.warn("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
35 44 return;
36 45 }
37 46  
38   - // 2. 使用连接时的 codecType 编码消息,并发送到设备
39   - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
40   - boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
  47 + // 2. 序列化 + 帧编码
  48 + byte[] serializedData = serializer.serialize(message);
  49 + Buffer frameData = codec.encode(serializedData);
  50 +
  51 + // 3. 发送到设备
  52 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes());
  53 + // TODO @AI:不成功,直接抛出异常;反正下面的日志也会打印失败的
41 54 if (success) {
42 55 log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
43   - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
  56 + message.getDeviceId(), message.getMethod(), message.getId(), frameData.length());
44 57 } else {
45 58 log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
46 59 message.getDeviceId(), message.getMethod(), message.getId());
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
5 6 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -16,7 +16,7 @@ public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber
16 16  
17 17 private final IotTcpDownstreamHandler downstreamHandler;
18 18  
19   - public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol,
  19 + public IotTcpDownstreamSubscriber(IotProtocol protocol,
20 20 IotTcpDownstreamHandler downstreamHandler,
21 21 IotMessageBus messageBus) {
22 22 super(protocol, messageBus);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.hutool.core.util.BooleanUtil;
5 5 import cn.hutool.core.util.IdUtil;
6   -import cn.hutool.core.util.StrUtil;
7 6 import cn.hutool.extra.spring.SpringUtil;
8 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
... ... @@ -16,15 +15,16 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
16 15 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
17 16 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
18 17 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
19   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
20   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
21   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
22 20 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
  21 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
23 22 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
24 23 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
25 24 import io.vertx.core.Handler;
26 25 import io.vertx.core.buffer.Buffer;
27 26 import io.vertx.core.net.NetSocket;
  27 +import io.vertx.core.parsetools.RecordParser;
28 28 import lombok.extern.slf4j.Slf4j;
29 29  
30 30 import java.util.Map;
... ... @@ -37,9 +37,6 @@ import java.util.Map;
37 37 @Slf4j
38 38 public class IotTcpUpstreamHandler implements Handler<NetSocket> {
39 39  
40   - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
41   - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
42   -
43 40 private static final String AUTH_METHOD = "auth";
44 41  
45 42 private final IotDeviceMessageService deviceMessageService;
... ... @@ -52,15 +49,29 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
52 49  
53 50 private final String serverId;
54 51  
55   - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol,
  52 + /**
  53 + * TCP 帧编解码器(处理粘包/拆包)
  54 + */
  55 + private final IotTcpFrameCodec codec;
  56 + /**
  57 + * 消息序列化器(处理业务消息序列化/反序列化)
  58 + */
  59 + private final IotMessageSerializer serializer;
  60 +
  61 + public IotTcpUpstreamHandler(IotProtocol protocol,
56 62 IotDeviceMessageService deviceMessageService,
57 63 IotDeviceService deviceService,
58   - IotTcpConnectionManager connectionManager) {
  64 + IotTcpConnectionManager connectionManager,
  65 + IotTcpFrameCodec codec,
  66 + IotMessageSerializer serializer) {
  67 + this.serverId = protocol.getServerId();
  68 + this.codec = codec;
  69 + this.serializer = serializer;
  70 + this.connectionManager = connectionManager;
  71 + // TODO @AI:都通过 springutil 获取下;
59 72 this.deviceMessageService = deviceMessageService;
60 73 this.deviceService = deviceService;
61   - this.connectionManager = connectionManager;
62 74 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
63   - this.serverId = protocol.getServerId();
64 75 }
65 76  
66 77 @Override
... ... @@ -78,18 +89,32 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
78 89 cleanupConnection(socket);
79 90 });
80 91  
81   - // 设置消息处理器
82   - socket.handler(buffer -> {
83   - // TODO @AI:TODO @芋艿:这里应该有拆粘包的问题;
  92 + // 设置消息处理器(带拆包支持)
  93 + Handler<Buffer> messageHandler = buffer -> {
  94 + // TODO @AI:需要跟 AI 讨论。哪些情况关闭;哪些情况,发送异常消息;
84 95 try {
85 96 processMessage(clientId, buffer, socket);
86 97 } catch (Exception e) {
  98 + // TODO @AI:这里能合并到 exceptionHandler 么?还是怎么搞好点;
87 99 log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
88 100 clientId, socket.remoteAddress(), e.getMessage());
89 101 cleanupConnection(socket);
90 102 socket.close();
91 103 }
92   - });
  104 + };
  105 +
  106 + // 根据是否配置了 FrameCodec 来决定是否使用拆包器
  107 + // TODO @AI:必须配置!
  108 + if (codec != null) {
  109 + // 使用拆包器处理粘包/拆包
  110 + RecordParser parser = codec.createDecodeParser(messageHandler);
  111 + socket.handler(parser);
  112 + log.debug("[handle][启用 {} 拆包器,客户端 ID: {}]", codec.getType(), clientId);
  113 + } else {
  114 + // 未配置拆包器,直接处理原始数据(可能存在粘包问题)
  115 + socket.handler(messageHandler);
  116 + log.debug("[handle][未配置拆包器,客户端 ID: {}]", clientId);
  117 + }
93 118 }
94 119  
95 120 /**
... ... @@ -102,43 +127,42 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
102 127 */
103 128 private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception {
104 129 // 1. 基础检查
  130 + // TODO @AI:不太应该为空?!
105 131 if (buffer == null || buffer.length() == 0) {
106 132 return;
107 133 }
108 134  
109   - // 2. 获取消息格式类型
110   - String codecType = getMessageCodecType(buffer, socket);
111   -
112   - // 3. 解码消息
  135 + // 2. 反序列化消息
113 136 IotDeviceMessage message;
114 137 try {
115   - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
  138 + message = serializer.deserialize(buffer.getBytes());
116 139 if (message == null) {
117   - throw new Exception("解码后消息为空");
  140 + throw new IllegalArgumentException("反序列化后消息为空");
118 141 }
119 142 } catch (Exception e) {
120   - // 消息格式错误时抛出异常,由上层处理连接断开
121   - throw new Exception("消息解码失败: " + e.getMessage(), e);
  143 + // TODO @AI:是不是不用 try catch?
  144 + throw new Exception("消息反序列化失败: " + e.getMessage(), e);
122 145 }
123 146  
124   - // 4. 根据消息类型路由处理
  147 + // 3. 根据消息类型路由处理
125 148 try {
126 149 if (AUTH_METHOD.equals(message.getMethod())) {
127 150 // 认证请求
128   - handleAuthenticationRequest(clientId, message, codecType, socket);
  151 + handleAuthenticationRequest(clientId, message, socket);
129 152 } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
130 153 // 设备动态注册请求
131   - handleRegisterRequest(clientId, message, codecType, socket);
  154 + handleRegisterRequest(clientId, message, socket);
132 155 } else {
133 156 // 业务消息
134   - handleBusinessRequest(clientId, message, codecType, socket);
  157 + handleBusinessRequest(clientId, message, socket);
135 158 }
136 159 } catch (Exception e) {
137   - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]",
138   - clientId, message.getMethod(), e);
  160 + // TODO @AI:如果参数不正确,不断开连接;
  161 + log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", clientId, message.getMethod(), e);
139 162 // 发送错误响应,避免客户端一直等待
  163 + // TODO @AI:发送失败,是不是不用 try catch?
140 164 try {
141   - sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType);
  165 + sendErrorResponse(socket, message.getRequestId(), "消息处理失败");
142 166 } catch (Exception responseEx) {
143 167 log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx);
144 168 }
... ... @@ -148,74 +172,73 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
148 172 /**
149 173 * 处理认证请求
150 174 *
151   - * @param clientId 客户端 ID
152   - * @param message 消息信息
153   - * @param codecType 消息编解码类型
154   - * @param socket 网络连接
  175 + * @param clientId 客户端 ID
  176 + * @param message 消息信息
  177 + * @param socket 网络连接
155 178 */
156   - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType,
157   - NetSocket socket) {
  179 + private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) {
158 180 try {
159 181 // 1.1 解析认证参数
  182 + // TODO @AI:直接 JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);然后,校验参数,不正确抛出 invalid exception;和 http 那一样;
160 183 IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
161 184 if (authParams == null) {
162 185 log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId);
163   - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType);
  186 + sendErrorResponse(socket, message.getRequestId(), "认证参数不完整");
164 187 return;
165 188 }
166 189 // 1.2 执行认证
167 190 if (!validateDeviceAuth(authParams)) {
168 191 log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
169 192 clientId, authParams.getUsername());
170   - sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType);
  193 + sendErrorResponse(socket, message.getRequestId(), "认证失败");
171 194 return;
172 195 }
173 196  
174 197 // 2.1 解析设备信息
175 198 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
  199 + // TODO @AI:这里就断言 deviceInfo 不为空了?!
176 200 if (deviceInfo == null) {
177   - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
  201 + sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败");
178 202 return;
179 203 }
180 204 // 2.2 获取设备信息
181   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
182   - deviceInfo.getDeviceName());
  205 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  206 + // TODO @AI:这里就断言 device 不为空了?!
183 207 if (device == null) {
184   - sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType);
  208 + sendErrorResponse(socket, message.getRequestId(), "设备不存在");
185 209 return;
186 210 }
187 211  
188 212 // 3.1 注册连接
189   - registerConnection(socket, device, clientId, codecType);
  213 + registerConnection(socket, device, clientId);
190 214 // 3.2 发送上线消息
191 215 sendOnlineMessage(device);
192 216 // 3.3 发送成功响应
193   - sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType);
  217 + sendSuccessResponse(socket, message.getRequestId(), "认证成功");
194 218 log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
195 219 device.getId(), device.getDeviceName());
196 220 } catch (Exception e) {
  221 + // TODO @AI:最大化去掉 try catch;(这个方法里的)
197 222 log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
198   - sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType);
  223 + sendErrorResponse(socket, message.getRequestId(), "认证处理异常");
199 224 }
200 225 }
201 226  
202 227 /**
203 228 * 处理设备动态注册请求(一型一密,不需要认证)
204 229 *
205   - * @param clientId 客户端 ID
206   - * @param message 消息信息
207   - * @param codecType 消息编解码类型
208   - * @param socket 网络连接
  230 + * @param clientId 客户端 ID
  231 + * @param message 消息信息
  232 + * @param socket 网络连接
209 233 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
210 234 */
211   - private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType,
212   - NetSocket socket) {
  235 + private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) {
213 236 try {
214 237 // 1. 解析注册参数
215 238 IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
216 239 if (params == null) {
217 240 log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
218   - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType);
  241 + sendErrorResponse(socket, message.getRequestId(), "注册参数不完整");
219 242 return;
220 243 }
221 244  
... ... @@ -223,34 +246,33 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
223 246 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
224 247 if (result.isError()) {
225 248 log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
226   - sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType);
  249 + sendErrorResponse(socket, message.getRequestId(), result.getMsg());
227 250 return;
228 251 }
229 252  
230 253 // 3. 发送成功响应(包含 deviceSecret)
231   - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType);
  254 + sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData());
232 255 log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]",
233 256 clientId, params.getDeviceName());
234 257 } catch (Exception e) {
235 258 log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
236   - sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType);
  259 + sendErrorResponse(socket, message.getRequestId(), "注册处理异常");
237 260 }
238 261 }
239 262  
240 263 /**
241 264 * 处理业务请求
242 265 *
243   - * @param clientId 客户端 ID
244   - * @param message 消息信息
245   - * @param codecType 消息编解码类型
246   - * @param socket 网络连接
  266 + * @param clientId 客户端 ID
  267 + * @param message 消息信息
  268 + * @param socket 网络连接
247 269 */
248   - private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) {
  270 + private void handleBusinessRequest(String clientId, IotDeviceMessage message, NetSocket socket) {
249 271 try {
250 272 // 1. 检查认证状态
251 273 if (connectionManager.isNotAuthenticated(socket)) {
252 274 log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId);
253   - sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType);
  275 + sendErrorResponse(socket, message.getRequestId(), "请先进行认证");
254 276 return;
255 277 }
256 278  
... ... @@ -268,41 +290,18 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
268 290 }
269 291  
270 292 /**
271   - * 获取消息编解码类型
272   - *
273   - * @param buffer 消息
274   - * @param socket 网络连接
275   - * @return 消息编解码类型
276   - */
277   - private String getMessageCodecType(Buffer buffer, NetSocket socket) {
278   - // 1. 如果已认证,优先使用缓存的编解码类型
279   - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
280   - if (connectionInfo != null
281   - && StrUtil.isNotBlank(connectionInfo.getCodecType())) {
282   - return connectionInfo.getCodecType();
283   - }
284   -
285   - // 2. 未认证时检测消息格式类型
286   - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY
287   - : CODEC_TYPE_JSON;
288   - }
289   -
290   - /**
291 293 * 注册连接信息
292 294 *
293   - * @param socket 网络连接
294   - * @param device 设备
295   - * @param clientId 客户端 ID
296   - * @param codecType 消息编解码类型
  295 + * @param socket 网络连接
  296 + * @param device 设备
  297 + * @param clientId 客户端 ID
297 298 */
298   - private void registerConnection(NetSocket socket, IotDeviceRespDTO device,
299   - String clientId, String codecType) {
  299 + private void registerConnection(NetSocket socket, IotDeviceRespDTO device, String clientId) {
300 300 IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo()
301 301 .setDeviceId(device.getId())
302 302 .setProductKey(device.getProductKey())
303 303 .setDeviceName(device.getDeviceName())
304   - .setClientId(clientId)
305   - .setCodecType(codecType);
  304 + .setClientId(clientId);
306 305 // 注册连接
307 306 connectionManager.registerConnection(socket, device.getId(), connectionInfo);
308 307 }
... ... @@ -351,10 +350,10 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
351 350 * @param success 是否成功
352 351 * @param message 消息
353 352 * @param requestId 请求 ID
354   - * @param codecType 消息编解码类型
355 353 */
356   - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) {
  354 + private void sendResponse(NetSocket socket, boolean success, String message, String requestId) {
357 355 try {
  356 + // TODO @AI:是不是不用
358 357 Object responseData = MapUtil.builder()
359 358 .put("success", success)
360 359 .put("message", message)
... ... @@ -364,14 +363,17 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
364 363 IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData,
365 364 code, message);
366 365  
367   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
368   - socket.write(Buffer.buffer(encodedData));
  366 + // 序列化 + 帧编码
  367 + byte[] serializedData = serializer.serialize(responseMessage);
  368 + Buffer frameData = codec.encode(serializedData);
  369 + socket.write(frameData);
369 370  
370 371 } catch (Exception e) {
371 372 log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e);
372 373 }
373 374 }
374 375  
  376 + // TODO @AI:合并到 handleAuthenticationRequest 里;
375 377 /**
376 378 * 验证设备认证信息
377 379 *
... ... @@ -397,10 +399,9 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
397 399 * @param socket 网络连接
398 400 * @param requestId 请求 ID
399 401 * @param errorMessage 错误消息
400   - * @param codecType 消息编解码类型
401 402 */
402   - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) {
403   - sendResponse(socket, false, errorMessage, requestId, codecType);
  403 + private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage) {
  404 + sendResponse(socket, false, errorMessage, requestId);
404 405 }
405 406  
406 407 /**
... ... @@ -409,11 +410,10 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
409 410 * @param socket 网络连接
410 411 * @param requestId 请求 ID
411 412 * @param message 消息
412   - * @param codecType 消息编解码类型
413 413 */
414 414 @SuppressWarnings("SameParameterValue")
415   - private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) {
416   - sendResponse(socket, true, message, requestId, codecType);
  415 + private void sendSuccessResponse(NetSocket socket, String requestId, String message) {
  416 + sendResponse(socket, true, message, requestId);
417 417 }
418 418  
419 419 /**
... ... @@ -489,17 +489,17 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
489 489 * @param socket 网络连接
490 490 * @param requestId 请求 ID
491 491 * @param registerResp 注册响应
492   - * @param codecType 消息编解码类型
493 492 */
494 493 private void sendRegisterSuccessResponse(NetSocket socket, String requestId,
495   - IotDeviceRegisterRespDTO registerResp, String codecType) {
  494 + IotDeviceRegisterRespDTO registerResp) {
496 495 try {
497 496 // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
498 497 IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
499 498 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
500   - // 2. 发送响应
501   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
502   - socket.write(Buffer.buffer(encodedData));
  499 + // 2. 序列化 + 帧编码
  500 + byte[] serializedData = serializer.serialize(responseMessage);
  501 + Buffer frameData = codec.encode(serializedData);
  502 + socket.write(frameData);
503 503 } catch (Exception e) {
504 504 log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e);
505 505 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
... ... @@ -81,6 +81,7 @@ public class IotTcpConnectionManager {
81 81 return info != null;
82 82 }
83 83  
  84 + // TODO @AI:是不是可以去掉;因为现在只有认证成功的,才会注册连接;
84 85 /**
85 86 * 检查连接是否未认证
86 87 */
... ... @@ -148,10 +149,6 @@ public class IotTcpConnectionManager {
148 149 * 客户端 ID
149 150 */
150 151 private String clientId;
151   - /**
152   - * 消息编解码类型(认证后确定)
153   - */
154   - private String codecType;
155 152  
156 153 }
157 154  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -42,7 +42,7 @@ yudao:
42 42 secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位
43 43 expiration: 7d
44 44  
45   - # 协议实例列表(新版配置方式)
  45 + # 协议实例列表
46 46 protocols:
47 47 # ====================================
48 48 # 针对引入的 HTTP 组件的配置
... ... @@ -53,6 +53,22 @@ yudao:
53 53 enabled: true
54 54 http:
55 55 ssl-enabled: false
  56 + # ====================================
  57 + # 针对引入的 TCP 组件的配置
  58 + # ====================================
  59 + - id: tcp-json
  60 + type: tcp
  61 + port: 8091
  62 + enabled: true
  63 + serialize: json
  64 + tcp:
  65 + max-connections: 1000
  66 + keep-alive-timeout-ms: 30000
  67 + ssl-enabled: false
  68 + codec:
  69 + type: delimiter # 拆包类型:length_field / delimiter / fixed_length
  70 + delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符)
  71 + max-frame-length: 1048576 # 最大帧长度(字节)
56 72  
57 73 # 协议配置(旧版,保持兼容)
58 74 protocol:
... ... @@ -91,17 +107,6 @@ yudao:
91 107 trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
92 108 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
93 109 # ====================================
94   - # 针对引入的 TCP 组件的配置
95   - # ====================================
96   - tcp:
97   - enabled: false
98   - port: 8091
99   - keep-alive-timeout-ms: 30000
100   - max-connections: 1000
101   - ssl-enabled: false
102   - ssl-cert-path: "classpath:certs/client.jks"
103   - ssl-key-path: "classpath:certs/client.jks"
104   - # ====================================
105 110 # 针对引入的 UDP 组件的配置
106 111 # ====================================
107 112 udp:
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java
... ... @@ -10,32 +10,40 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
13   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
14   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
15   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
  13 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  14 +import cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer;
  15 +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
16 16 import lombok.extern.slf4j.Slf4j;
17 17 import org.junit.jupiter.api.Disabled;
18 18 import org.junit.jupiter.api.Test;
19 19  
20   -import java.io.InputStream;
  20 +import java.io.BufferedReader;
  21 +import java.io.InputStreamReader;
21 22 import java.io.OutputStream;
22 23 import java.net.Socket;
  24 +import java.nio.charset.StandardCharsets;
23 25  
24 26 /**
25 27 * IoT 直连设备 TCP 协议集成测试(手动测试)
26 28 *
27 29 * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台
28 30 *
29   - * <p>支持两种编解码格式:
  31 + * <p>支持两种序列化格式:
30 32 * <ul>
31   - * <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
32   - * <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
  33 + * <li>{@link IotJsonSerializer} - JSON 格式</li>
  34 + * <li>{@link IotBinarySerializer} - 二进制格式</li>
  35 + * </ul>
  36 + *
  37 + * <p>TCP 拆包配置(需与 application.yaml 中的 codec 配置一致):
  38 + * <ul>
  39 + * <li>type: delimiter - 基于分隔符拆包</li>
  40 + * <li>delimiter: \n - 换行符作为分隔符</li>
33 41 * </ul>
34 42 *
35 43 * <p>使用步骤:
36 44 * <ol>
37 45 * <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
38   - * <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
  46 + * <li>修改 {@link #SERIALIZER} 选择测试的序列化格式(Delimiter 模式只支持 JSON)</li>
39 47 * <li>运行以下测试方法:
40 48 * <ul>
41 49 * <li>{@link #testAuth()} - 设备认证</li>
... ... @@ -58,10 +66,16 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
58 66 private static final int SERVER_PORT = 8091;
59 67 private static final int TIMEOUT_MS = 5000;
60 68  
61   - // ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
  69 + // TODO @AI:这里可以通过 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec 么?例如说:使用 vertx vertx tcp client???从而更好的复用解码逻辑;
  70 + /**
  71 + * 分隔符(需与 application.yaml 中的 delimiter 配置一致)
  72 + */
  73 + private static final String DELIMITER = "\n";
62 74  
63   -// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
64   - private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
  75 + // ===================== 序列化器选择(Delimiter 模式推荐使用 JSON) =====================
  76 +
  77 + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
  78 +// private static final IotMessageSerializer SERIALIZER = new IotBinarySerializer();
65 79  
66 80 // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
67 81  
... ... @@ -82,18 +96,19 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
82 96 .setClientId(authInfo.getClientId())
83 97 .setUsername(authInfo.getUsername())
84 98 .setPassword(authInfo.getPassword());
85   - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
86   - // 1.2 编码
87   - byte[] payload = CODEC.encode(request);
88   - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
  99 + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
  100 + // 1.2 序列化
  101 + // TODO @AI:是不是把 SERIALIZER 放到 sendAndReceive 里;
  102 + byte[] payload = SERIALIZER.serialize(request);
  103 + log.info("[testAuth][Serializer: {}, 请求消息: {}, 数据包长度: {} 字节]", SERIALIZER.getType(), request, payload.length);
89 104  
90 105 // 2.1 发送请求
91 106 try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
92 107 socket.setSoTimeout(TIMEOUT_MS);
93 108 byte[] responseBytes = sendAndReceive(socket, payload);
94   - // 2.2 解码响应
  109 + // 2.2 反序列化响应
95 110 if (responseBytes != null) {
96   - IotDeviceMessage response = CODEC.decode(responseBytes);
  111 + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes);
97 112 log.info("[testAuth][响应消息: {}]", response);
98 113 } else {
99 114 log.warn("[testAuth][未收到响应]");
... ... @@ -119,17 +134,17 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
119 134 registerReqDTO.setProductSecret("test-product-secret");
120 135 IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
121 136 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
122   - // 1.2 编码
123   - byte[] payload = CODEC.encode(request);
124   - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
  137 + // 1.2 序列化
  138 + byte[] payload = SERIALIZER.serialize(request);
  139 + log.info("[testDeviceRegister][Serializer: {}, 请求消息: {}, 数据包长度: {} 字节]", SERIALIZER.getType(), request, payload.length);
125 140  
126 141 // 2.1 发送请求
127 142 try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
128 143 socket.setSoTimeout(TIMEOUT_MS);
129 144 byte[] responseBytes = sendAndReceive(socket, payload);
130   - // 2.2 解码响应
  145 + // 2.2 反序列化响应
131 146 if (responseBytes != null) {
132   - IotDeviceMessage response = CODEC.decode(responseBytes);
  147 + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes);
133 148 log.info("[testDeviceRegister][响应消息: {}]", response);
134 149 log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
135 150 } else {
... ... @@ -161,15 +176,15 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
161 176 .put("height", "2")
162 177 .build()),
163 178 null, null, null);
164   - // 2.2 编码
165   - byte[] payload = CODEC.encode(request);
166   - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  179 + // 2.2 序列化
  180 + byte[] payload = SERIALIZER.serialize(request);
  181 + log.info("[testPropertyPost][Serializer: {}, 请求消息: {}]", SERIALIZER.getType(), request);
167 182  
168 183 // 3.1 发送请求
169 184 byte[] responseBytes = sendAndReceive(socket, payload);
170   - // 3.2 解码响应
  185 + // 3.2 反序列化响应
171 186 if (responseBytes != null) {
172   - IotDeviceMessage response = CODEC.decode(responseBytes);
  187 + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes);
173 188 log.info("[testPropertyPost][响应消息: {}]", response);
174 189 } else {
175 190 log.warn("[testPropertyPost][未收到响应]");
... ... @@ -200,15 +215,15 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
200 215 MapUtil.<String, Object>builder().put("rice", 3).build(),
201 216 System.currentTimeMillis()),
202 217 null, null, null);
203   - // 2.2 编码
204   - byte[] payload = CODEC.encode(request);
205   - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  218 + // 2.2 序列化
  219 + byte[] payload = SERIALIZER.serialize(request);
  220 + log.info("[testEventPost][Serializer: {}, 请求消息: {}]", SERIALIZER.getType(), request);
206 221  
207 222 // 3.1 发送请求
208 223 byte[] responseBytes = sendAndReceive(socket, payload);
209   - // 3.2 解码响应
  224 + // 3.2 反序列化响应
210 225 if (responseBytes != null) {
211   - IotDeviceMessage response = CODEC.decode(responseBytes);
  226 + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes);
212 227 log.info("[testEventPost][响应消息: {}]", response);
213 228 } else {
214 229 log.warn("[testEventPost][未收到响应]");
... ... @@ -231,41 +246,44 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
231 246 .setUsername(authInfo.getUsername())
232 247 .setPassword(authInfo.getPassword());
233 248 IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
234   - byte[] payload = CODEC.encode(request);
  249 + byte[] payload = SERIALIZER.serialize(request);
235 250 byte[] responseBytes = sendAndReceive(socket, payload);
236 251 if (responseBytes != null) {
237 252 log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]",
238 253 responseBytes.length,
239 254 String.format("%02X", responseBytes[0]),
240 255 HexUtil.encodeHexStr(responseBytes));
241   - return CODEC.decode(responseBytes);
  256 + return SERIALIZER.deserialize(responseBytes);
242 257 }
243 258 return null;
244 259 }
245 260  
246 261 /**
247   - * 发送 TCP 请求并接收响应
  262 + * 发送 TCP 请求并接收响应(支持 Delimiter 分隔符协议)
  263 + * <p>
  264 + * 发送格式:[消息体][分隔符]
  265 + * 接收格式:[消息体][分隔符]
248 266 *
249 267 * @param socket TCP Socket
250   - * @param payload 请求数据
251   - * @return 响应数据
  268 + * @param payload 请求数据(消息体,不含分隔符)
  269 + * @return 响应数据(消息体,不含分隔符)
252 270 */
253 271 private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
254   - // 1. 发送请求
255 272 OutputStream out = socket.getOutputStream();
256   - InputStream in = socket.getInputStream();
  273 + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
  274 +
  275 + // 1. 发送请求(添加分隔符后缀)
257 276 out.write(payload);
  277 + out.write(DELIMITER.getBytes(StandardCharsets.UTF_8));
258 278 out.flush();
  279 + log.info("[sendAndReceive][发送数据: {} 字节(不含分隔符)]", payload.length);
259 280  
260   - // 2.1 等待一小段时间让服务器处理
261   - Thread.sleep(100);
262   - // 2.2 接收响应
263   - byte[] buffer = new byte[4096];
  281 + // 2. 接收响应(读取到分隔符为止)
264 282 try {
265   - int length = in.read(buffer);
266   - if (length > 0) {
267   - byte[] response = new byte[length];
268   - System.arraycopy(buffer, 0, response, 0, length);
  283 + String responseLine = in.readLine();
  284 + if (responseLine != null) {
  285 + byte[] response = responseLine.getBytes(StandardCharsets.UTF_8);
  286 + log.info("[sendAndReceive][接收数据: {} 字节]", response.length);
269 287 return response;
270 288 }
271 289 return null;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md deleted
1   -# TCP 二进制协议数据包格式说明
2   -
3   -## 1. 协议概述
4   -
5   -TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。
6   -
7   -### 1.1 协议特点
8   -
9   -- **高效传输**:完全二进制格式,减少数据传输量
10   -- **版本控制**:内置协议版本号,支持协议升级
11   -- **类型安全**:明确的消息类型标识
12   -- **简洁设计**:去除冗余字段,协议更加精简
13   -- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容
14   -
15   -## 2. 协议格式
16   -
17   -### 2.1 整体结构
18   -
19   -```
20   -+--------+--------+--------+---------------------------+--------+--------+
21   -| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) |
22   -+--------+--------+--------+---------------------------+--------+--------+
23   -| 消息 ID 长度(2字节) | 消息 ID (变长字符串) |
24   -+--------+--------+--------+--------+--------+--------+--------+--------+
25   -| 方法名长度(2字节) | 方法名(变长字符串) |
26   -+--------+--------+--------+--------+--------+--------+--------+--------+
27   -| 消息体数据(变长) |
28   -+--------+--------+--------+--------+--------+--------+--------+--------+
29   -```
30   -
31   -### 2.2 字段详细说明
32   -
33   -| 字段 | 长度 | 类型 | 说明 |
34   -|------|------|------|------|
35   -| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 |
36   -| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 |
37   -| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 |
38   -| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) |
39   -| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 |
40   -| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) |
41   -| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 |
42   -| 方法名 | 变长 | string | 消息方法名(UTF-8编码) |
43   -| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 |
44   -
45   -**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
46   -
47   -### 2.3 协议常量定义
48   -
49   -```java
50   -// 协议标识
51   -private static final byte MAGIC_NUMBER = (byte) 0x7E;
52   -private static final byte PROTOCOL_VERSION = (byte) 0x01;
53   -
54   -// 消息类型
55   -private static final byte REQUEST = (byte) 0x01; // 请求消息
56   -private static final byte RESPONSE = (byte) 0x02; // 响应消息
57   -
58   -// 协议长度
59   -private static final int HEADER_FIXED_LENGTH = 7; // 固定头部长度
60   -private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度
61   -```
62   -
63   -## 3. 消息类型和格式
64   -
65   -### 3.1 请求消息 (REQUEST - 0x01)
66   -
67   -请求消息用于设备向服务器发送数据或请求。
68   -
69   -#### 3.1.1 消息体格式
70   -```
71   -消息体 = params 数据(JSON格式)
72   -```
73   -
74   -#### 3.1.2 示例:设备认证请求
75   -
76   -**消息内容:**
77   -- 消息 ID: `auth_1704067200000_123`
78   -- 方法名: `auth`
79   -- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}`
80   -
81   -**二进制数据包结构:**
82   -```
83   -7E // 魔术字 (0x7E)
84   -01 // 版本号 (0x01)
85   -01 // 消息类型 (REQUEST)
86   -00 00 00 89 // 消息长度 (137字节)
87   -00 19 // 消息 ID 长度 (25字节)
88   -61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123"
89   -36 37 32 30 30 30 30 30 5F 31
90   -32 33
91   -00 04 // 方法名长度 (4字节)
92   -61 75 74 68 // 方法名: "auth"
93   -7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据
94   -22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001",
95   -30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName",
96   -6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"}
97   -64 75 63 74 4B 65 79 5F 64 65
98   -76 69 63 65 4E 61 6D 65 22 2C
99   -22 70 61 73 73 77 6F 72 64 22
100   -3A 22 64 65 76 69 63 65 5F 70
101   -61 73 73 77 6F 72 64 22 7D
102   -```
103   -
104   -#### 3.1.3 示例:属性数据上报
105   -
106   -**消息内容:**
107   -- 消息 ID: `property_1704067200000_456`
108   -- 方法名: `thing.property.post`
109   -- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}`
110   -
111   -### 3.2 响应消息 (RESPONSE - 0x02)
112   -
113   -响应消息用于服务器向设备回复请求结果。
114   -
115   -#### 3.2.1 消息体格式
116   -```
117   -消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON)
118   -```
119   -
120   -#### 3.2.2 字段说明
121   -
122   -| 字段 | 长度 | 类型 | 说明 |
123   -|------|------|------|------|
124   -| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 |
125   -| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 |
126   -| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) |
127   -| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) |
128   -
129   -#### 3.2.3 示例:认证成功响应
130   -
131   -**消息内容:**
132   -- 消息 ID: `auth_response_1704067200000_123`
133   -- 方法名: `auth`
134   -- 响应码: `0`
135   -- 响应消息: `认证成功`
136   -- 响应数据: `{"success":true,"message":"认证成功"}`
137   -
138   -**二进制数据包结构:**
139   -```
140   -7E // 魔术字 (0x7E)
141   -01 // 版本号 (0x01)
142   -02 // 消息类型 (RESPONSE)
143   -00 00 00 A4 // 消息长度 (164字节)
144   -00 22 // 消息 ID 长度 (34字节)
145   -61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123"
146   -6E 73 65 5F 31 37 30 34 30 36
147   -37 32 30 30 30 30 30 5F 31 32
148   -33
149   -00 04 // 方法名长度 (4字节)
150   -61 75 74 68 // 方法名: "auth"
151   -00 00 00 00 // 响应码 (0 = 成功)
152   -00 0C // 响应消息长度 (12字节)
153   -E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8)
154   -8A 9F
155   -7B 22 73 75 63 63 65 73 73 22 // JSON响应数据
156   -3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"}
157   -73 61 67 65 22 3A 22 E8 AE A4
158   -E8 AF 81 E6 88 90 E5 8A 9F 22
159   -7D
160   -```
161   -
162   -## 4. 编解码器标识
163   -
164   -```java
165   -public static final String TYPE = "TCP_BINARY";
166   -```
167   -
168   -## 5. 协议优势
169   -
170   -- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量
171   -- **解析高效**:直接二进制操作,减少字符串转换开销
172   -- **类型安全**:明确的消息类型和字段定义
173   -- **设计简洁**:去除冗余字段,协议更加精简高效
174   -- **版本控制**:内置版本号支持协议升级
175   -
176   -## 6. 与 JSON 协议对比
177   -
178   -| 特性 | 二进制协议 | JSON协议 |
179   -|------|-------------|--------|
180   -| 数据大小 | 小(节省30-50%) | 大 |
181   -| 解析性能 | 高 | 中等 |
182   -| 网络开销 | 低 | 高 |
183   -| 可读性 | 差 | 优秀 |
184   -| 调试难度 | 高 | 低 |
185   -| 扩展性 | 良好 | 优秀 |
186   -
187   -**推荐场景**:
188   -- ✅ **高频数据传输**:传感器数据实时上报
189   -- ✅ **带宽受限环境**:移动网络、卫星通信
190   -- ✅ **性能要求高**:需要低延迟、高吞吐的场景
191   -- ✅ **设备资源有限**:嵌入式设备、低功耗设备
192   -- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议
193   -- ❌ **快速原型开发**:开发效率低
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md deleted
1   -# TCP JSON 格式协议说明
2   -
3   -## 1. 协议概述
4   -
5   -TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点:
6   -
7   -- **标准化**:使用标准 JSON 格式,易于解析和处理
8   -- **可读性**:人类可读,便于调试和维护
9   -- **扩展性**:可以轻松添加新字段,向后兼容
10   -- **跨平台**:JSON 格式支持所有主流编程语言
11   -- **安全优化**:移除冗余的 deviceId 字段,提高安全性
12   -
13   -## 2. 消息格式
14   -
15   -### 2.1 基础消息结构
16   -
17   -```json
18   -{
19   - "id": "消息唯一标识",
20   - "method": "消息方法",
21   - "params": {
22   - // 请求参数
23   - },
24   - "data": {
25   - // 响应数据
26   - },
27   - "code": 响应码,
28   - "msg": "响应消息",
29   - "timestamp": 时间戳
30   -}
31   -```
32   -
33   -**⚠️ 重要说明**:
34   -- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID
35   -- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息
36   -
37   -### 2.2 字段详细说明
38   -
39   -| 字段名 | 类型 | 必填 | 用途 | 说明 |
40   -|--------|------|------|------|------|
41   -| id | String | 是 | 所有消息 | 消息唯一标识 |
42   -| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` |
43   -| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 |
44   -| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 |
45   -| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 |
46   -| msg | String | 否 | 响应消息 | 响应提示信息 |
47   -| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 |
48   -
49   -### 2.3 消息分类
50   -
51   -#### 2.3.1 请求消息(上行)
52   -- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段
53   -- **方向**:设备 → 服务器
54   -- **用途**:设备认证、数据上报、状态更新等
55   -
56   -#### 2.3.2 响应消息(下行)
57   -- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段
58   -- **方向**:服务器 → 设备
59   -- **用途**:认证结果、指令响应、错误提示等
60   -
61   -## 3. 消息示例
62   -
63   -### 3.1 设备认证 (auth)
64   -
65   -#### 认证请求格式
66   -**消息方向**:设备 → 服务器
67   -
68   -```json
69   -{
70   - "id": "auth_1704067200000_123",
71   - "method": "auth",
72   - "params": {
73   - "clientId": "device_001",
74   - "username": "productKey_deviceName",
75   - "password": "设备密码"
76   - },
77   - "timestamp": 1704067200000
78   -}
79   -```
80   -
81   -**认证参数说明:**
82   -
83   -| 字段名 | 类型 | 必填 | 说明 |
84   -|--------|------|------|------|
85   -| clientId | String | 是 | 客户端唯一标识,用于连接管理 |
86   -| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` |
87   -| password | String | 是 | 设备密码,在设备管理平台配置 |
88   -
89   -#### 认证响应格式
90   -**消息方向**:服务器 → 设备
91   -
92   -**认证成功响应:**
93   -```json
94   -{
95   - "id": "response_auth_1704067200000_123",
96   - "method": "auth",
97   - "data": {
98   - "success": true,
99   - "message": "认证成功"
100   - },
101   - "code": 0,
102   - "msg": "认证成功",
103   - "timestamp": 1704067200001
104   -}
105   -```
106   -
107   -**认证失败响应:**
108   -```json
109   -{
110   - "id": "response_auth_1704067200000_123",
111   - "method": "auth",
112   - "data": {
113   - "success": false,
114   - "message": "认证失败:用户名或密码错误"
115   - },
116   - "code": 401,
117   - "msg": "认证失败",
118   - "timestamp": 1704067200001
119   -}
120   -```
121   -
122   -### 3.2 属性数据上报 (thing.property.post)
123   -
124   -**消息方向**:设备 → 服务器
125   -
126   -**示例:温度传感器数据上报**
127   -```json
128   -{
129   - "id": "property_1704067200000_456",
130   - "method": "thing.property.post",
131   - "params": {
132   - "temperature": 25.5,
133   - "humidity": 60.2,
134   - "pressure": 1013.25,
135   - "battery": 85,
136   - "signal_strength": -65
137   - },
138   - "timestamp": 1704067200000
139   -}
140   -```
141   -
142   -### 3.3 设备状态更新 (thing.state.update)
143   -
144   -**消息方向**:设备 → 服务器
145   -
146   -**示例:心跳请求**
147   -```json
148   -{
149   - "id": "heartbeat_1704067200000_321",
150   - "method": "thing.state.update",
151   - "params": {
152   - "state": "online",
153   - "uptime": 86400,
154   - "memory_usage": 65.2,
155   - "cpu_usage": 12.8
156   - },
157   - "timestamp": 1704067200000
158   -}
159   -```
160   -
161   -## 4. 编解码器标识
162   -
163   -```java
164   -public static final String TYPE = "TCP_JSON";
165   -```
166   -
167   -## 5. 协议优势
168   -
169   -- **开发效率高**:JSON 格式,开发和调试简单
170   -- **跨语言支持**:所有主流语言都支持 JSON
171   -- **可读性优秀**:可以直接查看消息内容
172   -- **扩展性强**:可以轻松添加新字段
173   -- **安全性高**:移除 deviceId 字段,防止伪造攻击
174   -
175   -## 6. 与二进制协议对比
176   -
177   -| 特性 | JSON协议 | 二进制协议 |
178   -|------|----------|------------|
179   -| 开发难度 | 低 | 高 |
180   -| 调试难度 | 低 | 高 |
181   -| 可读性 | 优秀 | 差 |
182   -| 数据大小 | 中等 | 小(节省30-50%) |
183   -| 解析性能 | 中等 | 高 |
184   -| 学习成本 | 低 | 高 |
185   -
186   -**推荐场景**:
187   -- ✅ **开发调试阶段**:调试友好,开发效率高
188   -- ✅ **快速原型开发**:实现简单,快速迭代
189   -- ✅ **多语言集成**:广泛的语言支持
190   -- ❌ **高频数据传输**:建议使用二进制协议
191   -- ❌ **带宽受限环境**:建议使用二进制协议
192 0 \ No newline at end of file