Commit e275cb57e9519878557323924aef9c7a75c5f7f9
Merge branch 'feature/iot-protocol' of https://gitee.com/zhijiantianya/ruoyi-vue…
…-pro into feature/iot-modbus # Conflicts: # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java # yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
Showing
71 changed files
with
2877 additions
and
3161 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| @@ -75,7 +75,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | @@ -75,7 +75,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 75 | return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { | 75 | return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { |
| 76 | IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); | 76 | IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); |
| 77 | if (product != null) { | 77 | if (product != null) { |
| 78 | - deviceDTO.setCodecType(product.getCodecType()); | 78 | + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType()); |
| 79 | } | 79 | } |
| 80 | })); | 80 | })); |
| 81 | } | 81 | } |
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,10 +67,15 @@ public class IotProductRespVO { | ||
| 67 | @DictFormat(DictTypeConstants.NET_TYPE) | 67 | @DictFormat(DictTypeConstants.NET_TYPE) |
| 68 | private Integer netType; | 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 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 80 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 76 | @ExcelProperty("创建时间") | 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 | package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; | 1 | package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.validation.InEnum; | 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 | import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; | 6 | import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; |
| 5 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; | 7 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 6 | import io.swagger.v3.oas.annotations.media.Schema; | 8 | import io.swagger.v3.oas.annotations.media.Schema; |
| @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { | @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { | ||
| 44 | @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") | 46 | @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") |
| 45 | private Integer netType; | 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 | @Schema(description = "是否开启动态注册", example = "false") | 59 | @Schema(description = "是否开启动态注册", example = "false") |
| 52 | @NotNull(message = "是否开启动态注册不能为空") | 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,12 +78,16 @@ public class IotProductDO extends TenantBaseDO { | ||
| 78 | */ | 78 | */ |
| 79 | private Integer netType; | 79 | private Integer netType; |
| 80 | /** | 80 | /** |
| 81 | - * 数据格式(编解码器类型) | 81 | + * 协议类型 |
| 82 | * <p> | 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 | \ No newline at end of file | 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 +8,8 @@ package cn.iocoder.yudao.module.iot.enums; | ||
| 8 | public class DictTypeConstants { | 8 | public class DictTypeConstants { |
| 9 | 9 | ||
| 10 | public static final String NET_TYPE = "iot_net_type"; | 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 | public static final String PRODUCT_STATUS = "iot_product_status"; | 14 | public static final String PRODUCT_STATUS = "iot_product_status"; |
| 15 | public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; | 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,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; | ||
| 29 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; | 29 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 30 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; | 30 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; |
| 31 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 31 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 32 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | ||
| 32 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 33 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 33 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; | 34 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; |
| 34 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | 35 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { | @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { | ||
| 819 | if (BooleanUtil.isFalse(product.getRegisterEnabled())) { | 820 | if (BooleanUtil.isFalse(product.getRegisterEnabled())) { |
| 820 | throw exception(DEVICE_REGISTER_DISABLED); | 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 | throw exception(DEVICE_REGISTER_SECRET_INVALID); | 826 | throw exception(DEVICE_REGISTER_SECRET_INVALID); |
| 825 | } | 827 | } |
| 826 | return TenantUtils.execute(product.getTenantId(), () -> { | 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,8 +34,12 @@ public class IotDeviceRespDTO { | ||
| 34 | */ | 34 | */ |
| 35 | private Long productId; | 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 | \ No newline at end of file | 46 | \ No newline at end of file |
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,9 +27,11 @@ public class IotDeviceRegisterReqDTO { | ||
| 27 | private String deviceName; | 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/IotDeviceMessageCodec.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.codec; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 4 | - | ||
| 5 | -/** | ||
| 6 | - * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器 | ||
| 7 | - * | ||
| 8 | - * @author 芋道源码 | ||
| 9 | - */ | ||
| 10 | -public interface IotDeviceMessageCodec { | ||
| 11 | - | ||
| 12 | - /** | ||
| 13 | - * 编码消息 | ||
| 14 | - * | ||
| 15 | - * @param message 消息 | ||
| 16 | - * @return 编码后的消息内容 | ||
| 17 | - */ | ||
| 18 | - byte[] encode(IotDeviceMessage message); | ||
| 19 | - | ||
| 20 | - /** | ||
| 21 | - * 解码消息 | ||
| 22 | - * | ||
| 23 | - * @param bytes 消息内容 | ||
| 24 | - * @return 解码后的消息内容 | ||
| 25 | - */ | ||
| 26 | - IotDeviceMessage decode(byte[] bytes); | ||
| 27 | - | ||
| 28 | - /** | ||
| 29 | - * @return 数据格式(编码器类型) | ||
| 30 | - */ | ||
| 31 | - String type(); | ||
| 32 | - | ||
| 33 | -} |
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/tcp/IotTcpBinaryDeviceMessageCodec.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.core.util.IotDeviceMessageUtils; | ||
| 8 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 9 | -import io.vertx.core.buffer.Buffer; | ||
| 10 | -import lombok.extern.slf4j.Slf4j; | ||
| 11 | -import org.springframework.stereotype.Component; | ||
| 12 | - | ||
| 13 | -import java.nio.charset.StandardCharsets; | ||
| 14 | - | ||
| 15 | -/** | ||
| 16 | - * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器 | ||
| 17 | - * <p> | ||
| 18 | - * 二进制协议格式(所有数值使用大端序): | ||
| 19 | - * | ||
| 20 | - * <pre> | ||
| 21 | - * +--------+--------+--------+---------------------------+--------+--------+ | ||
| 22 | - * | 魔术字 | 版本号 | 消息类型| 消息长度(4 字节) | | ||
| 23 | - * +--------+--------+--------+---------------------------+--------+--------+ | ||
| 24 | - * | 消息 ID 长度(2 字节) | 消息 ID (变长字符串) | | ||
| 25 | - * +--------+--------+--------+--------+--------+--------+--------+--------+ | ||
| 26 | - * | 方法名长度(2 字节) | 方法名(变长字符串) | | ||
| 27 | - * +--------+--------+--------+--------+--------+--------+--------+--------+ | ||
| 28 | - * | 消息体数据(变长) | | ||
| 29 | - * +--------+--------+--------+--------+--------+--------+--------+--------+ | ||
| 30 | - * </pre> | ||
| 31 | - * <p> | ||
| 32 | - * 消息体格式: | ||
| 33 | - * - 请求消息:params 数据(JSON) | ||
| 34 | - * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) | ||
| 35 | - * <p> | ||
| 36 | - * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 | ||
| 37 | - * | ||
| 38 | - * @author 芋道源码 | ||
| 39 | - */ | ||
| 40 | -@Slf4j | ||
| 41 | -@Component | ||
| 42 | -public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { | ||
| 43 | - | ||
| 44 | - public static final String TYPE = "TcpBinary"; | ||
| 45 | - | ||
| 46 | - /** | ||
| 47 | - * 协议魔术字,用于协议识别 | ||
| 48 | - */ | ||
| 49 | - private static final byte MAGIC_NUMBER = (byte) 0x7E; | ||
| 50 | - | ||
| 51 | - /** | ||
| 52 | - * 协议版本号 | ||
| 53 | - */ | ||
| 54 | - private static final byte PROTOCOL_VERSION = (byte) 0x01; | ||
| 55 | - | ||
| 56 | - /** | ||
| 57 | - * 请求消息类型 | ||
| 58 | - */ | ||
| 59 | - private static final byte REQUEST = (byte) 0x01; | ||
| 60 | - | ||
| 61 | - /** | ||
| 62 | - * 响应消息类型 | ||
| 63 | - */ | ||
| 64 | - private static final byte RESPONSE = (byte) 0x02; | ||
| 65 | - | ||
| 66 | - /** | ||
| 67 | - * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度) | ||
| 68 | - */ | ||
| 69 | - private static final int HEADER_FIXED_LENGTH = 7; | ||
| 70 | - | ||
| 71 | - /** | ||
| 72 | - * 最小消息长度(头部 + 消息ID长度 + 方法名长度) | ||
| 73 | - */ | ||
| 74 | - private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; | ||
| 75 | - | ||
| 76 | - @Override | ||
| 77 | - public String type() { | ||
| 78 | - return TYPE; | ||
| 79 | - } | ||
| 80 | - | ||
| 81 | - @Override | ||
| 82 | - public byte[] encode(IotDeviceMessage message) { | ||
| 83 | - Assert.notNull(message, "消息不能为空"); | ||
| 84 | - Assert.notBlank(message.getMethod(), "消息方法不能为空"); | ||
| 85 | - try { | ||
| 86 | - // 1. 确定消息类型 | ||
| 87 | - byte messageType = determineMessageType(message); | ||
| 88 | - // 2. 构建消息体 | ||
| 89 | - byte[] bodyData = buildMessageBody(message, messageType); | ||
| 90 | - // 3. 构建完整消息 | ||
| 91 | - return buildCompleteMessage(message, messageType, bodyData); | ||
| 92 | - } catch (Exception e) { | ||
| 93 | - log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); | ||
| 94 | - throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); | ||
| 95 | - } | ||
| 96 | - } | ||
| 97 | - | ||
| 98 | - @Override | ||
| 99 | - public IotDeviceMessage decode(byte[] bytes) { | ||
| 100 | - Assert.notNull(bytes, "待解码数据不能为空"); | ||
| 101 | - Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); | ||
| 102 | - try { | ||
| 103 | - Buffer buffer = Buffer.buffer(bytes); | ||
| 104 | - // 解析协议头部和消息内容 | ||
| 105 | - int index = 0; | ||
| 106 | - // 1. 验证魔术字 | ||
| 107 | - byte magic = buffer.getByte(index++); | ||
| 108 | - Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); | ||
| 109 | - | ||
| 110 | - // 2. 验证版本号 | ||
| 111 | - byte version = buffer.getByte(index++); | ||
| 112 | - Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); | ||
| 113 | - | ||
| 114 | - // 3. 读取消息类型 | ||
| 115 | - byte messageType = buffer.getByte(index++); | ||
| 116 | - // 直接验证消息类型,无需抽取方法 | ||
| 117 | - Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, | ||
| 118 | - "无效的消息类型: " + messageType); | ||
| 119 | - | ||
| 120 | - // 4. 读取消息长度 | ||
| 121 | - int messageLength = buffer.getInt(index); | ||
| 122 | - index += 4; | ||
| 123 | - Assert.isTrue(messageLength == buffer.length(), | ||
| 124 | - "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); | ||
| 125 | - | ||
| 126 | - // 5. 读取消息 ID | ||
| 127 | - short messageIdLength = buffer.getShort(index); | ||
| 128 | - index += 2; | ||
| 129 | - String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); | ||
| 130 | - index += messageIdLength; | ||
| 131 | - | ||
| 132 | - // 6. 读取方法名 | ||
| 133 | - short methodLength = buffer.getShort(index); | ||
| 134 | - index += 2; | ||
| 135 | - String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); | ||
| 136 | - index += methodLength; | ||
| 137 | - | ||
| 138 | - // 7. 解析消息体 | ||
| 139 | - return parseMessageBody(buffer, index, messageType, messageId, method); | ||
| 140 | - } catch (Exception e) { | ||
| 141 | - log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); | ||
| 142 | - throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); | ||
| 143 | - } | ||
| 144 | - } | ||
| 145 | - | ||
| 146 | - /** | ||
| 147 | - * 确定消息类型 | ||
| 148 | - * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 | ||
| 149 | - */ | ||
| 150 | - private byte determineMessageType(IotDeviceMessage message) { | ||
| 151 | - // 判断是否为响应消息:有响应码或响应消息时为响应 | ||
| 152 | - if (message.getCode() != null) { | ||
| 153 | - return RESPONSE; | ||
| 154 | - } | ||
| 155 | - // 默认为请求消息 | ||
| 156 | - return REQUEST; | ||
| 157 | - } | ||
| 158 | - | ||
| 159 | - /** | ||
| 160 | - * 构建消息体 | ||
| 161 | - */ | ||
| 162 | - private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { | ||
| 163 | - Buffer bodyBuffer = Buffer.buffer(); | ||
| 164 | - if (messageType == RESPONSE) { | ||
| 165 | - // code | ||
| 166 | - bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); | ||
| 167 | - // msg | ||
| 168 | - String msg = message.getMsg() != null ? message.getMsg() : ""; | ||
| 169 | - byte[] msgBytes = StrUtil.utf8Bytes(msg); | ||
| 170 | - bodyBuffer.appendShort((short) msgBytes.length); | ||
| 171 | - bodyBuffer.appendBytes(msgBytes); | ||
| 172 | - // data | ||
| 173 | - if (message.getData() != null) { | ||
| 174 | - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); | ||
| 175 | - } | ||
| 176 | - } else { | ||
| 177 | - // 请求消息只处理 params 参数 | ||
| 178 | - // TODO @haohao:如果为空,是不是得写个长度 0 哈? | ||
| 179 | - if (message.getParams() != null) { | ||
| 180 | - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); | ||
| 181 | - } | ||
| 182 | - } | ||
| 183 | - return bodyBuffer.getBytes(); | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - /** | ||
| 187 | - * 构建完整消息 | ||
| 188 | - */ | ||
| 189 | - private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { | ||
| 190 | - Buffer buffer = Buffer.buffer(); | ||
| 191 | - // 1. 写入协议头部 | ||
| 192 | - buffer.appendByte(MAGIC_NUMBER); | ||
| 193 | - buffer.appendByte(PROTOCOL_VERSION); | ||
| 194 | - buffer.appendByte(messageType); | ||
| 195 | - // 2. 预留消息长度位置(在 5. 更新消息长度) | ||
| 196 | - int lengthPosition = buffer.length(); | ||
| 197 | - buffer.appendInt(0); | ||
| 198 | - // 3. 写入消息 ID | ||
| 199 | - String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() | ||
| 200 | - : IotDeviceMessageUtils.generateMessageId(); | ||
| 201 | - byte[] messageIdBytes = StrUtil.utf8Bytes(messageId); | ||
| 202 | - buffer.appendShort((short) messageIdBytes.length); | ||
| 203 | - buffer.appendBytes(messageIdBytes); | ||
| 204 | - // 4. 写入方法名 | ||
| 205 | - byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod()); | ||
| 206 | - buffer.appendShort((short) methodBytes.length); | ||
| 207 | - buffer.appendBytes(methodBytes); | ||
| 208 | - // 5. 写入消息体 | ||
| 209 | - buffer.appendBytes(bodyData); | ||
| 210 | - // 6. 更新消息长度 | ||
| 211 | - buffer.setInt(lengthPosition, buffer.length()); | ||
| 212 | - return buffer.getBytes(); | ||
| 213 | - } | ||
| 214 | - | ||
| 215 | - /** | ||
| 216 | - * 解析消息体 | ||
| 217 | - */ | ||
| 218 | - private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, | ||
| 219 | - String messageId, String method) { | ||
| 220 | - if (startIndex >= buffer.length()) { | ||
| 221 | - // 空消息体 | ||
| 222 | - return IotDeviceMessage.of(messageId, method, null, null, null, null); | ||
| 223 | - } | ||
| 224 | - | ||
| 225 | - if (messageType == RESPONSE) { | ||
| 226 | - // 响应消息:解析 code + msg + data | ||
| 227 | - return parseResponseMessage(buffer, startIndex, messageId, method); | ||
| 228 | - } else { | ||
| 229 | - // 请求消息:解析 payload | ||
| 230 | - Object payload = parseJsonData(buffer, startIndex, buffer.length()); | ||
| 231 | - return IotDeviceMessage.of(messageId, method, payload, null, null, null); | ||
| 232 | - } | ||
| 233 | - } | ||
| 234 | - | ||
| 235 | - /** | ||
| 236 | - * 解析响应消息 | ||
| 237 | - */ | ||
| 238 | - private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { | ||
| 239 | - int index = startIndex; | ||
| 240 | - | ||
| 241 | - // 1. 读取响应码 | ||
| 242 | - Integer code = buffer.getInt(index); | ||
| 243 | - index += 4; | ||
| 244 | - | ||
| 245 | - // 2. 读取响应消息 | ||
| 246 | - short msgLength = buffer.getShort(index); | ||
| 247 | - index += 2; | ||
| 248 | - String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; | ||
| 249 | - index += msgLength; | ||
| 250 | - | ||
| 251 | - // 3. 读取响应数据 | ||
| 252 | - Object data = null; | ||
| 253 | - if (index < buffer.length()) { | ||
| 254 | - data = parseJsonData(buffer, index, buffer.length()); | ||
| 255 | - } | ||
| 256 | - | ||
| 257 | - return IotDeviceMessage.of(messageId, method, null, data, code, msg); | ||
| 258 | - } | ||
| 259 | - | ||
| 260 | - /** | ||
| 261 | - * 解析 JSON 数据 | ||
| 262 | - */ | ||
| 263 | - private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { | ||
| 264 | - if (startIndex >= endIndex) { | ||
| 265 | - return null; | ||
| 266 | - } | ||
| 267 | - try { | ||
| 268 | - String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); | ||
| 269 | - return JsonUtils.parseObject(jsonStr, Object.class); | ||
| 270 | - } catch (Exception e) { | ||
| 271 | - log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); | ||
| 272 | - return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); | ||
| 273 | - } | ||
| 274 | - } | ||
| 275 | - | ||
| 276 | - /** | ||
| 277 | - * 快速检测是否为二进制格式 | ||
| 278 | - * | ||
| 279 | - * @param data 数据 | ||
| 280 | - * @return 是否为二进制格式 | ||
| 281 | - */ | ||
| 282 | - public static boolean isBinaryFormatQuick(byte[] data) { | ||
| 283 | - return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; | ||
| 284 | - } | ||
| 285 | - | ||
| 286 | -} | ||
| 287 | \ No newline at end of file | 0 | \ No newline at end of file |
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 = "TcpJson"; | ||
| 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
| @@ -17,6 +17,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr | @@ -17,6 +17,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr | ||
| 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 19 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | 19 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; |
| 20 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpDownstreamSubscriber; | ||
| 21 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpUpstreamProtocol; | ||
| 22 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; | ||
| 23 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; | ||
| 24 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; | ||
| 25 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; | ||
| 26 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; | ||
| 27 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; | ||
| 28 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; | ||
| 20 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | 29 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; |
| 21 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 30 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 22 | import io.vertx.core.Vertx; | 31 | import io.vertx.core.Vertx; |
| @@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j; | @@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j; | ||
| 24 | import org.redisson.api.RedissonClient; | 33 | import org.redisson.api.RedissonClient; |
| 25 | import org.springframework.beans.factory.annotation.Qualifier; | 34 | import org.springframework.beans.factory.annotation.Qualifier; |
| 26 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | 35 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 36 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 37 | +import io.vertx.core.Vertx; | ||
| 38 | +import lombok.extern.slf4j.Slf4j; | ||
| 39 | +import org.redisson.api.RedissonClient; | ||
| 40 | +import org.springframework.beans.factory.annotation.Qualifier; | ||
| 41 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| 27 | import org.springframework.boot.context.properties.EnableConfigurationProperties; | 42 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 28 | import org.springframework.context.annotation.Bean; | 43 | import org.springframework.context.annotation.Bean; |
| 29 | import org.springframework.context.annotation.Configuration; | 44 | import org.springframework.context.annotation.Configuration; |
| 30 | 45 | ||
| 31 | - | ||
| 32 | @Configuration | 46 | @Configuration |
| 33 | @EnableConfigurationProperties(IotGatewayProperties.class) | 47 | @EnableConfigurationProperties(IotGatewayProperties.class) |
| 34 | -@Slf4j | ||
| 35 | public class IotGatewayConfiguration { | 48 | public class IotGatewayConfiguration { |
| 36 | 49 | ||
| 37 | @Bean | 50 | @Bean |
| @@ -45,75 +58,6 @@ public class IotGatewayConfiguration { | @@ -45,75 +58,6 @@ public class IotGatewayConfiguration { | ||
| 45 | } | 58 | } |
| 46 | 59 | ||
| 47 | /** | 60 | /** |
| 48 | - * IoT 网关 EMQX 协议配置类 | ||
| 49 | - */ | ||
| 50 | - @Configuration | ||
| 51 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") | ||
| 52 | - @Slf4j | ||
| 53 | - public static class EmqxProtocolConfiguration { | ||
| 54 | - | ||
| 55 | - @Bean(name = "emqxVertx", destroyMethod = "close") | ||
| 56 | - public Vertx emqxVertx() { | ||
| 57 | - return Vertx.vertx(); | ||
| 58 | - } | ||
| 59 | - | ||
| 60 | - @Bean | ||
| 61 | - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, | ||
| 62 | - @Qualifier("emqxVertx") Vertx emqxVertx) { | ||
| 63 | - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); | ||
| 64 | - } | ||
| 65 | - | ||
| 66 | - @Bean | ||
| 67 | - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, | ||
| 68 | - @Qualifier("emqxVertx") Vertx emqxVertx) { | ||
| 69 | - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - @Bean | ||
| 73 | - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, | ||
| 74 | - IotMessageBus messageBus) { | ||
| 75 | - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); | ||
| 76 | - } | ||
| 77 | - } | ||
| 78 | - | ||
| 79 | - /** | ||
| 80 | - * IoT 网关 MQTT 协议配置类 | ||
| 81 | - */ | ||
| 82 | - @Configuration | ||
| 83 | - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") | ||
| 84 | - @Slf4j | ||
| 85 | - public static class MqttProtocolConfiguration { | ||
| 86 | - | ||
| 87 | - @Bean(name = "mqttVertx", destroyMethod = "close") | ||
| 88 | - public Vertx mqttVertx() { | ||
| 89 | - return Vertx.vertx(); | ||
| 90 | - } | ||
| 91 | - | ||
| 92 | - @Bean | ||
| 93 | - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, | ||
| 94 | - IotDeviceMessageService messageService, | ||
| 95 | - IotMqttConnectionManager connectionManager, | ||
| 96 | - @Qualifier("mqttVertx") Vertx mqttVertx) { | ||
| 97 | - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, | ||
| 98 | - connectionManager, mqttVertx); | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - @Bean | ||
| 102 | - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, | ||
| 103 | - IotMqttConnectionManager connectionManager) { | ||
| 104 | - return new IotMqttDownstreamHandler(messageService, connectionManager); | ||
| 105 | - } | ||
| 106 | - | ||
| 107 | - @Bean | ||
| 108 | - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, | ||
| 109 | - IotMqttDownstreamHandler downstreamHandler, | ||
| 110 | - IotMessageBus messageBus) { | ||
| 111 | - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); | ||
| 112 | - } | ||
| 113 | - | ||
| 114 | - } | ||
| 115 | - | ||
| 116 | - /** | ||
| 117 | * IoT 网关 Modbus TCP 协议配置类 | 61 | * IoT 网关 Modbus TCP 协议配置类 |
| 118 | */ | 62 | */ |
| 119 | @Configuration | 63 | @Configuration |
| @@ -188,8 +132,8 @@ public class IotGatewayConfiguration { | @@ -188,8 +132,8 @@ public class IotGatewayConfiguration { | ||
| 188 | 132 | ||
| 189 | @Bean | 133 | @Bean |
| 190 | public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, | 134 | public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, |
| 191 | - IotModbusTcpDownstreamHandler downstreamHandler, | ||
| 192 | - IotMessageBus messageBus) { | 135 | + IotModbusTcpDownstreamHandler downstreamHandler, |
| 136 | + IotMessageBus messageBus) { | ||
| 193 | return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); | 137 | return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); |
| 194 | } | 138 | } |
| 195 | 139 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.iot.gateway.config; | @@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.iot.gateway.config; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; | ||
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; | ||
| 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; |
| 9 | -import io.vertx.core.net.KeyCertOptions; | ||
| 10 | -import io.vertx.core.net.TrustOptions; | ||
| 11 | import jakarta.validation.Valid; | 11 | import jakarta.validation.Valid; |
| 12 | import jakarta.validation.constraints.NotEmpty; | 12 | import jakarta.validation.constraints.NotEmpty; |
| 13 | import jakarta.validation.constraints.NotNull; | 13 | import jakarta.validation.constraints.NotNull; |
| @@ -33,14 +33,9 @@ public class IotGatewayProperties { | @@ -33,14 +33,9 @@ public class IotGatewayProperties { | ||
| 33 | private TokenProperties token; | 33 | private TokenProperties token; |
| 34 | 34 | ||
| 35 | /** | 35 | /** |
| 36 | - * 协议配置(旧版,保持兼容) | ||
| 37 | - */ | ||
| 38 | - private ProtocolProperties protocol; | ||
| 39 | - | ||
| 40 | - /** | ||
| 41 | * 协议实例列表 | 36 | * 协议实例列表 |
| 42 | */ | 37 | */ |
| 43 | - private List<ProtocolInstanceProperties> protocols; | 38 | + private List<ProtocolProperties> protocols; |
| 44 | 39 | ||
| 45 | @Data | 40 | @Data |
| 46 | public static class RpcProperties { | 41 | public static class RpcProperties { |
| @@ -79,367 +74,155 @@ public class IotGatewayProperties { | @@ -79,367 +74,155 @@ public class IotGatewayProperties { | ||
| 79 | 74 | ||
| 80 | } | 75 | } |
| 81 | 76 | ||
| 77 | + /** | ||
| 78 | + * 协议实例配置 | ||
| 79 | + */ | ||
| 82 | @Data | 80 | @Data |
| 83 | public static class ProtocolProperties { | 81 | public static class ProtocolProperties { |
| 84 | 82 | ||
| 85 | /** | 83 | /** |
| 86 | - * EMQX 组件配置 | ||
| 87 | - */ | ||
| 88 | - private EmqxProperties emqx; | ||
| 89 | - | ||
| 90 | - /** | ||
| 91 | - * MQTT 组件配置 | 84 | + * 协议实例 ID,如 "http-alink"、"tcp-binary" |
| 92 | */ | 85 | */ |
| 93 | - private MqttProperties mqtt; | ||
| 94 | - | 86 | + @NotEmpty(message = "协议实例 ID 不能为空") |
| 87 | + private String id; | ||
| 95 | /** | 88 | /** |
| 96 | - * Modbus TCP 组件配置 | 89 | + * 是否启用 |
| 97 | */ | 90 | */ |
| 98 | - private ModbusTcpProperties modbusTcp; | ||
| 99 | - | ||
| 100 | - } | ||
| 101 | - | ||
| 102 | - @Data | ||
| 103 | - public static class HttpProperties { | ||
| 104 | - | 91 | + @NotNull(message = "是否启用不能为空") |
| 92 | + private Boolean enabled = true; | ||
| 105 | /** | 93 | /** |
| 106 | - * 是否开启 | 94 | + * 协议类型 |
| 95 | + * | ||
| 96 | + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum | ||
| 107 | */ | 97 | */ |
| 108 | - @NotNull(message = "是否开启不能为空") | ||
| 109 | - private Boolean enabled; | 98 | + @NotEmpty(message = "协议类型不能为空") |
| 99 | + private String protocol; | ||
| 110 | /** | 100 | /** |
| 111 | * 服务端口 | 101 | * 服务端口 |
| 112 | - */ | ||
| 113 | - private Integer serverPort; | ||
| 114 | - | ||
| 115 | - /** | ||
| 116 | - * 是否开启 SSL | ||
| 117 | - */ | ||
| 118 | - @NotNull(message = "是否开启 SSL 不能为空") | ||
| 119 | - private Boolean sslEnabled = false; | ||
| 120 | - | ||
| 121 | - /** | ||
| 122 | - * SSL 证书路径 | ||
| 123 | - */ | ||
| 124 | - private String sslKeyPath; | ||
| 125 | - /** | ||
| 126 | - * SSL 证书路径 | ||
| 127 | - */ | ||
| 128 | - private String sslCertPath; | ||
| 129 | - | ||
| 130 | - } | ||
| 131 | - | ||
| 132 | - @Data | ||
| 133 | - public static class EmqxProperties { | ||
| 134 | - | ||
| 135 | - /** | ||
| 136 | - * 是否开启 | ||
| 137 | - */ | ||
| 138 | - @NotNull(message = "是否开启不能为空") | ||
| 139 | - private Boolean enabled; | ||
| 140 | - | ||
| 141 | - /** | ||
| 142 | - * HTTP 服务端口(默认:8090) | ||
| 143 | - */ | ||
| 144 | - private Integer httpPort = 8090; | ||
| 145 | - | ||
| 146 | - /** | ||
| 147 | - * MQTT 服务器地址 | ||
| 148 | - */ | ||
| 149 | - @NotEmpty(message = "MQTT 服务器地址不能为空") | ||
| 150 | - private String mqttHost; | ||
| 151 | - | ||
| 152 | - /** | ||
| 153 | - * MQTT 服务器端口(默认:1883) | ||
| 154 | - */ | ||
| 155 | - @NotNull(message = "MQTT 服务器端口不能为空") | ||
| 156 | - private Integer mqttPort = 1883; | ||
| 157 | - | ||
| 158 | - /** | ||
| 159 | - * MQTT 用户名 | ||
| 160 | - */ | ||
| 161 | - @NotEmpty(message = "MQTT 用户名不能为空") | ||
| 162 | - private String mqttUsername; | ||
| 163 | - | ||
| 164 | - /** | ||
| 165 | - * MQTT 密码 | ||
| 166 | - */ | ||
| 167 | - @NotEmpty(message = "MQTT 密码不能为空") | ||
| 168 | - private String mqttPassword; | ||
| 169 | - | ||
| 170 | - /** | ||
| 171 | - * MQTT 客户端的 SSL 开关 | ||
| 172 | - */ | ||
| 173 | - @NotNull(message = "MQTT 是否开启 SSL 不能为空") | ||
| 174 | - private Boolean mqttSsl = false; | ||
| 175 | - | ||
| 176 | - /** | ||
| 177 | - * MQTT 客户端 ID(如果为空,系统将自动生成) | ||
| 178 | - */ | ||
| 179 | - @NotEmpty(message = "MQTT 客户端 ID 不能为空") | ||
| 180 | - private String mqttClientId; | ||
| 181 | - | ||
| 182 | - /** | ||
| 183 | - * MQTT 订阅的主题 | ||
| 184 | - */ | ||
| 185 | - @NotEmpty(message = "MQTT 主题不能为空") | ||
| 186 | - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; | ||
| 187 | - | ||
| 188 | - /** | ||
| 189 | - * 默认 QoS 级别 | ||
| 190 | * <p> | 102 | * <p> |
| 191 | - * 0 - 最多一次 | ||
| 192 | - * 1 - 至少一次 | ||
| 193 | - * 2 - 刚好一次 | 103 | + * 不同协议含义不同: |
| 104 | + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口 | ||
| 105 | + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event) | ||
| 194 | */ | 106 | */ |
| 195 | - private Integer mqttQos = 1; | ||
| 196 | - | 107 | + @NotNull(message = "服务端口不能为空") |
| 108 | + private Integer port; | ||
| 197 | /** | 109 | /** |
| 198 | - * 连接超时时间(秒) | 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} 协议,目前支持根据产品(设备)配置的序列化类型来解析 | ||
| 199 | */ | 117 | */ |
| 200 | - private Integer connectTimeoutSeconds = 10; | 118 | + private String serialize; |
| 201 | 119 | ||
| 202 | - /** | ||
| 203 | - * 重连延迟时间(毫秒) | ||
| 204 | - */ | ||
| 205 | - private Long reconnectDelayMs = 5000L; | 120 | + // ========== SSL 配置 ========== |
| 206 | 121 | ||
| 207 | /** | 122 | /** |
| 208 | - * 是否启用 Clean Session (清理会话) | ||
| 209 | - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 | ||
| 210 | - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 | 123 | + * SSL 配置(可选,配置文件中不配置则为 null) |
| 211 | */ | 124 | */ |
| 212 | - private Boolean cleanSession = true; | 125 | + @Valid |
| 126 | + private SslConfig ssl; | ||
| 127 | + | ||
| 128 | + // ========== 各协议配置 ========== | ||
| 213 | 129 | ||
| 214 | /** | 130 | /** |
| 215 | - * 心跳间隔(秒) | ||
| 216 | - * 用于保持连接活性,及时发现网络中断。 | 131 | + * HTTP 协议配置 |
| 217 | */ | 132 | */ |
| 218 | - private Integer keepAliveIntervalSeconds = 60; | ||
| 219 | - | 133 | + @Valid |
| 134 | + private IotHttpConfig http; | ||
| 220 | /** | 135 | /** |
| 221 | - * 最大未确认消息队列大小 | ||
| 222 | - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 | 136 | + * WebSocket 协议配置 |
| 223 | */ | 137 | */ |
| 224 | - private Integer maxInflightQueue = 10000; | 138 | + @Valid |
| 139 | + private IotWebSocketConfig websocket; | ||
| 225 | 140 | ||
| 226 | /** | 141 | /** |
| 227 | - * 是否信任所有 SSL 证书 | ||
| 228 | - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! | ||
| 229 | - * 在生产环境中,应设置为 false,并配置正确的信任库。 | 142 | + * TCP 协议配置 |
| 230 | */ | 143 | */ |
| 231 | - private Boolean trustAll = false; | ||
| 232 | - | 144 | + @Valid |
| 145 | + private IotTcpConfig tcp; | ||
| 233 | /** | 146 | /** |
| 234 | - * 遗嘱消息配置 (用于网关异常下线时通知其他系统) | 147 | + * UDP 协议配置 |
| 235 | */ | 148 | */ |
| 236 | - private final Will will = new Will(); | 149 | + @Valid |
| 150 | + private IotUdpConfig udp; | ||
| 237 | 151 | ||
| 238 | /** | 152 | /** |
| 239 | - * 高级 SSL/TLS 配置 (用于生产环境) | 153 | + * CoAP 协议配置 |
| 240 | */ | 154 | */ |
| 241 | - private final Ssl sslOptions = new Ssl(); | 155 | + @Valid |
| 156 | + private IotCoapConfig coap; | ||
| 242 | 157 | ||
| 243 | /** | 158 | /** |
| 244 | - * 遗嘱消息 (Last Will and Testament) | 159 | + * MQTT 协议配置 |
| 245 | */ | 160 | */ |
| 246 | - @Data | ||
| 247 | - public static class Will { | ||
| 248 | - | ||
| 249 | - /** | ||
| 250 | - * 是否启用遗嘱消息 | ||
| 251 | - */ | ||
| 252 | - private boolean enabled = false; | ||
| 253 | - /** | ||
| 254 | - * 遗嘱消息主题 | ||
| 255 | - */ | ||
| 256 | - private String topic; | ||
| 257 | - /** | ||
| 258 | - * 遗嘱消息内容 | ||
| 259 | - */ | ||
| 260 | - private String payload; | ||
| 261 | - /** | ||
| 262 | - * 遗嘱消息 QoS 等级 | ||
| 263 | - */ | ||
| 264 | - private Integer qos = 1; | ||
| 265 | - /** | ||
| 266 | - * 遗嘱消息是否作为保留消息发布 | ||
| 267 | - */ | ||
| 268 | - private boolean retain = true; | ||
| 269 | - | ||
| 270 | - } | ||
| 271 | - | 161 | + @Valid |
| 162 | + private IotMqttConfig mqtt; | ||
| 272 | /** | 163 | /** |
| 273 | - * 高级 SSL/TLS 配置 | 164 | + * EMQX 协议配置 |
| 274 | */ | 165 | */ |
| 275 | - @Data | ||
| 276 | - public static class Ssl { | ||
| 277 | - | ||
| 278 | - /** | ||
| 279 | - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks | ||
| 280 | - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 | ||
| 281 | - */ | ||
| 282 | - private String keyStorePath; | ||
| 283 | - /** | ||
| 284 | - * 密钥库密码 | ||
| 285 | - */ | ||
| 286 | - private String keyStorePassword; | ||
| 287 | - /** | ||
| 288 | - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks | ||
| 289 | - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 | ||
| 290 | - */ | ||
| 291 | - private String trustStorePath; | ||
| 292 | - /** | ||
| 293 | - * 信任库密码 | ||
| 294 | - */ | ||
| 295 | - private String trustStorePassword; | ||
| 296 | - | ||
| 297 | - } | 166 | + @Valid |
| 167 | + private IotEmqxConfig emqx; | ||
| 298 | 168 | ||
| 299 | } | 169 | } |
| 300 | 170 | ||
| 171 | + /** | ||
| 172 | + * SSL 配置 | ||
| 173 | + */ | ||
| 301 | @Data | 174 | @Data |
| 302 | - public static class MqttProperties { | 175 | + public static class SslConfig { |
| 303 | 176 | ||
| 304 | /** | 177 | /** |
| 305 | - * 是否开启 | 178 | + * 是否启用 SSL |
| 306 | */ | 179 | */ |
| 307 | - @NotNull(message = "是否开启不能为空") | ||
| 308 | - private Boolean enabled; | 180 | + @NotNull(message = "是否启用 SSL 不能为空") |
| 181 | + private Boolean ssl = false; | ||
| 309 | 182 | ||
| 310 | /** | 183 | /** |
| 311 | - * 服务器端口 | 184 | + * SSL 证书路径 |
| 312 | */ | 185 | */ |
| 313 | - private Integer port = 1883; | 186 | + @NotEmpty(message = "SSL 证书路径不能为空") |
| 187 | + private String sslCertPath; | ||
| 314 | 188 | ||
| 315 | /** | 189 | /** |
| 316 | - * 最大消息大小(字节) | 190 | + * SSL 私钥路径 |
| 317 | */ | 191 | */ |
| 318 | - private Integer maxMessageSize = 8192; | 192 | + @NotEmpty(message = "SSL 私钥路径不能为空") |
| 193 | + private String sslKeyPath; | ||
| 319 | 194 | ||
| 320 | /** | 195 | /** |
| 321 | - * 连接超时时间(秒) | 196 | + * 密钥库(KeyStore)路径 |
| 197 | + * <p> | ||
| 198 | + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证) | ||
| 322 | */ | 199 | */ |
| 323 | - private Integer connectTimeoutSeconds = 60; | 200 | + private String keyStorePath; |
| 324 | /** | 201 | /** |
| 325 | - * 保持连接超时时间(秒) | 202 | + * 密钥库密码 |
| 326 | */ | 203 | */ |
| 327 | - private Integer keepAliveTimeoutSeconds = 300; | 204 | + private String keyStorePassword; |
| 328 | 205 | ||
| 329 | - // NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低) | ||
| 330 | - /** | ||
| 331 | - * 是否启用 SSL | ||
| 332 | - */ | ||
| 333 | - private Boolean sslEnabled = false; | ||
| 334 | /** | 206 | /** |
| 335 | - * SSL 配置 | 207 | + * 信任库(TrustStore)路径 |
| 208 | + * <p> | ||
| 209 | + * 包含服务端信任的 CA 证书,用于验证服务端的身份 | ||
| 336 | */ | 210 | */ |
| 337 | - private SslOptions sslOptions = new SslOptions(); | ||
| 338 | - | 211 | + private String trustStorePath; |
| 339 | /** | 212 | /** |
| 340 | - * SSL 配置选项 | 213 | + * 信任库密码 |
| 341 | */ | 214 | */ |
| 342 | - @Data | ||
| 343 | - public static class SslOptions { | ||
| 344 | - | ||
| 345 | - /** | ||
| 346 | - * 密钥证书选项 | ||
| 347 | - */ | ||
| 348 | - private KeyCertOptions keyCertOptions; | ||
| 349 | - /** | ||
| 350 | - * 信任选项 | ||
| 351 | - */ | ||
| 352 | - private TrustOptions trustOptions; | ||
| 353 | - /** | ||
| 354 | - * SSL 证书路径 | ||
| 355 | - */ | ||
| 356 | - private String certPath; | ||
| 357 | - /** | ||
| 358 | - * SSL 私钥路径 | ||
| 359 | - */ | ||
| 360 | - private String keyPath; | ||
| 361 | - /** | ||
| 362 | - * 信任存储路径 | ||
| 363 | - */ | ||
| 364 | - private String trustStorePath; | ||
| 365 | - /** | ||
| 366 | - * 信任存储密码 | ||
| 367 | - */ | ||
| 368 | - private String trustStorePassword; | ||
| 369 | - | ||
| 370 | - } | 215 | + private String trustStorePassword; |
| 371 | 216 | ||
| 372 | } | 217 | } |
| 373 | 218 | ||
| 374 | - // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整 | ||
| 375 | - /** | ||
| 376 | - * 协议实例配置 | ||
| 377 | - */ | ||
| 378 | @Data | 219 | @Data |
| 379 | - public static class ProtocolInstanceProperties { | ||
| 380 | - | ||
| 381 | - /** | ||
| 382 | - * 协议实例 ID,如 "http-alink"、"tcp-binary" | ||
| 383 | - */ | ||
| 384 | - @NotEmpty(message = "协议实例 ID 不能为空") | ||
| 385 | - private String id; | ||
| 386 | - /** | ||
| 387 | - * 是否启用 | ||
| 388 | - */ | ||
| 389 | - @NotNull(message = "是否启用不能为空") | ||
| 390 | - private Boolean enabled = true; | ||
| 391 | - /** | ||
| 392 | - * 协议类型 | ||
| 393 | - * | ||
| 394 | - * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum | ||
| 395 | - */ | ||
| 396 | - @NotEmpty(message = "协议类型不能为空") | ||
| 397 | - private String type; | ||
| 398 | - /** | ||
| 399 | - * 服务端口 | ||
| 400 | - */ | ||
| 401 | - @NotNull(message = "服务端口不能为空") | ||
| 402 | - private Integer port; | ||
| 403 | - /** | ||
| 404 | - * 序列化类型(可选) | ||
| 405 | - * | ||
| 406 | - * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum | ||
| 407 | - * | ||
| 408 | - * 为什么是可选的呢? | ||
| 409 | - * 1. {@link IotProtocolTypeEnum#HTTP}、${@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 | ||
| 410 | - * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 | ||
| 411 | - */ | ||
| 412 | - private String serialize; | ||
| 413 | - | ||
| 414 | - /** | ||
| 415 | - * HTTP 协议配置 | ||
| 416 | - */ | ||
| 417 | - @Valid | ||
| 418 | - private IotHttpConfig http; | ||
| 419 | - | ||
| 420 | - /** | ||
| 421 | - * TCP 协议配置 | ||
| 422 | - */ | ||
| 423 | - @Valid | ||
| 424 | - private IotTcpConfig tcp; | ||
| 425 | - | ||
| 426 | - /** | ||
| 427 | - * UDP 协议配置 | ||
| 428 | - */ | ||
| 429 | - @Valid | ||
| 430 | - private IotUdpConfig udp; | ||
| 431 | - | ||
| 432 | - /** | ||
| 433 | - * CoAP 协议配置 | ||
| 434 | - */ | ||
| 435 | - @Valid | ||
| 436 | - private IotCoapConfig coap; | 220 | + public static class ProtocolProperties { |
| 437 | 221 | ||
| 438 | /** | 222 | /** |
| 439 | - * WebSocket 协议配置 | 223 | + * Modbus TCP 组件配置 |
| 440 | */ | 224 | */ |
| 441 | - @Valid | ||
| 442 | - private IotWebSocketConfig websocket; | 225 | + private ModbusTcpProperties modbusTcp; |
| 443 | 226 | ||
| 444 | } | 227 | } |
| 445 | 228 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
| @@ -5,7 +5,9 @@ import cn.hutool.core.util.BooleanUtil; | @@ -5,7 +5,9 @@ import cn.hutool.core.util.BooleanUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | ||
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | ||
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; |
| @@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 43 | if (running) { | 45 | if (running) { |
| 44 | return; | 46 | return; |
| 45 | } | 47 | } |
| 46 | - List<IotGatewayProperties.ProtocolInstanceProperties> protocolConfigs = gatewayProperties.getProtocols(); | 48 | + List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols(); |
| 47 | if (CollUtil.isEmpty(protocolConfigs)) { | 49 | if (CollUtil.isEmpty(protocolConfigs)) { |
| 48 | log.info("[start][没有配置协议实例,跳过启动]"); | 50 | log.info("[start][没有配置协议实例,跳过启动]"); |
| 49 | return; | 51 | return; |
| 50 | } | 52 | } |
| 51 | 53 | ||
| 52 | - for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) { | 54 | + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { |
| 53 | if (BooleanUtil.isFalse(config.getEnabled())) { | 55 | if (BooleanUtil.isFalse(config.getEnabled())) { |
| 54 | log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); | 56 | log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); |
| 55 | continue; | 57 | continue; |
| @@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 89 | * @return 协议实例 | 91 | * @return 协议实例 |
| 90 | */ | 92 | */ |
| 91 | @SuppressWarnings({"EnhancedSwitchMigration"}) | 93 | @SuppressWarnings({"EnhancedSwitchMigration"}) |
| 92 | - private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | ||
| 93 | - IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType()); | 94 | + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) { |
| 95 | + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); | ||
| 94 | if (protocolType == null) { | 96 | if (protocolType == null) { |
| 95 | - log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType()); | 97 | + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); |
| 96 | return null; | 98 | return null; |
| 97 | } | 99 | } |
| 98 | switch (protocolType) { | 100 | switch (protocolType) { |
| @@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 106 | return createCoapProtocol(config); | 108 | return createCoapProtocol(config); |
| 107 | case WEBSOCKET: | 109 | case WEBSOCKET: |
| 108 | return createWebSocketProtocol(config); | 110 | return createWebSocketProtocol(config); |
| 111 | + case MQTT: | ||
| 112 | + return createMqttProtocol(config); | ||
| 113 | + case EMQX: | ||
| 114 | + return createEmqxProtocol(config); | ||
| 109 | default: | 115 | default: |
| 110 | throw new IllegalArgumentException(String.format( | 116 | throw new IllegalArgumentException(String.format( |
| 111 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); | 117 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| @@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 118 | * @param config 协议实例配置 | 124 | * @param config 协议实例配置 |
| 119 | * @return HTTP 协议实例 | 125 | * @return HTTP 协议实例 |
| 120 | */ | 126 | */ |
| 121 | - private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 127 | + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { |
| 122 | return new IotHttpProtocol(config); | 128 | return new IotHttpProtocol(config); |
| 123 | } | 129 | } |
| 124 | 130 | ||
| @@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 128 | * @param config 协议实例配置 | 134 | * @param config 协议实例配置 |
| 129 | * @return TCP 协议实例 | 135 | * @return TCP 协议实例 |
| 130 | */ | 136 | */ |
| 131 | - private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 137 | + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { |
| 132 | return new IotTcpProtocol(config); | 138 | return new IotTcpProtocol(config); |
| 133 | } | 139 | } |
| 134 | 140 | ||
| @@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 138 | * @param config 协议实例配置 | 144 | * @param config 协议实例配置 |
| 139 | * @return UDP 协议实例 | 145 | * @return UDP 协议实例 |
| 140 | */ | 146 | */ |
| 141 | - private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 147 | + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { |
| 142 | return new IotUdpProtocol(config); | 148 | return new IotUdpProtocol(config); |
| 143 | } | 149 | } |
| 144 | 150 | ||
| @@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 148 | * @param config 协议实例配置 | 154 | * @param config 协议实例配置 |
| 149 | * @return CoAP 协议实例 | 155 | * @return CoAP 协议实例 |
| 150 | */ | 156 | */ |
| 151 | - private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 157 | + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { |
| 152 | return new IotCoapProtocol(config); | 158 | return new IotCoapProtocol(config); |
| 153 | } | 159 | } |
| 154 | 160 | ||
| @@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 158 | * @param config 协议实例配置 | 164 | * @param config 协议实例配置 |
| 159 | * @return WebSocket 协议实例 | 165 | * @return WebSocket 协议实例 |
| 160 | */ | 166 | */ |
| 161 | - private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | 167 | + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) { |
| 162 | return new IotWebSocketProtocol(config); | 168 | return new IotWebSocketProtocol(config); |
| 163 | } | 169 | } |
| 164 | 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 | + | ||
| 165 | } | 191 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java
| @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil; | @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 7 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; | 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; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; |
| @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource; | @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource; | ||
| 21 | import org.eclipse.californium.core.CoapServer; | 21 | import org.eclipse.californium.core.CoapServer; |
| 22 | import org.eclipse.californium.core.config.CoapConfig; | 22 | import org.eclipse.californium.core.config.CoapConfig; |
| 23 | import org.eclipse.californium.elements.config.Configuration; | 23 | import org.eclipse.californium.elements.config.Configuration; |
| 24 | -import org.springframework.util.Assert; | 24 | +import cn.hutool.core.lang.Assert; |
| 25 | 25 | ||
| 26 | import java.util.concurrent.TimeUnit; | 26 | import java.util.concurrent.TimeUnit; |
| 27 | 27 | ||
| @@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol { | @@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol { | ||
| 43 | /** | 43 | /** |
| 44 | * 协议配置 | 44 | * 协议配置 |
| 45 | */ | 45 | */ |
| 46 | - private final ProtocolInstanceProperties properties; | 46 | + private final ProtocolProperties properties; |
| 47 | /** | 47 | /** |
| 48 | * 服务器 ID(用于消息追踪,全局唯一) | 48 | * 服务器 ID(用于消息追踪,全局唯一) |
| 49 | */ | 49 | */ |
| @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol { | @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol { | ||
| 66 | */ | 66 | */ |
| 67 | private final IotCoapDownstreamSubscriber downstreamSubscriber; | 67 | private final IotCoapDownstreamSubscriber downstreamSubscriber; |
| 68 | 68 | ||
| 69 | - public IotCoapProtocol(ProtocolInstanceProperties properties) { | 69 | + public IotCoapProtocol(ProtocolProperties properties) { |
| 70 | IotCoapConfig coapConfig = properties.getCoap(); | 70 | IotCoapConfig coapConfig = properties.getCoap(); |
| 71 | Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); | 71 | Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); |
| 72 | this.properties = properties; | 72 | this.properties = properties; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java
| @@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler { | @@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler { | ||
| 33 | Assert.notNull(request, "请求体不能为空"); | 33 | Assert.notNull(request, "请求体不能为空"); |
| 34 | Assert.notBlank(request.getProductKey(), "productKey 不能为空"); | 34 | Assert.notBlank(request.getProductKey(), "productKey 不能为空"); |
| 35 | Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); | 35 | Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); |
| 36 | - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); | 36 | + Assert.notBlank(request.getSign(), "sign 不能为空"); |
| 37 | 37 | ||
| 38 | // 2. 调用动态注册 | 38 | // 2. 调用动态注册 |
| 39 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); | 39 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); |
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 | \ No newline at end of file | 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.enums.IotProtocolTypeEnum; | ||
| 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.IotProtocol; | ||
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; | ||
| 10 | -import io.netty.handler.codec.mqtt.MqttQoS; | ||
| 11 | -import io.vertx.core.Vertx; | ||
| 12 | -import io.vertx.core.buffer.Buffer; | ||
| 13 | -import io.vertx.core.net.JksOptions; | ||
| 14 | -import io.vertx.mqtt.MqttClient; | ||
| 15 | -import io.vertx.mqtt.MqttClientOptions; | ||
| 16 | -import jakarta.annotation.PostConstruct; | ||
| 17 | -import jakarta.annotation.PreDestroy; | ||
| 18 | -import lombok.Getter; | ||
| 19 | -import lombok.extern.slf4j.Slf4j; | ||
| 20 | - | ||
| 21 | -import java.util.HashMap; | ||
| 22 | -import java.util.List; | ||
| 23 | -import java.util.Map; | ||
| 24 | -import java.util.concurrent.CountDownLatch; | ||
| 25 | -import java.util.concurrent.atomic.AtomicBoolean; | ||
| 26 | - | ||
| 27 | -/** | ||
| 28 | - * IoT 网关 EMQX 协议:接收设备上行消息 | ||
| 29 | - * | ||
| 30 | - * @author 芋道源码 | ||
| 31 | - */ | ||
| 32 | -@Slf4j | ||
| 33 | -public class IotEmqxUpstreamProtocol implements IotProtocol { | ||
| 34 | - | ||
| 35 | - private static final String ID = "emqx"; | ||
| 36 | - | ||
| 37 | - private final IotGatewayProperties.EmqxProperties emqxProperties; | ||
| 38 | - | ||
| 39 | - private volatile boolean running = false; | ||
| 40 | - | ||
| 41 | - private final Vertx vertx; | ||
| 42 | - | ||
| 43 | - @Getter | ||
| 44 | - private final String serverId; | ||
| 45 | - | ||
| 46 | - private MqttClient mqttClient; | ||
| 47 | - | ||
| 48 | - private IotEmqxUpstreamHandler upstreamHandler; | ||
| 49 | - | ||
| 50 | - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties, | ||
| 51 | - Vertx vertx) { | ||
| 52 | - this.emqxProperties = emqxProperties; | ||
| 53 | - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); | ||
| 54 | - this.vertx = vertx; | ||
| 55 | - } | ||
| 56 | - | ||
| 57 | - @Override | ||
| 58 | - public String getId() { | ||
| 59 | - return ID; | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - @Override | ||
| 63 | - public IotProtocolTypeEnum getType() { | ||
| 64 | - return IotProtocolTypeEnum.EMQX; | ||
| 65 | - } | ||
| 66 | - | ||
| 67 | - @Override | ||
| 68 | - @PostConstruct | ||
| 69 | - public void start() { | ||
| 70 | - if (running) { | ||
| 71 | - return; | ||
| 72 | - } | ||
| 73 | - | ||
| 74 | - try { | ||
| 75 | - // 1. 启动 MQTT 客户端 | ||
| 76 | - startMqttClient(); | ||
| 77 | - | ||
| 78 | - // 2. 标记服务为运行状态 | ||
| 79 | - running = true; | ||
| 80 | - log.info("[start][IoT 网关 EMQX 协议启动成功]"); | ||
| 81 | - } catch (Exception e) { | ||
| 82 | - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); | ||
| 83 | - stop(); | ||
| 84 | - | ||
| 85 | - // 异步关闭应用 | ||
| 86 | - Thread shutdownThread = new Thread(() -> { | ||
| 87 | - try { | ||
| 88 | - // 确保日志输出完成,使用更优雅的方式 | ||
| 89 | - log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); | ||
| 90 | - // 等待日志输出完成 | ||
| 91 | - Thread.sleep(1000); | ||
| 92 | - } catch (InterruptedException ie) { | ||
| 93 | - Thread.currentThread().interrupt(); | ||
| 94 | - log.warn("[start][应用关闭被中断]"); | ||
| 95 | - } | ||
| 96 | - System.exit(1); | ||
| 97 | - }); | ||
| 98 | - shutdownThread.setDaemon(true); | ||
| 99 | - shutdownThread.setName("emergency-shutdown"); | ||
| 100 | - shutdownThread.start(); | ||
| 101 | - | ||
| 102 | - throw e; | ||
| 103 | - } | ||
| 104 | - } | ||
| 105 | - | ||
| 106 | - @Override | ||
| 107 | - @PreDestroy | ||
| 108 | - public void stop() { | ||
| 109 | - if (!running) { | ||
| 110 | - return; | ||
| 111 | - } | ||
| 112 | - | ||
| 113 | - // 1. 停止 MQTT 客户端 | ||
| 114 | - stopMqttClient(); | ||
| 115 | - | ||
| 116 | - // 2. 标记服务为停止状态 | ||
| 117 | - running = false; | ||
| 118 | - log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - @Override | ||
| 122 | - public boolean isRunning() { | ||
| 123 | - return running; | ||
| 124 | - } | ||
| 125 | - | ||
| 126 | - /** | ||
| 127 | - * 启动 MQTT 客户端 | ||
| 128 | - */ | ||
| 129 | - private void startMqttClient() { | ||
| 130 | - try { | ||
| 131 | - // 1. 初始化消息处理器 | ||
| 132 | - this.upstreamHandler = new IotEmqxUpstreamHandler(this); | ||
| 133 | - | ||
| 134 | - // 2. 创建 MQTT 客户端 | ||
| 135 | - createMqttClient(); | ||
| 136 | - | ||
| 137 | - // 3. 同步连接 MQTT Broker | ||
| 138 | - connectMqttSync(); | ||
| 139 | - } catch (Exception e) { | ||
| 140 | - log.error("[startMqttClient][MQTT 客户端启动失败]", e); | ||
| 141 | - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); | ||
| 142 | - } | ||
| 143 | - } | ||
| 144 | - | ||
| 145 | - /** | ||
| 146 | - * 同步连接 MQTT Broker | ||
| 147 | - */ | ||
| 148 | - private void connectMqttSync() { | ||
| 149 | - String host = emqxProperties.getMqttHost(); | ||
| 150 | - int port = emqxProperties.getMqttPort(); | ||
| 151 | - // 1. 连接 MQTT Broker | ||
| 152 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 153 | - AtomicBoolean success = new AtomicBoolean(false); | ||
| 154 | - mqttClient.connect(port, host, connectResult -> { | ||
| 155 | - if (connectResult.succeeded()) { | ||
| 156 | - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); | ||
| 157 | - setupMqttHandlers(); | ||
| 158 | - subscribeToTopics(); | ||
| 159 | - success.set(true); | ||
| 160 | - } else { | ||
| 161 | - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", | ||
| 162 | - host, port, connectResult.cause()); | ||
| 163 | - } | ||
| 164 | - latch.countDown(); | ||
| 165 | - }); | ||
| 166 | - | ||
| 167 | - // 2. 等待连接结果 | ||
| 168 | - try { | ||
| 169 | - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制 | ||
| 170 | - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); | ||
| 171 | - if (!awaitResult) { | ||
| 172 | - log.error("[connectMqttSync][等待连接结果超时]"); | ||
| 173 | - throw new RuntimeException("连接 MQTT Broker 超时"); | ||
| 174 | - } | ||
| 175 | - if (!success.get()) { | ||
| 176 | - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port)); | ||
| 177 | - } | ||
| 178 | - } catch (InterruptedException e) { | ||
| 179 | - Thread.currentThread().interrupt(); | ||
| 180 | - log.error("[connectMqttSync][等待连接结果被中断]", e); | ||
| 181 | - throw new RuntimeException("连接 MQTT Broker 被中断", e); | ||
| 182 | - } | ||
| 183 | - } | ||
| 184 | - | ||
| 185 | - /** | ||
| 186 | - * 异步连接 MQTT Broker | ||
| 187 | - */ | ||
| 188 | - private void connectMqttAsync() { | ||
| 189 | - String host = emqxProperties.getMqttHost(); | ||
| 190 | - int port = emqxProperties.getMqttPort(); | ||
| 191 | - mqttClient.connect(port, host, connectResult -> { | ||
| 192 | - if (connectResult.succeeded()) { | ||
| 193 | - log.info("[connectMqttAsync][MQTT 客户端重连成功]"); | ||
| 194 | - setupMqttHandlers(); | ||
| 195 | - subscribeToTopics(); | ||
| 196 | - } else { | ||
| 197 | - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", | ||
| 198 | - host, port, connectResult.cause()); | ||
| 199 | - log.warn("[connectMqttAsync][重连失败,将再次尝试]"); | ||
| 200 | - reconnectWithDelay(); | ||
| 201 | - } | ||
| 202 | - }); | ||
| 203 | - } | ||
| 204 | - | ||
| 205 | - /** | ||
| 206 | - * 延迟重连 | ||
| 207 | - */ | ||
| 208 | - private void reconnectWithDelay() { | ||
| 209 | - if (!running) { | ||
| 210 | - return; | ||
| 211 | - } | ||
| 212 | - if (mqttClient != null && mqttClient.isConnected()) { | ||
| 213 | - return; | ||
| 214 | - } | ||
| 215 | - | ||
| 216 | - long delay = emqxProperties.getReconnectDelayMs(); | ||
| 217 | - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); | ||
| 218 | - vertx.setTimer(delay, timerId -> { | ||
| 219 | - if (!running) { | ||
| 220 | - return; | ||
| 221 | - } | ||
| 222 | - if (mqttClient != null && mqttClient.isConnected()) { | ||
| 223 | - return; | ||
| 224 | - } | ||
| 225 | - | ||
| 226 | - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); | ||
| 227 | - try { | ||
| 228 | - createMqttClient(); | ||
| 229 | - connectMqttAsync(); | ||
| 230 | - } catch (Exception e) { | ||
| 231 | - log.error("[reconnectWithDelay][重连过程中发生异常]", e); | ||
| 232 | - vertx.setTimer(delay, t -> reconnectWithDelay()); | ||
| 233 | - } | ||
| 234 | - }); | ||
| 235 | - } | ||
| 236 | - | ||
| 237 | - /** | ||
| 238 | - * 停止 MQTT 客户端 | ||
| 239 | - */ | ||
| 240 | - private void stopMqttClient() { | ||
| 241 | - if (mqttClient == null) { | ||
| 242 | - return; | ||
| 243 | - } | ||
| 244 | - try { | ||
| 245 | - if (mqttClient.isConnected()) { | ||
| 246 | - // 1. 取消订阅所有主题 | ||
| 247 | - List<String> topicList = emqxProperties.getMqttTopics(); | ||
| 248 | - for (String topic : topicList) { | ||
| 249 | - try { | ||
| 250 | - mqttClient.unsubscribe(topic); | ||
| 251 | - } catch (Exception e) { | ||
| 252 | - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | ||
| 253 | - } | ||
| 254 | - } | ||
| 255 | - | ||
| 256 | - // 2. 断开 MQTT 客户端连接 | ||
| 257 | - try { | ||
| 258 | - CountDownLatch disconnectLatch = new CountDownLatch(1); | ||
| 259 | - mqttClient.disconnect(ar -> disconnectLatch.countDown()); | ||
| 260 | - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { | ||
| 261 | - log.warn("[stopMqttClient][断开 MQTT 连接超时]"); | ||
| 262 | - } | ||
| 263 | - } catch (Exception e) { | ||
| 264 | - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); | ||
| 265 | - } | ||
| 266 | - } | ||
| 267 | - } catch (Exception e) { | ||
| 268 | - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); | ||
| 269 | - } finally { | ||
| 270 | - mqttClient = null; | ||
| 271 | - } | ||
| 272 | - } | ||
| 273 | - | ||
| 274 | - /** | ||
| 275 | - * 创建 MQTT 客户端 | ||
| 276 | - */ | ||
| 277 | - private void createMqttClient() { | ||
| 278 | - // 1.1 创建基础配置 | ||
| 279 | - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions() | ||
| 280 | - .setClientId(emqxProperties.getMqttClientId()) | ||
| 281 | - .setUsername(emqxProperties.getMqttUsername()) | ||
| 282 | - .setPassword(emqxProperties.getMqttPassword()) | ||
| 283 | - .setSsl(emqxProperties.getMqttSsl()) | ||
| 284 | - .setCleanSession(emqxProperties.getCleanSession()) | ||
| 285 | - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds()) | ||
| 286 | - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue()) | ||
| 287 | - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒 | ||
| 288 | - .setTrustAll(emqxProperties.getTrustAll()); | ||
| 289 | - // 1.2 配置遗嘱消息 | ||
| 290 | - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill(); | ||
| 291 | - if (will.isEnabled()) { | ||
| 292 | - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空"); | ||
| 293 | - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空"); | ||
| 294 | - options.setWillFlag(true) | ||
| 295 | - .setWillTopic(will.getTopic()) | ||
| 296 | - .setWillMessageBytes(Buffer.buffer(will.getPayload())) | ||
| 297 | - .setWillQoS(will.getQos()) | ||
| 298 | - .setWillRetain(will.isRetain()); | ||
| 299 | - } | ||
| 300 | - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效) | ||
| 301 | - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) { | ||
| 302 | - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions(); | ||
| 303 | - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { | ||
| 304 | - options.setTrustStoreOptions(new JksOptions() | ||
| 305 | - .setPath(sslOptions.getTrustStorePath()) | ||
| 306 | - .setPassword(sslOptions.getTrustStorePassword())); | ||
| 307 | - } | ||
| 308 | - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { | ||
| 309 | - options.setKeyStoreOptions(new JksOptions() | ||
| 310 | - .setPath(sslOptions.getKeyStorePath()) | ||
| 311 | - .setPassword(sslOptions.getKeyStorePassword())); | ||
| 312 | - } | ||
| 313 | - } | ||
| 314 | - // 1.4 安全警告日志 | ||
| 315 | - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) { | ||
| 316 | - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]"); | ||
| 317 | - } | ||
| 318 | - | ||
| 319 | - // 2. 创建客户端实例 | ||
| 320 | - this.mqttClient = MqttClient.create(vertx, options); | ||
| 321 | - } | ||
| 322 | - | ||
| 323 | - /** | ||
| 324 | - * 设置 MQTT 处理器 | ||
| 325 | - */ | ||
| 326 | - private void setupMqttHandlers() { | ||
| 327 | - // 1. 设置断开重连监听器 | ||
| 328 | - mqttClient.closeHandler(closeEvent -> { | ||
| 329 | - if (!running) { | ||
| 330 | - return; | ||
| 331 | - } | ||
| 332 | - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); | ||
| 333 | - reconnectWithDelay(); | ||
| 334 | - }); | ||
| 335 | - | ||
| 336 | - // 2. 设置异常处理器 | ||
| 337 | - mqttClient.exceptionHandler(exception -> | ||
| 338 | - log.error("[exceptionHandler][MQTT 客户端异常]", exception)); | ||
| 339 | - | ||
| 340 | - // 3. 设置消息处理器 | ||
| 341 | - mqttClient.publishHandler(upstreamHandler::handle); | ||
| 342 | - } | ||
| 343 | - | ||
| 344 | - /** | ||
| 345 | - * 订阅设备上行消息主题 | ||
| 346 | - */ | ||
| 347 | - private void subscribeToTopics() { | ||
| 348 | - // 1. 校验 MQTT 客户端是否连接 | ||
| 349 | - List<String> topicList = emqxProperties.getMqttTopics(); | ||
| 350 | - if (mqttClient == null || !mqttClient.isConnected()) { | ||
| 351 | - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); | ||
| 352 | - return; | ||
| 353 | - } | ||
| 354 | - | ||
| 355 | - // 2. 批量订阅所有主题 | ||
| 356 | - Map<String, Integer> topics = new HashMap<>(); | ||
| 357 | - int qos = emqxProperties.getMqttQos(); | ||
| 358 | - for (String topic : topicList) { | ||
| 359 | - topics.put(topic, qos); | ||
| 360 | - } | ||
| 361 | - mqttClient.subscribe(topics, subscribeResult -> { | ||
| 362 | - if (subscribeResult.succeeded()) { | ||
| 363 | - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); | ||
| 364 | - } else { | ||
| 365 | - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]", | ||
| 366 | - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause()); | ||
| 367 | - } | ||
| 368 | - }); | ||
| 369 | - } | ||
| 370 | - | ||
| 371 | - /** | ||
| 372 | - * 发布消息到 MQTT Broker | ||
| 373 | - * | ||
| 374 | - * @param topic 主题 | ||
| 375 | - * @param payload 消息内容 | ||
| 376 | - */ | ||
| 377 | - public void publishMessage(String topic, byte[] payload) { | ||
| 378 | - if (mqttClient == null || !mqttClient.isConnected()) { | ||
| 379 | - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); | ||
| 380 | - return; | ||
| 381 | - } | ||
| 382 | - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); | ||
| 383 | - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); | ||
| 384 | - } | ||
| 385 | - | ||
| 386 | -} | ||
| 387 | \ No newline at end of file | 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 | import cn.hutool.core.util.StrUtil; | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.hutool.extra.spring.SpringUtil; | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 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 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | 11 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; |
| @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; | @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; | ||
| 21 | @Slf4j | 21 | @Slf4j |
| 22 | public class IotEmqxDownstreamHandler { | 22 | public class IotEmqxDownstreamHandler { |
| 23 | 23 | ||
| 24 | - private final IotEmqxUpstreamProtocol protocol; | 24 | + private final IotEmqxProtocol protocol; |
| 25 | 25 | ||
| 26 | private final IotDeviceService deviceService; | 26 | private final IotDeviceService deviceService; |
| 27 | 27 | ||
| 28 | private final IotDeviceMessageService deviceMessageService; | 28 | private final IotDeviceMessageService deviceMessageService; |
| 29 | 29 | ||
| 30 | - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { | 30 | + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) { |
| 31 | this.protocol = protocol; | 31 | this.protocol = protocol; |
| 32 | this.deviceService = SpringUtil.getBean(IotDeviceService.class); | 32 | this.deviceService = SpringUtil.getBean(IotDeviceService.class); |
| 33 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | 33 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { | @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { | ||
| 53 | return; | 53 | return; |
| 54 | } | 54 | } |
| 55 | // 2.2 构建载荷 | 55 | // 2.2 构建载荷 |
| 56 | - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), | 56 | + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), |
| 57 | deviceInfo.getDeviceName()); | 57 | deviceInfo.getDeviceName()); |
| 58 | - // 2.3 发布消息 | 58 | + |
| 59 | + // 3. 发布消息 | ||
| 59 | protocol.publishMessage(topic, payload); | 60 | protocol.publishMessage(topic, payload); |
| 60 | } | 61 | } |
| 61 | 62 | ||
| @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { | @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { | ||
| 74 | return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); | 75 | return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); |
| 75 | } | 76 | } |
| 76 | 77 | ||
| 77 | -} | ||
| 78 | \ No newline at end of file | 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/IotEmqxDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 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; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; | 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; |
| 8 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 9 | 8 | ||
| 10 | /** | 9 | /** |
| @@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber | @@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber | ||
| 17 | 16 | ||
| 18 | private final IotEmqxDownstreamHandler downstreamHandler; | 17 | private final IotEmqxDownstreamHandler downstreamHandler; |
| 19 | 18 | ||
| 20 | - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { | 19 | + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { |
| 21 | super(protocol, messageBus); | 20 | super(protocol, messageBus); |
| 22 | this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); | 21 | this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); |
| 23 | } | 22 | } |
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 | import cn.hutool.core.util.BooleanUtil; | 4 | import cn.hutool.core.util.BooleanUtil; |
| 4 | import cn.hutool.core.util.StrUtil; | 5 | import cn.hutool.core.util.StrUtil; |
| 5 | import cn.hutool.extra.spring.SpringUtil; | 6 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 7 | 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.IotDeviceCommonApi; |
| 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 9 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 11 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 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 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 15 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | ||
| 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 17 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | ||
| 13 | import io.vertx.core.json.JsonObject; | 19 | import io.vertx.core.json.JsonObject; |
| 14 | import io.vertx.ext.web.RoutingContext; | 20 | import io.vertx.ext.web.RoutingContext; |
| 15 | import lombok.extern.slf4j.Slf4j; | 21 | import lombok.extern.slf4j.Slf4j; |
| 16 | 22 | ||
| 23 | +import java.util.Locale; | ||
| 24 | + | ||
| 17 | /** | 25 | /** |
| 18 | * IoT 网关 EMQX 认证事件处理器 | 26 | * IoT 网关 EMQX 认证事件处理器 |
| 19 | * <p> | 27 | * <p> |
| 20 | * 为 EMQX 提供 HTTP 接口服务,包括: | 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 | * @author 芋道源码 | 34 | * @author 芋道源码 |
| 25 | */ | 35 | */ |
| @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { | @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { | ||
| 45 | private static final String RESULT_IGNORE = "ignore"; | 55 | private static final String RESULT_IGNORE = "ignore"; |
| 46 | 56 | ||
| 47 | /** | 57 | /** |
| 48 | - * EMQX 事件类型常量 | 58 | + * EMQX 事件类型常量 - 客户端连接 |
| 49 | */ | 59 | */ |
| 50 | private static final String EVENT_CLIENT_CONNECTED = "client.connected"; | 60 | private static final String EVENT_CLIENT_CONNECTED = "client.connected"; |
| 61 | + /** | ||
| 62 | + * EMQX 事件类型常量 - 客户端断开连接 | ||
| 63 | + */ | ||
| 51 | private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; | 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 | private final String serverId; | 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 | private final IotDeviceCommonApi deviceApi; | 76 | private final IotDeviceCommonApi deviceApi; |
| 58 | 77 | ||
| 59 | - public IotEmqxAuthEventHandler(String serverId) { | 78 | + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { |
| 60 | this.serverId = serverId; | 79 | this.serverId = serverId; |
| 80 | + this.protocol = protocol; | ||
| 61 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | 81 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 62 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | 82 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 63 | } | 83 | } |
| 64 | 84 | ||
| 85 | + // ========== 认证处理 ========== | ||
| 86 | + | ||
| 65 | /** | 87 | /** |
| 66 | * EMQX 认证接口 | 88 | * EMQX 认证接口 |
| 67 | */ | 89 | */ |
| 68 | public void handleAuth(RoutingContext context) { | 90 | public void handleAuth(RoutingContext context) { |
| 91 | + JsonObject body = null; | ||
| 69 | try { | 92 | try { |
| 70 | // 1. 参数校验 | 93 | // 1. 参数校验 |
| 71 | - JsonObject body = parseRequestBody(context); | 94 | + body = parseRequestBody(context); |
| 72 | if (body == null) { | 95 | if (body == null) { |
| 73 | return; | 96 | return; |
| 74 | } | 97 | } |
| @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { | @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { | ||
| 82 | return; | 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 | boolean authResult = handleDeviceAuth(clientId, username, password); | 115 | boolean authResult = handleDeviceAuth(clientId, username, password); |
| 87 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); | 116 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); |
| 88 | if (authResult) { | 117 | if (authResult) { |
| @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler { | @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler { | ||
| 91 | sendAuthResponse(context, RESULT_DENY); | 120 | sendAuthResponse(context, RESULT_DENY); |
| 92 | } | 121 | } |
| 93 | } catch (Exception e) { | 122 | } catch (Exception e) { |
| 94 | - log.error("[handleAuth][设备认证异常]", e); | 123 | + log.error("[handleAuth][设备认证异常][body={}]", body, e); |
| 95 | sendAuthResponse(context, RESULT_IGNORE); | 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 | * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 | 297 | * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 |
| 101 | * 支持的事件类型:client.connected、client.disconnected 等 | 298 | * 支持的事件类型:client.connected、client.disconnected 等 |
| 102 | */ | 299 | */ |
| @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler { | @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler { | ||
| 124 | break; | 321 | break; |
| 125 | } | 322 | } |
| 126 | 323 | ||
| 127 | - // EMQX Webhook 只需要 200 状态码,无需响应体 | 324 | + // 3. EMQX Webhook 只需要 200 状态码,无需响应体 |
| 128 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); | 325 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 129 | } catch (Exception e) { | 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 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); | 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 | * <p> | 335 | * <p> |
| 182 | * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范) | 336 | * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范) |
| @@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler { | @@ -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,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 | \ No newline at end of file | 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 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.hutool.extra.spring.SpringUtil; | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | ||
| 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | import io.vertx.mqtt.messages.MqttPublishMessage; | 8 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler { | @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler { | ||
| 20 | 20 | ||
| 21 | private final String serverId; | 21 | private final String serverId; |
| 22 | 22 | ||
| 23 | - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { | 23 | + public IotEmqxUpstreamHandler(String serverId) { |
| 24 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | 24 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 25 | - this.serverId = protocol.getServerId(); | 25 | + this.serverId = serverId; |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | /** | 28 | /** |
| 29 | * 处理 MQTT 发布消息 | 29 | * 处理 MQTT 发布消息 |
| 30 | */ | 30 | */ |
| 31 | public void handle(MqttPublishMessage mqttMessage) { | 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 | String topic = mqttMessage.topicName(); | 33 | String topic = mqttMessage.topicName(); |
| 34 | byte[] payload = mqttMessage.payload().getBytes(); | 34 | byte[] payload = mqttMessage.payload().getBytes(); |
| 35 | try { | 35 | try { |
| 36 | // 1. 解析主题,一次性获取所有信息 | 36 | // 1. 解析主题,一次性获取所有信息 |
| 37 | String[] topicParts = topic.split("/"); | 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 | log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | 41 | log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); |
| 40 | return; | 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 | if (message == null) { | 47 | if (message == null) { |
| 49 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); | 48 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 50 | return; | 49 | return; |
| 51 | } | 50 | } |
| 52 | 51 | ||
| 53 | - // 4. 发送消息到队列 | 52 | + // 3. 发送消息到队列 |
| 54 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | 53 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); |
| 55 | } catch (Exception e) { | 54 | } catch (Exception e) { |
| 56 | log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); | 55 | log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); |
| 57 | } | 56 | } |
| 58 | } | 57 | } |
| 59 | 58 | ||
| 60 | -} | ||
| 61 | \ No newline at end of file | 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
| @@ -10,19 +10,4 @@ import lombok.Data; | @@ -10,19 +10,4 @@ import lombok.Data; | ||
| 10 | @Data | 10 | @Data |
| 11 | public class IotHttpConfig { | 11 | public class IotHttpConfig { |
| 12 | 12 | ||
| 13 | - /** | ||
| 14 | - * 是否启用 SSL | ||
| 15 | - */ | ||
| 16 | - private Boolean sslEnabled = false; | ||
| 17 | - | ||
| 18 | - /** | ||
| 19 | - * SSL 证书路径 | ||
| 20 | - */ | ||
| 21 | - private String sslCertPath; | ||
| 22 | - | ||
| 23 | - /** | ||
| 24 | - * SSL 私钥路径 | ||
| 25 | - */ | ||
| 26 | - private String sslKeyPath; | ||
| 27 | - | ||
| 28 | } | 13 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
| @@ -4,7 +4,8 @@ import cn.hutool.extra.spring.SpringUtil; | @@ -4,7 +4,8 @@ import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 7 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 8 | +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.IotProtocol; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; |
| @@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol { | @@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 33 | /** | 34 | /** |
| 34 | * 协议配置 | 35 | * 协议配置 |
| 35 | */ | 36 | */ |
| 36 | - private final ProtocolInstanceProperties properties; | 37 | + private final ProtocolProperties properties; |
| 37 | /** | 38 | /** |
| 38 | * 服务器 ID(用于消息追踪,全局唯一) | 39 | * 服务器 ID(用于消息追踪,全局唯一) |
| 39 | */ | 40 | */ |
| @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol { | @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 60 | */ | 61 | */ |
| 61 | private IotHttpDownstreamSubscriber downstreamSubscriber; | 62 | private IotHttpDownstreamSubscriber downstreamSubscriber; |
| 62 | 63 | ||
| 63 | - public IotHttpProtocol(ProtocolInstanceProperties properties) { | 64 | + public IotHttpProtocol(ProtocolProperties properties) { |
| 64 | this.properties = properties; | 65 | this.properties = properties; |
| 65 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 66 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 66 | 67 | ||
| @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol { | @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 104 | router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); | 105 | router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); |
| 105 | 106 | ||
| 106 | // 1.4 启动 HTTP 服务器 | 107 | // 1.4 启动 HTTP 服务器 |
| 107 | - IotHttpConfig httpConfig = properties.getHttp(); | ||
| 108 | HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); | 108 | HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); |
| 109 | - if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { | 109 | + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); |
| 110 | + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { | ||
| 110 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | 111 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 111 | - .setKeyPath(httpConfig.getSslKeyPath()) | ||
| 112 | - .setCertPath(httpConfig.getSslCertPath()); | 112 | + .setKeyPath(sslConfig.getSslKeyPath()) |
| 113 | + .setCertPath(sslConfig.getSslCertPath()); | ||
| 113 | options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | 114 | options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); |
| 114 | } | 115 | } |
| 115 | try { | 116 | try { |
| @@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol { | @@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 125 | this.downstreamSubscriber.start(); | 126 | this.downstreamSubscriber.start(); |
| 126 | } catch (Exception e) { | 127 | } catch (Exception e) { |
| 127 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); | 128 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); |
| 128 | - // 启动失败时关闭 Vertx | 129 | + // 启动失败时关闭资源 |
| 130 | + if (httpServer != null) { | ||
| 131 | + httpServer.close(); | ||
| 132 | + httpServer = null; | ||
| 133 | + } | ||
| 129 | if (vertx != null) { | 134 | if (vertx != null) { |
| 130 | vertx.close(); | 135 | vertx.close(); |
| 131 | vertx = null; | 136 | vertx = null; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java
| @@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { | @@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { | ||
| 35 | Assert.notNull(request, "请求参数不能为空"); | 35 | Assert.notNull(request, "请求参数不能为空"); |
| 36 | Assert.notBlank(request.getProductKey(), "productKey 不能为空"); | 36 | Assert.notBlank(request.getProductKey(), "productKey 不能为空"); |
| 37 | Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); | 37 | Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); |
| 38 | - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); | 38 | + Assert.notBlank(request.getSign(), "sign 不能为空"); |
| 39 | 39 | ||
| 40 | // 2. 调用动态注册 | 40 | // 2. 调用动态注册 |
| 41 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); | 41 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); |
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/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.enums.IotProtocolTypeEnum; | ||
| 4 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 5 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | ||
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | ||
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; | ||
| 9 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 10 | -import io.vertx.core.Vertx; | ||
| 11 | -import io.vertx.mqtt.MqttServer; | ||
| 12 | -import io.vertx.mqtt.MqttServerOptions; | ||
| 13 | -import jakarta.annotation.PostConstruct; | ||
| 14 | -import jakarta.annotation.PreDestroy; | ||
| 15 | -import lombok.Getter; | ||
| 16 | -import lombok.extern.slf4j.Slf4j; | ||
| 17 | - | ||
| 18 | -/** | ||
| 19 | - * IoT 网关 MQTT 协议:接收设备上行消息 | ||
| 20 | - * | ||
| 21 | - * @author 芋道源码 | ||
| 22 | - */ | ||
| 23 | -@Slf4j | ||
| 24 | -public class IotMqttUpstreamProtocol implements IotProtocol { | ||
| 25 | - | ||
| 26 | - private static final String ID = "mqtt"; | ||
| 27 | - | ||
| 28 | - private volatile boolean running = false; | ||
| 29 | - | ||
| 30 | - private final IotGatewayProperties.MqttProperties mqttProperties; | ||
| 31 | - | ||
| 32 | - private final IotDeviceMessageService messageService; | ||
| 33 | - | ||
| 34 | - private final IotMqttConnectionManager connectionManager; | ||
| 35 | - | ||
| 36 | - private final Vertx vertx; | ||
| 37 | - | ||
| 38 | - @Getter | ||
| 39 | - private final String serverId; | ||
| 40 | - | ||
| 41 | - private MqttServer mqttServer; | ||
| 42 | - | ||
| 43 | - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, | ||
| 44 | - IotDeviceMessageService messageService, | ||
| 45 | - IotMqttConnectionManager connectionManager, | ||
| 46 | - Vertx vertx) { | ||
| 47 | - this.mqttProperties = mqttProperties; | ||
| 48 | - this.messageService = messageService; | ||
| 49 | - this.connectionManager = connectionManager; | ||
| 50 | - this.vertx = vertx; | ||
| 51 | - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); | ||
| 52 | - } | ||
| 53 | - | ||
| 54 | - @Override | ||
| 55 | - public String getId() { | ||
| 56 | - return ID; | ||
| 57 | - } | ||
| 58 | - | ||
| 59 | - @Override | ||
| 60 | - public IotProtocolTypeEnum getType() { | ||
| 61 | - return IotProtocolTypeEnum.MQTT; | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | - @Override | ||
| 65 | - public boolean isRunning() { | ||
| 66 | - return running; | ||
| 67 | - } | ||
| 68 | - | ||
| 69 | - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; | ||
| 70 | - @Override | ||
| 71 | - @PostConstruct | ||
| 72 | - public void start() { | ||
| 73 | - // 创建服务器选项 | ||
| 74 | - MqttServerOptions options = new MqttServerOptions() | ||
| 75 | - .setPort(mqttProperties.getPort()) | ||
| 76 | - .setMaxMessageSize(mqttProperties.getMaxMessageSize()) | ||
| 77 | - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); | ||
| 78 | - | ||
| 79 | - // 配置 SSL(如果启用) | ||
| 80 | - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { | ||
| 81 | - options.setSsl(true) | ||
| 82 | - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) | ||
| 83 | - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); | ||
| 84 | - } | ||
| 85 | - | ||
| 86 | - // 创建服务器并设置连接处理器 | ||
| 87 | - mqttServer = MqttServer.create(vertx, options); | ||
| 88 | - mqttServer.endpointHandler(endpoint -> { | ||
| 89 | - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); | ||
| 90 | - handler.handle(endpoint); | ||
| 91 | - }); | ||
| 92 | - | ||
| 93 | - // 启动服务器 | ||
| 94 | - try { | ||
| 95 | - mqttServer.listen().result(); | ||
| 96 | - running = true; | ||
| 97 | - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); | ||
| 98 | - } catch (Exception e) { | ||
| 99 | - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); | ||
| 100 | - throw e; | ||
| 101 | - } | ||
| 102 | - } | ||
| 103 | - | ||
| 104 | - @Override | ||
| 105 | - @PreDestroy | ||
| 106 | - public void stop() { | ||
| 107 | - if (mqttServer != null) { | ||
| 108 | - try { | ||
| 109 | - mqttServer.close().result(); | ||
| 110 | - running = false; | ||
| 111 | - log.info("[stop][IoT 网关 MQTT 协议已停止]"); | ||
| 112 | - } catch (Exception e) { | ||
| 113 | - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); | ||
| 114 | - } | ||
| 115 | - } | ||
| 116 | - } | ||
| 117 | -} |
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/IotMqttDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; |
| 7 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| 10 | - * IoT 网关 MQTT 协议:下行消息订阅器 | ||
| 11 | - * <p> | ||
| 12 | - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 | 10 | + * IoT 网关 MQTT 协议:接收下行给设备的消息 |
| 13 | * | 11 | * |
| 14 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 15 | */ | 13 | */ |
| @@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber | @@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber | ||
| 18 | 16 | ||
| 19 | private final IotMqttDownstreamHandler downstreamHandler; | 17 | private final IotMqttDownstreamHandler downstreamHandler; |
| 20 | 18 | ||
| 21 | - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, | 19 | + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, |
| 22 | IotMqttDownstreamHandler downstreamHandler, | 20 | IotMqttDownstreamHandler downstreamHandler, |
| 23 | IotMessageBus messageBus) { | 21 | IotMessageBus messageBus) { |
| 24 | super(protocol, messageBus); | 22 | super(protocol, messageBus); |
| @@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber | @@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber | ||
| 27 | 25 | ||
| 28 | @Override | 26 | @Override |
| 29 | protected void handleMessage(IotDeviceMessage message) { | 27 | protected void handleMessage(IotDeviceMessage message) { |
| 30 | - boolean success = downstreamHandler.handleDownstreamMessage(message); | ||
| 31 | - if (success) { | ||
| 32 | - log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", | ||
| 33 | - message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 34 | - } else { | ||
| 35 | - log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", | ||
| 36 | - message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 37 | - } | 28 | + downstreamHandler.handle(message); |
| 38 | } | 29 | } |
| 39 | 30 | ||
| 40 | } | 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,7 +66,6 @@ public class IotMqttConnectionManager { | ||
| 66 | } catch (Exception ignored) { | 66 | } catch (Exception ignored) { |
| 67 | // 连接已关闭,忽略异常 | 67 | // 连接已关闭,忽略异常 |
| 68 | } | 68 | } |
| 69 | - | ||
| 70 | return realTimeAddress; | 69 | return realTimeAddress; |
| 71 | } | 70 | } |
| 72 | 71 | ||
| @@ -74,24 +73,24 @@ public class IotMqttConnectionManager { | @@ -74,24 +73,24 @@ public class IotMqttConnectionManager { | ||
| 74 | * 注册设备连接(包含认证信息) | 73 | * 注册设备连接(包含认证信息) |
| 75 | * | 74 | * |
| 76 | * @param endpoint MQTT 连接端点 | 75 | * @param endpoint MQTT 连接端点 |
| 77 | - * @param deviceId 设备 ID | ||
| 78 | * @param connectionInfo 连接信息 | 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 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); | 81 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); |
| 83 | if (oldEndpoint != null && oldEndpoint != endpoint) { | 82 | if (oldEndpoint != null && oldEndpoint != endpoint) { |
| 84 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", | 83 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 85 | deviceId, getEndpointAddress(oldEndpoint)); | 84 | deviceId, getEndpointAddress(oldEndpoint)); |
| 86 | - oldEndpoint.close(); | ||
| 87 | - // 清理旧连接的映射 | 85 | + // 先清理映射,再关闭连接(避免旧连接处理器干扰) |
| 88 | connectionMap.remove(oldEndpoint); | 86 | connectionMap.remove(oldEndpoint); |
| 87 | + oldEndpoint.close(); | ||
| 89 | } | 88 | } |
| 90 | 89 | ||
| 91 | // 注册新连接 | 90 | // 注册新连接 |
| 92 | connectionMap.put(endpoint, connectionInfo); | 91 | connectionMap.put(endpoint, connectionInfo); |
| 93 | deviceEndpointMap.put(deviceId, endpoint); | 92 | deviceEndpointMap.put(deviceId, endpoint); |
| 94 | - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", | 93 | + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", |
| 95 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); | 94 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); |
| 96 | } | 95 | } |
| 97 | 96 | ||
| @@ -129,26 +128,11 @@ public class IotMqttConnectionManager { | @@ -129,26 +128,11 @@ public class IotMqttConnectionManager { | ||
| 129 | if (endpoint == null) { | 128 | if (endpoint == null) { |
| 130 | return null; | 129 | return null; |
| 131 | } | 130 | } |
| 132 | - | ||
| 133 | // 获取连接信息 | 131 | // 获取连接信息 |
| 134 | return getConnectionInfo(endpoint); | 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 | * @param deviceId 设备 ID | 138 | * @param deviceId 设备 ID |
| @@ -192,28 +176,16 @@ public class IotMqttConnectionManager { | @@ -192,28 +176,16 @@ public class IotMqttConnectionManager { | ||
| 192 | * 设备 ID | 176 | * 设备 ID |
| 193 | */ | 177 | */ |
| 194 | private Long deviceId; | 178 | private Long deviceId; |
| 195 | - | ||
| 196 | /** | 179 | /** |
| 197 | * 产品 Key | 180 | * 产品 Key |
| 198 | */ | 181 | */ |
| 199 | private String productKey; | 182 | private String productKey; |
| 200 | - | ||
| 201 | /** | 183 | /** |
| 202 | * 设备名称 | 184 | * 设备名称 |
| 203 | */ | 185 | */ |
| 204 | private String deviceName; | 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 | private String remoteAddress; | 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
| @@ -28,20 +28,6 @@ public class IotTcpConfig { | @@ -28,20 +28,6 @@ public class IotTcpConfig { | ||
| 28 | private Long keepAliveTimeoutMs = 30000L; | 28 | private Long keepAliveTimeoutMs = 30000L; |
| 29 | 29 | ||
| 30 | /** | 30 | /** |
| 31 | - * 是否启用 SSL | ||
| 32 | - */ | ||
| 33 | - @NotNull(message = "是否启用 SSL 不能为空") | ||
| 34 | - private Boolean sslEnabled = false; | ||
| 35 | - /** | ||
| 36 | - * SSL 证书路径 | ||
| 37 | - */ | ||
| 38 | - private String sslCertPath; | ||
| 39 | - /** | ||
| 40 | - * SSL 私钥路径 | ||
| 41 | - */ | ||
| 42 | - private String sslKeyPath; | ||
| 43 | - | ||
| 44 | - /** | ||
| 45 | * 拆包配置 | 31 | * 拆包配置 |
| 46 | */ | 32 | */ |
| 47 | @Valid | 33 | @Valid |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
| @@ -5,7 +5,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | @@ -5,7 +5,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 8 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 9 | +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.IotProtocol; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; |
| @@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions; | @@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions; | ||
| 21 | import io.vertx.core.net.PemKeyCertOptions; | 22 | import io.vertx.core.net.PemKeyCertOptions; |
| 22 | import lombok.Getter; | 23 | import lombok.Getter; |
| 23 | import lombok.extern.slf4j.Slf4j; | 24 | import lombok.extern.slf4j.Slf4j; |
| 24 | -import org.springframework.util.Assert; | 25 | +import cn.hutool.core.lang.Assert; |
| 25 | 26 | ||
| 26 | /** | 27 | /** |
| 27 | * IoT TCP 协议实现 | 28 | * IoT TCP 协议实现 |
| @@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol { | @@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol { | ||
| 36 | /** | 37 | /** |
| 37 | * 协议配置 | 38 | * 协议配置 |
| 38 | */ | 39 | */ |
| 39 | - private final ProtocolInstanceProperties properties; | 40 | + private final ProtocolProperties properties; |
| 40 | /** | 41 | /** |
| 41 | * 服务器 ID(用于消息追踪,全局唯一) | 42 | * 服务器 ID(用于消息追踪,全局唯一) |
| 42 | */ | 43 | */ |
| @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol { | @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol { | ||
| 76 | */ | 77 | */ |
| 77 | private final IotTcpFrameCodec frameCodec; | 78 | private final IotTcpFrameCodec frameCodec; |
| 78 | 79 | ||
| 79 | - public IotTcpProtocol(ProtocolInstanceProperties properties) { | 80 | + public IotTcpProtocol(ProtocolProperties properties) { |
| 80 | IotTcpConfig tcpConfig = properties.getTcp(); | 81 | IotTcpConfig tcpConfig = properties.getTcp(); |
| 81 | Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); | 82 | Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); |
| 82 | Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); | 83 | Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); |
| @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol { | @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol { | ||
| 128 | .setTcpNoDelay(true) | 129 | .setTcpNoDelay(true) |
| 129 | .setReuseAddress(true) | 130 | .setReuseAddress(true) |
| 130 | .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 | 131 | .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 |
| 131 | - if (Boolean.TRUE.equals(tcpConfig.getSslEnabled())) { | 132 | + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); |
| 133 | + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { | ||
| 132 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | 134 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 133 | - .setKeyPath(tcpConfig.getSslKeyPath()) | ||
| 134 | - .setCertPath(tcpConfig.getSslCertPath()); | 135 | + .setKeyPath(sslConfig.getSslKeyPath()) |
| 136 | + .setCertPath(sslConfig.getSslCertPath()); | ||
| 135 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | 137 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); |
| 136 | } | 138 | } |
| 137 | 139 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java
| @@ -8,7 +8,7 @@ import io.vertx.core.Handler; | @@ -8,7 +8,7 @@ import io.vertx.core.Handler; | ||
| 8 | import io.vertx.core.buffer.Buffer; | 8 | import io.vertx.core.buffer.Buffer; |
| 9 | import io.vertx.core.parsetools.RecordParser; | 9 | import io.vertx.core.parsetools.RecordParser; |
| 10 | import lombok.extern.slf4j.Slf4j; | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | -import org.springframework.util.Assert; | 11 | +import cn.hutool.core.lang.Assert; |
| 12 | 12 | ||
| 13 | /** | 13 | /** |
| 14 | * IoT TCP 分隔符帧编解码器 | 14 | * IoT TCP 分隔符帧编解码器 |
| @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { | @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { | ||
| 39 | private final byte[] delimiterBytes; | 39 | private final byte[] delimiterBytes; |
| 40 | 40 | ||
| 41 | public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { | 41 | public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { |
| 42 | - Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); | 42 | + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); |
| 43 | this.delimiterBytes = parseDelimiter(config.getDelimiter()); | 43 | this.delimiterBytes = parseDelimiter(config.getDelimiter()); |
| 44 | } | 44 | } |
| 45 | 45 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java
| @@ -7,7 +7,7 @@ import io.vertx.core.Handler; | @@ -7,7 +7,7 @@ import io.vertx.core.Handler; | ||
| 7 | import io.vertx.core.buffer.Buffer; | 7 | import io.vertx.core.buffer.Buffer; |
| 8 | import io.vertx.core.parsetools.RecordParser; | 8 | import io.vertx.core.parsetools.RecordParser; |
| 9 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | -import org.springframework.util.Assert; | 10 | +import cn.hutool.core.lang.Assert; |
| 11 | 11 | ||
| 12 | /** | 12 | /** |
| 13 | * IoT TCP 定长帧编解码器 | 13 | * IoT TCP 定长帧编解码器 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java
| @@ -7,7 +7,7 @@ import io.vertx.core.Handler; | @@ -7,7 +7,7 @@ import io.vertx.core.Handler; | ||
| 7 | import io.vertx.core.buffer.Buffer; | 7 | import io.vertx.core.buffer.Buffer; |
| 8 | import io.vertx.core.parsetools.RecordParser; | 8 | import io.vertx.core.parsetools.RecordParser; |
| 9 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | -import org.springframework.util.Assert; | 10 | +import cn.hutool.core.lang.Assert; |
| 11 | 11 | ||
| 12 | import java.util.concurrent.atomic.AtomicReference; | 12 | import java.util.concurrent.atomic.AtomicReference; |
| 13 | 13 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
| @@ -45,8 +45,8 @@ public class IotTcpDownstreamHandler { | @@ -45,8 +45,8 @@ public class IotTcpDownstreamHandler { | ||
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | // 2. 序列化 + 帧编码 | 47 | // 2. 序列化 + 帧编码 |
| 48 | - byte[] serializedData = serializer.serialize(message); | ||
| 49 | - Buffer frameData = codec.encode(serializedData); | 48 | + byte[] payload = serializer.serialize(message); |
| 49 | + Buffer frameData = codec.encode(payload); | ||
| 50 | 50 | ||
| 51 | // 3. 发送到设备 | 51 | // 3. 发送到设备 |
| 52 | boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); | 52 | boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
| @@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer; | @@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer; | ||
| 24 | import io.vertx.core.net.NetSocket; | 24 | import io.vertx.core.net.NetSocket; |
| 25 | import io.vertx.core.parsetools.RecordParser; | 25 | import io.vertx.core.parsetools.RecordParser; |
| 26 | import lombok.extern.slf4j.Slf4j; | 26 | import lombok.extern.slf4j.Slf4j; |
| 27 | -import org.springframework.util.Assert; | 27 | +import cn.hutool.core.lang.Assert; |
| 28 | 28 | ||
| 29 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | 29 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| 30 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 30 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 167 | // 1. 解析认证参数 | 167 | // 1. 解析认证参数 |
| 168 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | 168 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| 169 | Assert.notNull(authParams, "认证参数不能为空"); | 169 | Assert.notNull(authParams, "认证参数不能为空"); |
| 170 | - Assert.hasText(authParams.getUsername(), "username 不能为空"); | ||
| 171 | - Assert.hasText(authParams.getPassword(), "password 不能为空"); | 170 | + Assert.notBlank(authParams.getUsername(), "username 不能为空"); |
| 171 | + Assert.notBlank(authParams.getPassword(), "password 不能为空"); | ||
| 172 | 172 | ||
| 173 | // 2.1 执行认证 | 173 | // 2.1 执行认证 |
| 174 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | 174 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 204 | // 1. 解析注册参数 | 204 | // 1. 解析注册参数 |
| 205 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | 205 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| 206 | Assert.notNull(params, "注册参数不能为空"); | 206 | Assert.notNull(params, "注册参数不能为空"); |
| 207 | - Assert.hasText(params.getProductKey(), "productKey 不能为空"); | ||
| 208 | - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); | 207 | + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); |
| 208 | + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); | ||
| 209 | + Assert.notBlank(params.getSign(), "sign 不能为空"); | ||
| 209 | 210 | ||
| 210 | // 2. 调用动态注册 | 211 | // 2. 调用动态注册 |
| 211 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | 212 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
| @@ -47,8 +47,8 @@ public class IotTcpConnectionManager { | @@ -47,8 +47,8 @@ public class IotTcpConnectionManager { | ||
| 47 | * @param deviceId 设备 ID | 47 | * @param deviceId 设备 ID |
| 48 | * @param connectionInfo 连接信息 | 48 | * @param connectionInfo 连接信息 |
| 49 | */ | 49 | */ |
| 50 | - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { | ||
| 51 | - // 检查连接数是否已达上限 | 50 | + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { |
| 51 | + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) | ||
| 52 | if (connectionMap.size() >= maxConnections) { | 52 | if (connectionMap.size() >= maxConnections) { |
| 53 | throw new IllegalStateException("连接数已达上限: " + maxConnections); | 53 | throw new IllegalStateException("连接数已达上限: " + maxConnections); |
| 54 | } | 54 | } |
| @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { | @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { | ||
| 57 | if (oldSocket != null && oldSocket != socket) { | 57 | if (oldSocket != null && oldSocket != socket) { |
| 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", | 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 59 | deviceId, oldSocket.remoteAddress()); | 59 | deviceId, oldSocket.remoteAddress()); |
| 60 | - oldSocket.close(); | ||
| 61 | - // 清理旧连接的映射 | 60 | + // 先清理映射,再关闭连接 |
| 62 | connectionMap.remove(oldSocket); | 61 | connectionMap.remove(oldSocket); |
| 62 | + oldSocket.close(); | ||
| 63 | } | 63 | } |
| 64 | 64 | ||
| 65 | // 注册新连接 | 65 | // 注册新连接 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
| @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil; | @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 7 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; | 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; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; |
| @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket; | @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket; | ||
| 18 | import io.vertx.core.datagram.DatagramSocketOptions; | 18 | import io.vertx.core.datagram.DatagramSocketOptions; |
| 19 | import lombok.Getter; | 19 | import lombok.Getter; |
| 20 | import lombok.extern.slf4j.Slf4j; | 20 | import lombok.extern.slf4j.Slf4j; |
| 21 | -import org.springframework.util.Assert; | 21 | +import cn.hutool.core.lang.Assert; |
| 22 | 22 | ||
| 23 | /** | 23 | /** |
| 24 | * IoT UDP 协议实现 | 24 | * IoT UDP 协议实现 |
| @@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol { | @@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol { | ||
| 33 | /** | 33 | /** |
| 34 | * 协议配置 | 34 | * 协议配置 |
| 35 | */ | 35 | */ |
| 36 | - private final ProtocolInstanceProperties properties; | 36 | + private final ProtocolProperties properties; |
| 37 | /** | 37 | /** |
| 38 | * 服务器 ID(用于消息追踪,全局唯一) | 38 | * 服务器 ID(用于消息追踪,全局唯一) |
| 39 | */ | 39 | */ |
| @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol { | @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol { | ||
| 70 | */ | 70 | */ |
| 71 | private final IotMessageSerializer serializer; | 71 | private final IotMessageSerializer serializer; |
| 72 | 72 | ||
| 73 | - public IotUdpProtocol(ProtocolInstanceProperties properties) { | 73 | + public IotUdpProtocol(ProtocolProperties properties) { |
| 74 | IotUdpConfig udpConfig = properties.getUdp(); | 74 | IotUdpConfig udpConfig = properties.getUdp(); |
| 75 | Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); | 75 | Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); |
| 76 | this.properties = properties; | 76 | this.properties = properties; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
| @@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer; | @@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer; | ||
| 27 | import io.vertx.core.datagram.DatagramPacket; | 27 | import io.vertx.core.datagram.DatagramPacket; |
| 28 | import io.vertx.core.datagram.DatagramSocket; | 28 | import io.vertx.core.datagram.DatagramSocket; |
| 29 | import lombok.extern.slf4j.Slf4j; | 29 | import lombok.extern.slf4j.Slf4j; |
| 30 | -import org.springframework.util.Assert; | 30 | +import cn.hutool.core.lang.Assert; |
| 31 | 31 | ||
| 32 | import java.net.InetSocketAddress; | 32 | import java.net.InetSocketAddress; |
| 33 | import java.util.Map; | 33 | import java.util.Map; |
| @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler { | @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler { | ||
| 173 | // 1. 解析认证参数 | 173 | // 1. 解析认证参数 |
| 174 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | 174 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| 175 | Assert.notNull(authParams, "认证参数不能为空"); | 175 | Assert.notNull(authParams, "认证参数不能为空"); |
| 176 | - Assert.hasText(authParams.getUsername(), "username 不能为空"); | ||
| 177 | - Assert.hasText(authParams.getPassword(), "password 不能为空"); | 176 | + Assert.notBlank(authParams.getUsername(), "username 不能为空"); |
| 177 | + Assert.notBlank(authParams.getPassword(), "password 不能为空"); | ||
| 178 | 178 | ||
| 179 | // 2.1 执行认证 | 179 | // 2.1 执行认证 |
| 180 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | 180 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler { | @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler { | ||
| 218 | // 1. 解析注册参数 | 218 | // 1. 解析注册参数 |
| 219 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | 219 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| 220 | Assert.notNull(params, "注册参数不能为空"); | 220 | Assert.notNull(params, "注册参数不能为空"); |
| 221 | - Assert.hasText(params.getProductKey(), "productKey 不能为空"); | ||
| 222 | - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); | 221 | + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); |
| 222 | + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); | ||
| 223 | + Assert.notBlank(params.getSign(), "sign 不能为空"); | ||
| 223 | 224 | ||
| 224 | // 2. 调用动态注册 | 225 | // 2. 调用动态注册 |
| 225 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | 226 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
| @@ -9,6 +9,7 @@ import lombok.Data; | @@ -9,6 +9,7 @@ import lombok.Data; | ||
| 9 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | ||
| 11 | import java.net.InetSocketAddress; | 11 | import java.net.InetSocketAddress; |
| 12 | +import java.util.concurrent.CompletableFuture; | ||
| 12 | import java.util.concurrent.TimeUnit; | 13 | import java.util.concurrent.TimeUnit; |
| 13 | 14 | ||
| 14 | /** | 15 | /** |
| @@ -47,8 +48,8 @@ public class IotUdpSessionManager { | @@ -47,8 +48,8 @@ public class IotUdpSessionManager { | ||
| 47 | * @param deviceId 设备 ID | 48 | * @param deviceId 设备 ID |
| 48 | * @param sessionInfo 会话信息 | 49 | * @param sessionInfo 会话信息 |
| 49 | */ | 50 | */ |
| 50 | - public void registerSession(Long deviceId, SessionInfo sessionInfo) { | ||
| 51 | - // 检查是否为新设备,且会话数已达上限 | 51 | + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { |
| 52 | + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) | ||
| 52 | if (deviceSessionCache.getIfPresent(deviceId) == null | 53 | if (deviceSessionCache.getIfPresent(deviceId) == null |
| 53 | && deviceSessionCache.size() >= maxSessions) { | 54 | && deviceSessionCache.size() >= maxSessions) { |
| 54 | throw new IllegalStateException("会话数已达上限: " + maxSessions); | 55 | throw new IllegalStateException("会话数已达上限: " + maxSessions); |
| @@ -113,16 +114,21 @@ public class IotUdpSessionManager { | @@ -113,16 +114,21 @@ public class IotUdpSessionManager { | ||
| 113 | } | 114 | } |
| 114 | InetSocketAddress address = sessionInfo.getAddress(); | 115 | InetSocketAddress address = sessionInfo.getAddress(); |
| 115 | try { | 116 | try { |
| 117 | + // 使用 CompletableFuture 同步等待发送结果 | ||
| 118 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); | ||
| 116 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { | 119 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 117 | if (result.succeeded()) { | 120 | if (result.succeeded()) { |
| 118 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", | 121 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", |
| 119 | deviceId, buildAddressKey(address), data.length); | 122 | deviceId, buildAddressKey(address), data.length); |
| 120 | - return; | 123 | + future.complete(true); |
| 124 | + } else { | ||
| 125 | + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | ||
| 126 | + deviceId, buildAddressKey(address), result.cause()); | ||
| 127 | + future.complete(false); | ||
| 121 | } | 128 | } |
| 122 | - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | ||
| 123 | - deviceId, buildAddressKey(address), result.cause()); | ||
| 124 | }); | 129 | }); |
| 125 | - return true; | 130 | + // 同步等待结果,超时 5 秒 |
| 131 | + return future.get(5, TimeUnit.SECONDS); | ||
| 126 | } catch (Exception e) { | 132 | } catch (Exception e) { |
| 127 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); | 133 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); |
| 128 | return false; | 134 | return false; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java
| @@ -35,20 +35,4 @@ public class IotWebSocketConfig { | @@ -35,20 +35,4 @@ public class IotWebSocketConfig { | ||
| 35 | @NotNull(message = "空闲超时时间不能为空") | 35 | @NotNull(message = "空闲超时时间不能为空") |
| 36 | private Integer idleTimeoutSeconds = 60; | 36 | private Integer idleTimeoutSeconds = 60; |
| 37 | 37 | ||
| 38 | - /** | ||
| 39 | - * 是否启用 SSL(wss://) | ||
| 40 | - */ | ||
| 41 | - @NotNull(message = "是否启用 SSL 不能为空") | ||
| 42 | - private Boolean sslEnabled = false; | ||
| 43 | - | ||
| 44 | - /** | ||
| 45 | - * SSL 证书路径 | ||
| 46 | - */ | ||
| 47 | - private String sslCertPath; | ||
| 48 | - | ||
| 49 | - /** | ||
| 50 | - * SSL 私钥路径 | ||
| 51 | - */ | ||
| 52 | - private String sslKeyPath; | ||
| 53 | - | ||
| 54 | } | 38 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java
| @@ -6,7 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | @@ -6,7 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 8 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 8 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; | 9 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 10 | +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.IotProtocol; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; |
| @@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions; | @@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions; | ||
| 20 | import io.vertx.core.net.PemKeyCertOptions; | 21 | import io.vertx.core.net.PemKeyCertOptions; |
| 21 | import lombok.Getter; | 22 | import lombok.Getter; |
| 22 | import lombok.extern.slf4j.Slf4j; | 23 | import lombok.extern.slf4j.Slf4j; |
| 23 | -import org.springframework.util.Assert; | 24 | +import cn.hutool.core.lang.Assert; |
| 24 | 25 | ||
| 25 | /** | 26 | /** |
| 26 | * IoT WebSocket 协议实现 | 27 | * IoT WebSocket 协议实现 |
| @@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol { | @@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol { | ||
| 35 | /** | 36 | /** |
| 36 | * 协议配置 | 37 | * 协议配置 |
| 37 | */ | 38 | */ |
| 38 | - private final ProtocolInstanceProperties properties; | 39 | + private final ProtocolProperties properties; |
| 39 | /** | 40 | /** |
| 40 | * 服务器 ID(用于消息追踪,全局唯一) | 41 | * 服务器 ID(用于消息追踪,全局唯一) |
| 41 | */ | 42 | */ |
| @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol { | @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol { | ||
| 71 | */ | 72 | */ |
| 72 | private final IotMessageSerializer serializer; | 73 | private final IotMessageSerializer serializer; |
| 73 | 74 | ||
| 74 | - public IotWebSocketProtocol(ProtocolInstanceProperties properties) { | 75 | + public IotWebSocketProtocol(ProtocolProperties properties) { |
| 75 | Assert.notNull(properties, "协议实例配置不能为空"); | 76 | Assert.notNull(properties, "协议实例配置不能为空"); |
| 76 | Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); | 77 | Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); |
| 77 | this.properties = properties; | 78 | this.properties = properties; |
| @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol { | @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol { | ||
| 120 | .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) | 121 | .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) |
| 121 | .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) | 122 | .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) |
| 122 | .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); | 123 | .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); |
| 123 | - if (Boolean.TRUE.equals(wsConfig.getSslEnabled())) { | 124 | + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); |
| 125 | + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { | ||
| 124 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | 126 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 125 | - .setKeyPath(wsConfig.getSslKeyPath()) | ||
| 126 | - .setCertPath(wsConfig.getSslCertPath()); | 127 | + .setKeyPath(sslConfig.getSslKeyPath()) |
| 128 | + .setCertPath(sslConfig.getSslCertPath()); | ||
| 127 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | 129 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); |
| 128 | } | 130 | } |
| 129 | 131 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java
| @@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa | @@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa | ||
| 23 | import io.vertx.core.Handler; | 23 | import io.vertx.core.Handler; |
| 24 | import io.vertx.core.http.ServerWebSocket; | 24 | import io.vertx.core.http.ServerWebSocket; |
| 25 | import lombok.extern.slf4j.Slf4j; | 25 | import lombok.extern.slf4j.Slf4j; |
| 26 | -import org.springframework.util.Assert; | 26 | +import cn.hutool.core.lang.Assert; |
| 27 | 27 | ||
| 28 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | 28 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| 29 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 29 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 109 | // 1.2 解码消息 | 109 | // 1.2 解码消息 |
| 110 | message = serializer.deserialize(payload); | 110 | message = serializer.deserialize(payload); |
| 111 | Assert.notNull(message, "消息反序列化失败"); | 111 | Assert.notNull(message, "消息反序列化失败"); |
| 112 | - Assert.hasText(message.getMethod(), "method 不能为空"); | 112 | + Assert.notBlank(message.getMethod(), "method 不能为空"); |
| 113 | 113 | ||
| 114 | // 2. 根据消息类型路由处理 | 114 | // 2. 根据消息类型路由处理 |
| 115 | if (AUTH_METHOD.equals(message.getMethod())) { | 115 | if (AUTH_METHOD.equals(message.getMethod())) { |
| @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 150 | // 1. 解析认证参数 | 150 | // 1. 解析认证参数 |
| 151 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); | 151 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| 152 | Assert.notNull(authParams, "认证参数不能为空"); | 152 | Assert.notNull(authParams, "认证参数不能为空"); |
| 153 | - Assert.hasText(authParams.getUsername(), "username 不能为空"); | ||
| 154 | - Assert.hasText(authParams.getPassword(), "password 不能为空"); | 153 | + Assert.notBlank(authParams.getUsername(), "username 不能为空"); |
| 154 | + Assert.notBlank(authParams.getPassword(), "password 不能为空"); | ||
| 155 | 155 | ||
| 156 | // 2.1 执行认证 | 156 | // 2.1 执行认证 |
| 157 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | 157 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { | ||
| 187 | // 1. 解析注册参数 | 187 | // 1. 解析注册参数 |
| 188 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); | 188 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| 189 | Assert.notNull(params, "注册参数不能为空"); | 189 | Assert.notNull(params, "注册参数不能为空"); |
| 190 | - Assert.hasText(params.getProductKey(), "productKey 不能为空"); | ||
| 191 | - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); | 190 | + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); |
| 191 | + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); | ||
| 192 | + Assert.notBlank(params.getSign(), "sign 不能为空"); | ||
| 192 | 193 | ||
| 193 | // 2. 调用动态注册 | 194 | // 2. 调用动态注册 |
| 194 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | 195 | CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java
0 → 100644
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.service.device.message; | 1 | package cn.iocoder.yudao.module.iot.gateway.service.device.message; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | 5 | ||
| 5 | /** | 6 | /** |
| @@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | @@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 10 | public interface IotDeviceMessageService { | 11 | public interface IotDeviceMessageService { |
| 11 | 12 | ||
| 12 | /** | 13 | /** |
| 13 | - * 编码消息 | 14 | + * 序列化消息 |
| 14 | * | 15 | * |
| 15 | * @param message 消息 | 16 | * @param message 消息 |
| 16 | * @param productKey 产品 Key | 17 | * @param productKey 产品 Key |
| 17 | * @param deviceName 设备名称 | 18 | * @param deviceName 设备名称 |
| 18 | - * @return 编码后的消息内容 | 19 | + * @return 序列化后的消息内容 |
| 19 | */ | 20 | */ |
| 20 | - byte[] encodeDeviceMessage(IotDeviceMessage message, | ||
| 21 | - String productKey, String deviceName); | 21 | + byte[] serializeDeviceMessage(IotDeviceMessage message, |
| 22 | + String productKey, String deviceName); | ||
| 22 | 23 | ||
| 23 | /** | 24 | /** |
| 24 | - * 编码消息 | 25 | + * 序列化消息 |
| 25 | * | 26 | * |
| 26 | - * @param message 消息 | ||
| 27 | - * @param codecType 编解码器类型 | ||
| 28 | - * @return 编码后的消息内容 | 27 | + * @param message 消息 |
| 28 | + * @param serializeType 序列化类型 | ||
| 29 | + * @return 序列化后的消息内容 | ||
| 29 | */ | 30 | */ |
| 30 | - byte[] encodeDeviceMessage(IotDeviceMessage message, | ||
| 31 | - String codecType); | 31 | + byte[] serializeDeviceMessage(IotDeviceMessage message, |
| 32 | + IotSerializeTypeEnum serializeType); | ||
| 32 | 33 | ||
| 33 | /** | 34 | /** |
| 34 | - * 解码消息 | 35 | + * 反序列化消息 |
| 35 | * | 36 | * |
| 36 | * @param bytes 消息内容 | 37 | * @param bytes 消息内容 |
| 37 | * @param productKey 产品 Key | 38 | * @param productKey 产品 Key |
| 38 | * @param deviceName 设备名称 | 39 | * @param deviceName 设备名称 |
| 39 | - * @return 解码后的消息内容 | 40 | + * @return 反序列化后的消息内容 |
| 40 | */ | 41 | */ |
| 41 | - IotDeviceMessage decodeDeviceMessage(byte[] bytes, | ||
| 42 | - String productKey, String deviceName); | 42 | + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, |
| 43 | + String productKey, String deviceName); | ||
| 43 | 44 | ||
| 44 | /** | 45 | /** |
| 45 | - * 解码消息 | 46 | + * 反序列化消息 |
| 46 | * | 47 | * |
| 47 | - * @param bytes 消息内容 | ||
| 48 | - * @param codecType 编解码器类型 | ||
| 49 | - * @return 解码后的消息内容 | 48 | + * @param bytes 消息内容 |
| 49 | + * @param serializeType 序列化类型 | ||
| 50 | + * @return 反序列化后的消息内容 | ||
| 50 | */ | 51 | */ |
| 51 | - IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); | 52 | + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType); |
| 52 | 53 | ||
| 53 | /** | 54 | /** |
| 54 | * 发送消息 | 55 | * 发送消息 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.service.device.message; | 1 | package cn.iocoder.yudao.module.iot.gateway.service.device.message; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 3 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | 8 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; |
| 8 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 9 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | 10 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; | ||
| 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 11 | import jakarta.annotation.Resource; | 13 | import jakarta.annotation.Resource; |
| 12 | import lombok.extern.slf4j.Slf4j; | 14 | import lombok.extern.slf4j.Slf4j; |
| 13 | import org.springframework.stereotype.Service; | 15 | import org.springframework.stereotype.Service; |
| 14 | 16 | ||
| 15 | import java.time.LocalDateTime; | 17 | import java.time.LocalDateTime; |
| 16 | -import java.util.List; | ||
| 17 | -import java.util.Map; | ||
| 18 | 18 | ||
| 19 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 19 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 20 | import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; | 20 | import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; |
| @@ -28,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC | @@ -28,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC | ||
| 28 | @Slf4j | 28 | @Slf4j |
| 29 | public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | 29 | public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 30 | 30 | ||
| 31 | - /** | ||
| 32 | - * 编解码器 | ||
| 33 | - */ | ||
| 34 | - private final Map<String, IotDeviceMessageCodec> codes; | ||
| 35 | - | ||
| 36 | @Resource | 31 | @Resource |
| 37 | private IotDeviceService deviceService; | 32 | private IotDeviceService deviceService; |
| 38 | 33 | ||
| 39 | @Resource | 34 | @Resource |
| 40 | private IotDeviceMessageProducer deviceMessageProducer; | 35 | private IotDeviceMessageProducer deviceMessageProducer; |
| 41 | 36 | ||
| 42 | - public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) { | ||
| 43 | - this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type); | ||
| 44 | - } | 37 | + @Resource |
| 38 | + private IotMessageSerializerManager messageSerializerManager; | ||
| 45 | 39 | ||
| 46 | @Override | 40 | @Override |
| 47 | - public byte[] encodeDeviceMessage(IotDeviceMessage message, | ||
| 48 | - String productKey, String deviceName) { | 41 | + public byte[] serializeDeviceMessage(IotDeviceMessage message, |
| 42 | + String productKey, String deviceName) { | ||
| 49 | // 1.1 获取设备信息 | 43 | // 1.1 获取设备信息 |
| 50 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); | 44 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 51 | if (device == null) { | 45 | if (device == null) { |
| 52 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); | 46 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); |
| 53 | } | 47 | } |
| 54 | - // 1.2 获取编解码器 | ||
| 55 | - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); | ||
| 56 | - if (codec == null) { | ||
| 57 | - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); | ||
| 58 | - } | 48 | + // 1.2 获取序列化器 |
| 49 | + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); | ||
| 50 | + Assert.notNull(serializeType, "设备序列化类型不能为空"); | ||
| 59 | 51 | ||
| 60 | - // 2. 编码消息 | ||
| 61 | - return codec.encode(message); | 52 | + // 2. 序列化消息 |
| 53 | + return serializeDeviceMessage(message, serializeType); | ||
| 62 | } | 54 | } |
| 63 | 55 | ||
| 64 | @Override | 56 | @Override |
| 65 | - public byte[] encodeDeviceMessage(IotDeviceMessage message, | ||
| 66 | - String codecType) { | ||
| 67 | - // 1. 获取编解码器 | ||
| 68 | - IotDeviceMessageCodec codec = codes.get(codecType); | ||
| 69 | - if (codec == null) { | ||
| 70 | - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); | 57 | + public byte[] serializeDeviceMessage(IotDeviceMessage message, |
| 58 | + IotSerializeTypeEnum serializeType) { | ||
| 59 | + // 1. 获取序列化器 | ||
| 60 | + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); | ||
| 61 | + if (serializer == null) { | ||
| 62 | + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); | ||
| 71 | } | 63 | } |
| 72 | 64 | ||
| 73 | - // 2. 编码消息 | ||
| 74 | - return codec.encode(message); | 65 | + // 2. 序列化消息 |
| 66 | + return serializer.serialize(message); | ||
| 75 | } | 67 | } |
| 76 | 68 | ||
| 77 | @Override | 69 | @Override |
| 78 | - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, | ||
| 79 | - String productKey, String deviceName) { | 70 | + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, |
| 71 | + String productKey, String deviceName) { | ||
| 80 | // 1.1 获取设备信息 | 72 | // 1.1 获取设备信息 |
| 81 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); | 73 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 82 | if (device == null) { | 74 | if (device == null) { |
| 83 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); | 75 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); |
| 84 | } | 76 | } |
| 85 | - // 1.2 获取编解码器 | ||
| 86 | - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); | ||
| 87 | - if (codec == null) { | ||
| 88 | - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); | ||
| 89 | - } | 77 | + // 1.2 获取序列化器 |
| 78 | + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); | ||
| 79 | + Assert.notNull(serializeType, "设备序列化类型不能为空"); | ||
| 90 | 80 | ||
| 91 | - // 2. 解码消息 | ||
| 92 | - return codec.decode(bytes); | 81 | + // 2. 反序列化消息 |
| 82 | + return deserializeDeviceMessage(bytes, serializeType); | ||
| 93 | } | 83 | } |
| 94 | 84 | ||
| 95 | @Override | 85 | @Override |
| 96 | - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { | ||
| 97 | - // 1. 获取编解码器 | ||
| 98 | - IotDeviceMessageCodec codec = codes.get(codecType); | ||
| 99 | - if (codec == null) { | ||
| 100 | - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); | 86 | + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType) { |
| 87 | + // 1. 获取序列化器 | ||
| 88 | + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); | ||
| 89 | + if (serializer == null) { | ||
| 90 | + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); | ||
| 101 | } | 91 | } |
| 102 | 92 | ||
| 103 | - // 2. 解码消息 | ||
| 104 | - return codec.decode(bytes); | 93 | + // 2. 反序列化消息 |
| 94 | + return serializer.deserialize(bytes); | ||
| 105 | } | 95 | } |
| 106 | 96 | ||
| 107 | @Override | 97 | @Override |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| @@ -38,6 +38,12 @@ public final class IotMqttTopicUtils { | @@ -38,6 +38,12 @@ public final class IotMqttTopicUtils { | ||
| 38 | */ | 38 | */ |
| 39 | public static final String MQTT_EVENT_PATH = "/mqtt/event"; | 39 | public static final String MQTT_EVENT_PATH = "/mqtt/event"; |
| 40 | 40 | ||
| 41 | + /** | ||
| 42 | + * MQTT ACL 接口路径 | ||
| 43 | + * 对应 EMQX HTTP ACL 插件的 ACL 请求接口 | ||
| 44 | + */ | ||
| 45 | + public static final String MQTT_ACL_PATH = "/mqtt/acl"; | ||
| 46 | + | ||
| 41 | // ========== 工具方法 ========== | 47 | // ========== 工具方法 ========== |
| 42 | 48 | ||
| 43 | /** | 49 | /** |
| @@ -63,4 +69,50 @@ public final class IotMqttTopicUtils { | @@ -63,4 +69,50 @@ public final class IotMqttTopicUtils { | ||
| 63 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; | 69 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; |
| 64 | } | 70 | } |
| 65 | 71 | ||
| 66 | -} | ||
| 67 | \ No newline at end of file | 72 | \ No newline at end of file |
| 73 | + /** | ||
| 74 | + * 校验主题是否允许订阅 | ||
| 75 | + * <p> | ||
| 76 | + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头, | ||
| 77 | + * 或者是通配符形式 /sys/{productKey}/{deviceName}/# | ||
| 78 | + * | ||
| 79 | + * @param topic 订阅的主题 | ||
| 80 | + * @param productKey 产品 Key | ||
| 81 | + * @param deviceName 设备名称 | ||
| 82 | + * @return 是否允许订阅 | ||
| 83 | + */ | ||
| 84 | + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) { | ||
| 85 | + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { | ||
| 86 | + return false; | ||
| 87 | + } | ||
| 88 | + // 构建设备主题前缀 | ||
| 89 | + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; | ||
| 90 | + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式 | ||
| 91 | + return topic.startsWith(deviceTopicPrefix) | ||
| 92 | + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 校验主题是否允许发布 | ||
| 97 | + * <p> | ||
| 98 | + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,且不允许包含通配符(+/#)。 | ||
| 99 | + * | ||
| 100 | + * @param topic 发布的主题 | ||
| 101 | + * @param productKey 产品 Key | ||
| 102 | + * @param deviceName 设备名称 | ||
| 103 | + * @return 是否允许发布 | ||
| 104 | + */ | ||
| 105 | + // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用; | ||
| 106 | + // TODO @AI:那你改下 mqtt,也调用!!! | ||
| 107 | + public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { | ||
| 108 | + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { | ||
| 109 | + return false; | ||
| 110 | + } | ||
| 111 | + // MQTT publish topic 不允许包含通配符,但这里做一次兜底校验 | ||
| 112 | + if (topic.contains("#") || topic.contains("+")) { | ||
| 113 | + return false; | ||
| 114 | + } | ||
| 115 | + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; | ||
| 116 | + return topic.startsWith(deviceTopicPrefix); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -48,7 +48,7 @@ yudao: | @@ -48,7 +48,7 @@ yudao: | ||
| 48 | # 针对引入的 HTTP 组件的配置 | 48 | # 针对引入的 HTTP 组件的配置 |
| 49 | # ==================================== | 49 | # ==================================== |
| 50 | - id: http-json | 50 | - id: http-json |
| 51 | - type: http | 51 | + protocol: http |
| 52 | port: 8092 | 52 | port: 8092 |
| 53 | enabled: false | 53 | enabled: false |
| 54 | http: | 54 | http: |
| @@ -57,9 +57,9 @@ yudao: | @@ -57,9 +57,9 @@ yudao: | ||
| 57 | # 针对引入的 TCP 组件的配置 | 57 | # 针对引入的 TCP 组件的配置 |
| 58 | # ==================================== | 58 | # ==================================== |
| 59 | - id: tcp-json | 59 | - id: tcp-json |
| 60 | - type: tcp | ||
| 61 | - port: 8091 | ||
| 62 | enabled: false | 60 | enabled: false |
| 61 | + protocol: tcp | ||
| 62 | + port: 8091 | ||
| 63 | serialize: json | 63 | serialize: json |
| 64 | tcp: | 64 | tcp: |
| 65 | max-connections: 1000 | 65 | max-connections: 1000 |
| @@ -79,9 +79,9 @@ yudao: | @@ -79,9 +79,9 @@ yudao: | ||
| 79 | # 针对引入的 UDP 组件的配置 | 79 | # 针对引入的 UDP 组件的配置 |
| 80 | # ==================================== | 80 | # ==================================== |
| 81 | - id: udp-json | 81 | - id: udp-json |
| 82 | - type: udp | ||
| 83 | - port: 8093 | ||
| 84 | enabled: false | 82 | enabled: false |
| 83 | + protocol: udp | ||
| 84 | + port: 8093 | ||
| 85 | serialize: json | 85 | serialize: json |
| 86 | udp: | 86 | udp: |
| 87 | max-sessions: 1000 # 最大会话数 | 87 | max-sessions: 1000 # 最大会话数 |
| @@ -92,9 +92,9 @@ yudao: | @@ -92,9 +92,9 @@ yudao: | ||
| 92 | # 针对引入的 WebSocket 组件的配置 | 92 | # 针对引入的 WebSocket 组件的配置 |
| 93 | # ==================================== | 93 | # ==================================== |
| 94 | - id: websocket-json | 94 | - id: websocket-json |
| 95 | - type: websocket | 95 | + enabled: false |
| 96 | + protocol: websocket | ||
| 96 | port: 8094 | 97 | port: 8094 |
| 97 | - enabled: true | ||
| 98 | serialize: json | 98 | serialize: json |
| 99 | websocket: | 99 | websocket: |
| 100 | path: /ws | 100 | path: /ws |
| @@ -106,21 +106,68 @@ yudao: | @@ -106,21 +106,68 @@ yudao: | ||
| 106 | # 针对引入的 CoAP 组件的配置 | 106 | # 针对引入的 CoAP 组件的配置 |
| 107 | # ==================================== | 107 | # ==================================== |
| 108 | - id: coap-json | 108 | - id: coap-json |
| 109 | - type: coap | 109 | + enabled: false |
| 110 | + protocol: coap | ||
| 110 | port: 5683 | 111 | port: 5683 |
| 111 | - enabled: true | ||
| 112 | coap: | 112 | coap: |
| 113 | max-message-size: 1024 # 最大消息大小(字节) | 113 | max-message-size: 1024 # 最大消息大小(字节) |
| 114 | ack-timeout-ms: 2000 # ACK 超时时间(毫秒) | 114 | ack-timeout-ms: 2000 # ACK 超时时间(毫秒) |
| 115 | max-retransmit: 4 # 最大重传次数 | 115 | max-retransmit: 4 # 最大重传次数 |
| 116 | - | ||
| 117 | - # 协议配置(旧版,保持兼容) | ||
| 118 | - protocol: | 116 | + # ==================================== |
| 117 | + # 针对引入的 MQTT 组件的配置 | ||
| 118 | + # ==================================== | ||
| 119 | + - id: mqtt-json | ||
| 120 | + enabled: true | ||
| 121 | + protocol: mqtt | ||
| 122 | + port: 1883 | ||
| 123 | + serialize: json | ||
| 124 | + mqtt: | ||
| 125 | + max-message-size: 8192 # 最大消息大小(字节) | ||
| 126 | + connect-timeout-seconds: 60 # 连接超时时间(秒) | ||
| 127 | + ssl-enabled: false # 是否启用 SSL | ||
| 119 | # ==================================== | 128 | # ==================================== |
| 120 | # 针对引入的 EMQX 组件的配置 | 129 | # 针对引入的 EMQX 组件的配置 |
| 121 | # ==================================== | 130 | # ==================================== |
| 122 | - emqx: | 131 | + - id: emqx-1 |
| 123 | enabled: false | 132 | enabled: false |
| 133 | + protocol: emqx | ||
| 134 | + port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) | ||
| 135 | + emqx: | ||
| 136 | + mqtt-host: 127.0.0.1 # MQTT Broker 地址 | ||
| 137 | + mqtt-port: 1883 # MQTT Broker 端口 | ||
| 138 | + mqtt-username: admin # MQTT 用户名 | ||
| 139 | + mqtt-password: public # MQTT 密码 | ||
| 140 | + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID | ||
| 141 | + mqtt-ssl: false # 是否开启 SSL | ||
| 142 | + mqtt-topics: | ||
| 143 | + - "/sys/#" # 系统主题 | ||
| 144 | + mqtt-qos: 1 # 默认 QoS | ||
| 145 | + clean-session: true # 是否启用 Clean Session (默认: true) | ||
| 146 | + keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) | ||
| 147 | + max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 | ||
| 148 | + connect-timeout-seconds: 10 # 连接超时,单位:秒 | ||
| 149 | + reconnect-delay-ms: 5000 # 重连延迟,单位:毫秒 | ||
| 150 | + # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! | ||
| 151 | + # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true | ||
| 152 | + trust-all: true # 在 dev 环境可以设为 true | ||
| 153 | + # EMQX HTTP Hook 回调网关的 HTTPS 配置(可选) | ||
| 154 | + http: | ||
| 155 | + ssl-enabled: false | ||
| 156 | + # ssl-cert-path: "path/to/server.crt" | ||
| 157 | + # ssl-key-path: "path/to/server.key" | ||
| 158 | + # 遗嘱消息配置 (用于网关异常下线时通知其他系统) | ||
| 159 | + will: | ||
| 160 | + enabled: true # 生产环境强烈建议开启 | ||
| 161 | + topic: "gateway/status/iot-gateway-mqtt" # 遗嘱消息主题 | ||
| 162 | + payload: "offline" # 遗嘱消息负载 | ||
| 163 | + qos: 1 # 遗嘱消息 QoS | ||
| 164 | + retain: true # 遗嘱消息是否保留 | ||
| 165 | + # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) | ||
| 166 | + ssl-options: | ||
| 167 | + key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 | ||
| 168 | + key-store-password: "your-keystore-password" # 客户端证书库密码 | ||
| 169 | + trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 | ||
| 170 | + trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 | ||
| 124 | http-port: 8090 # MQTT HTTP 服务端口 | 171 | http-port: 8090 # MQTT HTTP 服务端口 |
| 125 | mqtt-host: 127.0.0.1 # MQTT Broker 地址 | 172 | mqtt-host: 127.0.0.1 # MQTT Broker 地址 |
| 126 | mqtt-port: 1883 # MQTT Broker 端口 | 173 | mqtt-port: 1883 # MQTT Broker 端口 |
| @@ -153,25 +200,8 @@ yudao: | @@ -153,25 +200,8 @@ yudao: | ||
| 153 | # ==================================== | 200 | # ==================================== |
| 154 | # 针对引入的 MQTT 组件的配置 | 201 | # 针对引入的 MQTT 组件的配置 |
| 155 | # ==================================== | 202 | # ==================================== |
| 156 | - mqtt: | ||
| 157 | - enabled: false | ||
| 158 | - port: 1883 | ||
| 159 | - max-message-size: 8192 | ||
| 160 | - connect-timeout-seconds: 60 | ||
| 161 | - ssl-enabled: false | ||
| 162 | - # ==================================== | ||
| 163 | - # 针对引入的 MQTT WebSocket 组件的配置 | ||
| 164 | - # ==================================== | ||
| 165 | - mqtt-ws: | ||
| 166 | - enabled: true # 是否启用 MQTT WebSocket | ||
| 167 | - port: 8083 # WebSocket 服务端口 | ||
| 168 | - path: /mqtt # WebSocket 路径 | ||
| 169 | - max-message-size: 8192 # 最大消息大小(字节) | ||
| 170 | - max-frame-size: 65536 # 最大帧大小(字节) | ||
| 171 | - connect-timeout-seconds: 60 # 连接超时时间(秒) | ||
| 172 | - keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) | ||
| 173 | - ssl-enabled: false # 是否启用 SSL(wss://) | ||
| 174 | - sub-protocol: mqtt # WebSocket 子协议 | 203 | + # 协议配置(旧版,保持兼容) |
| 204 | + protocol: | ||
| 175 | modbus-tcp: | 205 | modbus-tcp: |
| 176 | enabled: true | 206 | enabled: true |
| 177 | config-refresh-interval: 30 # 配置刷新间隔(秒) | 207 | config-refresh-interval: 30 # 配置刷新间隔(秒) |
| @@ -194,7 +224,6 @@ logging: | @@ -194,7 +224,6 @@ logging: | ||
| 194 | cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG | 224 | cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG |
| 195 | cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG | 225 | cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG |
| 196 | cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG | 226 | cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG |
| 197 | - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG | ||
| 198 | cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG | 227 | cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG |
| 199 | cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG | 228 | cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG |
| 200 | # 根日志级别 | 229 | # 根日志级别 |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java
| @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 12 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | ||
| 12 | import lombok.extern.slf4j.Slf4j; | 13 | import lombok.extern.slf4j.Slf4j; |
| 13 | import org.eclipse.californium.core.CoapClient; | 14 | import org.eclipse.californium.core.CoapClient; |
| 14 | import org.eclipse.californium.core.CoapResponse; | 15 | import org.eclipse.californium.core.CoapResponse; |
| @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { | @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { | ||
| 203 | // 1.1 构建请求 | 204 | // 1.1 构建请求 |
| 204 | String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); | 205 | String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); |
| 205 | // 1.2 构建请求参数 | 206 | // 1.2 构建请求参数 |
| 206 | - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); | ||
| 207 | - reqDTO.setProductKey(PRODUCT_KEY); | ||
| 208 | - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); | ||
| 209 | - reqDTO.setProductSecret("test-product-secret"); | 207 | + String deviceName = "test-" + System.currentTimeMillis(); |
| 208 | + String productSecret = "test-product-secret"; // 替换为实际的 productSecret | ||
| 209 | + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); | ||
| 210 | + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() | ||
| 211 | + .setProductKey(PRODUCT_KEY) | ||
| 212 | + .setDeviceName(deviceName) | ||
| 213 | + .setSign(sign); | ||
| 210 | String payload = JsonUtils.toJsonString(reqDTO); | 214 | String payload = JsonUtils.toJsonString(reqDTO); |
| 211 | // 1.3 输出请求 | 215 | // 1.3 输出请求 |
| 212 | log.info("[testDeviceRegister][请求 URI: {}]", uri); | 216 | log.info("[testDeviceRegister][请求 URI: {}]", uri); |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java
0 → 100644
| 1 | +/** | ||
| 2 | + * IoT 网关 EMQX 协议集成测试包 | ||
| 3 | + * | ||
| 4 | + * <p> | ||
| 5 | + * 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。 | ||
| 6 | + * | ||
| 7 | + * @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt | ||
| 8 | + * | ||
| 9 | + * <h2>架构</h2> | ||
| 10 | + * <pre> | ||
| 11 | + * +--------+ MQTT +-------------+ HTTP Hook +---------+ | ||
| 12 | + * | 设备 | --------------> | EMQX Broker | ----------------> | 网关 | | ||
| 13 | + * +--------+ +-------------+ +---------+ | ||
| 14 | + * </pre> | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java
| @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 10 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 10 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 13 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | ||
| 13 | import lombok.extern.slf4j.Slf4j; | 14 | import lombok.extern.slf4j.Slf4j; |
| 14 | import org.junit.jupiter.api.Disabled; | 15 | import org.junit.jupiter.api.Disabled; |
| 15 | import org.junit.jupiter.api.Test; | 16 | import org.junit.jupiter.api.Test; |
| @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { | @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { | ||
| 158 | // 1.1 构建请求 | 159 | // 1.1 构建请求 |
| 159 | String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); | 160 | String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); |
| 160 | // 1.2 构建请求参数 | 161 | // 1.2 构建请求参数 |
| 162 | + String deviceName = "test-" + System.currentTimeMillis(); | ||
| 163 | + String productSecret = "test-product-secret"; // 替换为实际的 productSecret | ||
| 164 | + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); | ||
| 161 | IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() | 165 | IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() |
| 162 | .setProductKey(PRODUCT_KEY) | 166 | .setProductKey(PRODUCT_KEY) |
| 163 | - .setDeviceName("test-" + System.currentTimeMillis()) | ||
| 164 | - .setProductSecret("test-product-secret"); | 167 | + .setDeviceName(deviceName) |
| 168 | + .setSign(sign); | ||
| 165 | String payload = JsonUtils.toJsonString(reqDTO); | 169 | String payload = JsonUtils.toJsonString(reqDTO); |
| 166 | // 1.3 输出请求 | 170 | // 1.3 输出请求 |
| 167 | log.info("[testDeviceRegister][请求 URL: {}]", url); | 171 | log.info("[testDeviceRegister][请求 URL: {}]", url); |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 7 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 8 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 9 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 13 | -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; | 10 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | ||
| 12 | +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | ||
| 14 | import io.netty.handler.codec.mqtt.MqttQoS; | 13 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 15 | import io.vertx.core.Vertx; | 14 | import io.vertx.core.Vertx; |
| 16 | import io.vertx.core.buffer.Buffer; | 15 | import io.vertx.core.buffer.Buffer; |
| @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; | @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; | ||
| 23 | import org.junit.jupiter.api.Test; | 22 | import org.junit.jupiter.api.Test; |
| 24 | 23 | ||
| 25 | import java.util.concurrent.CompletableFuture; | 24 | import java.util.concurrent.CompletableFuture; |
| 26 | -import java.util.concurrent.CountDownLatch; | ||
| 27 | import java.util.concurrent.TimeUnit; | 25 | import java.util.concurrent.TimeUnit; |
| 28 | 26 | ||
| 29 | /** | 27 | /** |
| @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 59 | 57 | ||
| 60 | private static Vertx vertx; | 58 | private static Vertx vertx; |
| 61 | 59 | ||
| 62 | - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== | 60 | + // ===================== 序列化器 ===================== |
| 63 | 61 | ||
| 64 | - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); | 62 | + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); |
| 65 | 63 | ||
| 66 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== | 64 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 67 | 65 | ||
| @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 88 | */ | 86 | */ |
| 89 | @Test | 87 | @Test |
| 90 | public void testAuth() throws Exception { | 88 | public void testAuth() throws Exception { |
| 91 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 92 | - | ||
| 93 | // 1. 构建认证信息 | 89 | // 1. 构建认证信息 |
| 94 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 90 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 95 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", | 91 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", |
| 96 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 92 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 97 | 93 | ||
| 98 | // 2. 创建客户端并连接 | 94 | // 2. 创建客户端并连接 |
| 99 | - MqttClient client = connect(authInfo); | ||
| 100 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 101 | - .onComplete(ar -> { | ||
| 102 | - if (ar.succeeded()) { | ||
| 103 | - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 104 | - // 断开连接 | ||
| 105 | - client.disconnect() | ||
| 106 | - .onComplete(disconnectAr -> { | ||
| 107 | - if (disconnectAr.succeeded()) { | ||
| 108 | - log.info("[testAuth][断开连接成功]"); | ||
| 109 | - } else { | ||
| 110 | - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); | ||
| 111 | - } | ||
| 112 | - latch.countDown(); | ||
| 113 | - }); | ||
| 114 | - } else { | ||
| 115 | - log.error("[testAuth][连接失败]", ar.cause()); | ||
| 116 | - latch.countDown(); | ||
| 117 | - } | ||
| 118 | - }); | ||
| 119 | - | ||
| 120 | - // 3. 等待测试完成 | ||
| 121 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 122 | - if (!completed) { | ||
| 123 | - log.warn("[testAuth][测试超时]"); | 95 | + MqttClient client = createClient(authInfo); |
| 96 | + try { | ||
| 97 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 98 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 99 | + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 100 | + } finally { | ||
| 101 | + disconnect(client); | ||
| 124 | } | 102 | } |
| 125 | } | 103 | } |
| 126 | 104 | ||
| @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 135 | MqttClient client = connectAndAuth(); | 113 | MqttClient client = connectAndAuth(); |
| 136 | log.info("[testPropertyPost][连接认证成功]"); | 114 | log.info("[testPropertyPost][连接认证成功]"); |
| 137 | 115 | ||
| 138 | - // 2. 订阅 _reply 主题 | ||
| 139 | - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 140 | - subscribeReply(client, replyTopic); | ||
| 141 | - | ||
| 142 | - // 3. 构建属性上报消息 | ||
| 143 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 144 | - IdUtil.fastSimpleUUID(), | ||
| 145 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 146 | - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 147 | - .put("width", 1) | ||
| 148 | - .put("height", "2") | ||
| 149 | - .build()), | ||
| 150 | - null, null, null); | ||
| 151 | - | ||
| 152 | - // 4. 发布消息并等待响应 | ||
| 153 | - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 154 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 155 | - log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 156 | - | ||
| 157 | - // 5. 断开连接 | ||
| 158 | - disconnect(client); | 116 | + try { |
| 117 | + // 2.1 构建属性上报消息 | ||
| 118 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 119 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 120 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 121 | + .put("width", 1) | ||
| 122 | + .put("height", "2") | ||
| 123 | + .build())); | ||
| 124 | + | ||
| 125 | + // 2.2 订阅 _reply 主题 | ||
| 126 | + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 127 | + subscribe(client, replyTopic); | ||
| 128 | + | ||
| 129 | + // 2.2 发布消息并等待响应 | ||
| 130 | + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 131 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 132 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 133 | + } finally { | ||
| 134 | + disconnect(client); | ||
| 135 | + } | ||
| 159 | } | 136 | } |
| 160 | 137 | ||
| 161 | // ===================== 直连设备事件上报测试 ===================== | 138 | // ===================== 直连设备事件上报测试 ===================== |
| @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 169 | MqttClient client = connectAndAuth(); | 146 | MqttClient client = connectAndAuth(); |
| 170 | log.info("[testEventPost][连接认证成功]"); | 147 | log.info("[testEventPost][连接认证成功]"); |
| 171 | 148 | ||
| 172 | - // 2. 订阅 _reply 主题 | ||
| 173 | - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 174 | - subscribeReply(client, replyTopic); | ||
| 175 | - | ||
| 176 | - // 3. 构建事件上报消息 | ||
| 177 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 178 | - IdUtil.fastSimpleUUID(), | ||
| 179 | - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 180 | - IotDeviceEventPostReqDTO.of( | ||
| 181 | - "eat", | ||
| 182 | - MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 183 | - System.currentTimeMillis()), | ||
| 184 | - null, null, null); | ||
| 185 | - | ||
| 186 | - // 4. 发布消息并等待响应 | ||
| 187 | - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 188 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 189 | - log.info("[testEventPost][响应消息: {}]", response); | ||
| 190 | - | ||
| 191 | - // 5. 断开连接 | ||
| 192 | - disconnect(client); | 149 | + try { |
| 150 | + // 2.1 构建事件上报消息 | ||
| 151 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 152 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 153 | + IotDeviceEventPostReqDTO.of( | ||
| 154 | + "eat", | ||
| 155 | + MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 156 | + System.currentTimeMillis())); | ||
| 157 | + | ||
| 158 | + // 2.2 订阅 _reply 主题 | ||
| 159 | + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 160 | + subscribe(client, replyTopic); | ||
| 161 | + | ||
| 162 | + // 3. 发布消息并等待响应 | ||
| 163 | + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 164 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 165 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 166 | + } finally { | ||
| 167 | + disconnect(client); | ||
| 168 | + } | ||
| 193 | } | 169 | } |
| 194 | 170 | ||
| 195 | // ===================== 设备动态注册测试(一型一密) ===================== | 171 | // ===================== 设备动态注册测试(一型一密) ===================== |
| @@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 197 | /** | 173 | /** |
| 198 | * 直连设备动态注册测试(一型一密) | 174 | * 直连设备动态注册测试(一型一密) |
| 199 | * <p> | 175 | * <p> |
| 200 | - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) | 176 | + * 认证方式: |
| 177 | + * - clientId: 任意值 + "|authType=register|" 后缀 | ||
| 178 | + * - username: {deviceName}&{productKey}(与普通认证相同) | ||
| 179 | + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256) | ||
| 201 | * <p> | 180 | * <p> |
| 202 | - * 注意:此接口不需要认证 | 181 | + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证 |
| 203 | */ | 182 | */ |
| 204 | @Test | 183 | @Test |
| 205 | public void testDeviceRegister() throws Exception { | 184 | public void testDeviceRegister() throws Exception { |
| 206 | - // 1. 连接并认证(使用已有设备连接) | ||
| 207 | - MqttClient client = connectAndAuth(); | ||
| 208 | - log.info("[testDeviceRegister][连接认证成功]"); | ||
| 209 | - | ||
| 210 | - // 2.1 构建注册消息 | ||
| 211 | - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 212 | - registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 213 | - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); | ||
| 214 | - registerReqDTO.setProductSecret("test-product-secret"); | ||
| 215 | - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | ||
| 216 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | ||
| 217 | - // 2.2 订阅 _reply 主题 | ||
| 218 | - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", | ||
| 219 | - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 220 | - subscribeReply(client, replyTopic); | ||
| 221 | - | ||
| 222 | - // 3. 发布消息并等待响应 | ||
| 223 | - String topic = String.format("/sys/%s/%s/thing/auth/register", | ||
| 224 | - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 225 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 226 | - log.info("[testDeviceRegister][响应消息: {}]", response); | ||
| 227 | - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 228 | - | ||
| 229 | - // 4. 断开连接 | ||
| 230 | - disconnect(client); | 185 | + // 1.1 构建注册参数 |
| 186 | + String deviceName = "test-mqtt-" + System.currentTimeMillis(); | ||
| 187 | + String productSecret = "test-product-secret"; // 替换为实际的 productSecret | ||
| 188 | + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); | ||
| 189 | + // 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀) | ||
| 190 | + String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|"; | ||
| 191 | + String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName); | ||
| 192 | + log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]", | ||
| 193 | + clientId, username, sign); | ||
| 194 | + // 1.3 创建客户端并连接(连接时服务端自动处理注册) | ||
| 195 | + MqttClientOptions options = new MqttClientOptions() | ||
| 196 | + .setClientId(clientId) | ||
| 197 | + .setUsername(username) | ||
| 198 | + .setPassword(sign) | ||
| 199 | + .setCleanSession(true) | ||
| 200 | + .setKeepAliveInterval(60); | ||
| 201 | + MqttClient client = MqttClient.create(vertx, options); | ||
| 202 | + | ||
| 203 | + try { | ||
| 204 | + // 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应) | ||
| 205 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 206 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 207 | + log.info("[testDeviceRegister][连接成功,等待注册响应...]"); | ||
| 208 | + | ||
| 209 | + // 3.1 设置消息处理器,接收注册响应 | ||
| 210 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); | ||
| 211 | + client.publishHandler(message -> { | ||
| 212 | + log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", | ||
| 213 | + message.topicName(), message.payload().toString()); | ||
| 214 | + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); | ||
| 215 | + responseFuture.complete(response); | ||
| 216 | + }); | ||
| 217 | + // 3.2 订阅 _reply 主题 | ||
| 218 | + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName); | ||
| 219 | + subscribe(client, replyTopic); | ||
| 220 | + | ||
| 221 | + // 4. 等待注册响应 | ||
| 222 | + IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 223 | + log.info("[testDeviceRegister][注册响应: {}]", response); | ||
| 224 | + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 225 | + } finally { | ||
| 226 | + disconnect(client); | ||
| 227 | + } | ||
| 231 | } | 228 | } |
| 232 | 229 | ||
| 233 | // ===================== 订阅下行消息测试 ===================== | 230 | // ===================== 订阅下行消息测试 ===================== |
| @@ -237,44 +234,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -237,44 +234,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 237 | */ | 234 | */ |
| 238 | @Test | 235 | @Test |
| 239 | public void testSubscribe() throws Exception { | 236 | public void testSubscribe() throws Exception { |
| 240 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 241 | - | ||
| 242 | // 1. 连接并认证 | 237 | // 1. 连接并认证 |
| 243 | MqttClient client = connectAndAuth(); | 238 | MqttClient client = connectAndAuth(); |
| 244 | log.info("[testSubscribe][连接认证成功]"); | 239 | log.info("[testSubscribe][连接认证成功]"); |
| 245 | 240 | ||
| 246 | - // 2. 设置消息处理器 | ||
| 247 | - client.publishHandler(message -> { | ||
| 248 | - log.info("[testSubscribe][收到消息: topic={}, payload={}]", | ||
| 249 | - message.topicName(), message.payload().toString()); | ||
| 250 | - }); | ||
| 251 | - | ||
| 252 | - // 3. 订阅下行主题 | ||
| 253 | - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | ||
| 254 | - log.info("[testSubscribe][订阅主题: {}]", topic); | ||
| 255 | - | ||
| 256 | - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 257 | - .onComplete(subscribeAr -> { | ||
| 258 | - if (subscribeAr.succeeded()) { | ||
| 259 | - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ||
| 260 | - // 保持连接 30 秒等待消息 | ||
| 261 | - vertx.setTimer(30000, id -> { | ||
| 262 | - client.disconnect() | ||
| 263 | - .onComplete(disconnectAr -> { | ||
| 264 | - log.info("[testSubscribe][断开连接]"); | ||
| 265 | - latch.countDown(); | ||
| 266 | - }); | ||
| 267 | - }); | ||
| 268 | - } else { | ||
| 269 | - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); | ||
| 270 | - latch.countDown(); | ||
| 271 | - } | ||
| 272 | - }); | ||
| 273 | - | ||
| 274 | - // 4. 等待测试完成 | ||
| 275 | - boolean completed = latch.await(60, TimeUnit.SECONDS); | ||
| 276 | - if (!completed) { | ||
| 277 | - log.warn("[testSubscribe][测试超时]"); | 241 | + try { |
| 242 | + // 2. 设置消息处理器 | ||
| 243 | + client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", | ||
| 244 | + message.topicName(), message.payload().toString())); | ||
| 245 | + | ||
| 246 | + // 3. 订阅下行主题 | ||
| 247 | + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | ||
| 248 | + log.info("[testSubscribe][订阅主题: {}]", topic); | ||
| 249 | + subscribe(client, topic); | ||
| 250 | + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ||
| 251 | + | ||
| 252 | + // 4. 保持连接 30 秒等待消息 | ||
| 253 | + Thread.sleep(30000); | ||
| 254 | + } finally { | ||
| 255 | + disconnect(client); | ||
| 278 | } | 256 | } |
| 279 | } | 257 | } |
| 280 | 258 | ||
| @@ -286,7 +264,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -286,7 +264,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 286 | * @param authInfo 认证信息 | 264 | * @param authInfo 认证信息 |
| 287 | * @return MQTT 客户端 | 265 | * @return MQTT 客户端 |
| 288 | */ | 266 | */ |
| 289 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | 267 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { |
| 290 | MqttClientOptions options = new MqttClientOptions() | 268 | MqttClientOptions options = new MqttClientOptions() |
| 291 | .setClientId(authInfo.getClientId()) | 269 | .setClientId(authInfo.getClientId()) |
| 292 | .setUsername(authInfo.getUsername()) | 270 | .setUsername(authInfo.getUsername()) |
| @@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 302 | * @return 已认证的 MQTT 客户端 | 280 | * @return 已认证的 MQTT 客户端 |
| 303 | */ | 281 | */ |
| 304 | private MqttClient connectAndAuth() throws Exception { | 282 | private MqttClient connectAndAuth() throws Exception { |
| 305 | - // 1. 创建客户端并连接 | ||
| 306 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 283 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 307 | - MqttClient client = connect(authInfo); | ||
| 308 | - | ||
| 309 | - // 2.1 连接 | ||
| 310 | - CompletableFuture<MqttClient> future = new CompletableFuture<>(); | 284 | + MqttClient client = createClient(authInfo); |
| 311 | client.connect(SERVER_PORT, SERVER_HOST) | 285 | client.connect(SERVER_PORT, SERVER_HOST) |
| 312 | - .onComplete(ar -> { | ||
| 313 | - if (ar.succeeded()) { | ||
| 314 | - future.complete(client); | ||
| 315 | - } else { | ||
| 316 | - future.completeExceptionally(ar.cause()); | ||
| 317 | - } | ||
| 318 | - }); | ||
| 319 | - // 2.2 等待连接结果 | ||
| 320 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 286 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 287 | + return client; | ||
| 321 | } | 288 | } |
| 322 | 289 | ||
| 323 | /** | 290 | /** |
| 324 | - * 订阅响应主题 | 291 | + * 订阅主题 |
| 325 | * | 292 | * |
| 326 | - * @param client MQTT 客户端 | ||
| 327 | - * @param replyTopic 响应主题 | 293 | + * @param client MQTT 客户端 |
| 294 | + * @param topic 主题 | ||
| 328 | */ | 295 | */ |
| 329 | - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 330 | - // 1. 订阅响应主题 | ||
| 331 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 332 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 333 | - .onComplete(ar -> { | ||
| 334 | - if (ar.succeeded()) { | ||
| 335 | - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 336 | - future.complete(null); | ||
| 337 | - } else { | ||
| 338 | - future.completeExceptionally(ar.cause()); | ||
| 339 | - } | ||
| 340 | - }); | ||
| 341 | - // 2. 等待订阅结果 | ||
| 342 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 296 | + private void subscribe(MqttClient client, String topic) throws Exception { |
| 297 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 298 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 299 | + log.info("[subscribe][订阅主题成功: {}]", topic); | ||
| 343 | } | 300 | } |
| 344 | 301 | ||
| 345 | /** | 302 | /** |
| @@ -350,34 +307,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -350,34 +307,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 350 | * @param request 请求消息 | 307 | * @param request 请求消息 |
| 351 | * @return 响应消息 | 308 | * @return 响应消息 |
| 352 | */ | 309 | */ |
| 353 | - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | 310 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) |
| 311 | + throws Exception { | ||
| 354 | // 1. 设置消息处理器,接收响应 | 312 | // 1. 设置消息处理器,接收响应 |
| 355 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | 313 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 356 | client.publishHandler(message -> { | 314 | client.publishHandler(message -> { |
| 357 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | 315 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 358 | message.topicName(), message.payload().toString()); | 316 | message.topicName(), message.payload().toString()); |
| 359 | - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | ||
| 360 | - future.complete(response); | 317 | + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); |
| 318 | + responseFuture.complete(response); | ||
| 361 | }); | 319 | }); |
| 362 | 320 | ||
| 363 | - // 2. 编码并发布消息 | ||
| 364 | - byte[] payload = CODEC.encode(request); | ||
| 365 | - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | ||
| 366 | - CODEC.type(), topic, new String(payload)); | ||
| 367 | - | 321 | + // 2. 序列化并发布消息 |
| 322 | + byte[] payload = SERIALIZER.serialize(request); | ||
| 323 | + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", | ||
| 324 | + SERIALIZER.getType(), topic, new String(payload)); | ||
| 368 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | 325 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) |
| 369 | - .onComplete(ar -> { | ||
| 370 | - if (ar.succeeded()) { | ||
| 371 | - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 372 | - } else { | ||
| 373 | - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 374 | - future.completeExceptionally(ar.cause()); | ||
| 375 | - } | ||
| 376 | - }); | ||
| 377 | - | ||
| 378 | - // 3. 等待响应(超时返回 null) | 326 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 327 | + log.info("[publishAndWaitReply][消息发布成功]"); | ||
| 328 | + | ||
| 329 | + // 3. 等待响应 | ||
| 379 | try { | 330 | try { |
| 380 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 331 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 381 | } catch (Exception e) { | 332 | } catch (Exception e) { |
| 382 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); | 333 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 383 | return null; | 334 | return null; |
| @@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 390 | * @param client MQTT 客户端 | 341 | * @param client MQTT 客户端 |
| 391 | */ | 342 | */ |
| 392 | private void disconnect(MqttClient client) throws Exception { | 343 | private void disconnect(MqttClient client) throws Exception { |
| 393 | - // 1. 断开连接 | ||
| 394 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 395 | client.disconnect() | 344 | client.disconnect() |
| 396 | - .onComplete(ar -> { | ||
| 397 | - if (ar.succeeded()) { | ||
| 398 | - log.info("[disconnect][断开连接成功]"); | ||
| 399 | - future.complete(null); | ||
| 400 | - } else { | ||
| 401 | - future.completeExceptionally(ar.cause()); | ||
| 402 | - } | ||
| 403 | - }); | ||
| 404 | - // 2. 等待断开结果 | ||
| 405 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 345 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 346 | + log.info("[disconnect][断开连接成功]"); | ||
| 406 | } | 347 | } |
| 407 | 348 | ||
| 408 | } | 349 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java
| @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.ListUtil; | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | import cn.hutool.core.map.MapUtil; | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | -import cn.hutool.core.util.IdUtil; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -13,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; | @@ -13,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; | ||
| 13 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; | 12 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 14 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; | 13 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; |
| 15 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 14 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 16 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 17 | -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; | 15 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | ||
| 18 | import io.netty.handler.codec.mqtt.MqttQoS; | 17 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 19 | import io.vertx.core.Vertx; | 18 | import io.vertx.core.Vertx; |
| 20 | import io.vertx.core.buffer.Buffer; | 19 | import io.vertx.core.buffer.Buffer; |
| @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; | @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; | ||
| 27 | import org.junit.jupiter.api.Test; | 26 | import org.junit.jupiter.api.Test; |
| 28 | 27 | ||
| 29 | import java.util.Collections; | 28 | import java.util.Collections; |
| 30 | -import java.util.List; | ||
| 31 | import java.util.Map; | 29 | import java.util.Map; |
| 32 | import java.util.concurrent.CompletableFuture; | 30 | import java.util.concurrent.CompletableFuture; |
| 33 | -import java.util.concurrent.CountDownLatch; | ||
| 34 | import java.util.concurrent.TimeUnit; | 31 | import java.util.concurrent.TimeUnit; |
| 35 | 32 | ||
| 36 | /** | 33 | /** |
| @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 68 | 65 | ||
| 69 | private static Vertx vertx; | 66 | private static Vertx vertx; |
| 70 | 67 | ||
| 71 | - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== | 68 | + // ===================== 序列化器 ===================== |
| 72 | 69 | ||
| 73 | - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); | 70 | + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); |
| 74 | 71 | ||
| 75 | // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== | 72 | // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== |
| 76 | 73 | ||
| @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 103 | */ | 100 | */ |
| 104 | @Test | 101 | @Test |
| 105 | public void testAuth() throws Exception { | 102 | public void testAuth() throws Exception { |
| 106 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 107 | - | ||
| 108 | // 1. 构建认证信息 | 103 | // 1. 构建认证信息 |
| 109 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | 104 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 110 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | 105 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 112 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 107 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 113 | 108 | ||
| 114 | // 2. 创建客户端并连接 | 109 | // 2. 创建客户端并连接 |
| 115 | - MqttClient client = connect(authInfo); | ||
| 116 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 117 | - .onComplete(ar -> { | ||
| 118 | - if (ar.succeeded()) { | ||
| 119 | - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 120 | - // 断开连接 | ||
| 121 | - client.disconnect() | ||
| 122 | - .onComplete(disconnectAr -> { | ||
| 123 | - if (disconnectAr.succeeded()) { | ||
| 124 | - log.info("[testAuth][断开连接成功]"); | ||
| 125 | - } else { | ||
| 126 | - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); | ||
| 127 | - } | ||
| 128 | - latch.countDown(); | ||
| 129 | - }); | ||
| 130 | - } else { | ||
| 131 | - log.error("[testAuth][连接失败]", ar.cause()); | ||
| 132 | - latch.countDown(); | ||
| 133 | - } | ||
| 134 | - }); | ||
| 135 | - | ||
| 136 | - // 3. 等待测试完成 | ||
| 137 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 138 | - if (!completed) { | ||
| 139 | - log.warn("[testAuth][测试超时]"); | 110 | + MqttClient client = createClient(authInfo); |
| 111 | + try { | ||
| 112 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 113 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 114 | + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 115 | + } finally { | ||
| 116 | + disconnect(client); | ||
| 140 | } | 117 | } |
| 141 | } | 118 | } |
| 142 | 119 | ||
| @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 153 | MqttClient client = connectAndAuth(); | 130 | MqttClient client = connectAndAuth(); |
| 154 | log.info("[testTopoAdd][连接认证成功]"); | 131 | log.info("[testTopoAdd][连接认证成功]"); |
| 155 | 132 | ||
| 156 | - // 2.1 订阅 _reply 主题 | ||
| 157 | - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", | ||
| 158 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 159 | - subscribeReply(client, replyTopic); | ||
| 160 | - | ||
| 161 | - // 2.2 构建子设备认证信息 | ||
| 162 | - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 163 | - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | ||
| 164 | - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | ||
| 165 | - .setClientId(subAuthInfo.getClientId()) | ||
| 166 | - .setUsername(subAuthInfo.getUsername()) | ||
| 167 | - .setPassword(subAuthInfo.getPassword()); | ||
| 168 | - | ||
| 169 | - // 2.3 构建请求消息 | ||
| 170 | - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); | ||
| 171 | - params.setSubDevices(Collections.singletonList(subDeviceAuth)); | ||
| 172 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 173 | - IdUtil.fastSimpleUUID(), | ||
| 174 | - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 175 | - params, | ||
| 176 | - null, null, null); | ||
| 177 | - | ||
| 178 | - // 3. 发布消息并等待响应 | ||
| 179 | - String topic = String.format("/sys/%s/%s/thing/topo/add", | ||
| 180 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 181 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 182 | - log.info("[testTopoAdd][响应消息: {}]", response); | ||
| 183 | - | ||
| 184 | - // 4. 断开连接 | ||
| 185 | - disconnect(client); | 133 | + try { |
| 134 | + // 2.1 构建子设备认证信息 | ||
| 135 | + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( | ||
| 136 | + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); | ||
| 137 | + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() | ||
| 138 | + .setClientId(subAuthInfo.getClientId()) | ||
| 139 | + .setUsername(subAuthInfo.getUsername()) | ||
| 140 | + .setPassword(subAuthInfo.getPassword()); | ||
| 141 | + | ||
| 142 | + // 2.2 构建请求消息 | ||
| 143 | + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO() | ||
| 144 | + .setSubDevices(Collections.singletonList(subDeviceAuth)); | ||
| 145 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 146 | + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), | ||
| 147 | + params); | ||
| 148 | + | ||
| 149 | + // 2.3 订阅 _reply 主题 | ||
| 150 | + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", | ||
| 151 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 152 | + subscribe(client, replyTopic); | ||
| 153 | + | ||
| 154 | + // 3. 发布消息并等待响应 | ||
| 155 | + String topic = String.format("/sys/%s/%s/thing/topo/add", | ||
| 156 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 157 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 158 | + log.info("[testTopoAdd][响应消息: {}]", response); | ||
| 159 | + } finally { | ||
| 160 | + disconnect(client); | ||
| 161 | + } | ||
| 186 | } | 162 | } |
| 187 | 163 | ||
| 188 | /** | 164 | /** |
| @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 196 | MqttClient client = connectAndAuth(); | 172 | MqttClient client = connectAndAuth(); |
| 197 | log.info("[testTopoDelete][连接认证成功]"); | 173 | log.info("[testTopoDelete][连接认证成功]"); |
| 198 | 174 | ||
| 199 | - // 2.1 订阅 _reply 主题 | ||
| 200 | - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", | ||
| 201 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 202 | - subscribeReply(client, replyTopic); | ||
| 203 | - | ||
| 204 | - // 2.2 构建请求消息 | ||
| 205 | - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); | ||
| 206 | - params.setSubDevices(Collections.singletonList( | ||
| 207 | - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | ||
| 208 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 209 | - IdUtil.fastSimpleUUID(), | ||
| 210 | - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 211 | - params, | ||
| 212 | - null, null, null); | ||
| 213 | - | ||
| 214 | - // 3. 发布消息并等待响应 | ||
| 215 | - String topic = String.format("/sys/%s/%s/thing/topo/delete", | ||
| 216 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 217 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 218 | - log.info("[testTopoDelete][响应消息: {}]", response); | ||
| 219 | - | ||
| 220 | - // 4. 断开连接 | ||
| 221 | - disconnect(client); | 175 | + try { |
| 176 | + // 2.1 构建请求消息 | ||
| 177 | + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO() | ||
| 178 | + .setSubDevices(Collections.singletonList( | ||
| 179 | + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); | ||
| 180 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 181 | + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), | ||
| 182 | + params); | ||
| 183 | + | ||
| 184 | + // 2.2 订阅 _reply 主题 | ||
| 185 | + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", | ||
| 186 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 187 | + subscribe(client, replyTopic); | ||
| 188 | + | ||
| 189 | + // 3. 发布消息并等待响应 | ||
| 190 | + String topic = String.format("/sys/%s/%s/thing/topo/delete", | ||
| 191 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 192 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 193 | + log.info("[testTopoDelete][响应消息: {}]", response); | ||
| 194 | + } finally { | ||
| 195 | + disconnect(client); | ||
| 196 | + } | ||
| 222 | } | 197 | } |
| 223 | 198 | ||
| 224 | /** | 199 | /** |
| @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 232 | MqttClient client = connectAndAuth(); | 207 | MqttClient client = connectAndAuth(); |
| 233 | log.info("[testTopoGet][连接认证成功]"); | 208 | log.info("[testTopoGet][连接认证成功]"); |
| 234 | 209 | ||
| 235 | - // 2.1 订阅 _reply 主题 | ||
| 236 | - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", | ||
| 237 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 238 | - subscribeReply(client, replyTopic); | ||
| 239 | - | ||
| 240 | - // 2.2 构建请求消息 | ||
| 241 | - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | ||
| 242 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 243 | - IdUtil.fastSimpleUUID(), | ||
| 244 | - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 245 | - params, | ||
| 246 | - null, null, null); | ||
| 247 | - | ||
| 248 | - // 3. 发布消息并等待响应 | ||
| 249 | - String topic = String.format("/sys/%s/%s/thing/topo/get", | ||
| 250 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 251 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 252 | - log.info("[testTopoGet][响应消息: {}]", response); | ||
| 253 | - | ||
| 254 | - // 4. 断开连接 | ||
| 255 | - disconnect(client); | 210 | + try { |
| 211 | + // 2.1 构建请求消息 | ||
| 212 | + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); | ||
| 213 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 214 | + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), | ||
| 215 | + params); | ||
| 216 | + | ||
| 217 | + // 2.2 订阅 _reply 主题 | ||
| 218 | + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", | ||
| 219 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 220 | + subscribe(client, replyTopic); | ||
| 221 | + | ||
| 222 | + // 3. 发布消息并等待响应 | ||
| 223 | + String topic = String.format("/sys/%s/%s/thing/topo/get", | ||
| 224 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 225 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 226 | + log.info("[testTopoGet][响应消息: {}]", response); | ||
| 227 | + } finally { | ||
| 228 | + disconnect(client); | ||
| 229 | + } | ||
| 256 | } | 230 | } |
| 257 | 231 | ||
| 258 | // ===================== 子设备注册测试 ===================== | 232 | // ===================== 子设备注册测试 ===================== |
| @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 270 | MqttClient client = connectAndAuth(); | 244 | MqttClient client = connectAndAuth(); |
| 271 | log.info("[testSubDeviceRegister][连接认证成功]"); | 245 | log.info("[testSubDeviceRegister][连接认证成功]"); |
| 272 | 246 | ||
| 273 | - // 2.1 订阅 _reply 主题 | ||
| 274 | - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", | ||
| 275 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 276 | - subscribeReply(client, replyTopic); | ||
| 277 | - | ||
| 278 | - // 2.2 构建请求消息 | ||
| 279 | - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); | ||
| 280 | - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); | ||
| 281 | - subDevice.setDeviceName("mougezishebei-mqtt"); | ||
| 282 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 283 | - IdUtil.fastSimpleUUID(), | ||
| 284 | - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | ||
| 285 | - Collections.singletonList(subDevice), | ||
| 286 | - null, null, null); | ||
| 287 | - | ||
| 288 | - // 3. 发布消息并等待响应 | ||
| 289 | - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", | ||
| 290 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 291 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 292 | - log.info("[testSubDeviceRegister][响应消息: {}]", response); | ||
| 293 | - | ||
| 294 | - // 4. 断开连接 | ||
| 295 | - disconnect(client); | 247 | + try { |
| 248 | + // 2.1 构建请求消息 | ||
| 249 | + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() | ||
| 250 | + .setProductKey(SUB_DEVICE_PRODUCT_KEY) | ||
| 251 | + .setDeviceName("mougezishebei-mqtt"); | ||
| 252 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 253 | + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), | ||
| 254 | + Collections.singletonList(subDevice)); | ||
| 255 | + | ||
| 256 | + // 2.2 订阅 _reply 主题 | ||
| 257 | + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", | ||
| 258 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 259 | + subscribe(client, replyTopic); | ||
| 260 | + | ||
| 261 | + // 3. 发布消息并等待响应 | ||
| 262 | + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", | ||
| 263 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 264 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 265 | + log.info("[testSubDeviceRegister][响应消息: {}]", response); | ||
| 266 | + } finally { | ||
| 267 | + disconnect(client); | ||
| 268 | + } | ||
| 296 | } | 269 | } |
| 297 | 270 | ||
| 298 | // ===================== 批量上报测试 ===================== | 271 | // ===================== 批量上报测试 ===================== |
| @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 308 | MqttClient client = connectAndAuth(); | 281 | MqttClient client = connectAndAuth(); |
| 309 | log.info("[testPropertyPackPost][连接认证成功]"); | 282 | log.info("[testPropertyPackPost][连接认证成功]"); |
| 310 | 283 | ||
| 311 | - // 2.1 订阅 _reply 主题 | ||
| 312 | - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", | ||
| 313 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 314 | - subscribeReply(client, replyTopic); | ||
| 315 | - | ||
| 316 | - // 2.2 构建【网关设备】自身属性 | ||
| 317 | - Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder() | ||
| 318 | - .put("temperature", 25.5) | ||
| 319 | - .build(); | ||
| 320 | - | ||
| 321 | - // 2.3 构建【网关设备】自身事件 | ||
| 322 | - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 323 | - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); | ||
| 324 | - gatewayEvent.setTime(System.currentTimeMillis()); | ||
| 325 | - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil | ||
| 326 | - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 327 | - .put("statusReport", gatewayEvent) | ||
| 328 | - .build(); | ||
| 329 | - | ||
| 330 | - // 2.4 构建【网关子设备】属性 | ||
| 331 | - Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder() | ||
| 332 | - .put("power", 100) | ||
| 333 | - .build(); | ||
| 334 | - | ||
| 335 | - // 2.5 构建【网关子设备】事件 | ||
| 336 | - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); | ||
| 337 | - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); | ||
| 338 | - subDeviceEvent.setTime(System.currentTimeMillis()); | ||
| 339 | - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil | ||
| 340 | - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 341 | - .put("healthCheck", subDeviceEvent) | ||
| 342 | - .build(); | ||
| 343 | - | ||
| 344 | - // 2.6 构建子设备数据 | ||
| 345 | - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); | ||
| 346 | - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); | ||
| 347 | - subDeviceData.setProperties(subDeviceProperties); | ||
| 348 | - subDeviceData.setEvents(subDeviceEvents); | ||
| 349 | - | ||
| 350 | - // 2.7 构建请求消息 | ||
| 351 | - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); | ||
| 352 | - params.setProperties(gatewayProperties); | ||
| 353 | - params.setEvents(gatewayEvents); | ||
| 354 | - params.setSubDevices(ListUtil.of(subDeviceData)); | ||
| 355 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 356 | - IdUtil.fastSimpleUUID(), | ||
| 357 | - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 358 | - params, | ||
| 359 | - null, null, null); | ||
| 360 | - | ||
| 361 | - // 3. 发布消息并等待响应 | ||
| 362 | - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", | ||
| 363 | - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 364 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 365 | - log.info("[testPropertyPackPost][响应消息: {}]", response); | ||
| 366 | - | ||
| 367 | - // 4. 断开连接 | ||
| 368 | - disconnect(client); | 284 | + try { |
| 285 | + // 2.1 构建【网关设备】自身属性 | ||
| 286 | + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder() | ||
| 287 | + .put("temperature", 25.5) | ||
| 288 | + .build(); | ||
| 289 | + | ||
| 290 | + // 2.2 构建【网关设备】自身事件 | ||
| 291 | + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() | ||
| 292 | + .setValue(MapUtil.builder().put("message", "gateway started").build()) | ||
| 293 | + .setTime(System.currentTimeMillis()); | ||
| 294 | + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil | ||
| 295 | + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 296 | + .put("statusReport", gatewayEvent) | ||
| 297 | + .build(); | ||
| 298 | + | ||
| 299 | + // 2.3 构建【网关子设备】属性 | ||
| 300 | + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder() | ||
| 301 | + .put("power", 100) | ||
| 302 | + .build(); | ||
| 303 | + | ||
| 304 | + // 2.4 构建【网关子设备】事件 | ||
| 305 | + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() | ||
| 306 | + .setValue(MapUtil.builder().put("errorCode", 0).build()) | ||
| 307 | + .setTime(System.currentTimeMillis()); | ||
| 308 | + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil | ||
| 309 | + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder() | ||
| 310 | + .put("healthCheck", subDeviceEvent) | ||
| 311 | + .build(); | ||
| 312 | + | ||
| 313 | + // 2.5 构建子设备数据 | ||
| 314 | + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() | ||
| 315 | + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) | ||
| 316 | + .setProperties(subDeviceProperties) | ||
| 317 | + .setEvents(subDeviceEvents); | ||
| 318 | + | ||
| 319 | + // 2.6 构建请求消息 | ||
| 320 | + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO() | ||
| 321 | + .setProperties(gatewayProperties) | ||
| 322 | + .setEvents(gatewayEvents) | ||
| 323 | + .setSubDevices(ListUtil.of(subDeviceData)); | ||
| 324 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 325 | + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), | ||
| 326 | + params); | ||
| 327 | + | ||
| 328 | + // 2.7 订阅 _reply 主题 | ||
| 329 | + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", | ||
| 330 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 331 | + subscribe(client, replyTopic); | ||
| 332 | + | ||
| 333 | + // 3. 发布消息并等待响应 | ||
| 334 | + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", | ||
| 335 | + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); | ||
| 336 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 337 | + log.info("[testPropertyPackPost][响应消息: {}]", response); | ||
| 338 | + } finally { | ||
| 339 | + disconnect(client); | ||
| 340 | + } | ||
| 369 | } | 341 | } |
| 370 | 342 | ||
| 371 | // ===================== 辅助方法 ===================== | 343 | // ===================== 辅助方法 ===================== |
| @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 376 | * @param authInfo 认证信息 | 348 | * @param authInfo 认证信息 |
| 377 | * @return MQTT 客户端 | 349 | * @return MQTT 客户端 |
| 378 | */ | 350 | */ |
| 379 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | 351 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { |
| 380 | MqttClientOptions options = new MqttClientOptions() | 352 | MqttClientOptions options = new MqttClientOptions() |
| 381 | .setClientId(authInfo.getClientId()) | 353 | .setClientId(authInfo.getClientId()) |
| 382 | .setUsername(authInfo.getUsername()) | 354 | .setUsername(authInfo.getUsername()) |
| @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 392 | * @return 已认证的 MQTT 客户端 | 364 | * @return 已认证的 MQTT 客户端 |
| 393 | */ | 365 | */ |
| 394 | private MqttClient connectAndAuth() throws Exception { | 366 | private MqttClient connectAndAuth() throws Exception { |
| 395 | - // 1. 创建客户端并连接 | ||
| 396 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( | 367 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 397 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); | 368 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| 398 | - MqttClient client = connect(authInfo); | ||
| 399 | - | ||
| 400 | - // 2.1 连接 | ||
| 401 | - CompletableFuture<MqttClient> future = new CompletableFuture<>(); | 369 | + MqttClient client = createClient(authInfo); |
| 402 | client.connect(SERVER_PORT, SERVER_HOST) | 370 | client.connect(SERVER_PORT, SERVER_HOST) |
| 403 | - .onComplete(ar -> { | ||
| 404 | - if (ar.succeeded()) { | ||
| 405 | - future.complete(client); | ||
| 406 | - } else { | ||
| 407 | - future.completeExceptionally(ar.cause()); | ||
| 408 | - } | ||
| 409 | - }); | ||
| 410 | - // 2.2 等待连接结果 | ||
| 411 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 371 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 372 | + return client; | ||
| 412 | } | 373 | } |
| 413 | 374 | ||
| 414 | /** | 375 | /** |
| 415 | - * 订阅响应主题 | 376 | + * 订阅主题 |
| 416 | * | 377 | * |
| 417 | - * @param client MQTT 客户端 | ||
| 418 | - * @param replyTopic 响应主题 | 378 | + * @param client MQTT 客户端 |
| 379 | + * @param topic 主题 | ||
| 419 | */ | 380 | */ |
| 420 | - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 421 | - // 1. 订阅响应主题 | ||
| 422 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 423 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 424 | - .onComplete(ar -> { | ||
| 425 | - if (ar.succeeded()) { | ||
| 426 | - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 427 | - future.complete(null); | ||
| 428 | - } else { | ||
| 429 | - future.completeExceptionally(ar.cause()); | ||
| 430 | - } | ||
| 431 | - }); | ||
| 432 | - // 2. 等待订阅结果 | ||
| 433 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 381 | + private void subscribe(MqttClient client, String topic) throws Exception { |
| 382 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 383 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 384 | + log.info("[subscribe][订阅主题成功: {}]", topic); | ||
| 434 | } | 385 | } |
| 435 | 386 | ||
| 436 | /** | 387 | /** |
| @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 441 | * @param request 请求消息 | 392 | * @param request 请求消息 |
| 442 | * @return 响应消息 | 393 | * @return 响应消息 |
| 443 | */ | 394 | */ |
| 444 | - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | 395 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) |
| 396 | + throws Exception { | ||
| 445 | // 1. 设置消息处理器,接收响应 | 397 | // 1. 设置消息处理器,接收响应 |
| 446 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | 398 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 447 | client.publishHandler(message -> { | 399 | client.publishHandler(message -> { |
| 448 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | 400 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 449 | message.topicName(), message.payload().toString()); | 401 | message.topicName(), message.payload().toString()); |
| 450 | - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | ||
| 451 | - future.complete(response); | 402 | + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); |
| 403 | + responseFuture.complete(response); | ||
| 452 | }); | 404 | }); |
| 453 | 405 | ||
| 454 | - // 2. 编码并发布消息 | ||
| 455 | - byte[] payload = CODEC.encode(request); | ||
| 456 | - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | ||
| 457 | - CODEC.type(), topic, new String(payload)); | ||
| 458 | - | 406 | + // 2. 序列化并发布消息 |
| 407 | + byte[] payload = SERIALIZER.serialize(request); | ||
| 408 | + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", | ||
| 409 | + SERIALIZER.getType(), topic, new String(payload)); | ||
| 459 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | 410 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) |
| 460 | - .onComplete(ar -> { | ||
| 461 | - if (ar.succeeded()) { | ||
| 462 | - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 463 | - } else { | ||
| 464 | - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 465 | - future.completeExceptionally(ar.cause()); | ||
| 466 | - } | ||
| 467 | - }); | ||
| 468 | - | ||
| 469 | - // 3. 等待响应(超时返回 null) | 411 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 412 | + log.info("[publishAndWaitReply][消息发布成功]"); | ||
| 413 | + | ||
| 414 | + // 3. 等待响应 | ||
| 470 | try { | 415 | try { |
| 471 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 416 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 472 | } catch (Exception e) { | 417 | } catch (Exception e) { |
| 473 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); | 418 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 474 | return null; | 419 | return null; |
| @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { | ||
| 481 | * @param client MQTT 客户端 | 426 | * @param client MQTT 客户端 |
| 482 | */ | 427 | */ |
| 483 | private void disconnect(MqttClient client) throws Exception { | 428 | private void disconnect(MqttClient client) throws Exception { |
| 484 | - // 1. 断开连接 | ||
| 485 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 486 | client.disconnect() | 429 | client.disconnect() |
| 487 | - .onComplete(ar -> { | ||
| 488 | - if (ar.succeeded()) { | ||
| 489 | - log.info("[disconnect][断开连接成功]"); | ||
| 490 | - future.complete(null); | ||
| 491 | - } else { | ||
| 492 | - future.completeExceptionally(ar.cause()); | ||
| 493 | - } | ||
| 494 | - }); | ||
| 495 | - // 2. 等待断开结果 | ||
| 496 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 430 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 431 | + log.info("[disconnect][断开连接成功]"); | ||
| 497 | } | 432 | } |
| 498 | 433 | ||
| 499 | } | 434 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 7 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 8 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 9 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 12 | -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; | 10 | +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | ||
| 13 | import io.netty.handler.codec.mqtt.MqttQoS; | 12 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 14 | import io.vertx.core.Vertx; | 13 | import io.vertx.core.Vertx; |
| 15 | import io.vertx.core.buffer.Buffer; | 14 | import io.vertx.core.buffer.Buffer; |
| @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; | @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; | ||
| 22 | import org.junit.jupiter.api.Test; | 21 | import org.junit.jupiter.api.Test; |
| 23 | 22 | ||
| 24 | import java.util.concurrent.CompletableFuture; | 23 | import java.util.concurrent.CompletableFuture; |
| 25 | -import java.util.concurrent.CountDownLatch; | ||
| 26 | import java.util.concurrent.TimeUnit; | 24 | import java.util.concurrent.TimeUnit; |
| 27 | 25 | ||
| 28 | /** | 26 | /** |
| @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 61 | 59 | ||
| 62 | private static Vertx vertx; | 60 | private static Vertx vertx; |
| 63 | 61 | ||
| 64 | - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== | 62 | + // ===================== 序列化器 ===================== |
| 65 | 63 | ||
| 66 | - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); | 64 | + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); |
| 67 | 65 | ||
| 68 | // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== | 66 | // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== |
| 69 | 67 | ||
| @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 90 | */ | 88 | */ |
| 91 | @Test | 89 | @Test |
| 92 | public void testAuth() throws Exception { | 90 | public void testAuth() throws Exception { |
| 93 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 94 | - | ||
| 95 | // 1. 构建认证信息 | 91 | // 1. 构建认证信息 |
| 96 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 92 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 97 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", | 93 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", |
| 98 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 94 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 99 | 95 | ||
| 100 | // 2. 创建客户端并连接 | 96 | // 2. 创建客户端并连接 |
| 101 | - MqttClient client = connect(authInfo); | ||
| 102 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 103 | - .onComplete(ar -> { | ||
| 104 | - if (ar.succeeded()) { | ||
| 105 | - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 106 | - // 断开连接 | ||
| 107 | - client.disconnect() | ||
| 108 | - .onComplete(disconnectAr -> { | ||
| 109 | - if (disconnectAr.succeeded()) { | ||
| 110 | - log.info("[testAuth][断开连接成功]"); | ||
| 111 | - } else { | ||
| 112 | - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); | ||
| 113 | - } | ||
| 114 | - latch.countDown(); | ||
| 115 | - }); | ||
| 116 | - } else { | ||
| 117 | - log.error("[testAuth][连接失败]", ar.cause()); | ||
| 118 | - latch.countDown(); | ||
| 119 | - } | ||
| 120 | - }); | ||
| 121 | - | ||
| 122 | - // 3. 等待测试完成 | ||
| 123 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 124 | - if (!completed) { | ||
| 125 | - log.warn("[testAuth][测试超时]"); | 97 | + MqttClient client = createClient(authInfo); |
| 98 | + try { | ||
| 99 | + client.connect(SERVER_PORT, SERVER_HOST) | ||
| 100 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 101 | + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); | ||
| 102 | + } finally { | ||
| 103 | + disconnect(client); | ||
| 126 | } | 104 | } |
| 127 | } | 105 | } |
| 128 | 106 | ||
| @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 138 | log.info("[testPropertyPost][连接认证成功]"); | 116 | log.info("[testPropertyPost][连接认证成功]"); |
| 139 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); | 117 | log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); |
| 140 | 118 | ||
| 141 | - // 2. 订阅 _reply 主题 | ||
| 142 | - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 143 | - subscribeReply(client, replyTopic); | ||
| 144 | - | ||
| 145 | - // 3. 构建属性上报消息 | ||
| 146 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 147 | - IdUtil.fastSimpleUUID(), | ||
| 148 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 149 | - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 150 | - .put("power", 100) | ||
| 151 | - .put("status", "online") | ||
| 152 | - .put("temperature", 36.5) | ||
| 153 | - .build()), | ||
| 154 | - null, null, null); | ||
| 155 | - | ||
| 156 | - // 4. 发布消息并等待响应 | ||
| 157 | - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 158 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 159 | - log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 160 | - | ||
| 161 | - // 5. 断开连接 | ||
| 162 | - disconnect(client); | 119 | + try { |
| 120 | + // 2.1 构建属性上报消息 | ||
| 121 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 122 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 123 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 124 | + .put("power", 100) | ||
| 125 | + .put("status", "online") | ||
| 126 | + .put("temperature", 36.5) | ||
| 127 | + .build())); | ||
| 128 | + | ||
| 129 | + // 2.2 订阅 _reply 主题 | ||
| 130 | + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 131 | + subscribe(client, replyTopic); | ||
| 132 | + | ||
| 133 | + // 3. 发布消息并等待响应 | ||
| 134 | + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 135 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 136 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 137 | + } finally { | ||
| 138 | + disconnect(client); | ||
| 139 | + } | ||
| 163 | } | 140 | } |
| 164 | 141 | ||
| 165 | // ===================== 子设备事件上报测试 ===================== | 142 | // ===================== 子设备事件上报测试 ===================== |
| @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 174 | log.info("[testEventPost][连接认证成功]"); | 151 | log.info("[testEventPost][连接认证成功]"); |
| 175 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); | 152 | log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); |
| 176 | 153 | ||
| 177 | - // 2. 订阅 _reply 主题 | ||
| 178 | - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 179 | - subscribeReply(client, replyTopic); | ||
| 180 | - | ||
| 181 | - // 3. 构建事件上报消息 | ||
| 182 | - IotDeviceMessage request = IotDeviceMessage.of( | ||
| 183 | - IdUtil.fastSimpleUUID(), | ||
| 184 | - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 185 | - IotDeviceEventPostReqDTO.of( | ||
| 186 | - "alarm", | ||
| 187 | - MapUtil.<String, Object>builder() | ||
| 188 | - .put("level", "warning") | ||
| 189 | - .put("message", "temperature too high") | ||
| 190 | - .put("threshold", 40) | ||
| 191 | - .put("current", 42) | ||
| 192 | - .build(), | ||
| 193 | - System.currentTimeMillis()), | ||
| 194 | - null, null, null); | ||
| 195 | - | ||
| 196 | - // 4. 发布消息并等待响应 | ||
| 197 | - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 198 | - IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 199 | - log.info("[testEventPost][响应消息: {}]", response); | ||
| 200 | - | ||
| 201 | - // 5. 断开连接 | ||
| 202 | - disconnect(client); | 154 | + try { |
| 155 | + // 2.1 构建事件上报消息 | ||
| 156 | + IotDeviceMessage request = IotDeviceMessage.requestOf( | ||
| 157 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 158 | + IotDeviceEventPostReqDTO.of( | ||
| 159 | + "alarm", | ||
| 160 | + MapUtil.<String, Object>builder() | ||
| 161 | + .put("level", "warning") | ||
| 162 | + .put("message", "temperature too high") | ||
| 163 | + .put("threshold", 40) | ||
| 164 | + .put("current", 42) | ||
| 165 | + .build(), | ||
| 166 | + System.currentTimeMillis())); | ||
| 167 | + | ||
| 168 | + // 2.2 订阅 _reply 主题 | ||
| 169 | + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 170 | + subscribe(client, replyTopic); | ||
| 171 | + | ||
| 172 | + // 3. 发布消息并等待响应 | ||
| 173 | + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 174 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 175 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 176 | + } finally { | ||
| 177 | + disconnect(client); | ||
| 178 | + } | ||
| 203 | } | 179 | } |
| 204 | 180 | ||
| 205 | // ===================== 辅助方法 ===================== | 181 | // ===================== 辅助方法 ===================== |
| @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 210 | * @param authInfo 认证信息 | 186 | * @param authInfo 认证信息 |
| 211 | * @return MQTT 客户端 | 187 | * @return MQTT 客户端 |
| 212 | */ | 188 | */ |
| 213 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | 189 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { |
| 214 | MqttClientOptions options = new MqttClientOptions() | 190 | MqttClientOptions options = new MqttClientOptions() |
| 215 | .setClientId(authInfo.getClientId()) | 191 | .setClientId(authInfo.getClientId()) |
| 216 | .setUsername(authInfo.getUsername()) | 192 | .setUsername(authInfo.getUsername()) |
| @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 226 | * @return 已认证的 MQTT 客户端 | 202 | * @return 已认证的 MQTT 客户端 |
| 227 | */ | 203 | */ |
| 228 | private MqttClient connectAndAuth() throws Exception { | 204 | private MqttClient connectAndAuth() throws Exception { |
| 229 | - // 1. 创建客户端并连接 | ||
| 230 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 205 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 231 | - MqttClient client = connect(authInfo); | ||
| 232 | - | ||
| 233 | - // 2.1 连接 | ||
| 234 | - CompletableFuture<MqttClient> future = new CompletableFuture<>(); | 206 | + MqttClient client = createClient(authInfo); |
| 235 | client.connect(SERVER_PORT, SERVER_HOST) | 207 | client.connect(SERVER_PORT, SERVER_HOST) |
| 236 | - .onComplete(ar -> { | ||
| 237 | - if (ar.succeeded()) { | ||
| 238 | - future.complete(client); | ||
| 239 | - } else { | ||
| 240 | - future.completeExceptionally(ar.cause()); | ||
| 241 | - } | ||
| 242 | - }); | ||
| 243 | - // 2.2 等待连接结果 | ||
| 244 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 208 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 209 | + return client; | ||
| 245 | } | 210 | } |
| 246 | 211 | ||
| 247 | /** | 212 | /** |
| 248 | - * 订阅响应主题 | 213 | + * 订阅主题 |
| 249 | * | 214 | * |
| 250 | - * @param client MQTT 客户端 | ||
| 251 | - * @param replyTopic 响应主题 | 215 | + * @param client MQTT 客户端 |
| 216 | + * @param topic 主题 | ||
| 252 | */ | 217 | */ |
| 253 | - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 254 | - // 1. 订阅响应主题 | ||
| 255 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 256 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 257 | - .onComplete(ar -> { | ||
| 258 | - if (ar.succeeded()) { | ||
| 259 | - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 260 | - future.complete(null); | ||
| 261 | - } else { | ||
| 262 | - future.completeExceptionally(ar.cause()); | ||
| 263 | - } | ||
| 264 | - }); | ||
| 265 | - // 2. 等待订阅结果 | ||
| 266 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 218 | + private void subscribe(MqttClient client, String topic) throws Exception { |
| 219 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 220 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 221 | + log.info("[subscribe][订阅主题成功: {}]", topic); | ||
| 267 | } | 222 | } |
| 268 | 223 | ||
| 269 | /** | 224 | /** |
| @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 274 | * @param request 请求消息 | 229 | * @param request 请求消息 |
| 275 | * @return 响应消息 | 230 | * @return 响应消息 |
| 276 | */ | 231 | */ |
| 277 | - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | 232 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) |
| 233 | + throws Exception { | ||
| 278 | // 1. 设置消息处理器,接收响应 | 234 | // 1. 设置消息处理器,接收响应 |
| 279 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | 235 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 280 | client.publishHandler(message -> { | 236 | client.publishHandler(message -> { |
| 281 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | 237 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 282 | message.topicName(), message.payload().toString()); | 238 | message.topicName(), message.payload().toString()); |
| 283 | - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | ||
| 284 | - future.complete(response); | 239 | + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); |
| 240 | + responseFuture.complete(response); | ||
| 285 | }); | 241 | }); |
| 286 | 242 | ||
| 287 | - // 2. 编码并发布消息 | ||
| 288 | - byte[] payload = CODEC.encode(request); | ||
| 289 | - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | ||
| 290 | - CODEC.type(), topic, new String(payload)); | ||
| 291 | - | 243 | + // 2. 序列化并发布消息 |
| 244 | + byte[] payload = SERIALIZER.serialize(request); | ||
| 245 | + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", | ||
| 246 | + SERIALIZER.getType(), topic, new String(payload)); | ||
| 292 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | 247 | client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) |
| 293 | - .onComplete(ar -> { | ||
| 294 | - if (ar.succeeded()) { | ||
| 295 | - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 296 | - } else { | ||
| 297 | - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 298 | - future.completeExceptionally(ar.cause()); | ||
| 299 | - } | ||
| 300 | - }); | ||
| 301 | - | ||
| 302 | - // 3. 等待响应(超时返回 null) | 248 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 249 | + log.info("[publishAndWaitReply][消息发布成功]"); | ||
| 250 | + | ||
| 251 | + // 3. 等待响应 | ||
| 303 | try { | 252 | try { |
| 304 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 253 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 305 | } catch (Exception e) { | 254 | } catch (Exception e) { |
| 306 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); | 255 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 307 | return null; | 256 | return null; |
| @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { | ||
| 314 | * @param client MQTT 客户端 | 263 | * @param client MQTT 客户端 |
| 315 | */ | 264 | */ |
| 316 | private void disconnect(MqttClient client) throws Exception { | 265 | private void disconnect(MqttClient client) throws Exception { |
| 317 | - // 1. 断开连接 | ||
| 318 | - CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 319 | client.disconnect() | 266 | client.disconnect() |
| 320 | - .onComplete(ar -> { | ||
| 321 | - if (ar.succeeded()) { | ||
| 322 | - log.info("[disconnect][断开连接成功]"); | ||
| 323 | - future.complete(null); | ||
| 324 | - } else { | ||
| 325 | - future.completeExceptionally(ar.cause()); | ||
| 326 | - } | ||
| 327 | - }); | ||
| 328 | - // 2. 等待断开结果 | ||
| 329 | - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 267 | + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 268 | + log.info("[disconnect][断开连接成功]"); | ||
| 330 | } | 269 | } |
| 331 | 270 | ||
| 332 | } | 271 | } |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java
| @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | ||
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; |
| @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { | @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { | ||
| 146 | @Test | 147 | @Test |
| 147 | public void testDeviceRegister() throws Exception { | 148 | public void testDeviceRegister() throws Exception { |
| 148 | // 1. 构建注册消息 | 149 | // 1. 构建注册消息 |
| 150 | + String deviceName = "test-tcp-" + System.currentTimeMillis(); | ||
| 151 | + String productSecret = "test-product-secret"; // 替换为实际的 productSecret | ||
| 152 | + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); | ||
| 149 | IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() | 153 | IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() |
| 150 | .setProductKey(PRODUCT_KEY) | 154 | .setProductKey(PRODUCT_KEY) |
| 151 | - .setDeviceName("test-tcp-" + System.currentTimeMillis()) | ||
| 152 | - .setProductSecret("test-product-secret"); | 155 | + .setDeviceName(deviceName) |
| 156 | + .setSign(sign); | ||
| 153 | IotDeviceMessage request = IotDeviceMessage.requestOf( | 157 | IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 154 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); | 158 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); |
| 155 | 159 |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
| @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | ||
| 11 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | 12 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | 13 | import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; |
| 13 | import lombok.extern.slf4j.Slf4j; | 14 | import lombok.extern.slf4j.Slf4j; |
| @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { | ||
| 100 | @Test | 101 | @Test |
| 101 | public void testDeviceRegister() throws Exception { | 102 | public void testDeviceRegister() throws Exception { |
| 102 | // 1. 构建注册消息 | 103 | // 1. 构建注册消息 |
| 104 | + String deviceName = "test-udp-" + System.currentTimeMillis(); | ||
| 105 | + String productSecret = "test-product-secret"; // 替换为实际的 productSecret | ||
| 106 | + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); | ||
| 103 | IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() | 107 | IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() |
| 104 | .setProductKey(PRODUCT_KEY) | 108 | .setProductKey(PRODUCT_KEY) |
| 105 | - .setDeviceName("test-udp-" + System.currentTimeMillis()) | ||
| 106 | - .setProductSecret("test-product-secret"); | 109 | + .setDeviceName(deviceName) |
| 110 | + .setSign(sign); | ||
| 107 | IotDeviceMessage request = IotDeviceMessage.requestOf( | 111 | IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 108 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); | 112 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); |
| 109 | 113 |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java
| @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 10 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 10 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; | 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 13 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | ||
| 13 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; | 14 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 14 | import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; | 15 | import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; |
| 15 | import io.vertx.core.Vertx; | 16 | import io.vertx.core.Vertx; |
| @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { | ||
| 131 | @Test | 132 | @Test |
| 132 | public void testDeviceRegister() throws Exception { | 133 | public void testDeviceRegister() throws Exception { |
| 133 | // 1.1 构建注册消息 | 134 | // 1.1 构建注册消息 |
| 134 | - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 135 | - registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 136 | - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); | ||
| 137 | - registerReqDTO.setProductSecret("test-product-secret"); | 135 | + String deviceName = "test-ws-" + System.currentTimeMillis(); |
| 136 | + String productSecret = "test-product-secret"; // 替换为实际的 productSecret | ||
| 137 | + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); | ||
| 138 | + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() | ||
| 139 | + .setProductKey(PRODUCT_KEY) | ||
| 140 | + .setDeviceName(deviceName) | ||
| 141 | + .setSign(sign); | ||
| 138 | IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | 142 | IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), |
| 139 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | 143 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); |
| 140 | // 1.2 序列化 | 144 | // 1.2 序列化 |