Commit ff2618bbf1f29b107936936ff9a420d7b83e4b8e
Committed by
Gitee
!1513 feat(iot):【协议改造】http、websocket、tcp、udp、coap、mqtt 初步改造
Merge pull request !1513 from 芋道源码/feature/iot-protocol
Showing
138 changed files
with
7708 additions
and
7679 deletions
Too many changes.
To preserve performance only 100 of 138 files are displayed.
yudao-module-iot/yudao-module-iot-biz/pom.xml
| ... | ... | @@ -101,18 +101,6 @@ |
| 101 | 101 | <artifactId>spring-boot-starter-amqp</artifactId> |
| 102 | 102 | <optional>true</optional> |
| 103 | 103 | </dependency> |
| 104 | - | |
| 105 | - <!-- IoT 网络组件:接收来自设备的上行数据 --> | |
| 106 | - <!-- <dependency>--> | |
| 107 | - <!-- <groupId>cn.iocoder.boot</groupId>--> | |
| 108 | - <!-- <artifactId>yudao-module-iot-net-component-http</artifactId>--> | |
| 109 | - <!-- <version>${revision}</version>--> | |
| 110 | - <!-- </dependency>--> | |
| 111 | - <!-- <dependency>--> | |
| 112 | - <!-- <groupId>cn.iocoder.boot</groupId>--> | |
| 113 | - <!-- <artifactId>yudao-module-iot-net-component-emqx</artifactId>--> | |
| 114 | - <!-- <version>${revision}</version>--> | |
| 115 | - <!-- </dependency>--> | |
| 116 | 104 | </dependencies> |
| 117 | 105 | |
| 118 | 106 | </project> | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| ... | ... | @@ -58,7 +58,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { |
| 58 | 58 | return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { |
| 59 | 59 | IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); |
| 60 | 60 | if (product != null) { |
| 61 | - deviceDTO.setCodecType(product.getCodecType()); | |
| 61 | + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType()); | |
| 62 | 62 | } |
| 63 | 63 | })); |
| 64 | 64 | } | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
| ... | ... | @@ -67,10 +67,15 @@ public class IotProductRespVO { |
| 67 | 67 | @DictFormat(DictTypeConstants.NET_TYPE) |
| 68 | 68 | private Integer netType; |
| 69 | 69 | |
| 70 | - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | |
| 71 | - @ExcelProperty(value = "数据格式", converter = DictConvert.class) | |
| 72 | - @DictFormat(DictTypeConstants.CODEC_TYPE) | |
| 73 | - private String codecType; | |
| 70 | + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") | |
| 71 | + @ExcelProperty(value = "协议类型", converter = DictConvert.class) | |
| 72 | + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) | |
| 73 | + private String protocolType; | |
| 74 | + | |
| 75 | + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") | |
| 76 | + @ExcelProperty(value = "序列化类型", converter = DictConvert.class) | |
| 77 | + @DictFormat(DictTypeConstants.SERIALIZE_TYPE) | |
| 78 | + private String serializeType; | |
| 74 | 79 | |
| 75 | 80 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 76 | 81 | @ExcelProperty("创建时间") | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.validation.InEnum; |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | |
| 4 | 6 | import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; |
| 5 | 7 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 6 | 8 | import io.swagger.v3.oas.annotations.media.Schema; |
| ... | ... | @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { |
| 44 | 46 | @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") |
| 45 | 47 | private Integer netType; |
| 46 | 48 | |
| 47 | - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | |
| 48 | - @NotEmpty(message = "数据格式不能为空") | |
| 49 | - private String codecType; | |
| 49 | + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") | |
| 50 | + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}") | |
| 51 | + @NotEmpty(message = "协议类型不能为空") | |
| 52 | + private String protocolType; | |
| 53 | + | |
| 54 | + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") | |
| 55 | + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}") | |
| 56 | + @NotEmpty(message = "序列化类型不能为空") | |
| 57 | + private String serializeType; | |
| 50 | 58 | |
| 51 | 59 | @Schema(description = "是否开启动态注册", example = "false") |
| 52 | 60 | @NotNull(message = "是否开启动态注册不能为空") | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
| ... | ... | @@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO { |
| 78 | 78 | */ |
| 79 | 79 | private Integer netType; |
| 80 | 80 | /** |
| 81 | - * 数据格式(编解码器类型) | |
| 81 | + * 协议类型 | |
| 82 | 82 | * <p> |
| 83 | - * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} | |
| 84 | - * | |
| 85 | - * 目的:用于 gateway-server 解析消息格式 | |
| 83 | + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum} | |
| 86 | 84 | */ |
| 87 | - private String codecType; | |
| 85 | + private String protocolType; | |
| 86 | + /** | |
| 87 | + * 序列化类型 | |
| 88 | + * <p> | |
| 89 | + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum} | |
| 90 | + */ | |
| 91 | + private String serializeType; | |
| 88 | 92 | |
| 89 | 93 | } |
| 90 | 94 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java
| ... | ... | @@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums; |
| 8 | 8 | public class DictTypeConstants { |
| 9 | 9 | |
| 10 | 10 | public static final String NET_TYPE = "iot_net_type"; |
| 11 | - public static final String LOCATION_TYPE = "iot_location_type"; | |
| 12 | - public static final String CODEC_TYPE = "iot_codec_type"; | |
| 11 | + public static final String PROTOCOL_TYPE = "iot_protocol_type"; | |
| 12 | + public static final String SERIALIZE_TYPE = "iot_serialize_type"; | |
| 13 | 13 | |
| 14 | 14 | public static final String PRODUCT_STATUS = "iot_product_status"; |
| 15 | 15 | public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
| ... | ... | @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; |
| 29 | 29 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 30 | 30 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; |
| 31 | 31 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 32 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 32 | 33 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 33 | 34 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; |
| 34 | 35 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| ... | ... | @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 819 | 820 | if (BooleanUtil.isFalse(product.getRegisterEnabled())) { |
| 820 | 821 | throw exception(DEVICE_REGISTER_DISABLED); |
| 821 | 822 | } |
| 822 | - // 1.3 验证 productSecret | |
| 823 | - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) { | |
| 823 | + // 1.3 【重要!!!】验证签名 | |
| 824 | + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(), | |
| 825 | + product.getProductSecret(), reqDTO.getSign())) { | |
| 824 | 826 | throw exception(DEVICE_REGISTER_SECRET_INVALID); |
| 825 | 827 | } |
| 826 | 828 | return TenantUtils.execute(product.getTenantId(), () -> { | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java
| ... | ... | @@ -34,8 +34,12 @@ public class IotDeviceRespDTO { |
| 34 | 34 | */ |
| 35 | 35 | private Long productId; |
| 36 | 36 | /** |
| 37 | - * 编解码器类型 | |
| 37 | + * 协议类型 | |
| 38 | 38 | */ |
| 39 | - private String codecType; | |
| 39 | + private String protocolType; | |
| 40 | + /** | |
| 41 | + * 序列化类型 | |
| 42 | + */ | |
| 43 | + private String serializeType; | |
| 40 | 44 | |
| 41 | 45 | } |
| 42 | 46 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.enums; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 4 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 5 | +import lombok.Getter; | |
| 6 | +import lombok.RequiredArgsConstructor; | |
| 7 | + | |
| 8 | +import java.util.Arrays; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT 协议类型枚举 | |
| 12 | + * | |
| 13 | + * 用于定义传输层协议类型 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@RequiredArgsConstructor | |
| 18 | +@Getter | |
| 19 | +public enum IotProtocolTypeEnum implements ArrayValuable<String> { | |
| 20 | + | |
| 21 | + TCP("tcp"), | |
| 22 | + UDP("udp"), | |
| 23 | + WEBSOCKET("websocket"), | |
| 24 | + HTTP("http"), | |
| 25 | + MQTT("mqtt"), | |
| 26 | + EMQX("emqx"), | |
| 27 | + COAP("coap"), | |
| 28 | + MODBUS_TCP("modbus_tcp"); | |
| 29 | + | |
| 30 | + public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 类型 | |
| 34 | + */ | |
| 35 | + private final String type; | |
| 36 | + | |
| 37 | + @Override | |
| 38 | + public String[] array() { | |
| 39 | + return ARRAYS; | |
| 40 | + } | |
| 41 | + | |
| 42 | + public static IotProtocolTypeEnum of(String type) { | |
| 43 | + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); | |
| 44 | + } | |
| 45 | + | |
| 46 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.enums; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 4 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 5 | +import lombok.Getter; | |
| 6 | +import lombok.RequiredArgsConstructor; | |
| 7 | + | |
| 8 | +import java.util.Arrays; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT 序列化类型枚举 | |
| 12 | + * | |
| 13 | + * 用于定义设备消息的序列化格式 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@RequiredArgsConstructor | |
| 18 | +@Getter | |
| 19 | +public enum IotSerializeTypeEnum implements ArrayValuable<String> { | |
| 20 | + | |
| 21 | + JSON("json"), | |
| 22 | + BINARY("binary"); | |
| 23 | + | |
| 24 | + public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new); | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 类型 | |
| 28 | + */ | |
| 29 | + private final String type; | |
| 30 | + | |
| 31 | + @Override | |
| 32 | + public String[] array() { | |
| 33 | + return ARRAYS; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public static IotSerializeTypeEnum of(String type) { | |
| 37 | + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); | |
| 38 | + } | |
| 39 | + | |
| 40 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java
| ... | ... | @@ -24,4 +24,14 @@ public interface IotMessageBus { |
| 24 | 24 | */ |
| 25 | 25 | void register(IotMessageSubscriber<?> subscriber); |
| 26 | 26 | |
| 27 | + /** | |
| 28 | + * 取消注册消息订阅者 | |
| 29 | + * | |
| 30 | + * @param subscriber 订阅者 | |
| 31 | + */ | |
| 32 | + default void unregister(IotMessageSubscriber<?> subscriber) { | |
| 33 | + // TODO 芋艿:暂时不实现,需求量不大,但是 | |
| 34 | + // throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现"); | |
| 35 | + } | |
| 36 | + | |
| 27 | 37 | } |
| 28 | 38 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java
| ... | ... | @@ -26,4 +26,16 @@ public interface IotMessageSubscriber<T> { |
| 26 | 26 | */ |
| 27 | 27 | void onMessage(T message); |
| 28 | 28 | |
| 29 | + /** | |
| 30 | + * 启动订阅 | |
| 31 | + */ | |
| 32 | + default void start() { | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 停止订阅 | |
| 37 | + */ | |
| 38 | + default void stop() { | |
| 39 | + } | |
| 40 | + | |
| 29 | 41 | } |
| 30 | 42 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
| ... | ... | @@ -60,7 +60,7 @@ public class IotDeviceMessage { |
| 60 | 60 | */ |
| 61 | 61 | private String serverId; |
| 62 | 62 | |
| 63 | - // ========== codec(编解码)字段 ========== | |
| 63 | + // ========== serialize(序列化)相关字段 ========== | |
| 64 | 64 | |
| 65 | 65 | /** |
| 66 | 66 | * 请求编号 |
| ... | ... | @@ -94,7 +94,7 @@ public class IotDeviceMessage { |
| 94 | 94 | */ |
| 95 | 95 | private String msg; |
| 96 | 96 | |
| 97 | - // ========== 基础方法:只传递"codec(编解码)字段" ========== | |
| 97 | + // ========== 基础方法:只传递"serialize(序列化)相关字段" ========== | |
| 98 | 98 | |
| 99 | 99 | public static IotDeviceMessage requestOf(String method) { |
| 100 | 100 | return requestOf(null, method, null); | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
| ... | ... | @@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO { |
| 27 | 27 | private String deviceName; |
| 28 | 28 | |
| 29 | 29 | /** |
| 30 | - * 产品密钥 | |
| 30 | + * 注册签名 | |
| 31 | + * | |
| 32 | + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String) | |
| 31 | 33 | */ |
| 32 | - @NotEmpty(message = "产品密钥不能为空") | |
| 33 | - private String productSecret; | |
| 34 | + @NotEmpty(message = "签名不能为空") | |
| 35 | + private String sign; | |
| 34 | 36 | |
| 35 | 37 | } | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.util; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.hutool.crypto.digest.DigestUtil; | |
| 5 | +import cn.hutool.crypto.digest.HmacAlgorithm; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT 产品【动态注册】认证工具类 | |
| 9 | + * <p> | |
| 10 | + * 用于一型一密场景,使用 productSecret 生成签名 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +public class IotProductAuthUtils { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 生成设备动态注册签名 | |
| 18 | + * | |
| 19 | + * @param productKey 产品标识 | |
| 20 | + * @param deviceName 设备名称 | |
| 21 | + * @param productSecret 产品密钥 | |
| 22 | + * @return 签名 | |
| 23 | + */ | |
| 24 | + public static String buildSign(String productKey, String deviceName, String productSecret) { | |
| 25 | + String content = buildContent(productKey, deviceName); | |
| 26 | + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret)) | |
| 27 | + .digestHex(content); | |
| 28 | + } | |
| 29 | + | |
| 30 | + /** | |
| 31 | + * 验证设备动态注册签名 | |
| 32 | + * | |
| 33 | + * @param productKey 产品标识 | |
| 34 | + * @param deviceName 设备名称 | |
| 35 | + * @param productSecret 产品密钥 | |
| 36 | + * @param sign 待验证的签名 | |
| 37 | + * @return 是否验证通过 | |
| 38 | + */ | |
| 39 | + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) { | |
| 40 | + String expectedSign = buildSign(productKey, deviceName, productSecret); | |
| 41 | + return expectedSign.equals(sign); | |
| 42 | + } | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 构建签名内容 | |
| 46 | + * | |
| 47 | + * @param productKey 产品标识 | |
| 48 | + * @param deviceName 设备名称 | |
| 49 | + * @return 签名内容 | |
| 50 | + */ | |
| 51 | + private static String buildContent(String productKey, String deviceName) { | |
| 52 | + return "deviceName" + deviceName + "productKey" + productKey; | |
| 53 | + } | |
| 54 | + | |
| 55 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.codec.alink; | |
| 2 | - | |
| 3 | -import cn.hutool.core.lang.Assert; | |
| 4 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | |
| 8 | -import lombok.AllArgsConstructor; | |
| 9 | -import lombok.Data; | |
| 10 | -import lombok.NoArgsConstructor; | |
| 11 | -import org.springframework.stereotype.Component; | |
| 12 | - | |
| 13 | -/** | |
| 14 | - * 阿里云 Alink {@link IotDeviceMessage} 的编解码器 | |
| 15 | - * | |
| 16 | - * @author 芋道源码 | |
| 17 | - */ | |
| 18 | -@Component | |
| 19 | -public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { | |
| 20 | - | |
| 21 | - public static final String TYPE = "Alink"; | |
| 22 | - | |
| 23 | - @Data | |
| 24 | - @NoArgsConstructor | |
| 25 | - @AllArgsConstructor | |
| 26 | - private static class AlinkMessage { | |
| 27 | - | |
| 28 | - public static final String VERSION_1 = "1.0"; | |
| 29 | - | |
| 30 | - /** | |
| 31 | - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 | |
| 32 | - */ | |
| 33 | - private String id; | |
| 34 | - | |
| 35 | - /** | |
| 36 | - * 版本号 | |
| 37 | - */ | |
| 38 | - private String version; | |
| 39 | - | |
| 40 | - /** | |
| 41 | - * 请求方法 | |
| 42 | - */ | |
| 43 | - private String method; | |
| 44 | - | |
| 45 | - /** | |
| 46 | - * 请求参数 | |
| 47 | - */ | |
| 48 | - private Object params; | |
| 49 | - | |
| 50 | - /** | |
| 51 | - * 响应结果 | |
| 52 | - */ | |
| 53 | - private Object data; | |
| 54 | - /** | |
| 55 | - * 响应错误码 | |
| 56 | - */ | |
| 57 | - private Integer code; | |
| 58 | - /** | |
| 59 | - * 响应提示 | |
| 60 | - * | |
| 61 | - * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。 | |
| 62 | - */ | |
| 63 | - private String msg; | |
| 64 | - | |
| 65 | - } | |
| 66 | - | |
| 67 | - @Override | |
| 68 | - public String type() { | |
| 69 | - return TYPE; | |
| 70 | - } | |
| 71 | - | |
| 72 | - @Override | |
| 73 | - public byte[] encode(IotDeviceMessage message) { | |
| 74 | - AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, | |
| 75 | - message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg()); | |
| 76 | - return JsonUtils.toJsonByte(alinkMessage); | |
| 77 | - } | |
| 78 | - | |
| 79 | - @Override | |
| 80 | - @SuppressWarnings("DataFlowIssue") | |
| 81 | - public IotDeviceMessage decode(byte[] bytes) { | |
| 82 | - AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class); | |
| 83 | - Assert.notNull(alinkMessage, "消息不能为空"); | |
| 84 | - Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0"); | |
| 85 | - return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(), | |
| 86 | - alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); | |
| 87 | - } | |
| 88 | - | |
| 89 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java deleted
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java deleted
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | - | |
| 3 | -import cn.hutool.core.lang.Assert; | |
| 4 | -import cn.hutool.core.util.StrUtil; | |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | |
| 8 | -import lombok.AllArgsConstructor; | |
| 9 | -import lombok.Data; | |
| 10 | -import lombok.NoArgsConstructor; | |
| 11 | -import org.springframework.stereotype.Component; | |
| 12 | - | |
| 13 | -/** | |
| 14 | - * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器 | |
| 15 | - * | |
| 16 | - * 采用纯 JSON 格式传输,格式如下: | |
| 17 | - * { | |
| 18 | - * "id": "消息 ID", | |
| 19 | - * "method": "消息方法", | |
| 20 | - * "params": {...}, // 请求参数 | |
| 21 | - * "data": {...}, // 响应结果 | |
| 22 | - * "code": 200, // 响应错误码 | |
| 23 | - * "msg": "success", // 响应提示 | |
| 24 | - * "timestamp": 时间戳 | |
| 25 | - * } | |
| 26 | - * | |
| 27 | - * @author 芋道源码 | |
| 28 | - */ | |
| 29 | -@Component | |
| 30 | -public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | |
| 31 | - | |
| 32 | - public static final String TYPE = "TCP_JSON"; | |
| 33 | - | |
| 34 | - @Data | |
| 35 | - @NoArgsConstructor | |
| 36 | - @AllArgsConstructor | |
| 37 | - private static class TcpJsonMessage { | |
| 38 | - | |
| 39 | - /** | |
| 40 | - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 | |
| 41 | - */ | |
| 42 | - private String id; | |
| 43 | - | |
| 44 | - /** | |
| 45 | - * 请求方法 | |
| 46 | - */ | |
| 47 | - private String method; | |
| 48 | - | |
| 49 | - /** | |
| 50 | - * 请求参数 | |
| 51 | - */ | |
| 52 | - private Object params; | |
| 53 | - | |
| 54 | - /** | |
| 55 | - * 响应结果 | |
| 56 | - */ | |
| 57 | - private Object data; | |
| 58 | - | |
| 59 | - /** | |
| 60 | - * 响应错误码 | |
| 61 | - */ | |
| 62 | - private Integer code; | |
| 63 | - | |
| 64 | - /** | |
| 65 | - * 响应提示 | |
| 66 | - */ | |
| 67 | - private String msg; | |
| 68 | - | |
| 69 | - /** | |
| 70 | - * 时间戳 | |
| 71 | - */ | |
| 72 | - private Long timestamp; | |
| 73 | - | |
| 74 | - } | |
| 75 | - | |
| 76 | - @Override | |
| 77 | - public String type() { | |
| 78 | - return TYPE; | |
| 79 | - } | |
| 80 | - | |
| 81 | - @Override | |
| 82 | - public byte[] encode(IotDeviceMessage message) { | |
| 83 | - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( | |
| 84 | - message.getRequestId(), | |
| 85 | - message.getMethod(), | |
| 86 | - message.getParams(), | |
| 87 | - message.getData(), | |
| 88 | - message.getCode(), | |
| 89 | - message.getMsg(), | |
| 90 | - System.currentTimeMillis()); | |
| 91 | - return JsonUtils.toJsonByte(tcpJsonMessage); | |
| 92 | - } | |
| 93 | - | |
| 94 | - @Override | |
| 95 | - @SuppressWarnings("DataFlowIssue") | |
| 96 | - public IotDeviceMessage decode(byte[] bytes) { | |
| 97 | - String jsonStr = StrUtil.utf8Str(bytes).trim(); | |
| 98 | - TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class); | |
| 99 | - Assert.notNull(tcpJsonMessage, "消息不能为空"); | |
| 100 | - Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); | |
| 101 | - return IotDeviceMessage.of( | |
| 102 | - tcpJsonMessage.getId(), | |
| 103 | - tcpJsonMessage.getMethod(), | |
| 104 | - tcpJsonMessage.getParams(), | |
| 105 | - tcpJsonMessage.getData(), | |
| 106 | - tcpJsonMessage.getCode(), | |
| 107 | - tcpJsonMessage.getMsg()); | |
| 108 | - } | |
| 109 | - | |
| 110 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; | |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; | |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | |
| 13 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 14 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; | |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | |
| 17 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 18 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; | |
| 19 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; | |
| 20 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | |
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; | |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; | |
| 23 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; | |
| 24 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 25 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 26 | -import io.vertx.core.Vertx; | |
| 27 | -import lombok.extern.slf4j.Slf4j; | |
| 28 | -import org.springframework.beans.factory.annotation.Qualifier; | |
| 29 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
| 3 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | |
| 30 | 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 31 | 6 | import org.springframework.context.annotation.Bean; |
| 32 | 7 | import org.springframework.context.annotation.Configuration; |
| 33 | 8 | |
| 34 | 9 | @Configuration |
| 35 | 10 | @EnableConfigurationProperties(IotGatewayProperties.class) |
| 36 | -@Slf4j | |
| 37 | 11 | public class IotGatewayConfiguration { |
| 38 | 12 | |
| 39 | - /** | |
| 40 | - * IoT 网关 HTTP 协议配置类 | |
| 41 | - */ | |
| 42 | - @Configuration | |
| 43 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true") | |
| 44 | - @Slf4j | |
| 45 | - public static class HttpProtocolConfiguration { | |
| 46 | - | |
| 47 | - @Bean(name = "httpVertx", destroyMethod = "close") | |
| 48 | - public Vertx httpVertx() { | |
| 49 | - return Vertx.vertx(); | |
| 50 | - } | |
| 51 | - | |
| 52 | - @Bean | |
| 53 | - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 54 | - @Qualifier("httpVertx") Vertx httpVertx) { | |
| 55 | - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx); | |
| 56 | - } | |
| 57 | - | |
| 58 | - @Bean | |
| 59 | - public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, | |
| 60 | - IotMessageBus messageBus) { | |
| 61 | - return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); | |
| 62 | - } | |
| 63 | - } | |
| 64 | - | |
| 65 | - /** | |
| 66 | - * IoT 网关 EMQX 协议配置类 | |
| 67 | - */ | |
| 68 | - @Configuration | |
| 69 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") | |
| 70 | - @Slf4j | |
| 71 | - public static class EmqxProtocolConfiguration { | |
| 72 | - | |
| 73 | - @Bean(name = "emqxVertx", destroyMethod = "close") | |
| 74 | - public Vertx emqxVertx() { | |
| 75 | - return Vertx.vertx(); | |
| 76 | - } | |
| 77 | - | |
| 78 | - @Bean | |
| 79 | - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, | |
| 80 | - @Qualifier("emqxVertx") Vertx emqxVertx) { | |
| 81 | - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); | |
| 82 | - } | |
| 83 | - | |
| 84 | - @Bean | |
| 85 | - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 86 | - @Qualifier("emqxVertx") Vertx emqxVertx) { | |
| 87 | - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); | |
| 88 | - } | |
| 89 | - | |
| 90 | - @Bean | |
| 91 | - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, | |
| 92 | - IotMessageBus messageBus) { | |
| 93 | - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); | |
| 94 | - } | |
| 13 | + @Bean | |
| 14 | + public IotMessageSerializerManager iotMessageSerializerManager() { | |
| 15 | + return new IotMessageSerializerManager(); | |
| 95 | 16 | } |
| 96 | 17 | |
| 97 | - /** | |
| 98 | - * IoT 网关 TCP 协议配置类 | |
| 99 | - */ | |
| 100 | - @Configuration | |
| 101 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true") | |
| 102 | - @Slf4j | |
| 103 | - public static class TcpProtocolConfiguration { | |
| 104 | - | |
| 105 | - @Bean(name = "tcpVertx", destroyMethod = "close") | |
| 106 | - public Vertx tcpVertx() { | |
| 107 | - return Vertx.vertx(); | |
| 108 | - } | |
| 109 | - | |
| 110 | - @Bean | |
| 111 | - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 112 | - IotDeviceService deviceService, | |
| 113 | - IotDeviceMessageService messageService, | |
| 114 | - IotTcpConnectionManager connectionManager, | |
| 115 | - @Qualifier("tcpVertx") Vertx tcpVertx) { | |
| 116 | - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), | |
| 117 | - deviceService, messageService, connectionManager, tcpVertx); | |
| 118 | - } | |
| 119 | - | |
| 120 | - @Bean | |
| 121 | - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, | |
| 122 | - IotDeviceMessageService messageService, | |
| 123 | - IotTcpConnectionManager connectionManager, | |
| 124 | - IotMessageBus messageBus) { | |
| 125 | - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); | |
| 126 | - } | |
| 127 | - | |
| 128 | - } | |
| 129 | - | |
| 130 | - /** | |
| 131 | - * IoT 网关 MQTT 协议配置类 | |
| 132 | - */ | |
| 133 | - @Configuration | |
| 134 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") | |
| 135 | - @Slf4j | |
| 136 | - public static class MqttProtocolConfiguration { | |
| 137 | - | |
| 138 | - @Bean(name = "mqttVertx", destroyMethod = "close") | |
| 139 | - public Vertx mqttVertx() { | |
| 140 | - return Vertx.vertx(); | |
| 141 | - } | |
| 142 | - | |
| 143 | - @Bean | |
| 144 | - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 145 | - IotDeviceMessageService messageService, | |
| 146 | - IotMqttConnectionManager connectionManager, | |
| 147 | - @Qualifier("mqttVertx") Vertx mqttVertx) { | |
| 148 | - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, | |
| 149 | - connectionManager, mqttVertx); | |
| 150 | - } | |
| 151 | - | |
| 152 | - @Bean | |
| 153 | - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, | |
| 154 | - IotMqttConnectionManager connectionManager) { | |
| 155 | - return new IotMqttDownstreamHandler(messageService, connectionManager); | |
| 156 | - } | |
| 157 | - | |
| 158 | - @Bean | |
| 159 | - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, | |
| 160 | - IotMqttDownstreamHandler downstreamHandler, | |
| 161 | - IotMessageBus messageBus) { | |
| 162 | - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); | |
| 163 | - } | |
| 164 | - | |
| 165 | - } | |
| 166 | - | |
| 167 | - /** | |
| 168 | - * IoT 网关 UDP 协议配置类 | |
| 169 | - */ | |
| 170 | - @Configuration | |
| 171 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true") | |
| 172 | - @Slf4j | |
| 173 | - public static class UdpProtocolConfiguration { | |
| 174 | - | |
| 175 | - @Bean(name = "udpVertx", destroyMethod = "close") | |
| 176 | - public Vertx udpVertx() { | |
| 177 | - return Vertx.vertx(); | |
| 178 | - } | |
| 179 | - | |
| 180 | - @Bean | |
| 181 | - public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 182 | - IotDeviceService deviceService, | |
| 183 | - IotDeviceMessageService messageService, | |
| 184 | - IotUdpSessionManager sessionManager, | |
| 185 | - @Qualifier("udpVertx") Vertx udpVertx) { | |
| 186 | - return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(), | |
| 187 | - deviceService, messageService, sessionManager, udpVertx); | |
| 188 | - } | |
| 189 | - | |
| 190 | - @Bean | |
| 191 | - public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, | |
| 192 | - IotDeviceMessageService messageService, | |
| 193 | - IotUdpSessionManager sessionManager, | |
| 194 | - IotMessageBus messageBus) { | |
| 195 | - return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus); | |
| 196 | - } | |
| 197 | - | |
| 198 | - } | |
| 199 | - | |
| 200 | - /** | |
| 201 | - * IoT 网关 CoAP 协议配置类 | |
| 202 | - */ | |
| 203 | - @Configuration | |
| 204 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true") | |
| 205 | - @Slf4j | |
| 206 | - public static class CoapProtocolConfiguration { | |
| 207 | - | |
| 208 | - @Bean | |
| 209 | - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) { | |
| 210 | - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap()); | |
| 211 | - } | |
| 212 | - | |
| 213 | - @Bean | |
| 214 | - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol, | |
| 215 | - IotMessageBus messageBus) { | |
| 216 | - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus); | |
| 217 | - } | |
| 218 | - | |
| 219 | - } | |
| 220 | - | |
| 221 | - /** | |
| 222 | - * IoT 网关 WebSocket 协议配置类 | |
| 223 | - */ | |
| 224 | - @Configuration | |
| 225 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true") | |
| 226 | - @Slf4j | |
| 227 | - public static class WebSocketProtocolConfiguration { | |
| 228 | - | |
| 229 | - @Bean(name = "websocketVertx", destroyMethod = "close") | |
| 230 | - public Vertx websocketVertx() { | |
| 231 | - return Vertx.vertx(); | |
| 232 | - } | |
| 233 | - | |
| 234 | - @Bean | |
| 235 | - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties, | |
| 236 | - IotDeviceService deviceService, | |
| 237 | - IotDeviceMessageService messageService, | |
| 238 | - IotWebSocketConnectionManager connectionManager, | |
| 239 | - @Qualifier("websocketVertx") Vertx websocketVertx) { | |
| 240 | - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(), | |
| 241 | - deviceService, messageService, connectionManager, websocketVertx); | |
| 242 | - } | |
| 243 | - | |
| 244 | - @Bean | |
| 245 | - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler, | |
| 246 | - IotDeviceMessageService messageService, | |
| 247 | - IotWebSocketConnectionManager connectionManager, | |
| 248 | - IotMessageBus messageBus) { | |
| 249 | - return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); | |
| 250 | - } | |
| 251 | - | |
| 18 | + @Bean | |
| 19 | + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) { | |
| 20 | + return new IotProtocolManager(gatewayProperties); | |
| 252 | 21 | } |
| 253 | 22 | |
| 254 | 23 | } | ... | ... |
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 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; | |
| 11 | +import jakarta.validation.Valid; | |
| 3 | 12 | import jakarta.validation.constraints.NotEmpty; |
| 4 | 13 | import jakarta.validation.constraints.NotNull; |
| 5 | 14 | import lombok.Data; |
| ... | ... | @@ -24,9 +33,9 @@ public class IotGatewayProperties { |
| 24 | 33 | private TokenProperties token; |
| 25 | 34 | |
| 26 | 35 | /** |
| 27 | - * 协议配置 | |
| 36 | + * 协议实例列表 | |
| 28 | 37 | */ |
| 29 | - private ProtocolProperties protocol; | |
| 38 | + private List<ProtocolProperties> protocols; | |
| 30 | 39 | |
| 31 | 40 | @Data |
| 32 | 41 | public static class RpcProperties { |
| ... | ... | @@ -65,581 +74,145 @@ public class IotGatewayProperties { |
| 65 | 74 | |
| 66 | 75 | } |
| 67 | 76 | |
| 77 | + /** | |
| 78 | + * 协议实例配置 | |
| 79 | + */ | |
| 68 | 80 | @Data |
| 69 | 81 | public static class ProtocolProperties { |
| 70 | 82 | |
| 71 | 83 | /** |
| 72 | - * HTTP 组件配置 | |
| 73 | - */ | |
| 74 | - private HttpProperties http; | |
| 75 | - | |
| 76 | - /** | |
| 77 | - * EMQX 组件配置 | |
| 78 | - */ | |
| 79 | - private EmqxProperties emqx; | |
| 80 | - | |
| 81 | - /** | |
| 82 | - * TCP 组件配置 | |
| 84 | + * 协议实例 ID,如 "http-alink"、"tcp-binary" | |
| 83 | 85 | */ |
| 84 | - private TcpProperties tcp; | |
| 85 | - | |
| 86 | - /** | |
| 87 | - * MQTT 组件配置 | |
| 88 | - */ | |
| 89 | - private MqttProperties mqtt; | |
| 90 | - | |
| 86 | + @NotEmpty(message = "协议实例 ID 不能为空") | |
| 87 | + private String id; | |
| 91 | 88 | /** |
| 92 | - * MQTT WebSocket 组件配置 | |
| 89 | + * 是否启用 | |
| 93 | 90 | */ |
| 94 | - private MqttWsProperties mqttWs; | |
| 95 | - | |
| 91 | + @NotNull(message = "是否启用不能为空") | |
| 92 | + private Boolean enabled = true; | |
| 96 | 93 | /** |
| 97 | - * UDP 组件配置 | |
| 94 | + * 协议类型 | |
| 95 | + * | |
| 96 | + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum | |
| 98 | 97 | */ |
| 99 | - private UdpProperties udp; | |
| 100 | - | |
| 101 | - /** | |
| 102 | - * CoAP 组件配置 | |
| 103 | - */ | |
| 104 | - private CoapProperties coap; | |
| 105 | - | |
| 106 | - /** | |
| 107 | - * WebSocket 组件配置 | |
| 108 | - */ | |
| 109 | - private WebSocketProperties websocket; | |
| 110 | - | |
| 111 | - } | |
| 112 | - | |
| 113 | - @Data | |
| 114 | - public static class HttpProperties { | |
| 115 | - | |
| 116 | - /** | |
| 117 | - * 是否开启 | |
| 118 | - */ | |
| 119 | - @NotNull(message = "是否开启不能为空") | |
| 120 | - private Boolean enabled; | |
| 98 | + @NotEmpty(message = "协议类型不能为空") | |
| 99 | + private String protocol; | |
| 121 | 100 | /** |
| 122 | 101 | * 服务端口 |
| 123 | - */ | |
| 124 | - private Integer serverPort; | |
| 125 | - | |
| 126 | - /** | |
| 127 | - * 是否开启 SSL | |
| 128 | - */ | |
| 129 | - @NotNull(message = "是否开启 SSL 不能为空") | |
| 130 | - private Boolean sslEnabled = false; | |
| 131 | - | |
| 132 | - /** | |
| 133 | - * SSL 证书路径 | |
| 134 | - */ | |
| 135 | - private String sslKeyPath; | |
| 136 | - /** | |
| 137 | - * SSL 证书路径 | |
| 138 | - */ | |
| 139 | - private String sslCertPath; | |
| 140 | - | |
| 141 | - } | |
| 142 | - | |
| 143 | - @Data | |
| 144 | - public static class EmqxProperties { | |
| 145 | - | |
| 146 | - /** | |
| 147 | - * 是否开启 | |
| 148 | - */ | |
| 149 | - @NotNull(message = "是否开启不能为空") | |
| 150 | - private Boolean enabled; | |
| 151 | - | |
| 152 | - /** | |
| 153 | - * HTTP 服务端口(默认:8090) | |
| 154 | - */ | |
| 155 | - private Integer httpPort = 8090; | |
| 156 | - | |
| 157 | - /** | |
| 158 | - * MQTT 服务器地址 | |
| 159 | - */ | |
| 160 | - @NotEmpty(message = "MQTT 服务器地址不能为空") | |
| 161 | - private String mqttHost; | |
| 162 | - | |
| 163 | - /** | |
| 164 | - * MQTT 服务器端口(默认:1883) | |
| 165 | - */ | |
| 166 | - @NotNull(message = "MQTT 服务器端口不能为空") | |
| 167 | - private Integer mqttPort = 1883; | |
| 168 | - | |
| 169 | - /** | |
| 170 | - * MQTT 用户名 | |
| 171 | - */ | |
| 172 | - @NotEmpty(message = "MQTT 用户名不能为空") | |
| 173 | - private String mqttUsername; | |
| 174 | - | |
| 175 | - /** | |
| 176 | - * MQTT 密码 | |
| 177 | - */ | |
| 178 | - @NotEmpty(message = "MQTT 密码不能为空") | |
| 179 | - private String mqttPassword; | |
| 180 | - | |
| 181 | - /** | |
| 182 | - * MQTT 客户端的 SSL 开关 | |
| 183 | - */ | |
| 184 | - @NotNull(message = "MQTT 是否开启 SSL 不能为空") | |
| 185 | - private Boolean mqttSsl = false; | |
| 186 | - | |
| 187 | - /** | |
| 188 | - * MQTT 客户端 ID(如果为空,系统将自动生成) | |
| 189 | - */ | |
| 190 | - @NotEmpty(message = "MQTT 客户端 ID 不能为空") | |
| 191 | - private String mqttClientId; | |
| 192 | - | |
| 193 | - /** | |
| 194 | - * MQTT 订阅的主题 | |
| 195 | - */ | |
| 196 | - @NotEmpty(message = "MQTT 主题不能为空") | |
| 197 | - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; | |
| 198 | - | |
| 199 | - /** | |
| 200 | - * 默认 QoS 级别 | |
| 201 | 102 | * <p> |
| 202 | - * 0 - 最多一次 | |
| 203 | - * 1 - 至少一次 | |
| 204 | - * 2 - 刚好一次 | |
| 205 | - */ | |
| 206 | - private Integer mqttQos = 1; | |
| 207 | - | |
| 208 | - /** | |
| 209 | - * 连接超时时间(秒) | |
| 210 | - */ | |
| 211 | - private Integer connectTimeoutSeconds = 10; | |
| 212 | - | |
| 213 | - /** | |
| 214 | - * 重连延迟时间(毫秒) | |
| 215 | - */ | |
| 216 | - private Long reconnectDelayMs = 5000L; | |
| 217 | - | |
| 218 | - /** | |
| 219 | - * 是否启用 Clean Session (清理会话) | |
| 220 | - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 | |
| 221 | - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 | |
| 222 | - */ | |
| 223 | - private Boolean cleanSession = true; | |
| 224 | - | |
| 225 | - /** | |
| 226 | - * 心跳间隔(秒) | |
| 227 | - * 用于保持连接活性,及时发现网络中断。 | |
| 228 | - */ | |
| 229 | - private Integer keepAliveIntervalSeconds = 60; | |
| 230 | - | |
| 231 | - /** | |
| 232 | - * 最大未确认消息队列大小 | |
| 233 | - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 | |
| 234 | - */ | |
| 235 | - private Integer maxInflightQueue = 10000; | |
| 236 | - | |
| 237 | - /** | |
| 238 | - * 是否信任所有 SSL 证书 | |
| 239 | - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! | |
| 240 | - * 在生产环境中,应设置为 false,并配置正确的信任库。 | |
| 241 | - */ | |
| 242 | - private Boolean trustAll = false; | |
| 243 | - | |
| 244 | - /** | |
| 245 | - * 遗嘱消息配置 (用于网关异常下线时通知其他系统) | |
| 246 | - */ | |
| 247 | - private final Will will = new Will(); | |
| 248 | - | |
| 249 | - /** | |
| 250 | - * 高级 SSL/TLS 配置 (用于生产环境) | |
| 103 | + * 不同协议含义不同: | |
| 104 | + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口 | |
| 105 | + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event) | |
| 251 | 106 | */ |
| 252 | - private final Ssl sslOptions = new Ssl(); | |
| 253 | - | |
| 107 | + @NotNull(message = "服务端口不能为空") | |
| 108 | + private Integer port; | |
| 254 | 109 | /** |
| 255 | - * 遗嘱消息 (Last Will and Testament) | |
| 110 | + * 序列化类型(可选) | |
| 111 | + * | |
| 112 | + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum | |
| 113 | + * | |
| 114 | + * 为什么是可选的呢? | |
| 115 | + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 | |
| 116 | + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 | |
| 256 | 117 | */ |
| 257 | - @Data | |
| 258 | - public static class Will { | |
| 259 | - | |
| 260 | - /** | |
| 261 | - * 是否启用遗嘱消息 | |
| 262 | - */ | |
| 263 | - private boolean enabled = false; | |
| 264 | - /** | |
| 265 | - * 遗嘱消息主题 | |
| 266 | - */ | |
| 267 | - private String topic; | |
| 268 | - /** | |
| 269 | - * 遗嘱消息内容 | |
| 270 | - */ | |
| 271 | - private String payload; | |
| 272 | - /** | |
| 273 | - * 遗嘱消息 QoS 等级 | |
| 274 | - */ | |
| 275 | - private Integer qos = 1; | |
| 276 | - /** | |
| 277 | - * 遗嘱消息是否作为保留消息发布 | |
| 278 | - */ | |
| 279 | - private boolean retain = true; | |
| 118 | + private String serialize; | |
| 280 | 119 | |
| 281 | - } | |
| 120 | + // ========== SSL 配置 ========== | |
| 282 | 121 | |
| 283 | 122 | /** |
| 284 | - * 高级 SSL/TLS 配置 | |
| 123 | + * SSL 配置(可选,配置文件中不配置则为 null) | |
| 285 | 124 | */ |
| 286 | - @Data | |
| 287 | - public static class Ssl { | |
| 125 | + @Valid | |
| 126 | + private SslConfig ssl; | |
| 288 | 127 | |
| 289 | - /** | |
| 290 | - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks | |
| 291 | - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 | |
| 292 | - */ | |
| 293 | - private String keyStorePath; | |
| 294 | - /** | |
| 295 | - * 密钥库密码 | |
| 296 | - */ | |
| 297 | - private String keyStorePassword; | |
| 298 | - /** | |
| 299 | - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks | |
| 300 | - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 | |
| 301 | - */ | |
| 302 | - private String trustStorePath; | |
| 303 | - /** | |
| 304 | - * 信任库密码 | |
| 305 | - */ | |
| 306 | - private String trustStorePassword; | |
| 307 | - | |
| 308 | - } | |
| 309 | - | |
| 310 | - } | |
| 311 | - | |
| 312 | - @Data | |
| 313 | - public static class TcpProperties { | |
| 128 | + // ========== 各协议配置 ========== | |
| 314 | 129 | |
| 315 | 130 | /** |
| 316 | - * 是否开启 | |
| 131 | + * HTTP 协议配置 | |
| 317 | 132 | */ |
| 318 | - @NotNull(message = "是否开启不能为空") | |
| 319 | - private Boolean enabled; | |
| 320 | - | |
| 133 | + @Valid | |
| 134 | + private IotHttpConfig http; | |
| 321 | 135 | /** |
| 322 | - * 服务器端口 | |
| 136 | + * WebSocket 协议配置 | |
| 323 | 137 | */ |
| 324 | - private Integer port = 8091; | |
| 138 | + @Valid | |
| 139 | + private IotWebSocketConfig websocket; | |
| 325 | 140 | |
| 326 | 141 | /** |
| 327 | - * 心跳超时时间(毫秒) | |
| 142 | + * TCP 协议配置 | |
| 328 | 143 | */ |
| 329 | - private Long keepAliveTimeoutMs = 30000L; | |
| 330 | - | |
| 144 | + @Valid | |
| 145 | + private IotTcpConfig tcp; | |
| 331 | 146 | /** |
| 332 | - * 最大连接数 | |
| 147 | + * UDP 协议配置 | |
| 333 | 148 | */ |
| 334 | - private Integer maxConnections = 1000; | |
| 149 | + @Valid | |
| 150 | + private IotUdpConfig udp; | |
| 335 | 151 | |
| 336 | 152 | /** |
| 337 | - * 是否启用SSL | |
| 153 | + * CoAP 协议配置 | |
| 338 | 154 | */ |
| 339 | - private Boolean sslEnabled = false; | |
| 155 | + @Valid | |
| 156 | + private IotCoapConfig coap; | |
| 340 | 157 | |
| 341 | 158 | /** |
| 342 | - * SSL证书路径 | |
| 159 | + * MQTT 协议配置 | |
| 343 | 160 | */ |
| 344 | - private String sslCertPath; | |
| 345 | - | |
| 161 | + @Valid | |
| 162 | + private IotMqttConfig mqtt; | |
| 346 | 163 | /** |
| 347 | - * SSL私钥路径 | |
| 164 | + * EMQX 协议配置 | |
| 348 | 165 | */ |
| 349 | - private String sslKeyPath; | |
| 166 | + @Valid | |
| 167 | + private IotEmqxConfig emqx; | |
| 350 | 168 | |
| 351 | 169 | } |
| 352 | 170 | |
| 171 | + /** | |
| 172 | + * SSL 配置 | |
| 173 | + */ | |
| 353 | 174 | @Data |
| 354 | - public static class MqttProperties { | |
| 355 | - | |
| 356 | - /** | |
| 357 | - * 是否开启 | |
| 358 | - */ | |
| 359 | - @NotNull(message = "是否开启不能为空") | |
| 360 | - private Boolean enabled; | |
| 361 | - | |
| 362 | - /** | |
| 363 | - * 服务器端口 | |
| 364 | - */ | |
| 365 | - private Integer port = 1883; | |
| 366 | - | |
| 367 | - /** | |
| 368 | - * 最大消息大小(字节) | |
| 369 | - */ | |
| 370 | - private Integer maxMessageSize = 8192; | |
| 371 | - | |
| 372 | - /** | |
| 373 | - * 连接超时时间(秒) | |
| 374 | - */ | |
| 375 | - private Integer connectTimeoutSeconds = 60; | |
| 376 | - /** | |
| 377 | - * 保持连接超时时间(秒) | |
| 378 | - */ | |
| 379 | - private Integer keepAliveTimeoutSeconds = 300; | |
| 175 | + public static class SslConfig { | |
| 380 | 176 | |
| 381 | 177 | /** |
| 382 | 178 | * 是否启用 SSL |
| 383 | 179 | */ |
| 384 | - private Boolean sslEnabled = false; | |
| 385 | - /** | |
| 386 | - * SSL 配置 | |
| 387 | - */ | |
| 388 | - private SslOptions sslOptions = new SslOptions(); | |
| 389 | - | |
| 390 | - /** | |
| 391 | - * SSL 配置选项 | |
| 392 | - */ | |
| 393 | - @Data | |
| 394 | - public static class SslOptions { | |
| 395 | - | |
| 396 | - /** | |
| 397 | - * 密钥证书选项 | |
| 398 | - */ | |
| 399 | - private io.vertx.core.net.KeyCertOptions keyCertOptions; | |
| 400 | - /** | |
| 401 | - * 信任选项 | |
| 402 | - */ | |
| 403 | - private io.vertx.core.net.TrustOptions trustOptions; | |
| 404 | - /** | |
| 405 | - * SSL 证书路径 | |
| 406 | - */ | |
| 407 | - private String certPath; | |
| 408 | - /** | |
| 409 | - * SSL 私钥路径 | |
| 410 | - */ | |
| 411 | - private String keyPath; | |
| 412 | - /** | |
| 413 | - * 信任存储路径 | |
| 414 | - */ | |
| 415 | - private String trustStorePath; | |
| 416 | - /** | |
| 417 | - * 信任存储密码 | |
| 418 | - */ | |
| 419 | - private String trustStorePassword; | |
| 420 | - | |
| 421 | - } | |
| 422 | - | |
| 423 | - } | |
| 424 | - | |
| 425 | - @Data | |
| 426 | - public static class MqttWsProperties { | |
| 180 | + @NotNull(message = "是否启用 SSL 不能为空") | |
| 181 | + private Boolean ssl = false; | |
| 427 | 182 | |
| 428 | 183 | /** |
| 429 | - * 是否开启 | |
| 430 | - */ | |
| 431 | - @NotNull(message = "是否开启不能为空") | |
| 432 | - private Boolean enabled; | |
| 433 | - | |
| 434 | - /** | |
| 435 | - * WebSocket 服务器端口(默认:8083) | |
| 436 | - */ | |
| 437 | - private Integer port = 8083; | |
| 438 | - | |
| 439 | - /** | |
| 440 | - * WebSocket 路径(默认:/mqtt) | |
| 441 | - */ | |
| 442 | - @NotEmpty(message = "WebSocket 路径不能为空") | |
| 443 | - private String path = "/mqtt"; | |
| 444 | - | |
| 445 | - /** | |
| 446 | - * 最大消息大小(字节) | |
| 447 | - */ | |
| 448 | - private Integer maxMessageSize = 8192; | |
| 449 | - | |
| 450 | - /** | |
| 451 | - * 连接超时时间(秒) | |
| 452 | - */ | |
| 453 | - private Integer connectTimeoutSeconds = 60; | |
| 454 | - | |
| 455 | - /** | |
| 456 | - * 保持连接超时时间(秒) | |
| 457 | - */ | |
| 458 | - private Integer keepAliveTimeoutSeconds = 300; | |
| 459 | - | |
| 460 | - /** | |
| 461 | - * 是否启用 SSL(wss://) | |
| 462 | - */ | |
| 463 | - private Boolean sslEnabled = false; | |
| 464 | - | |
| 465 | - /** | |
| 466 | - * SSL 配置 | |
| 467 | - */ | |
| 468 | - private SslOptions sslOptions = new SslOptions(); | |
| 469 | - | |
| 470 | - /** | |
| 471 | - * WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1") | |
| 472 | - */ | |
| 473 | - @NotEmpty(message = "WebSocket 子协议不能为空") | |
| 474 | - private String subProtocol = "mqtt"; | |
| 475 | - | |
| 476 | - /** | |
| 477 | - * 最大帧大小(字节) | |
| 478 | - */ | |
| 479 | - private Integer maxFrameSize = 65536; | |
| 480 | - | |
| 481 | - /** | |
| 482 | - * SSL 配置选项 | |
| 483 | - */ | |
| 484 | - @Data | |
| 485 | - public static class SslOptions { | |
| 486 | - | |
| 487 | - /** | |
| 488 | - * 密钥证书选项 | |
| 489 | - */ | |
| 490 | - private io.vertx.core.net.KeyCertOptions keyCertOptions; | |
| 491 | - | |
| 492 | - /** | |
| 493 | - * 信任选项 | |
| 494 | - */ | |
| 495 | - private io.vertx.core.net.TrustOptions trustOptions; | |
| 496 | - | |
| 497 | - /** | |
| 498 | - * SSL 证书路径 | |
| 499 | - */ | |
| 500 | - private String certPath; | |
| 501 | - | |
| 502 | - /** | |
| 503 | - * SSL 私钥路径 | |
| 504 | - */ | |
| 505 | - private String keyPath; | |
| 506 | - | |
| 507 | - /** | |
| 508 | - * 信任存储路径 | |
| 509 | - */ | |
| 510 | - private String trustStorePath; | |
| 511 | - | |
| 512 | - /** | |
| 513 | - * 信任存储密码 | |
| 514 | - */ | |
| 515 | - private String trustStorePassword; | |
| 516 | - | |
| 517 | - } | |
| 518 | - | |
| 519 | - } | |
| 520 | - | |
| 521 | - @Data | |
| 522 | - public static class UdpProperties { | |
| 523 | - | |
| 524 | - /** | |
| 525 | - * 是否开启 | |
| 526 | - */ | |
| 527 | - @NotNull(message = "是否开启不能为空") | |
| 528 | - private Boolean enabled; | |
| 529 | - | |
| 530 | - /** | |
| 531 | - * 服务端口(默认 8093) | |
| 532 | - */ | |
| 533 | - private Integer port = 8093; | |
| 534 | - | |
| 535 | - /** | |
| 536 | - * 接收缓冲区大小(默认 64KB) | |
| 184 | + * SSL 证书路径 | |
| 537 | 185 | */ |
| 538 | - private Integer receiveBufferSize = 65536; | |
| 186 | + @NotEmpty(message = "SSL 证书路径不能为空") | |
| 187 | + private String sslCertPath; | |
| 539 | 188 | |
| 540 | 189 | /** |
| 541 | - * 发送缓冲区大小(默认 64KB) | |
| 190 | + * SSL 私钥路径 | |
| 542 | 191 | */ |
| 543 | - private Integer sendBufferSize = 65536; | |
| 192 | + @NotEmpty(message = "SSL 私钥路径不能为空") | |
| 193 | + private String sslKeyPath; | |
| 544 | 194 | |
| 545 | 195 | /** |
| 546 | - * 会话超时时间(毫秒,默认 60 秒) | |
| 196 | + * 密钥库(KeyStore)路径 | |
| 547 | 197 | * <p> |
| 548 | - * 用于清理不活跃的设备地址映射 | |
| 549 | - */ | |
| 550 | - private Long sessionTimeoutMs = 60000L; | |
| 551 | - | |
| 552 | - /** | |
| 553 | - * 会话清理间隔(毫秒,默认 30 秒) | |
| 554 | - */ | |
| 555 | - private Long sessionCleanIntervalMs = 30000L; | |
| 556 | - | |
| 557 | - } | |
| 558 | - | |
| 559 | - @Data | |
| 560 | - public static class CoapProperties { | |
| 561 | - | |
| 562 | - /** | |
| 563 | - * 是否开启 | |
| 564 | - */ | |
| 565 | - @NotNull(message = "是否开启不能为空") | |
| 566 | - private Boolean enabled; | |
| 567 | - | |
| 568 | - /** | |
| 569 | - * 服务端口(CoAP 默认端口 5683) | |
| 570 | - */ | |
| 571 | - @NotNull(message = "服务端口不能为空") | |
| 572 | - private Integer port = 5683; | |
| 573 | - | |
| 574 | - /** | |
| 575 | - * 最大消息大小(字节) | |
| 576 | - */ | |
| 577 | - @NotNull(message = "最大消息大小不能为空") | |
| 578 | - private Integer maxMessageSize = 1024; | |
| 579 | - | |
| 580 | - /** | |
| 581 | - * ACK 超时时间(毫秒) | |
| 582 | - */ | |
| 583 | - @NotNull(message = "ACK 超时时间不能为空") | |
| 584 | - private Integer ackTimeout = 2000; | |
| 585 | - | |
| 586 | - /** | |
| 587 | - * 最大重传次数 | |
| 588 | - */ | |
| 589 | - @NotNull(message = "最大重传次数不能为空") | |
| 590 | - private Integer maxRetransmit = 4; | |
| 591 | - | |
| 592 | - } | |
| 593 | - | |
| 594 | - @Data | |
| 595 | - public static class WebSocketProperties { | |
| 596 | - | |
| 597 | - /** | |
| 598 | - * 是否开启 | |
| 599 | - */ | |
| 600 | - @NotNull(message = "是否开启不能为空") | |
| 601 | - private Boolean enabled; | |
| 602 | - | |
| 603 | - /** | |
| 604 | - * 服务器端口(默认:8094) | |
| 605 | - */ | |
| 606 | - private Integer port = 8094; | |
| 607 | - | |
| 608 | - /** | |
| 609 | - * WebSocket 路径(默认:/ws) | |
| 610 | - */ | |
| 611 | - @NotEmpty(message = "WebSocket 路径不能为空") | |
| 612 | - private String path = "/ws"; | |
| 613 | - | |
| 614 | - /** | |
| 615 | - * 最大消息大小(字节,默认 64KB) | |
| 616 | - */ | |
| 617 | - private Integer maxMessageSize = 65536; | |
| 618 | - | |
| 619 | - /** | |
| 620 | - * 最大帧大小(字节,默认 64KB) | |
| 198 | + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证) | |
| 621 | 199 | */ |
| 622 | - private Integer maxFrameSize = 65536; | |
| 623 | - | |
| 200 | + private String keyStorePath; | |
| 624 | 201 | /** |
| 625 | - * 空闲超时时间(秒,默认 60) | |
| 202 | + * 密钥库密码 | |
| 626 | 203 | */ |
| 627 | - private Integer idleTimeoutSeconds = 60; | |
| 204 | + private String keyStorePassword; | |
| 628 | 205 | |
| 629 | 206 | /** |
| 630 | - * 是否启用 SSL(wss://) | |
| 631 | - */ | |
| 632 | - private Boolean sslEnabled = false; | |
| 633 | - | |
| 634 | - /** | |
| 635 | - * SSL 证书路径 | |
| 207 | + * 信任库(TrustStore)路径 | |
| 208 | + * <p> | |
| 209 | + * 包含服务端信任的 CA 证书,用于验证服务端的身份 | |
| 636 | 210 | */ |
| 637 | - private String sslCertPath; | |
| 638 | - | |
| 211 | + private String trustStorePath; | |
| 639 | 212 | /** |
| 640 | - * SSL 私钥路径 | |
| 213 | + * 信任库密码 | |
| 641 | 214 | */ |
| 642 | - private String sslKeyPath; | |
| 215 | + private String trustStorePassword; | |
| 643 | 216 | |
| 644 | 217 | } |
| 645 | 218 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * IoT 协议接口 | |
| 7 | + * | |
| 8 | + * 定义传输层协议的生命周期管理 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +public interface IotProtocol { | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * 获取协议实例 ID | |
| 16 | + * | |
| 17 | + * @return 协议实例 ID,如 "http-alink"、"tcp-binary" | |
| 18 | + */ | |
| 19 | + String getId(); | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 获取服务器 ID(用于消息追踪,全局唯一) | |
| 23 | + * | |
| 24 | + * @return 服务器 ID | |
| 25 | + */ | |
| 26 | + String getServerId(); | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 获取协议类型 | |
| 30 | + * | |
| 31 | + * @return 协议类型枚举 | |
| 32 | + */ | |
| 33 | + IotProtocolTypeEnum getType(); | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 启动协议服务 | |
| 37 | + */ | |
| 38 | + void start(); | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 停止协议服务 | |
| 42 | + */ | |
| 43 | + void stop(); | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * 检查协议服务是否正在运行 | |
| 47 | + * | |
| 48 | + * @return 是否正在运行 | |
| 49 | + */ | |
| 50 | + boolean isRunning(); | |
| 51 | + | |
| 52 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol; | |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; | |
| 8 | -import jakarta.annotation.PostConstruct; | |
| 8 | +import lombok.AllArgsConstructor; | |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | |
| 11 | 11 | /** |
| 12 | - * IoT 网关 EMQX 订阅者:接收下行给设备的消息 | |
| 12 | + * IoT 协议下行消息订阅者抽象类 | |
| 13 | + * | |
| 14 | + * 负责接收来自消息总线的下行消息,并委托给子类进行业务处理 | |
| 13 | 15 | * |
| 14 | 16 | * @author 芋道源码 |
| 15 | 17 | */ |
| 18 | +@AllArgsConstructor | |
| 16 | 19 | @Slf4j |
| 17 | -public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 20 | +public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 18 | 21 | |
| 19 | - private final IotEmqxDownstreamHandler downstreamHandler; | |
| 22 | + private final IotProtocol protocol; | |
| 20 | 23 | |
| 21 | 24 | private final IotMessageBus messageBus; |
| 22 | 25 | |
| 23 | - private final IotEmqxUpstreamProtocol protocol; | |
| 24 | - | |
| 25 | - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { | |
| 26 | - this.protocol = protocol; | |
| 27 | - this.messageBus = messageBus; | |
| 28 | - this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); | |
| 29 | - } | |
| 30 | - | |
| 31 | - @PostConstruct | |
| 32 | - public void init() { | |
| 33 | - messageBus.register(this); | |
| 34 | - } | |
| 35 | - | |
| 36 | 26 | @Override |
| 37 | 27 | public String getTopic() { |
| 38 | 28 | return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); |
| 39 | 29 | } |
| 40 | 30 | |
| 31 | + /** | |
| 32 | + * 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 33 | + */ | |
| 41 | 34 | @Override |
| 42 | 35 | public String getGroup() { |
| 43 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 44 | 36 | return getTopic(); |
| 45 | 37 | } |
| 46 | 38 | |
| 47 | 39 | @Override |
| 40 | + public void start() { | |
| 41 | + messageBus.register(this); | |
| 42 | + log.info("[start][{} 下行消息订阅成功,Topic:{}]", protocol.getType().name(), getTopic()); | |
| 43 | + } | |
| 44 | + | |
| 45 | + @Override | |
| 46 | + public void stop() { | |
| 47 | + messageBus.unregister(this); | |
| 48 | + log.info("[stop][{} 下行消息订阅已停止,Topic:{}]", protocol.getType().name(), getTopic()); | |
| 49 | + } | |
| 50 | + | |
| 51 | + @Override | |
| 48 | 52 | public void onMessage(IotDeviceMessage message) { |
| 49 | 53 | log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", |
| 50 | 54 | message.getId(), message.getMethod(), message.getDeviceId()); |
| 51 | 55 | try { |
| 52 | 56 | // 1. 校验 |
| 53 | 57 | String method = message.getMethod(); |
| 54 | - if (method == null) { | |
| 58 | + if (StrUtil.isBlank(method)) { | |
| 55 | 59 | log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", |
| 56 | 60 | message.getId(), message.getDeviceId()); |
| 57 | 61 | return; |
| 58 | 62 | } |
| 59 | 63 | |
| 60 | 64 | // 2. 处理下行消息 |
| 61 | - downstreamHandler.handle(message); | |
| 65 | + handleMessage(message); | |
| 62 | 66 | } catch (Exception e) { |
| 63 | 67 | log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", |
| 64 | 68 | message.getId(), message.getMethod(), message.getDeviceId(), e); |
| 65 | 69 | } |
| 66 | 70 | } |
| 67 | 71 | |
| 68 | -} | |
| 69 | 72 | \ No newline at end of file |
| 73 | + /** | |
| 74 | + * 处理下行消息 | |
| 75 | + * | |
| 76 | + * @param message 下行消息 | |
| 77 | + */ | |
| 78 | + protected abstract void handleMessage(IotDeviceMessage message); | |
| 79 | + | |
| 80 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.util.BooleanUtil; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; | |
| 14 | +import lombok.Getter; | |
| 15 | +import lombok.extern.slf4j.Slf4j; | |
| 16 | +import org.springframework.context.SmartLifecycle; | |
| 17 | + | |
| 18 | +import java.util.ArrayList; | |
| 19 | +import java.util.List; | |
| 20 | + | |
| 21 | +/** | |
| 22 | + * IoT 协议管理器:负责根据配置创建和管理协议实例 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +@Slf4j | |
| 27 | +public class IotProtocolManager implements SmartLifecycle { | |
| 28 | + | |
| 29 | + private final IotGatewayProperties gatewayProperties; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 协议实例列表 | |
| 33 | + */ | |
| 34 | + private final List<IotProtocol> protocols = new ArrayList<>(); | |
| 35 | + | |
| 36 | + @Getter | |
| 37 | + private volatile boolean running = false; | |
| 38 | + | |
| 39 | + public IotProtocolManager(IotGatewayProperties gatewayProperties) { | |
| 40 | + this.gatewayProperties = gatewayProperties; | |
| 41 | + } | |
| 42 | + | |
| 43 | + @Override | |
| 44 | + public void start() { | |
| 45 | + if (running) { | |
| 46 | + return; | |
| 47 | + } | |
| 48 | + List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols(); | |
| 49 | + if (CollUtil.isEmpty(protocolConfigs)) { | |
| 50 | + log.info("[start][没有配置协议实例,跳过启动]"); | |
| 51 | + return; | |
| 52 | + } | |
| 53 | + | |
| 54 | + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { | |
| 55 | + if (BooleanUtil.isFalse(config.getEnabled())) { | |
| 56 | + log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); | |
| 57 | + continue; | |
| 58 | + } | |
| 59 | + IotProtocol protocol = createProtocol(config); | |
| 60 | + if (protocol == null) { | |
| 61 | + continue; | |
| 62 | + } | |
| 63 | + protocol.start(); | |
| 64 | + protocols.add(protocol); | |
| 65 | + } | |
| 66 | + running = true; | |
| 67 | + log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size()); | |
| 68 | + } | |
| 69 | + | |
| 70 | + @Override | |
| 71 | + public void stop() { | |
| 72 | + if (!running) { | |
| 73 | + return; | |
| 74 | + } | |
| 75 | + for (IotProtocol protocol : protocols) { | |
| 76 | + try { | |
| 77 | + protocol.stop(); | |
| 78 | + } catch (Exception e) { | |
| 79 | + log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e); | |
| 80 | + } | |
| 81 | + } | |
| 82 | + protocols.clear(); | |
| 83 | + running = false; | |
| 84 | + log.info("[stop][协议管理器已停止]"); | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 创建协议实例 | |
| 89 | + * | |
| 90 | + * @param config 协议实例配置 | |
| 91 | + * @return 协议实例 | |
| 92 | + */ | |
| 93 | + @SuppressWarnings({"EnhancedSwitchMigration"}) | |
| 94 | + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 95 | + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); | |
| 96 | + if (protocolType == null) { | |
| 97 | + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); | |
| 98 | + return null; | |
| 99 | + } | |
| 100 | + switch (protocolType) { | |
| 101 | + case HTTP: | |
| 102 | + return createHttpProtocol(config); | |
| 103 | + case TCP: | |
| 104 | + return createTcpProtocol(config); | |
| 105 | + case UDP: | |
| 106 | + return createUdpProtocol(config); | |
| 107 | + case COAP: | |
| 108 | + return createCoapProtocol(config); | |
| 109 | + case WEBSOCKET: | |
| 110 | + return createWebSocketProtocol(config); | |
| 111 | + case MQTT: | |
| 112 | + return createMqttProtocol(config); | |
| 113 | + case EMQX: | |
| 114 | + return createEmqxProtocol(config); | |
| 115 | + default: | |
| 116 | + throw new IllegalArgumentException(String.format( | |
| 117 | + "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); | |
| 118 | + } | |
| 119 | + } | |
| 120 | + | |
| 121 | + /** | |
| 122 | + * 创建 HTTP 协议实例 | |
| 123 | + * | |
| 124 | + * @param config 协议实例配置 | |
| 125 | + * @return HTTP 协议实例 | |
| 126 | + */ | |
| 127 | + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 128 | + return new IotHttpProtocol(config); | |
| 129 | + } | |
| 130 | + | |
| 131 | + /** | |
| 132 | + * 创建 TCP 协议实例 | |
| 133 | + * | |
| 134 | + * @param config 协议实例配置 | |
| 135 | + * @return TCP 协议实例 | |
| 136 | + */ | |
| 137 | + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 138 | + return new IotTcpProtocol(config); | |
| 139 | + } | |
| 140 | + | |
| 141 | + /** | |
| 142 | + * 创建 UDP 协议实例 | |
| 143 | + * | |
| 144 | + * @param config 协议实例配置 | |
| 145 | + * @return UDP 协议实例 | |
| 146 | + */ | |
| 147 | + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 148 | + return new IotUdpProtocol(config); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * 创建 CoAP 协议实例 | |
| 153 | + * | |
| 154 | + * @param config 协议实例配置 | |
| 155 | + * @return CoAP 协议实例 | |
| 156 | + */ | |
| 157 | + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 158 | + return new IotCoapProtocol(config); | |
| 159 | + } | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * 创建 WebSocket 协议实例 | |
| 163 | + * | |
| 164 | + * @param config 协议实例配置 | |
| 165 | + * @return WebSocket 协议实例 | |
| 166 | + */ | |
| 167 | + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 168 | + return new IotWebSocketProtocol(config); | |
| 169 | + } | |
| 170 | + | |
| 171 | + /** | |
| 172 | + * 创建 MQTT 协议实例 | |
| 173 | + * | |
| 174 | + * @param config 协议实例配置 | |
| 175 | + * @return MQTT 协议实例 | |
| 176 | + */ | |
| 177 | + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 178 | + return new IotMqttProtocol(config); | |
| 179 | + } | |
| 180 | + | |
| 181 | + /** | |
| 182 | + * 创建 EMQX 协议实例 | |
| 183 | + * | |
| 184 | + * @param config 协议实例配置 | |
| 185 | + * @return EMQX 协议实例 | |
| 186 | + */ | |
| 187 | + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 188 | + return new IotEmqxProtocol(config); | |
| 189 | + } | |
| 190 | + | |
| 191 | +} | ... | ... |
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/IotCoapDownstreamSubscriber.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | -import jakarta.annotation.PostConstruct; | |
| 8 | -import lombok.RequiredArgsConstructor; | |
| 9 | -import lombok.extern.slf4j.Slf4j; | |
| 10 | - | |
| 11 | -/** | |
| 12 | - * IoT 网关 CoAP 订阅者:接收下行给设备的消息 | |
| 13 | - * | |
| 14 | - * @author 芋道源码 | |
| 15 | - */ | |
| 16 | -@RequiredArgsConstructor | |
| 17 | -@Slf4j | |
| 18 | -public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 19 | - | |
| 20 | - private final IotCoapUpstreamProtocol protocol; | |
| 21 | - | |
| 22 | - private final IotMessageBus messageBus; | |
| 23 | - | |
| 24 | - @PostConstruct | |
| 25 | - public void init() { | |
| 26 | - messageBus.register(this); | |
| 27 | - } | |
| 28 | - | |
| 29 | - @Override | |
| 30 | - public String getTopic() { | |
| 31 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | |
| 32 | - } | |
| 33 | - | |
| 34 | - @Override | |
| 35 | - public String getGroup() { | |
| 36 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 37 | - return getTopic(); | |
| 38 | - } | |
| 39 | - | |
| 40 | - @Override | |
| 41 | - public void onMessage(IotDeviceMessage message) { | |
| 42 | - // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) | |
| 43 | - log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); | |
| 44 | - } | |
| 45 | - | |
| 46 | -} |
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.ProtocolProperties; | |
| 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.upstream.IotCoapAuthHandler; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthResource; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterHandler; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterResource; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubHandler; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubResource; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamHandler; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.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 cn.hutool.core.lang.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 ProtocolProperties 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(ProtocolProperties 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.util.IotDeviceMessageUtils; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource; | |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler; | |
| 11 | -import jakarta.annotation.PostConstruct; | |
| 12 | -import jakarta.annotation.PreDestroy; | |
| 13 | -import lombok.Getter; | |
| 14 | -import lombok.extern.slf4j.Slf4j; | |
| 15 | -import org.eclipse.californium.core.CoapResource; | |
| 16 | -import org.eclipse.californium.core.CoapServer; | |
| 17 | -import org.eclipse.californium.core.config.CoapConfig; | |
| 18 | -import org.eclipse.californium.elements.config.Configuration; | |
| 19 | - | |
| 20 | -import java.util.concurrent.TimeUnit; | |
| 21 | - | |
| 22 | -/** | |
| 23 | - * IoT 网关 CoAP 协议:接收设备上行消息 | |
| 24 | - * | |
| 25 | - * 基于 Eclipse Californium 实现,支持: | |
| 26 | - * 1. 认证:POST /auth | |
| 27 | - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post | |
| 28 | - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post | |
| 29 | - * | |
| 30 | - * @author 芋道源码 | |
| 31 | - */ | |
| 32 | -@Slf4j | |
| 33 | -public class IotCoapUpstreamProtocol { | |
| 34 | - | |
| 35 | - private final IotGatewayProperties.CoapProperties coapProperties; | |
| 36 | - | |
| 37 | - private CoapServer coapServer; | |
| 38 | - | |
| 39 | - @Getter | |
| 40 | - private final String serverId; | |
| 41 | - | |
| 42 | - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { | |
| 43 | - this.coapProperties = coapProperties; | |
| 44 | - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); | |
| 45 | - } | |
| 46 | - | |
| 47 | - @PostConstruct | |
| 48 | - public void start() { | |
| 49 | - try { | |
| 50 | - // 1.1 创建网络配置(Californium 3.x API) | |
| 51 | - Configuration config = Configuration.createStandardWithoutFile(); | |
| 52 | - config.set(CoapConfig.COAP_PORT, coapProperties.getPort()); | |
| 53 | - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize()); | |
| 54 | - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS); | |
| 55 | - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit()); | |
| 56 | - // 1.2 创建 CoAP 服务器 | |
| 57 | - coapServer = new CoapServer(config); | |
| 58 | - | |
| 59 | - // 2.1 添加 /auth 认证资源 | |
| 60 | - IotCoapAuthHandler authHandler = new IotCoapAuthHandler(); | |
| 61 | - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); | |
| 62 | - coapServer.add(authResource); | |
| 63 | - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) | |
| 64 | - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); | |
| 65 | - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); | |
| 66 | - authResource.add(new CoapResource("register") {{ | |
| 67 | - add(registerResource); | |
| 68 | - }}); | |
| 69 | - // 2.3 添加 /topic 根资源(用于上行消息) | |
| 70 | - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(); | |
| 71 | - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); | |
| 72 | - coapServer.add(topicResource); | |
| 73 | - | |
| 74 | - // 3. 启动服务器 | |
| 75 | - coapServer.start(); | |
| 76 | - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); | |
| 77 | - } catch (Exception e) { | |
| 78 | - log.error("[start][IoT 网关 CoAP 协议启动失败]", e); | |
| 79 | - throw e; | |
| 80 | - } | |
| 81 | - } | |
| 82 | - | |
| 83 | - @PreDestroy | |
| 84 | - public void stop() { | |
| 85 | - if (coapServer != null) { | |
| 86 | - try { | |
| 87 | - coapServer.stop(); | |
| 88 | - log.info("[stop][IoT 网关 CoAP 协议已停止]"); | |
| 89 | - } catch (Exception e) { | |
| 90 | - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); | |
| 91 | - } | |
| 92 | - } | |
| 93 | - } | |
| 94 | - | |
| 95 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT 网关 CoAP 订阅者:接收下行给设备的消息 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { | |
| 18 | + super(protocol, messageBus); | |
| 19 | + } | |
| 20 | + | |
| 21 | + @Override | |
| 22 | + protected void handleMessage(IotDeviceMessage message) { | |
| 23 | + // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) | |
| 24 | + log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); | |
| 25 | + } | |
| 26 | + | |
| 27 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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/upstream/IotCoapAuthHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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/upstream/IotCoapAuthResource.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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/upstream/IotCoapRegisterHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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.getSign(), "sign 不能为空"); | |
| 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/upstream/IotCoapRegisterResource.java
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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/upstream/IotCoapRegisterSubResource.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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/upstream/IotCoapUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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/upstream/IotCoapUpstreamTopicResource.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; | |
| 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 | - * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler} | |
| 28 | - * | |
| 29 | - * @author 芋道源码 | |
| 30 | - */ | |
| 31 | -@Slf4j | |
| 32 | -public class IotCoapAuthHandler { | |
| 33 | - | |
| 34 | - private final IotDeviceTokenService deviceTokenService; | |
| 35 | - private final IotDeviceCommonApi deviceApi; | |
| 36 | - private final IotDeviceMessageService deviceMessageService; | |
| 37 | - | |
| 38 | - public IotCoapAuthHandler() { | |
| 39 | - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 40 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 41 | - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 42 | - } | |
| 43 | - | |
| 44 | - /** | |
| 45 | - * 处理认证请求 | |
| 46 | - * | |
| 47 | - * @param exchange CoAP 交换对象 | |
| 48 | - * @param protocol 协议对象 | |
| 49 | - */ | |
| 50 | - @SuppressWarnings("unchecked") | |
| 51 | - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { | |
| 52 | - try { | |
| 53 | - // 1.1 解析请求体 | |
| 54 | - byte[] payload = exchange.getRequestPayload(); | |
| 55 | - if (payload == null || payload.length == 0) { | |
| 56 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); | |
| 57 | - return; | |
| 58 | - } | |
| 59 | - Map<String, Object> body; | |
| 60 | - try { | |
| 61 | - body = JsonUtils.parseObject(new String(payload), Map.class); | |
| 62 | - } catch (Exception e) { | |
| 63 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); | |
| 64 | - return; | |
| 65 | - } | |
| 66 | - // 1.2 解析参数 | |
| 67 | - String clientId = MapUtil.getStr(body, "clientId"); | |
| 68 | - if (StrUtil.isEmpty(clientId)) { | |
| 69 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); | |
| 70 | - return; | |
| 71 | - } | |
| 72 | - String username = MapUtil.getStr(body, "username"); | |
| 73 | - if (StrUtil.isEmpty(username)) { | |
| 74 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); | |
| 75 | - return; | |
| 76 | - } | |
| 77 | - String password = MapUtil.getStr(body, "password"); | |
| 78 | - if (StrUtil.isEmpty(password)) { | |
| 79 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); | |
| 80 | - return; | |
| 81 | - } | |
| 82 | - | |
| 83 | - // 2.1 执行认证 | |
| 84 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 85 | - .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 86 | - if (result.isError()) { | |
| 87 | - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); | |
| 88 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); | |
| 89 | - return; | |
| 90 | - } | |
| 91 | - if (!BooleanUtil.isTrue(result.getData())) { | |
| 92 | - log.warn("[handle][认证失败,clientId: {}]", clientId); | |
| 93 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); | |
| 94 | - return; | |
| 95 | - } | |
| 96 | - // 2.2 生成 Token | |
| 97 | - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); | |
| 98 | - Assert.notNull(deviceInfo, "设备信息不能为空"); | |
| 99 | - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 100 | - Assert.notBlank(token, "生成 token 不能为空"); | |
| 101 | - | |
| 102 | - // 3. 执行上线 | |
| 103 | - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); | |
| 104 | - deviceMessageService.sendDeviceMessage(message, | |
| 105 | - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); | |
| 106 | - | |
| 107 | - // 4. 返回成功响应 | |
| 108 | - log.info("[handle][认证成功,productKey: {}, deviceName: {}]", | |
| 109 | - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 110 | - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token)); | |
| 111 | - } catch (Exception e) { | |
| 112 | - log.error("[handle][认证处理异常]", e); | |
| 113 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); | |
| 114 | - } | |
| 115 | - } | |
| 116 | - | |
| 117 | -} |
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 | - * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler | |
| 26 | - */ | |
| 27 | -@Slf4j | |
| 28 | -public class IotCoapRegisterHandler { | |
| 29 | - | |
| 30 | - private final IotDeviceCommonApi deviceApi; | |
| 31 | - | |
| 32 | - public IotCoapRegisterHandler() { | |
| 33 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 34 | - } | |
| 35 | - | |
| 36 | - /** | |
| 37 | - * 处理设备动态注册请求 | |
| 38 | - * | |
| 39 | - * @param exchange CoAP 交换对象 | |
| 40 | - */ | |
| 41 | - @SuppressWarnings("unchecked") | |
| 42 | - public void handle(CoapExchange exchange) { | |
| 43 | - try { | |
| 44 | - // 1.1 解析请求体 | |
| 45 | - byte[] payload = exchange.getRequestPayload(); | |
| 46 | - if (payload == null || payload.length == 0) { | |
| 47 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); | |
| 48 | - return; | |
| 49 | - } | |
| 50 | - Map<String, Object> body; | |
| 51 | - try { | |
| 52 | - body = JsonUtils.parseObject(new String(payload), Map.class); | |
| 53 | - } catch (Exception e) { | |
| 54 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); | |
| 55 | - return; | |
| 56 | - } | |
| 57 | - | |
| 58 | - // 1.2 解析参数 | |
| 59 | - String productKey = MapUtil.getStr(body, "productKey"); | |
| 60 | - if (StrUtil.isEmpty(productKey)) { | |
| 61 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); | |
| 62 | - return; | |
| 63 | - } | |
| 64 | - String deviceName = MapUtil.getStr(body, "deviceName"); | |
| 65 | - if (StrUtil.isEmpty(deviceName)) { | |
| 66 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); | |
| 67 | - return; | |
| 68 | - } | |
| 69 | - String productSecret = MapUtil.getStr(body, "productSecret"); | |
| 70 | - if (StrUtil.isEmpty(productSecret)) { | |
| 71 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空"); | |
| 72 | - return; | |
| 73 | - } | |
| 74 | - | |
| 75 | - // 2. 调用动态注册 | |
| 76 | - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() | |
| 77 | - .setProductKey(productKey) | |
| 78 | - .setDeviceName(deviceName) | |
| 79 | - .setProductSecret(productSecret); | |
| 80 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO); | |
| 81 | - if (result.isError()) { | |
| 82 | - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]", | |
| 83 | - productKey, deviceName, result.getMsg()); | |
| 84 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, | |
| 85 | - "设备动态注册失败:" + result.getMsg()); | |
| 86 | - return; | |
| 87 | - } | |
| 88 | - | |
| 89 | - // 3. 返回成功响应 | |
| 90 | - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName); | |
| 91 | - IotCoapUtils.respondSuccess(exchange, result.getData()); | |
| 92 | - } catch (Exception e) { | |
| 93 | - log.error("[handle][设备动态注册处理异常]", e); | |
| 94 | - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); | |
| 95 | - } | |
| 96 | - } | |
| 97 | - | |
| 98 | -} |
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 deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; | |
| 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.*; | |
| 12 | - | |
| 13 | -/** | |
| 14 | - * IoT CoAP 协议工具类 | |
| 15 | - * | |
| 16 | - * @author 芋道源码 | |
| 17 | - */ | |
| 18 | -public class IotCoapUtils { | |
| 19 | - | |
| 20 | - /** | |
| 21 | - * 自定义 CoAP Option 编号,用于携带 Token | |
| 22 | - * <p> | |
| 23 | - * CoAP Option 范围 2048-65535 属于实验/自定义范围 | |
| 24 | - */ | |
| 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 | - } | |
| 83 | - | |
| 84 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 7 | -import io.vertx.core.Vertx; | |
| 8 | -import io.vertx.core.http.HttpServer; | |
| 9 | -import io.vertx.ext.web.Router; | |
| 10 | -import io.vertx.ext.web.handler.BodyHandler; | |
| 11 | -import jakarta.annotation.PostConstruct; | |
| 12 | -import jakarta.annotation.PreDestroy; | |
| 13 | -import lombok.extern.slf4j.Slf4j; | |
| 14 | - | |
| 15 | -/** | |
| 16 | - * IoT 网关 EMQX 认证事件协议服务 | |
| 17 | - * <p> | |
| 18 | - * 为 EMQX 提供 HTTP 接口服务,包括: | |
| 19 | - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 | |
| 20 | - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 | |
| 21 | - * | |
| 22 | - * @author 芋道源码 | |
| 23 | - */ | |
| 24 | -@Slf4j | |
| 25 | -public class IotEmqxAuthEventProtocol { | |
| 26 | - | |
| 27 | - private final IotGatewayProperties.EmqxProperties emqxProperties; | |
| 28 | - | |
| 29 | - private final String serverId; | |
| 30 | - | |
| 31 | - private final Vertx vertx; | |
| 32 | - | |
| 33 | - private HttpServer httpServer; | |
| 34 | - | |
| 35 | - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties, | |
| 36 | - Vertx vertx) { | |
| 37 | - this.emqxProperties = emqxProperties; | |
| 38 | - this.vertx = vertx; | |
| 39 | - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); | |
| 40 | - } | |
| 41 | - | |
| 42 | - @PostConstruct | |
| 43 | - public void start() { | |
| 44 | - try { | |
| 45 | - startHttpServer(); | |
| 46 | - log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort()); | |
| 47 | - } catch (Exception e) { | |
| 48 | - log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e); | |
| 49 | - throw e; | |
| 50 | - } | |
| 51 | - } | |
| 52 | - | |
| 53 | - @PreDestroy | |
| 54 | - public void stop() { | |
| 55 | - stopHttpServer(); | |
| 56 | - log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]"); | |
| 57 | - } | |
| 58 | - | |
| 59 | - /** | |
| 60 | - * 启动 HTTP 服务器 | |
| 61 | - */ | |
| 62 | - private void startHttpServer() { | |
| 63 | - int port = emqxProperties.getHttpPort(); | |
| 64 | - | |
| 65 | - // 1. 创建路由 | |
| 66 | - Router router = Router.router(vertx); | |
| 67 | - router.route().handler(BodyHandler.create()); | |
| 68 | - | |
| 69 | - // 2. 创建处理器,传入 serverId | |
| 70 | - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); | |
| 71 | - router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); | |
| 72 | - router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); | |
| 73 | - // TODO @haohao:/mqtt/acl 需要处理么? | |
| 74 | - // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理 | |
| 75 | - | |
| 76 | - // 3. 启动 HTTP 服务器 | |
| 77 | - try { | |
| 78 | - httpServer = vertx.createHttpServer() | |
| 79 | - .requestHandler(router) | |
| 80 | - .listen(port) | |
| 81 | - .result(); | |
| 82 | - } catch (Exception e) { | |
| 83 | - log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e); | |
| 84 | - throw e; | |
| 85 | - } | |
| 86 | - } | |
| 87 | - | |
| 88 | - /** | |
| 89 | - * 停止 HTTP 服务器 | |
| 90 | - */ | |
| 91 | - private void stopHttpServer() { | |
| 92 | - if (httpServer == null) { | |
| 93 | - return; | |
| 94 | - } | |
| 95 | - | |
| 96 | - try { | |
| 97 | - httpServer.close().result(); | |
| 98 | - log.info("[stopHttpServer][HTTP 服务器已停止]"); | |
| 99 | - } catch (Exception e) { | |
| 100 | - log.error("[stopHttpServer][HTTP 服务器停止失败]", e); | |
| 101 | - } | |
| 102 | - } | |
| 103 | - | |
| 104 | -} | |
| 105 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; | |
| 2 | + | |
| 3 | +import jakarta.validation.Valid; | |
| 4 | +import jakarta.validation.constraints.Max; | |
| 5 | +import jakarta.validation.constraints.Min; | |
| 6 | +import jakarta.validation.constraints.NotEmpty; | |
| 7 | +import jakarta.validation.constraints.NotNull; | |
| 8 | +import lombok.Data; | |
| 9 | + | |
| 10 | +import java.util.List; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * IoT EMQX 协议配置 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@Data | |
| 18 | +public class IotEmqxConfig { | |
| 19 | + | |
| 20 | + // ========== MQTT Client 配置(连接 EMQX Broker) ========== | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * MQTT 服务器地址 | |
| 24 | + */ | |
| 25 | + @NotEmpty(message = "MQTT 服务器地址不能为空") | |
| 26 | + private String mqttHost; | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * MQTT 服务器端口(默认:1883) | |
| 30 | + */ | |
| 31 | + @NotNull(message = "MQTT 服务器端口不能为空") | |
| 32 | + private Integer mqttPort = 1883; | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * MQTT 用户名 | |
| 36 | + */ | |
| 37 | + @NotEmpty(message = "MQTT 用户名不能为空") | |
| 38 | + private String mqttUsername; | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * MQTT 密码 | |
| 42 | + */ | |
| 43 | + @NotEmpty(message = "MQTT 密码不能为空") | |
| 44 | + private String mqttPassword; | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * MQTT 客户端的 SSL 开关 | |
| 48 | + */ | |
| 49 | + @NotNull(message = "MQTT 是否开启 SSL 不能为空") | |
| 50 | + private Boolean mqttSsl = false; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * MQTT 客户端 ID | |
| 54 | + */ | |
| 55 | + @NotEmpty(message = "MQTT 客户端 ID 不能为空") | |
| 56 | + private String mqttClientId; | |
| 57 | + | |
| 58 | + /** | |
| 59 | + * MQTT 订阅的主题 | |
| 60 | + */ | |
| 61 | + @NotEmpty(message = "MQTT 主题不能为空") | |
| 62 | + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * 默认 QoS 级别 | |
| 66 | + * <p> | |
| 67 | + * 0 - 最多一次 | |
| 68 | + * 1 - 至少一次 | |
| 69 | + * 2 - 刚好一次 | |
| 70 | + */ | |
| 71 | + @NotNull(message = "MQTT QoS 不能为空") | |
| 72 | + @Min(value = 0, message = "MQTT QoS 不能小于 0") | |
| 73 | + @Max(value = 2, message = "MQTT QoS 不能大于 2") | |
| 74 | + private Integer mqttQos = 1; | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * 连接超时时间(秒) | |
| 78 | + */ | |
| 79 | + @NotNull(message = "连接超时时间不能为空") | |
| 80 | + @Min(value = 1, message = "连接超时时间不能小于 1 秒") | |
| 81 | + private Integer connectTimeoutSeconds = 10; | |
| 82 | + | |
| 83 | + /** | |
| 84 | + * 重连延迟时间(毫秒) | |
| 85 | + */ | |
| 86 | + @NotNull(message = "重连延迟时间不能为空") | |
| 87 | + @Min(value = 0, message = "重连延迟时间不能小于 0 毫秒") | |
| 88 | + private Long reconnectDelayMs = 5000L; | |
| 89 | + | |
| 90 | + /** | |
| 91 | + * 是否启用 Clean Session (清理会话) | |
| 92 | + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 | |
| 93 | + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 | |
| 94 | + */ | |
| 95 | + @NotNull(message = "是否启用 Clean Session 不能为空") | |
| 96 | + private Boolean cleanSession = true; | |
| 97 | + | |
| 98 | + /** | |
| 99 | + * 心跳间隔(秒) | |
| 100 | + * 用于保持连接活性,及时发现网络中断。 | |
| 101 | + */ | |
| 102 | + @NotNull(message = "心跳间隔不能为空") | |
| 103 | + @Min(value = 1, message = "心跳间隔不能小于 1 秒") | |
| 104 | + private Integer keepAliveIntervalSeconds = 60; | |
| 105 | + | |
| 106 | + /** | |
| 107 | + * 最大未确认消息队列大小 | |
| 108 | + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 | |
| 109 | + */ | |
| 110 | + @NotNull(message = "最大未确认消息队列大小不能为空") | |
| 111 | + @Min(value = 1, message = "最大未确认消息队列大小不能小于 1") | |
| 112 | + private Integer maxInflightQueue = 10000; | |
| 113 | + | |
| 114 | + /** | |
| 115 | + * 是否信任所有 SSL 证书 | |
| 116 | + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! | |
| 117 | + * 在生产环境中,应设置为 false,并配置正确的信任库。 | |
| 118 | + */ | |
| 119 | + @NotNull(message = "是否信任所有 SSL 证书不能为空") | |
| 120 | + private Boolean trustAll = false; | |
| 121 | + | |
| 122 | + // ========== MQTT Will / SSL 高级配置 ========== | |
| 123 | + | |
| 124 | + /** | |
| 125 | + * 遗嘱消息配置 (用于网关异常下线时通知其他系统) | |
| 126 | + */ | |
| 127 | + @Valid | |
| 128 | + private Will will = new Will(); | |
| 129 | + | |
| 130 | + /** | |
| 131 | + * 高级 SSL/TLS 配置 (用于生产环境) | |
| 132 | + */ | |
| 133 | + @Valid | |
| 134 | + private Ssl sslOptions = new Ssl(); | |
| 135 | + | |
| 136 | + // ========== HTTP Hook 配置(网关提供给 EMQX 调用) ========== | |
| 137 | + | |
| 138 | + /** | |
| 139 | + * HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event) | |
| 140 | + */ | |
| 141 | + @Valid | |
| 142 | + private Http http = new Http(); | |
| 143 | + | |
| 144 | + /** | |
| 145 | + * 遗嘱消息 (Last Will and Testament) | |
| 146 | + */ | |
| 147 | + @Data | |
| 148 | + public static class Will { | |
| 149 | + | |
| 150 | + /** | |
| 151 | + * 是否启用遗嘱消息 | |
| 152 | + */ | |
| 153 | + private boolean enabled = false; | |
| 154 | + /** | |
| 155 | + * 遗嘱消息主题 | |
| 156 | + */ | |
| 157 | + private String topic; | |
| 158 | + /** | |
| 159 | + * 遗嘱消息内容 | |
| 160 | + */ | |
| 161 | + private String payload; | |
| 162 | + /** | |
| 163 | + * 遗嘱消息 QoS 等级 | |
| 164 | + */ | |
| 165 | + @Min(value = 0, message = "遗嘱消息 QoS 不能小于 0") | |
| 166 | + @Max(value = 2, message = "遗嘱消息 QoS 不能大于 2") | |
| 167 | + private Integer qos = 1; | |
| 168 | + /** | |
| 169 | + * 遗嘱消息是否作为保留消息发布 | |
| 170 | + */ | |
| 171 | + private boolean retain = true; | |
| 172 | + | |
| 173 | + } | |
| 174 | + | |
| 175 | + /** | |
| 176 | + * 高级 SSL/TLS 配置 | |
| 177 | + */ | |
| 178 | + @Data | |
| 179 | + public static class Ssl { | |
| 180 | + | |
| 181 | + /** | |
| 182 | + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks | |
| 183 | + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 | |
| 184 | + */ | |
| 185 | + private String keyStorePath; | |
| 186 | + /** | |
| 187 | + * 密钥库密码 | |
| 188 | + */ | |
| 189 | + private String keyStorePassword; | |
| 190 | + /** | |
| 191 | + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks | |
| 192 | + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 | |
| 193 | + */ | |
| 194 | + private String trustStorePath; | |
| 195 | + /** | |
| 196 | + * 信任库密码 | |
| 197 | + */ | |
| 198 | + private String trustStorePassword; | |
| 199 | + | |
| 200 | + } | |
| 201 | + | |
| 202 | + /** | |
| 203 | + * HTTP Hook 服务 SSL 配置 | |
| 204 | + */ | |
| 205 | + @Data | |
| 206 | + public static class Http { | |
| 207 | + | |
| 208 | + /** | |
| 209 | + * 是否启用 SSL | |
| 210 | + */ | |
| 211 | + private Boolean sslEnabled = false; | |
| 212 | + | |
| 213 | + /** | |
| 214 | + * SSL 证书路径 | |
| 215 | + */ | |
| 216 | + private String sslCertPath; | |
| 217 | + | |
| 218 | + /** | |
| 219 | + * SSL 私钥路径 | |
| 220 | + */ | |
| 221 | + private String sslKeyPath; | |
| 222 | + | |
| 223 | + } | |
| 224 | + | |
| 225 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 6 | +import cn.hutool.extra.spring.SpringUtil; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 16 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
| 17 | +import io.vertx.core.Vertx; | |
| 18 | +import io.vertx.core.buffer.Buffer; | |
| 19 | +import io.vertx.core.http.HttpServer; | |
| 20 | +import io.vertx.core.http.HttpServerOptions; | |
| 21 | +import io.vertx.core.net.JksOptions; | |
| 22 | +import io.vertx.core.net.PemKeyCertOptions; | |
| 23 | +import io.vertx.ext.web.Router; | |
| 24 | +import io.vertx.ext.web.handler.BodyHandler; | |
| 25 | +import io.vertx.mqtt.MqttClient; | |
| 26 | +import io.vertx.mqtt.MqttClientOptions; | |
| 27 | +import lombok.Getter; | |
| 28 | +import lombok.extern.slf4j.Slf4j; | |
| 29 | + | |
| 30 | +import java.util.List; | |
| 31 | +import java.util.Map; | |
| 32 | +import java.util.concurrent.TimeUnit; | |
| 33 | + | |
| 34 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; | |
| 35 | + | |
| 36 | +/** | |
| 37 | + * IoT 网关 EMQX 协议实现: | |
| 38 | + * <p> | |
| 39 | + * 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用 | |
| 40 | + * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker | |
| 41 | + * | |
| 42 | + * @author 芋道源码 | |
| 43 | + */ | |
| 44 | +@Slf4j | |
| 45 | +public class IotEmqxProtocol implements IotProtocol { | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * 协议配置 | |
| 49 | + */ | |
| 50 | + private final ProtocolProperties properties; | |
| 51 | + /** | |
| 52 | + * EMQX 配置 | |
| 53 | + */ | |
| 54 | + private final IotEmqxConfig emqxConfig; | |
| 55 | + /** | |
| 56 | + * 服务器 ID | |
| 57 | + */ | |
| 58 | + @Getter | |
| 59 | + private final String serverId; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 运行状态 | |
| 63 | + */ | |
| 64 | + @Getter | |
| 65 | + private volatile boolean running = false; | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * Vert.x 实例 | |
| 69 | + */ | |
| 70 | + private Vertx vertx; | |
| 71 | + /** | |
| 72 | + * HTTP Hook 服务器 | |
| 73 | + */ | |
| 74 | + private HttpServer httpServer; | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * MQTT Client | |
| 78 | + */ | |
| 79 | + private volatile MqttClient mqttClient; | |
| 80 | + /** | |
| 81 | + * MQTT 重连定时器 ID | |
| 82 | + */ | |
| 83 | + private volatile Long reconnectTimerId; | |
| 84 | + | |
| 85 | + /** | |
| 86 | + * 上行消息处理器 | |
| 87 | + */ | |
| 88 | + private final IotEmqxUpstreamHandler upstreamHandler; | |
| 89 | + | |
| 90 | + /** | |
| 91 | + * 下行消息订阅者 | |
| 92 | + */ | |
| 93 | + private final IotEmqxDownstreamSubscriber downstreamSubscriber; | |
| 94 | + | |
| 95 | + public IotEmqxProtocol(ProtocolProperties properties) { | |
| 96 | + Assert.notNull(properties, "协议实例配置不能为空"); | |
| 97 | + Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空"); | |
| 98 | + this.properties = properties; | |
| 99 | + this.emqxConfig = properties.getEmqx(); | |
| 100 | + Assert.notNull(emqxConfig.getConnectTimeoutSeconds(), | |
| 101 | + "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); | |
| 102 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 103 | + this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); | |
| 104 | + | |
| 105 | + // 初始化下行消息订阅者 | |
| 106 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 107 | + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); | |
| 108 | + } | |
| 109 | + | |
| 110 | + @Override | |
| 111 | + public String getId() { | |
| 112 | + return properties.getId(); | |
| 113 | + } | |
| 114 | + | |
| 115 | + @Override | |
| 116 | + public IotProtocolTypeEnum getType() { | |
| 117 | + return IotProtocolTypeEnum.EMQX; | |
| 118 | + } | |
| 119 | + | |
| 120 | + @Override | |
| 121 | + public void start() { | |
| 122 | + if (running) { | |
| 123 | + log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId()); | |
| 124 | + return; | |
| 125 | + } | |
| 126 | + | |
| 127 | + // 1.1 创建 Vertx 实例 | |
| 128 | + this.vertx = Vertx.vertx(); | |
| 129 | + | |
| 130 | + try { | |
| 131 | + // 1.2 启动 HTTP Hook 服务 | |
| 132 | + startHttpServer(); | |
| 133 | + | |
| 134 | + // 1.3 启动 MQTT Client | |
| 135 | + startMqttClient(); | |
| 136 | + running = true; | |
| 137 | + log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]", | |
| 138 | + getId(), properties.getPort(), serverId); | |
| 139 | + | |
| 140 | + // 2. 启动下行消息订阅者 | |
| 141 | + this.downstreamSubscriber.start(); | |
| 142 | + } catch (Exception e) { | |
| 143 | + log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); | |
| 144 | + // 启动失败时,关闭资源 | |
| 145 | + stop0(); | |
| 146 | + throw e; | |
| 147 | + } | |
| 148 | + } | |
| 149 | + | |
| 150 | + @Override | |
| 151 | + public void stop() { | |
| 152 | + if (!running) { | |
| 153 | + return; | |
| 154 | + } | |
| 155 | + stop0(); | |
| 156 | + } | |
| 157 | + | |
| 158 | + private void stop0() { | |
| 159 | + // 1. 停止下行消息订阅者 | |
| 160 | + try { | |
| 161 | + downstreamSubscriber.stop(); | |
| 162 | + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); | |
| 163 | + } catch (Exception e) { | |
| 164 | + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 165 | + } | |
| 166 | + | |
| 167 | + // 2.1 先置为 false:避免 closeHandler 触发重连 | |
| 168 | + running = false; | |
| 169 | + stopMqttClientReconnectChecker(); | |
| 170 | + // 2.2 停止 MQTT Client | |
| 171 | + stopMqttClient(); | |
| 172 | + | |
| 173 | + // 2.3 停止 HTTP Hook 服务 | |
| 174 | + stopHttpServer(); | |
| 175 | + | |
| 176 | + // 2.4 关闭 Vertx | |
| 177 | + if (vertx != null) { | |
| 178 | + try { | |
| 179 | + vertx.close().toCompletionStage().toCompletableFuture() | |
| 180 | + .get(10, TimeUnit.SECONDS); | |
| 181 | + log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId()); | |
| 182 | + } catch (Exception e) { | |
| 183 | + log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e); | |
| 184 | + } | |
| 185 | + vertx = null; | |
| 186 | + } | |
| 187 | + | |
| 188 | + log.info("[stop][IoT EMQX 协议 {} 已停止]", getId()); | |
| 189 | + } | |
| 190 | + | |
| 191 | + // ======================================= HTTP Hook Server ======================================= | |
| 192 | + | |
| 193 | + /** | |
| 194 | + * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event) | |
| 195 | + */ | |
| 196 | + private void startHttpServer() { | |
| 197 | + // 1. 创建路由 | |
| 198 | + Router router = Router.router(vertx); | |
| 199 | + router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 | |
| 200 | + | |
| 201 | + // 2. 创建处理器 | |
| 202 | + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this); | |
| 203 | + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); | |
| 204 | + router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); | |
| 205 | + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); | |
| 206 | + | |
| 207 | + // 3. 启动 HTTP Server(支持 HTTPS) | |
| 208 | + IotEmqxConfig.Http httpConfig = emqxConfig.getHttp(); | |
| 209 | + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); | |
| 210 | + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { | |
| 211 | + Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空"); | |
| 212 | + Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空"); | |
| 213 | + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | |
| 214 | + .setKeyPath(httpConfig.getSslKeyPath()) | |
| 215 | + .setCertPath(httpConfig.getSslCertPath()); | |
| 216 | + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 217 | + } | |
| 218 | + try { | |
| 219 | + httpServer = vertx.createHttpServer(options) | |
| 220 | + .requestHandler(router) | |
| 221 | + .listen() | |
| 222 | + .toCompletionStage().toCompletableFuture() | |
| 223 | + .get(10, TimeUnit.SECONDS); | |
| 224 | + log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]", | |
| 225 | + getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())); | |
| 226 | + } catch (Exception e) { | |
| 227 | + log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e); | |
| 228 | + throw new RuntimeException("HTTP Hook 服务启动失败", e); | |
| 229 | + } | |
| 230 | + } | |
| 231 | + | |
| 232 | + private void stopHttpServer() { | |
| 233 | + if (httpServer == null) { | |
| 234 | + return; | |
| 235 | + } | |
| 236 | + try { | |
| 237 | + httpServer.close().toCompletionStage().toCompletableFuture() | |
| 238 | + .get(5, TimeUnit.SECONDS); | |
| 239 | + log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId()); | |
| 240 | + } catch (Exception e) { | |
| 241 | + log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e); | |
| 242 | + } finally { | |
| 243 | + httpServer = null; | |
| 244 | + } | |
| 245 | + } | |
| 246 | + | |
| 247 | + // ======================================= MQTT Client ====================================== | |
| 248 | + | |
| 249 | + private void startMqttClient() { | |
| 250 | + // 1.1 创建 MQTT Client | |
| 251 | + MqttClient client = createMqttClient(); | |
| 252 | + this.mqttClient = client; | |
| 253 | + // 1.2 连接 MQTT Broker | |
| 254 | + if (!connectMqttClient(client)) { | |
| 255 | + throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败"); | |
| 256 | + } | |
| 257 | + | |
| 258 | + // 2. 启动定时重连检查 | |
| 259 | + startMqttClientReconnectChecker(); | |
| 260 | + } | |
| 261 | + | |
| 262 | + private void stopMqttClient() { | |
| 263 | + MqttClient client = this.mqttClient; | |
| 264 | + this.mqttClient = null; // 先清理引用 | |
| 265 | + if (client == null) { | |
| 266 | + return; | |
| 267 | + } | |
| 268 | + | |
| 269 | + // 1. 批量取消订阅(仅在连接时) | |
| 270 | + if (client.isConnected()) { | |
| 271 | + List<String> topicList = emqxConfig.getMqttTopics(); | |
| 272 | + if (CollUtil.isNotEmpty(topicList)) { | |
| 273 | + try { | |
| 274 | + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() | |
| 275 | + .get(5, TimeUnit.SECONDS); | |
| 276 | + } catch (Exception e) { | |
| 277 | + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); | |
| 278 | + } | |
| 279 | + } | |
| 280 | + } | |
| 281 | + | |
| 282 | + // 2. 断开 MQTT 连接 | |
| 283 | + try { | |
| 284 | + client.disconnect().toCompletionStage().toCompletableFuture() | |
| 285 | + .get(5, TimeUnit.SECONDS); | |
| 286 | + } catch (Exception e) { | |
| 287 | + log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e); | |
| 288 | + } | |
| 289 | + } | |
| 290 | + | |
| 291 | + // ======================================= MQTT 基础方法 ====================================== | |
| 292 | + | |
| 293 | + /** | |
| 294 | + * 创建 MQTT 客户端 | |
| 295 | + * | |
| 296 | + * @return 新创建的 MqttClient | |
| 297 | + */ | |
| 298 | + private MqttClient createMqttClient() { | |
| 299 | + // 1.1 基础配置 | |
| 300 | + MqttClientOptions options = new MqttClientOptions() | |
| 301 | + .setClientId(emqxConfig.getMqttClientId()) | |
| 302 | + .setUsername(emqxConfig.getMqttUsername()) | |
| 303 | + .setPassword(emqxConfig.getMqttPassword()) | |
| 304 | + .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl())) | |
| 305 | + .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession())) | |
| 306 | + .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds()) | |
| 307 | + .setMaxInflightQueue(emqxConfig.getMaxInflightQueue()); | |
| 308 | + options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒 | |
| 309 | + options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll())); | |
| 310 | + // 1.2 配置遗嘱消息 | |
| 311 | + IotEmqxConfig.Will will = emqxConfig.getWill(); | |
| 312 | + if (will != null && will.isEnabled()) { | |
| 313 | + Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空"); | |
| 314 | + Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空"); | |
| 315 | + options.setWillFlag(true) | |
| 316 | + .setWillTopic(will.getTopic()) | |
| 317 | + .setWillMessageBytes(Buffer.buffer(will.getPayload())) | |
| 318 | + .setWillQoS(will.getQos()) | |
| 319 | + .setWillRetain(will.isRetain()); | |
| 320 | + } | |
| 321 | + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空) | |
| 322 | + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); | |
| 323 | + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) | |
| 324 | + && Boolean.FALSE.equals(emqxConfig.getTrustAll()) | |
| 325 | + && sslOptions != null) { | |
| 326 | + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { | |
| 327 | + options.setTrustStoreOptions(new JksOptions() | |
| 328 | + .setPath(sslOptions.getTrustStorePath()) | |
| 329 | + .setPassword(sslOptions.getTrustStorePassword())); | |
| 330 | + } | |
| 331 | + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { | |
| 332 | + options.setKeyStoreOptions(new JksOptions() | |
| 333 | + .setPath(sslOptions.getKeyStorePath()) | |
| 334 | + .setPassword(sslOptions.getKeyStorePassword())); | |
| 335 | + } | |
| 336 | + } | |
| 337 | + | |
| 338 | + // 2. 创建客户端 | |
| 339 | + return MqttClient.create(vertx, options); | |
| 340 | + } | |
| 341 | + | |
| 342 | + /** | |
| 343 | + * 连接 MQTT Broker(同步等待) | |
| 344 | + * | |
| 345 | + * @param client MQTT 客户端 | |
| 346 | + * @return 连接成功返回 true,失败返回 false | |
| 347 | + */ | |
| 348 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | |
| 349 | + private synchronized boolean connectMqttClient(MqttClient client) { | |
| 350 | + String host = emqxConfig.getMqttHost(); | |
| 351 | + int port = emqxConfig.getMqttPort(); | |
| 352 | + int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds(); | |
| 353 | + try { | |
| 354 | + // 1. 连接 Broker | |
| 355 | + client.connect(port, host).toCompletionStage().toCompletableFuture() | |
| 356 | + .get(timeoutSeconds, TimeUnit.SECONDS); | |
| 357 | + log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]", | |
| 358 | + getId(), host, port); | |
| 359 | + | |
| 360 | + // 2. 设置处理器 | |
| 361 | + setupMqttClientHandlers(client); | |
| 362 | + subscribeMqttClientTopics(client); | |
| 363 | + return true; | |
| 364 | + } catch (Exception e) { | |
| 365 | + log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e); | |
| 366 | + return false; | |
| 367 | + } | |
| 368 | + } | |
| 369 | + | |
| 370 | + /** | |
| 371 | + * 关闭 MQTT 客户端 | |
| 372 | + */ | |
| 373 | + private void closeMqttClient() { | |
| 374 | + MqttClient oldClient = this.mqttClient; | |
| 375 | + this.mqttClient = null; // 先清理引用 | |
| 376 | + if (oldClient == null) { | |
| 377 | + return; | |
| 378 | + } | |
| 379 | + // 尽力释放(无论是否连接都尝试 disconnect) | |
| 380 | + try { | |
| 381 | + oldClient.disconnect().toCompletionStage().toCompletableFuture() | |
| 382 | + .get(5, TimeUnit.SECONDS); | |
| 383 | + } catch (Exception ignored) { | |
| 384 | + } | |
| 385 | + } | |
| 386 | + | |
| 387 | + // ======================================= MQTT 重连机制 ====================================== | |
| 388 | + | |
| 389 | + /** | |
| 390 | + * 启动 MQTT Client 周期性重连检查器 | |
| 391 | + */ | |
| 392 | + private void startMqttClientReconnectChecker() { | |
| 393 | + long interval = emqxConfig.getReconnectDelayMs(); | |
| 394 | + this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> { | |
| 395 | + if (!running) { | |
| 396 | + return; | |
| 397 | + } | |
| 398 | + if (mqttClient != null && mqttClient.isConnected()) { | |
| 399 | + return; | |
| 400 | + } | |
| 401 | + log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId()); | |
| 402 | + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) | |
| 403 | + vertx.executeBlocking(() -> { | |
| 404 | + tryReconnectMqttClient(); | |
| 405 | + return null; | |
| 406 | + }); | |
| 407 | + }); | |
| 408 | + } | |
| 409 | + | |
| 410 | + /** | |
| 411 | + * 停止 MQTT Client 重连检查器 | |
| 412 | + */ | |
| 413 | + private void stopMqttClientReconnectChecker() { | |
| 414 | + if (reconnectTimerId != null && vertx != null) { | |
| 415 | + try { | |
| 416 | + vertx.cancelTimer(reconnectTimerId); | |
| 417 | + } catch (Exception ignored) { | |
| 418 | + } | |
| 419 | + reconnectTimerId = null; | |
| 420 | + } | |
| 421 | + } | |
| 422 | + | |
| 423 | + /** | |
| 424 | + * 尝试重连 MQTT Client | |
| 425 | + */ | |
| 426 | + private synchronized void tryReconnectMqttClient() { | |
| 427 | + // 1. 前置检查 | |
| 428 | + if (!running) { | |
| 429 | + return; | |
| 430 | + } | |
| 431 | + if (mqttClient != null && mqttClient.isConnected()) { | |
| 432 | + return; | |
| 433 | + } | |
| 434 | + | |
| 435 | + log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId()); | |
| 436 | + try { | |
| 437 | + // 2. 关闭旧客户端 | |
| 438 | + closeMqttClient(); | |
| 439 | + | |
| 440 | + // 3.1 创建新客户端 | |
| 441 | + MqttClient client = createMqttClient(); | |
| 442 | + this.mqttClient = client; | |
| 443 | + // 3.2 连接(失败只打印日志,等下次定时) | |
| 444 | + if (!connectMqttClient(client)) { | |
| 445 | + log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId()); | |
| 446 | + } | |
| 447 | + } catch (Exception e) { | |
| 448 | + log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e); | |
| 449 | + } | |
| 450 | + } | |
| 451 | + | |
| 452 | + // ======================================= MQTT Handler ====================================== | |
| 453 | + | |
| 454 | + /** | |
| 455 | + * 设置 MQTT Client 事件处理器 | |
| 456 | + */ | |
| 457 | + private void setupMqttClientHandlers(MqttClient client) { | |
| 458 | + // 1. 断开重连监听 | |
| 459 | + client.closeHandler(closeEvent -> { | |
| 460 | + if (!running) { | |
| 461 | + return; | |
| 462 | + } | |
| 463 | + log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId()); | |
| 464 | + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) | |
| 465 | + vertx.executeBlocking(() -> { | |
| 466 | + tryReconnectMqttClient(); | |
| 467 | + return null; | |
| 468 | + }); | |
| 469 | + }); | |
| 470 | + | |
| 471 | + // 2. 异常处理 | |
| 472 | + client.exceptionHandler(exception -> | |
| 473 | + log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception)); | |
| 474 | + | |
| 475 | + // 3. 上行消息处理 | |
| 476 | + client.publishHandler(upstreamHandler::handle); | |
| 477 | + } | |
| 478 | + | |
| 479 | + /** | |
| 480 | + * 订阅 MQTT Client 主题(同步等待) | |
| 481 | + */ | |
| 482 | + private void subscribeMqttClientTopics(MqttClient client) { | |
| 483 | + List<String> topicList = emqxConfig.getMqttTopics(); | |
| 484 | + if (!client.isConnected()) { | |
| 485 | + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId()); | |
| 486 | + return; | |
| 487 | + } | |
| 488 | + if (CollUtil.isEmpty(topicList)) { | |
| 489 | + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId()); | |
| 490 | + return; | |
| 491 | + } | |
| 492 | + // 执行订阅 | |
| 493 | + Map<String, Integer> topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic, | |
| 494 | + topic -> emqxConfig.getMqttQos()); | |
| 495 | + try { | |
| 496 | + client.subscribe(topics).toCompletionStage().toCompletableFuture() | |
| 497 | + .get(10, TimeUnit.SECONDS); | |
| 498 | + log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size()); | |
| 499 | + } catch (Exception e) { | |
| 500 | + log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e); | |
| 501 | + } | |
| 502 | + } | |
| 503 | + | |
| 504 | + /** | |
| 505 | + * 发布消息到 MQTT Broker | |
| 506 | + * | |
| 507 | + * @param topic 主题 | |
| 508 | + * @param payload 消息内容 | |
| 509 | + */ | |
| 510 | + public void publishMessage(String topic, byte[] payload) { | |
| 511 | + if (mqttClient == null || !mqttClient.isConnected()) { | |
| 512 | + log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId()); | |
| 513 | + return; | |
| 514 | + } | |
| 515 | + MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos()); | |
| 516 | + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false) | |
| 517 | + .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); | |
| 518 | + } | |
| 519 | + | |
| 520 | + /** | |
| 521 | + * 延迟发布消息到 MQTT Broker | |
| 522 | + * | |
| 523 | + * @param topic 主题 | |
| 524 | + * @param payload 消息内容 | |
| 525 | + * @param delayMs 延迟时间(毫秒) | |
| 526 | + */ | |
| 527 | + public void publishDelayMessage(String topic, byte[] payload, long delayMs) { | |
| 528 | + vertx.setTimer(delayMs, id -> publishMessage(topic, payload)); | |
| 529 | + } | |
| 530 | + | |
| 531 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; | |
| 2 | - | |
| 3 | -import cn.hutool.core.lang.Assert; | |
| 4 | -import cn.hutool.core.util.StrUtil; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; | |
| 8 | -import io.netty.handler.codec.mqtt.MqttQoS; | |
| 9 | -import io.vertx.core.Vertx; | |
| 10 | -import io.vertx.core.buffer.Buffer; | |
| 11 | -import io.vertx.core.net.JksOptions; | |
| 12 | -import io.vertx.mqtt.MqttClient; | |
| 13 | -import io.vertx.mqtt.MqttClientOptions; | |
| 14 | -import jakarta.annotation.PostConstruct; | |
| 15 | -import jakarta.annotation.PreDestroy; | |
| 16 | -import lombok.Getter; | |
| 17 | -import lombok.extern.slf4j.Slf4j; | |
| 18 | - | |
| 19 | -import java.util.HashMap; | |
| 20 | -import java.util.List; | |
| 21 | -import java.util.Map; | |
| 22 | -import java.util.concurrent.CountDownLatch; | |
| 23 | -import java.util.concurrent.atomic.AtomicBoolean; | |
| 24 | - | |
| 25 | -/** | |
| 26 | - * IoT 网关 EMQX 协议:接收设备上行消息 | |
| 27 | - * | |
| 28 | - * @author 芋道源码 | |
| 29 | - */ | |
| 30 | -@Slf4j | |
| 31 | -public class IotEmqxUpstreamProtocol { | |
| 32 | - | |
| 33 | - private final IotGatewayProperties.EmqxProperties emqxProperties; | |
| 34 | - | |
| 35 | - private volatile boolean isRunning = false; | |
| 36 | - | |
| 37 | - private final Vertx vertx; | |
| 38 | - | |
| 39 | - @Getter | |
| 40 | - private final String serverId; | |
| 41 | - | |
| 42 | - private MqttClient mqttClient; | |
| 43 | - | |
| 44 | - private IotEmqxUpstreamHandler upstreamHandler; | |
| 45 | - | |
| 46 | - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties, | |
| 47 | - Vertx vertx) { | |
| 48 | - this.emqxProperties = emqxProperties; | |
| 49 | - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); | |
| 50 | - this.vertx = vertx; | |
| 51 | - } | |
| 52 | - | |
| 53 | - @PostConstruct | |
| 54 | - public void start() { | |
| 55 | - if (isRunning) { | |
| 56 | - return; | |
| 57 | - } | |
| 58 | - | |
| 59 | - try { | |
| 60 | - // 1. 启动 MQTT 客户端 | |
| 61 | - startMqttClient(); | |
| 62 | - | |
| 63 | - // 2. 标记服务为运行状态 | |
| 64 | - isRunning = true; | |
| 65 | - log.info("[start][IoT 网关 EMQX 协议启动成功]"); | |
| 66 | - } catch (Exception e) { | |
| 67 | - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); | |
| 68 | - stop(); | |
| 69 | - | |
| 70 | - // 异步关闭应用 | |
| 71 | - Thread shutdownThread = new Thread(() -> { | |
| 72 | - try { | |
| 73 | - // 确保日志输出完成,使用更优雅的方式 | |
| 74 | - log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); | |
| 75 | - // 等待日志输出完成 | |
| 76 | - Thread.sleep(1000); | |
| 77 | - } catch (InterruptedException ie) { | |
| 78 | - Thread.currentThread().interrupt(); | |
| 79 | - log.warn("[start][应用关闭被中断]"); | |
| 80 | - } | |
| 81 | - System.exit(1); | |
| 82 | - }); | |
| 83 | - shutdownThread.setDaemon(true); | |
| 84 | - shutdownThread.setName("emergency-shutdown"); | |
| 85 | - shutdownThread.start(); | |
| 86 | - | |
| 87 | - throw e; | |
| 88 | - } | |
| 89 | - } | |
| 90 | - | |
| 91 | - @PreDestroy | |
| 92 | - public void stop() { | |
| 93 | - if (!isRunning) { | |
| 94 | - return; | |
| 95 | - } | |
| 96 | - | |
| 97 | - // 1. 停止 MQTT 客户端 | |
| 98 | - stopMqttClient(); | |
| 99 | - | |
| 100 | - // 2. 标记服务为停止状态 | |
| 101 | - isRunning = false; | |
| 102 | - log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); | |
| 103 | - } | |
| 104 | - | |
| 105 | - /** | |
| 106 | - * 启动 MQTT 客户端 | |
| 107 | - */ | |
| 108 | - private void startMqttClient() { | |
| 109 | - try { | |
| 110 | - // 1. 初始化消息处理器 | |
| 111 | - this.upstreamHandler = new IotEmqxUpstreamHandler(this); | |
| 112 | - | |
| 113 | - // 2. 创建 MQTT 客户端 | |
| 114 | - createMqttClient(); | |
| 115 | - | |
| 116 | - // 3. 同步连接 MQTT Broker | |
| 117 | - connectMqttSync(); | |
| 118 | - } catch (Exception e) { | |
| 119 | - log.error("[startMqttClient][MQTT 客户端启动失败]", e); | |
| 120 | - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); | |
| 121 | - } | |
| 122 | - } | |
| 123 | - | |
| 124 | - /** | |
| 125 | - * 同步连接 MQTT Broker | |
| 126 | - */ | |
| 127 | - private void connectMqttSync() { | |
| 128 | - String host = emqxProperties.getMqttHost(); | |
| 129 | - int port = emqxProperties.getMqttPort(); | |
| 130 | - // 1. 连接 MQTT Broker | |
| 131 | - CountDownLatch latch = new CountDownLatch(1); | |
| 132 | - AtomicBoolean success = new AtomicBoolean(false); | |
| 133 | - mqttClient.connect(port, host, connectResult -> { | |
| 134 | - if (connectResult.succeeded()) { | |
| 135 | - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); | |
| 136 | - setupMqttHandlers(); | |
| 137 | - subscribeToTopics(); | |
| 138 | - success.set(true); | |
| 139 | - } else { | |
| 140 | - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", | |
| 141 | - host, port, connectResult.cause()); | |
| 142 | - } | |
| 143 | - latch.countDown(); | |
| 144 | - }); | |
| 145 | - | |
| 146 | - // 2. 等待连接结果 | |
| 147 | - try { | |
| 148 | - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制 | |
| 149 | - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); | |
| 150 | - if (!awaitResult) { | |
| 151 | - log.error("[connectMqttSync][等待连接结果超时]"); | |
| 152 | - throw new RuntimeException("连接 MQTT Broker 超时"); | |
| 153 | - } | |
| 154 | - if (!success.get()) { | |
| 155 | - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port)); | |
| 156 | - } | |
| 157 | - } catch (InterruptedException e) { | |
| 158 | - Thread.currentThread().interrupt(); | |
| 159 | - log.error("[connectMqttSync][等待连接结果被中断]", e); | |
| 160 | - throw new RuntimeException("连接 MQTT Broker 被中断", e); | |
| 161 | - } | |
| 162 | - } | |
| 163 | - | |
| 164 | - /** | |
| 165 | - * 异步连接 MQTT Broker | |
| 166 | - */ | |
| 167 | - private void connectMqttAsync() { | |
| 168 | - String host = emqxProperties.getMqttHost(); | |
| 169 | - int port = emqxProperties.getMqttPort(); | |
| 170 | - mqttClient.connect(port, host, connectResult -> { | |
| 171 | - if (connectResult.succeeded()) { | |
| 172 | - log.info("[connectMqttAsync][MQTT 客户端重连成功]"); | |
| 173 | - setupMqttHandlers(); | |
| 174 | - subscribeToTopics(); | |
| 175 | - } else { | |
| 176 | - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", | |
| 177 | - host, port, connectResult.cause()); | |
| 178 | - log.warn("[connectMqttAsync][重连失败,将再次尝试]"); | |
| 179 | - reconnectWithDelay(); | |
| 180 | - } | |
| 181 | - }); | |
| 182 | - } | |
| 183 | - | |
| 184 | - /** | |
| 185 | - * 延迟重连 | |
| 186 | - */ | |
| 187 | - private void reconnectWithDelay() { | |
| 188 | - if (!isRunning) { | |
| 189 | - return; | |
| 190 | - } | |
| 191 | - if (mqttClient != null && mqttClient.isConnected()) { | |
| 192 | - return; | |
| 193 | - } | |
| 194 | - | |
| 195 | - long delay = emqxProperties.getReconnectDelayMs(); | |
| 196 | - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); | |
| 197 | - vertx.setTimer(delay, timerId -> { | |
| 198 | - if (!isRunning) { | |
| 199 | - return; | |
| 200 | - } | |
| 201 | - if (mqttClient != null && mqttClient.isConnected()) { | |
| 202 | - return; | |
| 203 | - } | |
| 204 | - | |
| 205 | - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); | |
| 206 | - try { | |
| 207 | - createMqttClient(); | |
| 208 | - connectMqttAsync(); | |
| 209 | - } catch (Exception e) { | |
| 210 | - log.error("[reconnectWithDelay][重连过程中发生异常]", e); | |
| 211 | - vertx.setTimer(delay, t -> reconnectWithDelay()); | |
| 212 | - } | |
| 213 | - }); | |
| 214 | - } | |
| 215 | - | |
| 216 | - /** | |
| 217 | - * 停止 MQTT 客户端 | |
| 218 | - */ | |
| 219 | - private void stopMqttClient() { | |
| 220 | - if (mqttClient == null) { | |
| 221 | - return; | |
| 222 | - } | |
| 223 | - try { | |
| 224 | - if (mqttClient.isConnected()) { | |
| 225 | - // 1. 取消订阅所有主题 | |
| 226 | - List<String> topicList = emqxProperties.getMqttTopics(); | |
| 227 | - for (String topic : topicList) { | |
| 228 | - try { | |
| 229 | - mqttClient.unsubscribe(topic); | |
| 230 | - } catch (Exception e) { | |
| 231 | - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | |
| 232 | - } | |
| 233 | - } | |
| 234 | - | |
| 235 | - // 2. 断开 MQTT 客户端连接 | |
| 236 | - try { | |
| 237 | - CountDownLatch disconnectLatch = new CountDownLatch(1); | |
| 238 | - mqttClient.disconnect(ar -> disconnectLatch.countDown()); | |
| 239 | - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { | |
| 240 | - log.warn("[stopMqttClient][断开 MQTT 连接超时]"); | |
| 241 | - } | |
| 242 | - } catch (Exception e) { | |
| 243 | - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); | |
| 244 | - } | |
| 245 | - } | |
| 246 | - } catch (Exception e) { | |
| 247 | - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); | |
| 248 | - } finally { | |
| 249 | - mqttClient = null; | |
| 250 | - } | |
| 251 | - } | |
| 252 | - | |
| 253 | - /** | |
| 254 | - * 创建 MQTT 客户端 | |
| 255 | - */ | |
| 256 | - private void createMqttClient() { | |
| 257 | - // 1.1 创建基础配置 | |
| 258 | - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions() | |
| 259 | - .setClientId(emqxProperties.getMqttClientId()) | |
| 260 | - .setUsername(emqxProperties.getMqttUsername()) | |
| 261 | - .setPassword(emqxProperties.getMqttPassword()) | |
| 262 | - .setSsl(emqxProperties.getMqttSsl()) | |
| 263 | - .setCleanSession(emqxProperties.getCleanSession()) | |
| 264 | - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds()) | |
| 265 | - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue()) | |
| 266 | - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒 | |
| 267 | - .setTrustAll(emqxProperties.getTrustAll()); | |
| 268 | - // 1.2 配置遗嘱消息 | |
| 269 | - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill(); | |
| 270 | - if (will.isEnabled()) { | |
| 271 | - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空"); | |
| 272 | - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空"); | |
| 273 | - options.setWillFlag(true) | |
| 274 | - .setWillTopic(will.getTopic()) | |
| 275 | - .setWillMessageBytes(Buffer.buffer(will.getPayload())) | |
| 276 | - .setWillQoS(will.getQos()) | |
| 277 | - .setWillRetain(will.isRetain()); | |
| 278 | - } | |
| 279 | - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效) | |
| 280 | - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) { | |
| 281 | - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions(); | |
| 282 | - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { | |
| 283 | - options.setTrustStoreOptions(new JksOptions() | |
| 284 | - .setPath(sslOptions.getTrustStorePath()) | |
| 285 | - .setPassword(sslOptions.getTrustStorePassword())); | |
| 286 | - } | |
| 287 | - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { | |
| 288 | - options.setKeyStoreOptions(new JksOptions() | |
| 289 | - .setPath(sslOptions.getKeyStorePath()) | |
| 290 | - .setPassword(sslOptions.getKeyStorePassword())); | |
| 291 | - } | |
| 292 | - } | |
| 293 | - // 1.4 安全警告日志 | |
| 294 | - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) { | |
| 295 | - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]"); | |
| 296 | - } | |
| 297 | - | |
| 298 | - // 2. 创建客户端实例 | |
| 299 | - this.mqttClient = MqttClient.create(vertx, options); | |
| 300 | - } | |
| 301 | - | |
| 302 | - /** | |
| 303 | - * 设置 MQTT 处理器 | |
| 304 | - */ | |
| 305 | - private void setupMqttHandlers() { | |
| 306 | - // 1. 设置断开重连监听器 | |
| 307 | - mqttClient.closeHandler(closeEvent -> { | |
| 308 | - if (!isRunning) { | |
| 309 | - return; | |
| 310 | - } | |
| 311 | - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); | |
| 312 | - reconnectWithDelay(); | |
| 313 | - }); | |
| 314 | - | |
| 315 | - // 2. 设置异常处理器 | |
| 316 | - mqttClient.exceptionHandler(exception -> | |
| 317 | - log.error("[exceptionHandler][MQTT 客户端异常]", exception)); | |
| 318 | - | |
| 319 | - // 3. 设置消息处理器 | |
| 320 | - mqttClient.publishHandler(upstreamHandler::handle); | |
| 321 | - } | |
| 322 | - | |
| 323 | - /** | |
| 324 | - * 订阅设备上行消息主题 | |
| 325 | - */ | |
| 326 | - private void subscribeToTopics() { | |
| 327 | - // 1. 校验 MQTT 客户端是否连接 | |
| 328 | - List<String> topicList = emqxProperties.getMqttTopics(); | |
| 329 | - if (mqttClient == null || !mqttClient.isConnected()) { | |
| 330 | - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); | |
| 331 | - return; | |
| 332 | - } | |
| 333 | - | |
| 334 | - // 2. 批量订阅所有主题 | |
| 335 | - Map<String, Integer> topics = new HashMap<>(); | |
| 336 | - int qos = emqxProperties.getMqttQos(); | |
| 337 | - for (String topic : topicList) { | |
| 338 | - topics.put(topic, qos); | |
| 339 | - } | |
| 340 | - mqttClient.subscribe(topics, subscribeResult -> { | |
| 341 | - if (subscribeResult.succeeded()) { | |
| 342 | - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); | |
| 343 | - } else { | |
| 344 | - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]", | |
| 345 | - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause()); | |
| 346 | - } | |
| 347 | - }); | |
| 348 | - } | |
| 349 | - | |
| 350 | - /** | |
| 351 | - * 发布消息到 MQTT Broker | |
| 352 | - * | |
| 353 | - * @param topic 主题 | |
| 354 | - * @param payload 消息内容 | |
| 355 | - */ | |
| 356 | - public void publishMessage(String topic, byte[] payload) { | |
| 357 | - if (mqttClient == null || !mqttClient.isConnected()) { | |
| 358 | - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); | |
| 359 | - return; | |
| 360 | - } | |
| 361 | - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); | |
| 362 | - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); | |
| 363 | - } | |
| 364 | - | |
| 365 | -} | |
| 366 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | 11 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; |
| ... | ... | @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; |
| 21 | 21 | @Slf4j |
| 22 | 22 | public class IotEmqxDownstreamHandler { |
| 23 | 23 | |
| 24 | - private final IotEmqxUpstreamProtocol protocol; | |
| 24 | + private final IotEmqxProtocol protocol; | |
| 25 | 25 | |
| 26 | 26 | private final IotDeviceService deviceService; |
| 27 | 27 | |
| 28 | 28 | private final IotDeviceMessageService deviceMessageService; |
| 29 | 29 | |
| 30 | - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { | |
| 30 | + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) { | |
| 31 | 31 | this.protocol = protocol; |
| 32 | 32 | this.deviceService = SpringUtil.getBean(IotDeviceService.class); |
| 33 | 33 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| ... | ... | @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { |
| 53 | 53 | return; |
| 54 | 54 | } |
| 55 | 55 | // 2.2 构建载荷 |
| 56 | - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), | |
| 56 | + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), | |
| 57 | 57 | deviceInfo.getDeviceName()); |
| 58 | - // 2.3 发布消息 | |
| 58 | + | |
| 59 | + // 3. 发布消息 | |
| 59 | 60 | protocol.publishMessage(topic, payload); |
| 60 | 61 | } |
| 61 | 62 | |
| ... | ... | @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { |
| 74 | 75 | return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); |
| 75 | 76 | } |
| 76 | 77 | |
| 77 | -} | |
| 78 | 78 | \ No newline at end of file |
| 79 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT 网关 EMQX 订阅者:接收下行给设备的消息 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + private final IotEmqxDownstreamHandler downstreamHandler; | |
| 18 | + | |
| 19 | + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { | |
| 20 | + super(protocol, messageBus); | |
| 21 | + this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); | |
| 22 | + } | |
| 23 | + | |
| 24 | + @Override | |
| 25 | + protected void handleMessage(IotDeviceMessage message) { | |
| 26 | + downstreamHandler.handle(message); | |
| 27 | + } | |
| 28 | + | |
| 29 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; | |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 3 | 4 | import cn.hutool.core.util.BooleanUtil; |
| 4 | 5 | import cn.hutool.core.util.StrUtil; |
| 5 | 6 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 8 | 9 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 9 | 11 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 13 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 14 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 11 | 15 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 12 | 17 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 13 | 19 | import io.vertx.core.json.JsonObject; |
| 14 | 20 | import io.vertx.ext.web.RoutingContext; |
| 15 | 21 | import lombok.extern.slf4j.Slf4j; |
| 16 | 22 | |
| 23 | +import java.util.Locale; | |
| 24 | + | |
| 17 | 25 | /** |
| 18 | 26 | * IoT 网关 EMQX 认证事件处理器 |
| 19 | 27 | * <p> |
| 20 | 28 | * 为 EMQX 提供 HTTP 接口服务,包括: |
| 21 | - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 | |
| 22 | - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 | |
| 29 | + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} | |
| 30 | + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} | |
| 31 | + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} | |
| 32 | + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)} | |
| 23 | 33 | * |
| 24 | 34 | * @author 芋道源码 |
| 25 | 35 | */ |
| ... | ... | @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { |
| 45 | 55 | private static final String RESULT_IGNORE = "ignore"; |
| 46 | 56 | |
| 47 | 57 | /** |
| 48 | - * EMQX 事件类型常量 | |
| 58 | + * EMQX 事件类型常量 - 客户端连接 | |
| 49 | 59 | */ |
| 50 | 60 | private static final String EVENT_CLIENT_CONNECTED = "client.connected"; |
| 61 | + /** | |
| 62 | + * EMQX 事件类型常量 - 客户端断开连接 | |
| 63 | + */ | |
| 51 | 64 | private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; |
| 52 | 65 | |
| 66 | + /** | |
| 67 | + * 认证类型标识 - 设备注册 | |
| 68 | + */ | |
| 69 | + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; | |
| 70 | + | |
| 53 | 71 | private final String serverId; |
| 54 | 72 | |
| 55 | - private final IotDeviceMessageService deviceMessageService; | |
| 73 | + private final IotEmqxProtocol protocol; | |
| 56 | 74 | |
| 75 | + private final IotDeviceMessageService deviceMessageService; | |
| 57 | 76 | private final IotDeviceCommonApi deviceApi; |
| 58 | 77 | |
| 59 | - public IotEmqxAuthEventHandler(String serverId) { | |
| 78 | + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { | |
| 60 | 79 | this.serverId = serverId; |
| 80 | + this.protocol = protocol; | |
| 61 | 81 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 62 | 82 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 63 | 83 | } |
| 64 | 84 | |
| 85 | + // ========== 认证处理 ========== | |
| 86 | + | |
| 65 | 87 | /** |
| 66 | 88 | * EMQX 认证接口 |
| 67 | 89 | */ |
| 68 | 90 | public void handleAuth(RoutingContext context) { |
| 91 | + JsonObject body = null; | |
| 69 | 92 | try { |
| 70 | 93 | // 1. 参数校验 |
| 71 | - JsonObject body = parseRequestBody(context); | |
| 94 | + body = parseRequestBody(context); | |
| 72 | 95 | if (body == null) { |
| 73 | 96 | return; |
| 74 | 97 | } |
| ... | ... | @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { |
| 82 | 105 | return; |
| 83 | 106 | } |
| 84 | 107 | |
| 85 | - // 2. 执行认证 | |
| 108 | + // 2.1 情况一:判断是否为注册请求 | |
| 109 | + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { | |
| 110 | + handleDeviceRegister(context, username, password); | |
| 111 | + return; | |
| 112 | + } | |
| 113 | + | |
| 114 | + // 2.2 情况二:执行认证 | |
| 86 | 115 | boolean authResult = handleDeviceAuth(clientId, username, password); |
| 87 | 116 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); |
| 88 | 117 | if (authResult) { |
| ... | ... | @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler { |
| 91 | 120 | sendAuthResponse(context, RESULT_DENY); |
| 92 | 121 | } |
| 93 | 122 | } catch (Exception e) { |
| 94 | - log.error("[handleAuth][设备认证异常]", e); | |
| 123 | + log.error("[handleAuth][设备认证异常][body={}]", body, e); | |
| 95 | 124 | sendAuthResponse(context, RESULT_IGNORE); |
| 96 | 125 | } |
| 97 | 126 | } |
| 98 | 127 | |
| 99 | 128 | /** |
| 129 | + * 解析认证接口请求体 | |
| 130 | + * <p> | |
| 131 | + * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) | |
| 132 | + * | |
| 133 | + * @param context 路由上下文 | |
| 134 | + * @return 请求体JSON对象,解析失败时返回null | |
| 135 | + */ | |
| 136 | + private JsonObject parseRequestBody(RoutingContext context) { | |
| 137 | + try { | |
| 138 | + JsonObject body = context.body().asJsonObject(); | |
| 139 | + if (body == null) { | |
| 140 | + log.info("[parseRequestBody][请求体为空]"); | |
| 141 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 142 | + return null; | |
| 143 | + } | |
| 144 | + return body; | |
| 145 | + } catch (Exception e) { | |
| 146 | + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); | |
| 147 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 148 | + return null; | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + /** | |
| 153 | + * 执行设备认证 | |
| 154 | + * | |
| 155 | + * @param clientId 客户端ID | |
| 156 | + * @param username 用户名 | |
| 157 | + * @param password 密码 | |
| 158 | + * @return 认证是否成功 | |
| 159 | + */ | |
| 160 | + private boolean handleDeviceAuth(String clientId, String username, String password) { | |
| 161 | + try { | |
| 162 | + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 163 | + .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 164 | + result.checkError(); | |
| 165 | + return BooleanUtil.isTrue(result.getData()); | |
| 166 | + } catch (Exception e) { | |
| 167 | + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); | |
| 168 | + throw e; | |
| 169 | + } | |
| 170 | + } | |
| 171 | + | |
| 172 | + /** | |
| 173 | + * 发送 EMQX 认证响应 | |
| 174 | + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 | |
| 175 | + * | |
| 176 | + * @param context 路由上下文 | |
| 177 | + * @param result 认证结果:allow、deny、ignore | |
| 178 | + */ | |
| 179 | + private void sendAuthResponse(RoutingContext context, String result) { | |
| 180 | + // 构建符合 EMQX 官方规范的响应 | |
| 181 | + JsonObject response = new JsonObject() | |
| 182 | + .put("result", result) | |
| 183 | + .put("is_superuser", false); | |
| 184 | + // 可以根据业务需求添加客户端属性 | |
| 185 | + // response.put("client_attrs", new JsonObject().put("role", "device")); | |
| 186 | + // 可以添加认证过期时间(可选) | |
| 187 | + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); | |
| 188 | + | |
| 189 | + // 回复响应 | |
| 190 | + context.response() | |
| 191 | + .setStatusCode(SUCCESS_STATUS_CODE) | |
| 192 | + .putHeader("Content-Type", "application/json; charset=utf-8") | |
| 193 | + .end(response.encode()); | |
| 194 | + } | |
| 195 | + | |
| 196 | + // ========== ACL 处理 ========== | |
| 197 | + | |
| 198 | + /** | |
| 199 | + * EMQX ACL 接口 | |
| 200 | + * <p> | |
| 201 | + * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。 | |
| 202 | + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。 | |
| 203 | + */ | |
| 204 | + public void handleAcl(RoutingContext context) { | |
| 205 | + JsonObject body = null; | |
| 206 | + try { | |
| 207 | + // 1.1 解析请求体 | |
| 208 | + body = parseRequestBody(context); | |
| 209 | + if (body == null) { | |
| 210 | + return; | |
| 211 | + } | |
| 212 | + String username = body.getString("username"); | |
| 213 | + String topic = body.getString("topic"); | |
| 214 | + if (StrUtil.hasBlank(username, topic)) { | |
| 215 | + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic); | |
| 216 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 217 | + return; | |
| 218 | + } | |
| 219 | + // 1.2 解析设备身份 | |
| 220 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 221 | + if (deviceInfo == null) { | |
| 222 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 223 | + return; | |
| 224 | + } | |
| 225 | + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段) | |
| 226 | + Boolean subscribe = parseAclSubscribeFlag(body); | |
| 227 | + if (subscribe == null) { | |
| 228 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 229 | + return; | |
| 230 | + } | |
| 231 | + | |
| 232 | + // 2. 执行 ACL 校验 | |
| 233 | + boolean allowed = subscribe | |
| 234 | + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()) | |
| 235 | + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 236 | + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY); | |
| 237 | + } catch (Exception e) { | |
| 238 | + log.error("[handleAcl][ACL 处理失败][body={}]", body, e); | |
| 239 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 240 | + } | |
| 241 | + } | |
| 242 | + | |
| 243 | + /** | |
| 244 | + * 解析 ACL 动作类型:订阅/发布 | |
| 245 | + * | |
| 246 | + * @param body ACL 请求体 | |
| 247 | + * @return true 订阅;false 发布;null 不识别 | |
| 248 | + */ | |
| 249 | + private static Boolean parseAclSubscribeFlag(JsonObject body) { | |
| 250 | + // 1. action 字段(常见为 publish/subscribe) | |
| 251 | + String action = body.getString("action"); | |
| 252 | + if (StrUtil.isNotBlank(action)) { | |
| 253 | + String lower = action.toLowerCase(Locale.ROOT); | |
| 254 | + if (lower.contains("sub")) { | |
| 255 | + return true; | |
| 256 | + } | |
| 257 | + if (lower.contains("pub")) { | |
| 258 | + return false; | |
| 259 | + } | |
| 260 | + } | |
| 261 | + | |
| 262 | + // 2. access 字段:可能是数字或字符串 | |
| 263 | + Integer access = body.getInteger("access"); | |
| 264 | + if (access != null) { | |
| 265 | + if (access == 1) { | |
| 266 | + return true; | |
| 267 | + } | |
| 268 | + if (access == 2) { | |
| 269 | + return false; | |
| 270 | + } | |
| 271 | + } | |
| 272 | + String accessText = body.getString("access"); | |
| 273 | + if (StrUtil.isNotBlank(accessText)) { | |
| 274 | + String lower = accessText.toLowerCase(Locale.ROOT); | |
| 275 | + if (lower.contains("sub")) { | |
| 276 | + return true; | |
| 277 | + } | |
| 278 | + if (lower.contains("pub")) { | |
| 279 | + return false; | |
| 280 | + } | |
| 281 | + if (StrUtil.isNumeric(accessText)) { | |
| 282 | + int value = Integer.parseInt(accessText); | |
| 283 | + if (value == 1) { | |
| 284 | + return true; | |
| 285 | + } | |
| 286 | + if (value == 2) { | |
| 287 | + return false; | |
| 288 | + } | |
| 289 | + } | |
| 290 | + } | |
| 291 | + return null; | |
| 292 | + } | |
| 293 | + | |
| 294 | + // ========== 事件处理 ========== | |
| 295 | + | |
| 296 | + /** | |
| 100 | 297 | * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 |
| 101 | 298 | * 支持的事件类型:client.connected、client.disconnected 等 |
| 102 | 299 | */ |
| ... | ... | @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler { |
| 124 | 321 | break; |
| 125 | 322 | } |
| 126 | 323 | |
| 127 | - // EMQX Webhook 只需要 200 状态码,无需响应体 | |
| 324 | + // 3. EMQX Webhook 只需要 200 状态码,无需响应体 | |
| 128 | 325 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 129 | 326 | } catch (Exception e) { |
| 130 | - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e); | |
| 131 | - // 即使处理失败,也返回 200 避免EMQX重试 | |
| 327 | + log.error("[handleEvent][事件处理失败][body={}]", body, e); | |
| 328 | + // 即使处理失败,也返回 200 避免 EMQX 重试 | |
| 132 | 329 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 133 | 330 | } |
| 134 | 331 | } |
| 135 | 332 | |
| 136 | 333 | /** |
| 137 | - * 处理客户端连接事件 | |
| 138 | - */ | |
| 139 | - private void handleClientConnected(JsonObject body) { | |
| 140 | - String username = body.getString("username"); | |
| 141 | - log.info("[handleClientConnected][设备上线: {}]", username); | |
| 142 | - handleDeviceStateChange(username, true); | |
| 143 | - } | |
| 144 | - | |
| 145 | - /** | |
| 146 | - * 处理客户端断开连接事件 | |
| 147 | - */ | |
| 148 | - private void handleClientDisconnected(JsonObject body) { | |
| 149 | - String username = body.getString("username"); | |
| 150 | - String reason = body.getString("reason"); | |
| 151 | - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); | |
| 152 | - handleDeviceStateChange(username, false); | |
| 153 | - } | |
| 154 | - | |
| 155 | - /** | |
| 156 | - * 解析认证接口请求体 | |
| 157 | - * <p> | |
| 158 | - * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) | |
| 159 | - * | |
| 160 | - * @param context 路由上下文 | |
| 161 | - * @return 请求体JSON对象,解析失败时返回null | |
| 162 | - */ | |
| 163 | - private JsonObject parseRequestBody(RoutingContext context) { | |
| 164 | - try { | |
| 165 | - JsonObject body = context.body().asJsonObject(); | |
| 166 | - if (body == null) { | |
| 167 | - log.info("[parseRequestBody][请求体为空]"); | |
| 168 | - sendAuthResponse(context, RESULT_IGNORE); | |
| 169 | - return null; | |
| 170 | - } | |
| 171 | - return body; | |
| 172 | - } catch (Exception e) { | |
| 173 | - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); | |
| 174 | - sendAuthResponse(context, RESULT_IGNORE); | |
| 175 | - return null; | |
| 176 | - } | |
| 177 | - } | |
| 178 | - | |
| 179 | - /** | |
| 180 | 334 | * 解析事件接口请求体 |
| 181 | 335 | * <p> |
| 182 | 336 | * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范) |
| ... | ... | @@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler { |
| 201 | 355 | } |
| 202 | 356 | |
| 203 | 357 | /** |
| 204 | - * 执行设备认证 | |
| 205 | - * | |
| 206 | - * @param clientId 客户端ID | |
| 207 | - * @param username 用户名 | |
| 208 | - * @param password 密码 | |
| 209 | - * @return 认证是否成功 | |
| 358 | + * 处理客户端连接事件 | |
| 210 | 359 | */ |
| 211 | - private boolean handleDeviceAuth(String clientId, String username, String password) { | |
| 212 | - try { | |
| 213 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 214 | - .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 215 | - result.checkError(); | |
| 216 | - return BooleanUtil.isTrue(result.getData()); | |
| 217 | - } catch (Exception e) { | |
| 218 | - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); | |
| 219 | - throw e; | |
| 220 | - } | |
| 360 | + private void handleClientConnected(JsonObject body) { | |
| 361 | + String username = body.getString("username"); | |
| 362 | + log.info("[handleClientConnected][设备上线: {}]", username); | |
| 363 | + handleDeviceStateChange(username, true); | |
| 364 | + } | |
| 365 | + | |
| 366 | + /** | |
| 367 | + * 处理客户端断开连接事件 | |
| 368 | + */ | |
| 369 | + private void handleClientDisconnected(JsonObject body) { | |
| 370 | + String username = body.getString("username"); | |
| 371 | + String reason = body.getString("reason"); | |
| 372 | + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); | |
| 373 | + handleDeviceStateChange(username, false); | |
| 221 | 374 | } |
| 222 | 375 | |
| 223 | 376 | /** |
| ... | ... | @@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler { |
| 247 | 400 | } |
| 248 | 401 | } |
| 249 | 402 | |
| 403 | + // ========= 注册处理 ========= | |
| 404 | + | |
| 250 | 405 | /** |
| 251 | - * 发送 EMQX 认证响应 | |
| 252 | - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 | |
| 406 | + * 处理设备注册请求(一型一密) | |
| 253 | 407 | * |
| 254 | - * @param context 路由上下文 | |
| 255 | - * @param result 认证结果:allow、deny、ignore | |
| 408 | + * @param context 路由上下文 | |
| 409 | + * @param username 用户名 | |
| 410 | + * @param password 密码(签名) | |
| 256 | 411 | */ |
| 257 | - private void sendAuthResponse(RoutingContext context, String result) { | |
| 258 | - // 构建符合 EMQX 官方规范的响应 | |
| 259 | - JsonObject response = new JsonObject() | |
| 260 | - .put("result", result) | |
| 261 | - .put("is_superuser", false); | |
| 412 | + private void handleDeviceRegister(RoutingContext context, String username, String password) { | |
| 413 | + try { | |
| 414 | + // 1. 解析设备信息 | |
| 415 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 416 | + if (deviceInfo == null) { | |
| 417 | + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username); | |
| 418 | + sendAuthResponse(context, RESULT_DENY); | |
| 419 | + return; | |
| 420 | + } | |
| 262 | 421 | |
| 263 | - // 可以根据业务需求添加客户端属性 | |
| 264 | - // response.put("client_attrs", new JsonObject().put("role", "device")); | |
| 422 | + // 2. 调用注册 API | |
| 423 | + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() | |
| 424 | + .setProductKey(deviceInfo.getProductKey()) | |
| 425 | + .setDeviceName(deviceInfo.getDeviceName()) | |
| 426 | + .setSign(password); | |
| 427 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 428 | + result.checkError(); | |
| 265 | 429 | |
| 266 | - // 可以添加认证过期时间(可选) | |
| 267 | - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); | |
| 430 | + // 3. 允许连接 | |
| 431 | + log.info("[handleDeviceRegister][设备注册成功: {}]", username); | |
| 432 | + sendAuthResponse(context, RESULT_ALLOW); | |
| 268 | 433 | |
| 269 | - context.response() | |
| 270 | - .setStatusCode(SUCCESS_STATUS_CODE) | |
| 271 | - .putHeader("Content-Type", "application/json; charset=utf-8") | |
| 272 | - .end(response.encode()); | |
| 434 | + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅) | |
| 435 | + sendRegisterResultMessage(username, result.getData()); | |
| 436 | + } catch (Exception e) { | |
| 437 | + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage()); | |
| 438 | + sendAuthResponse(context, RESULT_DENY); | |
| 439 | + } | |
| 440 | + } | |
| 441 | + | |
| 442 | + /** | |
| 443 | + * 发送注册结果消息给设备 | |
| 444 | + * <p> | |
| 445 | + * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。 | |
| 446 | + * | |
| 447 | + * @param username 用户名 | |
| 448 | + * @param result 注册结果 | |
| 449 | + */ | |
| 450 | + @SuppressWarnings("DataFlowIssue") | |
| 451 | + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) { | |
| 452 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 453 | + Assert.notNull(deviceInfo, "设备信息不能为空"); | |
| 454 | + try { | |
| 455 | + // 1.1 构建响应消息 | |
| 456 | + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); | |
| 457 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null); | |
| 458 | + // 1.2 序列化消息 | |
| 459 | + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, | |
| 460 | + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON); | |
| 461 | + // 1.3 构建响应主题 | |
| 462 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, | |
| 463 | + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true); | |
| 464 | + | |
| 465 | + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅) | |
| 466 | + protocol.publishDelayMessage(replyTopic, encodedData, 5000); | |
| 467 | + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic); | |
| 468 | + } catch (Exception e) { | |
| 469 | + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e); | |
| 470 | + } | |
| 273 | 471 | } |
| 274 | 472 | |
| 275 | -} | |
| 276 | 473 | \ No newline at end of file |
| 474 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; | |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 3 | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | 8 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler { |
| 20 | 20 | |
| 21 | 21 | private final String serverId; |
| 22 | 22 | |
| 23 | - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { | |
| 23 | + public IotEmqxUpstreamHandler(String serverId) { | |
| 24 | 24 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 25 | - this.serverId = protocol.getServerId(); | |
| 25 | + this.serverId = serverId; | |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | 28 | /** |
| 29 | 29 | * 处理 MQTT 发布消息 |
| 30 | 30 | */ |
| 31 | 31 | public void handle(MqttPublishMessage mqttMessage) { |
| 32 | - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); | |
| 32 | + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); | |
| 33 | 33 | String topic = mqttMessage.topicName(); |
| 34 | 34 | byte[] payload = mqttMessage.payload().getBytes(); |
| 35 | 35 | try { |
| 36 | 36 | // 1. 解析主题,一次性获取所有信息 |
| 37 | 37 | String[] topicParts = topic.split("/"); |
| 38 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 38 | + String productKey = ArrayUtil.get(topicParts, 2); | |
| 39 | + String deviceName = ArrayUtil.get(topicParts, 3); | |
| 40 | + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) { | |
| 39 | 41 | log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); |
| 40 | 42 | return; |
| 41 | 43 | } |
| 42 | 44 | |
| 43 | - String productKey = topicParts[2]; | |
| 44 | - String deviceName = topicParts[3]; | |
| 45 | - | |
| 46 | - // 3. 解码消息 | |
| 47 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 45 | + // 2. 反序列化消息 | |
| 46 | + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 48 | 47 | if (message == null) { |
| 49 | 48 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 50 | 49 | return; |
| 51 | 50 | } |
| 52 | 51 | |
| 53 | - // 4. 发送消息到队列 | |
| 52 | + // 3. 发送消息到队列 | |
| 54 | 53 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); |
| 55 | 54 | } catch (Exception e) { |
| 56 | 55 | log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); |
| 57 | 56 | } |
| 58 | 57 | } |
| 59 | 58 | |
| 60 | -} | |
| 61 | 59 | \ No newline at end of file |
| 60 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java
0 → 100644
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.http; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | -import jakarta.annotation.PostConstruct; | |
| 8 | -import lombok.RequiredArgsConstructor; | |
| 9 | -import lombok.extern.slf4j.Slf4j; | |
| 10 | - | |
| 11 | -/** | |
| 12 | - * IoT 网关 HTTP 订阅者:接收下行给设备的消息 | |
| 13 | - * | |
| 14 | - * @author 芋道源码 | |
| 15 | - */ | |
| 16 | -@RequiredArgsConstructor | |
| 17 | -@Slf4j | |
| 18 | -public class IotHttpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 19 | - | |
| 20 | - private final IotHttpUpstreamProtocol protocol; | |
| 21 | - | |
| 22 | - private final IotMessageBus messageBus; | |
| 23 | - | |
| 24 | - @PostConstruct | |
| 25 | - public void init() { | |
| 26 | - messageBus.register(this); | |
| 27 | - } | |
| 28 | - | |
| 29 | - @Override | |
| 30 | - public String getTopic() { | |
| 31 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | |
| 32 | - } | |
| 33 | - | |
| 34 | - @Override | |
| 35 | - public String getGroup() { | |
| 36 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 37 | - return getTopic(); | |
| 38 | - } | |
| 39 | - | |
| 40 | - @Override | |
| 41 | - public void onMessage(IotDeviceMessage message) { | |
| 42 | - log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); | |
| 43 | - } | |
| 44 | - | |
| 45 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.http; | |
| 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; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler; | |
| 15 | +import io.vertx.core.Vertx; | |
| 16 | +import io.vertx.core.http.HttpServer; | |
| 17 | +import io.vertx.core.http.HttpServerOptions; | |
| 18 | +import io.vertx.core.net.PemKeyCertOptions; | |
| 19 | +import io.vertx.ext.web.Router; | |
| 20 | +import io.vertx.ext.web.handler.BodyHandler; | |
| 21 | +import lombok.Getter; | |
| 22 | +import lombok.extern.slf4j.Slf4j; | |
| 23 | + | |
| 24 | +/** | |
| 25 | + * IoT HTTP 协议实现 | |
| 26 | + * <p> | |
| 27 | + * 基于 Vert.x 实现 HTTP 服务器,接收设备上行消息 | |
| 28 | + * | |
| 29 | + * @author 芋道源码 | |
| 30 | + */ | |
| 31 | +@Slf4j | |
| 32 | +public class IotHttpProtocol implements IotProtocol { | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 协议配置 | |
| 36 | + */ | |
| 37 | + private final ProtocolProperties properties; | |
| 38 | + /** | |
| 39 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 40 | + */ | |
| 41 | + @Getter | |
| 42 | + private final String serverId; | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 运行状态 | |
| 46 | + */ | |
| 47 | + @Getter | |
| 48 | + private volatile boolean running = false; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * Vert.x 实例 | |
| 52 | + */ | |
| 53 | + private Vertx vertx; | |
| 54 | + /** | |
| 55 | + * HTTP 服务器 | |
| 56 | + */ | |
| 57 | + private HttpServer httpServer; | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * 下行消息订阅者 | |
| 61 | + */ | |
| 62 | + private IotHttpDownstreamSubscriber downstreamSubscriber; | |
| 63 | + | |
| 64 | + public IotHttpProtocol(ProtocolProperties properties) { | |
| 65 | + this.properties = properties; | |
| 66 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 67 | + | |
| 68 | + // 初始化下行消息订阅者 | |
| 69 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 70 | + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); | |
| 71 | + } | |
| 72 | + | |
| 73 | + @Override | |
| 74 | + public String getId() { | |
| 75 | + return properties.getId(); | |
| 76 | + } | |
| 77 | + | |
| 78 | + @Override | |
| 79 | + public IotProtocolTypeEnum getType() { | |
| 80 | + return IotProtocolTypeEnum.HTTP; | |
| 81 | + } | |
| 82 | + | |
| 83 | + @Override | |
| 84 | + public void start() { | |
| 85 | + if (running) { | |
| 86 | + log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId()); | |
| 87 | + return; | |
| 88 | + } | |
| 89 | + | |
| 90 | + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) | |
| 91 | + this.vertx = Vertx.vertx(); | |
| 92 | + | |
| 93 | + // 1.2 创建路由 | |
| 94 | + Router router = Router.router(vertx); | |
| 95 | + router.route().handler(BodyHandler.create()); | |
| 96 | + | |
| 97 | + // 1.3 创建处理器,添加路由处理器 | |
| 98 | + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); | |
| 99 | + router.post(IotHttpAuthHandler.PATH).handler(authHandler); | |
| 100 | + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); | |
| 101 | + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); | |
| 102 | + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); | |
| 103 | + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); | |
| 104 | + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); | |
| 105 | + router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); | |
| 106 | + | |
| 107 | + // 1.4 启动 HTTP 服务器 | |
| 108 | + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); | |
| 109 | + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); | |
| 110 | + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { | |
| 111 | + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | |
| 112 | + .setKeyPath(sslConfig.getSslKeyPath()) | |
| 113 | + .setCertPath(sslConfig.getSslCertPath()); | |
| 114 | + options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 115 | + } | |
| 116 | + try { | |
| 117 | + httpServer = vertx.createHttpServer(options) | |
| 118 | + .requestHandler(router) | |
| 119 | + .listen() | |
| 120 | + .result(); | |
| 121 | + running = true; | |
| 122 | + log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{},serverId:{}]", | |
| 123 | + getId(), properties.getPort(), serverId); | |
| 124 | + | |
| 125 | + // 2. 启动下行消息订阅者 | |
| 126 | + this.downstreamSubscriber.start(); | |
| 127 | + } catch (Exception e) { | |
| 128 | + log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); | |
| 129 | + // 启动失败时关闭资源 | |
| 130 | + if (httpServer != null) { | |
| 131 | + httpServer.close(); | |
| 132 | + httpServer = null; | |
| 133 | + } | |
| 134 | + if (vertx != null) { | |
| 135 | + vertx.close(); | |
| 136 | + vertx = null; | |
| 137 | + } | |
| 138 | + throw e; | |
| 139 | + } | |
| 140 | + } | |
| 141 | + | |
| 142 | + @Override | |
| 143 | + public void stop() { | |
| 144 | + if (!running) { | |
| 145 | + return; | |
| 146 | + } | |
| 147 | + // 1. 停止下行消息订阅者 | |
| 148 | + if (downstreamSubscriber != null) { | |
| 149 | + try { | |
| 150 | + downstreamSubscriber.stop(); | |
| 151 | + log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 152 | + } catch (Exception e) { | |
| 153 | + log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 154 | + } | |
| 155 | + downstreamSubscriber = null; | |
| 156 | + } | |
| 157 | + | |
| 158 | + // 2.1 关闭 HTTP 服务器 | |
| 159 | + if (httpServer != null) { | |
| 160 | + try { | |
| 161 | + httpServer.close().result(); | |
| 162 | + log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId()); | |
| 163 | + } catch (Exception e) { | |
| 164 | + log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e); | |
| 165 | + } | |
| 166 | + httpServer = null; | |
| 167 | + } | |
| 168 | + // 2.2 关闭 Vertx 实例 | |
| 169 | + if (vertx != null) { | |
| 170 | + try { | |
| 171 | + vertx.close().result(); | |
| 172 | + log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId()); | |
| 173 | + } catch (Exception e) { | |
| 174 | + log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e); | |
| 175 | + } | |
| 176 | + vertx = null; | |
| 177 | + } | |
| 178 | + running = false; | |
| 179 | + log.info("[stop][IoT HTTP 协议 {} 已停止]", getId()); | |
| 180 | + } | |
| 181 | + | |
| 182 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.http; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; | |
| 9 | -import io.vertx.core.Vertx; | |
| 10 | -import io.vertx.core.http.HttpServer; | |
| 11 | -import io.vertx.core.http.HttpServerOptions; | |
| 12 | -import io.vertx.core.net.PemKeyCertOptions; | |
| 13 | -import io.vertx.ext.web.Router; | |
| 14 | -import io.vertx.ext.web.handler.BodyHandler; | |
| 15 | -import jakarta.annotation.PostConstruct; | |
| 16 | -import jakarta.annotation.PreDestroy; | |
| 17 | -import lombok.Getter; | |
| 18 | -import lombok.extern.slf4j.Slf4j; | |
| 19 | - | |
| 20 | -/** | |
| 21 | - * IoT 网关 HTTP 协议:接收设备上行消息 | |
| 22 | - * | |
| 23 | - * @author 芋道源码 | |
| 24 | - */ | |
| 25 | -@Slf4j | |
| 26 | -public class IotHttpUpstreamProtocol { | |
| 27 | - | |
| 28 | - private final IotGatewayProperties.HttpProperties httpProperties; | |
| 29 | - | |
| 30 | - private final Vertx vertx; | |
| 31 | - | |
| 32 | - private HttpServer httpServer; | |
| 33 | - | |
| 34 | - @Getter | |
| 35 | - private final String serverId; | |
| 36 | - | |
| 37 | - public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) { | |
| 38 | - this.httpProperties = httpProperties; | |
| 39 | - this.vertx = vertx; | |
| 40 | - this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort()); | |
| 41 | - } | |
| 42 | - | |
| 43 | - @PostConstruct | |
| 44 | - public void start() { | |
| 45 | - // 创建路由 | |
| 46 | - Router router = Router.router(vertx); | |
| 47 | - router.route().handler(BodyHandler.create()); | |
| 48 | - | |
| 49 | - // 创建处理器,添加路由处理器 | |
| 50 | - IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); | |
| 51 | - router.post(IotHttpAuthHandler.PATH).handler(authHandler); | |
| 52 | - IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); | |
| 53 | - router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); | |
| 54 | - IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); | |
| 55 | - router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); | |
| 56 | - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); | |
| 57 | - router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); | |
| 58 | - | |
| 59 | - // 启动 HTTP 服务器 | |
| 60 | - HttpServerOptions options = new HttpServerOptions() | |
| 61 | - .setPort(httpProperties.getServerPort()); | |
| 62 | - if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) { | |
| 63 | - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath()) | |
| 64 | - .setCertPath(httpProperties.getSslCertPath()); | |
| 65 | - options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 66 | - } | |
| 67 | - try { | |
| 68 | - httpServer = vertx.createHttpServer(options) | |
| 69 | - .requestHandler(router) | |
| 70 | - .listen() | |
| 71 | - .result(); | |
| 72 | - log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort()); | |
| 73 | - } catch (Exception e) { | |
| 74 | - log.error("[start][IoT 网关 HTTP 协议启动失败]", e); | |
| 75 | - throw e; | |
| 76 | - } | |
| 77 | - } | |
| 78 | - | |
| 79 | - @PreDestroy | |
| 80 | - public void stop() { | |
| 81 | - if (httpServer != null) { | |
| 82 | - try { | |
| 83 | - httpServer.close().result(); | |
| 84 | - log.info("[stop][IoT 网关 HTTP 协议已停止]"); | |
| 85 | - } catch (Exception e) { | |
| 86 | - log.error("[stop][IoT 网关 HTTP 协议停止失败]", e); | |
| 87 | - } | |
| 88 | - } | |
| 89 | - } | |
| 90 | - | |
| 91 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT 网关 HTTP 订阅者:接收下行给设备的消息 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | + | |
| 15 | +@Slf4j | |
| 16 | +public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 17 | + | |
| 18 | + public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { | |
| 19 | + super(protocol, messageBus); | |
| 20 | + } | |
| 21 | + | |
| 22 | + @Override | |
| 23 | + protected void handleMessage(IotDeviceMessage message) { | |
| 24 | + log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); | |
| 25 | + } | |
| 26 | + | |
| 27 | +} | ... | ... |
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 | +import cn.hutool.core.util.ArrayUtil; | |
| 4 | 5 | import cn.hutool.core.util.ObjUtil; |
| 5 | 6 | import cn.hutool.core.util.StrUtil; |
| 6 | 7 | import cn.hutool.extra.spring.SpringUtil; |
| ... | ... | @@ -13,12 +14,10 @@ import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; |
| 13 | 14 | import io.vertx.core.Handler; |
| 14 | 15 | import io.vertx.core.http.HttpHeaders; |
| 15 | 16 | import io.vertx.ext.web.RoutingContext; |
| 16 | -import lombok.RequiredArgsConstructor; | |
| 17 | 17 | import lombok.extern.slf4j.Slf4j; |
| 18 | 18 | import org.springframework.http.MediaType; |
| 19 | 19 | |
| 20 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; | |
| 21 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 20 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | |
| 22 | 21 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 23 | 22 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| 24 | 23 | |
| ... | ... | @@ -27,7 +26,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU |
| 27 | 26 | * |
| 28 | 27 | * @author 芋道源码 |
| 29 | 28 | */ |
| 30 | -@RequiredArgsConstructor | |
| 31 | 29 | @Slf4j |
| 32 | 30 | public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> { |
| 33 | 31 | |
| ... | ... | @@ -43,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> |
| 43 | 41 | CommonResult<Object> result = handle0(context); |
| 44 | 42 | writeResponse(context, result); |
| 45 | 43 | } catch (ServiceException e) { |
| 44 | + // 已知异常,返回对应的错误码和错误信息 | |
| 46 | 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())); | |
| 47 | 49 | } catch (Exception e) { |
| 50 | + // 其他未知异常,返回 500 错误 | |
| 48 | 51 | log.error("[handle][path({}) 处理异常]", context.request().path(), e); |
| 49 | 52 | writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); |
| 50 | 53 | } |
| 51 | 54 | } |
| 52 | 55 | |
| 56 | + /** | |
| 57 | + * 处理 HTTP 请求(子类实现) | |
| 58 | + * | |
| 59 | + * @param context RoutingContext 对象 | |
| 60 | + * @return 处理结果 | |
| 61 | + */ | |
| 53 | 62 | protected abstract CommonResult<Object> handle0(RoutingContext context); |
| 54 | 63 | |
| 64 | + /** | |
| 65 | + * 前置处理:认证等 | |
| 66 | + * | |
| 67 | + * @param context RoutingContext 对象 | |
| 68 | + */ | |
| 55 | 69 | private void beforeHandle(RoutingContext context) { |
| 56 | 70 | // 如果不需要认证,则不走前置处理 |
| 57 | 71 | String path = context.request().path(); |
| ... | ... | @@ -83,12 +97,26 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> |
| 83 | 97 | } |
| 84 | 98 | } |
| 85 | 99 | |
| 100 | + // ========== 序列化相关方法 ========== | |
| 101 | + | |
| 102 | + protected static <T> T deserializeRequest(RoutingContext context, Class<T> clazz) { | |
| 103 | + byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null; | |
| 104 | + if (ArrayUtil.isEmpty(body)) { | |
| 105 | + throw invalidParamException("请求体不能为空"); | |
| 106 | + } | |
| 107 | + return JsonUtils.parseObject(body, clazz); | |
| 108 | + } | |
| 109 | + | |
| 110 | + private static String serializeResponse(Object data) { | |
| 111 | + return JsonUtils.toJsonString(data); | |
| 112 | + } | |
| 113 | + | |
| 86 | 114 | @SuppressWarnings("deprecation") |
| 87 | - public static void writeResponse(RoutingContext context, Object data) { | |
| 115 | + public static void writeResponse(RoutingContext context, CommonResult<?> data) { | |
| 88 | 116 | context.response() |
| 89 | 117 | .setStatusCode(200) |
| 90 | 118 | .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) |
| 91 | - .end(JsonUtils.toJsonString(data)); | |
| 119 | + .end(serializeResponse(data)); | |
| 92 | 120 | } |
| 93 | 121 | |
| 94 | 122 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.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.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; |
| 10 | 9 | 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 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; | |
| 14 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; |
| 15 | 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 16 | -import io.vertx.core.json.JsonObject; | |
| 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 | |
| ... | ... | @@ -32,7 +29,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { |
| 32 | 29 | |
| 33 | 30 | public static final String PATH = "/auth"; |
| 34 | 31 | |
| 35 | - private final IotHttpUpstreamProtocol protocol; | |
| 32 | + private final String serverId; | |
| 36 | 33 | |
| 37 | 34 | private final IotDeviceTokenService deviceTokenService; |
| 38 | 35 | |
| ... | ... | @@ -40,42 +37,31 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { |
| 40 | 37 | |
| 41 | 38 | private final IotDeviceMessageService deviceMessageService; |
| 42 | 39 | |
| 43 | - public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { | |
| 44 | - this.protocol = protocol; | |
| 40 | + public IotHttpAuthHandler(IotHttpProtocol protocol) { | |
| 41 | + this.serverId = protocol.getServerId(); | |
| 45 | 42 | this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); |
| 46 | 43 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 47 | 44 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 48 | 45 | } |
| 49 | 46 | |
| 50 | 47 | @Override |
| 48 | + @SuppressWarnings("DuplicatedCode") | |
| 51 | 49 | public CommonResult<Object> handle0(RoutingContext context) { |
| 52 | 50 | // 1. 解析参数 |
| 53 | - JsonObject body = context.body().asJsonObject(); | |
| 54 | - if (body == null) { | |
| 55 | - throw invalidParamException("请求体不能为空"); | |
| 56 | - } | |
| 57 | - String clientId = body.getString("clientId"); | |
| 58 | - if (StrUtil.isEmpty(clientId)) { | |
| 59 | - throw invalidParamException("clientId 不能为空"); | |
| 60 | - } | |
| 61 | - String username = body.getString("username"); | |
| 62 | - if (StrUtil.isEmpty(username)) { | |
| 63 | - throw invalidParamException("username 不能为空"); | |
| 64 | - } | |
| 65 | - String password = body.getString("password"); | |
| 66 | - if (StrUtil.isEmpty(password)) { | |
| 67 | - throw invalidParamException("password 不能为空"); | |
| 68 | - } | |
| 51 | + IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class); | |
| 52 | + Assert.notNull(request, "请求参数不能为空"); | |
| 53 | + Assert.notBlank(request.getClientId(), "clientId 不能为空"); | |
| 54 | + Assert.notBlank(request.getUsername(), "username 不能为空"); | |
| 55 | + Assert.notBlank(request.getPassword(), "password 不能为空"); | |
| 69 | 56 | |
| 70 | 57 | // 2.1 执行认证 |
| 71 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 72 | - .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 58 | + CommonResult<Boolean> result = deviceApi.authDevice(request); | |
| 73 | 59 | result.checkError(); |
| 74 | - if (!BooleanUtil.isTrue(result.getData())) { | |
| 60 | + if (BooleanUtil.isFalse(result.getData())) { | |
| 75 | 61 | throw exception(DEVICE_AUTH_FAIL); |
| 76 | 62 | } |
| 77 | 63 | // 2.2 生成 Token |
| 78 | - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); | |
| 64 | + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); | |
| 79 | 65 | Assert.notNull(deviceInfo, "设备信息不能为空"); |
| 80 | 66 | String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 81 | 67 | Assert.notBlank(token, "生成 token 不能为空位"); |
| ... | ... | @@ -83,7 +69,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { |
| 83 | 69 | // 3. 执行上线 |
| 84 | 70 | IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); |
| 85 | 71 | deviceMessageService.sendDeviceMessage(message, |
| 86 | - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); | |
| 72 | + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); | |
| 87 | 73 | |
| 88 | 74 | // 构建响应数据 |
| 89 | 75 | return success(MapUtil.of("token", token)); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.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 | -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 io.vertx.core.json.JsonObject; | |
| 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 | /** |
| ... | ... | @@ -33,27 +31,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { |
| 33 | 31 | @Override |
| 34 | 32 | public CommonResult<Object> handle0(RoutingContext context) { |
| 35 | 33 | // 1. 解析参数 |
| 36 | - JsonObject body = context.body().asJsonObject(); | |
| 37 | - if (body == null) { | |
| 38 | - throw invalidParamException("请求体不能为空"); | |
| 39 | - } | |
| 40 | - String productKey = body.getString("productKey"); | |
| 41 | - if (StrUtil.isEmpty(productKey)) { | |
| 42 | - throw invalidParamException("productKey 不能为空"); | |
| 43 | - } | |
| 44 | - String deviceName = body.getString("deviceName"); | |
| 45 | - if (StrUtil.isEmpty(deviceName)) { | |
| 46 | - throw invalidParamException("deviceName 不能为空"); | |
| 47 | - } | |
| 48 | - String productSecret = body.getString("productSecret"); | |
| 49 | - if (StrUtil.isEmpty(productSecret)) { | |
| 50 | - throw invalidParamException("productSecret 不能为空"); | |
| 51 | - } | |
| 34 | + IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class); | |
| 35 | + Assert.notNull(request, "请求参数不能为空"); | |
| 36 | + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); | |
| 37 | + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); | |
| 38 | + Assert.notBlank(request.getSign(), "sign 不能为空"); | |
| 52 | 39 | |
| 53 | 40 | // 2. 调用动态注册 |
| 54 | - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() | |
| 55 | - .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret); | |
| 56 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO); | |
| 41 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); | |
| 57 | 42 | result.checkError(); |
| 58 | 43 | |
| 59 | 44 | // 3. 返回结果 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.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 | +import cn.hutool.core.lang.Assert; | |
| 3 | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 4 | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; |
| 8 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; | |
| 8 | 9 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 9 | -import io.vertx.core.json.JsonObject; | |
| 10 | 10 | import io.vertx.ext.web.RoutingContext; |
| 11 | +import lombok.Data; | |
| 11 | 12 | |
| 12 | 13 | import java.util.List; |
| 13 | 14 | |
| 14 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 15 | 15 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 16 | 16 | |
| 17 | 17 | /** |
| ... | ... | @@ -39,29 +39,31 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { |
| 39 | 39 | |
| 40 | 40 | @Override |
| 41 | 41 | public CommonResult<Object> handle0(RoutingContext context) { |
| 42 | - // 1. 解析通用参数 | |
| 42 | + // 1.1 解析通用参数 | |
| 43 | 43 | String productKey = context.pathParam("productKey"); |
| 44 | 44 | String deviceName = context.pathParam("deviceName"); |
| 45 | + // 1.2 解析子设备列表 | |
| 46 | + SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class); | |
| 47 | + Assert.notNull(request, "请求参数不能为空"); | |
| 48 | + Assert.notEmpty(request.getParams(), "params 不能为空"); | |
| 45 | 49 | |
| 46 | - // 2. 解析子设备列表 | |
| 47 | - JsonObject body = context.body().asJsonObject(); | |
| 48 | - if (body == null) { | |
| 49 | - throw invalidParamException("请求体不能为空"); | |
| 50 | - } | |
| 51 | - if (body.getJsonArray("params") == null) { | |
| 52 | - throw invalidParamException("params 不能为空"); | |
| 53 | - } | |
| 54 | - List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray( | |
| 55 | - body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class); | |
| 56 | - | |
| 57 | - // 3. 调用子设备动态注册 | |
| 50 | + // 2. 调用子设备动态注册 | |
| 58 | 51 | IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() |
| 59 | - .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices); | |
| 52 | + .setGatewayProductKey(productKey) | |
| 53 | + .setGatewayDeviceName(deviceName) | |
| 54 | + .setSubDevices(request.getParams()); | |
| 60 | 55 | CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO); |
| 61 | 56 | result.checkError(); |
| 62 | 57 | |
| 63 | - // 4. 返回结果 | |
| 58 | + // 3. 返回结果 | |
| 64 | 59 | return success(result.getData()); |
| 65 | 60 | } |
| 66 | 61 | |
| 62 | + @Data | |
| 63 | + public static class SubDeviceRegisterRequest { | |
| 64 | + | |
| 65 | + private List<IotSubDeviceRegisterReqDTO> params; | |
| 66 | + | |
| 67 | + } | |
| 68 | + | |
| 67 | 69 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.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.map.MapUtil; |
| ... | ... | @@ -6,55 +6,47 @@ import cn.hutool.core.text.StrPool; |
| 6 | 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 | -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; | |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | 11 | import io.vertx.ext.web.RoutingContext; |
| 12 | -import lombok.RequiredArgsConstructor; | |
| 13 | 12 | import lombok.extern.slf4j.Slf4j; |
| 14 | 13 | |
| 15 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 16 | - | |
| 17 | 14 | /** |
| 18 | 15 | * IoT 网关 HTTP 协议的【上行】处理器 |
| 19 | 16 | * |
| 20 | 17 | * @author 芋道源码 |
| 21 | 18 | */ |
| 22 | -@RequiredArgsConstructor | |
| 23 | 19 | @Slf4j |
| 24 | 20 | public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { |
| 25 | 21 | |
| 26 | 22 | public static final String PATH = "/topic/sys/:productKey/:deviceName/*"; |
| 27 | 23 | |
| 28 | - private final IotHttpUpstreamProtocol protocol; | |
| 24 | + private final String serverId; | |
| 29 | 25 | |
| 30 | 26 | private final IotDeviceMessageService deviceMessageService; |
| 31 | 27 | |
| 32 | - public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { | |
| 33 | - this.protocol = protocol; | |
| 28 | + public IotHttpUpstreamHandler(IotHttpProtocol protocol) { | |
| 29 | + this.serverId = protocol.getServerId(); | |
| 34 | 30 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 35 | 31 | } |
| 36 | 32 | |
| 37 | 33 | @Override |
| 38 | 34 | protected CommonResult<Object> handle0(RoutingContext context) { |
| 39 | - // 1. 解析通用参数 | |
| 35 | + // 1.1 解析通用参数 | |
| 40 | 36 | String productKey = context.pathParam("productKey"); |
| 41 | 37 | String deviceName = context.pathParam("deviceName"); |
| 42 | 38 | String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); |
| 43 | - | |
| 44 | - // 2.1 解析消息 | |
| 45 | - if (context.body().buffer() == null) { | |
| 46 | - throw invalidParamException("请求体不能为空"); | |
| 47 | - } | |
| 48 | - byte[] bytes = context.body().buffer().getBytes(); | |
| 49 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, | |
| 50 | - productKey, deviceName); | |
| 39 | + // 1.2 根据 Content-Type 反序列化消息 | |
| 40 | + IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class); | |
| 41 | + Assert.notNull(message, "请求参数不能为空"); | |
| 51 | 42 | Assert.equals(method, message.getMethod(), "method 不匹配"); |
| 52 | - // 2.2 发送消息 | |
| 43 | + | |
| 44 | + // 2. 发送消息 | |
| 53 | 45 | deviceMessageService.sendDeviceMessage(message, |
| 54 | - productKey, deviceName, protocol.getServerId()); | |
| 46 | + productKey, deviceName, serverId); | |
| 55 | 47 | |
| 56 | 48 | // 3. 返回结果 |
| 57 | 49 | return CommonResult.success(MapUtil.of("messageId", message.getId())); |
| 58 | 50 | } |
| 59 | 51 | |
| 60 | -} | |
| 61 | 52 | \ No newline at end of file |
| 53 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.Min; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT 网关 MQTT 协议配置 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +public class IotMqttConfig { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 最大消息大小(字节) | |
| 17 | + */ | |
| 18 | + @NotNull(message = "最大消息大小不能为空") | |
| 19 | + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节") | |
| 20 | + private Integer maxMessageSize = 8192; | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * 连接超时时间(秒) | |
| 24 | + */ | |
| 25 | + @NotNull(message = "连接超时时间不能为空") | |
| 26 | + @Min(value = 1, message = "连接超时时间不能小于 1 秒") | |
| 27 | + private Integer connectTimeoutSeconds = 60; | |
| 28 | + | |
| 29 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | |
| 8 | -import jakarta.annotation.PostConstruct; | |
| 9 | -import lombok.extern.slf4j.Slf4j; | |
| 10 | - | |
| 11 | -/** | |
| 12 | - * IoT 网关 MQTT 协议:下行消息订阅器 | |
| 13 | - * <p> | |
| 14 | - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 | |
| 15 | - * | |
| 16 | - * @author 芋道源码 | |
| 17 | - */ | |
| 18 | -@Slf4j | |
| 19 | -public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 20 | - | |
| 21 | - private final IotMqttUpstreamProtocol upstreamProtocol; | |
| 22 | - | |
| 23 | - private final IotMqttDownstreamHandler downstreamHandler; | |
| 24 | - | |
| 25 | - private final IotMessageBus messageBus; | |
| 26 | - | |
| 27 | - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol, | |
| 28 | - IotMqttDownstreamHandler downstreamHandler, | |
| 29 | - IotMessageBus messageBus) { | |
| 30 | - this.upstreamProtocol = upstreamProtocol; | |
| 31 | - this.downstreamHandler = downstreamHandler; | |
| 32 | - this.messageBus = messageBus; | |
| 33 | - } | |
| 34 | - | |
| 35 | - @PostConstruct | |
| 36 | - public void subscribe() { | |
| 37 | - messageBus.register(this); | |
| 38 | - log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic()); | |
| 39 | - } | |
| 40 | - | |
| 41 | - @Override | |
| 42 | - public String getTopic() { | |
| 43 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); | |
| 44 | - } | |
| 45 | - | |
| 46 | - @Override | |
| 47 | - public String getGroup() { | |
| 48 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 49 | - return getTopic(); | |
| 50 | - } | |
| 51 | - | |
| 52 | - @Override | |
| 53 | - public void onMessage(IotDeviceMessage message) { | |
| 54 | - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", | |
| 55 | - message.getId(), message.getMethod(), message.getDeviceId()); | |
| 56 | - try { | |
| 57 | - // 1. 校验 | |
| 58 | - String method = message.getMethod(); | |
| 59 | - if (method == null) { | |
| 60 | - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", | |
| 61 | - message.getId(), message.getDeviceId()); | |
| 62 | - return; | |
| 63 | - } | |
| 64 | - | |
| 65 | - // 2. 委托给下行处理器处理业务逻辑 | |
| 66 | - boolean success = downstreamHandler.handleDownstreamMessage(message); | |
| 67 | - if (success) { | |
| 68 | - log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", | |
| 69 | - message.getId(), message.getMethod(), message.getDeviceId()); | |
| 70 | - } else { | |
| 71 | - log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", | |
| 72 | - message.getId(), message.getMethod(), message.getDeviceId()); | |
| 73 | - } | |
| 74 | - } catch (Exception e) { | |
| 75 | - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", | |
| 76 | - message.getId(), message.getMethod(), message.getDeviceId(), e); | |
| 77 | - } | |
| 78 | - } | |
| 79 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.hutool.extra.spring.SpringUtil; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; | |
| 19 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 20 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 21 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 22 | +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | |
| 23 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
| 24 | +import io.vertx.core.Vertx; | |
| 25 | +import io.vertx.core.net.PemKeyCertOptions; | |
| 26 | +import io.vertx.mqtt.MqttEndpoint; | |
| 27 | +import io.vertx.mqtt.MqttServer; | |
| 28 | +import io.vertx.mqtt.MqttServerOptions; | |
| 29 | +import io.vertx.mqtt.MqttTopicSubscription; | |
| 30 | +import io.vertx.mqtt.messages.MqttPublishMessage; | |
| 31 | +import lombok.Getter; | |
| 32 | +import lombok.extern.slf4j.Slf4j; | |
| 33 | + | |
| 34 | +import java.util.ArrayList; | |
| 35 | +import java.util.List; | |
| 36 | + | |
| 37 | +/** | |
| 38 | + * IoT 网关 MQTT 协议:接收设备上行消息 | |
| 39 | + * | |
| 40 | + * @author 芋道源码 | |
| 41 | + */ | |
| 42 | +@Slf4j | |
| 43 | +public class IotMqttProtocol implements IotProtocol { | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * 注册连接的 clientId 标识 | |
| 47 | + * | |
| 48 | + * @see #handleEndpoint(MqttEndpoint) | |
| 49 | + */ | |
| 50 | + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * 协议配置 | |
| 54 | + */ | |
| 55 | + private final ProtocolProperties properties; | |
| 56 | + /** | |
| 57 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 58 | + */ | |
| 59 | + @Getter | |
| 60 | + private final String serverId; | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * 运行状态 | |
| 64 | + */ | |
| 65 | + @Getter | |
| 66 | + private volatile boolean running = false; | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * Vert.x 实例 | |
| 70 | + */ | |
| 71 | + private Vertx vertx; | |
| 72 | + /** | |
| 73 | + * MQTT 服务器 | |
| 74 | + */ | |
| 75 | + private MqttServer mqttServer; | |
| 76 | + | |
| 77 | + /** | |
| 78 | + * 连接管理器 | |
| 79 | + */ | |
| 80 | + private final IotMqttConnectionManager connectionManager; | |
| 81 | + /** | |
| 82 | + * 下行消息订阅者 | |
| 83 | + */ | |
| 84 | + private final IotMqttDownstreamSubscriber downstreamSubscriber; | |
| 85 | + | |
| 86 | + private final IotDeviceMessageService deviceMessageService; | |
| 87 | + | |
| 88 | + private final IotMqttAuthHandler authHandler; | |
| 89 | + private final IotMqttRegisterHandler registerHandler; | |
| 90 | + private final IotMqttUpstreamHandler upstreamHandler; | |
| 91 | + | |
| 92 | + public IotMqttProtocol(ProtocolProperties properties) { | |
| 93 | + IotMqttConfig mqttConfig = properties.getMqtt(); | |
| 94 | + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空"); | |
| 95 | + this.properties = properties; | |
| 96 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 97 | + | |
| 98 | + // 初始化连接管理器 | |
| 99 | + this.connectionManager = new IotMqttConnectionManager(); | |
| 100 | + | |
| 101 | + // 初始化 Handler | |
| 102 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 103 | + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 104 | + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); | |
| 105 | + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); | |
| 106 | + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); | |
| 107 | + | |
| 108 | + // 初始化下行消息订阅者 | |
| 109 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 110 | + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); | |
| 111 | + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 112 | + } | |
| 113 | + | |
| 114 | + @Override | |
| 115 | + public String getId() { | |
| 116 | + return properties.getId(); | |
| 117 | + } | |
| 118 | + | |
| 119 | + @Override | |
| 120 | + public IotProtocolTypeEnum getType() { | |
| 121 | + return IotProtocolTypeEnum.MQTT; | |
| 122 | + } | |
| 123 | + | |
| 124 | + @Override | |
| 125 | + public void start() { | |
| 126 | + if (running) { | |
| 127 | + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId()); | |
| 128 | + return; | |
| 129 | + } | |
| 130 | + | |
| 131 | + // 1.1 创建 Vertx 实例 | |
| 132 | + this.vertx = Vertx.vertx(); | |
| 133 | + | |
| 134 | + // 1.2 创建服务器选项 | |
| 135 | + IotMqttConfig mqttConfig = properties.getMqtt(); | |
| 136 | + MqttServerOptions options = new MqttServerOptions() | |
| 137 | + .setPort(properties.getPort()) | |
| 138 | + .setMaxMessageSize(mqttConfig.getMaxMessageSize()) | |
| 139 | + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds()); | |
| 140 | + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); | |
| 141 | + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { | |
| 142 | + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | |
| 143 | + .setKeyPath(sslConfig.getSslKeyPath()) | |
| 144 | + .setCertPath(sslConfig.getSslCertPath()); | |
| 145 | + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 146 | + } | |
| 147 | + | |
| 148 | + // 1.3 创建服务器并设置连接处理器 | |
| 149 | + mqttServer = MqttServer.create(vertx, options); | |
| 150 | + mqttServer.endpointHandler(this::handleEndpoint); | |
| 151 | + | |
| 152 | + // 1.4 启动 MQTT 服务器 | |
| 153 | + try { | |
| 154 | + mqttServer.listen().result(); | |
| 155 | + running = true; | |
| 156 | + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", | |
| 157 | + getId(), properties.getPort(), serverId); | |
| 158 | + | |
| 159 | + // 2. 启动下行消息订阅者 | |
| 160 | + this.downstreamSubscriber.start(); | |
| 161 | + } catch (Exception e) { | |
| 162 | + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); | |
| 163 | + // 启动失败时关闭资源 | |
| 164 | + if (mqttServer != null) { | |
| 165 | + mqttServer.close(); | |
| 166 | + mqttServer = null; | |
| 167 | + } | |
| 168 | + if (vertx != null) { | |
| 169 | + vertx.close(); | |
| 170 | + vertx = null; | |
| 171 | + } | |
| 172 | + throw e; | |
| 173 | + } | |
| 174 | + } | |
| 175 | + | |
| 176 | + @Override | |
| 177 | + public void stop() { | |
| 178 | + if (!running) { | |
| 179 | + return; | |
| 180 | + } | |
| 181 | + // 1. 停止下行消息订阅者 | |
| 182 | + try { | |
| 183 | + downstreamSubscriber.stop(); | |
| 184 | + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); | |
| 185 | + } catch (Exception e) { | |
| 186 | + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 187 | + } | |
| 188 | + | |
| 189 | + // 2.1 关闭 MQTT 服务器 | |
| 190 | + if (mqttServer != null) { | |
| 191 | + try { | |
| 192 | + mqttServer.close().result(); | |
| 193 | + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId()); | |
| 194 | + } catch (Exception e) { | |
| 195 | + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e); | |
| 196 | + } | |
| 197 | + mqttServer = null; | |
| 198 | + } | |
| 199 | + // 2.2 关闭 Vertx 实例 | |
| 200 | + if (vertx != null) { | |
| 201 | + try { | |
| 202 | + vertx.close().result(); | |
| 203 | + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId()); | |
| 204 | + } catch (Exception e) { | |
| 205 | + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e); | |
| 206 | + } | |
| 207 | + vertx = null; | |
| 208 | + } | |
| 209 | + running = false; | |
| 210 | + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); | |
| 211 | + } | |
| 212 | + | |
| 213 | + // ======================================= MQTT 连接处理 ====================================== | |
| 214 | + | |
| 215 | + /** | |
| 216 | + * 处理 MQTT 连接端点 | |
| 217 | + * | |
| 218 | + * @param endpoint MQTT 连接端点 | |
| 219 | + */ | |
| 220 | + private void handleEndpoint(MqttEndpoint endpoint) { | |
| 221 | + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程 | |
| 222 | + String clientId = endpoint.clientIdentifier(); | |
| 223 | + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { | |
| 224 | + // 情况一:设备注册请求 | |
| 225 | + registerHandler.handleRegister(endpoint); | |
| 226 | + return; | |
| 227 | + } else { | |
| 228 | + // 情况二:普通认证请求 | |
| 229 | + if (!authHandler.handleAuthenticationRequest(endpoint)) { | |
| 230 | + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | |
| 231 | + return; | |
| 232 | + } | |
| 233 | + } | |
| 234 | + | |
| 235 | + // 2.1 设置异常和关闭处理器 | |
| 236 | + endpoint.exceptionHandler(ex -> { | |
| 237 | + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]", | |
| 238 | + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); | |
| 239 | + endpoint.close(); | |
| 240 | + }); | |
| 241 | + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等) | |
| 242 | + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文 | |
| 243 | + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); | |
| 244 | + cleanupConnection(endpoint); | |
| 245 | + }); | |
| 246 | + // 2.2 设置心跳处理器 | |
| 247 | + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); | |
| 248 | + | |
| 249 | + // 3.1 设置消息处理器 | |
| 250 | + endpoint.publishHandler(message -> processMessage(endpoint, message)); | |
| 251 | + // 3.2 设置 QoS 2 消息的 PUBREL 处理器 | |
| 252 | + endpoint.publishReleaseHandler(endpoint::publishComplete); | |
| 253 | + | |
| 254 | + // 4.1 设置订阅处理器(带 ACL 校验) | |
| 255 | + endpoint.subscribeHandler(subscribe -> { | |
| 256 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 257 | + List<MqttQoS> grantedQoSLevels = new ArrayList<>(); | |
| 258 | + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) { | |
| 259 | + String topicName = sub.topicName(); | |
| 260 | + // 校验主题是否属于当前设备 | |
| 261 | + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed( | |
| 262 | + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) { | |
| 263 | + grantedQoSLevels.add(sub.qualityOfService()); | |
| 264 | + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName); | |
| 265 | + } else { | |
| 266 | + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName); | |
| 267 | + grantedQoSLevels.add(MqttQoS.FAILURE); | |
| 268 | + } | |
| 269 | + } | |
| 270 | + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | |
| 271 | + }); | |
| 272 | + // 4.2 设置取消订阅处理器 | |
| 273 | + endpoint.unsubscribeHandler(unsubscribe -> { | |
| 274 | + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); | |
| 275 | + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); | |
| 276 | + }); | |
| 277 | + | |
| 278 | + // 5. 接受连接 | |
| 279 | + endpoint.accept(false); | |
| 280 | + } | |
| 281 | + | |
| 282 | + /** | |
| 283 | + * 处理消息(发布) | |
| 284 | + * | |
| 285 | + * @param endpoint MQTT 连接端点 | |
| 286 | + * @param message 发布消息 | |
| 287 | + */ | |
| 288 | + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { | |
| 289 | + String clientId = endpoint.clientIdentifier(); | |
| 290 | + try { | |
| 291 | + // 1. 处理业务消息 | |
| 292 | + String topic = message.topicName(); | |
| 293 | + byte[] payload = message.payload().getBytes(); | |
| 294 | + upstreamHandler.handleBusinessRequest(endpoint, topic, payload); | |
| 295 | + | |
| 296 | + // 2. 根据 QoS 级别发送相应的确认消息 | |
| 297 | + handleQoSAck(endpoint, message); | |
| 298 | + } catch (Exception e) { | |
| 299 | + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | |
| 300 | + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | |
| 301 | + endpoint.close(); | |
| 302 | + } | |
| 303 | + } | |
| 304 | + | |
| 305 | + /** | |
| 306 | + * 处理 QoS 确认 | |
| 307 | + * | |
| 308 | + * @param endpoint MQTT 连接端点 | |
| 309 | + * @param message 发布消息 | |
| 310 | + */ | |
| 311 | + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) { | |
| 312 | + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | |
| 313 | + // QoS 1: 发送 PUBACK 确认 | |
| 314 | + endpoint.publishAcknowledge(message.messageId()); | |
| 315 | + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { | |
| 316 | + // QoS 2: 发送 PUBREC 确认 | |
| 317 | + endpoint.publishReceived(message.messageId()); | |
| 318 | + } | |
| 319 | + // QoS 0 无需确认 | |
| 320 | + } | |
| 321 | + | |
| 322 | + /** | |
| 323 | + * 清理连接 | |
| 324 | + * | |
| 325 | + * @param endpoint MQTT 连接端点 | |
| 326 | + */ | |
| 327 | + private void cleanupConnection(MqttEndpoint endpoint) { | |
| 328 | + try { | |
| 329 | + // 1. 发送设备离线消息 | |
| 330 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 331 | + if (connectionInfo != null) { | |
| 332 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 333 | + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 334 | + connectionInfo.getDeviceName(), serverId); | |
| 335 | + } | |
| 336 | + | |
| 337 | + // 2. 注销连接 | |
| 338 | + connectionManager.unregisterConnection(endpoint); | |
| 339 | + } catch (Exception e) { | |
| 340 | + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", | |
| 341 | + endpoint.clientIdentifier(), e.getMessage()); | |
| 342 | + } | |
| 343 | + } | |
| 344 | + | |
| 345 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 8 | -import io.vertx.core.Vertx; | |
| 9 | -import io.vertx.mqtt.MqttServer; | |
| 10 | -import io.vertx.mqtt.MqttServerOptions; | |
| 11 | -import jakarta.annotation.PostConstruct; | |
| 12 | -import jakarta.annotation.PreDestroy; | |
| 13 | -import lombok.Getter; | |
| 14 | -import lombok.extern.slf4j.Slf4j; | |
| 15 | - | |
| 16 | -/** | |
| 17 | - * IoT 网关 MQTT 协议:接收设备上行消息 | |
| 18 | - * | |
| 19 | - * @author 芋道源码 | |
| 20 | - */ | |
| 21 | -@Slf4j | |
| 22 | -public class IotMqttUpstreamProtocol { | |
| 23 | - | |
| 24 | - private final IotGatewayProperties.MqttProperties mqttProperties; | |
| 25 | - | |
| 26 | - private final IotDeviceMessageService messageService; | |
| 27 | - | |
| 28 | - private final IotMqttConnectionManager connectionManager; | |
| 29 | - | |
| 30 | - private final Vertx vertx; | |
| 31 | - | |
| 32 | - @Getter | |
| 33 | - private final String serverId; | |
| 34 | - | |
| 35 | - private MqttServer mqttServer; | |
| 36 | - | |
| 37 | - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, | |
| 38 | - IotDeviceMessageService messageService, | |
| 39 | - IotMqttConnectionManager connectionManager, | |
| 40 | - Vertx vertx) { | |
| 41 | - this.mqttProperties = mqttProperties; | |
| 42 | - this.messageService = messageService; | |
| 43 | - this.connectionManager = connectionManager; | |
| 44 | - this.vertx = vertx; | |
| 45 | - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); | |
| 46 | - } | |
| 47 | - | |
| 48 | - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; | |
| 49 | - @PostConstruct | |
| 50 | - public void start() { | |
| 51 | - // 创建服务器选项 | |
| 52 | - MqttServerOptions options = new MqttServerOptions() | |
| 53 | - .setPort(mqttProperties.getPort()) | |
| 54 | - .setMaxMessageSize(mqttProperties.getMaxMessageSize()) | |
| 55 | - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); | |
| 56 | - | |
| 57 | - // 配置 SSL(如果启用) | |
| 58 | - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { | |
| 59 | - options.setSsl(true) | |
| 60 | - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) | |
| 61 | - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); | |
| 62 | - } | |
| 63 | - | |
| 64 | - // 创建服务器并设置连接处理器 | |
| 65 | - mqttServer = MqttServer.create(vertx, options); | |
| 66 | - mqttServer.endpointHandler(endpoint -> { | |
| 67 | - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); | |
| 68 | - handler.handle(endpoint); | |
| 69 | - }); | |
| 70 | - | |
| 71 | - // 启动服务器 | |
| 72 | - try { | |
| 73 | - mqttServer.listen().result(); | |
| 74 | - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); | |
| 75 | - } catch (Exception e) { | |
| 76 | - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); | |
| 77 | - throw e; | |
| 78 | - } | |
| 79 | - } | |
| 80 | - | |
| 81 | - @PreDestroy | |
| 82 | - public void stop() { | |
| 83 | - if (mqttServer != null) { | |
| 84 | - try { | |
| 85 | - mqttServer.close().result(); | |
| 86 | - log.info("[stop][IoT 网关 MQTT 协议已停止]"); | |
| 87 | - } catch (Exception e) { | |
| 88 | - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); | |
| 89 | - } | |
| 90 | - } | |
| 91 | - } | |
| 92 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 9 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * IoT 网关 MQTT 协议:下行消息处理器 | |
| 15 | + * | |
| 16 | + * @author 芋道源码 | |
| 17 | + */ | |
| 18 | +@Slf4j | |
| 19 | +@RequiredArgsConstructor | |
| 20 | +public class IotMqttDownstreamHandler { | |
| 21 | + | |
| 22 | + private final IotDeviceMessageService deviceMessageService; | |
| 23 | + | |
| 24 | + private final IotMqttConnectionManager connectionManager; | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 处理下行消息 | |
| 28 | + * | |
| 29 | + * @param message 设备消息 | |
| 30 | + */ | |
| 31 | + public void handle(IotDeviceMessage message) { | |
| 32 | + try { | |
| 33 | + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", | |
| 34 | + message.getDeviceId(), message.getMethod(), message.getId()); | |
| 35 | + | |
| 36 | + // 1. 检查设备连接 | |
| 37 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( | |
| 38 | + message.getDeviceId()); | |
| 39 | + if (connectionInfo == null) { | |
| 40 | + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", | |
| 41 | + message.getDeviceId(), message.getMethod(), message.getId()); | |
| 42 | + return; | |
| 43 | + } | |
| 44 | + | |
| 45 | + // 2.1 序列化消息 | |
| 46 | + byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(), | |
| 47 | + connectionInfo.getDeviceName()); | |
| 48 | + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); | |
| 49 | + // 2.2 构建主题 | |
| 50 | + Assert.notBlank(message.getMethod(), "消息方法不能为空"); | |
| 51 | + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); | |
| 52 | + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(), | |
| 53 | + connectionInfo.getDeviceName(), isReply); | |
| 54 | + Assert.notBlank(topic, "主题不能为空"); | |
| 55 | + | |
| 56 | + // 3. 发送到设备 | |
| 57 | + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, | |
| 58 | + MqttQoS.AT_LEAST_ONCE.value(), false); | |
| 59 | + if (!success) { | |
| 60 | + throw new RuntimeException("下行消息发送失败"); | |
| 61 | + } | |
| 62 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]", | |
| 63 | + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length); | |
| 64 | + } catch (Exception e) { | |
| 65 | + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", | |
| 66 | + message.getDeviceId(), message.getMethod(), message, e); | |
| 67 | + } | |
| 68 | + } | |
| 69 | + | |
| 70 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT 网关 MQTT 协议:接收下行给设备的消息 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + private final IotMqttDownstreamHandler downstreamHandler; | |
| 18 | + | |
| 19 | + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, | |
| 20 | + IotMqttDownstreamHandler downstreamHandler, | |
| 21 | + IotMessageBus messageBus) { | |
| 22 | + super(protocol, messageBus); | |
| 23 | + this.downstreamHandler = downstreamHandler; | |
| 24 | + } | |
| 25 | + | |
| 26 | + @Override | |
| 27 | + protected void handleMessage(IotDeviceMessage message) { | |
| 28 | + downstreamHandler.handle(message); | |
| 29 | + } | |
| 30 | + | |
| 31 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 7 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
| 8 | +import io.vertx.core.buffer.Buffer; | |
| 9 | +import io.vertx.mqtt.MqttEndpoint; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * IoT 网关 MQTT 协议的处理器抽象基类 | |
| 15 | + * <p> | |
| 16 | + * 提供通用的连接校验、响应发送等功能 | |
| 17 | + * | |
| 18 | + * @author 芋道源码 | |
| 19 | + */ | |
| 20 | +@Slf4j | |
| 21 | +@RequiredArgsConstructor | |
| 22 | +public abstract class IotMqttAbstractHandler { | |
| 23 | + | |
| 24 | + protected final IotMqttConnectionManager connectionManager; | |
| 25 | + protected final IotDeviceMessageService deviceMessageService; | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 发送成功响应到设备 | |
| 29 | + * | |
| 30 | + * @param endpoint MQTT 连接端点 | |
| 31 | + * @param productKey 产品 Key | |
| 32 | + * @param deviceName 设备名称 | |
| 33 | + * @param requestId 请求 ID | |
| 34 | + * @param method 方法名 | |
| 35 | + * @param data 响应数据 | |
| 36 | + */ | |
| 37 | + @SuppressWarnings("SameParameterValue") | |
| 38 | + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 39 | + String requestId, String method, Object data) { | |
| 40 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); | |
| 41 | + writeResponse(endpoint, productKey, deviceName, method, responseMessage); | |
| 42 | + } | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 发送错误响应到设备 | |
| 46 | + * | |
| 47 | + * @param endpoint MQTT 连接端点 | |
| 48 | + * @param productKey 产品 Key | |
| 49 | + * @param deviceName 设备名称 | |
| 50 | + * @param requestId 请求 ID | |
| 51 | + * @param method 方法名 | |
| 52 | + * @param errorCode 错误码 | |
| 53 | + * @param errorMessage 错误消息 | |
| 54 | + */ | |
| 55 | + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 56 | + String requestId, String method, Integer errorCode, String errorMessage) { | |
| 57 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); | |
| 58 | + writeResponse(endpoint, productKey, deviceName, method, responseMessage); | |
| 59 | + } | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 写入响应消息到设备 | |
| 63 | + * | |
| 64 | + * @param endpoint MQTT 连接端点 | |
| 65 | + * @param productKey 产品 Key | |
| 66 | + * @param deviceName 设备名称 | |
| 67 | + * @param method 方法名 | |
| 68 | + * @param responseMessage 响应消息 | |
| 69 | + */ | |
| 70 | + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 71 | + String method, IotDeviceMessage responseMessage) { | |
| 72 | + try { | |
| 73 | + // 1.1 序列化消息(根据设备配置的序列化类型) | |
| 74 | + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName); | |
| 75 | + // 1.2 构建响应主题 | |
| 76 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); | |
| 77 | + | |
| 78 | + // 2. 发送响应消息 | |
| 79 | + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); | |
| 80 | + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); | |
| 81 | + } catch (Exception e) { | |
| 82 | + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.BooleanUtil; | |
| 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.biz.dto.IotDeviceAuthReqDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 15 | +import io.vertx.mqtt.MqttEndpoint; | |
| 16 | +import lombok.extern.slf4j.Slf4j; | |
| 17 | +import cn.hutool.core.lang.Assert; | |
| 18 | + | |
| 19 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | |
| 20 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * IoT 网关 MQTT 认证处理器 | |
| 24 | + * <p> | |
| 25 | + * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 | |
| 26 | + * | |
| 27 | + * @author 芋道源码 | |
| 28 | + */ | |
| 29 | +@Slf4j | |
| 30 | +public class IotMqttAuthHandler extends IotMqttAbstractHandler { | |
| 31 | + | |
| 32 | + private final IotDeviceCommonApi deviceApi; | |
| 33 | + private final IotDeviceService deviceService; | |
| 34 | + private final String serverId; | |
| 35 | + | |
| 36 | + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager, | |
| 37 | + IotDeviceMessageService deviceMessageService, | |
| 38 | + IotDeviceCommonApi deviceApi, | |
| 39 | + String serverId) { | |
| 40 | + super(connectionManager, deviceMessageService); | |
| 41 | + this.deviceApi = deviceApi; | |
| 42 | + this.deviceService = SpringUtil.getBean(IotDeviceService.class); | |
| 43 | + this.serverId = serverId; | |
| 44 | + } | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 处理 MQTT 连接(认证)请求 | |
| 48 | + * | |
| 49 | + * @param endpoint MQTT 连接端点 | |
| 50 | + * @return 认证是否成功 | |
| 51 | + */ | |
| 52 | + @SuppressWarnings("DataFlowIssue") | |
| 53 | + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { | |
| 54 | + String clientId = endpoint.clientIdentifier(); | |
| 55 | + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | |
| 56 | + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | |
| 57 | + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", | |
| 58 | + clientId, username, connectionManager.getEndpointAddress(endpoint)); | |
| 59 | + | |
| 60 | + try { | |
| 61 | + // 1.1 解析认证参数 | |
| 62 | + Assert.notBlank(clientId, "clientId 不能为空"); | |
| 63 | + Assert.notBlank(username, "username 不能为空"); | |
| 64 | + Assert.notBlank(password, "password 不能为空"); | |
| 65 | + // 1.2 构建认证参数 | |
| 66 | + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | |
| 67 | + .setClientId(clientId) | |
| 68 | + .setUsername(username) | |
| 69 | + .setPassword(password); | |
| 70 | + | |
| 71 | + // 2.1 执行认证 | |
| 72 | + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 73 | + authResult.checkError(); | |
| 74 | + if (BooleanUtil.isFalse(authResult.getData())) { | |
| 75 | + throw exception(DEVICE_AUTH_FAIL); | |
| 76 | + } | |
| 77 | + // 2.2 解析设备信息 | |
| 78 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 79 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | |
| 80 | + // 2.3 获取设备信息 | |
| 81 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 82 | + Assert.notNull(device, "设备不存在"); | |
| 83 | + | |
| 84 | + // 3.1 注册连接 | |
| 85 | + registerConnection(endpoint, device, clientId); | |
| 86 | + // 3.2 发送设备上线消息 | |
| 87 | + sendOnlineMessage(device); | |
| 88 | + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 89 | + return true; | |
| 90 | + } catch (Exception e) { | |
| 91 | + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]", | |
| 92 | + clientId, username, e.getMessage()); | |
| 93 | + return false; | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * 注册连接 | |
| 99 | + */ | |
| 100 | + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { | |
| 101 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() | |
| 102 | + .setDeviceId(device.getId()) | |
| 103 | + .setProductKey(device.getProductKey()) | |
| 104 | + .setDeviceName(device.getDeviceName()) | |
| 105 | + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); | |
| 106 | + connectionManager.registerConnection(endpoint, connectionInfo); | |
| 107 | + } | |
| 108 | + | |
| 109 | + /** | |
| 110 | + * 发送设备上线消息 | |
| 111 | + */ | |
| 112 | + private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 113 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 114 | + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 115 | + device.getDeviceName(), serverId); | |
| 116 | + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); | |
| 117 | + } | |
| 118 | + | |
| 119 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 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.enums.IotDeviceMessageMethodEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 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.core.util.IotDeviceAuthUtils; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 14 | +import io.vertx.mqtt.MqttEndpoint; | |
| 15 | +import lombok.extern.slf4j.Slf4j; | |
| 16 | + | |
| 17 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) | |
| 21 | + * | |
| 22 | + * @author 芋道源码 | |
| 23 | + */ | |
| 24 | +@Slf4j | |
| 25 | +public class IotMqttRegisterHandler extends IotMqttAbstractHandler { | |
| 26 | + | |
| 27 | + private final IotDeviceCommonApi deviceApi; | |
| 28 | + | |
| 29 | + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, | |
| 30 | + IotDeviceMessageService deviceMessageService) { | |
| 31 | + super(connectionManager, deviceMessageService); | |
| 32 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 处理注册连接 | |
| 37 | + * <p> | |
| 38 | + * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册 | |
| 39 | + * | |
| 40 | + * @param endpoint MQTT 连接端点 | |
| 41 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 42 | + */ | |
| 43 | + @SuppressWarnings("DataFlowIssue") | |
| 44 | + public void handleRegister(MqttEndpoint endpoint) { | |
| 45 | + String clientId = endpoint.clientIdentifier(); | |
| 46 | + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | |
| 47 | + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | |
| 48 | + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); | |
| 49 | + String productKey = null; | |
| 50 | + String deviceName = null; | |
| 51 | + | |
| 52 | + try { | |
| 53 | + // 1.1 校验参数 | |
| 54 | + Assert.notBlank(clientId, "clientId 不能为空"); | |
| 55 | + Assert.notBlank(username, "username 不能为空"); | |
| 56 | + Assert.notBlank(password, "password 不能为空"); | |
| 57 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 58 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | |
| 59 | + productKey = deviceInfo.getProductKey(); | |
| 60 | + deviceName = deviceInfo.getDeviceName(); | |
| 61 | + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]", | |
| 62 | + clientId, productKey, deviceName); | |
| 63 | + // 1.2 构建注册参数 | |
| 64 | + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() | |
| 65 | + .setProductKey(productKey) | |
| 66 | + .setDeviceName(deviceName) | |
| 67 | + .setSign(password); | |
| 68 | + | |
| 69 | + // 2. 调用动态注册 API | |
| 70 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 71 | + result.checkError(); | |
| 72 | + | |
| 73 | + // 3. 接受连接,并发送成功响应 | |
| 74 | + endpoint.accept(false); | |
| 75 | + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData()); | |
| 76 | + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId); | |
| 77 | + } catch (Exception e) { | |
| 78 | + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); | |
| 79 | + // 接受连接,并发送错误响应 | |
| 80 | + endpoint.accept(false); | |
| 81 | + sendErrorResponse(endpoint, productKey, deviceName, null, method, | |
| 82 | + INTERNAL_SERVER_ERROR.getCode(), e.getMessage()); | |
| 83 | + } finally { | |
| 84 | + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) | |
| 85 | + endpoint.close(); | |
| 86 | + } | |
| 87 | + } | |
| 88 | + | |
| 89 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 9 | +import io.vertx.mqtt.MqttEndpoint; | |
| 10 | +import lombok.extern.slf4j.Slf4j; | |
| 11 | + | |
| 12 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | |
| 13 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 14 | + | |
| 15 | +/** | |
| 16 | + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) | |
| 17 | + * | |
| 18 | + * @author 芋道源码 | |
| 19 | + */ | |
| 20 | +@Slf4j | |
| 21 | +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | |
| 22 | + | |
| 23 | + private final String serverId; | |
| 24 | + | |
| 25 | + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, | |
| 26 | + IotDeviceMessageService deviceMessageService, | |
| 27 | + String serverId) { | |
| 28 | + super(connectionManager, deviceMessageService); | |
| 29 | + this.serverId = serverId; | |
| 30 | + } | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 处理业务消息 | |
| 34 | + * | |
| 35 | + * @param endpoint MQTT 连接端点 | |
| 36 | + * @param topic 主题 | |
| 37 | + * @param payload 消息内容 | |
| 38 | + */ | |
| 39 | + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { | |
| 40 | + String clientId = endpoint.clientIdentifier(); | |
| 41 | + IotDeviceMessage message = null; | |
| 42 | + String productKey = null; | |
| 43 | + String deviceName = null; | |
| 44 | + | |
| 45 | + try { | |
| 46 | + // 1.1 基础检查 | |
| 47 | + if (ArrayUtil.isEmpty(payload)) { | |
| 48 | + return; | |
| 49 | + } | |
| 50 | + // 1.2 解析主题,获取 productKey 和 deviceName | |
| 51 | + String[] topicParts = topic.split("/"); | |
| 52 | + productKey = ArrayUtil.get(topicParts, 2); | |
| 53 | + deviceName = ArrayUtil.get(topicParts, 3); | |
| 54 | + Assert.notBlank(productKey, "产品 Key 不能为空"); | |
| 55 | + Assert.notBlank(deviceName, "设备名称不能为空"); | |
| 56 | + // 1.3 校验设备信息,防止伪造设备消息 | |
| 57 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 58 | + Assert.notNull(connectionInfo, "无法获取连接信息"); | |
| 59 | + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); | |
| 60 | + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); | |
| 61 | + | |
| 62 | + // 2. 反序列化消息 | |
| 63 | + message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 64 | + if (message == null) { | |
| 65 | + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 66 | + sendErrorResponse(endpoint, productKey, deviceName, null, null, | |
| 67 | + BAD_REQUEST.getCode(), "消息解码失败"); | |
| 68 | + return; | |
| 69 | + } | |
| 70 | + | |
| 71 | + // 3. 处理业务消息 | |
| 72 | + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 73 | + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); | |
| 74 | + } catch (ServiceException e) { | |
| 75 | + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", | |
| 76 | + clientId, topic, e.getMessage()); | |
| 77 | + String requestId = message != null ? message.getRequestId() : null; | |
| 78 | + String method = message != null ? message.getMethod() : null; | |
| 79 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); | |
| 80 | + } catch (IllegalArgumentException e) { | |
| 81 | + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", | |
| 82 | + clientId, topic, e.getMessage()); | |
| 83 | + String requestId = message != null ? message.getRequestId() : null; | |
| 84 | + String method = message != null ? message.getMethod() : null; | |
| 85 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | |
| 86 | + BAD_REQUEST.getCode(), e.getMessage()); | |
| 87 | + } catch (Exception e) { | |
| 88 | + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 89 | + clientId, topic, e.getMessage(), e); | |
| 90 | + String requestId = message != null ? message.getRequestId() : null; | |
| 91 | + String method = message != null ? message.getMethod() : null; | |
| 92 | + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | |
| 93 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| ... | ... | @@ -66,7 +66,6 @@ public class IotMqttConnectionManager { |
| 66 | 66 | } catch (Exception ignored) { |
| 67 | 67 | // 连接已关闭,忽略异常 |
| 68 | 68 | } |
| 69 | - | |
| 70 | 69 | return realTimeAddress; |
| 71 | 70 | } |
| 72 | 71 | |
| ... | ... | @@ -74,24 +73,24 @@ public class IotMqttConnectionManager { |
| 74 | 73 | * 注册设备连接(包含认证信息) |
| 75 | 74 | * |
| 76 | 75 | * @param endpoint MQTT 连接端点 |
| 77 | - * @param deviceId 设备 ID | |
| 78 | 76 | * @param connectionInfo 连接信息 |
| 79 | 77 | */ |
| 80 | - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { | |
| 78 | + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { | |
| 79 | + Long deviceId = connectionInfo.getDeviceId(); | |
| 81 | 80 | // 如果设备已有其他连接,先清理旧连接 |
| 82 | 81 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); |
| 83 | 82 | if (oldEndpoint != null && oldEndpoint != endpoint) { |
| 84 | 83 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 85 | 84 | deviceId, getEndpointAddress(oldEndpoint)); |
| 86 | - oldEndpoint.close(); | |
| 87 | - // 清理旧连接的映射 | |
| 85 | + // 先清理映射,再关闭连接(避免旧连接处理器干扰) | |
| 88 | 86 | connectionMap.remove(oldEndpoint); |
| 87 | + oldEndpoint.close(); | |
| 89 | 88 | } |
| 90 | 89 | |
| 91 | 90 | // 注册新连接 |
| 92 | 91 | connectionMap.put(endpoint, connectionInfo); |
| 93 | 92 | deviceEndpointMap.put(deviceId, endpoint); |
| 94 | - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", | |
| 93 | + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", | |
| 95 | 94 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); |
| 96 | 95 | } |
| 97 | 96 | |
| ... | ... | @@ -129,26 +128,11 @@ public class IotMqttConnectionManager { |
| 129 | 128 | if (endpoint == null) { |
| 130 | 129 | return null; |
| 131 | 130 | } |
| 132 | - | |
| 133 | 131 | // 获取连接信息 |
| 134 | 132 | return getConnectionInfo(endpoint); |
| 135 | 133 | } |
| 136 | 134 | |
| 137 | 135 | /** |
| 138 | - * 检查设备是否在线 | |
| 139 | - */ | |
| 140 | - public boolean isDeviceOnline(Long deviceId) { | |
| 141 | - return deviceEndpointMap.containsKey(deviceId); | |
| 142 | - } | |
| 143 | - | |
| 144 | - /** | |
| 145 | - * 检查设备是否离线 | |
| 146 | - */ | |
| 147 | - public boolean isDeviceOffline(Long deviceId) { | |
| 148 | - return !isDeviceOnline(deviceId); | |
| 149 | - } | |
| 150 | - | |
| 151 | - /** | |
| 152 | 136 | * 发送消息到设备 |
| 153 | 137 | * |
| 154 | 138 | * @param deviceId 设备 ID |
| ... | ... | @@ -192,28 +176,16 @@ public class IotMqttConnectionManager { |
| 192 | 176 | * 设备 ID |
| 193 | 177 | */ |
| 194 | 178 | private Long deviceId; |
| 195 | - | |
| 196 | 179 | /** |
| 197 | 180 | * 产品 Key |
| 198 | 181 | */ |
| 199 | 182 | private String productKey; |
| 200 | - | |
| 201 | 183 | /** |
| 202 | 184 | * 设备名称 |
| 203 | 185 | */ |
| 204 | 186 | private String deviceName; |
| 205 | 187 | |
| 206 | 188 | /** |
| 207 | - * 客户端 ID | |
| 208 | - */ | |
| 209 | - private String clientId; | |
| 210 | - | |
| 211 | - /** | |
| 212 | - * 是否已认证 | |
| 213 | - */ | |
| 214 | - private boolean authenticated; | |
| 215 | - | |
| 216 | - /** | |
| 217 | 189 | * 连接地址 |
| 218 | 190 | */ |
| 219 | 191 | private String remoteAddress; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 9 | -import io.netty.handler.codec.mqtt.MqttQoS; | |
| 10 | -import lombok.extern.slf4j.Slf4j; | |
| 11 | - | |
| 12 | -/** | |
| 13 | - * IoT 网关 MQTT 协议:下行消息处理器 | |
| 14 | - * <p> | |
| 15 | - * 专门处理下行消息的业务逻辑,包括: | |
| 16 | - * 1. 消息编码 | |
| 17 | - * 2. 主题构建 | |
| 18 | - * 3. 消息发送 | |
| 19 | - * | |
| 20 | - * @author 芋道源码 | |
| 21 | - */ | |
| 22 | -@Slf4j | |
| 23 | -public class IotMqttDownstreamHandler { | |
| 24 | - | |
| 25 | - private final IotDeviceMessageService deviceMessageService; | |
| 26 | - | |
| 27 | - private final IotMqttConnectionManager connectionManager; | |
| 28 | - | |
| 29 | - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, | |
| 30 | - IotMqttConnectionManager connectionManager) { | |
| 31 | - this.deviceMessageService = deviceMessageService; | |
| 32 | - this.connectionManager = connectionManager; | |
| 33 | - } | |
| 34 | - | |
| 35 | - /** | |
| 36 | - * 处理下行消息 | |
| 37 | - * | |
| 38 | - * @param message 设备消息 | |
| 39 | - * @return 是否处理成功 | |
| 40 | - */ | |
| 41 | - public boolean handleDownstreamMessage(IotDeviceMessage message) { | |
| 42 | - try { | |
| 43 | - // 1. 基础校验 | |
| 44 | - if (message == null || message.getDeviceId() == null) { | |
| 45 | - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); | |
| 46 | - return false; | |
| 47 | - } | |
| 48 | - | |
| 49 | - // 2. 检查设备是否在线 | |
| 50 | - if (connectionManager.isDeviceOffline(message.getDeviceId())) { | |
| 51 | - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); | |
| 52 | - return false; | |
| 53 | - } | |
| 54 | - | |
| 55 | - // 3. 获取连接信息 | |
| 56 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); | |
| 57 | - if (connectionInfo == null) { | |
| 58 | - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); | |
| 59 | - return false; | |
| 60 | - } | |
| 61 | - | |
| 62 | - // 4. 编码消息 | |
| 63 | - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), | |
| 64 | - connectionInfo.getDeviceName()); | |
| 65 | - if (payload == null || payload.length == 0) { | |
| 66 | - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); | |
| 67 | - return false; | |
| 68 | - } | |
| 69 | - | |
| 70 | - // 5. 发送消息到设备 | |
| 71 | - return sendMessageToDevice(message, connectionInfo, payload); | |
| 72 | - } catch (Exception e) { | |
| 73 | - if (message != null) { | |
| 74 | - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", | |
| 75 | - message.getDeviceId(), e.getMessage(), e); | |
| 76 | - } | |
| 77 | - return false; | |
| 78 | - } | |
| 79 | - } | |
| 80 | - | |
| 81 | - /** | |
| 82 | - * 发送消息到设备 | |
| 83 | - * | |
| 84 | - * @param message 设备消息 | |
| 85 | - * @param connectionInfo 连接信息 | |
| 86 | - * @param payload 消息负载 | |
| 87 | - * @return 是否发送成功 | |
| 88 | - */ | |
| 89 | - private boolean sendMessageToDevice(IotDeviceMessage message, | |
| 90 | - IotMqttConnectionManager.ConnectionInfo connectionInfo, | |
| 91 | - byte[] payload) { | |
| 92 | - // 1. 构建主题 | |
| 93 | - String topic = buildDownstreamTopic(message, connectionInfo); | |
| 94 | - if (StrUtil.isBlank(topic)) { | |
| 95 | - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", | |
| 96 | - message.getDeviceId(), message.getMethod()); | |
| 97 | - return false; | |
| 98 | - } | |
| 99 | - | |
| 100 | - // 2. 发送消息 | |
| 101 | - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); | |
| 102 | - if (success) { | |
| 103 | - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", | |
| 104 | - message.getDeviceId(), topic, message.getMethod()); | |
| 105 | - } else { | |
| 106 | - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", | |
| 107 | - message.getDeviceId(), topic, message.getMethod()); | |
| 108 | - } | |
| 109 | - return success; | |
| 110 | - } | |
| 111 | - | |
| 112 | - /** | |
| 113 | - * 构建下行消息主题 | |
| 114 | - * | |
| 115 | - * @param message 设备消息 | |
| 116 | - * @param connectionInfo 连接信息 | |
| 117 | - * @return 主题 | |
| 118 | - */ | |
| 119 | - private String buildDownstreamTopic(IotDeviceMessage message, | |
| 120 | - IotMqttConnectionManager.ConnectionInfo connectionInfo) { | |
| 121 | - String method = message.getMethod(); | |
| 122 | - if (StrUtil.isBlank(method)) { | |
| 123 | - return null; | |
| 124 | - } | |
| 125 | - | |
| 126 | - // 使用工具类构建主题,支持回复消息处理 | |
| 127 | - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); | |
| 128 | - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), | |
| 129 | - connectionInfo.getDeviceName(), isReply); | |
| 130 | - } | |
| 131 | - | |
| 132 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.hutool.core.util.ArrayUtil; | |
| 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.biz.dto.IotDeviceGetReqDTO; | |
| 13 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 14 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 15 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 16 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 17 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 18 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 19 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 20 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | |
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 23 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 24 | -import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | |
| 25 | -import io.netty.handler.codec.mqtt.MqttQoS; | |
| 26 | -import io.vertx.mqtt.MqttEndpoint; | |
| 27 | -import io.vertx.mqtt.MqttTopicSubscription; | |
| 28 | -import lombok.extern.slf4j.Slf4j; | |
| 29 | - | |
| 30 | -import java.util.List; | |
| 31 | -import java.util.Map; | |
| 32 | - | |
| 33 | -/** | |
| 34 | - * MQTT 上行消息处理器 | |
| 35 | - * | |
| 36 | - * @author 芋道源码 | |
| 37 | - */ | |
| 38 | -@Slf4j | |
| 39 | -public class IotMqttUpstreamHandler { | |
| 40 | - | |
| 41 | - /** | |
| 42 | - * 默认编解码类型(MQTT 使用 Alink 协议) | |
| 43 | - */ | |
| 44 | - private static final String DEFAULT_CODEC_TYPE = "Alink"; | |
| 45 | - | |
| 46 | - /** | |
| 47 | - * register 请求的 topic 后缀 | |
| 48 | - */ | |
| 49 | - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; | |
| 50 | - | |
| 51 | - private final IotDeviceMessageService deviceMessageService; | |
| 52 | - | |
| 53 | - private final IotMqttConnectionManager connectionManager; | |
| 54 | - | |
| 55 | - private final IotDeviceCommonApi deviceApi; | |
| 56 | - | |
| 57 | - private final String serverId; | |
| 58 | - | |
| 59 | - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, | |
| 60 | - IotDeviceMessageService deviceMessageService, | |
| 61 | - IotMqttConnectionManager connectionManager) { | |
| 62 | - this.deviceMessageService = deviceMessageService; | |
| 63 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 64 | - this.connectionManager = connectionManager; | |
| 65 | - this.serverId = protocol.getServerId(); | |
| 66 | - } | |
| 67 | - | |
| 68 | - /** | |
| 69 | - * 处理 MQTT 连接 | |
| 70 | - * | |
| 71 | - * @param endpoint MQTT 连接端点 | |
| 72 | - */ | |
| 73 | - public void handle(MqttEndpoint endpoint) { | |
| 74 | - String clientId = endpoint.clientIdentifier(); | |
| 75 | - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | |
| 76 | - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | |
| 77 | - | |
| 78 | - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", | |
| 79 | - clientId, username, connectionManager.getEndpointAddress(endpoint)); | |
| 80 | - | |
| 81 | - // 1. 先进行认证 | |
| 82 | - if (!authenticateDevice(clientId, username, password, endpoint)) { | |
| 83 | - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 84 | - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | |
| 85 | - return; | |
| 86 | - } | |
| 87 | - | |
| 88 | - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 89 | - | |
| 90 | - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息) | |
| 91 | - endpoint.pingHandler(v -> { | |
| 92 | - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId); | |
| 93 | - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理 | |
| 94 | - }); | |
| 95 | - | |
| 96 | - // 3. 设置异常和关闭处理器 | |
| 97 | - endpoint.exceptionHandler(ex -> { | |
| 98 | - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); | |
| 99 | - cleanupConnection(endpoint); | |
| 100 | - }); | |
| 101 | - endpoint.closeHandler(v -> { | |
| 102 | - cleanupConnection(endpoint); | |
| 103 | - }); | |
| 104 | - | |
| 105 | - // 4. 设置消息处理器 | |
| 106 | - endpoint.publishHandler(mqttMessage -> { | |
| 107 | - try { | |
| 108 | - // 4.1 根据 topic 判断是否为 register 请求 | |
| 109 | - String topic = mqttMessage.topicName(); | |
| 110 | - byte[] payload = mqttMessage.payload().getBytes(); | |
| 111 | - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { | |
| 112 | - // register 请求:使用默认编解码器处理(设备可能未注册) | |
| 113 | - processRegisterMessage(clientId, topic, payload, endpoint); | |
| 114 | - } else { | |
| 115 | - // 业务请求:正常处理 | |
| 116 | - processMessage(clientId, topic, payload); | |
| 117 | - } | |
| 118 | - | |
| 119 | - // 4.2 根据 QoS 级别发送相应的确认消息 | |
| 120 | - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | |
| 121 | - // QoS 1: 发送 PUBACK 确认 | |
| 122 | - endpoint.publishAcknowledge(mqttMessage.messageId()); | |
| 123 | - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { | |
| 124 | - // QoS 2: 发送 PUBREC 确认 | |
| 125 | - endpoint.publishReceived(mqttMessage.messageId()); | |
| 126 | - } | |
| 127 | - // QoS 0 无需确认 | |
| 128 | - } catch (Exception e) { | |
| 129 | - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | |
| 130 | - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | |
| 131 | - cleanupConnection(endpoint); | |
| 132 | - endpoint.close(); | |
| 133 | - } | |
| 134 | - }); | |
| 135 | - | |
| 136 | - // 5. 设置订阅处理器 | |
| 137 | - endpoint.subscribeHandler(subscribe -> { | |
| 138 | - // 提取主题名称列表用于日志显示 | |
| 139 | - List<String> topicNames = subscribe.topicSubscriptions().stream() | |
| 140 | - .map(MqttTopicSubscription::topicName) | |
| 141 | - .collect(java.util.stream.Collectors.toList()); | |
| 142 | - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | |
| 143 | - | |
| 144 | - // 提取 QoS 列表 | |
| 145 | - List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream() | |
| 146 | - .map(MqttTopicSubscription::qualityOfService) | |
| 147 | - .collect(java.util.stream.Collectors.toList()); | |
| 148 | - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | |
| 149 | - }); | |
| 150 | - | |
| 151 | - // 6. 设置取消订阅处理器 | |
| 152 | - endpoint.unsubscribeHandler(unsubscribe -> { | |
| 153 | - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); | |
| 154 | - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); | |
| 155 | - }); | |
| 156 | - | |
| 157 | - // 7. 设置 QoS 2消息的 PUBREL 处理器 | |
| 158 | - endpoint.publishReleaseHandler(endpoint::publishComplete); | |
| 159 | - | |
| 160 | - // 8. 设置断开连接处理器 | |
| 161 | - endpoint.disconnectHandler(v -> { | |
| 162 | - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); | |
| 163 | - cleanupConnection(endpoint); | |
| 164 | - }); | |
| 165 | - | |
| 166 | - // 9. 接受连接 | |
| 167 | - endpoint.accept(false); | |
| 168 | - } | |
| 169 | - | |
| 170 | - /** | |
| 171 | - * 处理消息 | |
| 172 | - * | |
| 173 | - * @param clientId 客户端 ID | |
| 174 | - * @param topic 主题 | |
| 175 | - * @param payload 消息内容 | |
| 176 | - */ | |
| 177 | - private void processMessage(String clientId, String topic, byte[] payload) { | |
| 178 | - // 1. 基础检查 | |
| 179 | - if (payload == null || payload.length == 0) { | |
| 180 | - return; | |
| 181 | - } | |
| 182 | - | |
| 183 | - // 2. 解析主题,获取 productKey 和 deviceName | |
| 184 | - String[] topicParts = topic.split("/"); | |
| 185 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 186 | - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | |
| 187 | - return; | |
| 188 | - } | |
| 189 | - | |
| 190 | - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) | |
| 191 | - String productKey = topicParts[2]; | |
| 192 | - String deviceName = topicParts[3]; | |
| 193 | - try { | |
| 194 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 195 | - if (message == null) { | |
| 196 | - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 197 | - return; | |
| 198 | - } | |
| 199 | - | |
| 200 | - // 4. 处理业务消息(认证已在连接时完成) | |
| 201 | - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", | |
| 202 | - productKey, deviceName, message.getMethod()); | |
| 203 | - handleBusinessRequest(message, productKey, deviceName); | |
| 204 | - } catch (Exception e) { | |
| 205 | - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 206 | - clientId, topic, e.getMessage(), e); | |
| 207 | - } | |
| 208 | - } | |
| 209 | - | |
| 210 | - /** | |
| 211 | - * 在 MQTT 连接时进行设备认证 | |
| 212 | - * | |
| 213 | - * @param clientId 客户端 ID | |
| 214 | - * @param username 用户名 | |
| 215 | - * @param password 密码 | |
| 216 | - * @param endpoint MQTT 连接端点 | |
| 217 | - * @return 认证是否成功 | |
| 218 | - */ | |
| 219 | - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { | |
| 220 | - try { | |
| 221 | - // 1. 参数校验 | |
| 222 | - if (StrUtil.hasEmpty(clientId, username, password)) { | |
| 223 | - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); | |
| 224 | - return false; | |
| 225 | - } | |
| 226 | - | |
| 227 | - // 2. 构建认证参数 | |
| 228 | - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | |
| 229 | - .setClientId(clientId) | |
| 230 | - .setUsername(username) | |
| 231 | - .setPassword(password); | |
| 232 | - | |
| 233 | - // 3. 调用设备认证 API | |
| 234 | - CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 235 | - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { | |
| 236 | - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 237 | - clientId, username, authResult.getMsg()); | |
| 238 | - return false; | |
| 239 | - } | |
| 240 | - | |
| 241 | - // 4. 获取设备信息 | |
| 242 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 243 | - if (deviceInfo == null) { | |
| 244 | - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); | |
| 245 | - return false; | |
| 246 | - } | |
| 247 | - | |
| 248 | - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() | |
| 249 | - .setProductKey(deviceInfo.getProductKey()) | |
| 250 | - .setDeviceName(deviceInfo.getDeviceName()); | |
| 251 | - | |
| 252 | - CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO); | |
| 253 | - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { | |
| 254 | - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 255 | - clientId, username, deviceResult.getMsg()); | |
| 256 | - return false; | |
| 257 | - } | |
| 258 | - | |
| 259 | - // 5. 注册连接 | |
| 260 | - IotDeviceRespDTO device = deviceResult.getData(); | |
| 261 | - registerConnection(endpoint, device, clientId); | |
| 262 | - | |
| 263 | - // 6. 发送设备上线消息 | |
| 264 | - sendOnlineMessage(device); | |
| 265 | - | |
| 266 | - return true; | |
| 267 | - } catch (Exception e) { | |
| 268 | - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); | |
| 269 | - return false; | |
| 270 | - } | |
| 271 | - } | |
| 272 | - | |
| 273 | - /** | |
| 274 | - * 处理 register 消息(设备动态注册,使用默认编解码器) | |
| 275 | - * | |
| 276 | - * @param clientId 客户端 ID | |
| 277 | - * @param topic 主题 | |
| 278 | - * @param payload 消息内容 | |
| 279 | - * @param endpoint MQTT 连接端点 | |
| 280 | - */ | |
| 281 | - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { | |
| 282 | - // 1.1 基础检查 | |
| 283 | - if (ArrayUtil.isEmpty(payload)) { | |
| 284 | - return; | |
| 285 | - } | |
| 286 | - // 1.2 解析主题,获取 productKey 和 deviceName | |
| 287 | - String[] topicParts = topic.split("/"); | |
| 288 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 289 | - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); | |
| 290 | - return; | |
| 291 | - } | |
| 292 | - String productKey = topicParts[2]; | |
| 293 | - String deviceName = topicParts[3]; | |
| 294 | - | |
| 295 | - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | |
| 296 | - IotDeviceMessage message; | |
| 297 | - try { | |
| 298 | - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | |
| 299 | - if (message == null) { | |
| 300 | - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 301 | - return; | |
| 302 | - } | |
| 303 | - } catch (Exception e) { | |
| 304 | - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", | |
| 305 | - clientId, topic, e.getMessage(), e); | |
| 306 | - return; | |
| 307 | - } | |
| 308 | - | |
| 309 | - // 3. 处理设备动态注册请求 | |
| 310 | - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", | |
| 311 | - productKey, deviceName, message.getMethod()); | |
| 312 | - try { | |
| 313 | - handleRegisterRequest(message, productKey, deviceName, endpoint); | |
| 314 | - } catch (Exception e) { | |
| 315 | - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 316 | - clientId, topic, e.getMessage(), e); | |
| 317 | - } | |
| 318 | - } | |
| 319 | - | |
| 320 | - /** | |
| 321 | - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) | |
| 322 | - * | |
| 323 | - * @param message 消息信息 | |
| 324 | - * @param productKey 产品 Key | |
| 325 | - * @param deviceName 设备名称 | |
| 326 | - * @param endpoint MQTT 连接端点 | |
| 327 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 328 | - */ | |
| 329 | - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { | |
| 330 | - String clientId = endpoint.clientIdentifier(); | |
| 331 | - try { | |
| 332 | - // 1. 解析注册参数 | |
| 333 | - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | |
| 334 | - if (params == null) { | |
| 335 | - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); | |
| 336 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); | |
| 337 | - return; | |
| 338 | - } | |
| 339 | - | |
| 340 | - // 2. 调用动态注册 API | |
| 341 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 342 | - if (result.isError()) { | |
| 343 | - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | |
| 344 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); | |
| 345 | - return; | |
| 346 | - } | |
| 347 | - | |
| 348 | - // 3. 发送成功响应(包含 deviceSecret) | |
| 349 | - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); | |
| 350 | - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", | |
| 351 | - params.getDeviceName(), clientId); | |
| 352 | - } catch (Exception e) { | |
| 353 | - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); | |
| 354 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); | |
| 355 | - } | |
| 356 | - } | |
| 357 | - | |
| 358 | - /** | |
| 359 | - * 解析注册参数 | |
| 360 | - * | |
| 361 | - * @param params 参数对象(通常为 Map 类型) | |
| 362 | - * @return 注册参数 DTO,解析失败时返回 null | |
| 363 | - */ | |
| 364 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 365 | - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | |
| 366 | - if (params == null) { | |
| 367 | - return null; | |
| 368 | - } | |
| 369 | - try { | |
| 370 | - // 参数默认为 Map 类型,直接转换 | |
| 371 | - if (params instanceof Map) { | |
| 372 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 373 | - return new IotDeviceRegisterReqDTO() | |
| 374 | - .setProductKey(MapUtil.getStr(paramMap, "productKey")) | |
| 375 | - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | |
| 376 | - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | |
| 377 | - } | |
| 378 | - // 如果已经是目标类型,直接返回 | |
| 379 | - if (params instanceof IotDeviceRegisterReqDTO) { | |
| 380 | - return (IotDeviceRegisterReqDTO) params; | |
| 381 | - } | |
| 382 | - | |
| 383 | - // 其他情况尝试 JSON 转换 | |
| 384 | - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); | |
| 385 | - } catch (Exception e) { | |
| 386 | - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | |
| 387 | - return null; | |
| 388 | - } | |
| 389 | - } | |
| 390 | - | |
| 391 | - /** | |
| 392 | - * 发送注册成功响应(包含 deviceSecret) | |
| 393 | - * | |
| 394 | - * @param endpoint MQTT 连接端点 | |
| 395 | - * @param productKey 产品 Key | |
| 396 | - * @param deviceName 设备名称 | |
| 397 | - * @param requestId 请求 ID | |
| 398 | - * @param registerResp 注册响应 | |
| 399 | - */ | |
| 400 | - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 401 | - String requestId, IotDeviceRegisterRespDTO registerResp) { | |
| 402 | - try { | |
| 403 | - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | |
| 404 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 405 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | |
| 406 | - | |
| 407 | - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | |
| 408 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 409 | - | |
| 410 | - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | |
| 411 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | |
| 412 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | |
| 413 | - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | |
| 414 | - MqttQoS.AT_LEAST_ONCE, false, false); | |
| 415 | - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); | |
| 416 | - } catch (Exception e) { | |
| 417 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", | |
| 418 | - endpoint.clientIdentifier(), e); | |
| 419 | - } | |
| 420 | - } | |
| 421 | - | |
| 422 | - /** | |
| 423 | - * 发送注册错误响应 | |
| 424 | - * | |
| 425 | - * @param endpoint MQTT 连接端点 | |
| 426 | - * @param productKey 产品 Key | |
| 427 | - * @param deviceName 设备名称 | |
| 428 | - * @param requestId 请求 ID | |
| 429 | - * @param errorMessage 错误消息 | |
| 430 | - */ | |
| 431 | - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 432 | - String requestId, String errorMessage) { | |
| 433 | - try { | |
| 434 | - // 1. 构建响应消息 | |
| 435 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 436 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); | |
| 437 | - | |
| 438 | - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | |
| 439 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 440 | - | |
| 441 | - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | |
| 442 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | |
| 443 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | |
| 444 | - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | |
| 445 | - MqttQoS.AT_LEAST_ONCE, false, false); | |
| 446 | - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); | |
| 447 | - } catch (Exception e) { | |
| 448 | - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", | |
| 449 | - endpoint.clientIdentifier(), e); | |
| 450 | - } | |
| 451 | - } | |
| 452 | - | |
| 453 | - /** | |
| 454 | - * 处理业务请求 | |
| 455 | - */ | |
| 456 | - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { | |
| 457 | - // 发送消息到消息总线 | |
| 458 | - message.setServerId(serverId); | |
| 459 | - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 460 | - } | |
| 461 | - | |
| 462 | - /** | |
| 463 | - * 注册连接 | |
| 464 | - */ | |
| 465 | - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { | |
| 466 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() | |
| 467 | - .setDeviceId(device.getId()) | |
| 468 | - .setProductKey(device.getProductKey()) | |
| 469 | - .setDeviceName(device.getDeviceName()) | |
| 470 | - .setClientId(clientId) | |
| 471 | - .setAuthenticated(true) | |
| 472 | - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); | |
| 473 | - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); | |
| 474 | - } | |
| 475 | - | |
| 476 | - /** | |
| 477 | - * 发送设备上线消息 | |
| 478 | - */ | |
| 479 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 480 | - try { | |
| 481 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 482 | - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 483 | - device.getDeviceName(), serverId); | |
| 484 | - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); | |
| 485 | - } catch (Exception e) { | |
| 486 | - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); | |
| 487 | - } | |
| 488 | - } | |
| 489 | - | |
| 490 | - /** | |
| 491 | - * 清理连接 | |
| 492 | - */ | |
| 493 | - private void cleanupConnection(MqttEndpoint endpoint) { | |
| 494 | - try { | |
| 495 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 496 | - if (connectionInfo != null) { | |
| 497 | - // 发送设备离线消息 | |
| 498 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 499 | - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 500 | - connectionInfo.getDeviceName(), serverId); | |
| 501 | - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); | |
| 502 | - } | |
| 503 | - | |
| 504 | - // 注销连接 | |
| 505 | - connectionManager.unregisterConnection(endpoint); | |
| 506 | - } catch (Exception e) { | |
| 507 | - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage()); | |
| 508 | - } | |
| 509 | - } | |
| 510 | - | |
| 511 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; | |
| 4 | +import jakarta.validation.Valid; | |
| 5 | +import jakarta.validation.constraints.Min; | |
| 6 | +import jakarta.validation.constraints.NotNull; | |
| 7 | +import lombok.Data; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT TCP 协议配置 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Data | |
| 15 | +public class IotTcpConfig { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 最大连接数 | |
| 19 | + */ | |
| 20 | + @NotNull(message = "最大连接数不能为空") | |
| 21 | + @Min(value = 1, message = "最大连接数必须大于 0") | |
| 22 | + private Integer maxConnections = 1000; | |
| 23 | + /** | |
| 24 | + * 心跳超时时间(毫秒) | |
| 25 | + */ | |
| 26 | + @NotNull(message = "心跳超时时间不能为空") | |
| 27 | + @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒") | |
| 28 | + private Long keepAliveTimeoutMs = 30000L; | |
| 29 | + | |
| 30 | + /** | |
| 31 | + * 拆包配置 | |
| 32 | + */ | |
| 33 | + @Valid | |
| 34 | + private CodecConfig codec; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * TCP 拆包配置 | |
| 38 | + */ | |
| 39 | + @Data | |
| 40 | + public static class CodecConfig { | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 拆包类型 | |
| 44 | + * | |
| 45 | + * @see IotTcpCodecTypeEnum | |
| 46 | + */ | |
| 47 | + @NotNull(message = "拆包类型不能为空") | |
| 48 | + private String type; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * LENGTH_FIELD: 长度字段偏移量 | |
| 52 | + * <p> | |
| 53 | + * 表示长度字段在消息中的起始位置(从 0 开始) | |
| 54 | + */ | |
| 55 | + private Integer lengthFieldOffset; | |
| 56 | + /** | |
| 57 | + * LENGTH_FIELD: 长度字段长度(字节数) | |
| 58 | + * <p> | |
| 59 | + * 常见值:1(最大 255)、2(最大 65535)、4(最大 2GB) | |
| 60 | + */ | |
| 61 | + private Integer lengthFieldLength; | |
| 62 | + /** | |
| 63 | + * LENGTH_FIELD: 长度调整值 | |
| 64 | + * <p> | |
| 65 | + * 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度 | |
| 66 | + */ | |
| 67 | + private Integer lengthAdjustment = 0; | |
| 68 | + /** | |
| 69 | + * LENGTH_FIELD: 跳过的初始字节数 | |
| 70 | + * <p> | |
| 71 | + * 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength | |
| 72 | + */ | |
| 73 | + private Integer initialBytesToStrip = 0; | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * DELIMITER: 分隔符 | |
| 77 | + * <p> | |
| 78 | + * 支持转义字符:\n(换行)、\r(回车)、\r\n(回车换行) | |
| 79 | + */ | |
| 80 | + private String delimiter; | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * FIXED_LENGTH: 固定消息长度(字节) | |
| 84 | + * <p> | |
| 85 | + * 每条消息的固定长度 | |
| 86 | + */ | |
| 87 | + private Integer fixedLength; | |
| 88 | + | |
| 89 | + } | |
| 90 | + | |
| 91 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 10 | -import jakarta.annotation.PostConstruct; | |
| 11 | -import lombok.RequiredArgsConstructor; | |
| 12 | -import lombok.extern.slf4j.Slf4j; | |
| 13 | - | |
| 14 | -/** | |
| 15 | - * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 | |
| 16 | - * | |
| 17 | - * @author 芋道源码 | |
| 18 | - */ | |
| 19 | -@Slf4j | |
| 20 | -@RequiredArgsConstructor | |
| 21 | -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 22 | - | |
| 23 | - private final IotTcpUpstreamProtocol protocol; | |
| 24 | - | |
| 25 | - private final IotDeviceMessageService messageService; | |
| 26 | - | |
| 27 | - private final IotTcpConnectionManager connectionManager; | |
| 28 | - | |
| 29 | - private final IotMessageBus messageBus; | |
| 30 | - | |
| 31 | - private IotTcpDownstreamHandler downstreamHandler; | |
| 32 | - | |
| 33 | - @PostConstruct | |
| 34 | - public void init() { | |
| 35 | - // 初始化下游处理器 | |
| 36 | - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager); | |
| 37 | - // 注册下游订阅者 | |
| 38 | - messageBus.register(this); | |
| 39 | - log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", | |
| 40 | - protocol.getServerId(), getTopic()); | |
| 41 | - } | |
| 42 | - | |
| 43 | - @Override | |
| 44 | - public String getTopic() { | |
| 45 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | |
| 46 | - } | |
| 47 | - | |
| 48 | - @Override | |
| 49 | - public String getGroup() { | |
| 50 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 51 | - return getTopic(); | |
| 52 | - } | |
| 53 | - | |
| 54 | - @Override | |
| 55 | - public void onMessage(IotDeviceMessage message) { | |
| 56 | - try { | |
| 57 | - downstreamHandler.handle(message); | |
| 58 | - } catch (Exception e) { | |
| 59 | - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", | |
| 60 | - message.getDeviceId(), message.getMethod(), message.getId(), e); | |
| 61 | - } | |
| 62 | - } | |
| 63 | - | |
| 64 | -} | |
| 65 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | |
| 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.enums.IotSerializeTypeEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | |
| 19 | +import io.vertx.core.Vertx; | |
| 20 | +import io.vertx.core.net.NetServer; | |
| 21 | +import io.vertx.core.net.NetServerOptions; | |
| 22 | +import io.vertx.core.net.PemKeyCertOptions; | |
| 23 | +import lombok.Getter; | |
| 24 | +import lombok.extern.slf4j.Slf4j; | |
| 25 | +import cn.hutool.core.lang.Assert; | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * IoT TCP 协议实现 | |
| 29 | + * <p> | |
| 30 | + * 基于 Vert.x 实现 TCP 服务器,接收设备上行消息 | |
| 31 | + * | |
| 32 | + * @author 芋道源码 | |
| 33 | + */ | |
| 34 | +@Slf4j | |
| 35 | +public class IotTcpProtocol implements IotProtocol { | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * 协议配置 | |
| 39 | + */ | |
| 40 | + private final ProtocolProperties properties; | |
| 41 | + /** | |
| 42 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 43 | + */ | |
| 44 | + @Getter | |
| 45 | + private final String serverId; | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * 运行状态 | |
| 49 | + */ | |
| 50 | + @Getter | |
| 51 | + private volatile boolean running = false; | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * Vert.x 实例 | |
| 55 | + */ | |
| 56 | + private Vertx vertx; | |
| 57 | + /** | |
| 58 | + * TCP 服务器 | |
| 59 | + */ | |
| 60 | + private NetServer tcpServer; | |
| 61 | + /** | |
| 62 | + * TCP 连接管理器 | |
| 63 | + */ | |
| 64 | + private final IotTcpConnectionManager connectionManager; | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * 下行消息订阅者 | |
| 68 | + */ | |
| 69 | + private final IotTcpDownstreamSubscriber downstreamSubscriber; | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * 消息序列化器 | |
| 73 | + */ | |
| 74 | + private final IotMessageSerializer serializer; | |
| 75 | + /** | |
| 76 | + * TCP 帧编解码器 | |
| 77 | + */ | |
| 78 | + private final IotTcpFrameCodec frameCodec; | |
| 79 | + | |
| 80 | + public IotTcpProtocol(ProtocolProperties properties) { | |
| 81 | + IotTcpConfig tcpConfig = properties.getTcp(); | |
| 82 | + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); | |
| 83 | + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); | |
| 84 | + this.properties = properties; | |
| 85 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 86 | + | |
| 87 | + // 初始化序列化器 | |
| 88 | + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); | |
| 89 | + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); | |
| 90 | + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); | |
| 91 | + this.serializer = serializerManager.get(serializeType); | |
| 92 | + // 初始化帧编解码器 | |
| 93 | + this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); | |
| 94 | + | |
| 95 | + // 初始化连接管理器 | |
| 96 | + this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); | |
| 97 | + | |
| 98 | + // 初始化下行消息订阅者 | |
| 99 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 100 | + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); | |
| 101 | + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 102 | + } | |
| 103 | + | |
| 104 | + @Override | |
| 105 | + public String getId() { | |
| 106 | + return properties.getId(); | |
| 107 | + } | |
| 108 | + | |
| 109 | + @Override | |
| 110 | + public IotProtocolTypeEnum getType() { | |
| 111 | + return IotProtocolTypeEnum.TCP; | |
| 112 | + } | |
| 113 | + | |
| 114 | + @Override | |
| 115 | + public void start() { | |
| 116 | + if (running) { | |
| 117 | + log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId()); | |
| 118 | + return; | |
| 119 | + } | |
| 120 | + | |
| 121 | + // 1.1 创建 Vertx 实例 | |
| 122 | + this.vertx = Vertx.vertx(); | |
| 123 | + | |
| 124 | + // 1.2 创建服务器选项 | |
| 125 | + IotTcpConfig tcpConfig = properties.getTcp(); | |
| 126 | + NetServerOptions options = new NetServerOptions() | |
| 127 | + .setPort(properties.getPort()) | |
| 128 | + .setTcpKeepAlive(true) | |
| 129 | + .setTcpNoDelay(true) | |
| 130 | + .setReuseAddress(true) | |
| 131 | + .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 | |
| 132 | + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); | |
| 133 | + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { | |
| 134 | + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | |
| 135 | + .setKeyPath(sslConfig.getSslKeyPath()) | |
| 136 | + .setCertPath(sslConfig.getSslCertPath()); | |
| 137 | + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 138 | + } | |
| 139 | + | |
| 140 | + // 1.3 创建服务器并设置连接处理器 | |
| 141 | + tcpServer = vertx.createNetServer(options); | |
| 142 | + tcpServer.connectHandler(socket -> { | |
| 143 | + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(serverId, frameCodec, serializer, connectionManager); | |
| 144 | + handler.handle(socket); | |
| 145 | + }); | |
| 146 | + | |
| 147 | + // 1.4 启动 TCP 服务器 | |
| 148 | + try { | |
| 149 | + tcpServer.listen().result(); | |
| 150 | + running = true; | |
| 151 | + log.info("[start][IoT TCP 协议 {} 启动成功,端口:{},serverId:{}]", | |
| 152 | + getId(), properties.getPort(), serverId); | |
| 153 | + | |
| 154 | + // 2. 启动下行消息订阅者 | |
| 155 | + this.downstreamSubscriber.start(); | |
| 156 | + } catch (Exception e) { | |
| 157 | + log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); | |
| 158 | + // 启动失败时关闭资源 | |
| 159 | + if (tcpServer != null) { | |
| 160 | + tcpServer.close(); | |
| 161 | + tcpServer = null; | |
| 162 | + } | |
| 163 | + if (vertx != null) { | |
| 164 | + vertx.close(); | |
| 165 | + vertx = null; | |
| 166 | + } | |
| 167 | + throw e; | |
| 168 | + } | |
| 169 | + } | |
| 170 | + | |
| 171 | + @Override | |
| 172 | + public void stop() { | |
| 173 | + if (!running) { | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + // 1. 停止下行消息订阅者 | |
| 177 | + try { | |
| 178 | + downstreamSubscriber.stop(); | |
| 179 | + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 180 | + } catch (Exception e) { | |
| 181 | + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 182 | + } | |
| 183 | + | |
| 184 | + // 2.1 关闭 TCP 服务器 | |
| 185 | + if (tcpServer != null) { | |
| 186 | + try { | |
| 187 | + tcpServer.close().result(); | |
| 188 | + log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId()); | |
| 189 | + } catch (Exception e) { | |
| 190 | + log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e); | |
| 191 | + } | |
| 192 | + tcpServer = null; | |
| 193 | + } | |
| 194 | + // 2.2 关闭 Vertx 实例 | |
| 195 | + if (vertx != null) { | |
| 196 | + try { | |
| 197 | + vertx.close().result(); | |
| 198 | + log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId()); | |
| 199 | + } catch (Exception e) { | |
| 200 | + log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e); | |
| 201 | + } | |
| 202 | + vertx = null; | |
| 203 | + } | |
| 204 | + running = false; | |
| 205 | + log.info("[stop][IoT TCP 协议 {} 已停止]", getId()); | |
| 206 | + } | |
| 207 | + | |
| 208 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 9 | -import io.vertx.core.Vertx; | |
| 10 | -import io.vertx.core.net.NetServer; | |
| 11 | -import io.vertx.core.net.NetServerOptions; | |
| 12 | -import io.vertx.core.net.PemKeyCertOptions; | |
| 13 | -import jakarta.annotation.PostConstruct; | |
| 14 | -import jakarta.annotation.PreDestroy; | |
| 15 | -import lombok.Getter; | |
| 16 | -import lombok.extern.slf4j.Slf4j; | |
| 17 | - | |
| 18 | -/** | |
| 19 | - * IoT 网关 TCP 协议:接收设备上行消息 | |
| 20 | - * | |
| 21 | - * @author 芋道源码 | |
| 22 | - */ | |
| 23 | -@Slf4j | |
| 24 | -public class IotTcpUpstreamProtocol { | |
| 25 | - | |
| 26 | - private final IotGatewayProperties.TcpProperties tcpProperties; | |
| 27 | - | |
| 28 | - private final IotDeviceService deviceService; | |
| 29 | - | |
| 30 | - private final IotDeviceMessageService messageService; | |
| 31 | - | |
| 32 | - private final IotTcpConnectionManager connectionManager; | |
| 33 | - | |
| 34 | - private final Vertx vertx; | |
| 35 | - | |
| 36 | - @Getter | |
| 37 | - private final String serverId; | |
| 38 | - | |
| 39 | - private NetServer tcpServer; | |
| 40 | - | |
| 41 | - public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, | |
| 42 | - IotDeviceService deviceService, | |
| 43 | - IotDeviceMessageService messageService, | |
| 44 | - IotTcpConnectionManager connectionManager, | |
| 45 | - Vertx vertx) { | |
| 46 | - this.tcpProperties = tcpProperties; | |
| 47 | - this.deviceService = deviceService; | |
| 48 | - this.messageService = messageService; | |
| 49 | - this.connectionManager = connectionManager; | |
| 50 | - this.vertx = vertx; | |
| 51 | - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); | |
| 52 | - } | |
| 53 | - | |
| 54 | - @PostConstruct | |
| 55 | - public void start() { | |
| 56 | - // 创建服务器选项 | |
| 57 | - NetServerOptions options = new NetServerOptions() | |
| 58 | - .setPort(tcpProperties.getPort()) | |
| 59 | - .setTcpKeepAlive(true) | |
| 60 | - .setTcpNoDelay(true) | |
| 61 | - .setReuseAddress(true); | |
| 62 | - // 配置 SSL(如果启用) | |
| 63 | - if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { | |
| 64 | - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | |
| 65 | - .setKeyPath(tcpProperties.getSslKeyPath()) | |
| 66 | - .setCertPath(tcpProperties.getSslCertPath()); | |
| 67 | - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | |
| 68 | - } | |
| 69 | - | |
| 70 | - // 创建服务器并设置连接处理器 | |
| 71 | - tcpServer = vertx.createNetServer(options); | |
| 72 | - tcpServer.connectHandler(socket -> { | |
| 73 | - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, | |
| 74 | - connectionManager); | |
| 75 | - handler.handle(socket); | |
| 76 | - }); | |
| 77 | - | |
| 78 | - // 启动服务器 | |
| 79 | - try { | |
| 80 | - tcpServer.listen().result(); | |
| 81 | - log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); | |
| 82 | - } catch (Exception e) { | |
| 83 | - log.error("[start][IoT 网关 TCP 协议启动失败]", e); | |
| 84 | - throw e; | |
| 85 | - } | |
| 86 | - } | |
| 87 | - | |
| 88 | - @PreDestroy | |
| 89 | - public void stop() { | |
| 90 | - if (tcpServer != null) { | |
| 91 | - try { | |
| 92 | - tcpServer.close().result(); | |
| 93 | - log.info("[stop][IoT 网关 TCP 协议已停止]"); | |
| 94 | - } catch (Exception e) { | |
| 95 | - log.error("[stop][IoT 网关 TCP 协议停止失败]", e); | |
| 96 | - } | |
| 97 | - } | |
| 98 | - } | |
| 99 | -} | |
| 100 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec; | |
| 7 | +import lombok.AllArgsConstructor; | |
| 8 | +import lombok.Getter; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT TCP 拆包类型枚举 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + */ | |
| 15 | +@AllArgsConstructor | |
| 16 | +@Getter | |
| 17 | +public enum IotTcpCodecTypeEnum { | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * 基于固定长度的拆包 | |
| 21 | + */ | |
| 22 | + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec.class), | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * 基于分隔符的拆包 | |
| 26 | + */ | |
| 27 | + DELIMITER("delimiter", IotTcpDelimiterFrameCodec.class), | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 基于长度字段的拆包 | |
| 31 | + */ | |
| 32 | + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec.class), | |
| 33 | + ; | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 类型标识 | |
| 37 | + */ | |
| 38 | + private final String type; | |
| 39 | + /** | |
| 40 | + * 编解码器类 | |
| 41 | + */ | |
| 42 | + private final Class<? extends IotTcpFrameCodec> codecClass; | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 根据类型获取枚举 | |
| 46 | + * | |
| 47 | + * @param type 类型标识 | |
| 48 | + * @return 枚举值 | |
| 49 | + */ | |
| 50 | + public static IotTcpCodecTypeEnum of(String type) { | |
| 51 | + return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values()); | |
| 52 | + } | |
| 53 | + | |
| 54 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; | |
| 2 | + | |
| 3 | +import io.vertx.core.Handler; | |
| 4 | +import io.vertx.core.buffer.Buffer; | |
| 5 | +import io.vertx.core.parsetools.RecordParser; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT TCP 帧编解码器接口 | |
| 9 | + * <p> | |
| 10 | + * 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +public interface IotTcpFrameCodec { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 获取编解码器类型 | |
| 18 | + * | |
| 19 | + * @return 编解码器类型 | |
| 20 | + */ | |
| 21 | + IotTcpCodecTypeEnum getType(); | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 创建解码器(RecordParser) | |
| 25 | + * <p> | |
| 26 | + * 每个连接调用一次,返回的 parser 需绑定到 socket.handler() | |
| 27 | + * | |
| 28 | + * @param handler 消息处理器,当收到完整的消息帧后回调 | |
| 29 | + * @return RecordParser 实例 | |
| 30 | + */ | |
| 31 | + RecordParser createDecodeParser(Handler<Buffer> handler); | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 编码消息(加帧) | |
| 35 | + * <p> | |
| 36 | + * 根据不同的编解码类型添加帧头/分隔符 | |
| 37 | + * | |
| 38 | + * @param data 原始数据 | |
| 39 | + * @return 编码后的数据(带帧头/分隔符) | |
| 40 | + */ | |
| 41 | + Buffer encode(byte[] data); | |
| 42 | + | |
| 43 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.ReflectUtil; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT TCP 帧编解码器工厂 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +public class IotTcpFrameCodecFactory { | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * 根据配置创建编解码器 | |
| 16 | + * | |
| 17 | + * @param config 拆包配置 | |
| 18 | + * @return 编解码器实例,如果配置为空则返回 null | |
| 19 | + */ | |
| 20 | + public static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) { | |
| 21 | + Assert.notNull(config, "CodecConfig 不能为空"); | |
| 22 | + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType()); | |
| 23 | + Assert.notNull(type, "不支持的 CodecType 类型:" + config.getType()); | |
| 24 | + return ReflectUtil.newInstance(type.getCodecClass(), config); | |
| 25 | + } | |
| 26 | + | |
| 27 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | |
| 7 | +import io.vertx.core.Handler; | |
| 8 | +import io.vertx.core.buffer.Buffer; | |
| 9 | +import io.vertx.core.parsetools.RecordParser; | |
| 10 | +import lombok.extern.slf4j.Slf4j; | |
| 11 | +import cn.hutool.core.lang.Assert; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * IoT TCP 分隔符帧编解码器 | |
| 15 | + * <p> | |
| 16 | + * 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符 | |
| 17 | + * <p> | |
| 18 | + * 支持的分隔符: | |
| 19 | + * <ul> | |
| 20 | + * <li>\n - 换行符</li> | |
| 21 | + * <li>\r - 回车符</li> | |
| 22 | + * <li>\r\n - 回车换行</li> | |
| 23 | + * <li>自定义字符串</li> | |
| 24 | + * </ul> | |
| 25 | + * | |
| 26 | + * @author 芋道源码 | |
| 27 | + */ | |
| 28 | +@Slf4j | |
| 29 | +public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 最大记录大小(64KB),防止 DoS 攻击 | |
| 33 | + */ | |
| 34 | + private static final int MAX_RECORD_SIZE = 65536; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 解析后的分隔符字节数组 | |
| 38 | + */ | |
| 39 | + private final byte[] delimiterBytes; | |
| 40 | + | |
| 41 | + public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { | |
| 42 | + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); | |
| 43 | + this.delimiterBytes = parseDelimiter(config.getDelimiter()); | |
| 44 | + } | |
| 45 | + | |
| 46 | + @Override | |
| 47 | + public IotTcpCodecTypeEnum getType() { | |
| 48 | + return IotTcpCodecTypeEnum.DELIMITER; | |
| 49 | + } | |
| 50 | + | |
| 51 | + @Override | |
| 52 | + public RecordParser createDecodeParser(Handler<Buffer> handler) { | |
| 53 | + RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes)); | |
| 54 | + parser.maxRecordSize(MAX_RECORD_SIZE); // 设置最大记录大小,防止 DoS 攻击 | |
| 55 | + // 处理完整消息(不包含分隔符) | |
| 56 | + parser.handler(handler); | |
| 57 | + parser.exceptionHandler(ex -> { | |
| 58 | + throw new RuntimeException("[createDecodeParser][解析异常]", ex); | |
| 59 | + }); | |
| 60 | + return parser; | |
| 61 | + } | |
| 62 | + | |
| 63 | + @Override | |
| 64 | + public Buffer encode(byte[] data) { | |
| 65 | + Buffer buffer = Buffer.buffer(); | |
| 66 | + buffer.appendBytes(data); | |
| 67 | + buffer.appendBytes(delimiterBytes); | |
| 68 | + return buffer; | |
| 69 | + } | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * 解析分隔符字符串为字节数组 | |
| 73 | + * <p> | |
| 74 | + * 支持转义字符:\n、\r、\r\n、\t | |
| 75 | + * | |
| 76 | + * @param delimiter 分隔符字符串 | |
| 77 | + * @return 分隔符字节数组 | |
| 78 | + */ | |
| 79 | + private byte[] parseDelimiter(String delimiter) { | |
| 80 | + // 处理转义字符 | |
| 81 | + String parsed = delimiter | |
| 82 | + .replace("\\r\\n", "\r\n") | |
| 83 | + .replace("\\r", "\r") | |
| 84 | + .replace("\\n", "\n") | |
| 85 | + .replace("\\t", "\t"); | |
| 86 | + return StrUtil.utf8Bytes(parsed); | |
| 87 | + } | |
| 88 | + | |
| 89 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | |
| 6 | +import io.vertx.core.Handler; | |
| 7 | +import io.vertx.core.buffer.Buffer; | |
| 8 | +import io.vertx.core.parsetools.RecordParser; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | +import cn.hutool.core.lang.Assert; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * IoT TCP 定长帧编解码器 | |
| 14 | + * <p> | |
| 15 | + * 基于固定长度的拆包策略,每条消息固定字节数 | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@Slf4j | |
| 20 | +public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * 固定消息长度 | |
| 24 | + */ | |
| 25 | + private final int fixedLength; | |
| 26 | + | |
| 27 | + public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) { | |
| 28 | + Assert.notNull(config.getFixedLength(), "fixedLength 不能为空"); | |
| 29 | + this.fixedLength = config.getFixedLength(); | |
| 30 | + } | |
| 31 | + | |
| 32 | + @Override | |
| 33 | + public IotTcpCodecTypeEnum getType() { | |
| 34 | + return IotTcpCodecTypeEnum.FIXED_LENGTH; | |
| 35 | + } | |
| 36 | + | |
| 37 | + @Override | |
| 38 | + public RecordParser createDecodeParser(Handler<Buffer> handler) { | |
| 39 | + RecordParser parser = RecordParser.newFixed(fixedLength); | |
| 40 | + parser.handler(handler); | |
| 41 | + parser.exceptionHandler(ex -> { | |
| 42 | + throw new RuntimeException("[createDecodeParser][解析异常]", ex); | |
| 43 | + }); | |
| 44 | + return parser; | |
| 45 | + } | |
| 46 | + | |
| 47 | + @Override | |
| 48 | + public Buffer encode(byte[] data) { | |
| 49 | + // 校验数据长度不能超过固定长度 | |
| 50 | + if (data.length > fixedLength) { | |
| 51 | + throw new IllegalArgumentException(String.format( | |
| 52 | + "数据长度 %d 超过固定长度 %d", data.length, fixedLength)); | |
| 53 | + } | |
| 54 | + Buffer buffer = Buffer.buffer(fixedLength); | |
| 55 | + buffer.appendBytes(data); | |
| 56 | + // 如果数据不足固定长度,填充 0(RecordParser.newFixed 解码时按固定长度读取,所以发送端需要填充) | |
| 57 | + if (data.length < fixedLength) { | |
| 58 | + byte[] padding = new byte[fixedLength - data.length]; | |
| 59 | + buffer.appendBytes(padding); | |
| 60 | + } | |
| 61 | + return buffer; | |
| 62 | + } | |
| 63 | + | |
| 64 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | |
| 6 | +import io.vertx.core.Handler; | |
| 7 | +import io.vertx.core.buffer.Buffer; | |
| 8 | +import io.vertx.core.parsetools.RecordParser; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | +import cn.hutool.core.lang.Assert; | |
| 11 | + | |
| 12 | +import java.util.concurrent.atomic.AtomicReference; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * IoT TCP 长度字段帧编解码器 | |
| 16 | + * <p> | |
| 17 | + * 基于长度字段的拆包策略,消息格式:[长度字段][消息体] | |
| 18 | + * <p> | |
| 19 | + * 参数说明: | |
| 20 | + * <ul> | |
| 21 | + * <li>lengthFieldOffset: 长度字段在消息中的偏移量</li> | |
| 22 | + * <li>lengthFieldLength: 长度字段的字节数(1/2/4)</li> | |
| 23 | + * <li>lengthAdjustment: 长度调整值,用于调整长度字段的实际含义</li> | |
| 24 | + * <li>initialBytesToStrip: 解码后跳过的字节数</li> | |
| 25 | + * </ul> | |
| 26 | + * | |
| 27 | + * @author 芋道源码 | |
| 28 | + */ | |
| 29 | +@Slf4j | |
| 30 | +public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 最大帧长度(64KB),防止 DoS 攻击 | |
| 34 | + */ | |
| 35 | + private static final int MAX_FRAME_LENGTH = 65536; | |
| 36 | + | |
| 37 | + private final int lengthFieldOffset; | |
| 38 | + private final int lengthFieldLength; | |
| 39 | + private final int lengthAdjustment; | |
| 40 | + private final int initialBytesToStrip; | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 头部长度 = 长度字段偏移量 + 长度字段长度 | |
| 44 | + */ | |
| 45 | + private final int headerLength; | |
| 46 | + | |
| 47 | + public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) { | |
| 48 | + Assert.notNull(config.getLengthFieldOffset(), "lengthFieldOffset 不能为空"); | |
| 49 | + Assert.notNull(config.getLengthFieldLength(), "lengthFieldLength 不能为空"); | |
| 50 | + Assert.notNull(config.getLengthAdjustment(), "lengthAdjustment 不能为空"); | |
| 51 | + Assert.notNull(config.getInitialBytesToStrip(), "initialBytesToStrip 不能为空"); | |
| 52 | + this.lengthFieldOffset = config.getLengthFieldOffset(); | |
| 53 | + this.lengthFieldLength = config.getLengthFieldLength(); | |
| 54 | + this.lengthAdjustment = config.getLengthAdjustment(); | |
| 55 | + this.initialBytesToStrip = config.getInitialBytesToStrip(); | |
| 56 | + this.headerLength = lengthFieldOffset + lengthFieldLength; | |
| 57 | + } | |
| 58 | + | |
| 59 | + @Override | |
| 60 | + public IotTcpCodecTypeEnum getType() { | |
| 61 | + return IotTcpCodecTypeEnum.LENGTH_FIELD; | |
| 62 | + } | |
| 63 | + | |
| 64 | + @Override | |
| 65 | + public RecordParser createDecodeParser(Handler<Buffer> handler) { | |
| 66 | + // 创建状态机:先读取头部,再读取消息体 | |
| 67 | + RecordParser parser = RecordParser.newFixed(headerLength); | |
| 68 | + parser.maxRecordSize(MAX_FRAME_LENGTH); // 设置最大记录大小,防止 DoS 攻击 | |
| 69 | + final AtomicReference<Integer> bodyLength = new AtomicReference<>(null); // 消息体长度,null 表示读取头部阶段 | |
| 70 | + final AtomicReference<Buffer> headerBuffer = new AtomicReference<>(null); // 头部消息 | |
| 71 | + | |
| 72 | + // 处理读取到的数据 | |
| 73 | + parser.handler(buffer -> { | |
| 74 | + if (bodyLength.get() == null) { | |
| 75 | + // 阶段 1: 读取头部,解析长度字段 | |
| 76 | + headerBuffer.set(buffer.copy()); | |
| 77 | + int length = readLength(buffer, lengthFieldOffset, lengthFieldLength); | |
| 78 | + int frameBodyLength = length + lengthAdjustment; | |
| 79 | + // 检查帧长度是否合法 | |
| 80 | + if (frameBodyLength < 0) { | |
| 81 | + throw new IllegalStateException(String.format( | |
| 82 | + "[createDecodeParser][帧长度异常,length: %d, frameBodyLength: %d]", | |
| 83 | + length, frameBodyLength)); | |
| 84 | + } | |
| 85 | + // 消息体为空,抛出异常 | |
| 86 | + if (frameBodyLength == 0) { | |
| 87 | + throw new IllegalStateException("[createDecodeParser][消息体不能为空]"); | |
| 88 | + } | |
| 89 | + | |
| 90 | + // 【重要】切换到读取消息体模式 | |
| 91 | + bodyLength.set(frameBodyLength); | |
| 92 | + parser.fixedSizeMode(frameBodyLength); | |
| 93 | + } else { | |
| 94 | + // 阶段 2: 读取消息体,组装完整帧 | |
| 95 | + Buffer frame = processFrame(headerBuffer.get(), buffer); | |
| 96 | + // 重置状态,准备读取下一帧 | |
| 97 | + bodyLength.set(null); | |
| 98 | + headerBuffer.set(null); | |
| 99 | + parser.fixedSizeMode(headerLength); | |
| 100 | + | |
| 101 | + // 【重要】处理完整消息 | |
| 102 | + handler.handle(frame); | |
| 103 | + } | |
| 104 | + }); | |
| 105 | + parser.exceptionHandler(ex -> { | |
| 106 | + throw new RuntimeException("[createDecodeParser][解析异常]", ex); | |
| 107 | + }); | |
| 108 | + return parser; | |
| 109 | + } | |
| 110 | + | |
| 111 | + @Override | |
| 112 | + public Buffer encode(byte[] data) { | |
| 113 | + Buffer buffer = Buffer.buffer(); | |
| 114 | + // 计算要写入的长度值 | |
| 115 | + int lengthValue = data.length - lengthAdjustment; | |
| 116 | + // 写入偏移量前的填充字节(如果有) | |
| 117 | + for (int i = 0; i < lengthFieldOffset; i++) { | |
| 118 | + buffer.appendByte((byte) 0); | |
| 119 | + } | |
| 120 | + // 写入长度字段 | |
| 121 | + writeLength(buffer, lengthValue, lengthFieldLength); | |
| 122 | + // 写入消息体 | |
| 123 | + buffer.appendBytes(data); | |
| 124 | + return buffer; | |
| 125 | + } | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * 从 Buffer 中读取长度字段 | |
| 129 | + */ | |
| 130 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 131 | + private int readLength(Buffer buffer, int offset, int length) { | |
| 132 | + switch (length) { | |
| 133 | + case 1: | |
| 134 | + return buffer.getUnsignedByte(offset); | |
| 135 | + case 2: | |
| 136 | + return buffer.getUnsignedShort(offset); | |
| 137 | + case 4: | |
| 138 | + return buffer.getInt(offset); | |
| 139 | + default: | |
| 140 | + throw new IllegalArgumentException("不支持的长度字段长度: " + length); | |
| 141 | + } | |
| 142 | + } | |
| 143 | + | |
| 144 | + /** | |
| 145 | + * 向 Buffer 中写入长度字段 | |
| 146 | + */ | |
| 147 | + private void writeLength(Buffer buffer, int length, int fieldLength) { | |
| 148 | + switch (fieldLength) { | |
| 149 | + case 1: | |
| 150 | + buffer.appendByte((byte) length); | |
| 151 | + break; | |
| 152 | + case 2: | |
| 153 | + buffer.appendShort((short) length); | |
| 154 | + break; | |
| 155 | + case 4: | |
| 156 | + buffer.appendInt(length); | |
| 157 | + break; | |
| 158 | + default: | |
| 159 | + throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength); | |
| 160 | + } | |
| 161 | + } | |
| 162 | + | |
| 163 | + /** | |
| 164 | + * 处理帧数据(根据 initialBytesToStrip 跳过指定字节) | |
| 165 | + */ | |
| 166 | + private Buffer processFrame(Buffer header, Buffer body) { | |
| 167 | + Buffer fullFrame = Buffer.buffer(); | |
| 168 | + if (header != null) { | |
| 169 | + fullFrame.appendBuffer(header); | |
| 170 | + } | |
| 171 | + if (body != null) { | |
| 172 | + fullFrame.appendBuffer(body); | |
| 173 | + } | |
| 174 | + // 根据 initialBytesToStrip 跳过指定字节 | |
| 175 | + if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) { | |
| 176 | + return fullFrame.slice(initialBytesToStrip, fullFrame.length()); | |
| 177 | + } | |
| 178 | + return fullFrame; | |
| 179 | + } | |
| 180 | + | |
| 181 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | |
| 4 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | |
| 7 | +import io.vertx.core.buffer.Buffer; | |
| 6 | 8 | import lombok.RequiredArgsConstructor; |
| 7 | 9 | import lombok.extern.slf4j.Slf4j; |
| 8 | 10 | |
| ... | ... | @@ -15,36 +17,44 @@ import lombok.extern.slf4j.Slf4j; |
| 15 | 17 | @RequiredArgsConstructor |
| 16 | 18 | public class IotTcpDownstreamHandler { |
| 17 | 19 | |
| 18 | - private final IotDeviceMessageService deviceMessageService; | |
| 19 | - | |
| 20 | 20 | private final IotTcpConnectionManager connectionManager; |
| 21 | 21 | |
| 22 | 22 | /** |
| 23 | + * TCP 帧编解码器(处理粘包/拆包) | |
| 24 | + */ | |
| 25 | + private final IotTcpFrameCodec codec; | |
| 26 | + /** | |
| 27 | + * 消息序列化器(处理业务消息序列化/反序列化) | |
| 28 | + */ | |
| 29 | + private final IotMessageSerializer serializer; | |
| 30 | + | |
| 31 | + /** | |
| 23 | 32 | * 处理下行消息 |
| 24 | 33 | */ |
| 25 | 34 | public void handle(IotDeviceMessage message) { |
| 26 | 35 | try { |
| 27 | 36 | log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 28 | 37 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 29 | - | |
| 30 | - // 1. 获取连接信息(包含 codecType) | |
| 38 | + // 1. 检查设备连接 | |
| 31 | 39 | IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( |
| 32 | 40 | message.getDeviceId()); |
| 33 | 41 | if (connectionInfo == null) { |
| 34 | - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); | |
| 42 | + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", | |
| 43 | + message.getDeviceId(), message.getMethod(), message.getId()); | |
| 35 | 44 | return; |
| 36 | 45 | } |
| 37 | 46 | |
| 38 | - // 2. 使用连接时的 codecType 编码消息,并发送到设备 | |
| 39 | - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); | |
| 40 | - boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); | |
| 41 | - if (success) { | |
| 42 | - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | |
| 43 | - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); | |
| 44 | - } else { | |
| 45 | - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", | |
| 46 | - message.getDeviceId(), message.getMethod(), message.getId()); | |
| 47 | + // 2. 序列化 + 帧编码 | |
| 48 | + byte[] payload = serializer.serialize(message); | |
| 49 | + Buffer frameData = codec.encode(payload); | |
| 50 | + | |
| 51 | + // 3. 发送到设备 | |
| 52 | + boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); | |
| 53 | + if (!success) { | |
| 54 | + throw new RuntimeException("下行消息发送失败"); | |
| 47 | 55 | } |
| 56 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | |
| 57 | + message.getDeviceId(), message.getMethod(), message.getId(), frameData.length()); | |
| 48 | 58 | } catch (Exception e) { |
| 49 | 59 | log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", |
| 50 | 60 | message.getDeviceId(), message.getMethod(), message, e); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + private final IotTcpDownstreamHandler downstreamHandler; | |
| 18 | + | |
| 19 | + public IotTcpDownstreamSubscriber(IotProtocol protocol, | |
| 20 | + IotTcpDownstreamHandler downstreamHandler, | |
| 21 | + IotMessageBus messageBus) { | |
| 22 | + super(protocol, messageBus); | |
| 23 | + this.downstreamHandler = downstreamHandler; | |
| 24 | + } | |
| 25 | + | |
| 26 | + @Override | |
| 27 | + protected void handleMessage(IotDeviceMessage message) { | |
| 28 | + downstreamHandler.handle(message); | |
| 29 | + } | |
| 30 | + | |
| 31 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.BooleanUtil; | |
| 4 | +import cn.hutool.extra.spring.SpringUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 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.biz.dto.IotDeviceAuthReqDTO; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 12 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 13 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 14 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 15 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 16 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 19 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | |
| 20 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 21 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 22 | +import io.vertx.core.Handler; | |
| 23 | +import io.vertx.core.buffer.Buffer; | |
| 24 | +import io.vertx.core.net.NetSocket; | |
| 25 | +import io.vertx.core.parsetools.RecordParser; | |
| 26 | +import lombok.extern.slf4j.Slf4j; | |
| 27 | +import cn.hutool.core.lang.Assert; | |
| 28 | + | |
| 29 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | |
| 30 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 31 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | |
| 32 | + | |
| 33 | +/** | |
| 34 | + * TCP 上行消息处理器 | |
| 35 | + * | |
| 36 | + * @author 芋道源码 | |
| 37 | + */ | |
| 38 | +@Slf4j | |
| 39 | +public class IotTcpUpstreamHandler implements Handler<NetSocket> { | |
| 40 | + | |
| 41 | + private static final String AUTH_METHOD = "auth"; | |
| 42 | + | |
| 43 | + private final String serverId; | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * TCP 帧编解码器(处理粘包/拆包) | |
| 47 | + */ | |
| 48 | + private final IotTcpFrameCodec codec; | |
| 49 | + /** | |
| 50 | + * 消息序列化器(处理业务消息序列化/反序列化) | |
| 51 | + */ | |
| 52 | + private final IotMessageSerializer serializer; | |
| 53 | + /** | |
| 54 | + * TCP 连接管理器 | |
| 55 | + */ | |
| 56 | + private final IotTcpConnectionManager connectionManager; | |
| 57 | + | |
| 58 | + private final IotDeviceMessageService deviceMessageService; | |
| 59 | + private final IotDeviceService deviceService; | |
| 60 | + private final IotDeviceCommonApi deviceApi; | |
| 61 | + | |
| 62 | + public IotTcpUpstreamHandler(String serverId, | |
| 63 | + IotTcpFrameCodec codec, | |
| 64 | + IotMessageSerializer serializer, | |
| 65 | + IotTcpConnectionManager connectionManager) { | |
| 66 | + Assert.notNull(codec, "TCP FrameCodec 必须配置"); | |
| 67 | + Assert.notNull(serializer, "消息序列化器必须配置"); | |
| 68 | + Assert.notNull(connectionManager, "连接管理器不能为空"); | |
| 69 | + this.serverId = serverId; | |
| 70 | + this.codec = codec; | |
| 71 | + this.serializer = serializer; | |
| 72 | + this.connectionManager = connectionManager; | |
| 73 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 74 | + this.deviceService = SpringUtil.getBean(IotDeviceService.class); | |
| 75 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 76 | + } | |
| 77 | + | |
| 78 | + @Override | |
| 79 | + @SuppressWarnings("DuplicatedCode") | |
| 80 | + public void handle(NetSocket socket) { | |
| 81 | + String remoteAddress = String.valueOf(socket.remoteAddress()); | |
| 82 | + log.debug("[handle][设备连接,地址: {}]", remoteAddress); | |
| 83 | + | |
| 84 | + // 1. 设置异常和关闭处理器 | |
| 85 | + socket.exceptionHandler(ex -> { | |
| 86 | + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); | |
| 87 | + socket.close(); | |
| 88 | + }); | |
| 89 | + socket.closeHandler(v -> { | |
| 90 | + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); | |
| 91 | + cleanupConnection(socket); | |
| 92 | + }); | |
| 93 | + | |
| 94 | + // 2.1 设置消息处理器 | |
| 95 | + Handler<Buffer> messageHandler = buffer -> { | |
| 96 | + try { | |
| 97 | + processMessage(buffer, socket); | |
| 98 | + } catch (Exception e) { | |
| 99 | + log.error("[handle][消息处理失败,地址: {}]", remoteAddress, e); | |
| 100 | + socket.close(); | |
| 101 | + } | |
| 102 | + }; | |
| 103 | + // 2.2 使用拆包器处理粘包/拆包 | |
| 104 | + RecordParser parser = codec.createDecodeParser(messageHandler); | |
| 105 | + socket.handler(parser); | |
| 106 | + log.debug("[handle][启用 {} 拆包器,地址: {}]", codec.getType(), remoteAddress); | |
| 107 | + } | |
| 108 | + | |
| 109 | + /** | |
| 110 | + * 处理消息 | |
| 111 | + * | |
| 112 | + * @param buffer 消息 | |
| 113 | + * @param socket 网络连接 | |
| 114 | + */ | |
| 115 | + private void processMessage(Buffer buffer, NetSocket socket) { | |
| 116 | + IotDeviceMessage message = null; | |
| 117 | + try { | |
| 118 | + // 1. 反序列化消息 | |
| 119 | + message = serializer.deserialize(buffer.getBytes()); | |
| 120 | + if (message == null) { | |
| 121 | + sendErrorResponse(socket, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); | |
| 122 | + return; | |
| 123 | + } | |
| 124 | + | |
| 125 | + // 2. 根据消息类型路由处理 | |
| 126 | + if (AUTH_METHOD.equals(message.getMethod())) { | |
| 127 | + // 认证请求 | |
| 128 | + handleAuthenticationRequest(message, socket); | |
| 129 | + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | |
| 130 | + // 设备动态注册请求 | |
| 131 | + handleRegisterRequest(message, socket); | |
| 132 | + } else { | |
| 133 | + // 业务消息 | |
| 134 | + handleBusinessRequest(message, socket); | |
| 135 | + } | |
| 136 | + } catch (ServiceException e) { | |
| 137 | + // 业务异常,返回对应的错误码和错误信息 | |
| 138 | + log.warn("[processMessage][业务异常,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); | |
| 139 | + String requestId = message != null ? message.getRequestId() : null; | |
| 140 | + String method = message != null ? message.getMethod() : null; | |
| 141 | + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); | |
| 142 | + } catch (IllegalArgumentException e) { | |
| 143 | + // 参数校验失败,返回 400 | |
| 144 | + log.warn("[processMessage][参数校验失败,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); | |
| 145 | + String requestId = message != null ? message.getRequestId() : null; | |
| 146 | + String method = message != null ? message.getMethod() : null; | |
| 147 | + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); | |
| 148 | + } catch (Exception e) { | |
| 149 | + // 其他异常,返回 500,并重新抛出让上层关闭连接 | |
| 150 | + log.error("[processMessage][处理消息失败,地址: {}]", socket.remoteAddress(), e); | |
| 151 | + String requestId = message != null ? message.getRequestId() : null; | |
| 152 | + String method = message != null ? message.getMethod() : null; | |
| 153 | + sendErrorResponse(socket, requestId, method, | |
| 154 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 155 | + throw e; | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 159 | + /** | |
| 160 | + * 处理认证请求 | |
| 161 | + * | |
| 162 | + * @param message 消息信息 | |
| 163 | + * @param socket 网络连接 | |
| 164 | + */ | |
| 165 | + @SuppressWarnings("DuplicatedCode") | |
| 166 | + private void handleAuthenticationRequest(IotDeviceMessage message, NetSocket socket) { | |
| 167 | + // 1. 解析认证参数 | |
| 168 | + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | |
| 169 | + Assert.notNull(authParams, "认证参数不能为空"); | |
| 170 | + Assert.notBlank(authParams.getUsername(), "username 不能为空"); | |
| 171 | + Assert.notBlank(authParams.getPassword(), "password 不能为空"); | |
| 172 | + | |
| 173 | + // 2.1 执行认证 | |
| 174 | + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 175 | + authResult.checkError(); | |
| 176 | + if (BooleanUtil.isFalse(authResult.getData())) { | |
| 177 | + throw exception(DEVICE_AUTH_FAIL); | |
| 178 | + } | |
| 179 | + // 2.2 解析设备信息 | |
| 180 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); | |
| 181 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | |
| 182 | + // 2.3 获取设备信息 | |
| 183 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 184 | + Assert.notNull(device, "设备不存在"); | |
| 185 | + | |
| 186 | + // 3.1 注册连接 | |
| 187 | + registerConnection(socket, device); | |
| 188 | + // 3.2 发送上线消息 | |
| 189 | + sendOnlineMessage(device); | |
| 190 | + // 3.3 发送成功响应 | |
| 191 | + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); | |
| 192 | + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); | |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * 处理设备动态注册请求(一型一密,不需要认证) | |
| 197 | + * | |
| 198 | + * @param message 消息信息 | |
| 199 | + * @param socket 网络连接 | |
| 200 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 201 | + */ | |
| 202 | + @SuppressWarnings("DuplicatedCode") | |
| 203 | + private void handleRegisterRequest(IotDeviceMessage message, NetSocket socket) { | |
| 204 | + // 1. 解析注册参数 | |
| 205 | + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | |
| 206 | + Assert.notNull(params, "注册参数不能为空"); | |
| 207 | + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); | |
| 208 | + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); | |
| 209 | + Assert.notBlank(params.getSign(), "sign 不能为空"); | |
| 210 | + | |
| 211 | + // 2. 调用动态注册 | |
| 212 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 213 | + result.checkError(); | |
| 214 | + | |
| 215 | + // 3. 发送成功响应 | |
| 216 | + sendSuccessResponse(socket, message.getRequestId(), | |
| 217 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); | |
| 218 | + log.info("[handleRegisterRequest][注册成功,地址: {},设备名: {}]", | |
| 219 | + socket.remoteAddress(), params.getDeviceName()); | |
| 220 | + } | |
| 221 | + | |
| 222 | + /** | |
| 223 | + * 处理业务请求 | |
| 224 | + * | |
| 225 | + * @param message 消息信息 | |
| 226 | + * @param socket 网络连接 | |
| 227 | + */ | |
| 228 | + private void handleBusinessRequest(IotDeviceMessage message, NetSocket socket) { | |
| 229 | + // 1. 获取认证信息并处理业务消息 | |
| 230 | + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); | |
| 231 | + if (connectionInfo == null) { | |
| 232 | + log.error("[handleBusinessRequest][无法获取连接信息,地址: {}]", socket.remoteAddress()); | |
| 233 | + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), | |
| 234 | + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); | |
| 235 | + return; | |
| 236 | + } | |
| 237 | + | |
| 238 | + // 2. 发送消息到消息总线 | |
| 239 | + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), | |
| 240 | + connectionInfo.getDeviceName(), serverId); | |
| 241 | + log.info("[handleBusinessRequest][发送消息到消息总线,地址: {},消息: {}]", socket.remoteAddress(), message); | |
| 242 | + } | |
| 243 | + | |
| 244 | + /** | |
| 245 | + * 注册连接信息 | |
| 246 | + * | |
| 247 | + * @param socket 网络连接 | |
| 248 | + * @param device 设备 | |
| 249 | + */ | |
| 250 | + private void registerConnection(NetSocket socket, IotDeviceRespDTO device) { | |
| 251 | + IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() | |
| 252 | + .setDeviceId(device.getId()) | |
| 253 | + .setProductKey(device.getProductKey()) | |
| 254 | + .setDeviceName(device.getDeviceName()); | |
| 255 | + connectionManager.registerConnection(socket, device.getId(), connectionInfo); | |
| 256 | + } | |
| 257 | + | |
| 258 | + /** | |
| 259 | + * 发送设备上线消息 | |
| 260 | + * | |
| 261 | + * @param device 设备信息 | |
| 262 | + */ | |
| 263 | + private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 264 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 265 | + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 266 | + device.getDeviceName(), serverId); | |
| 267 | + } | |
| 268 | + | |
| 269 | + /** | |
| 270 | + * 清理连接 | |
| 271 | + * | |
| 272 | + * @param socket 网络连接 | |
| 273 | + */ | |
| 274 | + private void cleanupConnection(NetSocket socket) { | |
| 275 | + // 1. 发送离线消息 | |
| 276 | + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); | |
| 277 | + if (connectionInfo != null) { | |
| 278 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 279 | + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 280 | + connectionInfo.getDeviceName(), serverId); | |
| 281 | + } | |
| 282 | + | |
| 283 | + // 2. 注销连接 | |
| 284 | + connectionManager.unregisterConnection(socket); | |
| 285 | + } | |
| 286 | + | |
| 287 | + // ===================== 发送响应消息 ===================== | |
| 288 | + | |
| 289 | + /** | |
| 290 | + * 发送成功响应 | |
| 291 | + * | |
| 292 | + * @param socket 网络连接 | |
| 293 | + * @param requestId 请求 ID | |
| 294 | + * @param method 方法名 | |
| 295 | + * @param data 响应数据 | |
| 296 | + */ | |
| 297 | + private void sendSuccessResponse(NetSocket socket, String requestId, String method, Object data) { | |
| 298 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); | |
| 299 | + writeResponse(socket, responseMessage); | |
| 300 | + } | |
| 301 | + | |
| 302 | + /** | |
| 303 | + * 发送错误响应 | |
| 304 | + * | |
| 305 | + * @param socket 网络连接 | |
| 306 | + * @param requestId 请求 ID | |
| 307 | + * @param method 方法名 | |
| 308 | + * @param code 错误码 | |
| 309 | + * @param msg 错误消息 | |
| 310 | + */ | |
| 311 | + private void sendErrorResponse(NetSocket socket, String requestId, String method, Integer code, String msg) { | |
| 312 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); | |
| 313 | + writeResponse(socket, responseMessage); | |
| 314 | + } | |
| 315 | + | |
| 316 | + /** | |
| 317 | + * 写入响应到 Socket | |
| 318 | + * | |
| 319 | + * @param socket 网络连接 | |
| 320 | + * @param responseMessage 响应消息 | |
| 321 | + */ | |
| 322 | + private void writeResponse(NetSocket socket, IotDeviceMessage responseMessage) { | |
| 323 | + byte[] serializedData = serializer.serialize(responseMessage); | |
| 324 | + Buffer frameData = codec.encode(serializedData); | |
| 325 | + socket.write(frameData); | |
| 326 | + } | |
| 327 | + | |
| 328 | +} | |
| 0 | 329 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
| ... | ... | @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer; |
| 4 | 4 | import io.vertx.core.net.NetSocket; |
| 5 | 5 | import lombok.Data; |
| 6 | 6 | import lombok.extern.slf4j.Slf4j; |
| 7 | -import org.springframework.stereotype.Component; | |
| 8 | 7 | |
| 9 | 8 | import java.util.Map; |
| 10 | 9 | import java.util.concurrent.ConcurrentHashMap; |
| ... | ... | @@ -20,10 +19,14 @@ import java.util.concurrent.ConcurrentHashMap; |
| 20 | 19 | * @author 芋道源码 |
| 21 | 20 | */ |
| 22 | 21 | @Slf4j |
| 23 | -@Component | |
| 24 | 22 | public class IotTcpConnectionManager { |
| 25 | 23 | |
| 26 | 24 | /** |
| 25 | + * 最大连接数 | |
| 26 | + */ | |
| 27 | + private final int maxConnections; | |
| 28 | + | |
| 29 | + /** | |
| 27 | 30 | * 连接信息映射:NetSocket -> 连接信息 |
| 28 | 31 | */ |
| 29 | 32 | private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>(); |
| ... | ... | @@ -33,6 +36,10 @@ public class IotTcpConnectionManager { |
| 33 | 36 | */ |
| 34 | 37 | private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); |
| 35 | 38 | |
| 39 | + public IotTcpConnectionManager(int maxConnections) { | |
| 40 | + this.maxConnections = maxConnections; | |
| 41 | + } | |
| 42 | + | |
| 36 | 43 | /** |
| 37 | 44 | * 注册设备连接(包含认证信息) |
| 38 | 45 | * |
| ... | ... | @@ -40,15 +47,19 @@ public class IotTcpConnectionManager { |
| 40 | 47 | * @param deviceId 设备 ID |
| 41 | 48 | * @param connectionInfo 连接信息 |
| 42 | 49 | */ |
| 43 | - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { | |
| 50 | + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { | |
| 51 | + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) | |
| 52 | + if (connectionMap.size() >= maxConnections) { | |
| 53 | + throw new IllegalStateException("连接数已达上限: " + maxConnections); | |
| 54 | + } | |
| 44 | 55 | // 如果设备已有其他连接,先清理旧连接 |
| 45 | 56 | NetSocket oldSocket = deviceSocketMap.get(deviceId); |
| 46 | 57 | if (oldSocket != null && oldSocket != socket) { |
| 47 | 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 48 | 59 | deviceId, oldSocket.remoteAddress()); |
| 49 | - oldSocket.close(); | |
| 50 | - // 清理旧连接的映射 | |
| 60 | + // 先清理映射,再关闭连接 | |
| 51 | 61 | connectionMap.remove(oldSocket); |
| 62 | + oldSocket.close(); | |
| 52 | 63 | } |
| 53 | 64 | |
| 54 | 65 | // 注册新连接 |
| ... | ... | @@ -69,26 +80,12 @@ public class IotTcpConnectionManager { |
| 69 | 80 | return; |
| 70 | 81 | } |
| 71 | 82 | Long deviceId = connectionInfo.getDeviceId(); |
| 72 | - deviceSocketMap.remove(deviceId); | |
| 83 | + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 | |
| 84 | + deviceSocketMap.remove(deviceId, socket); | |
| 73 | 85 | log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); |
| 74 | 86 | } |
| 75 | 87 | |
| 76 | 88 | /** |
| 77 | - * 检查连接是否已认证 | |
| 78 | - */ | |
| 79 | - public boolean isAuthenticated(NetSocket socket) { | |
| 80 | - ConnectionInfo info = connectionMap.get(socket); | |
| 81 | - return info != null; | |
| 82 | - } | |
| 83 | - | |
| 84 | - /** | |
| 85 | - * 检查连接是否未认证 | |
| 86 | - */ | |
| 87 | - public boolean isNotAuthenticated(NetSocket socket) { | |
| 88 | - return !isAuthenticated(socket); | |
| 89 | - } | |
| 90 | - | |
| 91 | - /** | |
| 92 | 89 | * 获取连接信息 |
| 93 | 90 | */ |
| 94 | 91 | public ConnectionInfo getConnectionInfo(NetSocket socket) { |
| ... | ... | @@ -144,15 +141,6 @@ public class IotTcpConnectionManager { |
| 144 | 141 | */ |
| 145 | 142 | private String deviceName; |
| 146 | 143 | |
| 147 | - /** | |
| 148 | - * 客户端 ID | |
| 149 | - */ | |
| 150 | - private String clientId; | |
| 151 | - /** | |
| 152 | - * 消息编解码类型(认证后确定) | |
| 153 | - */ | |
| 154 | - private String codecType; | |
| 155 | - | |
| 156 | 144 | } |
| 157 | 145 | |
| 158 | 146 | } |
| 159 | 147 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.hutool.core.util.BooleanUtil; | |
| 5 | -import cn.hutool.core.util.IdUtil; | |
| 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.biz.dto.IotDeviceRespDTO; | |
| 13 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 14 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 15 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 16 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 17 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 18 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 19 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | |
| 20 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | |
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; | |
| 23 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 24 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 25 | -import io.vertx.core.Handler; | |
| 26 | -import io.vertx.core.buffer.Buffer; | |
| 27 | -import io.vertx.core.net.NetSocket; | |
| 28 | -import lombok.extern.slf4j.Slf4j; | |
| 29 | - | |
| 30 | -import java.util.Map; | |
| 31 | - | |
| 32 | -/** | |
| 33 | - * TCP 上行消息处理器 | |
| 34 | - * | |
| 35 | - * @author 芋道源码 | |
| 36 | - */ | |
| 37 | -@Slf4j | |
| 38 | -public class IotTcpUpstreamHandler implements Handler<NetSocket> { | |
| 39 | - | |
| 40 | - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; | |
| 41 | - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; | |
| 42 | - | |
| 43 | - private static final String AUTH_METHOD = "auth"; | |
| 44 | - | |
| 45 | - private final IotDeviceMessageService deviceMessageService; | |
| 46 | - | |
| 47 | - private final IotDeviceService deviceService; | |
| 48 | - | |
| 49 | - private final IotTcpConnectionManager connectionManager; | |
| 50 | - | |
| 51 | - private final IotDeviceCommonApi deviceApi; | |
| 52 | - | |
| 53 | - private final String serverId; | |
| 54 | - | |
| 55 | - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, | |
| 56 | - IotDeviceMessageService deviceMessageService, | |
| 57 | - IotDeviceService deviceService, | |
| 58 | - IotTcpConnectionManager connectionManager) { | |
| 59 | - this.deviceMessageService = deviceMessageService; | |
| 60 | - this.deviceService = deviceService; | |
| 61 | - this.connectionManager = connectionManager; | |
| 62 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 63 | - this.serverId = protocol.getServerId(); | |
| 64 | - } | |
| 65 | - | |
| 66 | - @Override | |
| 67 | - public void handle(NetSocket socket) { | |
| 68 | - String clientId = IdUtil.simpleUUID(); | |
| 69 | - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | |
| 70 | - | |
| 71 | - // 设置异常和关闭处理器 | |
| 72 | - socket.exceptionHandler(ex -> { | |
| 73 | - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | |
| 74 | - cleanupConnection(socket); | |
| 75 | - }); | |
| 76 | - socket.closeHandler(v -> { | |
| 77 | - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); | |
| 78 | - cleanupConnection(socket); | |
| 79 | - }); | |
| 80 | - | |
| 81 | - // 设置消息处理器 | |
| 82 | - socket.handler(buffer -> { | |
| 83 | - // TODO @AI:TODO @芋艿:这里应该有拆粘包的问题; | |
| 84 | - try { | |
| 85 | - processMessage(clientId, buffer, socket); | |
| 86 | - } catch (Exception e) { | |
| 87 | - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | |
| 88 | - clientId, socket.remoteAddress(), e.getMessage()); | |
| 89 | - cleanupConnection(socket); | |
| 90 | - socket.close(); | |
| 91 | - } | |
| 92 | - }); | |
| 93 | - } | |
| 94 | - | |
| 95 | - /** | |
| 96 | - * 处理消息 | |
| 97 | - * | |
| 98 | - * @param clientId 客户端 ID | |
| 99 | - * @param buffer 消息 | |
| 100 | - * @param socket 网络连接 | |
| 101 | - * @throws Exception 消息解码失败时抛出异常 | |
| 102 | - */ | |
| 103 | - private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception { | |
| 104 | - // 1. 基础检查 | |
| 105 | - if (buffer == null || buffer.length() == 0) { | |
| 106 | - return; | |
| 107 | - } | |
| 108 | - | |
| 109 | - // 2. 获取消息格式类型 | |
| 110 | - String codecType = getMessageCodecType(buffer, socket); | |
| 111 | - | |
| 112 | - // 3. 解码消息 | |
| 113 | - IotDeviceMessage message; | |
| 114 | - try { | |
| 115 | - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); | |
| 116 | - if (message == null) { | |
| 117 | - throw new Exception("解码后消息为空"); | |
| 118 | - } | |
| 119 | - } catch (Exception e) { | |
| 120 | - // 消息格式错误时抛出异常,由上层处理连接断开 | |
| 121 | - throw new Exception("消息解码失败: " + e.getMessage(), e); | |
| 122 | - } | |
| 123 | - | |
| 124 | - // 4. 根据消息类型路由处理 | |
| 125 | - try { | |
| 126 | - if (AUTH_METHOD.equals(message.getMethod())) { | |
| 127 | - // 认证请求 | |
| 128 | - handleAuthenticationRequest(clientId, message, codecType, socket); | |
| 129 | - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | |
| 130 | - // 设备动态注册请求 | |
| 131 | - handleRegisterRequest(clientId, message, codecType, socket); | |
| 132 | - } else { | |
| 133 | - // 业务消息 | |
| 134 | - handleBusinessRequest(clientId, message, codecType, socket); | |
| 135 | - } | |
| 136 | - } catch (Exception e) { | |
| 137 | - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", | |
| 138 | - clientId, message.getMethod(), e); | |
| 139 | - // 发送错误响应,避免客户端一直等待 | |
| 140 | - try { | |
| 141 | - sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType); | |
| 142 | - } catch (Exception responseEx) { | |
| 143 | - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); | |
| 144 | - } | |
| 145 | - } | |
| 146 | - } | |
| 147 | - | |
| 148 | - /** | |
| 149 | - * 处理认证请求 | |
| 150 | - * | |
| 151 | - * @param clientId 客户端 ID | |
| 152 | - * @param message 消息信息 | |
| 153 | - * @param codecType 消息编解码类型 | |
| 154 | - * @param socket 网络连接 | |
| 155 | - */ | |
| 156 | - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType, | |
| 157 | - NetSocket socket) { | |
| 158 | - try { | |
| 159 | - // 1.1 解析认证参数 | |
| 160 | - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); | |
| 161 | - if (authParams == null) { | |
| 162 | - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); | |
| 163 | - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType); | |
| 164 | - return; | |
| 165 | - } | |
| 166 | - // 1.2 执行认证 | |
| 167 | - if (!validateDeviceAuth(authParams)) { | |
| 168 | - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", | |
| 169 | - clientId, authParams.getUsername()); | |
| 170 | - sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType); | |
| 171 | - return; | |
| 172 | - } | |
| 173 | - | |
| 174 | - // 2.1 解析设备信息 | |
| 175 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); | |
| 176 | - if (deviceInfo == null) { | |
| 177 | - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); | |
| 178 | - return; | |
| 179 | - } | |
| 180 | - // 2.2 获取设备信息 | |
| 181 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 182 | - deviceInfo.getDeviceName()); | |
| 183 | - if (device == null) { | |
| 184 | - sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType); | |
| 185 | - return; | |
| 186 | - } | |
| 187 | - | |
| 188 | - // 3.1 注册连接 | |
| 189 | - registerConnection(socket, device, clientId, codecType); | |
| 190 | - // 3.2 发送上线消息 | |
| 191 | - sendOnlineMessage(device); | |
| 192 | - // 3.3 发送成功响应 | |
| 193 | - sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType); | |
| 194 | - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", | |
| 195 | - device.getId(), device.getDeviceName()); | |
| 196 | - } catch (Exception e) { | |
| 197 | - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); | |
| 198 | - sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType); | |
| 199 | - } | |
| 200 | - } | |
| 201 | - | |
| 202 | - /** | |
| 203 | - * 处理设备动态注册请求(一型一密,不需要认证) | |
| 204 | - * | |
| 205 | - * @param clientId 客户端 ID | |
| 206 | - * @param message 消息信息 | |
| 207 | - * @param codecType 消息编解码类型 | |
| 208 | - * @param socket 网络连接 | |
| 209 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 210 | - */ | |
| 211 | - private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType, | |
| 212 | - NetSocket socket) { | |
| 213 | - try { | |
| 214 | - // 1. 解析注册参数 | |
| 215 | - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | |
| 216 | - if (params == null) { | |
| 217 | - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); | |
| 218 | - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType); | |
| 219 | - return; | |
| 220 | - } | |
| 221 | - | |
| 222 | - // 2. 调用动态注册 | |
| 223 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 224 | - if (result.isError()) { | |
| 225 | - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | |
| 226 | - sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType); | |
| 227 | - return; | |
| 228 | - } | |
| 229 | - | |
| 230 | - // 3. 发送成功响应(包含 deviceSecret) | |
| 231 | - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType); | |
| 232 | - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", | |
| 233 | - clientId, params.getDeviceName()); | |
| 234 | - } catch (Exception e) { | |
| 235 | - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); | |
| 236 | - sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType); | |
| 237 | - } | |
| 238 | - } | |
| 239 | - | |
| 240 | - /** | |
| 241 | - * 处理业务请求 | |
| 242 | - * | |
| 243 | - * @param clientId 客户端 ID | |
| 244 | - * @param message 消息信息 | |
| 245 | - * @param codecType 消息编解码类型 | |
| 246 | - * @param socket 网络连接 | |
| 247 | - */ | |
| 248 | - private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) { | |
| 249 | - try { | |
| 250 | - // 1. 检查认证状态 | |
| 251 | - if (connectionManager.isNotAuthenticated(socket)) { | |
| 252 | - log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); | |
| 253 | - sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType); | |
| 254 | - return; | |
| 255 | - } | |
| 256 | - | |
| 257 | - // 2. 获取认证信息并处理业务消息 | |
| 258 | - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); | |
| 259 | - | |
| 260 | - // 3. 发送消息到消息总线 | |
| 261 | - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), | |
| 262 | - connectionInfo.getDeviceName(), serverId); | |
| 263 | - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", | |
| 264 | - clientId, message.toString()); | |
| 265 | - } catch (Exception e) { | |
| 266 | - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); | |
| 267 | - } | |
| 268 | - } | |
| 269 | - | |
| 270 | - /** | |
| 271 | - * 获取消息编解码类型 | |
| 272 | - * | |
| 273 | - * @param buffer 消息 | |
| 274 | - * @param socket 网络连接 | |
| 275 | - * @return 消息编解码类型 | |
| 276 | - */ | |
| 277 | - private String getMessageCodecType(Buffer buffer, NetSocket socket) { | |
| 278 | - // 1. 如果已认证,优先使用缓存的编解码类型 | |
| 279 | - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); | |
| 280 | - if (connectionInfo != null | |
| 281 | - && StrUtil.isNotBlank(connectionInfo.getCodecType())) { | |
| 282 | - return connectionInfo.getCodecType(); | |
| 283 | - } | |
| 284 | - | |
| 285 | - // 2. 未认证时检测消息格式类型 | |
| 286 | - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY | |
| 287 | - : CODEC_TYPE_JSON; | |
| 288 | - } | |
| 289 | - | |
| 290 | - /** | |
| 291 | - * 注册连接信息 | |
| 292 | - * | |
| 293 | - * @param socket 网络连接 | |
| 294 | - * @param device 设备 | |
| 295 | - * @param clientId 客户端 ID | |
| 296 | - * @param codecType 消息编解码类型 | |
| 297 | - */ | |
| 298 | - private void registerConnection(NetSocket socket, IotDeviceRespDTO device, | |
| 299 | - String clientId, String codecType) { | |
| 300 | - IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() | |
| 301 | - .setDeviceId(device.getId()) | |
| 302 | - .setProductKey(device.getProductKey()) | |
| 303 | - .setDeviceName(device.getDeviceName()) | |
| 304 | - .setClientId(clientId) | |
| 305 | - .setCodecType(codecType); | |
| 306 | - // 注册连接 | |
| 307 | - connectionManager.registerConnection(socket, device.getId(), connectionInfo); | |
| 308 | - } | |
| 309 | - | |
| 310 | - /** | |
| 311 | - * 发送设备上线消息 | |
| 312 | - * | |
| 313 | - * @param device 设备信息 | |
| 314 | - */ | |
| 315 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 316 | - try { | |
| 317 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 318 | - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 319 | - device.getDeviceName(), serverId); | |
| 320 | - } catch (Exception e) { | |
| 321 | - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); | |
| 322 | - } | |
| 323 | - } | |
| 324 | - | |
| 325 | - /** | |
| 326 | - * 清理连接 | |
| 327 | - * | |
| 328 | - * @param socket 网络连接 | |
| 329 | - */ | |
| 330 | - private void cleanupConnection(NetSocket socket) { | |
| 331 | - try { | |
| 332 | - // 1. 发送离线消息(如果已认证) | |
| 333 | - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); | |
| 334 | - if (connectionInfo != null) { | |
| 335 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 336 | - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 337 | - connectionInfo.getDeviceName(), serverId); | |
| 338 | - } | |
| 339 | - | |
| 340 | - // 2. 注销连接 | |
| 341 | - connectionManager.unregisterConnection(socket); | |
| 342 | - } catch (Exception e) { | |
| 343 | - log.error("[cleanupConnection][清理连接失败]", e); | |
| 344 | - } | |
| 345 | - } | |
| 346 | - | |
| 347 | - /** | |
| 348 | - * 发送响应消息 | |
| 349 | - * | |
| 350 | - * @param socket 网络连接 | |
| 351 | - * @param success 是否成功 | |
| 352 | - * @param message 消息 | |
| 353 | - * @param requestId 请求 ID | |
| 354 | - * @param codecType 消息编解码类型 | |
| 355 | - */ | |
| 356 | - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { | |
| 357 | - try { | |
| 358 | - Object responseData = MapUtil.builder() | |
| 359 | - .put("success", success) | |
| 360 | - .put("message", message) | |
| 361 | - .build(); | |
| 362 | - | |
| 363 | - int code = success ? 0 : 401; | |
| 364 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, | |
| 365 | - code, message); | |
| 366 | - | |
| 367 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 368 | - socket.write(Buffer.buffer(encodedData)); | |
| 369 | - | |
| 370 | - } catch (Exception e) { | |
| 371 | - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); | |
| 372 | - } | |
| 373 | - } | |
| 374 | - | |
| 375 | - /** | |
| 376 | - * 验证设备认证信息 | |
| 377 | - * | |
| 378 | - * @param authParams 认证参数 | |
| 379 | - * @return 是否认证成功 | |
| 380 | - */ | |
| 381 | - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { | |
| 382 | - try { | |
| 383 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 384 | - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) | |
| 385 | - .setPassword(authParams.getPassword())); | |
| 386 | - result.checkError(); | |
| 387 | - return BooleanUtil.isTrue(result.getData()); | |
| 388 | - } catch (Exception e) { | |
| 389 | - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); | |
| 390 | - return false; | |
| 391 | - } | |
| 392 | - } | |
| 393 | - | |
| 394 | - /** | |
| 395 | - * 发送错误响应 | |
| 396 | - * | |
| 397 | - * @param socket 网络连接 | |
| 398 | - * @param requestId 请求 ID | |
| 399 | - * @param errorMessage 错误消息 | |
| 400 | - * @param codecType 消息编解码类型 | |
| 401 | - */ | |
| 402 | - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) { | |
| 403 | - sendResponse(socket, false, errorMessage, requestId, codecType); | |
| 404 | - } | |
| 405 | - | |
| 406 | - /** | |
| 407 | - * 发送成功响应 | |
| 408 | - * | |
| 409 | - * @param socket 网络连接 | |
| 410 | - * @param requestId 请求 ID | |
| 411 | - * @param message 消息 | |
| 412 | - * @param codecType 消息编解码类型 | |
| 413 | - */ | |
| 414 | - @SuppressWarnings("SameParameterValue") | |
| 415 | - private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) { | |
| 416 | - sendResponse(socket, true, message, requestId, codecType); | |
| 417 | - } | |
| 418 | - | |
| 419 | - /** | |
| 420 | - * 解析认证参数 | |
| 421 | - * | |
| 422 | - * @param params 参数对象(通常为 Map 类型) | |
| 423 | - * @return 认证参数 DTO,解析失败时返回 null | |
| 424 | - */ | |
| 425 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 426 | - private IotDeviceAuthReqDTO parseAuthParams(Object params) { | |
| 427 | - if (params == null) { | |
| 428 | - return null; | |
| 429 | - } | |
| 430 | - try { | |
| 431 | - // 参数默认为 Map 类型,直接转换 | |
| 432 | - if (params instanceof Map) { | |
| 433 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 434 | - return new IotDeviceAuthReqDTO() | |
| 435 | - .setClientId(MapUtil.getStr(paramMap, "clientId")) | |
| 436 | - .setUsername(MapUtil.getStr(paramMap, "username")) | |
| 437 | - .setPassword(MapUtil.getStr(paramMap, "password")); | |
| 438 | - } | |
| 439 | - // 如果已经是目标类型,直接返回 | |
| 440 | - if (params instanceof IotDeviceAuthReqDTO) { | |
| 441 | - return (IotDeviceAuthReqDTO) params; | |
| 442 | - } | |
| 443 | - | |
| 444 | - // 其他情况尝试 JSON 转换 | |
| 445 | - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); | |
| 446 | - } catch (Exception e) { | |
| 447 | - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); | |
| 448 | - return null; | |
| 449 | - } | |
| 450 | - } | |
| 451 | - | |
| 452 | - /** | |
| 453 | - * 解析注册参数 | |
| 454 | - * | |
| 455 | - * @param params 参数对象(通常为 Map 类型) | |
| 456 | - * @return 注册参数 DTO,解析失败时返回 null | |
| 457 | - */ | |
| 458 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 459 | - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | |
| 460 | - if (params == null) { | |
| 461 | - return null; | |
| 462 | - } | |
| 463 | - try { | |
| 464 | - // 参数默认为 Map 类型,直接转换 | |
| 465 | - if (params instanceof Map) { | |
| 466 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 467 | - return new IotDeviceRegisterReqDTO() | |
| 468 | - .setProductKey(MapUtil.getStr(paramMap, "productKey")) | |
| 469 | - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | |
| 470 | - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | |
| 471 | - } | |
| 472 | - // 如果已经是目标类型,直接返回 | |
| 473 | - if (params instanceof IotDeviceRegisterReqDTO) { | |
| 474 | - return (IotDeviceRegisterReqDTO) params; | |
| 475 | - } | |
| 476 | - | |
| 477 | - // 其他情况尝试 JSON 转换 | |
| 478 | - String jsonStr = JsonUtils.toJsonString(params); | |
| 479 | - return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class); | |
| 480 | - } catch (Exception e) { | |
| 481 | - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | |
| 482 | - return null; | |
| 483 | - } | |
| 484 | - } | |
| 485 | - | |
| 486 | - /** | |
| 487 | - * 发送注册成功响应(包含 deviceSecret) | |
| 488 | - * | |
| 489 | - * @param socket 网络连接 | |
| 490 | - * @param requestId 请求 ID | |
| 491 | - * @param registerResp 注册响应 | |
| 492 | - * @param codecType 消息编解码类型 | |
| 493 | - */ | |
| 494 | - private void sendRegisterSuccessResponse(NetSocket socket, String requestId, | |
| 495 | - IotDeviceRegisterRespDTO registerResp, String codecType) { | |
| 496 | - try { | |
| 497 | - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | |
| 498 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 499 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | |
| 500 | - // 2. 发送响应 | |
| 501 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 502 | - socket.write(Buffer.buffer(encodedData)); | |
| 503 | - } catch (Exception e) { | |
| 504 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); | |
| 505 | - } | |
| 506 | - } | |
| 507 | - | |
| 508 | -} | |
| 509 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.Min; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT UDP 协议配置 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +public class IotUdpConfig { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 最大会话数 | |
| 17 | + */ | |
| 18 | + @NotNull(message = "最大会话数不能为空") | |
| 19 | + @Min(value = 1, message = "最大会话数必须大于 0") | |
| 20 | + private Integer maxSessions = 1000; | |
| 21 | + /** | |
| 22 | + * 会话超时时间(毫秒) | |
| 23 | + * <p> | |
| 24 | + * 基于 Guava Cache 的 expireAfterAccess 实现自动过期清理 | |
| 25 | + */ | |
| 26 | + @NotNull(message = "会话超时时间不能为空") | |
| 27 | + @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒") | |
| 28 | + private Long sessionTimeoutMs = 60000L; | |
| 29 | + | |
| 30 | + /** | |
| 31 | + * 接收缓冲区大小(字节) | |
| 32 | + */ | |
| 33 | + @NotNull(message = "接收缓冲区大小不能为空") | |
| 34 | + @Min(value = 1024, message = "接收缓冲区大小必须大于 1024 字节") | |
| 35 | + private Integer receiveBufferSize = 65536; | |
| 36 | + /** | |
| 37 | + * 发送缓冲区大小(字节) | |
| 38 | + */ | |
| 39 | + @NotNull(message = "发送缓冲区大小不能为空") | |
| 40 | + @Min(value = 1024, message = "发送缓冲区大小必须大于 1024 字节") | |
| 41 | + private Integer sendBufferSize = 65536; | |
| 42 | + | |
| 43 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 10 | -import jakarta.annotation.PostConstruct; | |
| 11 | -import lombok.RequiredArgsConstructor; | |
| 12 | -import lombok.extern.slf4j.Slf4j; | |
| 13 | - | |
| 14 | -/** | |
| 15 | - * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 | |
| 16 | - * | |
| 17 | - * @author 芋道源码 | |
| 18 | - */ | |
| 19 | -@Slf4j | |
| 20 | -@RequiredArgsConstructor | |
| 21 | -public class IotUdpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 22 | - | |
| 23 | - private final IotUdpUpstreamProtocol protocol; | |
| 24 | - | |
| 25 | - private final IotDeviceMessageService messageService; | |
| 26 | - | |
| 27 | - private final IotUdpSessionManager sessionManager; | |
| 28 | - | |
| 29 | - private final IotMessageBus messageBus; | |
| 30 | - | |
| 31 | - private IotUdpDownstreamHandler downstreamHandler; | |
| 32 | - | |
| 33 | - @PostConstruct | |
| 34 | - public void init() { | |
| 35 | - // 初始化下游处理器 | |
| 36 | - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol); | |
| 37 | - // 注册下游订阅者 | |
| 38 | - messageBus.register(this); | |
| 39 | - log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", | |
| 40 | - protocol.getServerId(), getTopic()); | |
| 41 | - } | |
| 42 | - | |
| 43 | - @Override | |
| 44 | - public String getTopic() { | |
| 45 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | |
| 46 | - } | |
| 47 | - | |
| 48 | - @Override | |
| 49 | - public String getGroup() { | |
| 50 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 51 | - return getTopic(); | |
| 52 | - } | |
| 53 | - | |
| 54 | - @Override | |
| 55 | - public void onMessage(IotDeviceMessage message) { | |
| 56 | - try { | |
| 57 | - downstreamHandler.handle(message); | |
| 58 | - } catch (Exception e) { | |
| 59 | - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", | |
| 60 | - message.getDeviceId(), message.getMethod(), message.getId(), e); | |
| 61 | - } | |
| 62 | - } | |
| 63 | - | |
| 64 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | |
| 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.enums.IotSerializeTypeEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpUpstreamHandler; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | |
| 15 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 16 | +import io.vertx.core.Vertx; | |
| 17 | +import io.vertx.core.datagram.DatagramSocket; | |
| 18 | +import io.vertx.core.datagram.DatagramSocketOptions; | |
| 19 | +import lombok.Getter; | |
| 20 | +import lombok.extern.slf4j.Slf4j; | |
| 21 | +import cn.hutool.core.lang.Assert; | |
| 22 | + | |
| 23 | +/** | |
| 24 | + * IoT UDP 协议实现 | |
| 25 | + * <p> | |
| 26 | + * 基于 Vert.x 实现 UDP 服务器,接收设备上行消息 | |
| 27 | + * | |
| 28 | + * @author 芋道源码 | |
| 29 | + */ | |
| 30 | +@Slf4j | |
| 31 | +public class IotUdpProtocol implements IotProtocol { | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 协议配置 | |
| 35 | + */ | |
| 36 | + private final ProtocolProperties properties; | |
| 37 | + /** | |
| 38 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 39 | + */ | |
| 40 | + @Getter | |
| 41 | + private final String serverId; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 运行状态 | |
| 45 | + */ | |
| 46 | + @Getter | |
| 47 | + private volatile boolean running = false; | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * Vert.x 实例 | |
| 51 | + */ | |
| 52 | + private Vertx vertx; | |
| 53 | + /** | |
| 54 | + * UDP 服务器 | |
| 55 | + */ | |
| 56 | + @Getter | |
| 57 | + private DatagramSocket udpSocket; | |
| 58 | + /** | |
| 59 | + * UDP 会话管理器 | |
| 60 | + */ | |
| 61 | + private final IotUdpSessionManager sessionManager; | |
| 62 | + | |
| 63 | + /** | |
| 64 | + * 下行消息订阅者 | |
| 65 | + */ | |
| 66 | + private final IotUdpDownstreamSubscriber downstreamSubscriber; | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * 消息序列化器 | |
| 70 | + */ | |
| 71 | + private final IotMessageSerializer serializer; | |
| 72 | + | |
| 73 | + public IotUdpProtocol(ProtocolProperties properties) { | |
| 74 | + IotUdpConfig udpConfig = properties.getUdp(); | |
| 75 | + Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); | |
| 76 | + this.properties = properties; | |
| 77 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 78 | + | |
| 79 | + // 初始化序列化器 | |
| 80 | + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); | |
| 81 | + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); | |
| 82 | + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); | |
| 83 | + this.serializer = serializerManager.get(serializeType); | |
| 84 | + | |
| 85 | + // 初始化会话管理器 | |
| 86 | + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs()); | |
| 87 | + | |
| 88 | + // 初始化下行消息订阅者 | |
| 89 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 90 | + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); | |
| 91 | + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 92 | + } | |
| 93 | + | |
| 94 | + @Override | |
| 95 | + public String getId() { | |
| 96 | + return properties.getId(); | |
| 97 | + } | |
| 98 | + | |
| 99 | + @Override | |
| 100 | + public IotProtocolTypeEnum getType() { | |
| 101 | + return IotProtocolTypeEnum.UDP; | |
| 102 | + } | |
| 103 | + | |
| 104 | + @Override | |
| 105 | + public void start() { | |
| 106 | + if (running) { | |
| 107 | + log.warn("[start][IoT UDP 协议 {} 已经在运行中]", getId()); | |
| 108 | + return; | |
| 109 | + } | |
| 110 | + | |
| 111 | + // 1.1 创建 Vertx 实例 | |
| 112 | + this.vertx = Vertx.vertx(); | |
| 113 | + | |
| 114 | + // 1.2 创建 UDP Socket 选项 | |
| 115 | + IotUdpConfig udpConfig = properties.getUdp(); | |
| 116 | + DatagramSocketOptions options = new DatagramSocketOptions() | |
| 117 | + .setReceiveBufferSize(udpConfig.getReceiveBufferSize()) | |
| 118 | + .setSendBufferSize(udpConfig.getSendBufferSize()) | |
| 119 | + .setReuseAddress(true); | |
| 120 | + | |
| 121 | + // 1.3 创建 UDP Socket | |
| 122 | + udpSocket = vertx.createDatagramSocket(options); | |
| 123 | + | |
| 124 | + // 1.4 创建上行消息处理器 | |
| 125 | + IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer); | |
| 126 | + | |
| 127 | + // 1.5 启动 UDP 服务器(阻塞式) | |
| 128 | + try { | |
| 129 | + udpSocket.listen(properties.getPort(), "0.0.0.0").result(); | |
| 130 | + // 设置数据包处理器 | |
| 131 | + udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); | |
| 132 | + running = true; | |
| 133 | + log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]", | |
| 134 | + getId(), properties.getPort(), serverId); | |
| 135 | + | |
| 136 | + // 2. 启动下行消息订阅者 | |
| 137 | + this.downstreamSubscriber.start(); | |
| 138 | + } catch (Exception e) { | |
| 139 | + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e); | |
| 140 | + // 启动失败时关闭资源 | |
| 141 | + if (udpSocket != null) { | |
| 142 | + udpSocket.close(); | |
| 143 | + udpSocket = null; | |
| 144 | + } | |
| 145 | + if (vertx != null) { | |
| 146 | + vertx.close(); | |
| 147 | + vertx = null; | |
| 148 | + } | |
| 149 | + throw e; | |
| 150 | + } | |
| 151 | + } | |
| 152 | + | |
| 153 | + @Override | |
| 154 | + public void stop() { | |
| 155 | + if (!running) { | |
| 156 | + return; | |
| 157 | + } | |
| 158 | + // 1. 停止下行消息订阅者 | |
| 159 | + try { | |
| 160 | + downstreamSubscriber.stop(); | |
| 161 | + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 162 | + } catch (Exception e) { | |
| 163 | + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 164 | + } | |
| 165 | + | |
| 166 | + // 2.1 关闭 UDP Socket | |
| 167 | + if (udpSocket != null) { | |
| 168 | + try { | |
| 169 | + udpSocket.close().result(); | |
| 170 | + log.info("[stop][IoT UDP 协议 {} 服务器已停止]", getId()); | |
| 171 | + } catch (Exception e) { | |
| 172 | + log.error("[stop][IoT UDP 协议 {} 服务器停止失败]", getId(), e); | |
| 173 | + } | |
| 174 | + udpSocket = null; | |
| 175 | + } | |
| 176 | + // 2.2 关闭 Vertx 实例 | |
| 177 | + if (vertx != null) { | |
| 178 | + try { | |
| 179 | + vertx.close().result(); | |
| 180 | + log.info("[stop][IoT UDP 协议 {} Vertx 已关闭]", getId()); | |
| 181 | + } catch (Exception e) { | |
| 182 | + log.error("[stop][IoT UDP 协议 {} Vertx 关闭失败]", getId(), e); | |
| 183 | + } | |
| 184 | + vertx = null; | |
| 185 | + } | |
| 186 | + running = false; | |
| 187 | + log.info("[stop][IoT UDP 协议 {} 已停止]", getId()); | |
| 188 | + } | |
| 189 | + | |
| 190 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; | |
| 2 | - | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; | |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 12 | -import io.vertx.core.Vertx; | |
| 13 | -import io.vertx.core.datagram.DatagramSocket; | |
| 14 | -import io.vertx.core.datagram.DatagramSocketOptions; | |
| 15 | -import jakarta.annotation.PostConstruct; | |
| 16 | -import jakarta.annotation.PreDestroy; | |
| 17 | -import lombok.Getter; | |
| 18 | -import lombok.extern.slf4j.Slf4j; | |
| 19 | - | |
| 20 | -import java.util.List; | |
| 21 | - | |
| 22 | -/** | |
| 23 | - * IoT 网关 UDP 协议:接收设备上行消息 | |
| 24 | - * <p> | |
| 25 | - * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能: | |
| 26 | - * 1. 监听 UDP 端口,接收设备消息 | |
| 27 | - * 2. 定期清理不活跃的设备地址映射 | |
| 28 | - * 3. 提供 UDP Socket 用于下行消息发送 | |
| 29 | - * | |
| 30 | - * @author 芋道源码 | |
| 31 | - */ | |
| 32 | -@Slf4j | |
| 33 | -public class IotUdpUpstreamProtocol { | |
| 34 | - | |
| 35 | - private final IotGatewayProperties.UdpProperties udpProperties; | |
| 36 | - | |
| 37 | - private final IotDeviceService deviceService; | |
| 38 | - | |
| 39 | - private final IotDeviceMessageService messageService; | |
| 40 | - | |
| 41 | - private final IotUdpSessionManager sessionManager; | |
| 42 | - | |
| 43 | - private final Vertx vertx; | |
| 44 | - | |
| 45 | - @Getter | |
| 46 | - private final String serverId; | |
| 47 | - | |
| 48 | - @Getter | |
| 49 | - private DatagramSocket udpSocket; | |
| 50 | - | |
| 51 | - /** | |
| 52 | - * 会话清理定时器 ID | |
| 53 | - */ | |
| 54 | - private Long cleanTimerId; | |
| 55 | - | |
| 56 | - private IotUdpUpstreamHandler upstreamHandler; | |
| 57 | - | |
| 58 | - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, | |
| 59 | - IotDeviceService deviceService, | |
| 60 | - IotDeviceMessageService messageService, | |
| 61 | - IotUdpSessionManager sessionManager, | |
| 62 | - Vertx vertx) { | |
| 63 | - this.udpProperties = udpProperties; | |
| 64 | - this.deviceService = deviceService; | |
| 65 | - this.messageService = messageService; | |
| 66 | - this.sessionManager = sessionManager; | |
| 67 | - this.vertx = vertx; | |
| 68 | - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); | |
| 69 | - } | |
| 70 | - | |
| 71 | - @PostConstruct | |
| 72 | - public void start() { | |
| 73 | - // 1. 初始化上行消息处理器 | |
| 74 | - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager); | |
| 75 | - | |
| 76 | - // 2. 创建 UDP Socket 选项 | |
| 77 | - DatagramSocketOptions options = new DatagramSocketOptions() | |
| 78 | - .setReceiveBufferSize(udpProperties.getReceiveBufferSize()) | |
| 79 | - .setSendBufferSize(udpProperties.getSendBufferSize()) | |
| 80 | - .setReuseAddress(true); | |
| 81 | - | |
| 82 | - // 3. 创建 UDP Socket | |
| 83 | - udpSocket = vertx.createDatagramSocket(options); | |
| 84 | - | |
| 85 | - // 4. 监听端口 | |
| 86 | - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> { | |
| 87 | - if (result.failed()) { | |
| 88 | - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause()); | |
| 89 | - return; | |
| 90 | - } | |
| 91 | - // 设置数据包处理器 | |
| 92 | - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); | |
| 93 | - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", | |
| 94 | - udpProperties.getPort(), udpProperties.getReceiveBufferSize(), | |
| 95 | - udpProperties.getSendBufferSize()); | |
| 96 | - | |
| 97 | - // 5. 启动会话清理定时器 | |
| 98 | - startSessionCleanTimer(); | |
| 99 | - }); | |
| 100 | - } | |
| 101 | - | |
| 102 | - @PreDestroy | |
| 103 | - public void stop() { | |
| 104 | - // 1. 取消会话清理定时器 | |
| 105 | - if (cleanTimerId != null) { | |
| 106 | - vertx.cancelTimer(cleanTimerId); | |
| 107 | - cleanTimerId = null; | |
| 108 | - log.info("[stop][会话清理定时器已取消]"); | |
| 109 | - } | |
| 110 | - | |
| 111 | - // 2. 关闭 UDP Socket | |
| 112 | - if (udpSocket != null) { | |
| 113 | - try { | |
| 114 | - udpSocket.close().result(); | |
| 115 | - log.info("[stop][IoT 网关 UDP 协议已停止]"); | |
| 116 | - } catch (Exception e) { | |
| 117 | - log.error("[stop][IoT 网关 UDP 协议停止失败]", e); | |
| 118 | - } | |
| 119 | - } | |
| 120 | - } | |
| 121 | - | |
| 122 | - /** | |
| 123 | - * 启动会话清理定时器 | |
| 124 | - */ | |
| 125 | - private void startSessionCleanTimer() { | |
| 126 | - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> { | |
| 127 | - try { | |
| 128 | - // 1. 清理超时的设备地址映射,并获取离线设备列表 | |
| 129 | - List<Long> offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs()); | |
| 130 | - | |
| 131 | - // 2. 为每个离线设备发送离线消息 | |
| 132 | - for (Long deviceId : offlineDeviceIds) { | |
| 133 | - sendOfflineMessage(deviceId); | |
| 134 | - } | |
| 135 | - if (CollUtil.isNotEmpty(offlineDeviceIds)) { | |
| 136 | - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size()); | |
| 137 | - } | |
| 138 | - } catch (Exception e) { | |
| 139 | - log.error("[cleanExpiredMappings][清理超时会话失败]", e); | |
| 140 | - } | |
| 141 | - }); | |
| 142 | - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", | |
| 143 | - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs()); | |
| 144 | - } | |
| 145 | - | |
| 146 | - /** | |
| 147 | - * 发送设备离线消息 | |
| 148 | - * | |
| 149 | - * @param deviceId 设备 ID | |
| 150 | - */ | |
| 151 | - private void sendOfflineMessage(Long deviceId) { | |
| 152 | - try { | |
| 153 | - // 获取设备信息 | |
| 154 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); | |
| 155 | - if (device == null) { | |
| 156 | - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); | |
| 157 | - return; | |
| 158 | - } | |
| 159 | - | |
| 160 | - // 发送离线消息 | |
| 161 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 162 | - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), | |
| 163 | - device.getDeviceName(), serverId); | |
| 164 | - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", | |
| 165 | - deviceId, device.getDeviceName()); | |
| 166 | - } catch (Exception e) { | |
| 167 | - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); | |
| 168 | - } | |
| 169 | - } | |
| 170 | - | |
| 171 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; | |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | |
| 7 | 7 | import io.vertx.core.datagram.DatagramSocket; |
| 8 | +import lombok.RequiredArgsConstructor; | |
| 8 | 9 | import lombok.extern.slf4j.Slf4j; |
| 9 | 10 | |
| 10 | 11 | /** |
| ... | ... | @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j; |
| 13 | 14 | * @author 芋道源码 |
| 14 | 15 | */ |
| 15 | 16 | @Slf4j |
| 17 | +@RequiredArgsConstructor | |
| 16 | 18 | public class IotUdpDownstreamHandler { |
| 17 | 19 | |
| 18 | - private final IotDeviceMessageService deviceMessageService; | |
| 20 | + private final IotUdpProtocol protocol; | |
| 19 | 21 | |
| 20 | 22 | private final IotUdpSessionManager sessionManager; |
| 21 | 23 | |
| 22 | - private final IotUdpUpstreamProtocol protocol; | |
| 23 | - | |
| 24 | - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, | |
| 25 | - IotUdpSessionManager sessionManager, | |
| 26 | - IotUdpUpstreamProtocol protocol) { | |
| 27 | - this.deviceMessageService = deviceMessageService; | |
| 28 | - this.sessionManager = sessionManager; | |
| 29 | - this.protocol = protocol; | |
| 30 | - } | |
| 24 | + /** | |
| 25 | + * 消息序列化器(处理业务消息序列化/反序列化) | |
| 26 | + */ | |
| 27 | + private final IotMessageSerializer serializer; | |
| 31 | 28 | |
| 32 | 29 | /** |
| 33 | 30 | * 处理下行消息 |
| 34 | - * | |
| 35 | - * @param message 下行消息 | |
| 36 | 31 | */ |
| 37 | 32 | public void handle(IotDeviceMessage message) { |
| 38 | 33 | try { |
| 39 | 34 | log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 40 | 35 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 41 | - | |
| 42 | - // 1. 获取会话信息(包含 codecType) | |
| 43 | - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); | |
| 36 | + // 1. 检查设备会话 | |
| 37 | + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSession(message.getDeviceId()); | |
| 44 | 38 | if (sessionInfo == null) { |
| 45 | - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); | |
| 39 | + log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]", | |
| 40 | + message.getDeviceId(), message.getMethod(), message.getId()); | |
| 46 | 41 | return; |
| 47 | 42 | } |
| 48 | - | |
| 49 | - // 2. 使用会话中的 codecType 编码消息,并发送到设备 | |
| 50 | - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); | |
| 51 | 43 | DatagramSocket socket = protocol.getUdpSocket(); |
| 52 | 44 | if (socket == null) { |
| 53 | 45 | log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); |
| 54 | 46 | return; |
| 55 | 47 | } |
| 56 | - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket); | |
| 57 | - if (success) { | |
| 58 | - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | |
| 59 | - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); | |
| 60 | - } else { | |
| 61 | - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", | |
| 62 | - message.getDeviceId(), message.getMethod(), message.getId()); | |
| 48 | + | |
| 49 | + // 2. 序列化消息 | |
| 50 | + byte[] serializedData = serializer.serialize(message); | |
| 51 | + | |
| 52 | + // 3. 发送到设备 | |
| 53 | + boolean success = sessionManager.sendToDevice(message.getDeviceId(), serializedData, socket); | |
| 54 | + if (!success) { | |
| 55 | + throw new RuntimeException("下行消息发送失败"); | |
| 63 | 56 | } |
| 57 | + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", | |
| 58 | + message.getDeviceId(), message.getMethod(), message.getId(), serializedData.length); | |
| 64 | 59 | } catch (Exception e) { |
| 65 | 60 | log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", |
| 66 | 61 | message.getDeviceId(), message.getMethod(), message, e); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + private final IotUdpDownstreamHandler downstreamHandler; | |
| 18 | + | |
| 19 | + public IotUdpDownstreamSubscriber(IotProtocol protocol, | |
| 20 | + IotUdpDownstreamHandler downstreamHandler, | |
| 21 | + IotMessageBus messageBus) { | |
| 22 | + super(protocol, messageBus); | |
| 23 | + this.downstreamHandler = downstreamHandler; | |
| 24 | + } | |
| 25 | + | |
| 26 | + @Override | |
| 27 | + protected void handleMessage(IotDeviceMessage message) { | |
| 28 | + downstreamHandler.handle(message); | |
| 29 | + } | |
| 30 | + | |
| 31 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.map.MapUtil; | |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 5 | +import cn.hutool.core.util.BooleanUtil; | |
| 6 | +import cn.hutool.core.util.IdUtil; | |
| 7 | +import cn.hutool.core.util.StrUtil; | |
| 8 | +import cn.hutool.extra.spring.SpringUtil; | |
| 9 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 10 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 11 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 12 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 13 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 14 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 15 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 16 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 17 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 18 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 19 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 20 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 21 | +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | |
| 22 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | |
| 23 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 24 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 25 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 26 | +import io.vertx.core.buffer.Buffer; | |
| 27 | +import io.vertx.core.datagram.DatagramPacket; | |
| 28 | +import io.vertx.core.datagram.DatagramSocket; | |
| 29 | +import lombok.extern.slf4j.Slf4j; | |
| 30 | +import cn.hutool.core.lang.Assert; | |
| 31 | + | |
| 32 | +import java.net.InetSocketAddress; | |
| 33 | +import java.util.Map; | |
| 34 | + | |
| 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; | |
| 38 | + | |
| 39 | +/** | |
| 40 | + * UDP 上行消息处理器 | |
| 41 | + * <p> | |
| 42 | + * 采用无状态 Token 机制(每次请求携带 token): | |
| 43 | + * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password | |
| 44 | + * 2. 返回 Token:服务端验证后返回 JWT token | |
| 45 | + * 3. 后续请求:每次请求在 params 中携带 token | |
| 46 | + * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 | |
| 47 | + * | |
| 48 | + * @author 芋道源码 | |
| 49 | + */ | |
| 50 | +@Slf4j | |
| 51 | +public class IotUdpUpstreamHandler { | |
| 52 | + | |
| 53 | + private static final String AUTH_METHOD = "auth"; | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * Token 参数 Key | |
| 57 | + */ | |
| 58 | + private static final String PARAM_KEY_TOKEN = "token"; | |
| 59 | + /** | |
| 60 | + * Body 参数 Key(实际请求内容) | |
| 61 | + */ | |
| 62 | + private static final String PARAM_KEY_BODY = "body"; | |
| 63 | + | |
| 64 | + private final String serverId; | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * 消息序列化器(处理业务消息序列化/反序列化) | |
| 68 | + */ | |
| 69 | + private final IotMessageSerializer serializer; | |
| 70 | + /** | |
| 71 | + * UDP 会话管理器 | |
| 72 | + */ | |
| 73 | + private final IotUdpSessionManager sessionManager; | |
| 74 | + | |
| 75 | + private final IotDeviceMessageService deviceMessageService; | |
| 76 | + private final IotDeviceService deviceService; | |
| 77 | + private final IotDeviceTokenService deviceTokenService; | |
| 78 | + private final IotDeviceCommonApi deviceApi; | |
| 79 | + | |
| 80 | + public IotUdpUpstreamHandler(String serverId, | |
| 81 | + IotUdpSessionManager sessionManager, | |
| 82 | + IotMessageSerializer serializer) { | |
| 83 | + Assert.notNull(serializer, "消息序列化器必须配置"); | |
| 84 | + Assert.notNull(sessionManager, "会话管理器不能为空"); | |
| 85 | + this.serverId = serverId; | |
| 86 | + this.sessionManager = sessionManager; | |
| 87 | + this.serializer = serializer; | |
| 88 | + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 89 | + this.deviceService = SpringUtil.getBean(IotDeviceService.class); | |
| 90 | + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 91 | + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 92 | + } | |
| 93 | + | |
| 94 | + // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现 | |
| 95 | + /** | |
| 96 | + * 处理 UDP 数据包 | |
| 97 | + * | |
| 98 | + * @param packet 数据包 | |
| 99 | + * @param socket UDP Socket | |
| 100 | + */ | |
| 101 | + public void handle(DatagramPacket packet, DatagramSocket socket) { | |
| 102 | + InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); | |
| 103 | + Buffer data = packet.data(); | |
| 104 | + String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 105 | + log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); | |
| 106 | + processMessage(data, senderAddress, socket); | |
| 107 | + } | |
| 108 | + | |
| 109 | + /** | |
| 110 | + * 处理消息 | |
| 111 | + * | |
| 112 | + * @param buffer 消息 | |
| 113 | + * @param senderAddress 发送者地址 | |
| 114 | + * @param socket UDP Socket | |
| 115 | + */ | |
| 116 | + private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { | |
| 117 | + String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 118 | + // 1.1 基础检查 | |
| 119 | + if (ArrayUtil.isEmpty(buffer)) { | |
| 120 | + return; | |
| 121 | + } | |
| 122 | + // 1.2 反序列化消息 | |
| 123 | + IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); | |
| 124 | + if (message == null) { | |
| 125 | + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); | |
| 126 | + return; | |
| 127 | + } | |
| 128 | + | |
| 129 | + // 2. 根据消息类型路由处理 | |
| 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()); | |
| 159 | + } | |
| 160 | + } | |
| 161 | + | |
| 162 | + /** | |
| 163 | + * 处理认证请求 | |
| 164 | + * | |
| 165 | + * @param message 消息信息 | |
| 166 | + * @param senderAddress 发送者地址 | |
| 167 | + * @param socket UDP Socket | |
| 168 | + */ | |
| 169 | + @SuppressWarnings("DuplicatedCode") | |
| 170 | + private void handleAuthenticationRequest(IotDeviceMessage message, InetSocketAddress senderAddress, | |
| 171 | + DatagramSocket socket) { | |
| 172 | + String clientId = IdUtil.simpleUUID(); | |
| 173 | + // 1. 解析认证参数 | |
| 174 | + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | |
| 175 | + Assert.notNull(authParams, "认证参数不能为空"); | |
| 176 | + Assert.notBlank(authParams.getUsername(), "username 不能为空"); | |
| 177 | + Assert.notBlank(authParams.getPassword(), "password 不能为空"); | |
| 178 | + | |
| 179 | + // 2.1 执行认证 | |
| 180 | + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 181 | + authResult.checkError(); | |
| 182 | + if (!BooleanUtil.isTrue(authResult.getData())) { | |
| 183 | + throw exception(DEVICE_AUTH_FAIL); | |
| 184 | + } | |
| 185 | + // 2.2 解析设备信息 | |
| 186 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); | |
| 187 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | |
| 188 | + // 2.3 获取设备信息 | |
| 189 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 190 | + deviceInfo.getDeviceName()); | |
| 191 | + Assert.notNull(device, "设备不存在"); | |
| 192 | + | |
| 193 | + // 3. 生成 JWT Token(无状态) | |
| 194 | + String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); | |
| 195 | + | |
| 196 | + // 4.1 注册会话 | |
| 197 | + registerSession(senderAddress, device, clientId); | |
| 198 | + // 4.2 发送上线消息 | |
| 199 | + sendOnlineMessage(device); | |
| 200 | + // 4.3 发送成功响应(包含 token) | |
| 201 | + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, | |
| 202 | + MapUtil.of("token", token)); | |
| 203 | + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", | |
| 204 | + device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); | |
| 205 | + } | |
| 206 | + | |
| 207 | + /** | |
| 208 | + * 处理设备动态注册请求(一型一密,不需要认证) | |
| 209 | + * | |
| 210 | + * @param message 消息信息 | |
| 211 | + * @param senderAddress 发送者地址 | |
| 212 | + * @param socket UDP Socket | |
| 213 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 214 | + */ | |
| 215 | + @SuppressWarnings("DuplicatedCode") | |
| 216 | + private void handleRegisterRequest(IotDeviceMessage message, InetSocketAddress senderAddress, | |
| 217 | + DatagramSocket socket) { | |
| 218 | + // 1. 解析注册参数 | |
| 219 | + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | |
| 220 | + Assert.notNull(params, "注册参数不能为空"); | |
| 221 | + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); | |
| 222 | + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); | |
| 223 | + Assert.notBlank(params.getSign(), "sign 不能为空"); | |
| 224 | + | |
| 225 | + // 2. 调用动态注册 | |
| 226 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 227 | + result.checkError(); | |
| 228 | + | |
| 229 | + // 3. 发送成功响应 | |
| 230 | + sendSuccessResponse(socket, senderAddress, message.getRequestId(), | |
| 231 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); | |
| 232 | + log.info("[handleRegisterRequest][注册成功,来源: {},设备名: {}]", | |
| 233 | + sessionManager.buildAddressKey(senderAddress), params.getDeviceName()); | |
| 234 | + } | |
| 235 | + | |
| 236 | + /** | |
| 237 | + * 处理业务请求 | |
| 238 | + * <p> | |
| 239 | + * 请求参数格式: | |
| 240 | + * - token:JWT 令牌 | |
| 241 | + * - body:实际请求内容(可以是 Map、List 或其他类型) | |
| 242 | + * | |
| 243 | + * @param message 消息信息 | |
| 244 | + * @param senderAddress 发送者地址 | |
| 245 | + * @param socket UDP Socket | |
| 246 | + */ | |
| 247 | + @SuppressWarnings("unchecked") | |
| 248 | + private void handleBusinessRequest(IotDeviceMessage message, InetSocketAddress senderAddress, | |
| 249 | + DatagramSocket socket) { | |
| 250 | + String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 251 | + // 1.1 从消息中提取 token 和 body | |
| 252 | + String token = null; | |
| 253 | + Object body = null; | |
| 254 | + if (message.getParams() instanceof Map) { | |
| 255 | + Map<String, Object> paramsMap = (Map<String, Object>) message.getParams(); | |
| 256 | + token = (String) paramsMap.get(PARAM_KEY_TOKEN); | |
| 257 | + body = paramsMap.get(PARAM_KEY_BODY); | |
| 258 | + } | |
| 259 | + if (StrUtil.isBlank(token)) { | |
| 260 | + log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); | |
| 261 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | |
| 262 | + UNAUTHORIZED.getCode(), "请先进行认证"); | |
| 263 | + return; | |
| 264 | + } | |
| 265 | + // 1.2 验证 token,获取设备信息 | |
| 266 | + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); | |
| 267 | + if (deviceInfo == null) { | |
| 268 | + log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); | |
| 269 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | |
| 270 | + UNAUTHORIZED.getCode(), "token 无效或已过期"); | |
| 271 | + return; | |
| 272 | + } | |
| 273 | + // 1.3 获取设备详细信息 | |
| 274 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 275 | + deviceInfo.getDeviceName()); | |
| 276 | + if (device == null) { | |
| 277 | + log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", | |
| 278 | + addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 279 | + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), | |
| 280 | + BAD_REQUEST.getCode(), "设备不存在"); | |
| 281 | + return; | |
| 282 | + } | |
| 283 | + | |
| 284 | + // 2. 更新会话地址(如有变化) | |
| 285 | + sessionManager.updateSessionAddress(device.getId(), senderAddress); | |
| 286 | + | |
| 287 | + // 3. 将 body 设置为实际的 params,发送消息到消息总线 | |
| 288 | + message.setParams(body); | |
| 289 | + deviceMessageService.sendDeviceMessage(message, device.getProductKey(), | |
| 290 | + device.getDeviceName(), serverId); | |
| 291 | + log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", | |
| 292 | + device.getId(), message.getMethod(), addressKey); | |
| 293 | + } | |
| 294 | + | |
| 295 | + /** | |
| 296 | + * 注册会话信息 | |
| 297 | + * | |
| 298 | + * @param address 设备地址 | |
| 299 | + * @param device 设备 | |
| 300 | + * @param clientId 客户端 ID | |
| 301 | + */ | |
| 302 | + private void registerSession(InetSocketAddress address, IotDeviceRespDTO device, String clientId) { | |
| 303 | + IotUdpSessionManager.SessionInfo sessionInfo = new IotUdpSessionManager.SessionInfo() | |
| 304 | + .setDeviceId(device.getId()) | |
| 305 | + .setProductKey(device.getProductKey()) | |
| 306 | + .setDeviceName(device.getDeviceName()) | |
| 307 | + .setAddress(address); | |
| 308 | + sessionManager.registerSession(device.getId(), sessionInfo); | |
| 309 | + } | |
| 310 | + | |
| 311 | + /** | |
| 312 | + * 发送设备上线消息 | |
| 313 | + * | |
| 314 | + * @param device 设备信息 | |
| 315 | + */ | |
| 316 | + private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 317 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 318 | + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 319 | + device.getDeviceName(), serverId); | |
| 320 | + } | |
| 321 | + | |
| 322 | + // ===================== 发送响应消息 ===================== | |
| 323 | + | |
| 324 | + /** | |
| 325 | + * 发送成功响应 | |
| 326 | + * | |
| 327 | + * @param socket UDP Socket | |
| 328 | + * @param address 目标地址 | |
| 329 | + * @param requestId 请求 ID | |
| 330 | + * @param method 方法名 | |
| 331 | + * @param data 响应数据 | |
| 332 | + */ | |
| 333 | + private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address, | |
| 334 | + String requestId, String method, Object data) { | |
| 335 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); | |
| 336 | + writeResponse(socket, address, responseMessage); | |
| 337 | + } | |
| 338 | + | |
| 339 | + /** | |
| 340 | + * 发送错误响应 | |
| 341 | + * | |
| 342 | + * @param socket UDP Socket | |
| 343 | + * @param address 目标地址 | |
| 344 | + * @param requestId 请求 ID | |
| 345 | + * @param method 方法名 | |
| 346 | + * @param code 错误码 | |
| 347 | + * @param msg 错误消息 | |
| 348 | + */ | |
| 349 | + private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, | |
| 350 | + String requestId, String method, Integer code, String msg) { | |
| 351 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); | |
| 352 | + writeResponse(socket, address, responseMessage); | |
| 353 | + } | |
| 354 | + | |
| 355 | + /** | |
| 356 | + * 写入响应到 Socket | |
| 357 | + * | |
| 358 | + * @param socket UDP Socket | |
| 359 | + * @param address 目标地址 | |
| 360 | + * @param responseMessage 响应消息 | |
| 361 | + */ | |
| 362 | + private void writeResponse(DatagramSocket socket, InetSocketAddress address, IotDeviceMessage responseMessage) { | |
| 363 | + try { | |
| 364 | + byte[] serializedData = serializer.serialize(responseMessage); | |
| 365 | + socket.send(Buffer.buffer(serializedData), address.getPort(), address.getHostString(), result -> { | |
| 366 | + if (result.failed()) { | |
| 367 | + log.error("[writeResponse][发送响应失败,地址: {}]", | |
| 368 | + sessionManager.buildAddressKey(address), result.cause()); | |
| 369 | + } | |
| 370 | + }); | |
| 371 | + } catch (Exception e) { | |
| 372 | + log.error("[writeResponse][发送响应异常,地址: {}]", | |
| 373 | + sessionManager.buildAddressKey(address), e); | |
| 374 | + } | |
| 375 | + } | |
| 376 | + | |
| 377 | +} | ... | ... |
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 | -import org.springframework.stereotype.Component; | |
| 8 | 10 | |
| 9 | 11 | import java.net.InetSocketAddress; |
| 10 | -import java.time.LocalDateTime; | |
| 11 | -import java.util.ArrayList; | |
| 12 | -import java.util.Iterator; | |
| 13 | -import java.util.List; | |
| 14 | -import java.util.Map; | |
| 15 | -import java.util.concurrent.ConcurrentHashMap; | |
| 12 | +import java.util.concurrent.CompletableFuture; | |
| 13 | +import java.util.concurrent.TimeUnit; | |
| 16 | 14 | |
| 17 | 15 | /** |
| 18 | 16 | * IoT 网关 UDP 会话管理器 |
| 19 | 17 | * <p> |
| 20 | - * 采用无状态设计,SessionManager 主要用于: | |
| 21 | - * 1. 管理设备地址映射(用于下行消息发送) | |
| 22 | - * 2. 定期清理不活跃的设备地址映射 | |
| 23 | - * <p> | |
| 24 | - * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态 | |
| 18 | + * 基于 Guava Cache 实现会话的自动过期清理: | |
| 19 | + * 1. 管理设备会话信息(设备 ID -> 地址映射) | |
| 20 | + * 2. 自动清理超时会话(expireAfterAccess) | |
| 21 | + * 3. 限制最大会话数(maximumSize) | |
| 25 | 22 | * |
| 26 | 23 | * @author 芋道源码 |
| 27 | 24 | */ |
| 28 | 25 | @Slf4j |
| 29 | -@Component | |
| 30 | 26 | public class IotUdpSessionManager { |
| 31 | 27 | |
| 32 | 28 | /** |
| 33 | - * 设备 ID -> 会话信息(包含地址和 codecType) | |
| 29 | + * 设备会话缓存:设备 ID -> 会话信息 | |
| 30 | + * <p> | |
| 31 | + * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间 | |
| 34 | 32 | */ |
| 35 | - private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>(); | |
| 33 | + private final Cache<Long, SessionInfo> deviceSessionCache; | |
| 36 | 34 | |
| 37 | - /** | |
| 38 | - * 设备地址 Key -> 最后活跃时间(用于清理) | |
| 39 | - */ | |
| 40 | - private final Map<String, LocalDateTime> lastActiveTimeMap = new ConcurrentHashMap<>(); | |
| 35 | + private final int maxSessions; | |
| 41 | 36 | |
| 42 | - /** | |
| 43 | - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) | |
| 44 | - */ | |
| 45 | - private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>(); | |
| 46 | - | |
| 47 | - /** | |
| 48 | - * 更新设备会话(每次收到上行消息时调用) | |
| 49 | - * | |
| 50 | - * @param deviceId 设备 ID | |
| 51 | - * @param address 设备地址 | |
| 52 | - * @param codecType 消息编解码类型 | |
| 53 | - */ | |
| 54 | - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { | |
| 55 | - String addressKey = buildAddressKey(address); | |
| 56 | - // 更新设备会话映射 | |
| 57 | - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); | |
| 58 | - lastActiveTimeMap.put(addressKey, LocalDateTime.now()); | |
| 59 | - addressDeviceMap.put(addressKey, deviceId); | |
| 60 | - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); | |
| 37 | + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) { | |
| 38 | + this.maxSessions = maxSessions; | |
| 39 | + this.deviceSessionCache = CacheBuilder.newBuilder() | |
| 40 | + .maximumSize(maxSessions) | |
| 41 | + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS) | |
| 42 | + .build(); | |
| 61 | 43 | } |
| 62 | 44 | |
| 63 | 45 | /** |
| 64 | - * 更新设备地址(兼容旧接口,默认不更新 codecType) | |
| 46 | + * 注册设备会话 | |
| 65 | 47 | * |
| 66 | - * @param deviceId 设备 ID | |
| 67 | - * @param address 设备地址 | |
| 48 | + * @param deviceId 设备 ID | |
| 49 | + * @param sessionInfo 会话信息 | |
| 68 | 50 | */ |
| 69 | - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { | |
| 70 | - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | |
| 71 | - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null; | |
| 72 | - updateDeviceSession(deviceId, address, codecType); | |
| 51 | + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { | |
| 52 | + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) | |
| 53 | + if (deviceSessionCache.getIfPresent(deviceId) == null | |
| 54 | + && deviceSessionCache.size() >= maxSessions) { | |
| 55 | + throw new IllegalStateException("会话数已达上限: " + maxSessions); | |
| 56 | + } | |
| 57 | + // 注册会话 | |
| 58 | + deviceSessionCache.put(deviceId, sessionInfo); | |
| 59 | + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]", | |
| 60 | + deviceId, buildAddressKey(sessionInfo.getAddress()), | |
| 61 | + sessionInfo.getProductKey(), sessionInfo.getDeviceName()); | |
| 73 | 62 | } |
| 74 | 63 | |
| 75 | 64 | /** |
| 76 | - * 获取设备会话信息 | |
| 65 | + * 获取会话信息 | |
| 66 | + * <p> | |
| 67 | + * 注意:调用此方法会自动刷新会话的过期时间 | |
| 77 | 68 | * |
| 78 | 69 | * @param deviceId 设备 ID |
| 79 | - * @return 会话信息 | |
| 70 | + * @return 会话信息,不存在则返回 null | |
| 80 | 71 | */ |
| 81 | - public SessionInfo getSessionInfo(Long deviceId) { | |
| 82 | - return deviceSessionMap.get(deviceId); | |
| 72 | + public SessionInfo getSession(Long deviceId) { | |
| 73 | + return deviceSessionCache.getIfPresent(deviceId); | |
| 83 | 74 | } |
| 84 | 75 | |
| 85 | 76 | /** |
| 86 | - * 检查设备是否在线(即是否有地址映射) | |
| 77 | + * 更新设备会话地址(设备地址变更时调用) | |
| 78 | + * <p> | |
| 79 | + * 注意:getIfPresent 已自动刷新过期时间,无需重新 put | |
| 87 | 80 | * |
| 88 | - * @param deviceId 设备 ID | |
| 89 | - * @return 是否在线 | |
| 81 | + * @param deviceId 设备 ID | |
| 82 | + * @param newAddress 新地址 | |
| 90 | 83 | */ |
| 91 | - public boolean isDeviceOnline(Long deviceId) { | |
| 92 | - return deviceSessionMap.containsKey(deviceId); | |
| 93 | - } | |
| 84 | + public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { | |
| 85 | + // 地址未变化,无需更新 | |
| 86 | + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); | |
| 87 | + if (sessionInfo == null) { | |
| 88 | + return; | |
| 89 | + } | |
| 90 | + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) { | |
| 91 | + return; | |
| 92 | + } | |
| 94 | 93 | |
| 95 | - /** | |
| 96 | - * 检查设备是否离线 | |
| 97 | - * | |
| 98 | - * @param deviceId 设备 ID | |
| 99 | - * @return 是否离线 | |
| 100 | - */ | |
| 101 | - public boolean isDeviceOffline(Long deviceId) { | |
| 102 | - return !isDeviceOnline(deviceId); | |
| 94 | + // 更新地址 | |
| 95 | + String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); | |
| 96 | + sessionInfo.setAddress(newAddress); | |
| 97 | + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]", | |
| 98 | + deviceId, oldAddressKey, buildAddressKey(newAddress)); | |
| 103 | 99 | } |
| 104 | 100 | |
| 105 | 101 | /** |
| ... | ... | @@ -111,24 +107,28 @@ public class IotUdpSessionManager { |
| 111 | 107 | * @return 是否发送成功 |
| 112 | 108 | */ |
| 113 | 109 | public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { |
| 114 | - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); | |
| 110 | + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); | |
| 115 | 111 | if (sessionInfo == null || sessionInfo.getAddress() == null) { |
| 116 | 112 | log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); |
| 117 | 113 | return false; |
| 118 | 114 | } |
| 119 | - | |
| 120 | 115 | InetSocketAddress address = sessionInfo.getAddress(); |
| 121 | 116 | try { |
| 117 | + // 使用 CompletableFuture 同步等待发送结果 | |
| 118 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); | |
| 122 | 119 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 123 | 120 | if (result.succeeded()) { |
| 124 | 121 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", |
| 125 | 122 | deviceId, buildAddressKey(address), data.length); |
| 123 | + future.complete(true); | |
| 126 | 124 | } else { |
| 127 | 125 | log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", |
| 128 | 126 | deviceId, buildAddressKey(address), result.cause()); |
| 127 | + future.complete(false); | |
| 129 | 128 | } |
| 130 | 129 | }); |
| 131 | - return true; | |
| 130 | + // 同步等待结果,超时 5 秒 | |
| 131 | + return future.get(5, TimeUnit.SECONDS); | |
| 132 | 132 | } catch (Exception e) { |
| 133 | 133 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); |
| 134 | 134 | return false; |
| ... | ... | @@ -136,44 +136,7 @@ public class IotUdpSessionManager { |
| 136 | 136 | } |
| 137 | 137 | |
| 138 | 138 | /** |
| 139 | - * 定期清理不活跃的设备地址映射 | |
| 140 | - * | |
| 141 | - * @param timeoutMs 超时时间(毫秒) | |
| 142 | - * @return 清理的设备 ID 列表(用于发送离线消息) | |
| 143 | - */ | |
| 144 | - public List<Long> cleanExpiredMappings(long timeoutMs) { | |
| 145 | - List<Long> offlineDeviceIds = new ArrayList<>(); | |
| 146 | - LocalDateTime now = LocalDateTime.now(); | |
| 147 | - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); | |
| 148 | - Iterator<Map.Entry<String, LocalDateTime>> iterator = lastActiveTimeMap.entrySet().iterator(); | |
| 149 | - while (iterator.hasNext()) { | |
| 150 | - // 未过期,跳过 | |
| 151 | - Map.Entry<String, LocalDateTime> entry = iterator.next(); | |
| 152 | - if (entry.getValue().isAfter(expireTime)) { | |
| 153 | - continue; | |
| 154 | - } | |
| 155 | - // 过期处理:记录离线设备 ID | |
| 156 | - String addressKey = entry.getKey(); | |
| 157 | - Long deviceId = addressDeviceMap.remove(addressKey); | |
| 158 | - if (deviceId == null) { | |
| 159 | - iterator.remove(); | |
| 160 | - continue; | |
| 161 | - } | |
| 162 | - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); | |
| 163 | - if (sessionInfo == null) { | |
| 164 | - iterator.remove(); | |
| 165 | - continue; | |
| 166 | - } | |
| 167 | - offlineDeviceIds.add(deviceId); | |
| 168 | - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", | |
| 169 | - deviceId, addressKey, entry.getValue()); | |
| 170 | - iterator.remove(); | |
| 171 | - } | |
| 172 | - return offlineDeviceIds; | |
| 173 | - } | |
| 174 | - | |
| 175 | - /** | |
| 176 | - * 构建地址 Key | |
| 139 | + * 构建地址 Key(用于日志输出) | |
| 177 | 140 | * |
| 178 | 141 | * @param address 地址 |
| 179 | 142 | * @return 地址 Key |
| ... | ... | @@ -189,14 +152,22 @@ public class IotUdpSessionManager { |
| 189 | 152 | public static class SessionInfo { |
| 190 | 153 | |
| 191 | 154 | /** |
| 192 | - * 设备地址 | |
| 155 | + * 设备 ID | |
| 193 | 156 | */ |
| 194 | - private InetSocketAddress address; | |
| 157 | + private Long deviceId; | |
| 158 | + /** | |
| 159 | + * 产品 Key | |
| 160 | + */ | |
| 161 | + private String productKey; | |
| 162 | + /** | |
| 163 | + * 设备名称 | |
| 164 | + */ | |
| 165 | + private String deviceName; | |
| 195 | 166 | |
| 196 | 167 | /** |
| 197 | - * 消息编解码类型 | |
| 168 | + * 设备地址 | |
| 198 | 169 | */ |
| 199 | - private String codecType; | |
| 170 | + private InetSocketAddress address; | |
| 200 | 171 | |
| 201 | 172 | } |
| 202 | 173 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.hutool.core.util.BooleanUtil; | |
| 5 | -import cn.hutool.core.util.StrUtil; | |
| 6 | -import cn.hutool.extra.spring.SpringUtil; | |
| 7 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 8 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 9 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 11 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 12 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 13 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 14 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 15 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 16 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 17 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 18 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; | |
| 19 | -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; | |
| 20 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; | |
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; | |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | |
| 23 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 24 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 25 | -import io.vertx.core.buffer.Buffer; | |
| 26 | -import io.vertx.core.datagram.DatagramPacket; | |
| 27 | -import io.vertx.core.datagram.DatagramSocket; | |
| 28 | -import lombok.extern.slf4j.Slf4j; | |
| 29 | - | |
| 30 | -import java.net.InetSocketAddress; | |
| 31 | -import java.util.Map; | |
| 32 | - | |
| 33 | -/** | |
| 34 | - * UDP 上行消息处理器 | |
| 35 | - * <p> | |
| 36 | - * 采用无状态 Token 机制(每次请求携带 token): | |
| 37 | - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password | |
| 38 | - * 2. 返回 Token:服务端验证后返回 JWT token | |
| 39 | - * 3. 后续请求:每次请求在 params 中携带 token | |
| 40 | - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 | |
| 41 | - * | |
| 42 | - * @author 芋道源码 | |
| 43 | - */ | |
| 44 | -@Slf4j | |
| 45 | -public class IotUdpUpstreamHandler { | |
| 46 | - | |
| 47 | - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; | |
| 48 | - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; | |
| 49 | - | |
| 50 | - private static final String AUTH_METHOD = "auth"; | |
| 51 | - /** | |
| 52 | - * Token 参数 Key | |
| 53 | - */ | |
| 54 | - private static final String PARAM_KEY_TOKEN = "token"; | |
| 55 | - /** | |
| 56 | - * Body 参数 Key(实际请求内容) | |
| 57 | - */ | |
| 58 | - private static final String PARAM_KEY_BODY = "body"; | |
| 59 | - | |
| 60 | - private final IotDeviceMessageService deviceMessageService; | |
| 61 | - | |
| 62 | - private final IotDeviceService deviceService; | |
| 63 | - | |
| 64 | - private final IotUdpSessionManager sessionManager; | |
| 65 | - | |
| 66 | - private final IotDeviceTokenService deviceTokenService; | |
| 67 | - | |
| 68 | - private final IotDeviceCommonApi deviceApi; | |
| 69 | - | |
| 70 | - private final String serverId; | |
| 71 | - | |
| 72 | - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol, | |
| 73 | - IotDeviceMessageService deviceMessageService, | |
| 74 | - IotDeviceService deviceService, | |
| 75 | - IotUdpSessionManager sessionManager) { | |
| 76 | - this.deviceMessageService = deviceMessageService; | |
| 77 | - this.deviceService = deviceService; | |
| 78 | - this.sessionManager = sessionManager; | |
| 79 | - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | |
| 80 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 81 | - this.serverId = protocol.getServerId(); | |
| 82 | - } | |
| 83 | - | |
| 84 | - /** | |
| 85 | - * 处理 UDP 数据包 | |
| 86 | - * | |
| 87 | - * @param packet 数据包 | |
| 88 | - * @param socket UDP Socket | |
| 89 | - */ | |
| 90 | - public void handle(DatagramPacket packet, DatagramSocket socket) { | |
| 91 | - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); | |
| 92 | - Buffer data = packet.data(); | |
| 93 | - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", | |
| 94 | - sessionManager.buildAddressKey(senderAddress), data.length()); | |
| 95 | - try { | |
| 96 | - processMessage(data, senderAddress, socket); | |
| 97 | - } catch (Exception e) { | |
| 98 | - log.error("[handle][处理消息失败,来源: {},错误: {}]", | |
| 99 | - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e); | |
| 100 | - // UDP 无连接,不需要断开连接,只记录错误 | |
| 101 | - } | |
| 102 | - } | |
| 103 | - | |
| 104 | - /** | |
| 105 | - * 处理消息 | |
| 106 | - * | |
| 107 | - * @param buffer 消息 | |
| 108 | - * @param senderAddress 发送者地址 | |
| 109 | - * @param socket UDP Socket | |
| 110 | - */ | |
| 111 | - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { | |
| 112 | - // 1. 基础检查 | |
| 113 | - if (buffer == null || buffer.length() == 0) { | |
| 114 | - return; | |
| 115 | - } | |
| 116 | - | |
| 117 | - // 2. 获取消息格式类型 | |
| 118 | - String codecType = getMessageCodecType(buffer); | |
| 119 | - | |
| 120 | - // 3. 解码消息 | |
| 121 | - IotDeviceMessage message; | |
| 122 | - try { | |
| 123 | - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); | |
| 124 | - if (message == null) { | |
| 125 | - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress)); | |
| 126 | - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType); | |
| 127 | - return; | |
| 128 | - } | |
| 129 | - } catch (Exception e) { | |
| 130 | - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e); | |
| 131 | - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType); | |
| 132 | - return; | |
| 133 | - } | |
| 134 | - | |
| 135 | - // 4. 根据消息类型路由处理 | |
| 136 | - try { | |
| 137 | - if (AUTH_METHOD.equals(message.getMethod())) { | |
| 138 | - // 认证请求 | |
| 139 | - handleAuthenticationRequest(message, codecType, senderAddress, socket); | |
| 140 | - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { | |
| 141 | - // 设备动态注册请求 | |
| 142 | - handleRegisterRequest(message, codecType, senderAddress, socket); | |
| 143 | - } else { | |
| 144 | - // 业务消息 | |
| 145 | - handleBusinessRequest(message, codecType, senderAddress, socket); | |
| 146 | - } | |
| 147 | - } catch (Exception e) { | |
| 148 | - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]", | |
| 149 | - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e); | |
| 150 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType); | |
| 151 | - } | |
| 152 | - } | |
| 153 | - | |
| 154 | - /** | |
| 155 | - * 处理认证请求 | |
| 156 | - * | |
| 157 | - * @param message 消息信息 | |
| 158 | - * @param codecType 消息编解码类型 | |
| 159 | - * @param senderAddress 发送者地址 | |
| 160 | - * @param socket UDP Socket | |
| 161 | - */ | |
| 162 | - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType, | |
| 163 | - InetSocketAddress senderAddress, DatagramSocket socket) { | |
| 164 | - String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 165 | - try { | |
| 166 | - // 1.1 解析认证参数 | |
| 167 | - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); | |
| 168 | - if (authParams == null) { | |
| 169 | - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey); | |
| 170 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType); | |
| 171 | - return; | |
| 172 | - } | |
| 173 | - // 1.2 执行认证 | |
| 174 | - if (!validateDeviceAuth(authParams)) { | |
| 175 | - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]", | |
| 176 | - addressKey, authParams.getUsername()); | |
| 177 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType); | |
| 178 | - return; | |
| 179 | - } | |
| 180 | - | |
| 181 | - // 2.1 解析设备信息 | |
| 182 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); | |
| 183 | - if (deviceInfo == null) { | |
| 184 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); | |
| 185 | - return; | |
| 186 | - } | |
| 187 | - // 2.2 获取设备信息 | |
| 188 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 189 | - deviceInfo.getDeviceName()); | |
| 190 | - if (device == null) { | |
| 191 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); | |
| 192 | - return; | |
| 193 | - } | |
| 194 | - | |
| 195 | - // 3.1 生成 JWT Token(无状态) | |
| 196 | - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); | |
| 197 | - | |
| 198 | - // 3.2 更新设备会话信息(用于下行消息,保存 codecType) | |
| 199 | - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); | |
| 200 | - | |
| 201 | - // 3.3 发送上线消息 | |
| 202 | - sendOnlineMessage(device); | |
| 203 | - | |
| 204 | - // 3.4 发送成功响应(包含 token) | |
| 205 | - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType); | |
| 206 | - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", | |
| 207 | - device.getId(), device.getDeviceName(), addressKey); | |
| 208 | - } catch (Exception e) { | |
| 209 | - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e); | |
| 210 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType); | |
| 211 | - } | |
| 212 | - } | |
| 213 | - | |
| 214 | - /** | |
| 215 | - * 处理设备动态注册请求(一型一密,不需要 Token) | |
| 216 | - * | |
| 217 | - * @param message 消息信息 | |
| 218 | - * @param codecType 消息编解码类型 | |
| 219 | - * @param senderAddress 发送者地址 | |
| 220 | - * @param socket UDP Socket | |
| 221 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 222 | - */ | |
| 223 | - private void handleRegisterRequest(IotDeviceMessage message, String codecType, | |
| 224 | - InetSocketAddress senderAddress, DatagramSocket socket) { | |
| 225 | - String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 226 | - try { | |
| 227 | - // 1. 解析注册参数 | |
| 228 | - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | |
| 229 | - if (params == null) { | |
| 230 | - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey); | |
| 231 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType); | |
| 232 | - return; | |
| 233 | - } | |
| 234 | - | |
| 235 | - // 2. 调用动态注册 | |
| 236 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 237 | - if (result.isError()) { | |
| 238 | - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg()); | |
| 239 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType); | |
| 240 | - return; | |
| 241 | - } | |
| 242 | - | |
| 243 | - // 3. 发送成功响应(包含 deviceSecret) | |
| 244 | - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType); | |
| 245 | - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]", | |
| 246 | - params.getDeviceName(), addressKey); | |
| 247 | - } catch (Exception e) { | |
| 248 | - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e); | |
| 249 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType); | |
| 250 | - } | |
| 251 | - } | |
| 252 | - | |
| 253 | - /** | |
| 254 | - * 处理业务请求 | |
| 255 | - * <p> | |
| 256 | - * 请求参数格式: | |
| 257 | - * - token:JWT 令牌 | |
| 258 | - * - body:实际请求内容(可以是 Map、List 或其他类型) | |
| 259 | - * | |
| 260 | - * @param message 消息信息 | |
| 261 | - * @param codecType 消息编解码类型 | |
| 262 | - * @param senderAddress 发送者地址 | |
| 263 | - * @param socket UDP Socket | |
| 264 | - */ | |
| 265 | - @SuppressWarnings("unchecked") | |
| 266 | - private void handleBusinessRequest(IotDeviceMessage message, String codecType, | |
| 267 | - InetSocketAddress senderAddress, DatagramSocket socket) { | |
| 268 | - String addressKey = sessionManager.buildAddressKey(senderAddress); | |
| 269 | - try { | |
| 270 | - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}) | |
| 271 | - String token = null; | |
| 272 | - Object body = null; | |
| 273 | - if (message.getParams() instanceof Map) { | |
| 274 | - Map<String, Object> paramsMap = (Map<String, Object>) message.getParams(); | |
| 275 | - token = (String) paramsMap.get(PARAM_KEY_TOKEN); | |
| 276 | - body = paramsMap.get(PARAM_KEY_BODY); | |
| 277 | - } | |
| 278 | - if (StrUtil.isBlank(token)) { | |
| 279 | - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); | |
| 280 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType); | |
| 281 | - return; | |
| 282 | - } | |
| 283 | - // 1.2 验证 token,获取设备信息 | |
| 284 | - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); | |
| 285 | - if (deviceInfo == null) { | |
| 286 | - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); | |
| 287 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); | |
| 288 | - return; | |
| 289 | - } | |
| 290 | - | |
| 291 | - // 2. 获取设备详细信息 | |
| 292 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), | |
| 293 | - deviceInfo.getDeviceName()); | |
| 294 | - if (device == null) { | |
| 295 | - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", | |
| 296 | - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 297 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); | |
| 298 | - return; | |
| 299 | - } | |
| 300 | - | |
| 301 | - // 3. 更新设备会话信息(保持最新,保存 codecType) | |
| 302 | - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); | |
| 303 | - | |
| 304 | - // 4. 将 body 设置为实际的 params,发送消息到消息总线 | |
| 305 | - message.setParams(body); | |
| 306 | - deviceMessageService.sendDeviceMessage(message, device.getProductKey(), | |
| 307 | - device.getDeviceName(), serverId); | |
| 308 | - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", | |
| 309 | - device.getId(), message.getMethod(), addressKey); | |
| 310 | - } catch (Exception e) { | |
| 311 | - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e); | |
| 312 | - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType); | |
| 313 | - } | |
| 314 | - } | |
| 315 | - | |
| 316 | - /** | |
| 317 | - * 获取消息编解码类型 | |
| 318 | - * | |
| 319 | - * @param buffer 消息 | |
| 320 | - * @return 消息编解码类型 | |
| 321 | - */ | |
| 322 | - private String getMessageCodecType(Buffer buffer) { | |
| 323 | - // 检测消息格式类型 | |
| 324 | - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY | |
| 325 | - : CODEC_TYPE_JSON; | |
| 326 | - } | |
| 327 | - | |
| 328 | - /** | |
| 329 | - * 发送设备上线消息 | |
| 330 | - * | |
| 331 | - * @param device 设备信息 | |
| 332 | - */ | |
| 333 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 334 | - try { | |
| 335 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 336 | - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 337 | - device.getDeviceName(), serverId); | |
| 338 | - } catch (Exception e) { | |
| 339 | - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); | |
| 340 | - } | |
| 341 | - } | |
| 342 | - | |
| 343 | - /** | |
| 344 | - * 验证设备认证信息 | |
| 345 | - * | |
| 346 | - * @param authParams 认证参数 | |
| 347 | - * @return 是否认证成功 | |
| 348 | - */ | |
| 349 | - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { | |
| 350 | - try { | |
| 351 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 352 | - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) | |
| 353 | - .setPassword(authParams.getPassword())); | |
| 354 | - result.checkError(); | |
| 355 | - return BooleanUtil.isTrue(result.getData()); | |
| 356 | - } catch (Exception e) { | |
| 357 | - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); | |
| 358 | - return false; | |
| 359 | - } | |
| 360 | - } | |
| 361 | - | |
| 362 | - /** | |
| 363 | - * 发送认证成功响应(包含 token) | |
| 364 | - * | |
| 365 | - * @param socket UDP Socket | |
| 366 | - * @param address 目标地址 | |
| 367 | - * @param requestId 请求 ID | |
| 368 | - * @param token JWT Token | |
| 369 | - * @param codecType 消息编解码类型 | |
| 370 | - */ | |
| 371 | - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, | |
| 372 | - String requestId, String token, String codecType) { | |
| 373 | - try { | |
| 374 | - // 构建响应数据 | |
| 375 | - Object responseData = MapUtil.builder() | |
| 376 | - .put("success", true) | |
| 377 | - .put("token", token) | |
| 378 | - .put("message", "认证成功") | |
| 379 | - .build(); | |
| 380 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); | |
| 381 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 382 | - // 发送响应 | |
| 383 | - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { | |
| 384 | - if (result.failed()) { | |
| 385 | - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", | |
| 386 | - sessionManager.buildAddressKey(address), result.cause()); | |
| 387 | - } | |
| 388 | - }); | |
| 389 | - } catch (Exception e) { | |
| 390 | - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]", | |
| 391 | - sessionManager.buildAddressKey(address), e); | |
| 392 | - } | |
| 393 | - } | |
| 394 | - | |
| 395 | - /** | |
| 396 | - * 发送注册成功响应(包含 deviceSecret) | |
| 397 | - * | |
| 398 | - * @param socket UDP Socket | |
| 399 | - * @param address 目标地址 | |
| 400 | - * @param requestId 请求 ID | |
| 401 | - * @param registerResp 注册响应 | |
| 402 | - * @param codecType 消息编解码类型 | |
| 403 | - */ | |
| 404 | - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address, | |
| 405 | - String requestId, IotDeviceRegisterRespDTO registerResp, | |
| 406 | - String codecType) { | |
| 407 | - try { | |
| 408 | - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | |
| 409 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 410 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | |
| 411 | - // 2. 发送响应 | |
| 412 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 413 | - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { | |
| 414 | - if (result.failed()) { | |
| 415 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]", | |
| 416 | - sessionManager.buildAddressKey(address), result.cause()); | |
| 417 | - } | |
| 418 | - }); | |
| 419 | - } catch (Exception e) { | |
| 420 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]", | |
| 421 | - sessionManager.buildAddressKey(address), e); | |
| 422 | - } | |
| 423 | - } | |
| 424 | - | |
| 425 | - /** | |
| 426 | - * 发送错误响应 | |
| 427 | - * | |
| 428 | - * @param socket UDP Socket | |
| 429 | - * @param address 目标地址 | |
| 430 | - * @param requestId 请求 ID | |
| 431 | - * @param errorMessage 错误消息 | |
| 432 | - * @param codecType 消息编解码类型 | |
| 433 | - */ | |
| 434 | - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, | |
| 435 | - String requestId, String errorMessage, String codecType) { | |
| 436 | - sendResponse(socket, address, false, errorMessage, requestId, codecType); | |
| 437 | - } | |
| 438 | - | |
| 439 | - /** | |
| 440 | - * 发送响应消息 | |
| 441 | - * | |
| 442 | - * @param socket UDP Socket | |
| 443 | - * @param address 目标地址 | |
| 444 | - * @param success 是否成功 | |
| 445 | - * @param message 消息 | |
| 446 | - * @param requestId 请求 ID | |
| 447 | - * @param codecType 消息编解码类型 | |
| 448 | - */ | |
| 449 | - @SuppressWarnings("SameParameterValue") | |
| 450 | - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, | |
| 451 | - String message, String requestId, String codecType) { | |
| 452 | - try { | |
| 453 | - // 构建响应数据 | |
| 454 | - Object responseData = MapUtil.builder() | |
| 455 | - .put("success", success) | |
| 456 | - .put("message", message) | |
| 457 | - .build(); | |
| 458 | - int code = success ? 0 : 401; | |
| 459 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 460 | - "response", responseData, code, message); | |
| 461 | - | |
| 462 | - // 发送响应 | |
| 463 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | |
| 464 | - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { | |
| 465 | - if (ar.failed()) { | |
| 466 | - log.error("[sendResponse][发送响应失败,地址: {}]", | |
| 467 | - sessionManager.buildAddressKey(address), ar.cause()); | |
| 468 | - } | |
| 469 | - }); | |
| 470 | - } catch (Exception e) { | |
| 471 | - log.error("[sendResponse][发送响应异常,地址: {}]", | |
| 472 | - sessionManager.buildAddressKey(address), e); | |
| 473 | - } | |
| 474 | - } | |
| 475 | - | |
| 476 | - /** | |
| 477 | - * 解析认证参数 | |
| 478 | - * | |
| 479 | - * @param params 参数对象(通常为 Map 类型) | |
| 480 | - * @return 认证参数 DTO,解析失败时返回 null | |
| 481 | - */ | |
| 482 | - @SuppressWarnings("unchecked") | |
| 483 | - private IotDeviceAuthReqDTO parseAuthParams(Object params) { | |
| 484 | - if (params == null) { | |
| 485 | - return null; | |
| 486 | - } | |
| 487 | - try { | |
| 488 | - // 参数默认为 Map 类型,直接转换 | |
| 489 | - if (params instanceof Map) { | |
| 490 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 491 | - return new IotDeviceAuthReqDTO() | |
| 492 | - .setClientId(MapUtil.getStr(paramMap, "clientId")) | |
| 493 | - .setUsername(MapUtil.getStr(paramMap, "username")) | |
| 494 | - .setPassword(MapUtil.getStr(paramMap, "password")); | |
| 495 | - } | |
| 496 | - // 如果已经是目标类型,直接返回 | |
| 497 | - if (params instanceof IotDeviceAuthReqDTO) { | |
| 498 | - return (IotDeviceAuthReqDTO) params; | |
| 499 | - } | |
| 500 | - | |
| 501 | - // 其他情况尝试 JSON 转换 | |
| 502 | - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); | |
| 503 | - } catch (Exception e) { | |
| 504 | - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); | |
| 505 | - return null; | |
| 506 | - } | |
| 507 | - } | |
| 508 | - | |
| 509 | - /** | |
| 510 | - * 解析注册参数 | |
| 511 | - * | |
| 512 | - * @param params 参数对象(通常为 Map 类型) | |
| 513 | - * @return 注册参数 DTO,解析失败时返回 null | |
| 514 | - */ | |
| 515 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 516 | - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | |
| 517 | - if (params == null) { | |
| 518 | - return null; | |
| 519 | - } | |
| 520 | - try { | |
| 521 | - // 参数默认为 Map 类型,直接转换 | |
| 522 | - if (params instanceof Map) { | |
| 523 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 524 | - return new IotDeviceRegisterReqDTO() | |
| 525 | - .setProductKey(MapUtil.getStr(paramMap, "productKey")) | |
| 526 | - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | |
| 527 | - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | |
| 528 | - } | |
| 529 | - // 如果已经是目标类型,直接返回 | |
| 530 | - if (params instanceof IotDeviceRegisterReqDTO) { | |
| 531 | - return (IotDeviceRegisterReqDTO) params; | |
| 532 | - } | |
| 533 | - | |
| 534 | - // 其他情况尝试 JSON 转换 | |
| 535 | - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); | |
| 536 | - } catch (Exception e) { | |
| 537 | - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | |
| 538 | - return null; | |
| 539 | - } | |
| 540 | - } | |
| 541 | - | |
| 542 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.NotEmpty; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT WebSocket 协议配置 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +public class IotWebSocketConfig { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * WebSocket 路径(默认:/ws) | |
| 17 | + */ | |
| 18 | + @NotEmpty(message = "WebSocket 路径不能为空") | |
| 19 | + private String path = "/ws"; | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 最大消息大小(字节,默认 64KB) | |
| 23 | + */ | |
| 24 | + @NotNull(message = "最大消息大小不能为空") | |
| 25 | + private Integer maxMessageSize = 65536; | |
| 26 | + /** | |
| 27 | + * 最大帧大小(字节,默认 64KB) | |
| 28 | + */ | |
| 29 | + @NotNull(message = "最大帧大小不能为空") | |
| 30 | + private Integer maxFrameSize = 65536; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 空闲超时时间(秒,默认 60) | |
| 34 | + */ | |
| 35 | + @NotNull(message = "空闲超时时间不能为空") | |
| 36 | + private Integer idleTimeoutSeconds = 60; | |
| 37 | + | |
| 38 | +} | ... | ... |