Commit 44b1950e4a4825bbe8488d257ee0cb99cd568ea2
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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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<NetSocket> { |
| 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 |