Commit a5a3aea522a13300973dd343aafa20a16c80ba0a
1 parent
0593dbc9
feat:【IoT 物联网】新增 TCP 协议支持,添加 TCP 连接管理、上下行消息处理及相关配置
Showing
11 changed files
with
495 additions
and
41 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.config; | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; |
| 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | ||
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; | ||
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | ||
| 14 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 15 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 9 | import io.vertx.core.Vertx; | 16 | import io.vertx.core.Vertx; |
| 10 | import lombok.extern.slf4j.Slf4j; | 17 | import lombok.extern.slf4j.Slf4j; |
| 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | 18 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| @@ -33,7 +40,7 @@ public class IotGatewayConfiguration { | @@ -33,7 +40,7 @@ public class IotGatewayConfiguration { | ||
| 33 | 40 | ||
| 34 | @Bean | 41 | @Bean |
| 35 | public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, | 42 | public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, |
| 36 | - IotMessageBus messageBus) { | 43 | + IotMessageBus messageBus) { |
| 37 | return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); | 44 | return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); |
| 38 | } | 45 | } |
| 39 | } | 46 | } |
| @@ -53,21 +60,51 @@ public class IotGatewayConfiguration { | @@ -53,21 +60,51 @@ public class IotGatewayConfiguration { | ||
| 53 | 60 | ||
| 54 | @Bean | 61 | @Bean |
| 55 | public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, | 62 | public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, |
| 56 | - Vertx emqxVertx) { | 63 | + Vertx emqxVertx) { |
| 57 | return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); | 64 | return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); |
| 58 | } | 65 | } |
| 59 | 66 | ||
| 60 | @Bean | 67 | @Bean |
| 61 | public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, | 68 | public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, |
| 62 | - Vertx emqxVertx) { | 69 | + Vertx emqxVertx) { |
| 63 | return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); | 70 | return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); |
| 64 | } | 71 | } |
| 65 | 72 | ||
| 66 | @Bean | 73 | @Bean |
| 67 | public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, | 74 | public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, |
| 68 | - IotMessageBus messageBus) { | 75 | + IotMessageBus messageBus) { |
| 69 | return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); | 76 | return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); |
| 70 | } | 77 | } |
| 71 | } | 78 | } |
| 72 | 79 | ||
| 80 | + /** | ||
| 81 | + * IoT 网关 TCP 协议配置类 | ||
| 82 | + */ | ||
| 83 | + @Configuration | ||
| 84 | + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true") | ||
| 85 | + @Slf4j | ||
| 86 | + public static class TcpProtocolConfiguration { | ||
| 87 | + | ||
| 88 | + @Bean | ||
| 89 | + public Vertx tcpVertx() { | ||
| 90 | + return Vertx.vertx(); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + @Bean | ||
| 94 | + public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(Vertx tcpVertx, IotGatewayProperties gatewayProperties, | ||
| 95 | + IotTcpConnectionManager connectionManager, IotDeviceMessageService messageService, | ||
| 96 | + IotDeviceService deviceService, IotDeviceCommonApi deviceApi) { | ||
| 97 | + return new IotTcpUpstreamProtocol(tcpVertx, gatewayProperties, connectionManager, | ||
| 98 | + messageService, deviceService, deviceApi); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + @Bean | ||
| 102 | + public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol tcpUpstreamProtocol, | ||
| 103 | + IotMessageBus messageBus, | ||
| 104 | + IotTcpDownstreamHandler downstreamHandler) { | ||
| 105 | + return new IotTcpDownstreamSubscriber(tcpUpstreamProtocol, messageBus, downstreamHandler); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + } | ||
| 109 | + | ||
| 73 | } | 110 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -78,6 +78,11 @@ public class IotGatewayProperties { | @@ -78,6 +78,11 @@ public class IotGatewayProperties { | ||
| 78 | */ | 78 | */ |
| 79 | private EmqxProperties emqx; | 79 | private EmqxProperties emqx; |
| 80 | 80 | ||
| 81 | + /** | ||
| 82 | + * TCP 组件配置 | ||
| 83 | + */ | ||
| 84 | + private TcpProperties tcp; | ||
| 85 | + | ||
| 81 | } | 86 | } |
| 82 | 87 | ||
| 83 | @Data | 88 | @Data |
| @@ -96,6 +101,25 @@ public class IotGatewayProperties { | @@ -96,6 +101,25 @@ public class IotGatewayProperties { | ||
| 96 | } | 101 | } |
| 97 | 102 | ||
| 98 | @Data | 103 | @Data |
| 104 | + public static class TcpProperties { | ||
| 105 | + | ||
| 106 | + /** | ||
| 107 | + * 是否开启 | ||
| 108 | + */ | ||
| 109 | + @NotNull(message = "是否开启不能为空") | ||
| 110 | + private Boolean enabled; | ||
| 111 | + /** | ||
| 112 | + * 服务端口 | ||
| 113 | + */ | ||
| 114 | + private Integer serverPort; | ||
| 115 | + /** | ||
| 116 | + * 服务主机 | ||
| 117 | + */ | ||
| 118 | + private String serverHost; | ||
| 119 | + | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + @Data | ||
| 99 | public static class EmqxProperties { | 123 | public static class EmqxProperties { |
| 100 | 124 | ||
| 101 | /** | 125 | /** |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java
| @@ -97,28 +97,19 @@ public class IotEmqxDownstreamHandler { | @@ -97,28 +97,19 @@ public class IotEmqxDownstreamHandler { | ||
| 97 | break; | 97 | break; |
| 98 | 98 | ||
| 99 | case EVENT_POST: | 99 | case EVENT_POST: |
| 100 | - // TODO @haohao:不用 eventIdentifier 拼接哈,直接 data 里面,有 identifier 字段 | ||
| 101 | // 事件上报:只支持回复消息(下行) | 100 | // 事件上报:只支持回复消息(下行) |
| 102 | if (isReply) { | 101 | if (isReply) { |
| 103 | - String identifier = IotDeviceMessageUtils.getIdentifier(message); | ||
| 104 | - if (StrUtil.isNotBlank(identifier)) { | ||
| 105 | - return IotMqttTopicUtils.buildEventPostReplyTopic(productKey, deviceName, identifier); | ||
| 106 | - } | 102 | + return IotMqttTopicUtils.buildEventPostReplyTopicGeneric(productKey, deviceName); |
| 107 | } | 103 | } |
| 108 | break; | 104 | break; |
| 109 | 105 | ||
| 110 | case SERVICE_INVOKE: | 106 | case SERVICE_INVOKE: |
| 111 | // 服务调用:支持请求和回复 | 107 | // 服务调用:支持请求和回复 |
| 112 | - // TODO @haohao:不用 serviceIdentifier 拼接哈,直接 data 里面,有 identifier 字段 | ||
| 113 | - String serviceIdentifier = IotDeviceMessageUtils.getIdentifier(message); | ||
| 114 | - if (StrUtil.isNotBlank(serviceIdentifier)) { | ||
| 115 | - if (isReply) { | ||
| 116 | - return IotMqttTopicUtils.buildServiceReplyTopic(productKey, deviceName, serviceIdentifier); | ||
| 117 | - } else { | ||
| 118 | - return IotMqttTopicUtils.buildServiceTopic(productKey, deviceName, serviceIdentifier); | ||
| 119 | - } | 108 | + if (isReply) { |
| 109 | + return IotMqttTopicUtils.buildServiceReplyTopicGeneric(productKey, deviceName); | ||
| 110 | + } else { | ||
| 111 | + return IotMqttTopicUtils.buildServiceTopicGeneric(productKey, deviceName); | ||
| 120 | } | 112 | } |
| 121 | - break; | ||
| 122 | 113 | ||
| 123 | case CONFIG_PUSH: | 114 | case CONFIG_PUSH: |
| 124 | // 配置推送:平台向设备推送配置(下行请求),设备回复确认(上行回复) | 115 | // 配置推送:平台向设备推送配置(下行请求),设备回复确认(上行回复) |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConnectionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | + | ||
| 3 | +import io.vertx.core.net.NetSocket; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.stereotype.Component; | ||
| 6 | + | ||
| 7 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 8 | +import java.util.concurrent.ConcurrentMap; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * IoT TCP 连接管理器 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Component | ||
| 16 | +@Slf4j | ||
| 17 | +public class IotTcpConnectionManager { | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 连接集合 | ||
| 21 | + * | ||
| 22 | + * key:设备唯一标识 | ||
| 23 | + */ | ||
| 24 | + private final ConcurrentMap<String, NetSocket> connectionMap = new ConcurrentHashMap<>(); | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 添加一个新连接 | ||
| 28 | + * | ||
| 29 | + * @param deviceId 设备唯一标识 | ||
| 30 | + * @param socket Netty Channel | ||
| 31 | + */ | ||
| 32 | + public void addConnection(String deviceId, NetSocket socket) { | ||
| 33 | + log.info("[addConnection][设备({}) 连接({})]", deviceId, socket.remoteAddress()); | ||
| 34 | + connectionMap.put(deviceId, socket); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 根据设备 ID 获取连接 | ||
| 39 | + * | ||
| 40 | + * @param deviceId 设备 ID | ||
| 41 | + * @return 连接 | ||
| 42 | + */ | ||
| 43 | + public NetSocket getConnection(String deviceId) { | ||
| 44 | + return connectionMap.get(deviceId); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 移除指定连接 | ||
| 49 | + * | ||
| 50 | + * @param socket Netty Channel | ||
| 51 | + */ | ||
| 52 | + public void removeConnection(NetSocket socket) { | ||
| 53 | + connectionMap.entrySet().stream() | ||
| 54 | + .filter(entry -> entry.getValue().equals(socket)) | ||
| 55 | + .findFirst() | ||
| 56 | + .ifPresent(entry -> { | ||
| 57 | + log.info("[removeConnection][设备({}) 断开连接({})]", entry.getKey(), socket.remoteAddress()); | ||
| 58 | + connectionMap.remove(entry.getKey()); | ||
| 59 | + }); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | +} | ||
| 0 | \ No newline at end of file | 63 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | ||
| 8 | +import jakarta.annotation.PostConstruct; | ||
| 9 | +import lombok.RequiredArgsConstructor; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * IoT 网关 TCP 订阅者:接收下行给设备的消息 | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@RequiredArgsConstructor | ||
| 18 | +@Slf4j | ||
| 19 | +public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | ||
| 20 | + | ||
| 21 | + private final IotTcpUpstreamProtocol protocol; | ||
| 22 | + | ||
| 23 | + private final IotMessageBus messageBus; | ||
| 24 | + | ||
| 25 | + private final IotTcpDownstreamHandler downstreamHandler; | ||
| 26 | + | ||
| 27 | + @PostConstruct | ||
| 28 | + public void init() { | ||
| 29 | + messageBus.register(this); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @Override | ||
| 33 | + public String getTopic() { | ||
| 34 | + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + @Override | ||
| 38 | + public String getGroup() { | ||
| 39 | + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | ||
| 40 | + return getTopic(); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @Override | ||
| 44 | + public void onMessage(IotDeviceMessage message) { | ||
| 45 | + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", | ||
| 46 | + message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 47 | + try { | ||
| 48 | + // 1. 校验 | ||
| 49 | + String method = message.getMethod(); | ||
| 50 | + if (method == null) { | ||
| 51 | + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", | ||
| 52 | + message.getId(), message.getDeviceId()); | ||
| 53 | + return; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + // 2. 处理下行消息 | ||
| 57 | + downstreamHandler.handle(message); | ||
| 58 | + } catch (Exception e) { | ||
| 59 | + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", | ||
| 60 | + message.getId(), message.getMethod(), message.getDeviceId(), e); | ||
| 61 | + } | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | +} | ||
| 0 | \ No newline at end of file | 65 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 5 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpConnectionHandler; | ||
| 7 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 8 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 9 | +import io.vertx.core.Vertx; | ||
| 10 | +import io.vertx.core.net.NetServer; | ||
| 11 | +import jakarta.annotation.PostConstruct; | ||
| 12 | +import jakarta.annotation.PreDestroy; | ||
| 13 | +import lombok.Getter; | ||
| 14 | +import lombok.RequiredArgsConstructor; | ||
| 15 | +import lombok.extern.slf4j.Slf4j; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * IoT 网关 TCP 协议:接收设备上行消息 | ||
| 19 | + * | ||
| 20 | + * @author 芋道源码 | ||
| 21 | + */ | ||
| 22 | +@Slf4j | ||
| 23 | +@RequiredArgsConstructor | ||
| 24 | +public class IotTcpUpstreamProtocol { | ||
| 25 | + | ||
| 26 | + private final Vertx vertx; | ||
| 27 | + | ||
| 28 | + private final IotGatewayProperties gatewayProperties; | ||
| 29 | + | ||
| 30 | + private final IotTcpConnectionManager connectionManager; | ||
| 31 | + | ||
| 32 | + private final IotDeviceMessageService messageService; | ||
| 33 | + | ||
| 34 | + private final IotDeviceService deviceService; | ||
| 35 | + | ||
| 36 | + private final IotDeviceCommonApi deviceApi; | ||
| 37 | + | ||
| 38 | + @Getter | ||
| 39 | + private String serverId; | ||
| 40 | + | ||
| 41 | + private NetServer netServer; | ||
| 42 | + | ||
| 43 | + @PostConstruct | ||
| 44 | + public void start() { | ||
| 45 | + // 1. 初始化参数 | ||
| 46 | + IotGatewayProperties.TcpProperties tcpProperties = gatewayProperties.getProtocol().getTcp(); | ||
| 47 | + this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getServerPort()); | ||
| 48 | + | ||
| 49 | + // 2. 创建 TCP 服务器 | ||
| 50 | + netServer = vertx.createNetServer(); | ||
| 51 | + netServer.connectHandler(socket -> { | ||
| 52 | + new IotTcpConnectionHandler(socket, connectionManager, | ||
| 53 | + messageService, deviceService, deviceApi, serverId).start(); | ||
| 54 | + }); | ||
| 55 | + | ||
| 56 | + // 3. 启动 TCP 服务器 | ||
| 57 | + netServer.listen(tcpProperties.getServerPort(), tcpProperties.getServerHost()) | ||
| 58 | + .onSuccess(server -> log.info("[start][IoT 网关 TCP 服务启动成功,端口:{}]", server.actualPort())) | ||
| 59 | + .onFailure(e -> log.error("[start][IoT 网关 TCP 服务启动失败]", e)); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @PreDestroy | ||
| 63 | + public void stop() { | ||
| 64 | + if (netServer != null) { | ||
| 65 | + netServer.close() | ||
| 66 | + .onSuccess(v -> log.info("[stop][IoT 网关 TCP 服务已停止]")) | ||
| 67 | + .onFailure(e -> log.error("[stop][IoT 网关 TCP 服务停止失败]", e)); | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | +} | ||
| 0 | \ No newline at end of file | 72 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpConnectionHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.BooleanUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 8 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager; | ||
| 10 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 12 | +import io.vertx.core.Handler; | ||
| 13 | +import io.vertx.core.buffer.Buffer; | ||
| 14 | +import io.vertx.core.net.NetSocket; | ||
| 15 | +import io.vertx.core.parsetools.RecordParser; | ||
| 16 | +import lombok.RequiredArgsConstructor; | ||
| 17 | +import lombok.extern.slf4j.Slf4j; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * IoT TCP 连接处理器 | ||
| 21 | + * <p> | ||
| 22 | + * 核心负责: | ||
| 23 | + * 1. 【认证】创建连接后,设备需要发送认证消息,认证通过后,才能进行后续的通信 | ||
| 24 | + * 2. 【消息处理】接收设备发送的消息,解码后,发送到消息队列 | ||
| 25 | + * 3. 【断开】设备断开连接后,清理资源 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +@RequiredArgsConstructor | ||
| 30 | +@Slf4j | ||
| 31 | +public class IotTcpConnectionHandler implements Handler<Buffer> { | ||
| 32 | + | ||
| 33 | + private final NetSocket socket; | ||
| 34 | + /** | ||
| 35 | + * 是否已认证 | ||
| 36 | + */ | ||
| 37 | + private boolean authenticated = false; | ||
| 38 | + /** | ||
| 39 | + * 设备信息 | ||
| 40 | + */ | ||
| 41 | + private IotDeviceRespDTO device; | ||
| 42 | + | ||
| 43 | + private final IotTcpConnectionManager connectionManager; | ||
| 44 | + | ||
| 45 | + private final IotDeviceMessageService messageService; | ||
| 46 | + | ||
| 47 | + private final IotDeviceService deviceService; | ||
| 48 | + | ||
| 49 | + private final IotDeviceCommonApi deviceApi; | ||
| 50 | + | ||
| 51 | + private final String serverId; | ||
| 52 | + | ||
| 53 | + public void start() { | ||
| 54 | + // 1. 设置解析器 | ||
| 55 | + final RecordParser parser = RecordParser.newDelimited("\n", this); | ||
| 56 | + socket.handler(parser); | ||
| 57 | + | ||
| 58 | + // 2. 设置处理器 | ||
| 59 | + socket.closeHandler(v -> handleConnectionClose()); | ||
| 60 | + socket.exceptionHandler(this::handleException); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + @Override | ||
| 64 | + public void handle(Buffer buffer) { | ||
| 65 | + log.info("[handle][接收到数据: {}]", buffer); | ||
| 66 | + try { | ||
| 67 | + // 1. 处理认证 | ||
| 68 | + if (!authenticated) { | ||
| 69 | + handleAuthentication(buffer); | ||
| 70 | + return; | ||
| 71 | + } | ||
| 72 | + // 2. 处理消息 | ||
| 73 | + handleMessage(buffer); | ||
| 74 | + } catch (Exception e) { | ||
| 75 | + log.error("[handle][处理异常]", e); | ||
| 76 | + socket.close(); | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + private void handleAuthentication(Buffer buffer) { | ||
| 81 | + // 1. 解析认证信息 | ||
| 82 | + // TODO @芋艿:这里的认证协议,需要和设备端约定。默认为 productKey,deviceName,password | ||
| 83 | + String[] parts = buffer.toString().split(","); | ||
| 84 | + if (parts.length != 3) { | ||
| 85 | + log.error("[handleAuthentication][认证信息({})格式不正确]", buffer); | ||
| 86 | + socket.close(); | ||
| 87 | + return; | ||
| 88 | + } | ||
| 89 | + String productKey = parts[0]; | ||
| 90 | + String deviceName = parts[1]; | ||
| 91 | + String password = parts[2]; | ||
| 92 | + | ||
| 93 | + // 2. 执行认证 | ||
| 94 | + CommonResult<Boolean> authResult = deviceApi.authDevice(new IotDeviceAuthReqDTO() | ||
| 95 | + .setClientId(socket.remoteAddress().toString()).setUsername(productKey + "/" + deviceName) | ||
| 96 | + .setPassword(password)); | ||
| 97 | + if (authResult.isError() || !BooleanUtil.isTrue(authResult.getData())) { | ||
| 98 | + log.error("[handleAuthentication][认证失败,productKey({}) deviceName({}) password({})]", productKey, deviceName, | ||
| 99 | + password); | ||
| 100 | + socket.close(); | ||
| 101 | + return; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + // 3. 认证成功 | ||
| 105 | + this.authenticated = true; | ||
| 106 | + this.device = deviceService.getDeviceFromCache(productKey, deviceName); | ||
| 107 | + connectionManager.addConnection(String.valueOf(device.getId()), socket); | ||
| 108 | + | ||
| 109 | + // 4. 发送上线消息 | ||
| 110 | + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); | ||
| 111 | + messageService.sendDeviceMessage(message, productKey, deviceName, serverId); | ||
| 112 | + log.info("[handleAuthentication][认证成功]"); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + private void handleMessage(Buffer buffer) { | ||
| 116 | + // 1. 解码消息 | ||
| 117 | + IotDeviceMessage message = messageService.decodeDeviceMessage(buffer.getBytes(), | ||
| 118 | + device.getProductKey(), device.getDeviceName()); | ||
| 119 | + if (message == null) { | ||
| 120 | + log.warn("[handleMessage][解码消息失败]"); | ||
| 121 | + return; | ||
| 122 | + } | ||
| 123 | + // 2. 发送消息到队列 | ||
| 124 | + messageService.sendDeviceMessage(message, device.getProductKey(), device.getDeviceName(), serverId); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + private void handleConnectionClose() { | ||
| 128 | + // 1. 移除连接 | ||
| 129 | + connectionManager.removeConnection(socket); | ||
| 130 | + // 2. 发送离线消息 | ||
| 131 | + if (device != null) { | ||
| 132 | + IotDeviceMessage message = IotDeviceMessage.buildStateOffline(); | ||
| 133 | + messageService.sendDeviceMessage(message, device.getProductKey(), device.getDeviceName(), serverId); | ||
| 134 | + } | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + private void handleException(Throwable e) { | ||
| 138 | + log.error("[handleException][连接({}) 发生异常]", socket.remoteAddress(), e); | ||
| 139 | + socket.close(); | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | +} | ||
| 0 | \ No newline at end of file | 143 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager; | ||
| 5 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 6 | +import io.vertx.core.buffer.Buffer; | ||
| 7 | +import io.vertx.core.net.NetSocket; | ||
| 8 | +import lombok.RequiredArgsConstructor; | ||
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 10 | +import org.springframework.stereotype.Component; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * IoT 网关 TCP 下行消息处理器 | ||
| 14 | + * <p> | ||
| 15 | + * 从消息总线接收到下行消息,然后发布到 TCP 连接,从而被设备所接收 | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +@Slf4j | ||
| 20 | +@Component | ||
| 21 | +@RequiredArgsConstructor | ||
| 22 | +public class IotTcpDownstreamHandler { | ||
| 23 | + | ||
| 24 | + private final IotTcpConnectionManager connectionManager; | ||
| 25 | + private final IotDeviceMessageService messageService; | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 处理下行消息 | ||
| 29 | + * | ||
| 30 | + * @param message 设备消息 | ||
| 31 | + */ | ||
| 32 | + public void handle(IotDeviceMessage message) { | ||
| 33 | + // 1. 获取设备对应的连接 | ||
| 34 | + NetSocket socket = connectionManager.getConnection(String.valueOf(message.getDeviceId())); | ||
| 35 | + if (socket == null) { | ||
| 36 | + log.error("[handle][设备({})的连接不存在]", message.getDeviceId()); | ||
| 37 | + return; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + // 2. 编码消息 | ||
| 41 | + byte[] bytes = messageService.encodeDeviceMessage(message, null, null); | ||
| 42 | + | ||
| 43 | + // 3. 发送消息 | ||
| 44 | + socket.write(Buffer.buffer(bytes)); | ||
| 45 | + // TODO @芋艿:这里的换行符,需要和设备端约定 | ||
| 46 | + socket.write("\n"); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | +} | ||
| 0 | \ No newline at end of file | 50 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| @@ -80,50 +80,53 @@ public final class IotMqttTopicUtils { | @@ -80,50 +80,53 @@ public final class IotMqttTopicUtils { | ||
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | /** | 82 | /** |
| 83 | - * 构建设备服务调用主题 | 83 | + * 构建设备配置推送主题 |
| 84 | * | 84 | * |
| 85 | - * @param productKey 产品 Key | ||
| 86 | - * @param deviceName 设备名称 | ||
| 87 | - * @param serviceIdentifier 服务标识符 | 85 | + * @param productKey 产品 Key |
| 86 | + * @param deviceName 设备名称 | ||
| 88 | * @return 完整的主题路径 | 87 | * @return 完整的主题路径 |
| 89 | */ | 88 | */ |
| 90 | - public static String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) { | ||
| 91 | - return buildDeviceTopicPrefix(productKey, deviceName) + SERVICE_TOPIC_PREFIX + serviceIdentifier; | 89 | + public static String buildConfigPushTopic(String productKey, String deviceName) { |
| 90 | + return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/config/push"; | ||
| 92 | } | 91 | } |
| 93 | 92 | ||
| 94 | /** | 93 | /** |
| 95 | - * 构建设备服务调用回复主题 | 94 | + * 构建设备事件上报通用回复主题 |
| 95 | + * <p> | ||
| 96 | + * 不包含具体的事件标识符,事件标识符通过消息 data 中的 identifier 字段传递 | ||
| 96 | * | 97 | * |
| 97 | - * @param productKey 产品 Key | ||
| 98 | - * @param deviceName 设备名称 | ||
| 99 | - * @param serviceIdentifier 服务标识符 | 98 | + * @param productKey 产品 Key |
| 99 | + * @param deviceName 设备名称 | ||
| 100 | * @return 完整的主题路径 | 100 | * @return 完整的主题路径 |
| 101 | */ | 101 | */ |
| 102 | - public static String buildServiceReplyTopic(String productKey, String deviceName, String serviceIdentifier) { | ||
| 103 | - return buildDeviceTopicPrefix(productKey, deviceName) + SERVICE_TOPIC_PREFIX + serviceIdentifier + "_reply"; | 102 | + public static String buildEventPostReplyTopicGeneric(String productKey, String deviceName) { |
| 103 | + return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/event/post_reply"; | ||
| 104 | } | 104 | } |
| 105 | 105 | ||
| 106 | /** | 106 | /** |
| 107 | - * 构建设备事件上报回复主题 | 107 | + * 构建设备服务调用通用主题 |
| 108 | + * <p> | ||
| 109 | + * 不包含具体的服务标识符,服务标识符通过消息 data 中的 identifier 字段传递 | ||
| 108 | * | 110 | * |
| 109 | - * @param productKey 产品 Key | ||
| 110 | - * @param deviceName 设备名称 | ||
| 111 | - * @param eventIdentifier 事件标识符 | 111 | + * @param productKey 产品 Key |
| 112 | + * @param deviceName 设备名称 | ||
| 112 | * @return 完整的主题路径 | 113 | * @return 完整的主题路径 |
| 113 | */ | 114 | */ |
| 114 | - public static String buildEventPostReplyTopic(String productKey, String deviceName, String eventIdentifier) { | ||
| 115 | - return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/event/" + eventIdentifier + "_reply"; | 115 | + public static String buildServiceTopicGeneric(String productKey, String deviceName) { |
| 116 | + return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/service/invoke"; | ||
| 116 | } | 117 | } |
| 117 | 118 | ||
| 118 | /** | 119 | /** |
| 119 | - * 构建设备配置推送主题 | 120 | + * 构建设备服务调用通用回复主题 |
| 121 | + * <p> | ||
| 122 | + * 不包含具体的服务标识符,服务标识符通过消息 data 中的 identifier 字段传递 | ||
| 120 | * | 123 | * |
| 121 | * @param productKey 产品 Key | 124 | * @param productKey 产品 Key |
| 122 | * @param deviceName 设备名称 | 125 | * @param deviceName 设备名称 |
| 123 | * @return 完整的主题路径 | 126 | * @return 完整的主题路径 |
| 124 | */ | 127 | */ |
| 125 | - public static String buildConfigPushTopic(String productKey, String deviceName) { | ||
| 126 | - return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/config/push"; | 128 | + public static String buildServiceReplyTopicGeneric(String productKey, String deviceName) { |
| 129 | + return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/service/invoke_reply"; | ||
| 127 | } | 130 | } |
| 128 | 131 | ||
| 129 | } | 132 | } |
| 130 | \ No newline at end of file | 133 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application-local.yaml
| 1 | # ==================== IoT 网关本地开发环境配置 ==================== | 1 | # ==================== IoT 网关本地开发环境配置 ==================== |
| 2 | - | ||
| 3 | --- #################### 消息队列相关 #################### | 2 | --- #################### 消息队列相关 #################### |
| 4 | 3 | ||
| 5 | # rocketmq 配置项,对应 RocketMQProperties 配置类 | 4 | # rocketmq 配置项,对应 RocketMQProperties 配置类 |
| @@ -41,6 +40,13 @@ yudao: | @@ -41,6 +40,13 @@ yudao: | ||
| 41 | mqtt-ssl: false # 是否开启 SSL | 40 | mqtt-ssl: false # 是否开启 SSL |
| 42 | mqtt-topics: | 41 | mqtt-topics: |
| 43 | - "/sys/#" # 系统主题 | 42 | - "/sys/#" # 系统主题 |
| 43 | + # ==================================== | ||
| 44 | + # 针对引入的 TCP 组件的配置 | ||
| 45 | + # ==================================== | ||
| 46 | + tcp: | ||
| 47 | + enabled: true | ||
| 48 | + server-port: 8093 | ||
| 49 | + server-host: 0.0.0.0 | ||
| 44 | 50 | ||
| 45 | # 消息总线配置 | 51 | # 消息总线配置 |
| 46 | message-bus: | 52 | message-bus: |
| @@ -52,7 +58,7 @@ yudao: | @@ -52,7 +58,7 @@ yudao: | ||
| 52 | logging: | 58 | logging: |
| 53 | level: | 59 | level: |
| 54 | # 开发环境详细日志 | 60 | # 开发环境详细日志 |
| 55 | - cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG | 61 | + cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG |
| 56 | cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG | 62 | cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG |
| 57 | # MQTT 客户端日志 | 63 | # MQTT 客户端日志 |
| 58 | # io.vertx.mqtt: DEBUG | 64 | # io.vertx.mqtt: DEBUG |
| 59 | \ No newline at end of file | 65 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -41,6 +41,11 @@ yudao: | @@ -41,6 +41,11 @@ yudao: | ||
| 41 | mqtt-ssl: false | 41 | mqtt-ssl: false |
| 42 | mqtt-topics: | 42 | mqtt-topics: |
| 43 | - "/sys/#" # 系统主题 | 43 | - "/sys/#" # 系统主题 |
| 44 | + # ==================================== | ||
| 45 | + # 针对引入的 TCP 组件的配置 | ||
| 46 | + # ==================================== | ||
| 47 | + tcp: | ||
| 48 | + enabled: false | ||
| 44 | 49 | ||
| 45 | # 消息总线配置 | 50 | # 消息总线配置 |
| 46 | message-bus: | 51 | message-bus: |