Commit a5a3aea522a13300973dd343aafa20a16c80ba0a

Authored by haohao
1 parent 0593dbc9

feat:【IoT 物联网】新增 TCP 协议支持,添加 TCP 连接管理、上下行消息处理及相关配置

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: