Commit 4f6ee4c5eb49238cbe5e049a4740df0cd4609ff2
1 parent
83990086
feat(iot):【协议改造】优化各种 code review 的代码
Showing
12 changed files
with
97 additions
and
66 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
| @@ -125,7 +125,11 @@ public class IotHttpProtocol implements IotProtocol { | @@ -125,7 +125,11 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 125 | this.downstreamSubscriber.start(); | 125 | this.downstreamSubscriber.start(); |
| 126 | } catch (Exception e) { | 126 | } catch (Exception e) { |
| 127 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); | 127 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); |
| 128 | - // 启动失败时关闭 Vertx | 128 | + // 启动失败时关闭资源 |
| 129 | + if (httpServer != null) { | ||
| 130 | + httpServer.close(); | ||
| 131 | + httpServer = null; | ||
| 132 | + } | ||
| 129 | if (vertx != null) { | 133 | if (vertx != null) { |
| 130 | vertx.close(); | 134 | vertx.close(); |
| 131 | vertx = null; | 135 | vertx = null; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
| @@ -27,13 +27,6 @@ public class IotMqttConfig { | @@ -27,13 +27,6 @@ public class IotMqttConfig { | ||
| 27 | private Integer connectTimeoutSeconds = 60; | 27 | private Integer connectTimeoutSeconds = 60; |
| 28 | 28 | ||
| 29 | /** | 29 | /** |
| 30 | - * 保持连接超时时间(秒) | ||
| 31 | - */ | ||
| 32 | - @NotNull(message = "保持连接超时时间不能为空") | ||
| 33 | - @Min(value = 1, message = "保持连接超时时间不能小于 1 秒") | ||
| 34 | - private Integer keepAliveTimeoutSeconds = 300; | ||
| 35 | - | ||
| 36 | - /** | ||
| 37 | * 是否启用 SSL | 30 | * 是否启用 SSL |
| 38 | */ | 31 | */ |
| 39 | @NotNull(message = "是否启用 SSL 不能为空") | 32 | @NotNull(message = "是否启用 SSL 不能为空") |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 3 | import cn.hutool.extra.spring.SpringUtil; | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 4 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 10 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; |
| @@ -15,6 +17,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt | @@ -15,6 +17,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt | ||
| 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; |
| 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 17 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 19 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 20 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | ||
| 18 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | 21 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; |
| 19 | import io.netty.handler.codec.mqtt.MqttQoS; | 22 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 20 | import io.vertx.core.Vertx; | 23 | import io.vertx.core.Vertx; |
| @@ -26,13 +29,10 @@ import io.vertx.mqtt.MqttTopicSubscription; | @@ -26,13 +29,10 @@ import io.vertx.mqtt.MqttTopicSubscription; | ||
| 26 | import io.vertx.mqtt.messages.MqttPublishMessage; | 29 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 27 | import lombok.Getter; | 30 | import lombok.Getter; |
| 28 | import lombok.extern.slf4j.Slf4j; | 31 | import lombok.extern.slf4j.Slf4j; |
| 29 | -import cn.hutool.core.lang.Assert; | ||
| 30 | -import cn.hutool.core.util.StrUtil; | ||
| 31 | 32 | ||
| 33 | +import java.util.ArrayList; | ||
| 32 | import java.util.List; | 34 | import java.util.List; |
| 33 | 35 | ||
| 34 | -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | ||
| 35 | - | ||
| 36 | /** | 36 | /** |
| 37 | * IoT 网关 MQTT 协议:接收设备上行消息 | 37 | * IoT 网关 MQTT 协议:接收设备上行消息 |
| 38 | * | 38 | * |
| @@ -249,11 +249,22 @@ public class IotMqttProtocol implements IotProtocol { | @@ -249,11 +249,22 @@ public class IotMqttProtocol implements IotProtocol { | ||
| 249 | // 3.2 设置 QoS 2 消息的 PUBREL 处理器 | 249 | // 3.2 设置 QoS 2 消息的 PUBREL 处理器 |
| 250 | endpoint.publishReleaseHandler(endpoint::publishComplete); | 250 | endpoint.publishReleaseHandler(endpoint::publishComplete); |
| 251 | 251 | ||
| 252 | - // 4.1 设置订阅处理器 | 252 | + // 4.1 设置订阅处理器(带 ACL 校验) |
| 253 | endpoint.subscribeHandler(subscribe -> { | 253 | endpoint.subscribeHandler(subscribe -> { |
| 254 | - List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); | ||
| 255 | - log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | ||
| 256 | - List<MqttQoS> grantedQoSLevels = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::qualityOfService); | 254 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); |
| 255 | + List<MqttQoS> grantedQoSLevels = new ArrayList<>(); | ||
| 256 | + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) { | ||
| 257 | + String topicName = sub.topicName(); | ||
| 258 | + // 校验主题是否属于当前设备 | ||
| 259 | + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed( | ||
| 260 | + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) { | ||
| 261 | + grantedQoSLevels.add(sub.qualityOfService()); | ||
| 262 | + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName); | ||
| 263 | + } else { | ||
| 264 | + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName); | ||
| 265 | + grantedQoSLevels.add(MqttQoS.FAILURE); | ||
| 266 | + } | ||
| 267 | + } | ||
| 257 | endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | 268 | endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); |
| 258 | }); | 269 | }); |
| 259 | // 4.2 设置取消订阅处理器 | 270 | // 4.2 设置取消订阅处理器 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
| @@ -30,7 +30,6 @@ public abstract class IotMqttAbstractHandler { | @@ -30,7 +30,6 @@ public abstract class IotMqttAbstractHandler { | ||
| 30 | protected final IotMqttConnectionManager connectionManager; | 30 | protected final IotMqttConnectionManager connectionManager; |
| 31 | protected final IotDeviceMessageService deviceMessageService; | 31 | protected final IotDeviceMessageService deviceMessageService; |
| 32 | 32 | ||
| 33 | - // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic | ||
| 34 | /** | 33 | /** |
| 35 | * 发送成功响应到设备 | 34 | * 发送成功响应到设备 |
| 36 | * | 35 | * |
| @@ -43,20 +42,8 @@ public abstract class IotMqttAbstractHandler { | @@ -43,20 +42,8 @@ public abstract class IotMqttAbstractHandler { | ||
| 43 | */ | 42 | */ |
| 44 | protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | 43 | protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, |
| 45 | String requestId, String method, Object data) { | 44 | String requestId, String method, Object data) { |
| 46 | - try { | ||
| 47 | - // 1. 构建响应消息 | ||
| 48 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); | ||
| 49 | - | ||
| 50 | - // 2. 编码消息(使用默认编解码器) | ||
| 51 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | ||
| 52 | - | ||
| 53 | - // 3. 构建响应主题并发送 | ||
| 54 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); | ||
| 55 | - endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); | ||
| 56 | - log.debug("[sendSuccessResponse][发送成功响应,主题: {}]", replyTopic); | ||
| 57 | - } catch (Exception e) { | ||
| 58 | - log.error("[sendSuccessResponse][发送成功响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | ||
| 59 | - } | 45 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); |
| 46 | + writeResponse(endpoint, productKey, deviceName, method, responseMessage); | ||
| 60 | } | 47 | } |
| 61 | 48 | ||
| 62 | /** | 49 | /** |
| @@ -72,22 +59,32 @@ public abstract class IotMqttAbstractHandler { | @@ -72,22 +59,32 @@ public abstract class IotMqttAbstractHandler { | ||
| 72 | */ | 59 | */ |
| 73 | protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | 60 | protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, |
| 74 | String requestId, String method, Integer errorCode, String errorMessage) { | 61 | String requestId, String method, Integer errorCode, String errorMessage) { |
| 75 | - try { | ||
| 76 | - // 1. 构建响应消息 | ||
| 77 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); | 62 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); |
| 63 | + writeResponse(endpoint, productKey, deviceName, method, responseMessage); | ||
| 64 | + } | ||
| 78 | 65 | ||
| 79 | - // 2. 编码消息(使用默认编解码器) | 66 | + /** |
| 67 | + * 写入响应消息到设备 | ||
| 68 | + * | ||
| 69 | + * @param endpoint MQTT 连接端点 | ||
| 70 | + * @param productKey 产品 Key | ||
| 71 | + * @param deviceName 设备名称 | ||
| 72 | + * @param method 方法名 | ||
| 73 | + * @param responseMessage 响应消息 | ||
| 74 | + */ | ||
| 75 | + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, | ||
| 76 | + String method, IotDeviceMessage responseMessage) { | ||
| 77 | + try { | ||
| 78 | + // 1. 编码消息(使用默认编解码器) | ||
| 80 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | 79 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); |
| 81 | 80 | ||
| 82 | - // 3. 构建响应主题并发送 | 81 | + // 2. 构建响应主题,并发送 |
| 83 | String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); | 82 | String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); |
| 84 | endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); | 83 | endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); |
| 85 | - log.debug("[sendErrorResponse][发送错误响应,主题: {}]", replyTopic); | 84 | + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); |
| 86 | } catch (Exception e) { | 85 | } catch (Exception e) { |
| 87 | - log.error("[sendErrorResponse][发送错误响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | 86 | + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); |
| 88 | } | 87 | } |
| 89 | } | 88 | } |
| 90 | 89 | ||
| 91 | - // TODO @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse(必须抽取!!!) | ||
| 92 | - | ||
| 93 | } | 90 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java
| @@ -103,7 +103,7 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { | @@ -103,7 +103,7 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { | ||
| 103 | .setProductKey(device.getProductKey()) | 103 | .setProductKey(device.getProductKey()) |
| 104 | .setDeviceName(device.getDeviceName()) | 104 | .setDeviceName(device.getDeviceName()) |
| 105 | .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); | 105 | .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); |
| 106 | - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); | 106 | + connectionManager.registerConnection(endpoint, connectionInfo); |
| 107 | } | 107 | } |
| 108 | 108 | ||
| 109 | /** | 109 | /** |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
| @@ -77,6 +77,9 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | @@ -77,6 +77,9 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | ||
| 77 | // 接受连接,并发送错误响应 | 77 | // 接受连接,并发送错误响应 |
| 78 | endpoint.accept(false); | 78 | endpoint.accept(false); |
| 79 | sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage()); | 79 | sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage()); |
| 80 | + } finally { | ||
| 81 | + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) | ||
| 82 | + endpoint.close(); | ||
| 80 | } | 83 | } |
| 81 | } | 84 | } |
| 82 | 85 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
| @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.lang.Assert; | 3 | import cn.hutool.core.lang.Assert; |
| 4 | import cn.hutool.core.util.ArrayUtil; | 4 | import cn.hutool.core.util.ArrayUtil; |
| 5 | -import cn.hutool.core.util.StrUtil; | ||
| 6 | import cn.iocoder.yudao.framework.common.exception.ServiceException; | 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| @@ -50,12 +49,10 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | @@ -50,12 +49,10 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | ||
| 50 | } | 49 | } |
| 51 | // 1.2 解析主题,获取 productKey 和 deviceName | 50 | // 1.2 解析主题,获取 productKey 和 deviceName |
| 52 | String[] topicParts = topic.split("/"); | 51 | String[] topicParts = topic.split("/"); |
| 53 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | ||
| 54 | - log.warn("[handleBusinessRequest][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | ||
| 55 | - return; | ||
| 56 | - } | ||
| 57 | - productKey = topicParts[2]; | ||
| 58 | - deviceName = topicParts[3]; | 52 | + productKey = ArrayUtil.get(topicParts, 2); |
| 53 | + deviceName = ArrayUtil.get(topicParts, 3); | ||
| 54 | + Assert.notBlank(productKey, "产品 Key 不能为空"); | ||
| 55 | + Assert.notBlank(deviceName, "设备名称不能为空"); | ||
| 59 | // 1.3 校验设备信息,防止伪造设备消息 | 56 | // 1.3 校验设备信息,防止伪造设备消息 |
| 60 | IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | 57 | IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); |
| 61 | Assert.notNull(connectionInfo, "无法获取连接信息"); | 58 | Assert.notNull(connectionInfo, "无法获取连接信息"); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| @@ -73,25 +73,24 @@ public class IotMqttConnectionManager { | @@ -73,25 +73,24 @@ public class IotMqttConnectionManager { | ||
| 73 | * 注册设备连接(包含认证信息) | 73 | * 注册设备连接(包含认证信息) |
| 74 | * | 74 | * |
| 75 | * @param endpoint MQTT 连接端点 | 75 | * @param endpoint MQTT 连接端点 |
| 76 | - * @param deviceId 设备 ID | ||
| 77 | * @param connectionInfo 连接信息 | 76 | * @param connectionInfo 连接信息 |
| 78 | */ | 77 | */ |
| 79 | - // TODO @AI:移除掉 deviceId ???参考别的 tcp 等模块协议 | ||
| 80 | - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { | 78 | + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { |
| 79 | + Long deviceId = connectionInfo.getDeviceId(); | ||
| 81 | // 如果设备已有其他连接,先清理旧连接 | 80 | // 如果设备已有其他连接,先清理旧连接 |
| 82 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); | 81 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); |
| 83 | if (oldEndpoint != null && oldEndpoint != endpoint) { | 82 | if (oldEndpoint != null && oldEndpoint != endpoint) { |
| 84 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", | 83 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 85 | deviceId, getEndpointAddress(oldEndpoint)); | 84 | deviceId, getEndpointAddress(oldEndpoint)); |
| 86 | - oldEndpoint.close(); | ||
| 87 | - // 清理旧连接的映射 | 85 | + // 先清理映射,再关闭连接(避免旧连接处理器干扰) |
| 88 | connectionMap.remove(oldEndpoint); | 86 | connectionMap.remove(oldEndpoint); |
| 87 | + oldEndpoint.close(); | ||
| 89 | } | 88 | } |
| 90 | 89 | ||
| 91 | // 注册新连接 | 90 | // 注册新连接 |
| 92 | connectionMap.put(endpoint, connectionInfo); | 91 | connectionMap.put(endpoint, connectionInfo); |
| 93 | deviceEndpointMap.put(deviceId, endpoint); | 92 | deviceEndpointMap.put(deviceId, endpoint); |
| 94 | - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", | 93 | + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", |
| 95 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); | 94 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); |
| 96 | } | 95 | } |
| 97 | 96 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
| @@ -47,8 +47,8 @@ public class IotTcpConnectionManager { | @@ -47,8 +47,8 @@ public class IotTcpConnectionManager { | ||
| 47 | * @param deviceId 设备 ID | 47 | * @param deviceId 设备 ID |
| 48 | * @param connectionInfo 连接信息 | 48 | * @param connectionInfo 连接信息 |
| 49 | */ | 49 | */ |
| 50 | - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { | ||
| 51 | - // 检查连接数是否已达上限 | 50 | + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { |
| 51 | + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) | ||
| 52 | if (connectionMap.size() >= maxConnections) { | 52 | if (connectionMap.size() >= maxConnections) { |
| 53 | throw new IllegalStateException("连接数已达上限: " + maxConnections); | 53 | throw new IllegalStateException("连接数已达上限: " + maxConnections); |
| 54 | } | 54 | } |
| @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { | @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { | ||
| 57 | if (oldSocket != null && oldSocket != socket) { | 57 | if (oldSocket != null && oldSocket != socket) { |
| 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", | 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 59 | deviceId, oldSocket.remoteAddress()); | 59 | deviceId, oldSocket.remoteAddress()); |
| 60 | - oldSocket.close(); | ||
| 61 | - // 清理旧连接的映射 | 60 | + // 先清理映射,再关闭连接 |
| 62 | connectionMap.remove(oldSocket); | 61 | connectionMap.remove(oldSocket); |
| 62 | + oldSocket.close(); | ||
| 63 | } | 63 | } |
| 64 | 64 | ||
| 65 | // 注册新连接 | 65 | // 注册新连接 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
| @@ -9,6 +9,7 @@ import lombok.Data; | @@ -9,6 +9,7 @@ import lombok.Data; | ||
| 9 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | ||
| 11 | import java.net.InetSocketAddress; | 11 | import java.net.InetSocketAddress; |
| 12 | +import java.util.concurrent.CompletableFuture; | ||
| 12 | import java.util.concurrent.TimeUnit; | 13 | import java.util.concurrent.TimeUnit; |
| 13 | 14 | ||
| 14 | /** | 15 | /** |
| @@ -47,8 +48,8 @@ public class IotUdpSessionManager { | @@ -47,8 +48,8 @@ public class IotUdpSessionManager { | ||
| 47 | * @param deviceId 设备 ID | 48 | * @param deviceId 设备 ID |
| 48 | * @param sessionInfo 会话信息 | 49 | * @param sessionInfo 会话信息 |
| 49 | */ | 50 | */ |
| 50 | - public void registerSession(Long deviceId, SessionInfo sessionInfo) { | ||
| 51 | - // 检查是否为新设备,且会话数已达上限 | 51 | + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { |
| 52 | + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) | ||
| 52 | if (deviceSessionCache.getIfPresent(deviceId) == null | 53 | if (deviceSessionCache.getIfPresent(deviceId) == null |
| 53 | && deviceSessionCache.size() >= maxSessions) { | 54 | && deviceSessionCache.size() >= maxSessions) { |
| 54 | throw new IllegalStateException("会话数已达上限: " + maxSessions); | 55 | throw new IllegalStateException("会话数已达上限: " + maxSessions); |
| @@ -113,16 +114,21 @@ public class IotUdpSessionManager { | @@ -113,16 +114,21 @@ public class IotUdpSessionManager { | ||
| 113 | } | 114 | } |
| 114 | InetSocketAddress address = sessionInfo.getAddress(); | 115 | InetSocketAddress address = sessionInfo.getAddress(); |
| 115 | try { | 116 | try { |
| 117 | + // 使用 CompletableFuture 同步等待发送结果 | ||
| 118 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); | ||
| 116 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { | 119 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 117 | if (result.succeeded()) { | 120 | if (result.succeeded()) { |
| 118 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", | 121 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", |
| 119 | deviceId, buildAddressKey(address), data.length); | 122 | deviceId, buildAddressKey(address), data.length); |
| 120 | - return; | 123 | + future.complete(true); |
| 124 | + } else { | ||
| 125 | + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | ||
| 126 | + deviceId, buildAddressKey(address), result.cause()); | ||
| 127 | + future.complete(false); | ||
| 121 | } | 128 | } |
| 122 | - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | ||
| 123 | - deviceId, buildAddressKey(address), result.cause()); | ||
| 124 | }); | 129 | }); |
| 125 | - return true; | 130 | + // 同步等待结果,超时 5 秒 |
| 131 | + return future.get(5, TimeUnit.SECONDS); | ||
| 126 | } catch (Exception e) { | 132 | } catch (Exception e) { |
| 127 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); | 133 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); |
| 128 | return false; | 134 | return false; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| @@ -63,4 +63,26 @@ public final class IotMqttTopicUtils { | @@ -63,4 +63,26 @@ public final class IotMqttTopicUtils { | ||
| 63 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; | 63 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; |
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | + /** | ||
| 67 | + * 校验主题是否允许订阅 | ||
| 68 | + * <p> | ||
| 69 | + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头, | ||
| 70 | + * 或者是通配符形式 /sys/{productKey}/{deviceName}/# | ||
| 71 | + * | ||
| 72 | + * @param topic 订阅的主题 | ||
| 73 | + * @param productKey 产品 Key | ||
| 74 | + * @param deviceName 设备名称 | ||
| 75 | + * @return 是否允许订阅 | ||
| 76 | + */ | ||
| 77 | + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) { | ||
| 78 | + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { | ||
| 79 | + return false; | ||
| 80 | + } | ||
| 81 | + // 构建设备主题前缀 | ||
| 82 | + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; | ||
| 83 | + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式 | ||
| 84 | + return topic.startsWith(deviceTopicPrefix) | ||
| 85 | + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); | ||
| 86 | + } | ||
| 87 | + | ||
| 66 | } | 88 | } |
| 67 | \ No newline at end of file | 89 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -124,7 +124,6 @@ yudao: | @@ -124,7 +124,6 @@ yudao: | ||
| 124 | mqtt: | 124 | mqtt: |
| 125 | max-message-size: 8192 # 最大消息大小(字节) | 125 | max-message-size: 8192 # 最大消息大小(字节) |
| 126 | connect-timeout-seconds: 60 # 连接超时时间(秒) | 126 | connect-timeout-seconds: 60 # 连接超时时间(秒) |
| 127 | - keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) | ||
| 128 | ssl-enabled: false # 是否启用 SSL | 127 | ssl-enabled: false # 是否启用 SSL |
| 129 | 128 | ||
| 130 | # 协议配置(旧版,保持兼容) | 129 | # 协议配置(旧版,保持兼容) |