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 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; |
| 8 | 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 | 16 | import io.vertx.core.Vertx; |
| 10 | 17 | import lombok.extern.slf4j.Slf4j; |
| 11 | 18 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| ... | ... | @@ -33,7 +40,7 @@ public class IotGatewayConfiguration { |
| 33 | 40 | |
| 34 | 41 | @Bean |
| 35 | 42 | public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, |
| 36 | - IotMessageBus messageBus) { | |
| 43 | + IotMessageBus messageBus) { | |
| 37 | 44 | return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); |
| 38 | 45 | } |
| 39 | 46 | } |
| ... | ... | @@ -53,21 +60,51 @@ public class IotGatewayConfiguration { |
| 53 | 60 | |
| 54 | 61 | @Bean |
| 55 | 62 | public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, |
| 56 | - Vertx emqxVertx) { | |
| 63 | + Vertx emqxVertx) { | |
| 57 | 64 | return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); |
| 58 | 65 | } |
| 59 | 66 | |
| 60 | 67 | @Bean |
| 61 | 68 | public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, |
| 62 | - Vertx emqxVertx) { | |
| 69 | + Vertx emqxVertx) { | |
| 63 | 70 | return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); |
| 64 | 71 | } |
| 65 | 72 | |
| 66 | 73 | @Bean |
| 67 | 74 | public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, |
| 68 | - IotMessageBus messageBus) { | |
| 75 | + IotMessageBus messageBus) { | |
| 69 | 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 | 78 | */ |
| 79 | 79 | private EmqxProperties emqx; |
| 80 | 80 | |
| 81 | + /** | |
| 82 | + * TCP 组件配置 | |
| 83 | + */ | |
| 84 | + private TcpProperties tcp; | |
| 85 | + | |
| 81 | 86 | } |
| 82 | 87 | |
| 83 | 88 | @Data |
| ... | ... | @@ -96,6 +101,25 @@ public class IotGatewayProperties { |
| 96 | 101 | } |
| 97 | 102 | |
| 98 | 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 | 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 | 97 | break; |
| 98 | 98 | |
| 99 | 99 | case EVENT_POST: |
| 100 | - // TODO @haohao:不用 eventIdentifier 拼接哈,直接 data 里面,有 identifier 字段 | |
| 101 | 100 | // 事件上报:只支持回复消息(下行) |
| 102 | 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 | 104 | break; |
| 109 | 105 | |
| 110 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 124 | * @param productKey 产品 Key |
| 122 | 125 | * @param deviceName 设备名称 |
| 123 | 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 | 133 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application-local.yaml
| 1 | 1 | # ==================== IoT 网关本地开发环境配置 ==================== |
| 2 | - | |
| 3 | 2 | --- #################### 消息队列相关 #################### |
| 4 | 3 | |
| 5 | 4 | # rocketmq 配置项,对应 RocketMQProperties 配置类 |
| ... | ... | @@ -41,6 +40,13 @@ yudao: |
| 41 | 40 | mqtt-ssl: false # 是否开启 SSL |
| 42 | 41 | mqtt-topics: |
| 43 | 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 | 52 | message-bus: |
| ... | ... | @@ -52,7 +58,7 @@ yudao: |
| 52 | 58 | logging: |
| 53 | 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 | 62 | cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG |
| 57 | 63 | # MQTT 客户端日志 |
| 58 | 64 | # io.vertx.mqtt: DEBUG |
| 59 | 65 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml