Commit b87bc19116ffa22e0f0bb47a13a2fb92731b1738
1 parent
1b4ac9fb
feat:【iot】移除不再使用的 MQTT WebSocket 协议配置类(实现不够标准,使用 MQTT 即可)
Showing
9 changed files
with
3 additions
and
1531 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -12,10 +12,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr |
| 12 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 13 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 14 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsDownstreamSubscriber; | |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol; | |
| 17 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager; | |
| 18 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler; | |
| 19 | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; |
| 20 | 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 21 | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; |
| ... | ... | @@ -160,44 +156,6 @@ public class IotGatewayConfiguration { |
| 160 | 156 | } |
| 161 | 157 | |
| 162 | 158 | /** |
| 163 | - * IoT 网关 MQTT WebSocket 协议配置类 | |
| 164 | - */ | |
| 165 | - @Configuration | |
| 166 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt-ws", name = "enabled", havingValue = "true") | |
| 167 | - @Slf4j | |
| 168 | - public static class MqttWsProtocolConfiguration { | |
| 169 | - | |
| 170 | - @Bean(name = "mqttWsVertx", destroyMethod = "close") | |
| 171 | - public Vertx mqttWsVertx() { | |
| 172 | - return Vertx.vertx(); | |
| 173 | - } | |
| 174 | - | |
| 175 | - @Bean | |
| 176 | - public IotMqttWsUpstreamProtocol iotMqttWsUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 177 | - IotDeviceMessageService messageService, | |
| 178 | - IotMqttWsConnectionManager connectionManager, | |
| 179 | - @Qualifier("mqttWsVertx") Vertx mqttWsVertx) { | |
| 180 | - return new IotMqttWsUpstreamProtocol(gatewayProperties.getProtocol().getMqttWs(), | |
| 181 | - messageService, connectionManager, mqttWsVertx); | |
| 182 | - } | |
| 183 | - | |
| 184 | - @Bean | |
| 185 | - public IotMqttWsDownstreamHandler iotMqttWsDownstreamHandler(IotDeviceMessageService messageService, | |
| 186 | - IotDeviceService deviceService, | |
| 187 | - IotMqttWsConnectionManager connectionManager) { | |
| 188 | - return new IotMqttWsDownstreamHandler(messageService, deviceService, connectionManager); | |
| 189 | - } | |
| 190 | - | |
| 191 | - @Bean | |
| 192 | - public IotMqttWsDownstreamSubscriber iotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol mqttWsUpstreamProtocol, | |
| 193 | - IotMqttWsDownstreamHandler downstreamHandler, | |
| 194 | - IotMessageBus messageBus) { | |
| 195 | - return new IotMqttWsDownstreamSubscriber(mqttWsUpstreamProtocol, downstreamHandler, messageBus); | |
| 196 | - } | |
| 197 | - | |
| 198 | - } | |
| 199 | - | |
| 200 | - /** | |
| 201 | 159 | * IoT 网关 UDP 协议配置类 |
| 202 | 160 | */ |
| 203 | 161 | @Configuration | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | 4 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 5 | +import io.vertx.core.buffer.Buffer; | |
| 5 | 6 | import io.vertx.mqtt.MqttEndpoint; |
| 6 | 7 | import lombok.Data; |
| 7 | 8 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -166,7 +167,7 @@ public class IotMqttConnectionManager { |
| 166 | 167 | } |
| 167 | 168 | |
| 168 | 169 | try { |
| 169 | - endpoint.publish(topic, io.vertx.core.buffer.Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain); | |
| 170 | + endpoint.publish(topic, Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain); | |
| 170 | 171 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos); |
| 171 | 172 | return true; |
| 172 | 173 | } catch (Exception e) { | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/IotMqttWsDownstreamSubscriber.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws; | |
| 2 | - | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 7 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler; | |
| 9 | -import jakarta.annotation.PostConstruct; | |
| 10 | -import lombok.extern.slf4j.Slf4j; | |
| 11 | - | |
| 12 | -/** | |
| 13 | - * IoT MQTT WebSocket 下行消息订阅器 | |
| 14 | - * <p> | |
| 15 | - * 订阅消息总线的设备下行消息,并通过 WebSocket 发送到设备 | |
| 16 | - * | |
| 17 | - * @author 芋道源码 | |
| 18 | - */ | |
| 19 | -@Slf4j | |
| 20 | -public class IotMqttWsDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 21 | - | |
| 22 | - private final IotMqttWsUpstreamProtocol upstreamProtocol; | |
| 23 | - private final IotMqttWsDownstreamHandler downstreamHandler; | |
| 24 | - private final IotMessageBus messageBus; | |
| 25 | - | |
| 26 | - public IotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol upstreamProtocol, | |
| 27 | - IotMqttWsDownstreamHandler downstreamHandler, | |
| 28 | - IotMessageBus messageBus) { | |
| 29 | - this.upstreamProtocol = upstreamProtocol; | |
| 30 | - this.downstreamHandler = downstreamHandler; | |
| 31 | - this.messageBus = messageBus; | |
| 32 | - } | |
| 33 | - | |
| 34 | - @PostConstruct | |
| 35 | - public void init() { | |
| 36 | - messageBus.register(this); | |
| 37 | - log.info("[init][MQTT WebSocket 下行消息订阅器已启动,topic: {}]", getTopic()); | |
| 38 | - } | |
| 39 | - | |
| 40 | - @Override | |
| 41 | - public String getTopic() { | |
| 42 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); | |
| 43 | - } | |
| 44 | - | |
| 45 | - @Override | |
| 46 | - public String getGroup() { | |
| 47 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 48 | - return getTopic(); | |
| 49 | - } | |
| 50 | - | |
| 51 | - @Override | |
| 52 | - public void onMessage(IotDeviceMessage message) { | |
| 53 | - log.debug("[onMessage][收到下行消息,deviceId: {},method: {}]", | |
| 54 | - message.getDeviceId(), message.getMethod()); | |
| 55 | - try { | |
| 56 | - // 1. 校验 | |
| 57 | - String method = message.getMethod(); | |
| 58 | - if (StrUtil.isBlank(method)) { | |
| 59 | - log.warn("[onMessage][消息方法为空,deviceId: {}]", message.getDeviceId()); | |
| 60 | - return; | |
| 61 | - } | |
| 62 | - | |
| 63 | - // 2. 委托给下行处理器处理业务逻辑 | |
| 64 | - boolean success = downstreamHandler.handleDownstreamMessage(message); | |
| 65 | - if (success) { | |
| 66 | - log.debug("[onMessage][下行消息处理成功,deviceId: {},method: {}]", | |
| 67 | - message.getDeviceId(), message.getMethod()); | |
| 68 | - } else { | |
| 69 | - log.warn("[onMessage][下行消息处理失败,deviceId: {},method: {}]", | |
| 70 | - message.getDeviceId(), message.getMethod()); | |
| 71 | - } | |
| 72 | - } catch (Exception e) { | |
| 73 | - log.error("[onMessage][处理下行消息失败,deviceId: {},method: {}]", | |
| 74 | - message.getDeviceId(), message.getMethod(), e); | |
| 75 | - } | |
| 76 | - } | |
| 77 | - | |
| 78 | -} | |
| 79 | - |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/IotMqttWsUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsUpstreamHandler; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 8 | -import io.vertx.core.Vertx; | |
| 9 | -import io.vertx.core.http.HttpServer; | |
| 10 | -import io.vertx.core.http.HttpServerOptions; | |
| 11 | -import io.vertx.core.http.ServerWebSocket; | |
| 12 | -import jakarta.annotation.PostConstruct; | |
| 13 | -import jakarta.annotation.PreDestroy; | |
| 14 | -import lombok.Getter; | |
| 15 | -import lombok.extern.slf4j.Slf4j; | |
| 16 | - | |
| 17 | -/** | |
| 18 | - * IoT 网关 MQTT WebSocket 协议:接收设备上行消息 | |
| 19 | - * <p> | |
| 20 | - * 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持: | |
| 21 | - * - 标准 MQTT 3.1.1 协议 | |
| 22 | - * - WebSocket 协议升级 | |
| 23 | - * - SSL/TLS 加密(wss://) | |
| 24 | - * - 设备认证与连接管理 | |
| 25 | - * - QoS 0/1/2 消息质量保证 | |
| 26 | - * | |
| 27 | - * @author 芋道源码 | |
| 28 | - */ | |
| 29 | -@Slf4j | |
| 30 | -public class IotMqttWsUpstreamProtocol { | |
| 31 | - | |
| 32 | - private final IotGatewayProperties.MqttWsProperties mqttWsProperties; | |
| 33 | - | |
| 34 | - private final IotDeviceMessageService messageService; | |
| 35 | - | |
| 36 | - private final IotMqttWsConnectionManager connectionManager; | |
| 37 | - | |
| 38 | - private final Vertx vertx; | |
| 39 | - | |
| 40 | - @Getter | |
| 41 | - private final String serverId; | |
| 42 | - | |
| 43 | - private HttpServer httpServer; | |
| 44 | - | |
| 45 | - public IotMqttWsUpstreamProtocol(IotGatewayProperties.MqttWsProperties mqttWsProperties, | |
| 46 | - IotDeviceMessageService messageService, | |
| 47 | - IotMqttWsConnectionManager connectionManager, | |
| 48 | - Vertx vertx) { | |
| 49 | - this.mqttWsProperties = mqttWsProperties; | |
| 50 | - this.messageService = messageService; | |
| 51 | - this.connectionManager = connectionManager; | |
| 52 | - this.vertx = vertx; | |
| 53 | - this.serverId = IotDeviceMessageUtils.generateServerId(mqttWsProperties.getPort()); | |
| 54 | - } | |
| 55 | - | |
| 56 | - @PostConstruct | |
| 57 | - public void start() { | |
| 58 | - // 创建 HTTP 服务器选项 | |
| 59 | - HttpServerOptions options = new HttpServerOptions() | |
| 60 | - .setPort(mqttWsProperties.getPort()) | |
| 61 | - .setIdleTimeout(mqttWsProperties.getKeepAliveTimeoutSeconds()) | |
| 62 | - .setMaxWebSocketFrameSize(mqttWsProperties.getMaxFrameSize()) | |
| 63 | - .setMaxWebSocketMessageSize(mqttWsProperties.getMaxMessageSize()) | |
| 64 | - // 配置 WebSocket 子协议支持 | |
| 65 | - .addWebSocketSubProtocol(mqttWsProperties.getSubProtocol()); | |
| 66 | - | |
| 67 | - // 配置 SSL(如果启用) | |
| 68 | - if (Boolean.TRUE.equals(mqttWsProperties.getSslEnabled())) { | |
| 69 | - options.setSsl(true) | |
| 70 | - .setKeyCertOptions(mqttWsProperties.getSslOptions().getKeyCertOptions()) | |
| 71 | - .setTrustOptions(mqttWsProperties.getSslOptions().getTrustOptions()); | |
| 72 | - log.info("[start][MQTT WebSocket 已启用 SSL/TLS (wss://)]"); | |
| 73 | - } | |
| 74 | - | |
| 75 | - // 创建 HTTP 服务器 | |
| 76 | - httpServer = vertx.createHttpServer(options); | |
| 77 | - | |
| 78 | - // 设置 WebSocket 处理器 | |
| 79 | - httpServer.webSocketHandler(this::handleWebSocketConnection); | |
| 80 | - | |
| 81 | - // 启动服务器 | |
| 82 | - try { | |
| 83 | - httpServer.listen().result(); | |
| 84 | - log.info("[start][IoT 网关 MQTT WebSocket 协议启动成功,端口: {},路径: {},支持子协议: {}]", | |
| 85 | - mqttWsProperties.getPort(), mqttWsProperties.getPath(), | |
| 86 | - "mqtt, mqttv3.1, " + mqttWsProperties.getSubProtocol()); | |
| 87 | - } catch (Exception e) { | |
| 88 | - log.error("[start][IoT 网关 MQTT WebSocket 协议启动失败]", e); | |
| 89 | - throw e; | |
| 90 | - } | |
| 91 | - } | |
| 92 | - | |
| 93 | - @PreDestroy | |
| 94 | - public void stop() { | |
| 95 | - if (httpServer != null) { | |
| 96 | - try { | |
| 97 | - // 关闭所有连接 | |
| 98 | - connectionManager.closeAllConnections(); | |
| 99 | - | |
| 100 | - // 关闭服务器 | |
| 101 | - httpServer.close().result(); | |
| 102 | - log.info("[stop][IoT 网关 MQTT WebSocket 协议已停止]"); | |
| 103 | - } catch (Exception e) { | |
| 104 | - log.error("[stop][IoT 网关 MQTT WebSocket 协议停止失败]", e); | |
| 105 | - } | |
| 106 | - } | |
| 107 | - } | |
| 108 | - | |
| 109 | - /** | |
| 110 | - * 处理 WebSocket 连接请求 | |
| 111 | - * | |
| 112 | - * @param socket WebSocket 连接 | |
| 113 | - */ | |
| 114 | - private void handleWebSocketConnection(ServerWebSocket socket) { | |
| 115 | - String path = socket.path(); | |
| 116 | - String subProtocol = socket.subProtocol(); | |
| 117 | - | |
| 118 | - log.info("[handleWebSocketConnection][收到 WebSocket 连接请求,path: {},subProtocol: {},remoteAddress: {}]", | |
| 119 | - path, subProtocol, socket.remoteAddress()); | |
| 120 | - | |
| 121 | - // 验证路径 | |
| 122 | - if (!mqttWsProperties.getPath().equals(path)) { | |
| 123 | - log.warn("[handleWebSocketConnection][WebSocket 路径不匹配,拒绝连接,path: {},期望: {}]", | |
| 124 | - path, mqttWsProperties.getPath()); | |
| 125 | - socket.close(); | |
| 126 | - return; | |
| 127 | - } | |
| 128 | - | |
| 129 | - // 验证子协议 | |
| 130 | - // Vert.x 已经自动进行了子协议协商,这里只需要验证是否为 MQTT 相关协议 | |
| 131 | - if (subProtocol != null && !subProtocol.startsWith("mqtt")) { | |
| 132 | - log.warn("[handleWebSocketConnection][WebSocket 子协议不支持,拒绝连接,subProtocol: {}]", subProtocol); | |
| 133 | - socket.close(); | |
| 134 | - return; | |
| 135 | - } | |
| 136 | - | |
| 137 | - log.info("[handleWebSocketConnection][WebSocket 连接已接受,remoteAddress: {},subProtocol: {}]", | |
| 138 | - socket.remoteAddress(), subProtocol); | |
| 139 | - | |
| 140 | - // 创建处理器并处理连接 | |
| 141 | - IotMqttWsUpstreamHandler handler = new IotMqttWsUpstreamHandler( | |
| 142 | - this, messageService, connectionManager); | |
| 143 | - handler.handle(socket); | |
| 144 | - } | |
| 145 | - | |
| 146 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/manager/IotMqttWsConnectionManager.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager; | |
| 2 | - | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | -import io.vertx.core.http.ServerWebSocket; | |
| 5 | -import lombok.extern.slf4j.Slf4j; | |
| 6 | -import org.springframework.stereotype.Component; | |
| 7 | - | |
| 8 | -import java.util.Map; | |
| 9 | -import java.util.Set; | |
| 10 | -import java.util.concurrent.ConcurrentHashMap; | |
| 11 | -import java.util.concurrent.CopyOnWriteArraySet; | |
| 12 | - | |
| 13 | -/** | |
| 14 | - * IoT MQTT WebSocket 连接管理器 | |
| 15 | - * | |
| 16 | - * @author 芋道源码 | |
| 17 | - */ | |
| 18 | -@Slf4j | |
| 19 | -@Component | |
| 20 | -public class IotMqttWsConnectionManager { | |
| 21 | - | |
| 22 | - /** | |
| 23 | - * 存储设备连接 | |
| 24 | - * Key: 设备标识(deviceKey) | |
| 25 | - * Value: WebSocket 连接 | |
| 26 | - */ | |
| 27 | - private final Map<String, ServerWebSocket> connections = new ConcurrentHashMap<>(); | |
| 28 | - | |
| 29 | - /** | |
| 30 | - * 存储设备标识与 Socket ID 的映射 | |
| 31 | - * Key: 设备标识(deviceKey) | |
| 32 | - * Value: Socket ID(UUID) | |
| 33 | - */ | |
| 34 | - private final Map<String, String> deviceKeyToSocketId = new ConcurrentHashMap<>(); | |
| 35 | - | |
| 36 | - /** | |
| 37 | - * 存储 Socket ID 与设备标识的映射 | |
| 38 | - * Key: Socket ID(UUID) | |
| 39 | - * Value: 设备标识(deviceKey) | |
| 40 | - */ | |
| 41 | - private final Map<String, String> socketIdToDeviceKey = new ConcurrentHashMap<>(); | |
| 42 | - | |
| 43 | - /** | |
| 44 | - * 存储设备订阅的主题 | |
| 45 | - * Key: 设备标识(deviceKey) | |
| 46 | - * Value: 订阅的主题集合 | |
| 47 | - */ | |
| 48 | - private final Map<String, Set<String>> deviceSubscriptions = new ConcurrentHashMap<>(); | |
| 49 | - | |
| 50 | - /** | |
| 51 | - * 添加连接 | |
| 52 | - * | |
| 53 | - * @param deviceKey 设备标识 | |
| 54 | - * @param socket WebSocket 连接 | |
| 55 | - * @param socketId Socket ID(UUID) | |
| 56 | - */ | |
| 57 | - public void addConnection(String deviceKey, ServerWebSocket socket, String socketId) { | |
| 58 | - connections.put(deviceKey, socket); | |
| 59 | - deviceKeyToSocketId.put(deviceKey, socketId); | |
| 60 | - socketIdToDeviceKey.put(socketId, deviceKey); | |
| 61 | - log.info("[addConnection][设备连接已添加,deviceKey: {},socketId: {},当前连接数: {}]", | |
| 62 | - deviceKey, socketId, connections.size()); | |
| 63 | - } | |
| 64 | - | |
| 65 | - /** | |
| 66 | - * 移除连接 | |
| 67 | - * | |
| 68 | - * @param deviceKey 设备标识 | |
| 69 | - */ | |
| 70 | - public void removeConnection(String deviceKey) { | |
| 71 | - ServerWebSocket socket = connections.remove(deviceKey); | |
| 72 | - String socketId = deviceKeyToSocketId.remove(deviceKey); | |
| 73 | - if (socketId != null) { | |
| 74 | - socketIdToDeviceKey.remove(socketId); | |
| 75 | - } | |
| 76 | - if (socket != null) { | |
| 77 | - log.info("[removeConnection][设备连接已移除,deviceKey: {},socketId: {},当前连接数: {}]", | |
| 78 | - deviceKey, socketId, connections.size()); | |
| 79 | - } | |
| 80 | - } | |
| 81 | - | |
| 82 | - /** | |
| 83 | - * 根据 Socket ID 移除连接 | |
| 84 | - * | |
| 85 | - * @param socketId WebSocket 文本框架 ID | |
| 86 | - */ | |
| 87 | - public void removeConnectionBySocketId(String socketId) { | |
| 88 | - String deviceKey = socketIdToDeviceKey.remove(socketId); | |
| 89 | - if (deviceKey != null) { | |
| 90 | - connections.remove(deviceKey); | |
| 91 | - log.info("[removeConnectionBySocketId][设备连接已移除,socketId: {},deviceKey: {},当前连接数: {}]", | |
| 92 | - socketId, deviceKey, connections.size()); | |
| 93 | - } | |
| 94 | - } | |
| 95 | - | |
| 96 | - /** | |
| 97 | - * 获取连接 | |
| 98 | - * | |
| 99 | - * @param deviceKey 设备标识 | |
| 100 | - * @return WebSocket 连接 | |
| 101 | - */ | |
| 102 | - public ServerWebSocket getConnection(String deviceKey) { | |
| 103 | - return connections.get(deviceKey); | |
| 104 | - } | |
| 105 | - | |
| 106 | - /** | |
| 107 | - * 根据 Socket ID 获取设备标识 | |
| 108 | - * | |
| 109 | - * @param socketId WebSocket 文本框架 ID | |
| 110 | - * @return 设备标识 | |
| 111 | - */ | |
| 112 | - public String getDeviceKeyBySocketId(String socketId) { | |
| 113 | - return socketIdToDeviceKey.get(socketId); | |
| 114 | - } | |
| 115 | - | |
| 116 | - /** | |
| 117 | - * 检查设备是否在线 | |
| 118 | - * | |
| 119 | - * @param deviceKey 设备标识 | |
| 120 | - * @return 是否在线 | |
| 121 | - */ | |
| 122 | - public boolean isOnline(String deviceKey) { | |
| 123 | - return connections.containsKey(deviceKey); | |
| 124 | - } | |
| 125 | - | |
| 126 | - /** | |
| 127 | - * 获取当前连接数 | |
| 128 | - * | |
| 129 | - * @return 连接数 | |
| 130 | - */ | |
| 131 | - public int getConnectionCount() { | |
| 132 | - return connections.size(); | |
| 133 | - } | |
| 134 | - | |
| 135 | - /** | |
| 136 | - * 关闭所有连接 | |
| 137 | - */ | |
| 138 | - public void closeAllConnections() { | |
| 139 | - connections.forEach((deviceKey, socket) -> { | |
| 140 | - try { | |
| 141 | - socket.close(); | |
| 142 | - log.info("[closeAllConnections][关闭设备连接,deviceKey: {}]", deviceKey); | |
| 143 | - } catch (Exception e) { | |
| 144 | - log.error("[closeAllConnections][关闭设备连接失败,deviceKey: {}]", deviceKey, e); | |
| 145 | - } | |
| 146 | - }); | |
| 147 | - connections.clear(); | |
| 148 | - deviceKeyToSocketId.clear(); | |
| 149 | - socketIdToDeviceKey.clear(); | |
| 150 | - deviceSubscriptions.clear(); | |
| 151 | - log.info("[closeAllConnections][所有连接已关闭]"); | |
| 152 | - } | |
| 153 | - | |
| 154 | - // ==================== 订阅管理方法 ==================== | |
| 155 | - | |
| 156 | - /** | |
| 157 | - * 添加订阅 | |
| 158 | - * | |
| 159 | - * @param deviceKey 设备标识 | |
| 160 | - * @param topic 订阅主题 | |
| 161 | - */ | |
| 162 | - public void addSubscription(String deviceKey, String topic) { | |
| 163 | - deviceSubscriptions.computeIfAbsent(deviceKey, k -> new CopyOnWriteArraySet<>()).add(topic); | |
| 164 | - log.debug("[addSubscription][设备订阅主题,deviceKey: {},topic: {}]", deviceKey, topic); | |
| 165 | - } | |
| 166 | - | |
| 167 | - /** | |
| 168 | - * 移除订阅 | |
| 169 | - * | |
| 170 | - * @param deviceKey 设备标识 | |
| 171 | - * @param topic 订阅主题 | |
| 172 | - */ | |
| 173 | - public void removeSubscription(String deviceKey, String topic) { | |
| 174 | - Set<String> topics = deviceSubscriptions.get(deviceKey); | |
| 175 | - if (topics != null) { | |
| 176 | - topics.remove(topic); | |
| 177 | - log.debug("[removeSubscription][设备取消订阅,deviceKey: {},topic: {}]", deviceKey, topic); | |
| 178 | - } | |
| 179 | - } | |
| 180 | - | |
| 181 | - /** | |
| 182 | - * 检查设备是否订阅了指定主题 | |
| 183 | - * 支持 MQTT 通配符匹配(+ 和 #) | |
| 184 | - * | |
| 185 | - * @param deviceKey 设备标识 | |
| 186 | - * @param topic 发布主题 | |
| 187 | - * @return 是否匹配 | |
| 188 | - */ | |
| 189 | - public boolean isSubscribed(String deviceKey, String topic) { | |
| 190 | - Set<String> subscriptions = deviceSubscriptions.get(deviceKey); | |
| 191 | - if (CollUtil.isEmpty(subscriptions)) { | |
| 192 | - return false; | |
| 193 | - } | |
| 194 | - | |
| 195 | - // 检查是否有匹配的订阅 | |
| 196 | - for (String subscription : subscriptions) { | |
| 197 | - if (topicMatches(subscription, topic)) { | |
| 198 | - return true; | |
| 199 | - } | |
| 200 | - } | |
| 201 | - return false; | |
| 202 | - } | |
| 203 | - | |
| 204 | - /** | |
| 205 | - * 获取设备的所有订阅 | |
| 206 | - * | |
| 207 | - * @param deviceKey 设备标识 | |
| 208 | - * @return 订阅主题集合 | |
| 209 | - */ | |
| 210 | - public Set<String> getSubscriptions(String deviceKey) { | |
| 211 | - return deviceSubscriptions.get(deviceKey); | |
| 212 | - } | |
| 213 | - | |
| 214 | - // TODO @haohao:这个方法,是不是也可以考虑抽到 IotMqttTopicUtils 里面去哈;感觉更简洁一点? | |
| 215 | - /** | |
| 216 | - * MQTT 主题匹配 | |
| 217 | - * 支持通配符: | |
| 218 | - * - +:匹配单层主题 | |
| 219 | - * - #:匹配多层主题(必须在末尾) | |
| 220 | - * | |
| 221 | - * @param subscription 订阅主题(可能包含通配符) | |
| 222 | - * @param topic 发布主题(不包含通配符) | |
| 223 | - * @return 是否匹配 | |
| 224 | - */ | |
| 225 | - private boolean topicMatches(String subscription, String topic) { | |
| 226 | - // 完全匹配 | |
| 227 | - if (subscription.equals(topic)) { | |
| 228 | - return true; | |
| 229 | - } | |
| 230 | - | |
| 231 | - // 不包含通配符 | |
| 232 | - // TODO @haohao:这里要不要枚举下哈;+ # | |
| 233 | - if (!subscription.contains("+") && !subscription.contains("#")) { | |
| 234 | - return false; | |
| 235 | - } | |
| 236 | - | |
| 237 | - String[] subscriptionParts = subscription.split("/"); | |
| 238 | - String[] topicParts = topic.split("/"); | |
| 239 | - int i = 0; | |
| 240 | - for (; i < subscriptionParts.length && i < topicParts.length; i++) { | |
| 241 | - String subPart = subscriptionParts[i]; | |
| 242 | - String topicPart = topicParts[i]; | |
| 243 | - | |
| 244 | - // # 匹配剩余所有层级,且必须在末尾 | |
| 245 | - if (subPart.equals("#")) { | |
| 246 | - return i == subscriptionParts.length - 1; | |
| 247 | - } | |
| 248 | - | |
| 249 | - // 不是通配符且不匹配 | |
| 250 | - if (!subPart.equals("+") && !subPart.equals(topicPart)) { | |
| 251 | - return false; | |
| 252 | - } | |
| 253 | - } | |
| 254 | - | |
| 255 | - // 检查是否都匹配完 | |
| 256 | - return i == subscriptionParts.length && i == topicParts.length; | |
| 257 | - } | |
| 258 | - | |
| 259 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/package-info.java deleted
| 1 | -/** | |
| 2 | - * IoT 网关 MQTT WebSocket 协议实现 | |
| 3 | - * <p> | |
| 4 | - * 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持: | |
| 5 | - * - 标准 MQTT 3.1.1 协议 | |
| 6 | - * - WebSocket 协议升级 | |
| 7 | - * - SSL/TLS 加密(wss://) | |
| 8 | - * - 设备认证与连接管理 | |
| 9 | - * - QoS 0/1/2 消息质量保证 | |
| 10 | - * - 双向消息通信(上行/下行) | |
| 11 | - * | |
| 12 | - * @author 芋道源码 | |
| 13 | - */ | |
| 14 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws; | |
| 15 | - |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsDownstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 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.mqttws.manager.IotMqttWsConnectionManager; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 11 | -import io.vertx.core.buffer.Buffer; | |
| 12 | -import io.vertx.core.http.ServerWebSocket; | |
| 13 | -import lombok.extern.slf4j.Slf4j; | |
| 14 | - | |
| 15 | -import java.util.concurrent.atomic.AtomicInteger; | |
| 16 | - | |
| 17 | -/** | |
| 18 | - * IoT MQTT WebSocket 下行消息处理器 | |
| 19 | - * <p> | |
| 20 | - * 处理从消息总线发送到设备的消息,包括: | |
| 21 | - * - 属性设置 | |
| 22 | - * - 服务调用 | |
| 23 | - * - 事件通知 | |
| 24 | - * | |
| 25 | - * @author 芋道源码 | |
| 26 | - */ | |
| 27 | -@Slf4j | |
| 28 | -public class IotMqttWsDownstreamHandler { | |
| 29 | - | |
| 30 | - private final IotDeviceMessageService deviceMessageService; | |
| 31 | - | |
| 32 | - private final IotDeviceService deviceService; | |
| 33 | - | |
| 34 | - private final IotMqttWsConnectionManager connectionManager; | |
| 35 | - | |
| 36 | - /** | |
| 37 | - * 消息 ID 生成器(用于发布消息) | |
| 38 | - */ | |
| 39 | - private final AtomicInteger messageIdGenerator = new AtomicInteger(1); | |
| 40 | - | |
| 41 | - public IotMqttWsDownstreamHandler(IotDeviceMessageService deviceMessageService, | |
| 42 | - IotDeviceService deviceService, | |
| 43 | - IotMqttWsConnectionManager connectionManager) { | |
| 44 | - this.deviceMessageService = deviceMessageService; | |
| 45 | - this.deviceService = deviceService; | |
| 46 | - this.connectionManager = connectionManager; | |
| 47 | - } | |
| 48 | - | |
| 49 | - /** | |
| 50 | - * 处理下行消息 | |
| 51 | - * | |
| 52 | - * @param message 设备消息 | |
| 53 | - * @return 是否处理成功 | |
| 54 | - */ | |
| 55 | - public boolean handleDownstreamMessage(IotDeviceMessage message) { | |
| 56 | - try { | |
| 57 | - // 1. 基础校验 | |
| 58 | - if (message == null || message.getDeviceId() == null) { | |
| 59 | - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); | |
| 60 | - return false; | |
| 61 | - } | |
| 62 | - | |
| 63 | - // 2. 获取设备信息 | |
| 64 | - IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 65 | - if (deviceInfo == null) { | |
| 66 | - log.warn("[handleDownstreamMessage][设备不存在,设备 ID:{}]", message.getDeviceId()); | |
| 67 | - return false; | |
| 68 | - } | |
| 69 | - | |
| 70 | - // 3. 构建设备标识 | |
| 71 | - String deviceKey = deviceInfo.getProductKey() + ":" + deviceInfo.getDeviceName(); | |
| 72 | - | |
| 73 | - // 4. 检查设备是否在线 | |
| 74 | - if (!connectionManager.isOnline(deviceKey)) { | |
| 75 | - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,deviceKey: {}]", deviceKey); | |
| 76 | - return false; | |
| 77 | - } | |
| 78 | - | |
| 79 | - // 5. 构建主题 | |
| 80 | - String topic = buildDownstreamTopic(message, deviceInfo); | |
| 81 | - if (StrUtil.isBlank(topic)) { | |
| 82 | - log.warn("[handleDownstreamMessage][主题构建失败,设备 ID:{},方法:{}]", | |
| 83 | - message.getDeviceId(), message.getMethod()); | |
| 84 | - return false; | |
| 85 | - } | |
| 86 | - | |
| 87 | - // 6. 检查设备是否订阅了该主题 | |
| 88 | - if (!connectionManager.isSubscribed(deviceKey, topic)) { | |
| 89 | - log.warn("[handleDownstreamMessage][设备未订阅该主题,deviceKey: {},topic: {}]", deviceKey, topic); | |
| 90 | - return false; | |
| 91 | - } | |
| 92 | - | |
| 93 | - // 8. 编码消息 | |
| 94 | - byte[] payload = deviceMessageService.encodeDeviceMessage(message, | |
| 95 | - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 96 | - if (payload == null || payload.length == 0) { | |
| 97 | - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); | |
| 98 | - return false; | |
| 99 | - } | |
| 100 | - | |
| 101 | - // 9. 发送消息到设备 | |
| 102 | - return sendMessageToDevice(deviceKey, topic, payload, 1); | |
| 103 | - } catch (Exception e) { | |
| 104 | - if (message != null) { | |
| 105 | - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", | |
| 106 | - message.getDeviceId(), e.getMessage(), e); | |
| 107 | - } | |
| 108 | - return false; | |
| 109 | - } | |
| 110 | - } | |
| 111 | - | |
| 112 | - /** | |
| 113 | - * 构建下行消息主题 | |
| 114 | - * | |
| 115 | - * @param message 设备消息 | |
| 116 | - * @param deviceInfo 设备信息 | |
| 117 | - * @return 主题 | |
| 118 | - */ | |
| 119 | - private String buildDownstreamTopic(IotDeviceMessage message, IotDeviceRespDTO deviceInfo) { | |
| 120 | - String method = message.getMethod(); | |
| 121 | - if (StrUtil.isBlank(method)) { | |
| 122 | - return null; | |
| 123 | - } | |
| 124 | - | |
| 125 | - // 使用工具类构建主题,支持回复消息处理 | |
| 126 | - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); | |
| 127 | - return IotMqttTopicUtils.buildTopicByMethod(method, deviceInfo.getProductKey(), | |
| 128 | - deviceInfo.getDeviceName(), isReply); | |
| 129 | - } | |
| 130 | - | |
| 131 | - /** | |
| 132 | - * 发送消息到设备 | |
| 133 | - * | |
| 134 | - * @param deviceKey 设备标识(productKey:deviceName) | |
| 135 | - * @param topic 主题 | |
| 136 | - * @param payload 消息内容 | |
| 137 | - * @param qos QoS 级别 | |
| 138 | - * @return 是否发送成功 | |
| 139 | - */ | |
| 140 | - private boolean sendMessageToDevice(String deviceKey, String topic, byte[] payload, int qos) { | |
| 141 | - // 获取设备连接 | |
| 142 | - ServerWebSocket socket = connectionManager.getConnection(deviceKey); | |
| 143 | - if (socket == null) { | |
| 144 | - log.warn("[sendMessageToDevice][设备未连接,deviceKey: {}]", deviceKey); | |
| 145 | - return false; | |
| 146 | - } | |
| 147 | - | |
| 148 | - try { | |
| 149 | - int messageId = qos > 0 ? generateMessageId() : 0; | |
| 150 | - | |
| 151 | - // 手动编码 MQTT PUBLISH 消息 | |
| 152 | - io.netty.buffer.ByteBuf byteBuf = io.netty.buffer.Unpooled.buffer(); | |
| 153 | - | |
| 154 | - // 固定头:消息类型(PUBLISH=3) + DUP(0) + QoS + RETAIN | |
| 155 | - int fixedHeaderByte1 = 0x30 | (qos << 1); // PUBLISH类型 | |
| 156 | - byteBuf.writeByte(fixedHeaderByte1); | |
| 157 | - | |
| 158 | - // 计算剩余长度 | |
| 159 | - int topicLength = topic.getBytes().length; | |
| 160 | - int remainingLength = 2 + topicLength + (qos > 0 ? 2 : 0) + payload.length; | |
| 161 | - | |
| 162 | - // 写入剩余长度(简化版本,假设小于 128 字节) | |
| 163 | - if (remainingLength < 128) { | |
| 164 | - byteBuf.writeByte(remainingLength); | |
| 165 | - } else { | |
| 166 | - // 处理大于 127 的情况 | |
| 167 | - int x = remainingLength; | |
| 168 | - do { | |
| 169 | - int encodedByte = x % 128; | |
| 170 | - x = x / 128; | |
| 171 | - if (x > 0) { | |
| 172 | - encodedByte = encodedByte | 128; | |
| 173 | - } | |
| 174 | - byteBuf.writeByte(encodedByte); | |
| 175 | - } while (x > 0); | |
| 176 | - } | |
| 177 | - | |
| 178 | - // 可变头:主题名称 | |
| 179 | - byteBuf.writeShort(topicLength); | |
| 180 | - byteBuf.writeBytes(topic.getBytes()); | |
| 181 | - | |
| 182 | - // 可变头:消息 ID(仅 QoS > 0 时) | |
| 183 | - if (qos > 0) { | |
| 184 | - byteBuf.writeShort(messageId); | |
| 185 | - } | |
| 186 | - | |
| 187 | - // 有效载荷 | |
| 188 | - byteBuf.writeBytes(payload); | |
| 189 | - | |
| 190 | - // 发送 | |
| 191 | - byte[] bytes = new byte[byteBuf.readableBytes()]; | |
| 192 | - byteBuf.readBytes(bytes); | |
| 193 | - byteBuf.release(); | |
| 194 | - socket.writeBinaryMessage(Buffer.buffer(bytes)); | |
| 195 | - | |
| 196 | - log.info("[sendMessageToDevice][消息已发送到设备,deviceKey: {},topic: {},qos: {},messageId: {}]", | |
| 197 | - deviceKey, topic, qos, messageId); | |
| 198 | - return true; | |
| 199 | - } catch (Exception e) { | |
| 200 | - log.error("[sendMessageToDevice][发送消息到设备失败,deviceKey: {},topic: {}]", deviceKey, topic, e); | |
| 201 | - return false; | |
| 202 | - } | |
| 203 | - } | |
| 204 | - | |
| 205 | - /** | |
| 206 | - * 生成消息 ID | |
| 207 | - * | |
| 208 | - * @return 消息 ID | |
| 209 | - */ | |
| 210 | - private int generateMessageId() { | |
| 211 | - int id = messageIdGenerator.getAndIncrement(); | |
| 212 | - // MQTT 消息 ID 范围是 1-65535 | |
| 213 | - // TODO @haohao:并发可能有问题; | |
| 214 | - if (id > 65535) { | |
| 215 | - messageIdGenerator.set(1); | |
| 216 | - return 1; | |
| 217 | - } | |
| 218 | - return id; | |
| 219 | - } | |
| 220 | - | |
| 221 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.util.BooleanUtil; | |
| 4 | -import cn.hutool.core.util.IdUtil; | |
| 5 | -import cn.hutool.core.util.StrUtil; | |
| 6 | -import cn.hutool.extra.spring.SpringUtil; | |
| 7 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 8 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 9 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | |
| 11 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 12 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 13 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 14 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol; | |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager; | |
| 17 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 18 | -import io.netty.buffer.ByteBuf; | |
| 19 | -import io.netty.buffer.Unpooled; | |
| 20 | -import io.netty.channel.embedded.EmbeddedChannel; | |
| 21 | -import io.netty.handler.codec.DecoderException; | |
| 22 | -import io.netty.handler.codec.mqtt.*; | |
| 23 | -import io.vertx.core.buffer.Buffer; | |
| 24 | -import io.vertx.core.http.ServerWebSocket; | |
| 25 | -import lombok.extern.slf4j.Slf4j; | |
| 26 | - | |
| 27 | -import java.nio.charset.StandardCharsets; | |
| 28 | -import java.util.concurrent.ConcurrentHashMap; | |
| 29 | -import java.util.concurrent.atomic.AtomicInteger; | |
| 30 | - | |
| 31 | -/** | |
| 32 | - * IoT MQTT WebSocket 上行消息处理器 | |
| 33 | - * <p> | |
| 34 | - * 处理来自设备的 MQTT 消息,包括: | |
| 35 | - * - CONNECT:设备连接认证 | |
| 36 | - * - PUBLISH:设备发布消息 | |
| 37 | - * - SUBSCRIBE:设备订阅主题 | |
| 38 | - * - UNSUBSCRIBE:设备取消订阅 | |
| 39 | - * - PINGREQ:心跳请求 | |
| 40 | - * - DISCONNECT:设备断开连接 | |
| 41 | - * | |
| 42 | - * @author 芋道源码 | |
| 43 | - */ | |
| 44 | -@Slf4j | |
| 45 | -public class IotMqttWsUpstreamHandler { | |
| 46 | - | |
| 47 | - private final IotMqttWsUpstreamProtocol upstreamProtocol; | |
| 48 | - | |
| 49 | - private final IotDeviceCommonApi deviceApi; | |
| 50 | - | |
| 51 | - private final IotDeviceMessageService messageService; | |
| 52 | - | |
| 53 | - private final IotMqttWsConnectionManager connectionManager; | |
| 54 | - | |
| 55 | - /** | |
| 56 | - * 存储 WebSocket 连接到 Socket ID 的映射 | |
| 57 | - * Key: WebSocket 对象 | |
| 58 | - * Value: Socket ID(UUID) | |
| 59 | - */ | |
| 60 | - private final ConcurrentHashMap<ServerWebSocket, String> socketIdMap = new ConcurrentHashMap<>(); | |
| 61 | - | |
| 62 | - /** | |
| 63 | - * 存储 Socket ID 对应的设备信息 | |
| 64 | - * Key: Socket ID(UUID) | |
| 65 | - * Value: 设备信息 | |
| 66 | - */ | |
| 67 | - private final ConcurrentHashMap<String, IotDeviceRespDTO> socketDeviceMap = new ConcurrentHashMap<>(); | |
| 68 | - | |
| 69 | - /** | |
| 70 | - * 存储设备的消息 ID 生成器(用于 QoS > 0 的消息) | |
| 71 | - */ | |
| 72 | - private final ConcurrentHashMap<String, AtomicInteger> deviceMessageIdMap = new ConcurrentHashMap<>(); | |
| 73 | - | |
| 74 | - /** | |
| 75 | - * MQTT 解码通道(用于解析 WebSocket 中的 MQTT 二进制消息) | |
| 76 | - */ | |
| 77 | - private final ThreadLocal<EmbeddedChannel> decoderChannelThreadLocal = ThreadLocal | |
| 78 | - .withInitial(() -> new EmbeddedChannel(new MqttDecoder())); | |
| 79 | - | |
| 80 | - /** | |
| 81 | - * MQTT 编码通道(用于编码 MQTT 响应消息) | |
| 82 | - */ | |
| 83 | - private final ThreadLocal<EmbeddedChannel> encoderChannelThreadLocal = ThreadLocal | |
| 84 | - .withInitial(() -> new EmbeddedChannel(MqttEncoder.INSTANCE)); | |
| 85 | - | |
| 86 | - public IotMqttWsUpstreamHandler(IotMqttWsUpstreamProtocol upstreamProtocol, | |
| 87 | - IotDeviceMessageService messageService, | |
| 88 | - IotMqttWsConnectionManager connectionManager) { | |
| 89 | - this.upstreamProtocol = upstreamProtocol; | |
| 90 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 91 | - this.messageService = messageService; | |
| 92 | - this.connectionManager = connectionManager; | |
| 93 | - } | |
| 94 | - | |
| 95 | - /** | |
| 96 | - * 处理 WebSocket 连接 | |
| 97 | - * | |
| 98 | - * @param socket WebSocket 连接 | |
| 99 | - */ | |
| 100 | - public void handle(ServerWebSocket socket) { | |
| 101 | - // 生成唯一的 Socket ID(因为 MQTT 使用二进制协议,textHandlerID() 会返回 null) | |
| 102 | - String socketId = IdUtil.simpleUUID(); | |
| 103 | - socketIdMap.put(socket, socketId); | |
| 104 | - | |
| 105 | - log.info("[handle][WebSocket 连接建立,socketId: {},remoteAddress: {}]", | |
| 106 | - socketId, socket.remoteAddress()); | |
| 107 | - | |
| 108 | - // 设置二进制数据处理器 | |
| 109 | - socket.binaryMessageHandler(buffer -> { | |
| 110 | - try { | |
| 111 | - handleMqttMessage(socket, buffer); | |
| 112 | - } catch (Exception e) { | |
| 113 | - log.error("[handle][处理 MQTT 消息异常,socketId: {}]", socketId, e); | |
| 114 | - socket.close(); | |
| 115 | - } | |
| 116 | - }); | |
| 117 | - | |
| 118 | - // 设置关闭处理器 | |
| 119 | - socket.closeHandler(v -> { | |
| 120 | - socketIdMap.remove(socket); | |
| 121 | - IotDeviceRespDTO device = socketDeviceMap.remove(socketId); | |
| 122 | - if (device != null) { | |
| 123 | - String deviceKey = device.getProductKey() + ":" + device.getDeviceName(); | |
| 124 | - connectionManager.removeConnection(deviceKey); | |
| 125 | - deviceMessageIdMap.remove(deviceKey); | |
| 126 | - // 发送设备离线消息 | |
| 127 | - sendOfflineMessage(device); | |
| 128 | - log.info("[handle][WebSocket 连接关闭,deviceKey: {},socketId: {}]", deviceKey, socketId); | |
| 129 | - } | |
| 130 | - }); | |
| 131 | - | |
| 132 | - // 设置异常处理器 | |
| 133 | - socket.exceptionHandler(e -> { | |
| 134 | - log.error("[handle][WebSocket 连接异常,socketId: {}]", socketId, e); | |
| 135 | - socketIdMap.remove(socket); | |
| 136 | - IotDeviceRespDTO device = socketDeviceMap.remove(socketId); | |
| 137 | - if (device != null) { | |
| 138 | - String deviceKey = device.getProductKey() + ":" + device.getDeviceName(); | |
| 139 | - connectionManager.removeConnection(deviceKey); | |
| 140 | - deviceMessageIdMap.remove(deviceKey); | |
| 141 | - } | |
| 142 | - socket.close(); | |
| 143 | - }); | |
| 144 | - } | |
| 145 | - | |
| 146 | - /** | |
| 147 | - * 处理 MQTT 消息 | |
| 148 | - * | |
| 149 | - * @param socket WebSocket 连接 | |
| 150 | - * @param buffer 消息缓冲区 | |
| 151 | - */ | |
| 152 | - private void handleMqttMessage(ServerWebSocket socket, Buffer buffer) { | |
| 153 | - String socketId = socketIdMap.get(socket); | |
| 154 | - ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer.getBytes()); | |
| 155 | - | |
| 156 | - try { | |
| 157 | - // 使用 EmbeddedChannel 解码 MQTT 消息 | |
| 158 | - EmbeddedChannel decoderChannel = decoderChannelThreadLocal.get(); | |
| 159 | - decoderChannel.writeInbound(byteBuf.retain()); | |
| 160 | - | |
| 161 | - // 读取解码后的消息 | |
| 162 | - MqttMessage mqttMessage = decoderChannel.readInbound(); | |
| 163 | - if (mqttMessage == null) { | |
| 164 | - log.warn("[handleMqttMessage][MQTT 消息解码失败,socketId: {}]", socketId); | |
| 165 | - return; | |
| 166 | - } | |
| 167 | - | |
| 168 | - MqttMessageType messageType = mqttMessage.fixedHeader().messageType(); | |
| 169 | - log.debug("[handleMqttMessage][收到 MQTT 消息,类型: {},socketId: {}]", messageType, socketId); | |
| 170 | - | |
| 171 | - // 根据消息类型分发处理 | |
| 172 | - switch (messageType) { | |
| 173 | - case CONNECT: | |
| 174 | - handleConnect(socket, (MqttConnectMessage) mqttMessage); | |
| 175 | - break; | |
| 176 | - case PUBLISH: | |
| 177 | - handlePublish(socket, (MqttPublishMessage) mqttMessage); | |
| 178 | - break; | |
| 179 | - case PUBACK: | |
| 180 | - handlePubAck(socket, mqttMessage); | |
| 181 | - break; | |
| 182 | - case PUBREC: | |
| 183 | - handlePubRec(socket, mqttMessage); | |
| 184 | - break; | |
| 185 | - case PUBREL: | |
| 186 | - handlePubRel(socket, mqttMessage); | |
| 187 | - break; | |
| 188 | - case PUBCOMP: | |
| 189 | - handlePubComp(socket, mqttMessage); | |
| 190 | - break; | |
| 191 | - case SUBSCRIBE: | |
| 192 | - handleSubscribe(socket, (MqttSubscribeMessage) mqttMessage); | |
| 193 | - break; | |
| 194 | - case UNSUBSCRIBE: | |
| 195 | - handleUnsubscribe(socket, (MqttUnsubscribeMessage) mqttMessage); | |
| 196 | - break; | |
| 197 | - case PINGREQ: | |
| 198 | - handlePingReq(socket); | |
| 199 | - break; | |
| 200 | - case DISCONNECT: | |
| 201 | - handleDisconnect(socket); | |
| 202 | - break; | |
| 203 | - default: | |
| 204 | - log.warn("[handleMqttMessage][不支持的消息类型: {},socketId: {}]", messageType, socketId); | |
| 205 | - } | |
| 206 | - } catch (DecoderException e) { | |
| 207 | - log.error("[handleMqttMessage][MQTT 消息解码异常,socketId: {}]", socketId, e); | |
| 208 | - socket.close(); | |
| 209 | - } catch (Exception e) { | |
| 210 | - log.error("[handleMqttMessage][处理 MQTT 消息失败,socketId: {}]", socketId, e); | |
| 211 | - socket.close(); | |
| 212 | - } finally { | |
| 213 | - byteBuf.release(); | |
| 214 | - } | |
| 215 | - } | |
| 216 | - | |
| 217 | - /** | |
| 218 | - * 处理 CONNECT 消息(设备认证) | |
| 219 | - */ | |
| 220 | - private void handleConnect(ServerWebSocket socket, MqttConnectMessage message) { | |
| 221 | - String socketId = socketIdMap.get(socket); | |
| 222 | - try { | |
| 223 | - // 1. 解析 CONNECT 消息 | |
| 224 | - MqttConnectPayload payload = message.payload(); | |
| 225 | - String clientId = payload.clientIdentifier(); | |
| 226 | - String username = payload.userName(); | |
| 227 | - String password = payload.passwordInBytes() != null | |
| 228 | - ? new String(payload.passwordInBytes(), StandardCharsets.UTF_8) | |
| 229 | - : null; | |
| 230 | - | |
| 231 | - log.info("[handleConnect][收到 CONNECT 消息,clientId: {},username: {},socketId: {}]", | |
| 232 | - clientId, username, socketId); | |
| 233 | - | |
| 234 | - // 2. 设备认证 | |
| 235 | - IotDeviceRespDTO device = authenticateDevice(clientId, username, password); | |
| 236 | - if (device == null) { | |
| 237 | - log.warn("[handleConnect][设备认证失败,clientId: {},socketId: {}]", clientId, socketId); | |
| 238 | - sendConnAck(socket, MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | |
| 239 | - socket.close(); | |
| 240 | - return; | |
| 241 | - } | |
| 242 | - | |
| 243 | - // 3. 保存设备信息 | |
| 244 | - socketDeviceMap.put(socketId, device); | |
| 245 | - String deviceKey = device.getProductKey() + ":" + device.getDeviceName(); | |
| 246 | - connectionManager.addConnection(deviceKey, socket, socketId); | |
| 247 | - deviceMessageIdMap.put(deviceKey, new AtomicInteger(1)); | |
| 248 | - | |
| 249 | - log.info("[handleConnect][设备认证成功,deviceId: {},deviceKey: {},socketId: {}]", | |
| 250 | - device.getId(), deviceKey, socketId); | |
| 251 | - | |
| 252 | - // 4. 发送 CONNACK | |
| 253 | - sendConnAck(socket, MqttConnectReturnCode.CONNECTION_ACCEPTED); | |
| 254 | - | |
| 255 | - // 5. 发送设备上线消息 | |
| 256 | - sendOnlineMessage(device); | |
| 257 | - } catch (Exception e) { | |
| 258 | - log.error("[handleConnect][处理 CONNECT 消息失败,socketId: {}]", socketId, e); | |
| 259 | - sendConnAck(socket, MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE); | |
| 260 | - socket.close(); | |
| 261 | - } | |
| 262 | - } | |
| 263 | - | |
| 264 | - /** | |
| 265 | - * 处理 PUBLISH 消息(设备发布消息) | |
| 266 | - */ | |
| 267 | - private void handlePublish(ServerWebSocket socket, MqttPublishMessage message) { | |
| 268 | - String socketId = socketIdMap.get(socket); | |
| 269 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 270 | - | |
| 271 | - if (device == null) { | |
| 272 | - log.warn("[handlePublish][设备未认证,socketId: {}]", socketId); | |
| 273 | - socket.close(); | |
| 274 | - return; | |
| 275 | - } | |
| 276 | - | |
| 277 | - try { | |
| 278 | - // 1. 解析 PUBLISH 消息 | |
| 279 | - MqttFixedHeader fixedHeader = message.fixedHeader(); | |
| 280 | - MqttPublishVariableHeader variableHeader = message.variableHeader(); | |
| 281 | - ByteBuf payload = message.payload(); | |
| 282 | - | |
| 283 | - String topic = variableHeader.topicName(); | |
| 284 | - int messageId = variableHeader.packetId(); | |
| 285 | - MqttQoS qos = fixedHeader.qosLevel(); | |
| 286 | - | |
| 287 | - log.debug("[handlePublish][收到 PUBLISH 消息,topic: {},messageId: {},QoS: {},deviceId: {}]", | |
| 288 | - topic, messageId, qos, device.getId()); | |
| 289 | - | |
| 290 | - // 2. 读取 payload | |
| 291 | - byte[] payloadBytes = new byte[payload.readableBytes()]; | |
| 292 | - payload.readBytes(payloadBytes); | |
| 293 | - | |
| 294 | - // 3. 解码并发送消息 | |
| 295 | - IotDeviceMessage deviceMessage = messageService.decodeDeviceMessage(payloadBytes, | |
| 296 | - device.getProductKey(), device.getDeviceName()); | |
| 297 | - if (deviceMessage != null) { | |
| 298 | - deviceMessage.setServerId(upstreamProtocol.getServerId()); | |
| 299 | - messageService.sendDeviceMessage(deviceMessage, device.getProductKey(), | |
| 300 | - device.getDeviceName(), upstreamProtocol.getServerId()); | |
| 301 | - log.info("[handlePublish][设备消息已发送,method: {},deviceId: {}]", | |
| 302 | - deviceMessage.getMethod(), device.getId()); | |
| 303 | - } | |
| 304 | - | |
| 305 | - // 4. 根据 QoS 级别发送相应的确认消息 | |
| 306 | - if (qos == MqttQoS.AT_LEAST_ONCE) { | |
| 307 | - // QoS 1:发送 PUBACK | |
| 308 | - sendPubAck(socket, messageId); | |
| 309 | - } else if (qos == MqttQoS.EXACTLY_ONCE) { | |
| 310 | - // QoS 2:发送 PUBREC | |
| 311 | - sendPubRec(socket, messageId); | |
| 312 | - } | |
| 313 | - // QoS 0 无需确认 | |
| 314 | - } catch (Exception e) { | |
| 315 | - log.error("[handlePublish][处理 PUBLISH 消息失败,deviceId: {}]", device.getId(), e); | |
| 316 | - } | |
| 317 | - } | |
| 318 | - | |
| 319 | - /** | |
| 320 | - * 处理 PUBACK 消息(QoS 1 确认) | |
| 321 | - */ | |
| 322 | - private void handlePubAck(ServerWebSocket socket, MqttMessage message) { | |
| 323 | - String socketId = socketIdMap.get(socket); | |
| 324 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 325 | - if (device == null) { | |
| 326 | - log.warn("[handlePubAck][设备未认证,socketId: {}]", socketId); | |
| 327 | - socket.close(); | |
| 328 | - return; | |
| 329 | - } | |
| 330 | - | |
| 331 | - int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); | |
| 332 | - log.debug("[handlePubAck][收到 PUBACK,messageId: {},deviceId: {}]", messageId, device.getId()); | |
| 333 | - } | |
| 334 | - | |
| 335 | - /** | |
| 336 | - * 处理 PUBREC 消息(QoS 2 第一步确认) | |
| 337 | - */ | |
| 338 | - private void handlePubRec(ServerWebSocket socket, MqttMessage message) { | |
| 339 | - String socketId = socketIdMap.get(socket); | |
| 340 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 341 | - if (device == null) { | |
| 342 | - log.warn("[handlePubRec][设备未认证,socketId: {}]", socketId); | |
| 343 | - socket.close(); | |
| 344 | - return; | |
| 345 | - } | |
| 346 | - | |
| 347 | - int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); | |
| 348 | - log.debug("[handlePubRec][收到 PUBREC,messageId: {},deviceId: {}]", messageId, device.getId()); | |
| 349 | - // 发送 PUBREL | |
| 350 | - sendPubRel(socket, messageId); | |
| 351 | - } | |
| 352 | - | |
| 353 | - /** | |
| 354 | - * 处理 PUBREL 消息(QoS 2 第二步) | |
| 355 | - */ | |
| 356 | - private void handlePubRel(ServerWebSocket socket, MqttMessage message) { | |
| 357 | - String socketId = socketIdMap.get(socket); | |
| 358 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 359 | - | |
| 360 | - if (device == null) { | |
| 361 | - log.warn("[handlePubRel][设备未认证,socketId: {}]", socketId); | |
| 362 | - socket.close(); | |
| 363 | - return; | |
| 364 | - } | |
| 365 | - | |
| 366 | - int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); | |
| 367 | - log.debug("[handlePubRel][收到 PUBREL,messageId: {},deviceId: {}]", messageId, device.getId()); | |
| 368 | - // 发送 PUBCOMP | |
| 369 | - sendPubComp(socket, messageId); | |
| 370 | - } | |
| 371 | - | |
| 372 | - /** | |
| 373 | - * 处理 PUBCOMP 消息(QoS 2 完成确认) | |
| 374 | - */ | |
| 375 | - private void handlePubComp(ServerWebSocket socket, MqttMessage message) { | |
| 376 | - String socketId = socketIdMap.get(socket); | |
| 377 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 378 | - | |
| 379 | - if (device == null) { | |
| 380 | - log.warn("[handlePubComp][设备未认证,socketId: {}]", socketId); | |
| 381 | - socket.close(); | |
| 382 | - return; | |
| 383 | - } | |
| 384 | - | |
| 385 | - int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); | |
| 386 | - log.debug("[handlePubComp][收到 PUBCOMP,messageId: {},deviceId: {}]", messageId, device.getId()); | |
| 387 | - } | |
| 388 | - | |
| 389 | - /** | |
| 390 | - * 处理 SUBSCRIBE 消息(设备订阅主题) | |
| 391 | - */ | |
| 392 | - private void handleSubscribe(ServerWebSocket socket, MqttSubscribeMessage message) { | |
| 393 | - String socketId = socketIdMap.get(socket); | |
| 394 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 395 | - if (device == null) { | |
| 396 | - log.warn("[handleSubscribe][设备未认证,socketId: {}]", socketId); | |
| 397 | - socket.close(); | |
| 398 | - return; | |
| 399 | - } | |
| 400 | - | |
| 401 | - try { | |
| 402 | - // 1. 解析 SUBSCRIBE 消息 | |
| 403 | - int messageId = message.variableHeader().messageId(); | |
| 404 | - MqttSubscribePayload payload = message.payload(); | |
| 405 | - String deviceKey = device.getProductKey() + ":" + device.getDeviceName(); | |
| 406 | - | |
| 407 | - log.info("[handleSubscribe][设备订阅请求,deviceKey: {},messageId: {},主题数量: {}]", | |
| 408 | - deviceKey, messageId, payload.topicSubscriptions().size()); | |
| 409 | - | |
| 410 | - // 2. 构建 QoS 列表并记录订阅信息 | |
| 411 | - int[] grantedQosList = new int[payload.topicSubscriptions().size()]; | |
| 412 | - for (int i = 0; i < payload.topicSubscriptions().size(); i++) { | |
| 413 | - MqttTopicSubscription subscription = payload.topicSubscriptions().get(i); | |
| 414 | - String topic = subscription.topicFilter(); | |
| 415 | - grantedQosList[i] = subscription.qualityOfService().value(); | |
| 416 | - | |
| 417 | - // 记录订阅信息到连接管理器 | |
| 418 | - connectionManager.addSubscription(deviceKey, topic); | |
| 419 | - | |
| 420 | - log.info("[handleSubscribe][订阅主题: {},QoS: {},deviceKey: {}]", | |
| 421 | - topic, subscription.qualityOfService(), deviceKey); | |
| 422 | - } | |
| 423 | - | |
| 424 | - // 3. 发送 SUBACK | |
| 425 | - sendSubAck(socket, messageId, grantedQosList); | |
| 426 | - } catch (Exception e) { | |
| 427 | - log.error("[handleSubscribe][处理 SUBSCRIBE 消息失败,deviceId: {}]", device.getId(), e); | |
| 428 | - } | |
| 429 | - } | |
| 430 | - | |
| 431 | - /** | |
| 432 | - * 处理 UNSUBSCRIBE 消息(设备取消订阅) | |
| 433 | - */ | |
| 434 | - private void handleUnsubscribe(ServerWebSocket socket, MqttUnsubscribeMessage message) { | |
| 435 | - String socketId = socketIdMap.get(socket); | |
| 436 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 437 | - if (device == null) { | |
| 438 | - log.warn("[handleUnsubscribe][设备未认证,socketId: {}]", socketId); | |
| 439 | - socket.close(); | |
| 440 | - return; | |
| 441 | - } | |
| 442 | - | |
| 443 | - try { | |
| 444 | - // 1. 解析 UNSUBSCRIBE 消息 | |
| 445 | - int messageId = message.variableHeader().messageId(); | |
| 446 | - MqttUnsubscribePayload payload = message.payload(); | |
| 447 | - String deviceKey = device.getProductKey() + ":" + device.getDeviceName(); | |
| 448 | - | |
| 449 | - log.info("[handleUnsubscribe][设备取消订阅,deviceKey: {},messageId: {},主题数量: {}]", | |
| 450 | - deviceKey, messageId, payload.topics().size()); | |
| 451 | - | |
| 452 | - // 2. 移除订阅信息 | |
| 453 | - for (String topic : payload.topics()) { | |
| 454 | - connectionManager.removeSubscription(deviceKey, topic); | |
| 455 | - log.info("[handleUnsubscribe][取消订阅主题: {},deviceKey: {}]", topic, deviceKey); | |
| 456 | - } | |
| 457 | - | |
| 458 | - // 3. 发送 UNSUBACK | |
| 459 | - sendUnsubAck(socket, messageId); | |
| 460 | - } catch (Exception e) { | |
| 461 | - log.error("[handleUnsubscribe][处理 UNSUBSCRIBE 消息失败,deviceId: {}]", device.getId(), e); | |
| 462 | - } | |
| 463 | - } | |
| 464 | - | |
| 465 | - /** | |
| 466 | - * 处理 PINGREQ 消息(心跳请求) | |
| 467 | - */ | |
| 468 | - private void handlePingReq(ServerWebSocket socket) { | |
| 469 | - String socketId = socketIdMap.get(socket); | |
| 470 | - IotDeviceRespDTO device = socketDeviceMap.get(socketId); | |
| 471 | - if (device == null) { | |
| 472 | - log.warn("[handlePingReq][设备未认证,socketId: {}]", socketId); | |
| 473 | - socket.close(); | |
| 474 | - return; | |
| 475 | - } | |
| 476 | - | |
| 477 | - log.debug("[handlePingReq][收到心跳请求,deviceId: {}]", device.getId()); | |
| 478 | - // 发送 PINGRESP | |
| 479 | - sendPingResp(socket); | |
| 480 | - } | |
| 481 | - | |
| 482 | - /** | |
| 483 | - * 处理 DISCONNECT 消息(设备断开连接) | |
| 484 | - */ | |
| 485 | - private void handleDisconnect(ServerWebSocket socket) { | |
| 486 | - String socketId = socketIdMap.get(socket); | |
| 487 | - IotDeviceRespDTO device = socketDeviceMap.remove(socketId); | |
| 488 | - if (device != null) { | |
| 489 | - String deviceKey = device.getProductKey() + ":" + device.getDeviceName(); | |
| 490 | - connectionManager.removeConnection(deviceKey); | |
| 491 | - deviceMessageIdMap.remove(deviceKey); | |
| 492 | - sendOfflineMessage(device); | |
| 493 | - log.info("[handleDisconnect][设备主动断开连接,deviceKey: {}]", deviceKey); | |
| 494 | - } | |
| 495 | - | |
| 496 | - socket.close(); | |
| 497 | - } | |
| 498 | - | |
| 499 | - // ==================== 设备认证和状态相关方法 ==================== | |
| 500 | - | |
| 501 | - /** | |
| 502 | - * 设备认证 | |
| 503 | - */ | |
| 504 | - private IotDeviceRespDTO authenticateDevice(String clientId, String username, String password) { | |
| 505 | - try { | |
| 506 | - // 1. 参数校验 | |
| 507 | - if (StrUtil.hasEmpty(clientId, username, password)) { | |
| 508 | - log.warn("[authenticateDevice][认证参数不完整,clientId: {},username: {}]", clientId, username); | |
| 509 | - return null; | |
| 510 | - } | |
| 511 | - | |
| 512 | - // 2. 构建认证参数并调用 API | |
| 513 | - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | |
| 514 | - .setClientId(clientId) | |
| 515 | - .setUsername(username) | |
| 516 | - .setPassword(password); | |
| 517 | - | |
| 518 | - CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 519 | - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { | |
| 520 | - log.warn("[authenticateDevice][设备认证失败,clientId: {}]", clientId); | |
| 521 | - return null; | |
| 522 | - } | |
| 523 | - | |
| 524 | - // 3. 获取设备信息 | |
| 525 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 526 | - if (deviceInfo == null) { | |
| 527 | - log.warn("[authenticateDevice][用户名格式不正确,username: {}]", username); | |
| 528 | - return null; | |
| 529 | - } | |
| 530 | - | |
| 531 | - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() | |
| 532 | - .setProductKey(deviceInfo.getProductKey()) | |
| 533 | - .setDeviceName(deviceInfo.getDeviceName()); | |
| 534 | - | |
| 535 | - CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO); | |
| 536 | - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { | |
| 537 | - log.warn("[authenticateDevice][获取设备信息失败,username: {}]", username); | |
| 538 | - return null; | |
| 539 | - } | |
| 540 | - | |
| 541 | - return deviceResult.getData(); | |
| 542 | - } catch (Exception e) { | |
| 543 | - log.error("[authenticateDevice][设备认证异常,clientId: {}]", clientId, e); | |
| 544 | - return null; | |
| 545 | - } | |
| 546 | - } | |
| 547 | - | |
| 548 | - /** | |
| 549 | - * 发送设备上线消息 | |
| 550 | - */ | |
| 551 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 552 | - try { | |
| 553 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 554 | - messageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 555 | - device.getDeviceName(), upstreamProtocol.getServerId()); | |
| 556 | - log.info("[sendOnlineMessage][设备上线,deviceId: {}]", device.getId()); | |
| 557 | - } catch (Exception e) { | |
| 558 | - log.error("[sendOnlineMessage][发送设备上线消息失败,deviceId: {}]", device.getId(), e); | |
| 559 | - } | |
| 560 | - } | |
| 561 | - | |
| 562 | - /** | |
| 563 | - * 发送设备离线消息 | |
| 564 | - */ | |
| 565 | - private void sendOfflineMessage(IotDeviceRespDTO device) { | |
| 566 | - try { | |
| 567 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 568 | - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), | |
| 569 | - device.getDeviceName(), upstreamProtocol.getServerId()); | |
| 570 | - log.info("[sendOfflineMessage][设备离线,deviceId: {}]", device.getId()); | |
| 571 | - } catch (Exception e) { | |
| 572 | - log.error("[sendOfflineMessage][发送设备离线消息失败,deviceId: {}]", device.getId(), e); | |
| 573 | - } | |
| 574 | - } | |
| 575 | - | |
| 576 | - // ==================== 发送响应消息的辅助方法 ==================== | |
| 577 | - | |
| 578 | - /** | |
| 579 | - * 发送 CONNACK 消息 | |
| 580 | - */ | |
| 581 | - private void sendConnAck(ServerWebSocket socket, MqttConnectReturnCode returnCode) { | |
| 582 | - try { | |
| 583 | - // 构建 CONNACK 消息 | |
| 584 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 585 | - MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 586 | - MqttConnAckVariableHeader variableHeader = new MqttConnAckVariableHeader(returnCode, false); | |
| 587 | - MqttConnAckMessage connAckMessage = new MqttConnAckMessage(fixedHeader, variableHeader); | |
| 588 | - | |
| 589 | - // 编码并发送 | |
| 590 | - sendMqttMessage(socket, connAckMessage); | |
| 591 | - log.debug("[sendConnAck][发送 CONNACK 消息,returnCode: {}]", returnCode); | |
| 592 | - } catch (Exception e) { | |
| 593 | - log.error("[sendConnAck][发送 CONNACK 消息失败]", e); | |
| 594 | - } | |
| 595 | - } | |
| 596 | - | |
| 597 | - /** | |
| 598 | - * 发送 PUBACK 消息(QoS 1 确认) | |
| 599 | - */ | |
| 600 | - private void sendPubAck(ServerWebSocket socket, int messageId) { | |
| 601 | - try { | |
| 602 | - // 构建 PUBACK 消息 | |
| 603 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 604 | - MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 605 | - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); | |
| 606 | - MqttMessage pubAckMessage = new MqttMessage(fixedHeader, variableHeader); | |
| 607 | - | |
| 608 | - // 编码并发送 | |
| 609 | - sendMqttMessage(socket, pubAckMessage); | |
| 610 | - log.debug("[sendPubAck][发送 PUBACK 消息,messageId: {}]", messageId); | |
| 611 | - } catch (Exception e) { | |
| 612 | - log.error("[sendPubAck][发送 PUBACK 消息失败,messageId: {}]", messageId, e); | |
| 613 | - } | |
| 614 | - } | |
| 615 | - | |
| 616 | - /** | |
| 617 | - * 发送 PUBREC 消息(QoS 2 第一步确认) | |
| 618 | - */ | |
| 619 | - private void sendPubRec(ServerWebSocket socket, int messageId) { | |
| 620 | - try { | |
| 621 | - // 构建 PUBREC 消息 | |
| 622 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 623 | - MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 624 | - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); | |
| 625 | - MqttMessage pubRecMessage = new MqttMessage(fixedHeader, variableHeader); | |
| 626 | - | |
| 627 | - // 编码并发送 | |
| 628 | - sendMqttMessage(socket, pubRecMessage); | |
| 629 | - log.debug("[sendPubRec][发送 PUBREC 消息,messageId: {}]", messageId); | |
| 630 | - } catch (Exception e) { | |
| 631 | - log.error("[sendPubRec][发送 PUBREC 消息失败,messageId: {}]", messageId, e); | |
| 632 | - } | |
| 633 | - } | |
| 634 | - | |
| 635 | - /** | |
| 636 | - * 发送 PUBREL 消息(QoS 2 第二步) | |
| 637 | - */ | |
| 638 | - private void sendPubRel(ServerWebSocket socket, int messageId) { | |
| 639 | - try { | |
| 640 | - // 构建 PUBREL 消息 | |
| 641 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 642 | - MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0); | |
| 643 | - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); | |
| 644 | - MqttMessage pubRelMessage = new MqttMessage(fixedHeader, variableHeader); | |
| 645 | - | |
| 646 | - // 编码并发送 | |
| 647 | - sendMqttMessage(socket, pubRelMessage); | |
| 648 | - log.debug("[sendPubRel][发送 PUBREL 消息,messageId: {}]", messageId); | |
| 649 | - } catch (Exception e) { | |
| 650 | - log.error("[sendPubRel][发送 PUBREL 消息失败,messageId: {}]", messageId, e); | |
| 651 | - } | |
| 652 | - } | |
| 653 | - | |
| 654 | - /** | |
| 655 | - * 发送 PUBCOMP 消息(QoS 2 完成确认) | |
| 656 | - */ | |
| 657 | - private void sendPubComp(ServerWebSocket socket, int messageId) { | |
| 658 | - try { | |
| 659 | - // 构建 PUBCOMP 消息 | |
| 660 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 661 | - MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 662 | - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); | |
| 663 | - MqttMessage pubCompMessage = new MqttMessage(fixedHeader, variableHeader); | |
| 664 | - | |
| 665 | - // 编码并发送 | |
| 666 | - sendMqttMessage(socket, pubCompMessage); | |
| 667 | - log.debug("[sendPubComp][发送 PUBCOMP 消息,messageId: {}]", messageId); | |
| 668 | - } catch (Exception e) { | |
| 669 | - log.error("[sendPubComp][发送 PUBCOMP 消息失败,messageId: {}]", messageId, e); | |
| 670 | - } | |
| 671 | - } | |
| 672 | - | |
| 673 | - /** | |
| 674 | - * 发送 SUBACK 消息 | |
| 675 | - */ | |
| 676 | - private void sendSubAck(ServerWebSocket socket, int messageId, int[] grantedQosList) { | |
| 677 | - try { | |
| 678 | - // 构建 SUBACK 消息 | |
| 679 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 680 | - MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 681 | - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); | |
| 682 | - MqttSubAckPayload payload = new MqttSubAckPayload(grantedQosList); | |
| 683 | - MqttSubAckMessage subAckMessage = new MqttSubAckMessage(fixedHeader, variableHeader, payload); | |
| 684 | - | |
| 685 | - // 编码并发送 | |
| 686 | - sendMqttMessage(socket, subAckMessage); | |
| 687 | - log.debug("[sendSubAck][发送 SUBACK 消息,messageId: {},主题数量: {}]", messageId, grantedQosList.length); | |
| 688 | - } catch (Exception e) { | |
| 689 | - log.error("[sendSubAck][发送 SUBACK 消息失败,messageId: {}]", messageId, e); | |
| 690 | - } | |
| 691 | - } | |
| 692 | - | |
| 693 | - /** | |
| 694 | - * 发送 UNSUBACK 消息 | |
| 695 | - */ | |
| 696 | - private void sendUnsubAck(ServerWebSocket socket, int messageId) { | |
| 697 | - try { | |
| 698 | - // 构建 UNSUBACK 消息 | |
| 699 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 700 | - MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 701 | - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); | |
| 702 | - MqttUnsubAckMessage unsubAckMessage = new MqttUnsubAckMessage(fixedHeader, variableHeader); | |
| 703 | - | |
| 704 | - // 编码并发送 | |
| 705 | - sendMqttMessage(socket, unsubAckMessage); | |
| 706 | - log.debug("[sendUnsubAck][发送 UNSUBACK 消息,messageId: {}]", messageId); | |
| 707 | - } catch (Exception e) { | |
| 708 | - log.error("[sendUnsubAck][发送 UNSUBACK 消息失败,messageId: {}]", messageId, e); | |
| 709 | - } | |
| 710 | - } | |
| 711 | - | |
| 712 | - /** | |
| 713 | - * 发送 PINGRESP 消息 | |
| 714 | - */ | |
| 715 | - private void sendPingResp(ServerWebSocket socket) { | |
| 716 | - try { | |
| 717 | - // 构建 PINGRESP 消息 | |
| 718 | - MqttFixedHeader fixedHeader = new MqttFixedHeader( | |
| 719 | - MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0); | |
| 720 | - MqttMessage pingRespMessage = new MqttMessage(fixedHeader); | |
| 721 | - | |
| 722 | - // 编码并发送 | |
| 723 | - sendMqttMessage(socket, pingRespMessage); | |
| 724 | - log.debug("[sendPingResp][发送 PINGRESP 消息]"); | |
| 725 | - } catch (Exception e) { | |
| 726 | - log.error("[sendPingResp][发送 PINGRESP 消息失败]", e); | |
| 727 | - } | |
| 728 | - } | |
| 729 | - | |
| 730 | - /** | |
| 731 | - * 发送 MQTT 消息到 WebSocket | |
| 732 | - */ | |
| 733 | - private void sendMqttMessage(ServerWebSocket socket, MqttMessage mqttMessage) { | |
| 734 | - ByteBuf byteBuf = null; | |
| 735 | - try { | |
| 736 | - // 使用 EmbeddedChannel 编码 MQTT 消息 | |
| 737 | - EmbeddedChannel encoderChannel = encoderChannelThreadLocal.get(); | |
| 738 | - encoderChannel.writeOutbound(mqttMessage); | |
| 739 | - | |
| 740 | - // 读取编码后的 ByteBuf | |
| 741 | - byteBuf = encoderChannel.readOutbound(); | |
| 742 | - if (byteBuf != null) { | |
| 743 | - byte[] bytes = new byte[byteBuf.readableBytes()]; | |
| 744 | - byteBuf.readBytes(bytes); | |
| 745 | - socket.writeBinaryMessage(Buffer.buffer(bytes)); | |
| 746 | - } | |
| 747 | - } finally { | |
| 748 | - if (byteBuf != null) { | |
| 749 | - byteBuf.release(); | |
| 750 | - } | |
| 751 | - } | |
| 752 | - } | |
| 753 | - | |
| 754 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| ... | ... | @@ -115,23 +115,10 @@ yudao: |
| 115 | 115 | connect-timeout-seconds: 60 |
| 116 | 116 | ssl-enabled: false |
| 117 | 117 | # ==================================== |
| 118 | - # 针对引入的 MQTT WebSocket 组件的配置 | |
| 119 | - # ==================================== | |
| 120 | - mqtt-ws: | |
| 121 | - enabled: false # 是否启用 MQTT WebSocket | |
| 122 | - port: 8083 # WebSocket 服务端口 | |
| 123 | - path: /mqtt # WebSocket 路径 | |
| 124 | - max-message-size: 8192 # 最大消息大小(字节) | |
| 125 | - max-frame-size: 65536 # 最大帧大小(字节) | |
| 126 | - connect-timeout-seconds: 60 # 连接超时时间(秒) | |
| 127 | - keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) | |
| 128 | - ssl-enabled: false # 是否启用 SSL(wss://) | |
| 129 | - sub-protocol: mqtt # WebSocket 子协议 | |
| 130 | - # ==================================== | |
| 131 | 118 | # 针对引入的 CoAP 组件的配置 |
| 132 | 119 | # ==================================== |
| 133 | 120 | coap: |
| 134 | - enabled: true # 是否启用 CoAP 协议 | |
| 121 | + enabled: false # 是否启用 CoAP 协议 | |
| 135 | 122 | port: 5683 # CoAP 服务端口(默认 5683) |
| 136 | 123 | max-message-size: 1024 # 最大消息大小(字节) |
| 137 | 124 | ack-timeout: 2000 # ACK 超时时间(毫秒) | ... | ... |