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 | 75 | return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { |
| 76 | 76 | IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); |
| 77 | 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 | 67 | @DictFormat(DictTypeConstants.NET_TYPE) |
| 68 | 68 | private Integer netType; |
| 69 | 69 | |
| 70 | - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | |
| 71 | - @ExcelProperty(value = "数据格式", converter = DictConvert.class) | |
| 72 | - @DictFormat(DictTypeConstants.CODEC_TYPE) | |
| 73 | - private String codecType; | |
| 70 | + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") | |
| 71 | + @ExcelProperty(value = "协议类型", converter = DictConvert.class) | |
| 72 | + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) | |
| 73 | + private String protocolType; | |
| 74 | + | |
| 75 | + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") | |
| 76 | + @ExcelProperty(value = "序列化类型", converter = DictConvert.class) | |
| 77 | + @DictFormat(DictTypeConstants.SERIALIZE_TYPE) | |
| 78 | + private String serializeType; | |
| 74 | 79 | |
| 75 | 80 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 76 | 81 | @ExcelProperty("创建时间") | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.validation.InEnum; |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | |
| 4 | 6 | import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; |
| 5 | 7 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 6 | 8 | import io.swagger.v3.oas.annotations.media.Schema; |
| ... | ... | @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { |
| 44 | 46 | @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") |
| 45 | 47 | private Integer netType; |
| 46 | 48 | |
| 47 | - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | |
| 48 | - @NotEmpty(message = "数据格式不能为空") | |
| 49 | - private String codecType; | |
| 49 | + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") | |
| 50 | + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}") | |
| 51 | + @NotEmpty(message = "协议类型不能为空") | |
| 52 | + private String protocolType; | |
| 53 | + | |
| 54 | + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") | |
| 55 | + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}") | |
| 56 | + @NotEmpty(message = "序列化类型不能为空") | |
| 57 | + private String serializeType; | |
| 50 | 58 | |
| 51 | 59 | @Schema(description = "是否开启动态注册", example = "false") |
| 52 | 60 | @NotNull(message = "是否开启动态注册不能为空") | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
| ... | ... | @@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO { |
| 78 | 78 | */ |
| 79 | 79 | private Integer netType; |
| 80 | 80 | /** |
| 81 | - * 数据格式(编解码器类型) | |
| 81 | + * 协议类型 | |
| 82 | 82 | * <p> |
| 83 | - * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} | |
| 84 | - * | |
| 85 | - * 目的:用于 gateway-server 解析消息格式 | |
| 83 | + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum} | |
| 86 | 84 | */ |
| 87 | - private String codecType; | |
| 85 | + private String protocolType; | |
| 86 | + /** | |
| 87 | + * 序列化类型 | |
| 88 | + * <p> | |
| 89 | + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum} | |
| 90 | + */ | |
| 91 | + private String serializeType; | |
| 88 | 92 | |
| 89 | 93 | } |
| 90 | 94 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java
| ... | ... | @@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums; |
| 8 | 8 | public class DictTypeConstants { |
| 9 | 9 | |
| 10 | 10 | public static final String NET_TYPE = "iot_net_type"; |
| 11 | - public static final String LOCATION_TYPE = "iot_location_type"; | |
| 12 | - public static final String CODEC_TYPE = "iot_codec_type"; | |
| 11 | + public static final String PROTOCOL_TYPE = "iot_protocol_type"; | |
| 12 | + public static final String SERIALIZE_TYPE = "iot_serialize_type"; | |
| 13 | 13 | |
| 14 | 14 | public static final String PRODUCT_STATUS = "iot_product_status"; |
| 15 | 15 | public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
| ... | ... | @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; |
| 29 | 29 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 30 | 30 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; |
| 31 | 31 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 32 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 32 | 33 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 33 | 34 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; |
| 34 | 35 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| ... | ... | @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 819 | 820 | if (BooleanUtil.isFalse(product.getRegisterEnabled())) { |
| 820 | 821 | throw exception(DEVICE_REGISTER_DISABLED); |
| 821 | 822 | } |
| 822 | - // 1.3 验证 productSecret | |
| 823 | - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) { | |
| 823 | + // 1.3 【重要!!!】验证签名 | |
| 824 | + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(), | |
| 825 | + product.getProductSecret(), reqDTO.getSign())) { | |
| 824 | 826 | throw exception(DEVICE_REGISTER_SECRET_INVALID); |
| 825 | 827 | } |
| 826 | 828 | return TenantUtils.execute(product.getTenantId(), () -> { | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java
| ... | ... | @@ -34,8 +34,12 @@ public class IotDeviceRespDTO { |
| 34 | 34 | */ |
| 35 | 35 | private Long productId; |
| 36 | 36 | /** |
| 37 | - * 编解码器类型 | |
| 37 | + * 协议类型 | |
| 38 | 38 | */ |
| 39 | - private String codecType; | |
| 39 | + private String protocolType; | |
| 40 | + /** | |
| 41 | + * 序列化类型 | |
| 42 | + */ | |
| 43 | + private String serializeType; | |
| 40 | 44 | |
| 41 | 45 | } |
| 42 | 46 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
| ... | ... | @@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO { |
| 27 | 27 | private String deviceName; |
| 28 | 28 | |
| 29 | 29 | /** |
| 30 | - * 产品密钥 | |
| 30 | + * 注册签名 | |
| 31 | + * | |
| 32 | + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String) | |
| 31 | 33 | */ |
| 32 | - @NotEmpty(message = "产品密钥不能为空") | |
| 33 | - private String productSecret; | |
| 34 | + @NotEmpty(message = "签名不能为空") | |
| 35 | + private String sign; | |
| 34 | 36 | |
| 35 | 37 | } | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.util; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.hutool.crypto.digest.DigestUtil; | |
| 5 | +import cn.hutool.crypto.digest.HmacAlgorithm; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT 产品【动态注册】认证工具类 | |
| 9 | + * <p> | |
| 10 | + * 用于一型一密场景,使用 productSecret 生成签名 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +public class IotProductAuthUtils { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 生成设备动态注册签名 | |
| 18 | + * | |
| 19 | + * @param productKey 产品标识 | |
| 20 | + * @param deviceName 设备名称 | |
| 21 | + * @param productSecret 产品密钥 | |
| 22 | + * @return 签名 | |
| 23 | + */ | |
| 24 | + public static String buildSign(String productKey, String deviceName, String productSecret) { | |
| 25 | + String content = buildContent(productKey, deviceName); | |
| 26 | + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret)) | |
| 27 | + .digestHex(content); | |
| 28 | + } | |
| 29 | + | |
| 30 | + /** | |
| 31 | + * 验证设备动态注册签名 | |
| 32 | + * | |
| 33 | + * @param productKey 产品标识 | |
| 34 | + * @param deviceName 设备名称 | |
| 35 | + * @param productSecret 产品密钥 | |
| 36 | + * @param sign 待验证的签名 | |
| 37 | + * @return 是否验证通过 | |
| 38 | + */ | |
| 39 | + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) { | |
| 40 | + String expectedSign = buildSign(productKey, deviceName, productSecret); | |
| 41 | + return expectedSign.equals(sign); | |
| 42 | + } | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 构建签名内容 | |
| 46 | + * | |
| 47 | + * @param productKey 产品标识 | |
| 48 | + * @param deviceName 设备名称 | |
| 49 | + * @return 签名内容 | |
| 50 | + */ | |
| 51 | + private static String buildContent(String productKey, String deviceName) { | |
| 52 | + return "deviceName" + deviceName + "productKey" + productKey; | |
| 53 | + } | |
| 54 | + | |
| 55 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/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 | 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 | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 18 | 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 19 | 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 | 29 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; |
| 21 | 30 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 22 | 31 | import io.vertx.core.Vertx; |
| ... | ... | @@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j; |
| 24 | 33 | import org.redisson.api.RedissonClient; |
| 25 | 34 | import org.springframework.beans.factory.annotation.Qualifier; |
| 26 | 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 | 42 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 28 | 43 | import org.springframework.context.annotation.Bean; |
| 29 | 44 | import org.springframework.context.annotation.Configuration; |
| 30 | 45 | |
| 31 | - | |
| 32 | 46 | @Configuration |
| 33 | 47 | @EnableConfigurationProperties(IotGatewayProperties.class) |
| 34 | -@Slf4j | |
| 35 | 48 | public class IotGatewayConfiguration { |
| 36 | 49 | |
| 37 | 50 | @Bean |
| ... | ... | @@ -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 | 61 | * IoT 网关 Modbus TCP 协议配置类 |
| 118 | 62 | */ |
| 119 | 63 | @Configuration |
| ... | ... | @@ -188,8 +132,8 @@ public class IotGatewayConfiguration { |
| 188 | 132 | |
| 189 | 133 | @Bean |
| 190 | 134 | public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, |
| 191 | - IotModbusTcpDownstreamHandler downstreamHandler, | |
| 192 | - IotMessageBus messageBus) { | |
| 135 | + IotModbusTcpDownstreamHandler downstreamHandler, | |
| 136 | + IotMessageBus messageBus) { | |
| 193 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; | |
| 6 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 7 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; |
| 8 | 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 | 11 | import jakarta.validation.Valid; |
| 12 | 12 | import jakarta.validation.constraints.NotEmpty; |
| 13 | 13 | import jakarta.validation.constraints.NotNull; |
| ... | ... | @@ -33,14 +33,9 @@ public class IotGatewayProperties { |
| 33 | 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 | 40 | @Data |
| 46 | 41 | public static class RpcProperties { |
| ... | ... | @@ -79,367 +74,155 @@ public class IotGatewayProperties { |
| 79 | 74 | |
| 80 | 75 | } |
| 81 | 76 | |
| 77 | + /** | |
| 78 | + * 协议实例配置 | |
| 79 | + */ | |
| 82 | 80 | @Data |
| 83 | 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 | 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 | 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 | 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 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 8 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | |
| 9 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| 11 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; |
| ... | ... | @@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle { |
| 43 | 45 | if (running) { |
| 44 | 46 | return; |
| 45 | 47 | } |
| 46 | - List<IotGatewayProperties.ProtocolInstanceProperties> protocolConfigs = gatewayProperties.getProtocols(); | |
| 48 | + List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols(); | |
| 47 | 49 | if (CollUtil.isEmpty(protocolConfigs)) { |
| 48 | 50 | log.info("[start][没有配置协议实例,跳过启动]"); |
| 49 | 51 | return; |
| 50 | 52 | } |
| 51 | 53 | |
| 52 | - for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) { | |
| 54 | + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { | |
| 53 | 55 | if (BooleanUtil.isFalse(config.getEnabled())) { |
| 54 | 56 | log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); |
| 55 | 57 | continue; |
| ... | ... | @@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle { |
| 89 | 91 | * @return 协议实例 |
| 90 | 92 | */ |
| 91 | 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 | 96 | if (protocolType == null) { |
| 95 | - log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType()); | |
| 97 | + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); | |
| 96 | 98 | return null; |
| 97 | 99 | } |
| 98 | 100 | switch (protocolType) { |
| ... | ... | @@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle { |
| 106 | 108 | return createCoapProtocol(config); |
| 107 | 109 | case WEBSOCKET: |
| 108 | 110 | return createWebSocketProtocol(config); |
| 111 | + case MQTT: | |
| 112 | + return createMqttProtocol(config); | |
| 113 | + case EMQX: | |
| 114 | + return createEmqxProtocol(config); | |
| 109 | 115 | default: |
| 110 | 116 | throw new IllegalArgumentException(String.format( |
| 111 | 117 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| ... | ... | @@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle { |
| 118 | 124 | * @param config 协议实例配置 |
| 119 | 125 | * @return HTTP 协议实例 |
| 120 | 126 | */ |
| 121 | - private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 127 | + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 122 | 128 | return new IotHttpProtocol(config); |
| 123 | 129 | } |
| 124 | 130 | |
| ... | ... | @@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle { |
| 128 | 134 | * @param config 协议实例配置 |
| 129 | 135 | * @return TCP 协议实例 |
| 130 | 136 | */ |
| 131 | - private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 137 | + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 132 | 138 | return new IotTcpProtocol(config); |
| 133 | 139 | } |
| 134 | 140 | |
| ... | ... | @@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle { |
| 138 | 144 | * @param config 协议实例配置 |
| 139 | 145 | * @return UDP 协议实例 |
| 140 | 146 | */ |
| 141 | - private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 147 | + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 142 | 148 | return new IotUdpProtocol(config); |
| 143 | 149 | } |
| 144 | 150 | |
| ... | ... | @@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle { |
| 148 | 154 | * @param config 协议实例配置 |
| 149 | 155 | * @return CoAP 协议实例 |
| 150 | 156 | */ |
| 151 | - private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 157 | + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 152 | 158 | return new IotCoapProtocol(config); |
| 153 | 159 | } |
| 154 | 160 | |
| ... | ... | @@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle { |
| 158 | 164 | * @param config 协议实例配置 |
| 159 | 165 | * @return WebSocket 协议实例 |
| 160 | 166 | */ |
| 161 | - private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { | |
| 167 | + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 162 | 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 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 6 | 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 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; |
| ... | ... | @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource; |
| 21 | 21 | import org.eclipse.californium.core.CoapServer; |
| 22 | 22 | import org.eclipse.californium.core.config.CoapConfig; |
| 23 | 23 | import org.eclipse.californium.elements.config.Configuration; |
| 24 | -import org.springframework.util.Assert; | |
| 24 | +import cn.hutool.core.lang.Assert; | |
| 25 | 25 | |
| 26 | 26 | import java.util.concurrent.TimeUnit; |
| 27 | 27 | |
| ... | ... | @@ -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 | 48 | * 服务器 ID(用于消息追踪,全局唯一) |
| 49 | 49 | */ |
| ... | ... | @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol { |
| 66 | 66 | */ |
| 67 | 67 | private final IotCoapDownstreamSubscriber downstreamSubscriber; |
| 68 | 68 | |
| 69 | - public IotCoapProtocol(ProtocolInstanceProperties properties) { | |
| 69 | + public IotCoapProtocol(ProtocolProperties properties) { | |
| 70 | 70 | IotCoapConfig coapConfig = properties.getCoap(); |
| 71 | 71 | Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); |
| 72 | 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 | 33 | Assert.notNull(request, "请求体不能为空"); |
| 34 | 34 | Assert.notBlank(request.getProductKey(), "productKey 不能为空"); |
| 35 | 35 | Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); |
| 36 | - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); | |
| 36 | + Assert.notBlank(request.getSign(), "sign 不能为空"); | |
| 37 | 37 | |
| 38 | 38 | // 2. 调用动态注册 |
| 39 | 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 | 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 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; | |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 11 | 11 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; |
| ... | ... | @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; |
| 21 | 21 | @Slf4j |
| 22 | 22 | public class IotEmqxDownstreamHandler { |
| 23 | 23 | |
| 24 | - private final IotEmqxUpstreamProtocol protocol; | |
| 24 | + private final IotEmqxProtocol protocol; | |
| 25 | 25 | |
| 26 | 26 | private final IotDeviceService deviceService; |
| 27 | 27 | |
| 28 | 28 | private final IotDeviceMessageService deviceMessageService; |
| 29 | 29 | |
| 30 | - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { | |
| 30 | + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) { | |
| 31 | 31 | this.protocol = protocol; |
| 32 | 32 | this.deviceService = SpringUtil.getBean(IotDeviceService.class); |
| 33 | 33 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| ... | ... | @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { |
| 53 | 53 | return; |
| 54 | 54 | } |
| 55 | 55 | // 2.2 构建载荷 |
| 56 | - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), | |
| 56 | + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), | |
| 57 | 57 | deviceInfo.getDeviceName()); |
| 58 | - // 2.3 发布消息 | |
| 58 | + | |
| 59 | + // 3. 发布消息 | |
| 59 | 60 | protocol.publishMessage(topic, payload); |
| 60 | 61 | } |
| 61 | 62 | |
| ... | ... | @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { |
| 74 | 75 | return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); |
| 75 | 76 | } |
| 76 | 77 | |
| 77 | -} | |
| 78 | 78 | \ No newline at end of file |
| 79 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/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 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 6 | 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 | 7 | import lombok.extern.slf4j.Slf4j; |
| 9 | 8 | |
| 10 | 9 | /** |
| ... | ... | @@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber |
| 17 | 16 | |
| 18 | 17 | private final IotEmqxDownstreamHandler downstreamHandler; |
| 19 | 18 | |
| 20 | - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { | |
| 19 | + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { | |
| 21 | 20 | super(protocol, messageBus); |
| 22 | 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 | 4 | import cn.hutool.core.util.BooleanUtil; |
| 4 | 5 | import cn.hutool.core.util.StrUtil; |
| 5 | 6 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 8 | 9 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 9 | 11 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 13 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 14 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 11 | 15 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | |
| 12 | 17 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 13 | 19 | import io.vertx.core.json.JsonObject; |
| 14 | 20 | import io.vertx.ext.web.RoutingContext; |
| 15 | 21 | import lombok.extern.slf4j.Slf4j; |
| 16 | 22 | |
| 23 | +import java.util.Locale; | |
| 24 | + | |
| 17 | 25 | /** |
| 18 | 26 | * IoT 网关 EMQX 认证事件处理器 |
| 19 | 27 | * <p> |
| 20 | 28 | * 为 EMQX 提供 HTTP 接口服务,包括: |
| 21 | - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 | |
| 22 | - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 | |
| 29 | + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} | |
| 30 | + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} | |
| 31 | + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} | |
| 32 | + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)} | |
| 23 | 33 | * |
| 24 | 34 | * @author 芋道源码 |
| 25 | 35 | */ |
| ... | ... | @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { |
| 45 | 55 | private static final String RESULT_IGNORE = "ignore"; |
| 46 | 56 | |
| 47 | 57 | /** |
| 48 | - * EMQX 事件类型常量 | |
| 58 | + * EMQX 事件类型常量 - 客户端连接 | |
| 49 | 59 | */ |
| 50 | 60 | private static final String EVENT_CLIENT_CONNECTED = "client.connected"; |
| 61 | + /** | |
| 62 | + * EMQX 事件类型常量 - 客户端断开连接 | |
| 63 | + */ | |
| 51 | 64 | private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; |
| 52 | 65 | |
| 66 | + /** | |
| 67 | + * 认证类型标识 - 设备注册 | |
| 68 | + */ | |
| 69 | + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; | |
| 70 | + | |
| 53 | 71 | private final String serverId; |
| 54 | 72 | |
| 55 | - private final IotDeviceMessageService deviceMessageService; | |
| 73 | + private final IotEmqxProtocol protocol; | |
| 56 | 74 | |
| 75 | + private final IotDeviceMessageService deviceMessageService; | |
| 57 | 76 | private final IotDeviceCommonApi deviceApi; |
| 58 | 77 | |
| 59 | - public IotEmqxAuthEventHandler(String serverId) { | |
| 78 | + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { | |
| 60 | 79 | this.serverId = serverId; |
| 80 | + this.protocol = protocol; | |
| 61 | 81 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 62 | 82 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 63 | 83 | } |
| 64 | 84 | |
| 85 | + // ========== 认证处理 ========== | |
| 86 | + | |
| 65 | 87 | /** |
| 66 | 88 | * EMQX 认证接口 |
| 67 | 89 | */ |
| 68 | 90 | public void handleAuth(RoutingContext context) { |
| 91 | + JsonObject body = null; | |
| 69 | 92 | try { |
| 70 | 93 | // 1. 参数校验 |
| 71 | - JsonObject body = parseRequestBody(context); | |
| 94 | + body = parseRequestBody(context); | |
| 72 | 95 | if (body == null) { |
| 73 | 96 | return; |
| 74 | 97 | } |
| ... | ... | @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { |
| 82 | 105 | return; |
| 83 | 106 | } |
| 84 | 107 | |
| 85 | - // 2. 执行认证 | |
| 108 | + // 2.1 情况一:判断是否为注册请求 | |
| 109 | + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { | |
| 110 | + handleDeviceRegister(context, username, password); | |
| 111 | + return; | |
| 112 | + } | |
| 113 | + | |
| 114 | + // 2.2 情况二:执行认证 | |
| 86 | 115 | boolean authResult = handleDeviceAuth(clientId, username, password); |
| 87 | 116 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); |
| 88 | 117 | if (authResult) { |
| ... | ... | @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler { |
| 91 | 120 | sendAuthResponse(context, RESULT_DENY); |
| 92 | 121 | } |
| 93 | 122 | } catch (Exception e) { |
| 94 | - log.error("[handleAuth][设备认证异常]", e); | |
| 123 | + log.error("[handleAuth][设备认证异常][body={}]", body, e); | |
| 95 | 124 | sendAuthResponse(context, RESULT_IGNORE); |
| 96 | 125 | } |
| 97 | 126 | } |
| 98 | 127 | |
| 99 | 128 | /** |
| 129 | + * 解析认证接口请求体 | |
| 130 | + * <p> | |
| 131 | + * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) | |
| 132 | + * | |
| 133 | + * @param context 路由上下文 | |
| 134 | + * @return 请求体JSON对象,解析失败时返回null | |
| 135 | + */ | |
| 136 | + private JsonObject parseRequestBody(RoutingContext context) { | |
| 137 | + try { | |
| 138 | + JsonObject body = context.body().asJsonObject(); | |
| 139 | + if (body == null) { | |
| 140 | + log.info("[parseRequestBody][请求体为空]"); | |
| 141 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 142 | + return null; | |
| 143 | + } | |
| 144 | + return body; | |
| 145 | + } catch (Exception e) { | |
| 146 | + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); | |
| 147 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 148 | + return null; | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + /** | |
| 153 | + * 执行设备认证 | |
| 154 | + * | |
| 155 | + * @param clientId 客户端ID | |
| 156 | + * @param username 用户名 | |
| 157 | + * @param password 密码 | |
| 158 | + * @return 认证是否成功 | |
| 159 | + */ | |
| 160 | + private boolean handleDeviceAuth(String clientId, String username, String password) { | |
| 161 | + try { | |
| 162 | + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 163 | + .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 164 | + result.checkError(); | |
| 165 | + return BooleanUtil.isTrue(result.getData()); | |
| 166 | + } catch (Exception e) { | |
| 167 | + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); | |
| 168 | + throw e; | |
| 169 | + } | |
| 170 | + } | |
| 171 | + | |
| 172 | + /** | |
| 173 | + * 发送 EMQX 认证响应 | |
| 174 | + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 | |
| 175 | + * | |
| 176 | + * @param context 路由上下文 | |
| 177 | + * @param result 认证结果:allow、deny、ignore | |
| 178 | + */ | |
| 179 | + private void sendAuthResponse(RoutingContext context, String result) { | |
| 180 | + // 构建符合 EMQX 官方规范的响应 | |
| 181 | + JsonObject response = new JsonObject() | |
| 182 | + .put("result", result) | |
| 183 | + .put("is_superuser", false); | |
| 184 | + // 可以根据业务需求添加客户端属性 | |
| 185 | + // response.put("client_attrs", new JsonObject().put("role", "device")); | |
| 186 | + // 可以添加认证过期时间(可选) | |
| 187 | + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); | |
| 188 | + | |
| 189 | + // 回复响应 | |
| 190 | + context.response() | |
| 191 | + .setStatusCode(SUCCESS_STATUS_CODE) | |
| 192 | + .putHeader("Content-Type", "application/json; charset=utf-8") | |
| 193 | + .end(response.encode()); | |
| 194 | + } | |
| 195 | + | |
| 196 | + // ========== ACL 处理 ========== | |
| 197 | + | |
| 198 | + /** | |
| 199 | + * EMQX ACL 接口 | |
| 200 | + * <p> | |
| 201 | + * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。 | |
| 202 | + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。 | |
| 203 | + */ | |
| 204 | + public void handleAcl(RoutingContext context) { | |
| 205 | + JsonObject body = null; | |
| 206 | + try { | |
| 207 | + // 1.1 解析请求体 | |
| 208 | + body = parseRequestBody(context); | |
| 209 | + if (body == null) { | |
| 210 | + return; | |
| 211 | + } | |
| 212 | + String username = body.getString("username"); | |
| 213 | + String topic = body.getString("topic"); | |
| 214 | + if (StrUtil.hasBlank(username, topic)) { | |
| 215 | + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic); | |
| 216 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 217 | + return; | |
| 218 | + } | |
| 219 | + // 1.2 解析设备身份 | |
| 220 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 221 | + if (deviceInfo == null) { | |
| 222 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 223 | + return; | |
| 224 | + } | |
| 225 | + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段) | |
| 226 | + Boolean subscribe = parseAclSubscribeFlag(body); | |
| 227 | + if (subscribe == null) { | |
| 228 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 229 | + return; | |
| 230 | + } | |
| 231 | + | |
| 232 | + // 2. 执行 ACL 校验 | |
| 233 | + boolean allowed = subscribe | |
| 234 | + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()) | |
| 235 | + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 236 | + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY); | |
| 237 | + } catch (Exception e) { | |
| 238 | + log.error("[handleAcl][ACL 处理失败][body={}]", body, e); | |
| 239 | + sendAuthResponse(context, RESULT_IGNORE); | |
| 240 | + } | |
| 241 | + } | |
| 242 | + | |
| 243 | + /** | |
| 244 | + * 解析 ACL 动作类型:订阅/发布 | |
| 245 | + * | |
| 246 | + * @param body ACL 请求体 | |
| 247 | + * @return true 订阅;false 发布;null 不识别 | |
| 248 | + */ | |
| 249 | + private static Boolean parseAclSubscribeFlag(JsonObject body) { | |
| 250 | + // 1. action 字段(常见为 publish/subscribe) | |
| 251 | + String action = body.getString("action"); | |
| 252 | + if (StrUtil.isNotBlank(action)) { | |
| 253 | + String lower = action.toLowerCase(Locale.ROOT); | |
| 254 | + if (lower.contains("sub")) { | |
| 255 | + return true; | |
| 256 | + } | |
| 257 | + if (lower.contains("pub")) { | |
| 258 | + return false; | |
| 259 | + } | |
| 260 | + } | |
| 261 | + | |
| 262 | + // 2. access 字段:可能是数字或字符串 | |
| 263 | + Integer access = body.getInteger("access"); | |
| 264 | + if (access != null) { | |
| 265 | + if (access == 1) { | |
| 266 | + return true; | |
| 267 | + } | |
| 268 | + if (access == 2) { | |
| 269 | + return false; | |
| 270 | + } | |
| 271 | + } | |
| 272 | + String accessText = body.getString("access"); | |
| 273 | + if (StrUtil.isNotBlank(accessText)) { | |
| 274 | + String lower = accessText.toLowerCase(Locale.ROOT); | |
| 275 | + if (lower.contains("sub")) { | |
| 276 | + return true; | |
| 277 | + } | |
| 278 | + if (lower.contains("pub")) { | |
| 279 | + return false; | |
| 280 | + } | |
| 281 | + if (StrUtil.isNumeric(accessText)) { | |
| 282 | + int value = Integer.parseInt(accessText); | |
| 283 | + if (value == 1) { | |
| 284 | + return true; | |
| 285 | + } | |
| 286 | + if (value == 2) { | |
| 287 | + return false; | |
| 288 | + } | |
| 289 | + } | |
| 290 | + } | |
| 291 | + return null; | |
| 292 | + } | |
| 293 | + | |
| 294 | + // ========== 事件处理 ========== | |
| 295 | + | |
| 296 | + /** | |
| 100 | 297 | * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 |
| 101 | 298 | * 支持的事件类型:client.connected、client.disconnected 等 |
| 102 | 299 | */ |
| ... | ... | @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler { |
| 124 | 321 | break; |
| 125 | 322 | } |
| 126 | 323 | |
| 127 | - // EMQX Webhook 只需要 200 状态码,无需响应体 | |
| 324 | + // 3. EMQX Webhook 只需要 200 状态码,无需响应体 | |
| 128 | 325 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 129 | 326 | } catch (Exception e) { |
| 130 | - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e); | |
| 131 | - // 即使处理失败,也返回 200 避免EMQX重试 | |
| 327 | + log.error("[handleEvent][事件处理失败][body={}]", body, e); | |
| 328 | + // 即使处理失败,也返回 200 避免 EMQX 重试 | |
| 132 | 329 | context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); |
| 133 | 330 | } |
| 134 | 331 | } |
| 135 | 332 | |
| 136 | 333 | /** |
| 137 | - * 处理客户端连接事件 | |
| 138 | - */ | |
| 139 | - private void handleClientConnected(JsonObject body) { | |
| 140 | - String username = body.getString("username"); | |
| 141 | - log.info("[handleClientConnected][设备上线: {}]", username); | |
| 142 | - handleDeviceStateChange(username, true); | |
| 143 | - } | |
| 144 | - | |
| 145 | - /** | |
| 146 | - * 处理客户端断开连接事件 | |
| 147 | - */ | |
| 148 | - private void handleClientDisconnected(JsonObject body) { | |
| 149 | - String username = body.getString("username"); | |
| 150 | - String reason = body.getString("reason"); | |
| 151 | - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); | |
| 152 | - handleDeviceStateChange(username, false); | |
| 153 | - } | |
| 154 | - | |
| 155 | - /** | |
| 156 | - * 解析认证接口请求体 | |
| 157 | - * <p> | |
| 158 | - * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) | |
| 159 | - * | |
| 160 | - * @param context 路由上下文 | |
| 161 | - * @return 请求体JSON对象,解析失败时返回null | |
| 162 | - */ | |
| 163 | - private JsonObject parseRequestBody(RoutingContext context) { | |
| 164 | - try { | |
| 165 | - JsonObject body = context.body().asJsonObject(); | |
| 166 | - if (body == null) { | |
| 167 | - log.info("[parseRequestBody][请求体为空]"); | |
| 168 | - sendAuthResponse(context, RESULT_IGNORE); | |
| 169 | - return null; | |
| 170 | - } | |
| 171 | - return body; | |
| 172 | - } catch (Exception e) { | |
| 173 | - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); | |
| 174 | - sendAuthResponse(context, RESULT_IGNORE); | |
| 175 | - return null; | |
| 176 | - } | |
| 177 | - } | |
| 178 | - | |
| 179 | - /** | |
| 180 | 334 | * 解析事件接口请求体 |
| 181 | 335 | * <p> |
| 182 | 336 | * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范) |
| ... | ... | @@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler { |
| 201 | 355 | } |
| 202 | 356 | |
| 203 | 357 | /** |
| 204 | - * 执行设备认证 | |
| 205 | - * | |
| 206 | - * @param clientId 客户端ID | |
| 207 | - * @param username 用户名 | |
| 208 | - * @param password 密码 | |
| 209 | - * @return 认证是否成功 | |
| 358 | + * 处理客户端连接事件 | |
| 210 | 359 | */ |
| 211 | - private boolean handleDeviceAuth(String clientId, String username, String password) { | |
| 212 | - try { | |
| 213 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 214 | - .setClientId(clientId).setUsername(username).setPassword(password)); | |
| 215 | - result.checkError(); | |
| 216 | - return BooleanUtil.isTrue(result.getData()); | |
| 217 | - } catch (Exception e) { | |
| 218 | - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); | |
| 219 | - throw e; | |
| 220 | - } | |
| 360 | + private void handleClientConnected(JsonObject body) { | |
| 361 | + String username = body.getString("username"); | |
| 362 | + log.info("[handleClientConnected][设备上线: {}]", username); | |
| 363 | + handleDeviceStateChange(username, true); | |
| 364 | + } | |
| 365 | + | |
| 366 | + /** | |
| 367 | + * 处理客户端断开连接事件 | |
| 368 | + */ | |
| 369 | + private void handleClientDisconnected(JsonObject body) { | |
| 370 | + String username = body.getString("username"); | |
| 371 | + String reason = body.getString("reason"); | |
| 372 | + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); | |
| 373 | + handleDeviceStateChange(username, false); | |
| 221 | 374 | } |
| 222 | 375 | |
| 223 | 376 | /** |
| ... | ... | @@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler { |
| 247 | 400 | } |
| 248 | 401 | } |
| 249 | 402 | |
| 403 | + // ========= 注册处理 ========= | |
| 404 | + | |
| 250 | 405 | /** |
| 251 | - * 发送 EMQX 认证响应 | |
| 252 | - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 | |
| 406 | + * 处理设备注册请求(一型一密) | |
| 253 | 407 | * |
| 254 | - * @param context 路由上下文 | |
| 255 | - * @param result 认证结果:allow、deny、ignore | |
| 408 | + * @param context 路由上下文 | |
| 409 | + * @param username 用户名 | |
| 410 | + * @param password 密码(签名) | |
| 256 | 411 | */ |
| 257 | - private void sendAuthResponse(RoutingContext context, String result) { | |
| 258 | - // 构建符合 EMQX 官方规范的响应 | |
| 259 | - JsonObject response = new JsonObject() | |
| 260 | - .put("result", result) | |
| 261 | - .put("is_superuser", false); | |
| 412 | + private void handleDeviceRegister(RoutingContext context, String username, String password) { | |
| 413 | + try { | |
| 414 | + // 1. 解析设备信息 | |
| 415 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 416 | + if (deviceInfo == null) { | |
| 417 | + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username); | |
| 418 | + sendAuthResponse(context, RESULT_DENY); | |
| 419 | + return; | |
| 420 | + } | |
| 262 | 421 | |
| 263 | - // 可以根据业务需求添加客户端属性 | |
| 264 | - // response.put("client_attrs", new JsonObject().put("role", "device")); | |
| 422 | + // 2. 调用注册 API | |
| 423 | + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() | |
| 424 | + .setProductKey(deviceInfo.getProductKey()) | |
| 425 | + .setDeviceName(deviceInfo.getDeviceName()) | |
| 426 | + .setSign(password); | |
| 427 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 428 | + result.checkError(); | |
| 265 | 429 | |
| 266 | - // 可以添加认证过期时间(可选) | |
| 267 | - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); | |
| 430 | + // 3. 允许连接 | |
| 431 | + log.info("[handleDeviceRegister][设备注册成功: {}]", username); | |
| 432 | + sendAuthResponse(context, RESULT_ALLOW); | |
| 268 | 433 | |
| 269 | - context.response() | |
| 270 | - .setStatusCode(SUCCESS_STATUS_CODE) | |
| 271 | - .putHeader("Content-Type", "application/json; charset=utf-8") | |
| 272 | - .end(response.encode()); | |
| 434 | + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅) | |
| 435 | + sendRegisterResultMessage(username, result.getData()); | |
| 436 | + } catch (Exception e) { | |
| 437 | + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage()); | |
| 438 | + sendAuthResponse(context, RESULT_DENY); | |
| 439 | + } | |
| 440 | + } | |
| 441 | + | |
| 442 | + /** | |
| 443 | + * 发送注册结果消息给设备 | |
| 444 | + * <p> | |
| 445 | + * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。 | |
| 446 | + * | |
| 447 | + * @param username 用户名 | |
| 448 | + * @param result 注册结果 | |
| 449 | + */ | |
| 450 | + @SuppressWarnings("DataFlowIssue") | |
| 451 | + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) { | |
| 452 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 453 | + Assert.notNull(deviceInfo, "设备信息不能为空"); | |
| 454 | + try { | |
| 455 | + // 1.1 构建响应消息 | |
| 456 | + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); | |
| 457 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null); | |
| 458 | + // 1.2 序列化消息 | |
| 459 | + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, | |
| 460 | + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON); | |
| 461 | + // 1.3 构建响应主题 | |
| 462 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, | |
| 463 | + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true); | |
| 464 | + | |
| 465 | + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅) | |
| 466 | + protocol.publishDelayMessage(replyTopic, encodedData, 5000); | |
| 467 | + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic); | |
| 468 | + } catch (Exception e) { | |
| 469 | + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e); | |
| 470 | + } | |
| 273 | 471 | } |
| 274 | 472 | |
| 275 | -} | |
| 276 | 473 | \ No newline at end of file |
| 474 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; | |
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; | |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 3 | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | 8 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler { |
| 20 | 20 | |
| 21 | 21 | private final String serverId; |
| 22 | 22 | |
| 23 | - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { | |
| 23 | + public IotEmqxUpstreamHandler(String serverId) { | |
| 24 | 24 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 25 | - this.serverId = protocol.getServerId(); | |
| 25 | + this.serverId = serverId; | |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | 28 | /** |
| 29 | 29 | * 处理 MQTT 发布消息 |
| 30 | 30 | */ |
| 31 | 31 | public void handle(MqttPublishMessage mqttMessage) { |
| 32 | - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); | |
| 32 | + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); | |
| 33 | 33 | String topic = mqttMessage.topicName(); |
| 34 | 34 | byte[] payload = mqttMessage.payload().getBytes(); |
| 35 | 35 | try { |
| 36 | 36 | // 1. 解析主题,一次性获取所有信息 |
| 37 | 37 | String[] topicParts = topic.split("/"); |
| 38 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 38 | + String productKey = ArrayUtil.get(topicParts, 2); | |
| 39 | + String deviceName = ArrayUtil.get(topicParts, 3); | |
| 40 | + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) { | |
| 39 | 41 | log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); |
| 40 | 42 | return; |
| 41 | 43 | } |
| 42 | 44 | |
| 43 | - String productKey = topicParts[2]; | |
| 44 | - String deviceName = topicParts[3]; | |
| 45 | - | |
| 46 | - // 3. 解码消息 | |
| 47 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 45 | + // 2. 反序列化消息 | |
| 46 | + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 48 | 47 | if (message == null) { |
| 49 | 48 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 50 | 49 | return; |
| 51 | 50 | } |
| 52 | 51 | |
| 53 | - // 4. 发送消息到队列 | |
| 52 | + // 3. 发送消息到队列 | |
| 54 | 53 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); |
| 55 | 54 | } catch (Exception e) { |
| 56 | 55 | log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); |
| 57 | 56 | } |
| 58 | 57 | } |
| 59 | 58 | |
| 60 | -} | |
| 61 | 59 | \ No newline at end of file |
| 60 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java
| ... | ... | @@ -10,19 +10,4 @@ import lombok.Data; |
| 10 | 10 | @Data |
| 11 | 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 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 6 | 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 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 9 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; |
| ... | ... | @@ -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 | 39 | * 服务器 ID(用于消息追踪,全局唯一) |
| 39 | 40 | */ |
| ... | ... | @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol { |
| 60 | 61 | */ |
| 61 | 62 | private IotHttpDownstreamSubscriber downstreamSubscriber; |
| 62 | 63 | |
| 63 | - public IotHttpProtocol(ProtocolInstanceProperties properties) { | |
| 64 | + public IotHttpProtocol(ProtocolProperties properties) { | |
| 64 | 65 | this.properties = properties; |
| 65 | 66 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 66 | 67 | |
| ... | ... | @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol { |
| 104 | 105 | router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); |
| 105 | 106 | |
| 106 | 107 | // 1.4 启动 HTTP 服务器 |
| 107 | - IotHttpConfig httpConfig = properties.getHttp(); | |
| 108 | 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 | 111 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 111 | - .setKeyPath(httpConfig.getSslKeyPath()) | |
| 112 | - .setCertPath(httpConfig.getSslCertPath()); | |
| 112 | + .setKeyPath(sslConfig.getSslKeyPath()) | |
| 113 | + .setCertPath(sslConfig.getSslCertPath()); | |
| 113 | 114 | options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); |
| 114 | 115 | } |
| 115 | 116 | try { |
| ... | ... | @@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol { |
| 125 | 126 | this.downstreamSubscriber.start(); |
| 126 | 127 | } catch (Exception e) { |
| 127 | 128 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); |
| 128 | - // 启动失败时关闭 Vertx | |
| 129 | + // 启动失败时关闭资源 | |
| 130 | + if (httpServer != null) { | |
| 131 | + httpServer.close(); | |
| 132 | + httpServer = null; | |
| 133 | + } | |
| 129 | 134 | if (vertx != null) { |
| 130 | 135 | vertx.close(); |
| 131 | 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 | 35 | Assert.notNull(request, "请求参数不能为空"); |
| 36 | 36 | Assert.notBlank(request.getProductKey(), "productKey 不能为空"); |
| 37 | 37 | Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); |
| 38 | - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); | |
| 38 | + Assert.notBlank(request.getSign(), "sign 不能为空"); | |
| 39 | 39 | |
| 40 | 40 | // 2. 调用动态注册 |
| 41 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| 9 | 9 | /** |
| 10 | - * IoT 网关 MQTT 协议:下行消息订阅器 | |
| 11 | - * <p> | |
| 12 | - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 | |
| 10 | + * IoT 网关 MQTT 协议:接收下行给设备的消息 | |
| 13 | 11 | * |
| 14 | 12 | * @author 芋道源码 |
| 15 | 13 | */ |
| ... | ... | @@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber |
| 18 | 16 | |
| 19 | 17 | private final IotMqttDownstreamHandler downstreamHandler; |
| 20 | 18 | |
| 21 | - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, | |
| 19 | + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, | |
| 22 | 20 | IotMqttDownstreamHandler downstreamHandler, |
| 23 | 21 | IotMessageBus messageBus) { |
| 24 | 22 | super(protocol, messageBus); |
| ... | ... | @@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber |
| 27 | 25 | |
| 28 | 26 | @Override |
| 29 | 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 | 66 | } catch (Exception ignored) { |
| 67 | 67 | // 连接已关闭,忽略异常 |
| 68 | 68 | } |
| 69 | - | |
| 70 | 69 | return realTimeAddress; |
| 71 | 70 | } |
| 72 | 71 | |
| ... | ... | @@ -74,24 +73,24 @@ public class IotMqttConnectionManager { |
| 74 | 73 | * 注册设备连接(包含认证信息) |
| 75 | 74 | * |
| 76 | 75 | * @param endpoint MQTT 连接端点 |
| 77 | - * @param deviceId 设备 ID | |
| 78 | 76 | * @param connectionInfo 连接信息 |
| 79 | 77 | */ |
| 80 | - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { | |
| 78 | + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { | |
| 79 | + Long deviceId = connectionInfo.getDeviceId(); | |
| 81 | 80 | // 如果设备已有其他连接,先清理旧连接 |
| 82 | 81 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); |
| 83 | 82 | if (oldEndpoint != null && oldEndpoint != endpoint) { |
| 84 | 83 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 85 | 84 | deviceId, getEndpointAddress(oldEndpoint)); |
| 86 | - oldEndpoint.close(); | |
| 87 | - // 清理旧连接的映射 | |
| 85 | + // 先清理映射,再关闭连接(避免旧连接处理器干扰) | |
| 88 | 86 | connectionMap.remove(oldEndpoint); |
| 87 | + oldEndpoint.close(); | |
| 89 | 88 | } |
| 90 | 89 | |
| 91 | 90 | // 注册新连接 |
| 92 | 91 | connectionMap.put(endpoint, connectionInfo); |
| 93 | 92 | deviceEndpointMap.put(deviceId, endpoint); |
| 94 | - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", | |
| 93 | + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", | |
| 95 | 94 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); |
| 96 | 95 | } |
| 97 | 96 | |
| ... | ... | @@ -129,26 +128,11 @@ public class IotMqttConnectionManager { |
| 129 | 128 | if (endpoint == null) { |
| 130 | 129 | return null; |
| 131 | 130 | } |
| 132 | - | |
| 133 | 131 | // 获取连接信息 |
| 134 | 132 | return getConnectionInfo(endpoint); |
| 135 | 133 | } |
| 136 | 134 | |
| 137 | 135 | /** |
| 138 | - * 检查设备是否在线 | |
| 139 | - */ | |
| 140 | - public boolean isDeviceOnline(Long deviceId) { | |
| 141 | - return deviceEndpointMap.containsKey(deviceId); | |
| 142 | - } | |
| 143 | - | |
| 144 | - /** | |
| 145 | - * 检查设备是否离线 | |
| 146 | - */ | |
| 147 | - public boolean isDeviceOffline(Long deviceId) { | |
| 148 | - return !isDeviceOnline(deviceId); | |
| 149 | - } | |
| 150 | - | |
| 151 | - /** | |
| 152 | 136 | * 发送消息到设备 |
| 153 | 137 | * |
| 154 | 138 | * @param deviceId 设备 ID |
| ... | ... | @@ -192,28 +176,16 @@ public class IotMqttConnectionManager { |
| 192 | 176 | * 设备 ID |
| 193 | 177 | */ |
| 194 | 178 | private Long deviceId; |
| 195 | - | |
| 196 | 179 | /** |
| 197 | 180 | * 产品 Key |
| 198 | 181 | */ |
| 199 | 182 | private String productKey; |
| 200 | - | |
| 201 | 183 | /** |
| 202 | 184 | * 设备名称 |
| 203 | 185 | */ |
| 204 | 186 | private String deviceName; |
| 205 | 187 | |
| 206 | 188 | /** |
| 207 | - * 客户端 ID | |
| 208 | - */ | |
| 209 | - private String clientId; | |
| 210 | - | |
| 211 | - /** | |
| 212 | - * 是否已认证 | |
| 213 | - */ | |
| 214 | - private boolean authenticated; | |
| 215 | - | |
| 216 | - /** | |
| 217 | 189 | * 连接地址 |
| 218 | 190 | */ |
| 219 | 191 | private String remoteAddress; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 4 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 9 | -import io.netty.handler.codec.mqtt.MqttQoS; | |
| 10 | -import lombok.extern.slf4j.Slf4j; | |
| 11 | - | |
| 12 | -/** | |
| 13 | - * IoT 网关 MQTT 协议:下行消息处理器 | |
| 14 | - * <p> | |
| 15 | - * 专门处理下行消息的业务逻辑,包括: | |
| 16 | - * 1. 消息编码 | |
| 17 | - * 2. 主题构建 | |
| 18 | - * 3. 消息发送 | |
| 19 | - * | |
| 20 | - * @author 芋道源码 | |
| 21 | - */ | |
| 22 | -@Slf4j | |
| 23 | -public class IotMqttDownstreamHandler { | |
| 24 | - | |
| 25 | - private final IotDeviceMessageService deviceMessageService; | |
| 26 | - | |
| 27 | - private final IotMqttConnectionManager connectionManager; | |
| 28 | - | |
| 29 | - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, | |
| 30 | - IotMqttConnectionManager connectionManager) { | |
| 31 | - this.deviceMessageService = deviceMessageService; | |
| 32 | - this.connectionManager = connectionManager; | |
| 33 | - } | |
| 34 | - | |
| 35 | - /** | |
| 36 | - * 处理下行消息 | |
| 37 | - * | |
| 38 | - * @param message 设备消息 | |
| 39 | - * @return 是否处理成功 | |
| 40 | - */ | |
| 41 | - public boolean handleDownstreamMessage(IotDeviceMessage message) { | |
| 42 | - try { | |
| 43 | - // 1. 基础校验 | |
| 44 | - if (message == null || message.getDeviceId() == null) { | |
| 45 | - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); | |
| 46 | - return false; | |
| 47 | - } | |
| 48 | - | |
| 49 | - // 2. 检查设备是否在线 | |
| 50 | - if (connectionManager.isDeviceOffline(message.getDeviceId())) { | |
| 51 | - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); | |
| 52 | - return false; | |
| 53 | - } | |
| 54 | - | |
| 55 | - // 3. 获取连接信息 | |
| 56 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); | |
| 57 | - if (connectionInfo == null) { | |
| 58 | - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); | |
| 59 | - return false; | |
| 60 | - } | |
| 61 | - | |
| 62 | - // 4. 编码消息 | |
| 63 | - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), | |
| 64 | - connectionInfo.getDeviceName()); | |
| 65 | - if (payload == null || payload.length == 0) { | |
| 66 | - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); | |
| 67 | - return false; | |
| 68 | - } | |
| 69 | - | |
| 70 | - // 5. 发送消息到设备 | |
| 71 | - return sendMessageToDevice(message, connectionInfo, payload); | |
| 72 | - } catch (Exception e) { | |
| 73 | - if (message != null) { | |
| 74 | - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", | |
| 75 | - message.getDeviceId(), e.getMessage(), e); | |
| 76 | - } | |
| 77 | - return false; | |
| 78 | - } | |
| 79 | - } | |
| 80 | - | |
| 81 | - /** | |
| 82 | - * 发送消息到设备 | |
| 83 | - * | |
| 84 | - * @param message 设备消息 | |
| 85 | - * @param connectionInfo 连接信息 | |
| 86 | - * @param payload 消息负载 | |
| 87 | - * @return 是否发送成功 | |
| 88 | - */ | |
| 89 | - private boolean sendMessageToDevice(IotDeviceMessage message, | |
| 90 | - IotMqttConnectionManager.ConnectionInfo connectionInfo, | |
| 91 | - byte[] payload) { | |
| 92 | - // 1. 构建主题 | |
| 93 | - String topic = buildDownstreamTopic(message, connectionInfo); | |
| 94 | - if (StrUtil.isBlank(topic)) { | |
| 95 | - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", | |
| 96 | - message.getDeviceId(), message.getMethod()); | |
| 97 | - return false; | |
| 98 | - } | |
| 99 | - | |
| 100 | - // 2. 发送消息 | |
| 101 | - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); | |
| 102 | - if (success) { | |
| 103 | - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", | |
| 104 | - message.getDeviceId(), topic, message.getMethod()); | |
| 105 | - } else { | |
| 106 | - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", | |
| 107 | - message.getDeviceId(), topic, message.getMethod()); | |
| 108 | - } | |
| 109 | - return success; | |
| 110 | - } | |
| 111 | - | |
| 112 | - /** | |
| 113 | - * 构建下行消息主题 | |
| 114 | - * | |
| 115 | - * @param message 设备消息 | |
| 116 | - * @param connectionInfo 连接信息 | |
| 117 | - * @return 主题 | |
| 118 | - */ | |
| 119 | - private String buildDownstreamTopic(IotDeviceMessage message, | |
| 120 | - IotMqttConnectionManager.ConnectionInfo connectionInfo) { | |
| 121 | - String method = message.getMethod(); | |
| 122 | - if (StrUtil.isBlank(method)) { | |
| 123 | - return null; | |
| 124 | - } | |
| 125 | - | |
| 126 | - // 使用工具类构建主题,支持回复消息处理 | |
| 127 | - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); | |
| 128 | - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), | |
| 129 | - connectionInfo.getDeviceName(), isReply); | |
| 130 | - } | |
| 131 | - | |
| 132 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.hutool.core.util.ArrayUtil; | |
| 5 | -import cn.hutool.core.util.BooleanUtil; | |
| 6 | -import cn.hutool.core.util.StrUtil; | |
| 7 | -import cn.hutool.extra.spring.SpringUtil; | |
| 8 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 9 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 11 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 12 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | |
| 13 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 14 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 15 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 16 | -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 17 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 18 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | |
| 19 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 20 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | |
| 21 | -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 23 | -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 24 | -import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | |
| 25 | -import io.netty.handler.codec.mqtt.MqttQoS; | |
| 26 | -import io.vertx.mqtt.MqttEndpoint; | |
| 27 | -import io.vertx.mqtt.MqttTopicSubscription; | |
| 28 | -import lombok.extern.slf4j.Slf4j; | |
| 29 | - | |
| 30 | -import java.util.List; | |
| 31 | -import java.util.Map; | |
| 32 | - | |
| 33 | -/** | |
| 34 | - * MQTT 上行消息处理器 | |
| 35 | - * | |
| 36 | - * @author 芋道源码 | |
| 37 | - */ | |
| 38 | -@Slf4j | |
| 39 | -public class IotMqttUpstreamHandler { | |
| 40 | - | |
| 41 | - /** | |
| 42 | - * 默认编解码类型(MQTT 使用 Alink 协议) | |
| 43 | - */ | |
| 44 | - private static final String DEFAULT_CODEC_TYPE = "Alink"; | |
| 45 | - | |
| 46 | - /** | |
| 47 | - * register 请求的 topic 后缀 | |
| 48 | - */ | |
| 49 | - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; | |
| 50 | - | |
| 51 | - private final IotDeviceMessageService deviceMessageService; | |
| 52 | - | |
| 53 | - private final IotMqttConnectionManager connectionManager; | |
| 54 | - | |
| 55 | - private final IotDeviceCommonApi deviceApi; | |
| 56 | - | |
| 57 | - private final String serverId; | |
| 58 | - | |
| 59 | - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, | |
| 60 | - IotDeviceMessageService deviceMessageService, | |
| 61 | - IotMqttConnectionManager connectionManager) { | |
| 62 | - this.deviceMessageService = deviceMessageService; | |
| 63 | - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 64 | - this.connectionManager = connectionManager; | |
| 65 | - this.serverId = protocol.getServerId(); | |
| 66 | - } | |
| 67 | - | |
| 68 | - /** | |
| 69 | - * 处理 MQTT 连接 | |
| 70 | - * | |
| 71 | - * @param endpoint MQTT 连接端点 | |
| 72 | - */ | |
| 73 | - public void handle(MqttEndpoint endpoint) { | |
| 74 | - String clientId = endpoint.clientIdentifier(); | |
| 75 | - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; | |
| 76 | - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; | |
| 77 | - | |
| 78 | - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", | |
| 79 | - clientId, username, connectionManager.getEndpointAddress(endpoint)); | |
| 80 | - | |
| 81 | - // 1. 先进行认证 | |
| 82 | - if (!authenticateDevice(clientId, username, password, endpoint)) { | |
| 83 | - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 84 | - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); | |
| 85 | - return; | |
| 86 | - } | |
| 87 | - | |
| 88 | - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); | |
| 89 | - | |
| 90 | - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息) | |
| 91 | - endpoint.pingHandler(v -> { | |
| 92 | - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId); | |
| 93 | - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理 | |
| 94 | - }); | |
| 95 | - | |
| 96 | - // 3. 设置异常和关闭处理器 | |
| 97 | - endpoint.exceptionHandler(ex -> { | |
| 98 | - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); | |
| 99 | - cleanupConnection(endpoint); | |
| 100 | - }); | |
| 101 | - endpoint.closeHandler(v -> { | |
| 102 | - cleanupConnection(endpoint); | |
| 103 | - }); | |
| 104 | - | |
| 105 | - // 4. 设置消息处理器 | |
| 106 | - endpoint.publishHandler(mqttMessage -> { | |
| 107 | - try { | |
| 108 | - // 4.1 根据 topic 判断是否为 register 请求 | |
| 109 | - String topic = mqttMessage.topicName(); | |
| 110 | - byte[] payload = mqttMessage.payload().getBytes(); | |
| 111 | - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { | |
| 112 | - // register 请求:使用默认编解码器处理(设备可能未注册) | |
| 113 | - processRegisterMessage(clientId, topic, payload, endpoint); | |
| 114 | - } else { | |
| 115 | - // 业务请求:正常处理 | |
| 116 | - processMessage(clientId, topic, payload); | |
| 117 | - } | |
| 118 | - | |
| 119 | - // 4.2 根据 QoS 级别发送相应的确认消息 | |
| 120 | - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | |
| 121 | - // QoS 1: 发送 PUBACK 确认 | |
| 122 | - endpoint.publishAcknowledge(mqttMessage.messageId()); | |
| 123 | - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { | |
| 124 | - // QoS 2: 发送 PUBREC 确认 | |
| 125 | - endpoint.publishReceived(mqttMessage.messageId()); | |
| 126 | - } | |
| 127 | - // QoS 0 无需确认 | |
| 128 | - } catch (Exception e) { | |
| 129 | - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | |
| 130 | - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | |
| 131 | - cleanupConnection(endpoint); | |
| 132 | - endpoint.close(); | |
| 133 | - } | |
| 134 | - }); | |
| 135 | - | |
| 136 | - // 5. 设置订阅处理器 | |
| 137 | - endpoint.subscribeHandler(subscribe -> { | |
| 138 | - // 提取主题名称列表用于日志显示 | |
| 139 | - List<String> topicNames = subscribe.topicSubscriptions().stream() | |
| 140 | - .map(MqttTopicSubscription::topicName) | |
| 141 | - .collect(java.util.stream.Collectors.toList()); | |
| 142 | - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | |
| 143 | - | |
| 144 | - // 提取 QoS 列表 | |
| 145 | - List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream() | |
| 146 | - .map(MqttTopicSubscription::qualityOfService) | |
| 147 | - .collect(java.util.stream.Collectors.toList()); | |
| 148 | - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); | |
| 149 | - }); | |
| 150 | - | |
| 151 | - // 6. 设置取消订阅处理器 | |
| 152 | - endpoint.unsubscribeHandler(unsubscribe -> { | |
| 153 | - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); | |
| 154 | - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); | |
| 155 | - }); | |
| 156 | - | |
| 157 | - // 7. 设置 QoS 2消息的 PUBREL 处理器 | |
| 158 | - endpoint.publishReleaseHandler(endpoint::publishComplete); | |
| 159 | - | |
| 160 | - // 8. 设置断开连接处理器 | |
| 161 | - endpoint.disconnectHandler(v -> { | |
| 162 | - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); | |
| 163 | - cleanupConnection(endpoint); | |
| 164 | - }); | |
| 165 | - | |
| 166 | - // 9. 接受连接 | |
| 167 | - endpoint.accept(false); | |
| 168 | - } | |
| 169 | - | |
| 170 | - /** | |
| 171 | - * 处理消息 | |
| 172 | - * | |
| 173 | - * @param clientId 客户端 ID | |
| 174 | - * @param topic 主题 | |
| 175 | - * @param payload 消息内容 | |
| 176 | - */ | |
| 177 | - private void processMessage(String clientId, String topic, byte[] payload) { | |
| 178 | - // 1. 基础检查 | |
| 179 | - if (payload == null || payload.length == 0) { | |
| 180 | - return; | |
| 181 | - } | |
| 182 | - | |
| 183 | - // 2. 解析主题,获取 productKey 和 deviceName | |
| 184 | - String[] topicParts = topic.split("/"); | |
| 185 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 186 | - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | |
| 187 | - return; | |
| 188 | - } | |
| 189 | - | |
| 190 | - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) | |
| 191 | - String productKey = topicParts[2]; | |
| 192 | - String deviceName = topicParts[3]; | |
| 193 | - try { | |
| 194 | - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | |
| 195 | - if (message == null) { | |
| 196 | - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 197 | - return; | |
| 198 | - } | |
| 199 | - | |
| 200 | - // 4. 处理业务消息(认证已在连接时完成) | |
| 201 | - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", | |
| 202 | - productKey, deviceName, message.getMethod()); | |
| 203 | - handleBusinessRequest(message, productKey, deviceName); | |
| 204 | - } catch (Exception e) { | |
| 205 | - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 206 | - clientId, topic, e.getMessage(), e); | |
| 207 | - } | |
| 208 | - } | |
| 209 | - | |
| 210 | - /** | |
| 211 | - * 在 MQTT 连接时进行设备认证 | |
| 212 | - * | |
| 213 | - * @param clientId 客户端 ID | |
| 214 | - * @param username 用户名 | |
| 215 | - * @param password 密码 | |
| 216 | - * @param endpoint MQTT 连接端点 | |
| 217 | - * @return 认证是否成功 | |
| 218 | - */ | |
| 219 | - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { | |
| 220 | - try { | |
| 221 | - // 1. 参数校验 | |
| 222 | - if (StrUtil.hasEmpty(clientId, username, password)) { | |
| 223 | - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); | |
| 224 | - return false; | |
| 225 | - } | |
| 226 | - | |
| 227 | - // 2. 构建认证参数 | |
| 228 | - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() | |
| 229 | - .setClientId(clientId) | |
| 230 | - .setUsername(username) | |
| 231 | - .setPassword(password); | |
| 232 | - | |
| 233 | - // 3. 调用设备认证 API | |
| 234 | - CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); | |
| 235 | - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { | |
| 236 | - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 237 | - clientId, username, authResult.getMsg()); | |
| 238 | - return false; | |
| 239 | - } | |
| 240 | - | |
| 241 | - // 4. 获取设备信息 | |
| 242 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | |
| 243 | - if (deviceInfo == null) { | |
| 244 | - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); | |
| 245 | - return false; | |
| 246 | - } | |
| 247 | - | |
| 248 | - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() | |
| 249 | - .setProductKey(deviceInfo.getProductKey()) | |
| 250 | - .setDeviceName(deviceInfo.getDeviceName()); | |
| 251 | - | |
| 252 | - CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO); | |
| 253 | - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { | |
| 254 | - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", | |
| 255 | - clientId, username, deviceResult.getMsg()); | |
| 256 | - return false; | |
| 257 | - } | |
| 258 | - | |
| 259 | - // 5. 注册连接 | |
| 260 | - IotDeviceRespDTO device = deviceResult.getData(); | |
| 261 | - registerConnection(endpoint, device, clientId); | |
| 262 | - | |
| 263 | - // 6. 发送设备上线消息 | |
| 264 | - sendOnlineMessage(device); | |
| 265 | - | |
| 266 | - return true; | |
| 267 | - } catch (Exception e) { | |
| 268 | - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); | |
| 269 | - return false; | |
| 270 | - } | |
| 271 | - } | |
| 272 | - | |
| 273 | - /** | |
| 274 | - * 处理 register 消息(设备动态注册,使用默认编解码器) | |
| 275 | - * | |
| 276 | - * @param clientId 客户端 ID | |
| 277 | - * @param topic 主题 | |
| 278 | - * @param payload 消息内容 | |
| 279 | - * @param endpoint MQTT 连接端点 | |
| 280 | - */ | |
| 281 | - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { | |
| 282 | - // 1.1 基础检查 | |
| 283 | - if (ArrayUtil.isEmpty(payload)) { | |
| 284 | - return; | |
| 285 | - } | |
| 286 | - // 1.2 解析主题,获取 productKey 和 deviceName | |
| 287 | - String[] topicParts = topic.split("/"); | |
| 288 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 289 | - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); | |
| 290 | - return; | |
| 291 | - } | |
| 292 | - String productKey = topicParts[2]; | |
| 293 | - String deviceName = topicParts[3]; | |
| 294 | - | |
| 295 | - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | |
| 296 | - IotDeviceMessage message; | |
| 297 | - try { | |
| 298 | - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | |
| 299 | - if (message == null) { | |
| 300 | - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | |
| 301 | - return; | |
| 302 | - } | |
| 303 | - } catch (Exception e) { | |
| 304 | - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", | |
| 305 | - clientId, topic, e.getMessage(), e); | |
| 306 | - return; | |
| 307 | - } | |
| 308 | - | |
| 309 | - // 3. 处理设备动态注册请求 | |
| 310 | - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", | |
| 311 | - productKey, deviceName, message.getMethod()); | |
| 312 | - try { | |
| 313 | - handleRegisterRequest(message, productKey, deviceName, endpoint); | |
| 314 | - } catch (Exception e) { | |
| 315 | - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | |
| 316 | - clientId, topic, e.getMessage(), e); | |
| 317 | - } | |
| 318 | - } | |
| 319 | - | |
| 320 | - /** | |
| 321 | - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) | |
| 322 | - * | |
| 323 | - * @param message 消息信息 | |
| 324 | - * @param productKey 产品 Key | |
| 325 | - * @param deviceName 设备名称 | |
| 326 | - * @param endpoint MQTT 连接端点 | |
| 327 | - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | |
| 328 | - */ | |
| 329 | - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { | |
| 330 | - String clientId = endpoint.clientIdentifier(); | |
| 331 | - try { | |
| 332 | - // 1. 解析注册参数 | |
| 333 | - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); | |
| 334 | - if (params == null) { | |
| 335 | - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); | |
| 336 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); | |
| 337 | - return; | |
| 338 | - } | |
| 339 | - | |
| 340 | - // 2. 调用动态注册 API | |
| 341 | - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); | |
| 342 | - if (result.isError()) { | |
| 343 | - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | |
| 344 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); | |
| 345 | - return; | |
| 346 | - } | |
| 347 | - | |
| 348 | - // 3. 发送成功响应(包含 deviceSecret) | |
| 349 | - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); | |
| 350 | - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", | |
| 351 | - params.getDeviceName(), clientId); | |
| 352 | - } catch (Exception e) { | |
| 353 | - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); | |
| 354 | - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); | |
| 355 | - } | |
| 356 | - } | |
| 357 | - | |
| 358 | - /** | |
| 359 | - * 解析注册参数 | |
| 360 | - * | |
| 361 | - * @param params 参数对象(通常为 Map 类型) | |
| 362 | - * @return 注册参数 DTO,解析失败时返回 null | |
| 363 | - */ | |
| 364 | - @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 365 | - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | |
| 366 | - if (params == null) { | |
| 367 | - return null; | |
| 368 | - } | |
| 369 | - try { | |
| 370 | - // 参数默认为 Map 类型,直接转换 | |
| 371 | - if (params instanceof Map) { | |
| 372 | - Map<String, Object> paramMap = (Map<String, Object>) params; | |
| 373 | - return new IotDeviceRegisterReqDTO() | |
| 374 | - .setProductKey(MapUtil.getStr(paramMap, "productKey")) | |
| 375 | - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) | |
| 376 | - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); | |
| 377 | - } | |
| 378 | - // 如果已经是目标类型,直接返回 | |
| 379 | - if (params instanceof IotDeviceRegisterReqDTO) { | |
| 380 | - return (IotDeviceRegisterReqDTO) params; | |
| 381 | - } | |
| 382 | - | |
| 383 | - // 其他情况尝试 JSON 转换 | |
| 384 | - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); | |
| 385 | - } catch (Exception e) { | |
| 386 | - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | |
| 387 | - return null; | |
| 388 | - } | |
| 389 | - } | |
| 390 | - | |
| 391 | - /** | |
| 392 | - * 发送注册成功响应(包含 deviceSecret) | |
| 393 | - * | |
| 394 | - * @param endpoint MQTT 连接端点 | |
| 395 | - * @param productKey 产品 Key | |
| 396 | - * @param deviceName 设备名称 | |
| 397 | - * @param requestId 请求 ID | |
| 398 | - * @param registerResp 注册响应 | |
| 399 | - */ | |
| 400 | - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 401 | - String requestId, IotDeviceRegisterRespDTO registerResp) { | |
| 402 | - try { | |
| 403 | - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | |
| 404 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 405 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | |
| 406 | - | |
| 407 | - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | |
| 408 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 409 | - | |
| 410 | - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | |
| 411 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | |
| 412 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | |
| 413 | - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | |
| 414 | - MqttQoS.AT_LEAST_ONCE, false, false); | |
| 415 | - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); | |
| 416 | - } catch (Exception e) { | |
| 417 | - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", | |
| 418 | - endpoint.clientIdentifier(), e); | |
| 419 | - } | |
| 420 | - } | |
| 421 | - | |
| 422 | - /** | |
| 423 | - * 发送注册错误响应 | |
| 424 | - * | |
| 425 | - * @param endpoint MQTT 连接端点 | |
| 426 | - * @param productKey 产品 Key | |
| 427 | - * @param deviceName 设备名称 | |
| 428 | - * @param requestId 请求 ID | |
| 429 | - * @param errorMessage 错误消息 | |
| 430 | - */ | |
| 431 | - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 432 | - String requestId, String errorMessage) { | |
| 433 | - try { | |
| 434 | - // 1. 构建响应消息 | |
| 435 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | |
| 436 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); | |
| 437 | - | |
| 438 | - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | |
| 439 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 440 | - | |
| 441 | - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | |
| 442 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | |
| 443 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | |
| 444 | - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | |
| 445 | - MqttQoS.AT_LEAST_ONCE, false, false); | |
| 446 | - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); | |
| 447 | - } catch (Exception e) { | |
| 448 | - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", | |
| 449 | - endpoint.clientIdentifier(), e); | |
| 450 | - } | |
| 451 | - } | |
| 452 | - | |
| 453 | - /** | |
| 454 | - * 处理业务请求 | |
| 455 | - */ | |
| 456 | - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { | |
| 457 | - // 发送消息到消息总线 | |
| 458 | - message.setServerId(serverId); | |
| 459 | - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 460 | - } | |
| 461 | - | |
| 462 | - /** | |
| 463 | - * 注册连接 | |
| 464 | - */ | |
| 465 | - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { | |
| 466 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() | |
| 467 | - .setDeviceId(device.getId()) | |
| 468 | - .setProductKey(device.getProductKey()) | |
| 469 | - .setDeviceName(device.getDeviceName()) | |
| 470 | - .setClientId(clientId) | |
| 471 | - .setAuthenticated(true) | |
| 472 | - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); | |
| 473 | - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); | |
| 474 | - } | |
| 475 | - | |
| 476 | - /** | |
| 477 | - * 发送设备上线消息 | |
| 478 | - */ | |
| 479 | - private void sendOnlineMessage(IotDeviceRespDTO device) { | |
| 480 | - try { | |
| 481 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 482 | - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), | |
| 483 | - device.getDeviceName(), serverId); | |
| 484 | - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); | |
| 485 | - } catch (Exception e) { | |
| 486 | - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); | |
| 487 | - } | |
| 488 | - } | |
| 489 | - | |
| 490 | - /** | |
| 491 | - * 清理连接 | |
| 492 | - */ | |
| 493 | - private void cleanupConnection(MqttEndpoint endpoint) { | |
| 494 | - try { | |
| 495 | - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 496 | - if (connectionInfo != null) { | |
| 497 | - // 发送设备离线消息 | |
| 498 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 499 | - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), | |
| 500 | - connectionInfo.getDeviceName(), serverId); | |
| 501 | - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); | |
| 502 | - } | |
| 503 | - | |
| 504 | - // 注销连接 | |
| 505 | - connectionManager.unregisterConnection(endpoint); | |
| 506 | - } catch (Exception e) { | |
| 507 | - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage()); | |
| 508 | - } | |
| 509 | - } | |
| 510 | - | |
| 511 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java
| ... | ... | @@ -28,20 +28,6 @@ public class IotTcpConfig { |
| 28 | 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 | 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 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 7 | 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 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; |
| ... | ... | @@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions; |
| 21 | 22 | import io.vertx.core.net.PemKeyCertOptions; |
| 22 | 23 | import lombok.Getter; |
| 23 | 24 | import lombok.extern.slf4j.Slf4j; |
| 24 | -import org.springframework.util.Assert; | |
| 25 | +import cn.hutool.core.lang.Assert; | |
| 25 | 26 | |
| 26 | 27 | /** |
| 27 | 28 | * IoT TCP 协议实现 |
| ... | ... | @@ -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 | 42 | * 服务器 ID(用于消息追踪,全局唯一) |
| 42 | 43 | */ |
| ... | ... | @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol { |
| 76 | 77 | */ |
| 77 | 78 | private final IotTcpFrameCodec frameCodec; |
| 78 | 79 | |
| 79 | - public IotTcpProtocol(ProtocolInstanceProperties properties) { | |
| 80 | + public IotTcpProtocol(ProtocolProperties properties) { | |
| 80 | 81 | IotTcpConfig tcpConfig = properties.getTcp(); |
| 81 | 82 | Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); |
| 82 | 83 | Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); |
| ... | ... | @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol { |
| 128 | 129 | .setTcpNoDelay(true) |
| 129 | 130 | .setReuseAddress(true) |
| 130 | 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 | 134 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 133 | - .setKeyPath(tcpConfig.getSslKeyPath()) | |
| 134 | - .setCertPath(tcpConfig.getSslCertPath()); | |
| 135 | + .setKeyPath(sslConfig.getSslKeyPath()) | |
| 136 | + .setCertPath(sslConfig.getSslCertPath()); | |
| 135 | 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 | 8 | import io.vertx.core.buffer.Buffer; |
| 9 | 9 | import io.vertx.core.parsetools.RecordParser; |
| 10 | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | -import org.springframework.util.Assert; | |
| 11 | +import cn.hutool.core.lang.Assert; | |
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | 14 | * IoT TCP 分隔符帧编解码器 |
| ... | ... | @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { |
| 39 | 39 | private final byte[] delimiterBytes; |
| 40 | 40 | |
| 41 | 41 | public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { |
| 42 | - Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); | |
| 42 | + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); | |
| 43 | 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 | import io.vertx.core.buffer.Buffer; |
| 8 | 8 | import io.vertx.core.parsetools.RecordParser; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | -import org.springframework.util.Assert; | |
| 10 | +import cn.hutool.core.lang.Assert; | |
| 11 | 11 | |
| 12 | 12 | /** |
| 13 | 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 | import io.vertx.core.buffer.Buffer; |
| 8 | 8 | import io.vertx.core.parsetools.RecordParser; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | -import org.springframework.util.Assert; | |
| 10 | +import cn.hutool.core.lang.Assert; | |
| 11 | 11 | |
| 12 | 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 | 45 | } |
| 46 | 46 | |
| 47 | 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 | 51 | // 3. 发送到设备 |
| 52 | 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 | 24 | import io.vertx.core.net.NetSocket; |
| 25 | 25 | import io.vertx.core.parsetools.RecordParser; |
| 26 | 26 | import lombok.extern.slf4j.Slf4j; |
| 27 | -import org.springframework.util.Assert; | |
| 27 | +import cn.hutool.core.lang.Assert; | |
| 28 | 28 | |
| 29 | 29 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| 30 | 30 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 167 | 167 | // 1. 解析认证参数 |
| 168 | 168 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| 169 | 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 | 173 | // 2.1 执行认证 |
| 174 | 174 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| ... | ... | @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 204 | 204 | // 1. 解析注册参数 |
| 205 | 205 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| 206 | 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 | 211 | // 2. 调用动态注册 |
| 211 | 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 | 47 | * @param deviceId 设备 ID |
| 48 | 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 | 52 | if (connectionMap.size() >= maxConnections) { |
| 53 | 53 | throw new IllegalStateException("连接数已达上限: " + maxConnections); |
| 54 | 54 | } |
| ... | ... | @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { |
| 57 | 57 | if (oldSocket != null && oldSocket != socket) { |
| 58 | 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 59 | 59 | deviceId, oldSocket.remoteAddress()); |
| 60 | - oldSocket.close(); | |
| 61 | - // 清理旧连接的映射 | |
| 60 | + // 先清理映射,再关闭连接 | |
| 62 | 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 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 6 | 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 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; |
| ... | ... | @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket; |
| 18 | 18 | import io.vertx.core.datagram.DatagramSocketOptions; |
| 19 | 19 | import lombok.Getter; |
| 20 | 20 | import lombok.extern.slf4j.Slf4j; |
| 21 | -import org.springframework.util.Assert; | |
| 21 | +import cn.hutool.core.lang.Assert; | |
| 22 | 22 | |
| 23 | 23 | /** |
| 24 | 24 | * IoT UDP 协议实现 |
| ... | ... | @@ -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 | 38 | * 服务器 ID(用于消息追踪,全局唯一) |
| 39 | 39 | */ |
| ... | ... | @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol { |
| 70 | 70 | */ |
| 71 | 71 | private final IotMessageSerializer serializer; |
| 72 | 72 | |
| 73 | - public IotUdpProtocol(ProtocolInstanceProperties properties) { | |
| 73 | + public IotUdpProtocol(ProtocolProperties properties) { | |
| 74 | 74 | IotUdpConfig udpConfig = properties.getUdp(); |
| 75 | 75 | Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); |
| 76 | 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 | 27 | import io.vertx.core.datagram.DatagramPacket; |
| 28 | 28 | import io.vertx.core.datagram.DatagramSocket; |
| 29 | 29 | import lombok.extern.slf4j.Slf4j; |
| 30 | -import org.springframework.util.Assert; | |
| 30 | +import cn.hutool.core.lang.Assert; | |
| 31 | 31 | |
| 32 | 32 | import java.net.InetSocketAddress; |
| 33 | 33 | import java.util.Map; |
| ... | ... | @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler { |
| 173 | 173 | // 1. 解析认证参数 |
| 174 | 174 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| 175 | 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 | 179 | // 2.1 执行认证 |
| 180 | 180 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| ... | ... | @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler { |
| 218 | 218 | // 1. 解析注册参数 |
| 219 | 219 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| 220 | 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 | 225 | // 2. 调用动态注册 |
| 225 | 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 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | |
| 11 | 11 | import java.net.InetSocketAddress; |
| 12 | +import java.util.concurrent.CompletableFuture; | |
| 12 | 13 | import java.util.concurrent.TimeUnit; |
| 13 | 14 | |
| 14 | 15 | /** |
| ... | ... | @@ -47,8 +48,8 @@ public class IotUdpSessionManager { |
| 47 | 48 | * @param deviceId 设备 ID |
| 48 | 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 | 53 | if (deviceSessionCache.getIfPresent(deviceId) == null |
| 53 | 54 | && deviceSessionCache.size() >= maxSessions) { |
| 54 | 55 | throw new IllegalStateException("会话数已达上限: " + maxSessions); |
| ... | ... | @@ -113,16 +114,21 @@ public class IotUdpSessionManager { |
| 113 | 114 | } |
| 114 | 115 | InetSocketAddress address = sessionInfo.getAddress(); |
| 115 | 116 | try { |
| 117 | + // 使用 CompletableFuture 同步等待发送结果 | |
| 118 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); | |
| 116 | 119 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 117 | 120 | if (result.succeeded()) { |
| 118 | 121 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", |
| 119 | 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 | 132 | } catch (Exception e) { |
| 127 | 133 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); |
| 128 | 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 | 35 | @NotNull(message = "空闲超时时间不能为空") |
| 36 | 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 | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 8 | 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 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; |
| ... | ... | @@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions; |
| 20 | 21 | import io.vertx.core.net.PemKeyCertOptions; |
| 21 | 22 | import lombok.Getter; |
| 22 | 23 | import lombok.extern.slf4j.Slf4j; |
| 23 | -import org.springframework.util.Assert; | |
| 24 | +import cn.hutool.core.lang.Assert; | |
| 24 | 25 | |
| 25 | 26 | /** |
| 26 | 27 | * IoT WebSocket 协议实现 |
| ... | ... | @@ -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 | 41 | * 服务器 ID(用于消息追踪,全局唯一) |
| 41 | 42 | */ |
| ... | ... | @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 71 | 72 | */ |
| 72 | 73 | private final IotMessageSerializer serializer; |
| 73 | 74 | |
| 74 | - public IotWebSocketProtocol(ProtocolInstanceProperties properties) { | |
| 75 | + public IotWebSocketProtocol(ProtocolProperties properties) { | |
| 75 | 76 | Assert.notNull(properties, "协议实例配置不能为空"); |
| 76 | 77 | Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); |
| 77 | 78 | this.properties = properties; |
| ... | ... | @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 120 | 121 | .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) |
| 121 | 122 | .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) |
| 122 | 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 | 126 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 125 | - .setKeyPath(wsConfig.getSslKeyPath()) | |
| 126 | - .setCertPath(wsConfig.getSslCertPath()); | |
| 127 | + .setKeyPath(sslConfig.getSslKeyPath()) | |
| 128 | + .setCertPath(sslConfig.getSslCertPath()); | |
| 127 | 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 | 23 | import io.vertx.core.Handler; |
| 24 | 24 | import io.vertx.core.http.ServerWebSocket; |
| 25 | 25 | import lombok.extern.slf4j.Slf4j; |
| 26 | -import org.springframework.util.Assert; | |
| 26 | +import cn.hutool.core.lang.Assert; | |
| 27 | 27 | |
| 28 | 28 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| 29 | 29 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 109 | 109 | // 1.2 解码消息 |
| 110 | 110 | message = serializer.deserialize(payload); |
| 111 | 111 | Assert.notNull(message, "消息反序列化失败"); |
| 112 | - Assert.hasText(message.getMethod(), "method 不能为空"); | |
| 112 | + Assert.notBlank(message.getMethod(), "method 不能为空"); | |
| 113 | 113 | |
| 114 | 114 | // 2. 根据消息类型路由处理 |
| 115 | 115 | if (AUTH_METHOD.equals(message.getMethod())) { |
| ... | ... | @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 150 | 150 | // 1. 解析认证参数 |
| 151 | 151 | IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); |
| 152 | 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 | 156 | // 2.1 执行认证 |
| 157 | 157 | CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); |
| ... | ... | @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> { |
| 187 | 187 | // 1. 解析注册参数 |
| 188 | 188 | IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); |
| 189 | 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 | 194 | // 2. 调用动态注册 |
| 194 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.service.device.message; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | |
| 3 | 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 | 11 | public interface IotDeviceMessageService { |
| 11 | 12 | |
| 12 | 13 | /** |
| 13 | - * 编码消息 | |
| 14 | + * 序列化消息 | |
| 14 | 15 | * |
| 15 | 16 | * @param message 消息 |
| 16 | 17 | * @param productKey 产品 Key |
| 17 | 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 | 37 | * @param bytes 消息内容 |
| 37 | 38 | * @param productKey 产品 Key |
| 38 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.service.device.message; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 3 | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; | |
| 6 | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; |
| 8 | 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 | 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 11 | 13 | import jakarta.annotation.Resource; |
| 12 | 14 | import lombok.extern.slf4j.Slf4j; |
| 13 | 15 | import org.springframework.stereotype.Service; |
| 14 | 16 | |
| 15 | 17 | import java.time.LocalDateTime; |
| 16 | -import java.util.List; | |
| 17 | -import java.util.Map; | |
| 18 | 18 | |
| 19 | 19 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 20 | 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 | 28 | @Slf4j |
| 29 | 29 | public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 30 | 30 | |
| 31 | - /** | |
| 32 | - * 编解码器 | |
| 33 | - */ | |
| 34 | - private final Map<String, IotDeviceMessageCodec> codes; | |
| 35 | - | |
| 36 | 31 | @Resource |
| 37 | 32 | private IotDeviceService deviceService; |
| 38 | 33 | |
| 39 | 34 | @Resource |
| 40 | 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 | 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 | 43 | // 1.1 获取设备信息 |
| 50 | 44 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 51 | 45 | if (device == null) { |
| 52 | 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 | 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 | 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 | 72 | // 1.1 获取设备信息 |
| 81 | 73 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 82 | 74 | if (device == null) { |
| 83 | 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 | 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 | 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 | 38 | */ |
| 39 | 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 | 69 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; |
| 64 | 70 | } |
| 65 | 71 | |
| 66 | -} | |
| 67 | 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 | 48 | # 针对引入的 HTTP 组件的配置 |
| 49 | 49 | # ==================================== |
| 50 | 50 | - id: http-json |
| 51 | - type: http | |
| 51 | + protocol: http | |
| 52 | 52 | port: 8092 |
| 53 | 53 | enabled: false |
| 54 | 54 | http: |
| ... | ... | @@ -57,9 +57,9 @@ yudao: |
| 57 | 57 | # 针对引入的 TCP 组件的配置 |
| 58 | 58 | # ==================================== |
| 59 | 59 | - id: tcp-json |
| 60 | - type: tcp | |
| 61 | - port: 8091 | |
| 62 | 60 | enabled: false |
| 61 | + protocol: tcp | |
| 62 | + port: 8091 | |
| 63 | 63 | serialize: json |
| 64 | 64 | tcp: |
| 65 | 65 | max-connections: 1000 |
| ... | ... | @@ -79,9 +79,9 @@ yudao: |
| 79 | 79 | # 针对引入的 UDP 组件的配置 |
| 80 | 80 | # ==================================== |
| 81 | 81 | - id: udp-json |
| 82 | - type: udp | |
| 83 | - port: 8093 | |
| 84 | 82 | enabled: false |
| 83 | + protocol: udp | |
| 84 | + port: 8093 | |
| 85 | 85 | serialize: json |
| 86 | 86 | udp: |
| 87 | 87 | max-sessions: 1000 # 最大会话数 |
| ... | ... | @@ -92,9 +92,9 @@ yudao: |
| 92 | 92 | # 针对引入的 WebSocket 组件的配置 |
| 93 | 93 | # ==================================== |
| 94 | 94 | - id: websocket-json |
| 95 | - type: websocket | |
| 95 | + enabled: false | |
| 96 | + protocol: websocket | |
| 96 | 97 | port: 8094 |
| 97 | - enabled: true | |
| 98 | 98 | serialize: json |
| 99 | 99 | websocket: |
| 100 | 100 | path: /ws |
| ... | ... | @@ -106,21 +106,68 @@ yudao: |
| 106 | 106 | # 针对引入的 CoAP 组件的配置 |
| 107 | 107 | # ==================================== |
| 108 | 108 | - id: coap-json |
| 109 | - type: coap | |
| 109 | + enabled: false | |
| 110 | + protocol: coap | |
| 110 | 111 | port: 5683 |
| 111 | - enabled: true | |
| 112 | 112 | coap: |
| 113 | 113 | max-message-size: 1024 # 最大消息大小(字节) |
| 114 | 114 | ack-timeout-ms: 2000 # ACK 超时时间(毫秒) |
| 115 | 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 | 129 | # 针对引入的 EMQX 组件的配置 |
| 121 | 130 | # ==================================== |
| 122 | - emqx: | |
| 131 | + - id: emqx-1 | |
| 123 | 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 | 171 | http-port: 8090 # MQTT HTTP 服务端口 |
| 125 | 172 | mqtt-host: 127.0.0.1 # MQTT Broker 地址 |
| 126 | 173 | mqtt-port: 1883 # MQTT Broker 端口 |
| ... | ... | @@ -153,25 +200,8 @@ yudao: |
| 153 | 200 | # ==================================== |
| 154 | 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 | 205 | modbus-tcp: |
| 176 | 206 | enabled: true |
| 177 | 207 | config-refresh-interval: 30 # 配置刷新间隔(秒) |
| ... | ... | @@ -194,7 +224,6 @@ logging: |
| 194 | 224 | cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG |
| 195 | 225 | cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG |
| 196 | 226 | cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG |
| 197 | - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG | |
| 198 | 227 | cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG |
| 199 | 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 | 9 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 11 | 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 12 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 12 | 13 | import lombok.extern.slf4j.Slf4j; |
| 13 | 14 | import org.eclipse.californium.core.CoapClient; |
| 14 | 15 | import org.eclipse.californium.core.CoapResponse; |
| ... | ... | @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { |
| 203 | 204 | // 1.1 构建请求 |
| 204 | 205 | String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); |
| 205 | 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 | 214 | String payload = JsonUtils.toJsonString(reqDTO); |
| 211 | 215 | // 1.3 输出请求 |
| 212 | 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 | 10 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 11 | 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 12 | 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 13 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 13 | 14 | import lombok.extern.slf4j.Slf4j; |
| 14 | 15 | import org.junit.jupiter.api.Disabled; |
| 15 | 16 | import org.junit.jupiter.api.Test; |
| ... | ... | @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { |
| 158 | 159 | // 1.1 构建请求 |
| 159 | 160 | String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); |
| 160 | 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 | 165 | IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() |
| 162 | 166 | .setProductKey(PRODUCT_KEY) |
| 163 | - .setDeviceName("test-" + System.currentTimeMillis()) | |
| 164 | - .setProductSecret("test-product-secret"); | |
| 167 | + .setDeviceName(deviceName) | |
| 168 | + .setSign(sign); | |
| 165 | 169 | String payload = JsonUtils.toJsonString(reqDTO); |
| 166 | 170 | // 1.3 输出请求 |
| 167 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | |
| 9 | 7 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 10 | 8 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 11 | 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 | 13 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 15 | 14 | import io.vertx.core.Vertx; |
| 16 | 15 | import io.vertx.core.buffer.Buffer; |
| ... | ... | @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; |
| 23 | 22 | import org.junit.jupiter.api.Test; |
| 24 | 23 | |
| 25 | 24 | import java.util.concurrent.CompletableFuture; |
| 26 | -import java.util.concurrent.CountDownLatch; | |
| 27 | 25 | import java.util.concurrent.TimeUnit; |
| 28 | 26 | |
| 29 | 27 | /** |
| ... | ... | @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { |
| 59 | 57 | |
| 60 | 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 | 64 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 67 | 65 | |
| ... | ... | @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { |
| 88 | 86 | */ |
| 89 | 87 | @Test |
| 90 | 88 | public void testAuth() throws Exception { |
| 91 | - CountDownLatch latch = new CountDownLatch(1); | |
| 92 | - | |
| 93 | 89 | // 1. 构建认证信息 |
| 94 | 90 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 95 | 91 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", |
| 96 | 92 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 97 | 93 | |
| 98 | 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 | 113 | MqttClient client = connectAndAuth(); |
| 136 | 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 | 146 | MqttClient client = connectAndAuth(); |
| 170 | 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 | 173 | /** |
| 198 | 174 | * 直连设备动态注册测试(一型一密) |
| 199 | 175 | * <p> |
| 200 | - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) | |
| 176 | + * 认证方式: | |
| 177 | + * - clientId: 任意值 + "|authType=register|" 后缀 | |
| 178 | + * - username: {deviceName}&{productKey}(与普通认证相同) | |
| 179 | + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256) | |
| 201 | 180 | * <p> |
| 202 | - * 注意:此接口不需要认证 | |
| 181 | + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证 | |
| 203 | 182 | */ |
| 204 | 183 | @Test |
| 205 | 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 | 234 | */ |
| 238 | 235 | @Test |
| 239 | 236 | public void testSubscribe() throws Exception { |
| 240 | - CountDownLatch latch = new CountDownLatch(1); | |
| 241 | - | |
| 242 | 237 | // 1. 连接并认证 |
| 243 | 238 | MqttClient client = connectAndAuth(); |
| 244 | 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 | 264 | * @param authInfo 认证信息 |
| 287 | 265 | * @return MQTT 客户端 |
| 288 | 266 | */ |
| 289 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | |
| 267 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { | |
| 290 | 268 | MqttClientOptions options = new MqttClientOptions() |
| 291 | 269 | .setClientId(authInfo.getClientId()) |
| 292 | 270 | .setUsername(authInfo.getUsername()) |
| ... | ... | @@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { |
| 302 | 280 | * @return 已认证的 MQTT 客户端 |
| 303 | 281 | */ |
| 304 | 282 | private MqttClient connectAndAuth() throws Exception { |
| 305 | - // 1. 创建客户端并连接 | |
| 306 | 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 | 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 | 307 | * @param request 请求消息 |
| 351 | 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 | 312 | // 1. 设置消息处理器,接收响应 |
| 355 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | |
| 313 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); | |
| 356 | 314 | client.publishHandler(message -> { |
| 357 | 315 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 358 | 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 | 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 | 330 | try { |
| 380 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | |
| 331 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | |
| 381 | 332 | } catch (Exception e) { |
| 382 | 333 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 383 | 334 | return null; |
| ... | ... | @@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { |
| 390 | 341 | * @param client MQTT 客户端 |
| 391 | 342 | */ |
| 392 | 343 | private void disconnect(MqttClient client) throws Exception { |
| 393 | - // 1. 断开连接 | |
| 394 | - CompletableFuture<Void> future = new CompletableFuture<>(); | |
| 395 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | -import cn.hutool.core.util.IdUtil; | |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | 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 | 12 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 14 | 13 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; |
| 15 | 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 | 17 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 19 | 18 | import io.vertx.core.Vertx; |
| 20 | 19 | import io.vertx.core.buffer.Buffer; |
| ... | ... | @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; |
| 27 | 26 | import org.junit.jupiter.api.Test; |
| 28 | 27 | |
| 29 | 28 | import java.util.Collections; |
| 30 | -import java.util.List; | |
| 31 | 29 | import java.util.Map; |
| 32 | 30 | import java.util.concurrent.CompletableFuture; |
| 33 | -import java.util.concurrent.CountDownLatch; | |
| 34 | 31 | import java.util.concurrent.TimeUnit; |
| 35 | 32 | |
| 36 | 33 | /** |
| ... | ... | @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { |
| 68 | 65 | |
| 69 | 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 | 72 | // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== |
| 76 | 73 | |
| ... | ... | @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { |
| 103 | 100 | */ |
| 104 | 101 | @Test |
| 105 | 102 | public void testAuth() throws Exception { |
| 106 | - CountDownLatch latch = new CountDownLatch(1); | |
| 107 | - | |
| 108 | 103 | // 1. 构建认证信息 |
| 109 | 104 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 110 | 105 | GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); |
| ... | ... | @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { |
| 112 | 107 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 113 | 108 | |
| 114 | 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 | 130 | MqttClient client = connectAndAuth(); |
| 154 | 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 | 172 | MqttClient client = connectAndAuth(); |
| 197 | 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 | 207 | MqttClient client = connectAndAuth(); |
| 233 | 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 | 244 | MqttClient client = connectAndAuth(); |
| 271 | 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 | 281 | MqttClient client = connectAndAuth(); |
| 309 | 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 | 348 | * @param authInfo 认证信息 |
| 377 | 349 | * @return MQTT 客户端 |
| 378 | 350 | */ |
| 379 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | |
| 351 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { | |
| 380 | 352 | MqttClientOptions options = new MqttClientOptions() |
| 381 | 353 | .setClientId(authInfo.getClientId()) |
| 382 | 354 | .setUsername(authInfo.getUsername()) |
| ... | ... | @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { |
| 392 | 364 | * @return 已认证的 MQTT 客户端 |
| 393 | 365 | */ |
| 394 | 366 | private MqttClient connectAndAuth() throws Exception { |
| 395 | - // 1. 创建客户端并连接 | |
| 396 | 367 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( |
| 397 | 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 | 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 | 392 | * @param request 请求消息 |
| 442 | 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 | 397 | // 1. 设置消息处理器,接收响应 |
| 446 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | |
| 398 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); | |
| 447 | 399 | client.publishHandler(message -> { |
| 448 | 400 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 449 | 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 | 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 | 415 | try { |
| 471 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | |
| 416 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | |
| 472 | 417 | } catch (Exception e) { |
| 473 | 418 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 474 | 419 | return null; |
| ... | ... | @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { |
| 481 | 426 | * @param client MQTT 客户端 |
| 482 | 427 | */ |
| 483 | 428 | private void disconnect(MqttClient client) throws Exception { |
| 484 | - // 1. 断开连接 | |
| 485 | - CompletableFuture<Void> future = new CompletableFuture<>(); | |
| 486 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | 8 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | 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 | 12 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 14 | 13 | import io.vertx.core.Vertx; |
| 15 | 14 | import io.vertx.core.buffer.Buffer; |
| ... | ... | @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; |
| 22 | 21 | import org.junit.jupiter.api.Test; |
| 23 | 22 | |
| 24 | 23 | import java.util.concurrent.CompletableFuture; |
| 25 | -import java.util.concurrent.CountDownLatch; | |
| 26 | 24 | import java.util.concurrent.TimeUnit; |
| 27 | 25 | |
| 28 | 26 | /** |
| ... | ... | @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { |
| 61 | 59 | |
| 62 | 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 | 66 | // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== |
| 69 | 67 | |
| ... | ... | @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { |
| 90 | 88 | */ |
| 91 | 89 | @Test |
| 92 | 90 | public void testAuth() throws Exception { |
| 93 | - CountDownLatch latch = new CountDownLatch(1); | |
| 94 | - | |
| 95 | 91 | // 1. 构建认证信息 |
| 96 | 92 | IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); |
| 97 | 93 | log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", |
| 98 | 94 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 99 | 95 | |
| 100 | 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 | 116 | log.info("[testPropertyPost][连接认证成功]"); |
| 139 | 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 | 151 | log.info("[testEventPost][连接认证成功]"); |
| 175 | 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 | 186 | * @param authInfo 认证信息 |
| 211 | 187 | * @return MQTT 客户端 |
| 212 | 188 | */ |
| 213 | - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { | |
| 189 | + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { | |
| 214 | 190 | MqttClientOptions options = new MqttClientOptions() |
| 215 | 191 | .setClientId(authInfo.getClientId()) |
| 216 | 192 | .setUsername(authInfo.getUsername()) |
| ... | ... | @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { |
| 226 | 202 | * @return 已认证的 MQTT 客户端 |
| 227 | 203 | */ |
| 228 | 204 | private MqttClient connectAndAuth() throws Exception { |
| 229 | - // 1. 创建客户端并连接 | |
| 230 | 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 | 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 | 229 | * @param request 请求消息 |
| 275 | 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 | 234 | // 1. 设置消息处理器,接收响应 |
| 279 | - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | |
| 235 | + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); | |
| 280 | 236 | client.publishHandler(message -> { |
| 281 | 237 | log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", |
| 282 | 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 | 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 | 252 | try { |
| 304 | - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | |
| 253 | + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | |
| 305 | 254 | } catch (Exception e) { |
| 306 | 255 | log.warn("[publishAndWaitReply][等待响应超时或失败]"); |
| 307 | 256 | return null; |
| ... | ... | @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { |
| 314 | 263 | * @param client MQTT 客户端 |
| 315 | 264 | */ |
| 316 | 265 | private void disconnect(MqttClient client) throws Exception { |
| 317 | - // 1. 断开连接 | |
| 318 | - CompletableFuture<Void> future = new CompletableFuture<>(); | |
| 319 | 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 | 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; |
| 13 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; |
| ... | ... | @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { |
| 146 | 147 | @Test |
| 147 | 148 | public void testDeviceRegister() throws Exception { |
| 148 | 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 | 153 | IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() |
| 150 | 154 | .setProductKey(PRODUCT_KEY) |
| 151 | - .setDeviceName("test-tcp-" + System.currentTimeMillis()) | |
| 152 | - .setProductSecret("test-product-secret"); | |
| 155 | + .setDeviceName(deviceName) | |
| 156 | + .setSign(sign); | |
| 153 | 157 | IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 154 | 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 | 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; |
| 13 | 14 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { |
| 100 | 101 | @Test |
| 101 | 102 | public void testDeviceRegister() throws Exception { |
| 102 | 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 | 107 | IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() |
| 104 | 108 | .setProductKey(PRODUCT_KEY) |
| 105 | - .setDeviceName("test-udp-" + System.currentTimeMillis()) | |
| 106 | - .setProductSecret("test-product-secret"); | |
| 109 | + .setDeviceName(deviceName) | |
| 110 | + .setSign(sign); | |
| 107 | 111 | IotDeviceMessage request = IotDeviceMessage.requestOf( |
| 108 | 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 | 10 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; |
| 11 | 11 | import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; |
| 12 | 12 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 13 | +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; | |
| 13 | 14 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| 14 | 15 | import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; |
| 15 | 16 | import io.vertx.core.Vertx; |
| ... | ... | @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { |
| 131 | 132 | @Test |
| 132 | 133 | public void testDeviceRegister() throws Exception { |
| 133 | 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 | 142 | IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), |
| 139 | 143 | IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); |
| 140 | 144 | // 1.2 序列化 | ... | ... |