Commit a28a15295cdb60f340f7157a5bbd18a76fd4ccbb
1 parent
8e4b4cf2
feat(iot):【协议改造】coap 初步改造(100%)
Showing
30 changed files
with
955 additions
and
960 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; | |
| 7 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; |
| 8 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; |
| 9 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| ... | ... | @@ -11,12 +9,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr |
| 11 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 12 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 13 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; |
| 14 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; | |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; | |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; | |
| 17 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; | |
| 18 | 12 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; |
| 19 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 20 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 21 | 14 | import io.vertx.core.Vertx; |
| 22 | 15 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -111,63 +104,4 @@ public class IotGatewayConfiguration { |
| 111 | 104 | |
| 112 | 105 | } |
| 113 | 106 | |
| 114 | - /** | |
| 115 | - * IoT 网关 CoAP 协议配置类 | |
| 116 | - */ | |
| 117 | - @Configuration | |
| 118 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true") | |
| 119 | - @Slf4j | |
| 120 | - public static class CoapProtocolConfiguration { | |
| 121 | - | |
| 122 | - @Bean | |
| 123 | - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) { | |
| 124 | - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap()); | |
| 125 | - } | |
| 126 | - | |
| 127 | - @Bean | |
| 128 | - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol, | |
| 129 | - IotMessageBus messageBus) { | |
| 130 | - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus); | |
| 131 | - } | |
| 132 | - | |
| 133 | - } | |
| 134 | - | |
| 135 | - /** | |
| 136 | - * IoT 网关 WebSocket 协议配置类 | |
| 137 | - */ | |
| 138 | - @Configuration | |
| 139 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true") | |
| 140 | - @Slf4j | |
| 141 | - public static class WebSocketProtocolConfiguration { | |
| 142 | - | |
| 143 | - @Bean(name = "websocketVertx", destroyMethod = "close") | |
| 144 | - public Vertx websocketVertx() { | |
| 145 | - return Vertx.vertx(); | |
| 146 | - } | |
| 147 | - | |
| 148 | - @Bean | |
| 149 | - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 150 | - IotDeviceService deviceService, | |
| 151 | - IotDeviceMessageService messageService, | |
| 152 | - IotWebSocketConnectionManager connectionManager, | |
| 153 | - @Qualifier("websocketVertx") Vertx websocketVertx) { | |
| 154 | - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(), | |
| 155 | - deviceService, messageService, connectionManager, websocketVertx); | |
| 156 | - } | |
| 157 | - | |
| 158 | - @Bean | |
| 159 | - public IotWebSocketDownstreamHandler iotWebSocketDownstreamHandler(IotDeviceMessageService messageService, | |
| 160 | - IotWebSocketConnectionManager connectionManager) { | |
| 161 | - return new IotWebSocketDownstreamHandler(messageService, connectionManager); | |
| 162 | - } | |
| 163 | - | |
| 164 | - @Bean | |
| 165 | - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler, | |
| 166 | - IotWebSocketDownstreamHandler downstreamHandler, | |
| 167 | - IotMessageBus messageBus) { | |
| 168 | - return new IotWebSocketDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); | |
| 169 | - } | |
| 170 | - | |
| 171 | - } | |
| 172 | - | |
| 173 | 107 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; | |
| 4 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; | |
| 7 | 9 | import io.vertx.core.net.KeyCertOptions; |
| 8 | 10 | import io.vertx.core.net.TrustOptions; |
| 9 | 11 | import jakarta.validation.Valid; |
| ... | ... | @@ -90,16 +92,6 @@ public class IotGatewayProperties { |
| 90 | 92 | */ |
| 91 | 93 | private MqttProperties mqtt; |
| 92 | 94 | |
| 93 | - /** | |
| 94 | - * CoAP 组件配置 | |
| 95 | - */ | |
| 96 | - private CoapProperties coap; | |
| 97 | - | |
| 98 | - /** | |
| 99 | - * WebSocket 组件配置 | |
| 100 | - */ | |
| 101 | - private WebSocketProperties websocket; | |
| 102 | - | |
| 103 | 95 | } |
| 104 | 96 | |
| 105 | 97 | @Data |
| ... | ... | @@ -344,93 +336,6 @@ public class IotGatewayProperties { |
| 344 | 336 | |
| 345 | 337 | } |
| 346 | 338 | |
| 347 | - @Data | |
| 348 | - public static class CoapProperties { | |
| 349 | - | |
| 350 | - /** | |
| 351 | - * 是否开启 | |
| 352 | - */ | |
| 353 | - @NotNull(message = "是否开启不能为空") | |
| 354 | - private Boolean enabled; | |
| 355 | - | |
| 356 | - /** | |
| 357 | - * 服务端口(CoAP 默认端口 5683) | |
| 358 | - */ | |
| 359 | - @NotNull(message = "服务端口不能为空") | |
| 360 | - private Integer port = 5683; | |
| 361 | - | |
| 362 | - /** | |
| 363 | - * 最大消息大小(字节) | |
| 364 | - */ | |
| 365 | - @NotNull(message = "最大消息大小不能为空") | |
| 366 | - private Integer maxMessageSize = 1024; | |
| 367 | - | |
| 368 | - /** | |
| 369 | - * ACK 超时时间(毫秒) | |
| 370 | - */ | |
| 371 | - @NotNull(message = "ACK 超时时间不能为空") | |
| 372 | - private Integer ackTimeout = 2000; | |
| 373 | - | |
| 374 | - /** | |
| 375 | - * 最大重传次数 | |
| 376 | - */ | |
| 377 | - @NotNull(message = "最大重传次数不能为空") | |
| 378 | - private Integer maxRetransmit = 4; | |
| 379 | - | |
| 380 | - } | |
| 381 | - | |
| 382 | - @Data | |
| 383 | - public static class WebSocketProperties { | |
| 384 | - | |
| 385 | - /** | |
| 386 | - * 是否开启 | |
| 387 | - */ | |
| 388 | - @NotNull(message = "是否开启不能为空") | |
| 389 | - private Boolean enabled; | |
| 390 | - | |
| 391 | - /** | |
| 392 | - * 服务器端口(默认:8094) | |
| 393 | - */ | |
| 394 | - private Integer port = 8094; | |
| 395 | - | |
| 396 | - /** | |
| 397 | - * WebSocket 路径(默认:/ws) | |
| 398 | - */ | |
| 399 | - @NotEmpty(message = "WebSocket 路径不能为空") | |
| 400 | - private String path = "/ws"; | |
| 401 | - | |
| 402 | - /** | |
| 403 | - * 最大消息大小(字节,默认 64KB) | |
| 404 | - */ | |
| 405 | - private Integer maxMessageSize = 65536; | |
| 406 | - | |
| 407 | - /** | |
| 408 | - * 最大帧大小(字节,默认 64KB) | |
| 409 | - */ | |
| 410 | - private Integer maxFrameSize = 65536; | |
| 411 | - | |
| 412 | - /** | |
| 413 | - * 空闲超时时间(秒,默认 60) | |
| 414 | - */ | |
| 415 | - private Integer idleTimeoutSeconds = 60; | |
| 416 | - | |
| 417 | - /** | |
| 418 | - * 是否启用 SSL(wss://) | |
| 419 | - */ | |
| 420 | - private Boolean sslEnabled = false; | |
| 421 | - | |
| 422 | - /** | |
| 423 | - * SSL 证书路径 | |
| 424 | - */ | |
| 425 | - private String sslCertPath; | |
| 426 | - | |
| 427 | - /** | |
| 428 | - * SSL 私钥路径 | |
| 429 | - */ | |
| 430 | - private String sslKeyPath; | |
| 431 | - | |
| 432 | - } | |
| 433 | - | |
| 434 | 339 | // TODO @AI:【暂时忽略】改成 ProtocolProperties |
| 435 | 340 | /** |
| 436 | 341 | * 协议实例配置 |
| ... | ... | @@ -489,6 +394,18 @@ public class IotGatewayProperties { |
| 489 | 394 | @Valid |
| 490 | 395 | private IotUdpConfig udp; |
| 491 | 396 | |
| 397 | + /** | |
| 398 | + * CoAP 协议配置 | |
| 399 | + */ | |
| 400 | + @Valid | |
| 401 | + private IotCoapConfig coap; | |
| 402 | + | |
| 403 | + /** | |
| 404 | + * WebSocket 协议配置 | |
| 405 | + */ | |
| 406 | + @Valid | |
| 407 | + private IotWebSocketConfig websocket; | |
| 408 | + | |
| 492 | 409 | } |
| 493 | 410 | |
| 494 | 411 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
| ... | ... | @@ -4,9 +4,11 @@ import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.BooleanUtil; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | |
| 7 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 8 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 9 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; | |
| 10 | 12 | import lombok.Getter; |
| 11 | 13 | import lombok.extern.slf4j.Slf4j; |
| 12 | 14 | import org.springframework.context.SmartLifecycle; |
| ... | ... | @@ -100,6 +102,10 @@ public class IotProtocolManager implements SmartLifecycle { |
| 100 | 102 | return createTcpProtocol(config); |
| 101 | 103 | case UDP: |
| 102 | 104 | return createUdpProtocol(config); |
| 105 | + case COAP: | |
| 106 | + return createCoapProtocol(config); | |
| 107 | + case WEBSOCKET: | |
| 108 | + return createWebSocketProtocol(config); | |
| 103 | 109 | default: |
| 104 | 110 | throw new IllegalArgumentException(String.format( |
| 105 | 111 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| ... | ... | @@ -136,4 +142,24 @@ public class IotProtocolManager implements SmartLifecycle { |
| 136 | 142 | return new IotUdpProtocol(config); |
| 137 | 143 | } |
| 138 | 144 | |
| 145 | + /** | |
| 146 | + * 创建 CoAP 协议实例 | |
| 147 | + * | |
| 148 | + * @param config 协议实例配置 | |
| 149 | + * @return CoAP 协议实例 | |
| 150 | + */ | |
| 151 | + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 152 | + return new IotCoapProtocol(config); | |
| 153 | + } | |
| 154 | + | |
| 155 | + /** | |
| 156 | + * 创建 WebSocket 协议实例 | |
| 157 | + * | |
| 158 | + * @param config 协议实例配置 | |
| 159 | + * @return WebSocket 协议实例 | |
| 160 | + */ | |
| 161 | + private IotWebSocketUpstreamProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 162 | + return new IotWebSocketUpstreamProtocol(config); | |
| 163 | + } | |
| 164 | + | |
| 139 | 165 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.Min; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT CoAP 协议配置 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +public class IotCoapConfig { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 最大消息大小(字节) | |
| 17 | + */ | |
| 18 | + @NotNull(message = "最大消息大小不能为空") | |
| 19 | + @Min(value = 64, message = "最大消息大小必须大于 64 字节") | |
| 20 | + private Integer maxMessageSize = 1024; | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * ACK 超时时间(毫秒) | |
| 24 | + */ | |
| 25 | + @NotNull(message = "ACK 超时时间不能为空") | |
| 26 | + @Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒") | |
| 27 | + private Integer ackTimeoutMs = 2000; | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 最大重传次数 | |
| 31 | + */ | |
| 32 | + @NotNull(message = "最大重传次数不能为空") | |
| 33 | + @Min(value = 0, message = "最大重传次数必须大于等于 0") | |
| 34 | + private Integer maxRetransmit = 4; | |
| 35 | + | |
| 36 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; | |
| 2 | + | |
| 3 | +import cn.hutool.extra.spring.SpringUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthHandler; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthResource; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterHandler; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterResource; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubHandler; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubResource; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamHandler; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamTopicResource; | |
| 18 | +import lombok.Getter; | |
| 19 | +import lombok.extern.slf4j.Slf4j; | |
| 20 | +import org.eclipse.californium.core.CoapResource; | |
| 21 | +import org.eclipse.californium.core.CoapServer; | |
| 22 | +import org.eclipse.californium.core.config.CoapConfig; | |
| 23 | +import org.eclipse.californium.elements.config.Configuration; | |
| 24 | +import org.springframework.util.Assert; | |
| 25 | + | |
| 26 | +import java.util.concurrent.TimeUnit; | |
| 27 | + | |
| 28 | +/** | |
| 29 | + * IoT CoAP 协议实现 | |
| 30 | + * <p> | |
| 31 | + * 基于 Eclipse Californium 实现,支持: | |
| 32 | + * 1. 认证:POST /auth | |
| 33 | + * 2. 设备动态注册:POST /auth/register/device | |
| 34 | + * 3. 子设备动态注册:POST /auth/register/sub-device/{productKey}/{deviceName} | |
| 35 | + * 4. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post | |
| 36 | + * 5. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post | |
| 37 | + * | |
| 38 | + * @author 芋道源码 | |
| 39 | + */ | |
| 40 | +@Slf4j | |
| 41 | +public class IotCoapProtocol implements IotProtocol { | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 协议配置 | |
| 45 | + */ | |
| 46 | + private final ProtocolInstanceProperties properties; | |
| 47 | + /** | |
| 48 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 49 | + */ | |
| 50 | + @Getter | |
| 51 | + private final String serverId; | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 运行状态 | |
| 55 | + */ | |
| 56 | + @Getter | |
| 57 | + private volatile boolean running = false; | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * CoAP 服务器 | |
| 61 | + */ | |
| 62 | + private CoapServer coapServer; | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * 下行消息订阅者 | |
| 66 | + */ | |
| 67 | + private final IotCoapDownstreamSubscriber downstreamSubscriber; | |
| 68 | + | |
| 69 | + public IotCoapProtocol(ProtocolInstanceProperties properties) { | |
| 70 | + IotCoapConfig coapConfig = properties.getCoap(); | |
| 71 | + Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); | |
| 72 | + this.properties = properties; | |
| 73 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 74 | + | |
| 75 | + // 初始化下行消息订阅者 | |
| 76 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 77 | + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); | |
| 78 | + } | |
| 79 | + | |
| 80 | + @Override | |
| 81 | + public String getId() { | |
| 82 | + return properties.getId(); | |
| 83 | + } | |
| 84 | + | |
| 85 | + @Override | |
| 86 | + public IotProtocolTypeEnum getType() { | |
| 87 | + return IotProtocolTypeEnum.COAP; | |
| 88 | + } | |
| 89 | + | |
| 90 | + @Override | |
| 91 | + public void start() { | |
| 92 | + if (running) { | |
| 93 | + log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId()); | |
| 94 | + return; | |
| 95 | + } | |
| 96 | + | |
| 97 | + IotCoapConfig coapConfig = properties.getCoap(); | |
| 98 | + try { | |
| 99 | + // 1.1 创建 CoAP 配置 | |
| 100 | + Configuration config = Configuration.createStandardWithoutFile(); | |
| 101 | + config.set(CoapConfig.COAP_PORT, properties.getPort()); | |
| 102 | + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); | |
| 103 | + config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS); | |
| 104 | + config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit()); | |
| 105 | + // 1.2 创建 CoAP 服务器 | |
| 106 | + coapServer = new CoapServer(config); | |
| 107 | + | |
| 108 | + // 2.1 添加 /auth 认证资源 | |
| 109 | + IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId); | |
| 110 | + IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler); | |
| 111 | + coapServer.add(authResource); | |
| 112 | + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) | |
| 113 | + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); | |
| 114 | + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); | |
| 115 | + // 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源 | |
| 116 | + IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler(); | |
| 117 | + IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler); | |
| 118 | + authResource.add(new CoapResource("register") {{ | |
| 119 | + add(registerResource); | |
| 120 | + add(registerSubResource); | |
| 121 | + }}); | |
| 122 | + // 2.4 添加 /topic 根资源(用于上行消息) | |
| 123 | + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId); | |
| 124 | + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler); | |
| 125 | + coapServer.add(topicResource); | |
| 126 | + | |
| 127 | + // 3. 启动服务器 | |
| 128 | + coapServer.start(); | |
| 129 | + running = true; | |
| 130 | + log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{},serverId:{}]", | |
| 131 | + getId(), properties.getPort(), serverId); | |
| 132 | + | |
| 133 | + // 4. 启动下行消息订阅者 | |
| 134 | + this.downstreamSubscriber.start(); | |
| 135 | + } catch (Exception e) { | |
| 136 | + log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); | |
| 137 | + if (coapServer != null) { | |
| 138 | + coapServer.destroy(); | |
| 139 | + coapServer = null; | |
| 140 | + } | |
| 141 | + throw e; | |
| 142 | + } | |
| 143 | + } | |
| 144 | + | |
| 145 | + @Override | |
| 146 | + public void stop() { | |
| 147 | + if (!running) { | |
| 148 | + return; | |
| 149 | + } | |
| 150 | + // 1. 停止下行消息订阅者 | |
| 151 | + try { | |
| 152 | + downstreamSubscriber.stop(); | |
| 153 | + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 154 | + } catch (Exception e) { | |
| 155 | + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 156 | + } | |
| 157 | + | |
| 158 | + // 2. 关闭 CoAP 服务器 | |
| 159 | + if (coapServer != null) { | |
| 160 | + try { | |
| 161 | + coapServer.stop(); | |
| 162 | + coapServer.destroy(); | |
| 163 | + coapServer = null; | |
| 164 | + log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId()); | |
| 165 | + } catch (Exception e) { | |
| 166 | + log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e); | |
| 167 | + } | |
| 168 | + } | |
| 169 | + running = false; | |
| 170 | + log.info("[stop][IoT CoAP 协议 {} 已停止]", getId()); | |
| 171 | + } | |
| 172 | + | |
| 173 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler; | |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource; | |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource; | |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler; | |
| 13 | -import jakarta.annotation.PostConstruct; | |
| 14 | -import jakarta.annotation.PreDestroy; | |
| 15 | -import lombok.Getter; | |
| 16 | -import lombok.extern.slf4j.Slf4j; | |
| 17 | -import org.eclipse.californium.core.CoapResource; | |
| 18 | -import org.eclipse.californium.core.CoapServer; | |
| 19 | -import org.eclipse.californium.core.config.CoapConfig; | |
| 20 | -import org.eclipse.californium.elements.config.Configuration; | |
| 21 | - | |
| 22 | -import java.util.concurrent.TimeUnit; | |
| 23 | - | |
| 24 | -/** | |
| 25 | - * IoT 网关 CoAP 协议:接收设备上行消息 | |
| 26 | - * | |
| 27 | - * 基于 Eclipse Californium 实现,支持: | |
| 28 | - * 1. 认证:POST /auth | |
| 29 | - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post | |
| 30 | - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post | |
| 31 | - * | |
| 32 | - * @author 芋道源码 | |
| 33 | - */ | |
| 34 | -@Slf4j | |
| 35 | -public class IotCoapUpstreamProtocol implements IotProtocol { | |
| 36 | - | |
| 37 | - private static final String ID = "coap"; | |
| 38 | - | |
| 39 | - private final IotGatewayProperties.CoapProperties coapProperties; | |
| 40 | - | |
| 41 | - private CoapServer coapServer; | |
| 42 | - | |
| 43 | - @Getter | |
| 44 | - private final String serverId; | |
| 45 | - | |
| 46 | - private volatile boolean running = false; | |
| 47 | - | |
| 48 | - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { | |
| 49 | - this.coapProperties = coapProperties; | |
| 50 | - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); | |
| 51 | - } | |
| 52 | - | |
| 53 | - @Override | |
| 54 | - public String getId() { | |
| 55 | - return ID; | |
| 56 | - } | |
| 57 | - | |
| 58 | - @Override | |
| 59 | - public IotProtocolTypeEnum getType() { | |
| 60 | - return IotProtocolTypeEnum.COAP; | |
| 61 | - } | |
| 62 | - | |
| 63 | - @Override | |
| 64 | - @PostConstruct | |
| 65 | - public void start() { | |
| 66 | - try { | |
| 67 | - // 1.1 创建网络配置(Californium 3.x API) | |
| 68 | - Configuration config = Configuration.createStandardWithoutFile(); | |
| 69 | - config.set(CoapConfig.COAP_PORT, coapProperties.getPort()); | |
| 70 | - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize()); | |
| 71 | - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS); | |
| 72 | - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit()); | |
| 73 | - // 1.2 创建 CoAP 服务器 | |
| 74 | - coapServer = new CoapServer(config); | |
| 75 | - | |
| 76 | - // 2.1 添加 /auth 认证资源 | |
| 77 | - IotCoapAuthHandler authHandler = new IotCoapAuthHandler(); | |
| 78 | - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); | |
| 79 | - coapServer.add(authResource); | |
| 80 | - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) | |
| 81 | - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); | |
| 82 | - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); | |
| 83 | - authResource.add(new CoapResource("register") {{ | |
| 84 | - add(registerResource); | |
| 85 | - }}); | |
| 86 | - // 2.3 添加 /topic 根资源(用于上行消息) | |
| 87 | - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(); | |
| 88 | - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); | |
| 89 | - coapServer.add(topicResource); | |
| 90 | - | |
| 91 | - // 3. 启动服务器 | |
| 92 | - coapServer.start(); | |
| 93 | - running = true; | |
| 94 | - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); | |
| 95 | - } catch (Exception e) { | |
| 96 | - log.error("[start][IoT 网关 CoAP 协议启动失败]", e); | |
| 97 | - throw e; | |
| 98 | - } | |
| 99 | - } | |
| 100 | - | |
| 101 | - @Override | |
| 102 | - @PreDestroy | |
| 103 | - public void stop() { | |
| 104 | - if (coapServer != null) { | |
| 105 | - try { | |
| 106 | - coapServer.stop(); | |
| 107 | - running = false; | |
| 108 | - log.info("[stop][IoT 网关 CoAP 协议已停止]"); | |
| 109 | - } catch (Exception e) { | |
| 110 | - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); | |
| 111 | - } | |
| 112 | - } | |
| 113 | - } | |
| 114 | - | |
| 115 | - @Override | |
| 116 | - public boolean isRunning() { | |
| 117 | - return running; | |
| 118 | - } | |
| 119 | - | |
| 120 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | |
| 6 | 7 | import lombok.extern.slf4j.Slf4j; |
| 7 | 8 | |
| 8 | 9 | /** |
| ... | ... | @@ -13,7 +14,7 @@ import lombok.extern.slf4j.Slf4j; |
| 13 | 14 | @Slf4j |
| 14 | 15 | public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { |
| 15 | 16 | |
| 16 | - public IotCoapDownstreamSubscriber(IotCoapUpstreamProtocol protocol, IotMessageBus messageBus) { | |
| 17 | + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { | |
| 17 | 18 | super(protocol, messageBus); |
| 18 | 19 | } |
| 19 | 20 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 5 | +import cn.hutool.core.util.ObjUtil; | |
| 6 | +import cn.hutool.core.util.StrUtil; | |
| 7 | +import cn.hutool.extra.spring.SpringUtil; | |
| 8 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 9 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 10 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | +import org.eclipse.californium.core.coap.CoAP; | |
| 15 | +import org.eclipse.californium.core.coap.MediaTypeRegistry; | |
| 16 | +import org.eclipse.californium.core.coap.Option; | |
| 17 | +import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 18 | + | |
| 19 | +import java.util.List; | |
| 20 | + | |
| 21 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | |
| 22 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 23 | + | |
| 24 | +/** | |
| 25 | + * IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等 | |
| 26 | + * | |
| 27 | + * @author 芋道源码 | |
| 28 | + */ | |
| 29 | +@Slf4j | |
| 30 | +public abstract class IotCoapAbstractHandler { | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 自定义 CoAP Option 编号,用于携带 Token | |
| 34 | + * <p> | |
| 35 | + * CoAP Option 范围 2048-65535 属于实验/自定义范围 | |
| 36 | + */ | |
| 37 | + public static final int OPTION_TOKEN = 2088; | |
| 38 | + | |
| 39 | + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 处理 CoAP 请求(模板方法) | |
| 43 | + * | |
| 44 | + * @param exchange CoAP 交换对象 | |
| 45 | + */ | |
| 46 | + public final void handle(CoapExchange exchange) { | |
| 47 | + try { | |
| 48 | + // 1. 前置处理 | |
| 49 | + beforeHandle(exchange); | |
| 50 | + | |
| 51 | + // 2. 执行业务逻辑 | |
| 52 | + CommonResult<Object> result = handle0(exchange); | |
| 53 | + writeResponse(exchange, result); | |
| 54 | + } catch (ServiceException e) { | |
| 55 | + // 业务异常,返回对应的错误码和消息 | |
| 56 | + writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage())); | |
| 57 | + } catch (IllegalArgumentException e) { | |
| 58 | + // 参数校验异常(hutool Assert 抛出),返回 BAD_REQUEST | |
| 59 | + writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); | |
| 60 | + } catch (Exception e) { | |
| 61 | + // 其他未知异常,返回 INTERNAL_SERVER_ERROR | |
| 62 | + log.error("[handle][CoAP 请求处理异常]", e); | |
| 63 | + writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR)); | |
| 64 | + } | |
| 65 | + } | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 处理 CoAP 请求(子类实现) | |
| 69 | + * | |
| 70 | + * @param exchange CoAP 交换对象 | |
| 71 | + * @return 处理结果 | |
| 72 | + */ | |
| 73 | + protected abstract CommonResult<Object> handle0(CoapExchange exchange); | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * 前置处理:认证等 | |
| 77 | + * | |
| 78 | + * @param exchange CoAP 交换对象 | |
| 79 | + */ | |
| 80 | + private void beforeHandle(CoapExchange exchange) { | |
| 81 | + // 1.1 如果不需要认证,则不走前置处理 | |
| 82 | + if (!requiresAuthentication()) { | |
| 83 | + return; | |
| 84 | + } | |
| 85 | + // 1.2 从自定义 Option 获取 token | |
| 86 | + String token = getTokenFromOption(exchange); | |
| 87 | + if (StrUtil.isEmpty(token)) { | |
| 88 | + throw exception(UNAUTHORIZED); | |
| 89 | + } | |
| 90 | + // 1.3 校验 token | |
| 91 | + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); | |
| 92 | + if (deviceInfo == null) { | |
| 93 | + throw exception(UNAUTHORIZED); | |
| 94 | + } | |
| 95 | + | |
| 96 | + // 2.1 解析 productKey 和 deviceName | |
| 97 | + List<String> uriPath = exchange.getRequestOptions().getUriPath(); | |
| 98 | + String productKey = getProductKey(uriPath); | |
| 99 | + String deviceName = getDeviceName(uriPath); | |
| 100 | + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) { | |
| 101 | + throw exception(BAD_REQUEST); | |
| 102 | + } | |
| 103 | + // 2.2 校验设备信息是否匹配 | |
| 104 | + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) | |
| 105 | + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { | |
| 106 | + throw exception(FORBIDDEN); | |
| 107 | + } | |
| 108 | + } | |
| 109 | + | |
| 110 | + // ========== Token 相关方法 ========== | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 是否需要认证(子类可覆盖) | |
| 114 | + * <p> | |
| 115 | + * 默认不需要认证 | |
| 116 | + * | |
| 117 | + * @return 是否需要认证 | |
| 118 | + */ | |
| 119 | + protected boolean requiresAuthentication() { | |
| 120 | + return false; | |
| 121 | + } | |
| 122 | + | |
| 123 | + /** | |
| 124 | + * 从 URI 路径中获取 productKey(子类实现) | |
| 125 | + * <p> | |
| 126 | + * 默认抛出异常,需要认证的子类必须实现此方法 | |
| 127 | + * | |
| 128 | + * @param uriPath URI 路径 | |
| 129 | + * @return productKey | |
| 130 | + */ | |
| 131 | + protected String getProductKey(List<String> uriPath) { | |
| 132 | + throw new UnsupportedOperationException("子类需要实现 getProductKey 方法"); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 从 URI 路径中获取 deviceName(子类实现) | |
| 137 | + * <p> | |
| 138 | + * 默认抛出异常,需要认证的子类必须实现此方法 | |
| 139 | + * | |
| 140 | + * @param uriPath URI 路径 | |
| 141 | + * @return deviceName | |
| 142 | + */ | |
| 143 | + protected String getDeviceName(List<String> uriPath) { | |
| 144 | + throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法"); | |
| 145 | + } | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * 从自定义 CoAP Option 中获取 Token | |
| 149 | + * | |
| 150 | + * @param exchange CoAP 交换对象 | |
| 151 | + * @return Token 值,如果不存在则返回 null | |
| 152 | + */ | |
| 153 | + protected String getTokenFromOption(CoapExchange exchange) { | |
| 154 | + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), | |
| 155 | + o -> o.getNumber() == OPTION_TOKEN); | |
| 156 | + return option != null ? new String(option.getValue()) : null; | |
| 157 | + } | |
| 158 | + | |
| 159 | + // ========== 序列化相关方法 ========== | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * 解析请求体为指定类型 | |
| 163 | + * | |
| 164 | + * @param exchange CoAP 交换对象 | |
| 165 | + * @param clazz 目标类型 | |
| 166 | + * @param <T> 目标类型泛型 | |
| 167 | + * @return 解析后的对象,解析失败返回 null | |
| 168 | + */ | |
| 169 | + protected <T> T deserializeRequest(CoapExchange exchange, Class<T> clazz) { | |
| 170 | + byte[] payload = exchange.getRequestPayload(); | |
| 171 | + if (ArrayUtil.isEmpty(payload)) { | |
| 172 | + return null; | |
| 173 | + } | |
| 174 | + return JsonUtils.parseObject(payload, clazz); | |
| 175 | + } | |
| 176 | + | |
| 177 | + private static String serializeResponse(Object data) { | |
| 178 | + return JsonUtils.toJsonString(data); | |
| 179 | + } | |
| 180 | + | |
| 181 | + protected void writeResponse(CoapExchange exchange, CommonResult<?> data) { | |
| 182 | + String json = serializeResponse(data); | |
| 183 | + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); | |
| 184 | + } | |
| 185 | + | |
| 186 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.map.MapUtil; | |
| 5 | +import cn.hutool.core.util.BooleanUtil; | |
| 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.mq.message.IotDeviceMessage; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 14 | +import lombok.extern.slf4j.Slf4j; | |
| 15 | +import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 16 | + | |
| 17 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 18 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * IoT 网关 CoAP 协议的【认证】处理器 | |
| 22 | + * | |
| 23 | + * @author 芋道源码 | |
| 24 | + */ | |
| 25 | +@Slf4j | |
| 26 | +public class IotCoapAuthHandler extends IotCoapAbstractHandler { | |
| 27 | + | |
| 28 | + private final String serverId; | |
| 29 | + | |
| 30 | + private final IotDeviceTokenService deviceTokenService; | |
| 31 | + private final IotDeviceCommonApi deviceApi; | |
| 32 | + private final IotDeviceMessageService deviceMessageService; | |
| 33 | + | |
| 34 | + public IotCoapAuthHandler(String serverId) { | |
| 35 | + this.serverId = serverId; | |
| 36 | + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 37 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 38 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 39 | + } | |
| 40 | + | |
| 41 | + @Override | |
| 42 | + @SuppressWarnings("DuplicatedCode") | |
| 43 | + protected CommonResult<Object> handle0(CoapExchange exchange) { | |
| 44 | + // 1. 解析参数 | |
| 45 | + IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class); | |
| 46 | + Assert.notNull(request, "请求体不能为空"); | |
| 47 | + Assert.notBlank(request.getClientId(), "clientId 不能为空"); | |
| 48 | + Assert.notBlank(request.getUsername(), "username 不能为空"); | |
| 49 | + Assert.notBlank(request.getPassword(), "password 不能为空"); | |
| 50 | + | |
| 51 | + // 2.1 执行认证 | |
| 52 | + CommonResult<Boolean> result = deviceApi.authDevice(request); | |
| 53 | + result.checkError(); | |
| 54 | + if (BooleanUtil.isFalse(result.getData())) { | |
| 55 | + throw exception(DEVICE_AUTH_FAIL); | |
| 56 | + } | |
| 57 | + // 2.2 生成 Token | |
| 58 | + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); | |
| 59 | + Assert.notNull(deviceInfo, "设备信息不能为空"); | |
| 60 | + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 61 | + Assert.notBlank(token, "生成 token 不能为空"); | |
| 62 | + | |
| 63 | + // 3. 执行上线 | |
| 64 | + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); | |
| 65 | + deviceMessageService.sendDeviceMessage(message, | |
| 66 | + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); | |
| 67 | + | |
| 68 | + // 4. 构建响应数据 | |
| 69 | + return CommonResult.success(MapUtil.of("token", token)); | |
| 70 | + } | |
| 71 | + | |
| 72 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; | |
| 4 | 3 | import lombok.extern.slf4j.Slf4j; |
| 5 | 4 | import org.eclipse.californium.core.CoapResource; |
| 6 | 5 | import org.eclipse.californium.core.server.resources.CoapExchange; |
| ... | ... | @@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource { |
| 17 | 16 | |
| 18 | 17 | public static final String PATH = "auth"; |
| 19 | 18 | |
| 20 | - private final IotCoapUpstreamProtocol protocol; | |
| 21 | 19 | private final IotCoapAuthHandler authHandler; |
| 22 | 20 | |
| 23 | - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol, | |
| 24 | - IotCoapAuthHandler authHandler) { | |
| 21 | + public IotCoapAuthResource(IotCoapAuthHandler authHandler) { | |
| 25 | 22 | super(PATH); |
| 26 | - this.protocol = protocol; | |
| 27 | 23 | this.authHandler = authHandler; |
| 28 | 24 | log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH); |
| 29 | 25 | } |
| ... | ... | @@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource { |
| 31 | 27 | @Override |
| 32 | 28 | public void handlePOST(CoapExchange exchange) { |
| 33 | 29 | log.debug("[handlePOST][收到 /auth POST 请求]"); |
| 34 | - authHandler.handle(exchange, protocol); | |
| 30 | + authHandler.handle(exchange); | |
| 35 | 31 | } |
| 36 | 32 | |
| 37 | 33 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.extra.spring.SpringUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | +import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * IoT 网关 CoAP 协议的【设备动态注册】处理器 | |
| 14 | + * <p> | |
| 15 | + * 用于直连设备/网关的一型一密动态注册,不需要认证 | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 19 | + */ | |
| 20 | +@Slf4j | |
| 21 | +public class IotCoapRegisterHandler extends IotCoapAbstractHandler { | |
| 22 | + | |
| 23 | + private final IotDeviceCommonApi deviceApi; | |
| 24 | + | |
| 25 | + public IotCoapRegisterHandler() { | |
| 26 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 27 | + } | |
| 28 | + | |
| 29 | + @Override | |
| 30 | + protected CommonResult<Object> handle0(CoapExchange exchange) { | |
| 31 | + // 1. 解析参数 | |
| 32 | + IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class); | |
| 33 | + Assert.notNull(request, "请求体不能为空"); | |
| 34 | + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); | |
| 35 | + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); | |
| 36 | + Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); | |
| 37 | + | |
| 38 | + // 2. 调用动态注册 | |
| 39 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); | |
| 40 | + result.checkError(); | |
| 41 | + | |
| 42 | + // 3. 构建响应数据 | |
| 43 | + return CommonResult.success(result.getData()); | |
| 44 | + } | |
| 45 | + | |
| 46 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.hutool.extra.spring.SpringUtil; | |
| 6 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; | |
| 11 | +import lombok.Data; | |
| 12 | +import lombok.extern.slf4j.Slf4j; | |
| 13 | +import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 14 | + | |
| 15 | +import java.util.List; | |
| 16 | + | |
| 17 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * IoT 网关 CoAP 协议的【子设备动态注册】处理器 | |
| 21 | + * <p> | |
| 22 | + * 用于子设备的动态注册,需要网关认证 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a> | |
| 26 | + */ | |
| 27 | +@Slf4j | |
| 28 | +public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler { | |
| 29 | + | |
| 30 | + private final IotDeviceCommonApi deviceApi; | |
| 31 | + | |
| 32 | + public IotCoapRegisterSubHandler() { | |
| 33 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 34 | + } | |
| 35 | + | |
| 36 | + @Override | |
| 37 | + @SuppressWarnings("DuplicatedCode") | |
| 38 | + protected CommonResult<Object> handle0(CoapExchange exchange) { | |
| 39 | + // 1.1 解析通用参数(从 URI 路径获取网关设备信息) | |
| 40 | + List<String> uriPath = exchange.getRequestOptions().getUriPath(); | |
| 41 | + String productKey = getProductKey(uriPath); | |
| 42 | + String deviceName = getDeviceName(uriPath); | |
| 43 | + // 1.2 解析子设备列表 | |
| 44 | + SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class); | |
| 45 | + Assert.notNull(request, "请求参数不能为空"); | |
| 46 | + Assert.notEmpty(request.getParams(), "params 不能为空"); | |
| 47 | + | |
| 48 | + // 2. 调用子设备动态注册 | |
| 49 | + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() | |
| 50 | + .setGatewayProductKey(productKey) | |
| 51 | + .setGatewayDeviceName(deviceName) | |
| 52 | + .setSubDevices(request.getParams()); | |
| 53 | + CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO); | |
| 54 | + result.checkError(); | |
| 55 | + | |
| 56 | + // 3. 返回结果 | |
| 57 | + return success(result.getData()); | |
| 58 | + } | |
| 59 | + | |
| 60 | + @Override | |
| 61 | + protected boolean requiresAuthentication() { | |
| 62 | + return true; | |
| 63 | + } | |
| 64 | + | |
| 65 | + @Override | |
| 66 | + protected String getProductKey(List<String> uriPath) { | |
| 67 | + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} | |
| 68 | + return CollUtil.get(uriPath, 3); | |
| 69 | + } | |
| 70 | + | |
| 71 | + @Override | |
| 72 | + protected String getDeviceName(List<String> uriPath) { | |
| 73 | + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} | |
| 74 | + return CollUtil.get(uriPath, 4); | |
| 75 | + } | |
| 76 | + | |
| 77 | + @Data | |
| 78 | + public static class SubDeviceRegisterRequest { | |
| 79 | + | |
| 80 | + private List<IotSubDeviceRegisterReqDTO> params; | |
| 81 | + | |
| 82 | + } | |
| 83 | + | |
| 84 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | + | |
| 3 | +import lombok.extern.slf4j.Slf4j; | |
| 4 | +import org.eclipse.californium.core.CoapResource; | |
| 5 | +import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 6 | +import org.eclipse.californium.core.server.resources.Resource; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName}) | |
| 10 | + * <p> | |
| 11 | + * 用于子设备的动态注册,需要网关认证 | |
| 12 | + * <p> | |
| 13 | + * 支持动态路径匹配:productKey 和 deviceName 是网关设备的标识 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@Slf4j | |
| 18 | +public class IotCoapRegisterSubResource extends CoapResource { | |
| 19 | + | |
| 20 | + public static final String PATH = "sub-device"; | |
| 21 | + | |
| 22 | + private final IotCoapRegisterSubHandler registerSubHandler; | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * 创建根资源(/auth/register/sub-device) | |
| 26 | + */ | |
| 27 | + public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) { | |
| 28 | + this(PATH, registerSubHandler); | |
| 29 | + log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH); | |
| 30 | + } | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 创建子资源(动态路径) | |
| 34 | + */ | |
| 35 | + private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) { | |
| 36 | + super(name); | |
| 37 | + this.registerSubHandler = registerSubHandler; | |
| 38 | + } | |
| 39 | + | |
| 40 | + @Override | |
| 41 | + public Resource getChild(String name) { | |
| 42 | + // 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径 | |
| 43 | + return new IotCoapRegisterSubResource(name, registerSubHandler); | |
| 44 | + } | |
| 45 | + | |
| 46 | + @Override | |
| 47 | + public void handlePOST(CoapExchange exchange) { | |
| 48 | + log.debug("[handlePOST][收到子设备动态注册请求]"); | |
| 49 | + registerSubHandler.handle(exchange); | |
| 50 | + } | |
| 51 | + | |
| 52 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.hutool.core.map.MapUtil; | |
| 6 | +import cn.hutool.core.text.StrPool; | |
| 7 | +import cn.hutool.extra.spring.SpringUtil; | |
| 8 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | +import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 13 | + | |
| 14 | +import java.util.List; | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * IoT 网关 CoAP 协议的【上行】处理器 | |
| 18 | + * | |
| 19 | + * 处理设备通过 CoAP 协议发送的上行消息,包括: | |
| 20 | + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post | |
| 21 | + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post | |
| 22 | + * | |
| 23 | + * Token 通过自定义 CoAP Option 2088 携带 | |
| 24 | + * | |
| 25 | + * @author 芋道源码 | |
| 26 | + */ | |
| 27 | +@Slf4j | |
| 28 | +public class IotCoapUpstreamHandler extends IotCoapAbstractHandler { | |
| 29 | + | |
| 30 | + private final String serverId; | |
| 31 | + | |
| 32 | + private final IotDeviceMessageService deviceMessageService; | |
| 33 | + | |
| 34 | + public IotCoapUpstreamHandler(String serverId) { | |
| 35 | + this.serverId = serverId; | |
| 36 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 37 | + } | |
| 38 | + | |
| 39 | + @Override | |
| 40 | + @SuppressWarnings("DuplicatedCode") | |
| 41 | + protected CommonResult<Object> handle0(CoapExchange exchange) { | |
| 42 | + // 1.1 解析通用参数 | |
| 43 | + List<String> uriPath = exchange.getRequestOptions().getUriPath(); | |
| 44 | + String productKey = getProductKey(uriPath); | |
| 45 | + String deviceName = getDeviceName(uriPath); | |
| 46 | + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); | |
| 47 | + // 1.2 解析消息 | |
| 48 | + IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class); | |
| 49 | + Assert.notNull(message, "请求参数不能为空"); | |
| 50 | + Assert.equals(method, message.getMethod(), "method 不匹配"); | |
| 51 | + | |
| 52 | + // 2. 发送消息 | |
| 53 | + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 54 | + | |
| 55 | + // 3. 返回结果 | |
| 56 | + return CommonResult.success(MapUtil.of("messageId", message.getId())); | |
| 57 | + } | |
| 58 | + | |
| 59 | + @Override | |
| 60 | + protected boolean requiresAuthentication() { | |
| 61 | + return true; | |
| 62 | + } | |
| 63 | + | |
| 64 | + @Override | |
| 65 | + protected String getProductKey(List<String> uriPath) { | |
| 66 | + // 路径格式:/topic/sys/{productKey}/{deviceName}/... | |
| 67 | + return CollUtil.get(uriPath, 2); | |
| 68 | + } | |
| 69 | + | |
| 70 | + @Override | |
| 71 | + protected String getDeviceName(List<String> uriPath) { | |
| 72 | + // 路径格式:/topic/sys/{productKey}/{deviceName}/... | |
| 73 | + return CollUtil.get(uriPath, 3); | |
| 74 | + } | |
| 75 | + | |
| 76 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; | |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; | |
| 4 | 3 | import lombok.extern.slf4j.Slf4j; |
| 5 | 4 | import org.eclipse.californium.core.CoapResource; |
| 6 | 5 | import org.eclipse.californium.core.server.resources.CoapExchange; |
| ... | ... | @@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource { |
| 20 | 19 | |
| 21 | 20 | public static final String PATH = "topic"; |
| 22 | 21 | |
| 23 | - private final IotCoapUpstreamProtocol protocol; | |
| 22 | + private final String serverId; | |
| 24 | 23 | private final IotCoapUpstreamHandler upstreamHandler; |
| 25 | 24 | |
| 26 | 25 | /** |
| 27 | 26 | * 创建根资源(/topic) |
| 28 | 27 | */ |
| 29 | - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol, | |
| 28 | + public IotCoapUpstreamTopicResource(String serverId, | |
| 30 | 29 | IotCoapUpstreamHandler upstreamHandler) { |
| 31 | - this(PATH, protocol, upstreamHandler); | |
| 30 | + this(PATH, serverId, upstreamHandler); | |
| 32 | 31 | log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH); |
| 33 | 32 | } |
| 34 | 33 | |
| ... | ... | @@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource { |
| 36 | 35 | * 创建子资源(动态路径) |
| 37 | 36 | */ |
| 38 | 37 | private IotCoapUpstreamTopicResource(String name, |
| 39 | - IotCoapUpstreamProtocol protocol, | |
| 38 | + String serverId, | |
| 40 | 39 | IotCoapUpstreamHandler upstreamHandler) { |
| 41 | 40 | super(name); |
| 42 | - this.protocol = protocol; | |
| 41 | + this.serverId = serverId; | |
| 43 | 42 | this.upstreamHandler = upstreamHandler; |
| 44 | 43 | } |
| 45 | 44 | |
| 46 | 45 | @Override |
| 47 | 46 | public Resource getChild(String name) { |
| 48 | 47 | // 递归创建动态子资源,支持任意深度路径 |
| 49 | - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler); | |
| 48 | + return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler); | |
| 50 | 49 | } |
| 51 | 50 | |
| 52 | 51 | @Override |
| 53 | 52 | public void handleGET(CoapExchange exchange) { |
| 54 | - upstreamHandler.handle(exchange, protocol); | |
| 53 | + upstreamHandler.handle(exchange); | |
| 55 | 54 | } |
| 56 | 55 | |
| 57 | 56 | @Override |
| 58 | 57 | public void handlePOST(CoapExchange exchange) { |
| 59 | - upstreamHandler.handle(exchange, protocol); | |
| 58 | + upstreamHandler.handle(exchange); | |
| 60 | 59 | } |
| 61 | 60 | |
| 62 | 61 | @Override |
| 63 | 62 | public void handlePUT(CoapExchange exchange) { |
| 64 | - upstreamHandler.handle(exchange, protocol); | |
| 63 | + upstreamHandler.handle(exchange); | |
| 65 | 64 | } |
| 66 | 65 | |
| 67 | 66 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java
| ... | ... | @@ -2,12 +2,5 @@ |
| 2 | 2 | * CoAP 协议实现包 |
| 3 | 3 | * <p> |
| 4 | 4 | * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能 |
| 5 | - * <p> | |
| 6 | - * URI 路径: | |
| 7 | - * - 认证:POST /auth | |
| 8 | - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post | |
| 9 | - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post | |
| 10 | - * <p> | |
| 11 | - * Token 通过 CoAP Option 2088 携带 | |
| 12 | 5 | */ |
| 13 | 6 | package cn.iocoder.yudao.module.iot.gateway.protocol.coap; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.lang.Assert; | |
| 4 | -import cn.hutool.core.map.MapUtil; | |
| 5 | -import cn.hutool.core.util.BooleanUtil; | |
| 6 | -import cn.hutool.core.util.StrUtil; | |
| 7 | -import cn.hutool.extra.spring.SpringUtil; | |
| 8 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 9 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 11 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 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.gateway.protocol.coap.IotCoapUpstreamProtocol; | |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; | |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 17 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 18 | -import lombok.extern.slf4j.Slf4j; | |
| 19 | -import org.eclipse.californium.core.coap.CoAP; | |
| 20 | -import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 21 | - | |
| 22 | -import java.util.Map; | |
| 23 | - | |
| 24 | -/** | |
| 25 | - * IoT 网关 CoAP 协议的【认证】处理器 | |
| 26 | - * | |
| 27 | - * @author 芋道源码 | |
| 28 | - */ | |
| 29 | -@Slf4j | |
| 30 | -public class IotCoapAuthHandler { | |
| 31 | - | |
| 32 | - private final IotDeviceTokenService deviceTokenService; | |
| 33 | - private final IotDeviceCommonApi deviceApi; | |
| 34 | - private final IotDeviceMessageService deviceMessageService; | |
| 35 | - | |
| 36 | - public IotCoapAuthHandler() { | |
| 37 | - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 38 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 39 | - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 40 | - } | |
| 41 | - | |
| 42 | - /** | |
| 43 | - * 处理认证请求 | |
| 44 | - * | |
| 45 | - * @param exchange CoAP 交换对象 | |
| 46 | - * @param protocol 协议对象 | |
| 47 | - */ | |
| 48 | - @SuppressWarnings("unchecked") | |
| 49 | - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { | |
| 50 | - try { | |
| 51 | - // 1.1 解析请求体 | |
| 52 | - byte[] payload = exchange.getRequestPayload(); | |
| 53 | - if (payload == null || payload.length == 0) { | |
| 54 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); | |
| 55 | - return; | |
| 56 | - } | |
| 57 | - Map<String, Object> body; | |
| 58 | - try { | |
| 59 | - body = JsonUtils.parseObject(new String(payload), Map.class); | |
| 60 | - } catch (Exception e) { | |
| 61 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); | |
| 62 | - return; | |
| 63 | - } | |
| 64 | - // 1.2 解析参数 | |
| 65 | - String clientId = MapUtil.getStr(body, "clientId"); | |
| 66 | - if (StrUtil.isEmpty(clientId)) { | |
| 67 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); | |
| 68 | - return; | |
| 69 | - } | |
| 70 | - String username = MapUtil.getStr(body, "username"); | |
| 71 | - if (StrUtil.isEmpty(username)) { | |
| 72 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); | |
| 73 | - return; | |
| 74 | - } | |
| 75 | - String password = MapUtil.getStr(body, "password"); | |
| 76 | - if (StrUtil.isEmpty(password)) { | |
| 77 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); | |
| 78 | - return; | |
| 79 | - } | |
| 80 | - | |
| 81 | - // 2.1 执行认证 | |
| 82 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 83 | - .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 84 | - if (result.isError()) { | |
| 85 | - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); | |
| 86 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); | |
| 87 | - return; | |
| 88 | - } | |
| 89 | - if (!BooleanUtil.isTrue(result.getData())) { | |
| 90 | - log.warn("[handle][认证失败,clientId: {}]", clientId); | |
| 91 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); | |
| 92 | - return; | |
| 93 | - } | |
| 94 | - // 2.2 生成 Token | |
| 95 | - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); | |
| 96 | - Assert.notNull(deviceInfo, "设备信息不能为空"); | |
| 97 | - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 98 | - Assert.notBlank(token, "生成 token 不能为空"); | |
| 99 | - | |
| 100 | - // 3. 执行上线 | |
| 101 | - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); | |
| 102 | - deviceMessageService.sendDeviceMessage(message, | |
| 103 | - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); | |
| 104 | - | |
| 105 | - // 4. 返回成功响应 | |
| 106 | - log.info("[handle][认证成功,productKey: {}, deviceName: {}]", | |
| 107 | - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 108 | - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token)); | |
| 109 | - } catch (Exception e) { | |
| 110 | - log.error("[handle][认证处理异常]", e); | |
| 111 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); | |
| 112 | - } | |
| 113 | - } | |
| 114 | - | |
| 115 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.hutool.core.util.StrUtil; | |
| 5 | -import cn.hutool.extra.spring.SpringUtil; | |
| 6 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 7 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 8 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 9 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; | |
| 12 | -import lombok.extern.slf4j.Slf4j; | |
| 13 | -import org.eclipse.californium.core.coap.CoAP; | |
| 14 | -import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 15 | - | |
| 16 | -import java.util.Map; | |
| 17 | - | |
| 18 | -/** | |
| 19 | - * IoT 网关 CoAP 协议的【设备动态注册】处理器 | |
| 20 | - * <p> | |
| 21 | - * 用于直连设备/网关的一型一密动态注册,不需要认证 | |
| 22 | - * | |
| 23 | - * @author 芋道源码 | |
| 24 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 25 | - */ | |
| 26 | -@Slf4j | |
| 27 | -public class IotCoapRegisterHandler { | |
| 28 | - | |
| 29 | - private final IotDeviceCommonApi deviceApi; | |
| 30 | - | |
| 31 | - public IotCoapRegisterHandler() { | |
| 32 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 33 | - } | |
| 34 | - | |
| 35 | - /** | |
| 36 | - * 处理设备动态注册请求 | |
| 37 | - * | |
| 38 | - * @param exchange CoAP 交换对象 | |
| 39 | - */ | |
| 40 | - @SuppressWarnings("unchecked") | |
| 41 | - public void handle(CoapExchange exchange) { | |
| 42 | - try { | |
| 43 | - // 1.1 解析请求体 | |
| 44 | - byte[] payload = exchange.getRequestPayload(); | |
| 45 | - if (payload == null || payload.length == 0) { | |
| 46 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); | |
| 47 | - return; | |
| 48 | - } | |
| 49 | - Map<String, Object> body; | |
| 50 | - try { | |
| 51 | - body = JsonUtils.parseObject(new String(payload), Map.class); | |
| 52 | - } catch (Exception e) { | |
| 53 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); | |
| 54 | - return; | |
| 55 | - } | |
| 56 | - | |
| 57 | - // 1.2 解析参数 | |
| 58 | - String productKey = MapUtil.getStr(body, "productKey"); | |
| 59 | - if (StrUtil.isEmpty(productKey)) { | |
| 60 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); | |
| 61 | - return; | |
| 62 | - } | |
| 63 | - String deviceName = MapUtil.getStr(body, "deviceName"); | |
| 64 | - if (StrUtil.isEmpty(deviceName)) { | |
| 65 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); | |
| 66 | - return; | |
| 67 | - } | |
| 68 | - String productSecret = MapUtil.getStr(body, "productSecret"); | |
| 69 | - if (StrUtil.isEmpty(productSecret)) { | |
| 70 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空"); | |
| 71 | - return; | |
| 72 | - } | |
| 73 | - | |
| 74 | - // 2. 调用动态注册 | |
| 75 | - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() | |
| 76 | - .setProductKey(productKey) | |
| 77 | - .setDeviceName(deviceName) | |
| 78 | - .setProductSecret(productSecret); | |
| 79 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO); | |
| 80 | - if (result.isError()) { | |
| 81 | - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]", | |
| 82 | - productKey, deviceName, result.getMsg()); | |
| 83 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, | |
| 84 | - "设备动态注册失败:" + result.getMsg()); | |
| 85 | - return; | |
| 86 | - } | |
| 87 | - | |
| 88 | - // 3. 返回成功响应 | |
| 89 | - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName); | |
| 90 | - IotCoapUtils.respondSuccess(exchange, result.getData()); | |
| 91 | - } catch (Exception e) { | |
| 92 | - log.error("[handle][设备动态注册处理异常]", e); | |
| 93 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); | |
| 94 | - } | |
| 95 | - } | |
| 96 | - | |
| 97 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | -import cn.hutool.core.map.MapUtil; | |
| 5 | -import cn.hutool.core.text.StrPool; | |
| 6 | -import cn.hutool.core.util.ArrayUtil; | |
| 7 | -import cn.hutool.core.util.ObjUtil; | |
| 8 | -import cn.hutool.core.util.StrUtil; | |
| 9 | -import cn.hutool.extra.spring.SpringUtil; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 11 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; | |
| 13 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; | |
| 14 | -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 16 | -import lombok.extern.slf4j.Slf4j; | |
| 17 | -import org.eclipse.californium.core.coap.CoAP; | |
| 18 | -import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 19 | - | |
| 20 | -import java.util.List; | |
| 21 | - | |
| 22 | -/** | |
| 23 | - * IoT 网关 CoAP 协议的【上行】处理器 | |
| 24 | - * | |
| 25 | - * 处理设备通过 CoAP 协议发送的上行消息,包括: | |
| 26 | - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post | |
| 27 | - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post | |
| 28 | - * | |
| 29 | - * Token 通过自定义 CoAP Option 2088 携带 | |
| 30 | - * | |
| 31 | - * @author 芋道源码 | |
| 32 | - */ | |
| 33 | -@Slf4j | |
| 34 | -public class IotCoapUpstreamHandler { | |
| 35 | - | |
| 36 | - private final IotDeviceTokenService deviceTokenService; | |
| 37 | - private final IotDeviceMessageService deviceMessageService; | |
| 38 | - | |
| 39 | - public IotCoapUpstreamHandler() { | |
| 40 | - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 41 | - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 42 | - } | |
| 43 | - | |
| 44 | - /** | |
| 45 | - * 处理 CoAP 请求 | |
| 46 | - * | |
| 47 | - * @param exchange CoAP 交换对象 | |
| 48 | - * @param protocol 协议对象 | |
| 49 | - */ | |
| 50 | - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { | |
| 51 | - try { | |
| 52 | - // 1. 解析通用参数 | |
| 53 | - List<String> uriPath = exchange.getRequestOptions().getUriPath(); | |
| 54 | - String productKey = CollUtil.get(uriPath, 2); | |
| 55 | - String deviceName = CollUtil.get(uriPath, 3); | |
| 56 | - byte[] payload = exchange.getRequestPayload(); | |
| 57 | - if (StrUtil.isEmpty(productKey)) { | |
| 58 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); | |
| 59 | - return; | |
| 60 | - } | |
| 61 | - if (StrUtil.isEmpty(deviceName)) { | |
| 62 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); | |
| 63 | - return; | |
| 64 | - } | |
| 65 | - if (ArrayUtil.isEmpty(payload)) { | |
| 66 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); | |
| 67 | - return; | |
| 68 | - } | |
| 69 | - | |
| 70 | - // 2. 认证:从自定义 Option 获取 token | |
| 71 | - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN); | |
| 72 | - if (StrUtil.isEmpty(token)) { | |
| 73 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空"); | |
| 74 | - return; | |
| 75 | - } | |
| 76 | - // 验证 token | |
| 77 | - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); | |
| 78 | - if (deviceInfo == null) { | |
| 79 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期"); | |
| 80 | - return; | |
| 81 | - } | |
| 82 | - // 验证设备信息匹配 | |
| 83 | - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) | |
| 84 | - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { | |
| 85 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配"); | |
| 86 | - return; | |
| 87 | - } | |
| 88 | - | |
| 89 | - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接 | |
| 90 | - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post] | |
| 91 | - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); | |
| 92 | - | |
| 93 | - // 2.2 解码消息 | |
| 94 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 95 | - if (ObjUtil.notEqual(method, message.getMethod())) { | |
| 96 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配"); | |
| 97 | - return; | |
| 98 | - } | |
| 99 | - // 2.3 发送消息到消息总线 | |
| 100 | - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); | |
| 101 | - | |
| 102 | - // 3. 返回成功响应 | |
| 103 | - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId())); | |
| 104 | - } catch (Exception e) { | |
| 105 | - log.error("[handle][CoAP 请求处理异常]", e); | |
| 106 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); | |
| 107 | - } | |
| 108 | - } | |
| 109 | - | |
| 110 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | -import org.eclipse.californium.core.coap.CoAP; | |
| 7 | -import org.eclipse.californium.core.coap.MediaTypeRegistry; | |
| 8 | -import org.eclipse.californium.core.coap.Option; | |
| 9 | -import org.eclipse.californium.core.server.resources.CoapExchange; | |
| 10 | - | |
| 11 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | |
| 3 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAbstractHandler; | |
| 12 | 4 | |
| 13 | 5 | /** |
| 14 | 6 | * IoT CoAP 协议工具类 |
| ... | ... | @@ -22,63 +14,6 @@ public class IotCoapUtils { |
| 22 | 14 | * <p> |
| 23 | 15 | * CoAP Option 范围 2048-65535 属于实验/自定义范围 |
| 24 | 16 | */ |
| 25 | - public static final int OPTION_TOKEN = 2088; | |
| 26 | - | |
| 27 | - /** | |
| 28 | - * 返回成功响应 | |
| 29 | - * | |
| 30 | - * @param exchange CoAP 交换对象 | |
| 31 | - * @param data 响应数据 | |
| 32 | - */ | |
| 33 | - public static void respondSuccess(CoapExchange exchange, Object data) { | |
| 34 | - CommonResult<Object> result = CommonResult.success(data); | |
| 35 | - String json = JsonUtils.toJsonString(result); | |
| 36 | - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); | |
| 37 | - } | |
| 38 | - | |
| 39 | - /** | |
| 40 | - * 返回错误响应 | |
| 41 | - * | |
| 42 | - * @param exchange CoAP 交换对象 | |
| 43 | - * @param code CoAP 响应码 | |
| 44 | - * @param message 错误消息 | |
| 45 | - */ | |
| 46 | - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) { | |
| 47 | - int errorCode = mapCoapCodeToErrorCode(code); | |
| 48 | - CommonResult<Object> result = CommonResult.error(errorCode, message); | |
| 49 | - String json = JsonUtils.toJsonString(result); | |
| 50 | - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON); | |
| 51 | - } | |
| 52 | - | |
| 53 | - /** | |
| 54 | - * 从自定义 CoAP Option 中获取 Token | |
| 55 | - * | |
| 56 | - * @param exchange CoAP 交换对象 | |
| 57 | - * @param optionNumber Option 编号 | |
| 58 | - * @return Token 值,如果不存在则返回 null | |
| 59 | - */ | |
| 60 | - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) { | |
| 61 | - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), | |
| 62 | - o -> o.getNumber() == optionNumber); | |
| 63 | - return option != null ? new String(option.getValue()) : null; | |
| 64 | - } | |
| 65 | - | |
| 66 | - /** | |
| 67 | - * 将 CoAP 响应码映射到业务错误码 | |
| 68 | - * | |
| 69 | - * @param code CoAP 响应码 | |
| 70 | - * @return 业务错误码 | |
| 71 | - */ | |
| 72 | - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) { | |
| 73 | - if (code == CoAP.ResponseCode.BAD_REQUEST) { | |
| 74 | - return BAD_REQUEST.getCode(); | |
| 75 | - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) { | |
| 76 | - return UNAUTHORIZED.getCode(); | |
| 77 | - } else if (code == CoAP.ResponseCode.FORBIDDEN) { | |
| 78 | - return FORBIDDEN.getCode(); | |
| 79 | - } else { | |
| 80 | - return INTERNAL_SERVER_ERROR.getCode(); | |
| 81 | - } | |
| 82 | - } | |
| 17 | + public static final int OPTION_TOKEN = IotCoapAbstractHandler.OPTION_TOKEN; | |
| 83 | 18 | |
| 84 | 19 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.lang.Assert; |
| 4 | 4 | import cn.hutool.core.util.ArrayUtil; |
| ... | ... | @@ -10,9 +10,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 10 | 10 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 11 | 11 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 12 | 12 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 13 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; | |
| 14 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; | |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | |
| 16 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; |
| 17 | 14 | import io.vertx.core.Handler; |
| 18 | 15 | import io.vertx.core.http.HttpHeaders; |
| ... | ... | @@ -20,8 +17,7 @@ import io.vertx.ext.web.RoutingContext; |
| 20 | 17 | import lombok.extern.slf4j.Slf4j; |
| 21 | 18 | import org.springframework.http.MediaType; |
| 22 | 19 | |
| 23 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; | |
| 24 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 20 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | |
| 25 | 21 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 26 | 22 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| 27 | 23 | |
| ... | ... | @@ -35,8 +31,6 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> |
| 35 | 31 | |
| 36 | 32 | private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); |
| 37 | 33 | |
| 38 | - private final IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); | |
| 39 | - | |
| 40 | 34 | @Override |
| 41 | 35 | public final void handle(RoutingContext context) { |
| 42 | 36 | try { |
| ... | ... | @@ -47,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> |
| 47 | 41 | CommonResult<Object> result = handle0(context); |
| 48 | 42 | writeResponse(context, result); |
| 49 | 43 | } catch (ServiceException e) { |
| 44 | + // 已知异常,返回对应的错误码和错误信息 | |
| 50 | 45 | writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); |
| 46 | + } catch (IllegalArgumentException e) { | |
| 47 | + // 参数校验异常,返回 400 错误 | |
| 48 | + writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); | |
| 51 | 49 | } catch (Exception e) { |
| 50 | + // 其他未知异常,返回 500 错误 | |
| 52 | 51 | log.error("[handle][path({}) 处理异常]", context.request().path(), e); |
| 53 | 52 | writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); |
| 54 | 53 | } |
| 55 | 54 | } |
| 56 | 55 | |
| 56 | + /** | |
| 57 | + * 处理 HTTP 请求(子类实现) | |
| 58 | + * | |
| 59 | + * @param context RoutingContext 对象 | |
| 60 | + * @return 处理结果 | |
| 61 | + */ | |
| 57 | 62 | protected abstract CommonResult<Object> handle0(RoutingContext context); |
| 58 | 63 | |
| 64 | + /** | |
| 65 | + * 前置处理:认证等 | |
| 66 | + * | |
| 67 | + * @param context RoutingContext 对象 | |
| 68 | + */ | |
| 59 | 69 | private void beforeHandle(RoutingContext context) { |
| 60 | 70 | // 如果不需要认证,则不走前置处理 |
| 61 | 71 | String path = context.request().path(); |
| ... | ... | @@ -102,7 +112,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> |
| 102 | 112 | } |
| 103 | 113 | |
| 104 | 114 | @SuppressWarnings("deprecation") |
| 105 | - public static void writeResponse(RoutingContext context, Object data) { | |
| 115 | + public static void writeResponse(RoutingContext context, CommonResult<?> data) { | |
| 106 | 116 | context.response() |
| 107 | 117 | .setStatusCode(200) |
| 108 | 118 | .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; |
| 3 | 3 | import cn.hutool.core.lang.Assert; |
| 4 | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | 5 | import cn.hutool.core.util.BooleanUtil; |
| 6 | -import cn.hutool.core.util.StrUtil; | |
| 7 | 6 | import cn.hutool.extra.spring.SpringUtil; |
| 8 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 9 | 8 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| ... | ... | @@ -11,13 +10,11 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 11 | 10 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 12 | 11 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 13 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 14 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; | |
| 15 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; |
| 16 | 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 17 | 15 | import io.vertx.ext.web.RoutingContext; |
| 18 | 16 | |
| 19 | 17 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 20 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 21 | 18 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 22 | 19 | import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; |
| 23 | 20 | |
| ... | ... | @@ -48,23 +45,19 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { |
| 48 | 45 | } |
| 49 | 46 | |
| 50 | 47 | @Override |
| 48 | + @SuppressWarnings("DuplicatedCode") | |
| 51 | 49 | public CommonResult<Object> handle0(RoutingContext context) { |
| 52 | 50 | // 1. 解析参数 |
| 53 | 51 | IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class); |
| 54 | - if (StrUtil.isEmpty(request.getClientId())) { | |
| 55 | - throw invalidParamException("clientId 不能为空"); | |
| 56 | - } | |
| 57 | - if (StrUtil.isEmpty(request.getUsername())) { | |
| 58 | - throw invalidParamException("username 不能为空"); | |
| 59 | - } | |
| 60 | - if (StrUtil.isEmpty(request.getPassword())) { | |
| 61 | - throw invalidParamException("password 不能为空"); | |
| 62 | - } | |
| 52 | + Assert.notNull(request, "请求参数不能为空"); | |
| 53 | + Assert.notBlank(request.getClientId(), "clientId 不能为空"); | |
| 54 | + Assert.notBlank(request.getUsername(), "username 不能为空"); | |
| 55 | + Assert.notBlank(request.getPassword(), "password 不能为空"); | |
| 63 | 56 | |
| 64 | 57 | // 2.1 执行认证 |
| 65 | 58 | CommonResult<Boolean> result = deviceApi.authDevice(request); |
| 66 | 59 | result.checkError(); |
| 67 | - if (!BooleanUtil.isTrue(result.getData())) { | |
| 60 | + if (BooleanUtil.isFalse(result.getData())) { | |
| 68 | 61 | throw exception(DEVICE_AUTH_FAIL); |
| 69 | 62 | } |
| 70 | 63 | // 2.2 生成 Token | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; | |
| 10 | 9 | import io.vertx.ext.web.RoutingContext; |
| 11 | 10 | |
| 12 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 13 | 11 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 14 | 12 | |
| 15 | 13 | /** |
| ... | ... | @@ -34,15 +32,10 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { |
| 34 | 32 | public CommonResult<Object> handle0(RoutingContext context) { |
| 35 | 33 | // 1. 解析参数 |
| 36 | 34 | IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class); |
| 37 | - if (StrUtil.isEmpty(request.getProductKey())) { | |
| 38 | - throw invalidParamException("productKey 不能为空"); | |
| 39 | - } | |
| 40 | - if (StrUtil.isEmpty(request.getDeviceName())) { | |
| 41 | - throw invalidParamException("deviceName 不能为空"); | |
| 42 | - } | |
| 43 | - if (StrUtil.isEmpty(request.getProductSecret())) { | |
| 44 | - throw invalidParamException("productSecret 不能为空"); | |
| 45 | - } | |
| 35 | + Assert.notNull(request, "请求参数不能为空"); | |
| 36 | + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); | |
| 37 | + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); | |
| 38 | + Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); | |
| 46 | 39 | |
| 47 | 40 | // 2. 调用动态注册 |
| 48 | 41 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; | |
| 11 | 10 | import io.vertx.ext.web.RoutingContext; |
| 12 | 11 | import lombok.Data; |
| 13 | 12 | |
| 14 | 13 | import java.util.List; |
| 15 | 14 | |
| 16 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 17 | 15 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 18 | 16 | |
| 19 | 17 | /** |
| ... | ... | @@ -46,9 +44,8 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { |
| 46 | 44 | String deviceName = context.pathParam("deviceName"); |
| 47 | 45 | // 1.2 解析子设备列表 |
| 48 | 46 | SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class); |
| 49 | - if (CollUtil.isEmpty(request.getParams())) { | |
| 50 | - throw invalidParamException("params 不能为空"); | |
| 51 | - } | |
| 47 | + Assert.notNull(request, "请求参数不能为空"); | |
| 48 | + Assert.notEmpty(request.getParams(), "params 不能为空"); | |
| 52 | 49 | |
| 53 | 50 | // 2. 调用子设备动态注册 |
| 54 | 51 | IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java
| ... | ... | @@ -7,7 +7,6 @@ import cn.hutool.extra.spring.SpringUtil; |
| 7 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; | |
| 11 | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 12 | 11 | import io.vertx.ext.web.RoutingContext; |
| 13 | 12 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -33,15 +32,16 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { |
| 33 | 32 | |
| 34 | 33 | @Override |
| 35 | 34 | protected CommonResult<Object> handle0(RoutingContext context) { |
| 36 | - // 1. 解析通用参数 | |
| 35 | + // 1.1 解析通用参数 | |
| 37 | 36 | String productKey = context.pathParam("productKey"); |
| 38 | 37 | String deviceName = context.pathParam("deviceName"); |
| 39 | 38 | String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); |
| 40 | - | |
| 41 | - // 2.1 根据 Content-Type 反序列化消息 | |
| 39 | + // 1.2 根据 Content-Type 反序列化消息 | |
| 42 | 40 | IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class); |
| 41 | + Assert.notNull(message, "请求参数不能为空"); | |
| 43 | 42 | Assert.equals(method, message.getMethod(), "method 不匹配"); |
| 44 | - // 2.2 发送消息 | |
| 43 | + | |
| 44 | + // 2. 发送消息 | |
| 45 | 45 | deviceMessageService.sendDeviceMessage(message, |
| 46 | 46 | productKey, deviceName, serverId); |
| 47 | 47 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.BooleanUtil; | |
| 3 | 4 | import cn.hutool.core.util.IdUtil; |
| 4 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 5 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 6 | 8 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 7 | 9 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| ... | ... | @@ -26,7 +28,8 @@ import lombok.extern.slf4j.Slf4j; |
| 26 | 28 | import org.springframework.util.Assert; |
| 27 | 29 | |
| 28 | 30 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| 29 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; | |
| 31 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 32 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | |
| 30 | 33 | |
| 31 | 34 | /** |
| 32 | 35 | * TCP 上行消息处理器 |
| ... | ... | @@ -132,6 +135,12 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 132 | 135 | // 业务消息 |
| 133 | 136 | handleBusinessRequest(clientId, message, socket); |
| 134 | 137 | } |
| 138 | + } catch (ServiceException e) { | |
| 139 | + // 业务异常,返回对应的错误码和错误信息 | |
| 140 | + log.warn("[processMessage][业务异常,客户端 ID: {},错误: {}]", clientId, e.getMessage()); | |
| 141 | + String requestId = message != null ? message.getRequestId() : null; | |
| 142 | + String method = message != null ? message.getMethod() : null; | |
| 143 | + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); | |
| 135 | 144 | } catch (IllegalArgumentException e) { |
| 136 | 145 | // 参数校验失败,返回 400 |
| 137 | 146 | log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); |
| ... | ... | @@ -166,10 +175,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 166 | 175 | |
| 167 | 176 | // 2.1 执行认证 |
| 168 | 177 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| 169 | - if (authResult.isError()) { | |
| 170 | - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", clientId, authParams.getUsername()); | |
| 171 | - sendErrorResponse(socket, message.getRequestId(), AUTH_METHOD, authResult.getCode(), authResult.getMsg()); | |
| 172 | - return; | |
| 178 | + authResult.checkError(); | |
| 179 | + if (BooleanUtil.isFalse(authResult.getData())) { | |
| 180 | + throw exception(DEVICE_AUTH_FAIL); | |
| 173 | 181 | } |
| 174 | 182 | // 2.2 解析设备信息 |
| 175 | 183 | IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); |
| ... | ... | @@ -205,12 +213,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 205 | 213 | |
| 206 | 214 | // 2. 调用动态注册 |
| 207 | 215 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); |
| 208 | - if (result.isError()) { | |
| 209 | - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | |
| 210 | - sendErrorResponse(socket, message.getRequestId(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), | |
| 211 | - result.getCode(), result.getMsg()); | |
| 212 | - return; | |
| 213 | - } | |
| 216 | + result.checkError(); | |
| 214 | 217 | |
| 215 | 218 | // 3. 发送成功响应 |
| 216 | 219 | sendSuccessResponse(socket, message.getRequestId(), | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.map.MapUtil; | |
| 3 | 4 | import cn.hutool.core.util.ArrayUtil; |
| 5 | +import cn.hutool.core.util.BooleanUtil; | |
| 4 | 6 | import cn.hutool.core.util.IdUtil; |
| 5 | 7 | import cn.hutool.core.util.StrUtil; |
| 6 | 8 | import cn.hutool.extra.spring.SpringUtil; |
| 9 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 7 | 10 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 8 | 11 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 9 | 12 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| ... | ... | @@ -27,10 +30,11 @@ import lombok.extern.slf4j.Slf4j; |
| 27 | 30 | import org.springframework.util.Assert; |
| 28 | 31 | |
| 29 | 32 | import java.net.InetSocketAddress; |
| 30 | -import java.time.LocalDateTime; | |
| 31 | 33 | import java.util.Map; |
| 32 | 34 | |
| 33 | 35 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| 36 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 37 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | |
| 34 | 38 | |
| 35 | 39 | /** |
| 36 | 40 | * UDP 上行消息处理器 |
| ... | ... | @@ -87,7 +91,7 @@ public class IotUdpUpstreamHandler { |
| 87 | 91 | this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); |
| 88 | 92 | } |
| 89 | 93 | |
| 90 | - // TODO @AI:vertx 有 udp 的实现么? | |
| 94 | + // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现 | |
| 91 | 95 | /** |
| 92 | 96 | * 处理 UDP 数据包 |
| 93 | 97 | * |
| ... | ... | @@ -99,18 +103,7 @@ public class IotUdpUpstreamHandler { |
| 99 | 103 | Buffer data = packet.data(); |
| 100 | 104 | String addressKey = sessionManager.buildAddressKey(senderAddress); |
| 101 | 105 | log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); |
| 102 | - try { | |
| 103 | - processMessage(data, senderAddress, socket); | |
| 104 | - } catch (IllegalArgumentException e) { | |
| 105 | - // 参数校验失败,返回 400 | |
| 106 | - log.warn("[handle][参数校验失败,来源: {},错误: {}]", addressKey, e.getMessage()); | |
| 107 | - sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), e.getMessage()); | |
| 108 | - } catch (Exception e) { | |
| 109 | - // 其他异常,返回 500 | |
| 110 | - log.error("[handle][处理消息失败,来源: {}]", addressKey, e); | |
| 111 | - sendErrorResponse(socket, senderAddress, null, null, | |
| 112 | - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 113 | - } | |
| 106 | + processMessage(data, senderAddress, socket); | |
| 114 | 107 | } |
| 115 | 108 | |
| 116 | 109 | /** |
| ... | ... | @@ -121,6 +114,7 @@ public class IotUdpUpstreamHandler { |
| 121 | 114 | * @param socket UDP Socket |
| 122 | 115 | */ |
| 123 | 116 | private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { |
| 117 | + String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 124 | 118 | // 1.1 基础检查 |
| 125 | 119 | if (ArrayUtil.isEmpty(buffer)) { |
| 126 | 120 | return; |
| ... | ... | @@ -133,15 +127,35 @@ public class IotUdpUpstreamHandler { |
| 133 | 127 | } |
| 134 | 128 | |
| 135 | 129 | // 2. 根据消息类型路由处理 |
| 136 | - if (AUTH_METHOD.equals(message.getMethod())) { | |
| 137 | - // 认证请求 | |
| 138 | - handleAuthenticationRequest(message, senderAddress, socket); | |
| 139 | - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | |
| 140 | - // 设备动态注册请求 | |
| 141 | - handleRegisterRequest(message, senderAddress, socket); | |
| 142 | - } else { | |
| 143 | - // 业务消息 | |
| 144 | - handleBusinessRequest(message, senderAddress, socket); | |
| 130 | + try { | |
| 131 | + if (AUTH_METHOD.equals(message.getMethod())) { | |
| 132 | + // 认证请求 | |
| 133 | + handleAuthenticationRequest(message, senderAddress, socket); | |
| 134 | + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | |
| 135 | + // 设备动态注册请求 | |
| 136 | + handleRegisterRequest(message, senderAddress, socket); | |
| 137 | + } else { | |
| 138 | + // 业务消息 | |
| 139 | + handleBusinessRequest(message, senderAddress, socket); | |
| 140 | + } | |
| 141 | + } catch (ServiceException e) { | |
| 142 | + // 业务异常,返回对应的错误码和错误信息 | |
| 143 | + log.warn("[processMessage][业务异常,来源: {},requestId: {},method: {},错误: {}]", | |
| 144 | + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); | |
| 145 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | |
| 146 | + e.getCode(), e.getMessage()); | |
| 147 | + } catch (IllegalArgumentException e) { | |
| 148 | + // 参数校验失败,返回 400 | |
| 149 | + log.warn("[processMessage][参数校验失败,来源: {},requestId: {},method: {},错误: {}]", | |
| 150 | + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); | |
| 151 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | |
| 152 | + BAD_REQUEST.getCode(), e.getMessage()); | |
| 153 | + } catch (Exception e) { | |
| 154 | + // 其他异常,返回 500 | |
| 155 | + log.error("[processMessage][处理消息失败,来源: {},requestId: {},method: {}]", | |
| 156 | + addressKey, message.getRequestId(), message.getMethod(), e); | |
| 157 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | |
| 158 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 145 | 159 | } |
| 146 | 160 | } |
| 147 | 161 | |
| ... | ... | @@ -164,12 +178,9 @@ public class IotUdpUpstreamHandler { |
| 164 | 178 | |
| 165 | 179 | // 2.1 执行认证 |
| 166 | 180 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| 167 | - if (authResult.isError()) { | |
| 168 | - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", | |
| 169 | - clientId, authParams.getUsername()); | |
| 170 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, | |
| 171 | - authResult.getCode(), authResult.getMsg()); | |
| 172 | - return; | |
| 181 | + authResult.checkError(); | |
| 182 | + if (!BooleanUtil.isTrue(authResult.getData())) { | |
| 183 | + throw exception(DEVICE_AUTH_FAIL); | |
| 173 | 184 | } |
| 174 | 185 | // 2.2 解析设备信息 |
| 175 | 186 | IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); |
| ... | ... | @@ -187,7 +198,8 @@ public class IotUdpUpstreamHandler { |
| 187 | 198 | // 4.2 发送上线消息 |
| 188 | 199 | sendOnlineMessage(device); |
| 189 | 200 | // 4.3 发送成功响应(包含 token) |
| 190 | - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token); | |
| 201 | + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, | |
| 202 | + MapUtil.of("token", token)); | |
| 191 | 203 | log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", |
| 192 | 204 | device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); |
| 193 | 205 | } |
| ... | ... | @@ -211,13 +223,7 @@ public class IotUdpUpstreamHandler { |
| 211 | 223 | |
| 212 | 224 | // 2. 调用动态注册 |
| 213 | 225 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); |
| 214 | - if (result.isError()) { | |
| 215 | - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", | |
| 216 | - sessionManager.buildAddressKey(senderAddress), result.getMsg()); | |
| 217 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), | |
| 218 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getCode(), result.getMsg()); | |
| 219 | - return; | |
| 220 | - } | |
| 226 | + result.checkError(); | |
| 221 | 227 | |
| 222 | 228 | // 3. 发送成功响应 |
| 223 | 229 | sendSuccessResponse(socket, senderAddress, message.getRequestId(), |
| ... | ... | @@ -274,17 +280,8 @@ public class IotUdpUpstreamHandler { |
| 274 | 280 | return; |
| 275 | 281 | } |
| 276 | 282 | |
| 277 | - // 2. 更新会话活跃时间和地址 | |
| 278 | - // TODO @AI:是不是合并到 sessionManager 里面更好? | |
| 279 | - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(device.getId()); | |
| 280 | - if (sessionInfo != null) { | |
| 281 | - // 检查地址是否变化,变化则更新 | |
| 282 | - if (!senderAddress.equals(sessionInfo.getAddress())) { | |
| 283 | - sessionManager.updateSessionAddress(device.getId(), senderAddress); | |
| 284 | - } else { | |
| 285 | - sessionManager.updateSessionActivity(device.getId()); | |
| 286 | - } | |
| 287 | - } | |
| 283 | + // 2. 更新会话地址(如有变化) | |
| 284 | + sessionManager.updateSessionAddress(device.getId(), senderAddress); | |
| 288 | 285 | |
| 289 | 286 | // 3. 将 body 设置为实际的 params,发送消息到消息总线 |
| 290 | 287 | message.setParams(body); |
| ... | ... | @@ -306,8 +303,7 @@ public class IotUdpUpstreamHandler { |
| 306 | 303 | .setDeviceId(device.getId()) |
| 307 | 304 | .setProductKey(device.getProductKey()) |
| 308 | 305 | .setDeviceName(device.getDeviceName()) |
| 309 | - .setAddress(address) | |
| 310 | - .setLastActiveTime(LocalDateTime.now()); | |
| 306 | + .setAddress(address); | |
| 311 | 307 | sessionManager.registerSession(device.getId(), sessionInfo); |
| 312 | 308 | } |
| 313 | 309 | |
| ... | ... | @@ -325,21 +321,6 @@ public class IotUdpUpstreamHandler { |
| 325 | 321 | // ===================== 发送响应消息 ===================== |
| 326 | 322 | |
| 327 | 323 | /** |
| 328 | - * 发送认证成功响应(包含 token) | |
| 329 | - * | |
| 330 | - * @param socket UDP Socket | |
| 331 | - * @param address 目标地址 | |
| 332 | - * @param requestId 请求 ID | |
| 333 | - * @param token JWT Token | |
| 334 | - */ | |
| 335 | - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, | |
| 336 | - String requestId, String token) { | |
| 337 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, token, | |
| 338 | - SUCCESS.getCode(), null); | |
| 339 | - writeResponse(socket, address, responseMessage); | |
| 340 | - } | |
| 341 | - | |
| 342 | - /** | |
| 343 | 324 | * 发送成功响应 |
| 344 | 325 | * |
| 345 | 326 | * @param socket UDP Socket | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ObjUtil; | |
| 4 | +import com.google.common.cache.Cache; | |
| 5 | +import com.google.common.cache.CacheBuilder; | |
| 3 | 6 | import io.vertx.core.buffer.Buffer; |
| 4 | 7 | import io.vertx.core.datagram.DatagramSocket; |
| 5 | 8 | import lombok.Data; |
| 6 | 9 | import lombok.extern.slf4j.Slf4j; |
| 7 | 10 | |
| 8 | 11 | import java.net.InetSocketAddress; |
| 9 | -import java.time.LocalDateTime; | |
| 10 | -import java.util.ArrayList; | |
| 11 | -import java.util.Iterator; | |
| 12 | -import java.util.List; | |
| 13 | -import java.util.Map; | |
| 14 | -import java.util.concurrent.ConcurrentHashMap; | |
| 12 | +import java.util.concurrent.TimeUnit; | |
| 15 | 13 | |
| 16 | 14 | /** |
| 17 | 15 | * IoT 网关 UDP 会话管理器 |
| 18 | 16 | * <p> |
| 19 | - * 统一管理 UDP 会话的认证状态、设备会话和消息发送功能: | |
| 20 | - * 1. 管理 UDP 会话的认证状态 | |
| 21 | - * 2. 管理设备会话和在线状态 | |
| 22 | - * 3. 管理消息发送到设备 | |
| 17 | + * 基于 Guava Cache 实现会话的自动过期清理: | |
| 18 | + * 1. 管理设备会话信息(设备 ID -> 地址映射) | |
| 19 | + * 2. 自动清理超时会话(expireAfterAccess) | |
| 20 | + * 3. 限制最大会话数(maximumSize) | |
| 23 | 21 | * |
| 24 | 22 | * @author 芋道源码 |
| 25 | 23 | */ |
| ... | ... | @@ -27,109 +25,76 @@ import java.util.concurrent.ConcurrentHashMap; |
| 27 | 25 | public class IotUdpSessionManager { |
| 28 | 26 | |
| 29 | 27 | /** |
| 30 | - * 最大会话数 | |
| 28 | + * 设备会话缓存:设备 ID -> 会话信息 | |
| 29 | + * <p> | |
| 30 | + * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间 | |
| 31 | 31 | */ |
| 32 | - private final int maxSessions; | |
| 32 | + private final Cache<Long, SessionInfo> deviceSessionCache; | |
| 33 | 33 | |
| 34 | - /** | |
| 35 | - * 设备 ID -> 会话信息 | |
| 36 | - */ | |
| 37 | - private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>(); | |
| 38 | - | |
| 39 | - /** | |
| 40 | - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) | |
| 41 | - */ | |
| 42 | - // TODO @AI:1)这个变量是否必须?2)unregisterSession 这个方法是否必须? | |
| 43 | - private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>(); | |
| 34 | + private final int maxSessions; | |
| 44 | 35 | |
| 45 | - public IotUdpSessionManager(int maxSessions) { | |
| 36 | + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) { | |
| 46 | 37 | this.maxSessions = maxSessions; |
| 38 | + this.deviceSessionCache = CacheBuilder.newBuilder() | |
| 39 | + .maximumSize(maxSessions) | |
| 40 | + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS) | |
| 41 | + .build(); | |
| 47 | 42 | } |
| 48 | 43 | |
| 49 | 44 | /** |
| 50 | - * 注册设备会话(包含认证信息) | |
| 45 | + * 注册设备会话 | |
| 51 | 46 | * |
| 52 | 47 | * @param deviceId 设备 ID |
| 53 | 48 | * @param sessionInfo 会话信息 |
| 54 | 49 | */ |
| 55 | 50 | public void registerSession(Long deviceId, SessionInfo sessionInfo) { |
| 56 | - // 检查会话数是否已达上限 | |
| 57 | - if (deviceSessionMap.size() >= maxSessions) { | |
| 51 | + // 检查是否为新设备,且会话数已达上限 | |
| 52 | + if (deviceSessionCache.getIfPresent(deviceId) == null | |
| 53 | + && deviceSessionCache.size() >= maxSessions) { | |
| 58 | 54 | throw new IllegalStateException("会话数已达上限: " + maxSessions); |
| 59 | 55 | } |
| 60 | - // 如果设备已有其他会话,先清理旧会话 | |
| 61 | - SessionInfo oldSessionInfo = deviceSessionMap.get(deviceId); | |
| 62 | - if (oldSessionInfo != null) { | |
| 63 | - String oldAddressKey = buildAddressKey(oldSessionInfo.getAddress()); | |
| 64 | - addressDeviceMap.remove(oldAddressKey, deviceId); | |
| 65 | - log.info("[registerSession][设备已有其他会话,清理旧会话,设备 ID: {},旧地址: {}]", | |
| 66 | - deviceId, oldAddressKey); | |
| 67 | - } | |
| 68 | - | |
| 69 | - // 注册新会话 | |
| 70 | - String addressKey = buildAddressKey(sessionInfo.getAddress()); | |
| 71 | - deviceSessionMap.put(deviceId, sessionInfo); | |
| 72 | - addressDeviceMap.put(addressKey, deviceId); | |
| 73 | - log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},product key: {},device name: {}]", | |
| 74 | - deviceId, addressKey, sessionInfo.getProductKey(), sessionInfo.getDeviceName()); | |
| 56 | + // 注册会话 | |
| 57 | + deviceSessionCache.put(deviceId, sessionInfo); | |
| 58 | + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]", | |
| 59 | + deviceId, buildAddressKey(sessionInfo.getAddress()), | |
| 60 | + sessionInfo.getProductKey(), sessionInfo.getDeviceName()); | |
| 75 | 61 | } |
| 76 | 62 | |
| 77 | 63 | /** |
| 78 | - * 注销设备会话 | |
| 64 | + * 获取会话信息 | |
| 65 | + * <p> | |
| 66 | + * 注意:调用此方法会自动刷新会话的过期时间 | |
| 79 | 67 | * |
| 80 | 68 | * @param deviceId 设备 ID |
| 69 | + * @return 会话信息,不存在则返回 null | |
| 81 | 70 | */ |
| 82 | - public void unregisterSession(Long deviceId) { | |
| 83 | - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); | |
| 84 | - if (sessionInfo == null) { | |
| 85 | - return; | |
| 86 | - } | |
| 87 | - String addressKey = buildAddressKey(sessionInfo.getAddress()); | |
| 88 | - // 仅当 addressDeviceMap 中的 deviceId 是当前 deviceId 时才移除,避免误删新会话 | |
| 89 | - addressDeviceMap.remove(addressKey, deviceId); | |
| 90 | - log.info("[unregisterSession][注销设备会话,设备 ID: {},地址: {}]", deviceId, addressKey); | |
| 91 | - } | |
| 92 | - | |
| 93 | - /** | |
| 94 | - * 更新会话活跃时间(每次收到上行消息时调用) | |
| 95 | - * | |
| 96 | - * @param deviceId 设备 ID | |
| 97 | - */ | |
| 98 | - public void updateSessionActivity(Long deviceId) { | |
| 99 | - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | |
| 100 | - if (sessionInfo != null) { | |
| 101 | - sessionInfo.setLastActiveTime(LocalDateTime.now()); | |
| 102 | - } | |
| 71 | + public SessionInfo getSession(Long deviceId) { | |
| 72 | + return deviceSessionCache.getIfPresent(deviceId); | |
| 103 | 73 | } |
| 104 | 74 | |
| 105 | 75 | /** |
| 106 | 76 | * 更新设备会话地址(设备地址变更时调用) |
| 77 | + * <p> | |
| 78 | + * 注意:getIfPresent 已自动刷新过期时间,无需重新 put | |
| 107 | 79 | * |
| 108 | 80 | * @param deviceId 设备 ID |
| 109 | 81 | * @param newAddress 新地址 |
| 110 | 82 | */ |
| 111 | 83 | public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { |
| 112 | - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | |
| 84 | + // 地址未变化,无需更新 | |
| 85 | + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); | |
| 113 | 86 | if (sessionInfo == null) { |
| 114 | 87 | return; |
| 115 | 88 | } |
| 116 | - // 清理旧地址映射 | |
| 117 | - String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); | |
| 118 | - addressDeviceMap.remove(oldAddressKey, deviceId); | |
| 89 | + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) { | |
| 90 | + return; | |
| 91 | + } | |
| 119 | 92 | |
| 120 | - // 更新新地址 | |
| 121 | - String newAddressKey = buildAddressKey(newAddress); | |
| 93 | + // 更新地址 | |
| 94 | + String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); | |
| 122 | 95 | sessionInfo.setAddress(newAddress); |
| 123 | - sessionInfo.setLastActiveTime(LocalDateTime.now()); | |
| 124 | - addressDeviceMap.put(newAddressKey, deviceId); | |
| 125 | - log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},新地址: {}]", deviceId, newAddressKey); | |
| 126 | - } | |
| 127 | - | |
| 128 | - /** | |
| 129 | - * 获取会话信息 | |
| 130 | - */ | |
| 131 | - public SessionInfo getSessionInfo(Long deviceId) { | |
| 132 | - return deviceSessionMap.get(deviceId); | |
| 96 | + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]", | |
| 97 | + deviceId, oldAddressKey, buildAddressKey(newAddress)); | |
| 133 | 98 | } |
| 134 | 99 | |
| 135 | 100 | /** |
| ... | ... | @@ -141,7 +106,7 @@ public class IotUdpSessionManager { |
| 141 | 106 | * @return 是否发送成功 |
| 142 | 107 | */ |
| 143 | 108 | public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { |
| 144 | - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | |
| 109 | + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); | |
| 145 | 110 | if (sessionInfo == null || sessionInfo.getAddress() == null) { |
| 146 | 111 | log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); |
| 147 | 112 | return false; |
| ... | ... | @@ -165,38 +130,7 @@ public class IotUdpSessionManager { |
| 165 | 130 | } |
| 166 | 131 | |
| 167 | 132 | /** |
| 168 | - * 定期清理不活跃的设备会话 | |
| 169 | - * | |
| 170 | - * @param timeoutMs 超时时间(毫秒) | |
| 171 | - * @return 清理的设备 ID 列表(用于发送离线消息) | |
| 172 | - */ | |
| 173 | - public List<Long> cleanExpiredSessions(long timeoutMs) { | |
| 174 | - List<Long> offlineDeviceIds = new ArrayList<>(); | |
| 175 | - LocalDateTime now = LocalDateTime.now(); | |
| 176 | - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); | |
| 177 | - Iterator<Map.Entry<Long, SessionInfo>> iterator = deviceSessionMap.entrySet().iterator(); | |
| 178 | - // TODO @AI:改成 for each 会不会更好? | |
| 179 | - while (iterator.hasNext()) { | |
| 180 | - Map.Entry<Long, SessionInfo> entry = iterator.next(); | |
| 181 | - SessionInfo sessionInfo = entry.getValue(); | |
| 182 | - // 未过期,跳过 | |
| 183 | - if (sessionInfo.getLastActiveTime().isAfter(expireTime)) { | |
| 184 | - continue; | |
| 185 | - } | |
| 186 | - // 过期处理:记录离线设备 ID | |
| 187 | - Long deviceId = entry.getKey(); | |
| 188 | - String addressKey = buildAddressKey(sessionInfo.getAddress()); | |
| 189 | - addressDeviceMap.remove(addressKey, deviceId); | |
| 190 | - offlineDeviceIds.add(deviceId); | |
| 191 | - log.debug("[cleanExpiredSessions][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", | |
| 192 | - deviceId, addressKey, sessionInfo.getLastActiveTime()); | |
| 193 | - iterator.remove(); | |
| 194 | - } | |
| 195 | - return offlineDeviceIds; | |
| 196 | - } | |
| 197 | - | |
| 198 | - /** | |
| 199 | - * 构建地址 Key | |
| 133 | + * 构建地址 Key(用于日志输出) | |
| 200 | 134 | * |
| 201 | 135 | * @param address 地址 |
| 202 | 136 | * @return 地址 Key |
| ... | ... | @@ -206,7 +140,7 @@ public class IotUdpSessionManager { |
| 206 | 140 | } |
| 207 | 141 | |
| 208 | 142 | /** |
| 209 | - * 会话信息(包含认证信息) | |
| 143 | + * 会话信息 | |
| 210 | 144 | */ |
| 211 | 145 | @Data |
| 212 | 146 | public static class SessionInfo { |
| ... | ... | @@ -228,10 +162,6 @@ public class IotUdpSessionManager { |
| 228 | 162 | * 设备地址 |
| 229 | 163 | */ |
| 230 | 164 | private InetSocketAddress address; |
| 231 | - /** | |
| 232 | - * 最后活跃时间 | |
| 233 | - */ | |
| 234 | - private LocalDateTime lastActiveTime; | |
| 235 | 165 | |
| 236 | 166 | } |
| 237 | 167 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| ... | ... | @@ -81,13 +81,38 @@ yudao: |
| 81 | 81 | - id: udp-json |
| 82 | 82 | type: udp |
| 83 | 83 | port: 8093 |
| 84 | - enabled: true | |
| 84 | + enabled: false | |
| 85 | 85 | serialize: json |
| 86 | 86 | udp: |
| 87 | 87 | max-sessions: 1000 # 最大会话数 |
| 88 | 88 | session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期 |
| 89 | 89 | receive-buffer-size: 65536 # 接收缓冲区大小(字节) |
| 90 | 90 | send-buffer-size: 65536 # 发送缓冲区大小(字节) |
| 91 | + # ==================================== | |
| 92 | + # 针对引入的 WebSocket 组件的配置 | |
| 93 | + # ==================================== | |
| 94 | + - id: websocket-json | |
| 95 | + type: websocket | |
| 96 | + port: 8094 | |
| 97 | + enabled: false | |
| 98 | + serialize: json | |
| 99 | + websocket: | |
| 100 | + path: /ws | |
| 101 | + max-message-size: 65536 # 最大消息大小(字节,默认 64KB) | |
| 102 | + max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) | |
| 103 | + idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) | |
| 104 | + ssl-enabled: false # 是否启用 SSL(wss://) | |
| 105 | + # ==================================== | |
| 106 | + # 针对引入的 CoAP 组件的配置 | |
| 107 | + # ==================================== | |
| 108 | + - id: coap-json | |
| 109 | + type: coap | |
| 110 | + port: 5683 | |
| 111 | + enabled: true | |
| 112 | + coap: | |
| 113 | + max-message-size: 1024 # 最大消息大小(字节) | |
| 114 | + ack-timeout-ms: 2000 # ACK 超时时间(毫秒) | |
| 115 | + max-retransmit: 4 # 最大重传次数 | |
| 91 | 116 | |
| 92 | 117 | # 协议配置(旧版,保持兼容) |
| 93 | 118 | protocol: |
| ... | ... | @@ -134,27 +159,6 @@ yudao: |
| 134 | 159 | max-message-size: 8192 |
| 135 | 160 | connect-timeout-seconds: 60 |
| 136 | 161 | ssl-enabled: false |
| 137 | - # ==================================== | |
| 138 | - # 针对引入的 CoAP 组件的配置 | |
| 139 | - # ==================================== | |
| 140 | - coap: | |
| 141 | - enabled: false # 是否启用 CoAP 协议 | |
| 142 | - port: 5683 # CoAP 服务端口(默认 5683) | |
| 143 | - max-message-size: 1024 # 最大消息大小(字节) | |
| 144 | - ack-timeout: 2000 # ACK 超时时间(毫秒) | |
| 145 | - max-retransmit: 4 # 最大重传次数 | |
| 146 | - # ==================================== | |
| 147 | - # 针对引入的 WebSocket 组件的配置 | |
| 148 | - # ==================================== | |
| 149 | - websocket: | |
| 150 | - enabled: false # 是否启用 WebSocket 协议 | |
| 151 | - port: 8094 # WebSocket 服务端口(默认 8094) | |
| 152 | - path: /ws # WebSocket 路径(默认 /ws) | |
| 153 | - max-message-size: 65536 # 最大消息大小(字节,默认 64KB) | |
| 154 | - max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) | |
| 155 | - idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) | |
| 156 | - ssl-enabled: false # 是否启用 SSL(wss://) | |
| 157 | - | |
| 158 | 162 | --- #################### 日志相关配置 #################### |
| 159 | 163 | |
| 160 | 164 | # 基础日志配置 | ... | ... |