diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index d7531ea..048b79d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/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 { return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); if (product != null) { - deviceDTO.setCodecType(product.getCodecType()); + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType()); } })); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index ffc92a2..302b072 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/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 { @DictFormat(DictTypeConstants.NET_TYPE) private Integer netType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @ExcelProperty(value = "数据格式", converter = DictConvert.class) - @DictFormat(DictTypeConstants.CODEC_TYPE) - private String codecType; + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") + @ExcelProperty(value = "协议类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) + private String protocolType; + + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") + @ExcelProperty(value = "序列化类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SERIALIZE_TYPE) + private String serializeType; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 08c636f..fceede0 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") private Integer netType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @NotEmpty(message = "数据格式不能为空") - private String codecType; + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}") + @NotEmpty(message = "协议类型不能为空") + private String protocolType; + + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}") + @NotEmpty(message = "序列化类型不能为空") + private String serializeType; @Schema(description = "是否开启动态注册", example = "false") @NotNull(message = "是否开启动态注册不能为空") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index e296b35..a1a77fc 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/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 { */ private Integer netType; /** - * 数据格式(编解码器类型) + * 协议类型 *

- * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} - * - * 目的:用于 gateway-server 解析消息格式 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum} */ - private String codecType; + private String protocolType; + /** + * 序列化类型 + *

+ * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum} + */ + private String serializeType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java index 4f07ddf..cf6bec1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java +++ b/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; public class DictTypeConstants { public static final String NET_TYPE = "iot_net_type"; - public static final String LOCATION_TYPE = "iot_location_type"; - public static final String CODEC_TYPE = "iot_codec_type"; + public static final String PROTOCOL_TYPE = "iot_protocol_type"; + public static final String SERIALIZE_TYPE = "iot_serialize_type"; public static final String PRODUCT_STATUS = "iot_product_status"; public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index 4ec70e0..148dd07 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/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; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { if (BooleanUtil.isFalse(product.getRegisterEnabled())) { throw exception(DEVICE_REGISTER_DISABLED); } - // 1.3 验证 productSecret - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) { + // 1.3 【重要!!!】验证签名 + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(), + product.getProductSecret(), reqDTO.getSign())) { throw exception(DEVICE_REGISTER_SECRET_INVALID); } return TenantUtils.execute(product.getTenantId(), () -> { diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java index add1167..8ad2c5b 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java +++ b/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 { */ private Long productId; /** - * 编解码器类型 + * 协议类型 */ - private String codecType; + private String protocolType; + /** + * 序列化类型 + */ + private String serializeType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java index b8db15f..a77cd42 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java +++ b/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 { private String deviceName; /** - * 产品密钥 + * 注册签名 + * + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String) */ - @NotEmpty(message = "产品密钥不能为空") - private String productSecret; + @NotEmpty(message = "签名不能为空") + private String sign; } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java new file mode 100644 index 0000000..12d1229 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.core.util; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; + +/** + * IoT 产品【动态注册】认证工具类 + *

+ * 用于一型一密场景,使用 productSecret 生成签名 + * + * @author 芋道源码 + */ +public class IotProductAuthUtils { + + /** + * 生成设备动态注册签名 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param productSecret 产品密钥 + * @return 签名 + */ + public static String buildSign(String productKey, String deviceName, String productSecret) { + String content = buildContent(productKey, deviceName); + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret)) + .digestHex(content); + } + + /** + * 验证设备动态注册签名 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param productSecret 产品密钥 + * @param sign 待验证的签名 + * @return 是否验证通过 + */ + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) { + String expectedSign = buildSign(productKey, deviceName, productSecret); + return expectedSign.equals(sign); + } + + /** + * 构建签名内容 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @return 签名内容 + */ + private static String buildContent(String productKey, String deviceName) { + return "deviceName" + deviceName + "productKey" + productKey; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java deleted file mode 100644 index 94dd309..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; - -/** - * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器 - * - * @author 芋道源码 - */ -public interface IotDeviceMessageCodec { - - /** - * 编码消息 - * - * @param message 消息 - * @return 编码后的消息内容 - */ - byte[] encode(IotDeviceMessage message); - - /** - * 解码消息 - * - * @param bytes 消息内容 - * @return 解码后的消息内容 - */ - IotDeviceMessage decode(byte[] bytes); - - /** - * @return 数据格式(编码器类型) - */ - String type(); - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java deleted file mode 100644 index 5a4e47f..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java +++ /dev/null @@ -1,89 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.alink; - -import cn.hutool.core.lang.Assert; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * 阿里云 Alink {@link IotDeviceMessage} 的编解码器 - * - * @author 芋道源码 - */ -@Component -public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "Alink"; - - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class AlinkMessage { - - public static final String VERSION_1 = "1.0"; - - /** - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 - */ - private String id; - - /** - * 版本号 - */ - private String version; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - /** - * 响应错误码 - */ - private Integer code; - /** - * 响应提示 - * - * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。 - */ - private String msg; - - } - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, - message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg()); - return JsonUtils.toJsonByte(alinkMessage); - } - - @Override - @SuppressWarnings("DataFlowIssue") - public IotDeviceMessage decode(byte[] bytes) { - AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class); - Assert.notNull(alinkMessage, "消息不能为空"); - Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0"); - return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(), - alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java deleted file mode 100644 index e1dae77..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 提供设备接入的各种数据(请求、响应)的编解码 - */ -package cn.iocoder.yudao.module.iot.gateway.codec; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java deleted file mode 100644 index 7c043d9..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ /dev/null @@ -1,286 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; - -/** - * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器 - *

- * 二进制协议格式(所有数值使用大端序): - * - *

- * +--------+--------+--------+---------------------------+--------+--------+
- * | 魔术字 | 版本号 | 消息类型|         消息长度(4 字节)          |
- * +--------+--------+--------+---------------------------+--------+--------+
- * |           消息 ID 长度(2 字节)        |      消息 ID (变长字符串)         |
- * +--------+--------+--------+--------+--------+--------+--------+--------+
- * |           方法名长度(2 字节)        |      方法名(变长字符串)         |
- * +--------+--------+--------+--------+--------+--------+--------+--------+
- * |                        消息体数据(变长)                              |
- * +--------+--------+--------+--------+--------+--------+--------+--------+
- * 
- *

- * 消息体格式: - * - 请求消息:params 数据(JSON) - * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) - *

- * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TcpBinary"; - - /** - * 协议魔术字,用于协议识别 - */ - private static final byte MAGIC_NUMBER = (byte) 0x7E; - - /** - * 协议版本号 - */ - private static final byte PROTOCOL_VERSION = (byte) 0x01; - - /** - * 请求消息类型 - */ - private static final byte REQUEST = (byte) 0x01; - - /** - * 响应消息类型 - */ - private static final byte RESPONSE = (byte) 0x02; - - /** - * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度) - */ - private static final int HEADER_FIXED_LENGTH = 7; - - /** - * 最小消息长度(头部 + 消息ID长度 + 方法名长度) - */ - private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - Assert.notNull(message, "消息不能为空"); - Assert.notBlank(message.getMethod(), "消息方法不能为空"); - try { - // 1. 确定消息类型 - byte messageType = determineMessageType(message); - // 2. 构建消息体 - byte[] bodyData = buildMessageBody(message, messageType); - // 3. 构建完整消息 - return buildCompleteMessage(message, messageType, bodyData); - } catch (Exception e) { - log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); - throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); - } - } - - @Override - public IotDeviceMessage decode(byte[] bytes) { - Assert.notNull(bytes, "待解码数据不能为空"); - Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); - try { - Buffer buffer = Buffer.buffer(bytes); - // 解析协议头部和消息内容 - int index = 0; - // 1. 验证魔术字 - byte magic = buffer.getByte(index++); - Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); - - // 2. 验证版本号 - byte version = buffer.getByte(index++); - Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); - - // 3. 读取消息类型 - byte messageType = buffer.getByte(index++); - // 直接验证消息类型,无需抽取方法 - Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, - "无效的消息类型: " + messageType); - - // 4. 读取消息长度 - int messageLength = buffer.getInt(index); - index += 4; - Assert.isTrue(messageLength == buffer.length(), - "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); - - // 5. 读取消息 ID - short messageIdLength = buffer.getShort(index); - index += 2; - String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); - index += messageIdLength; - - // 6. 读取方法名 - short methodLength = buffer.getShort(index); - index += 2; - String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); - index += methodLength; - - // 7. 解析消息体 - return parseMessageBody(buffer, index, messageType, messageId, method); - } catch (Exception e) { - log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); - throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); - } - } - - /** - * 确定消息类型 - * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 - */ - private byte determineMessageType(IotDeviceMessage message) { - // 判断是否为响应消息:有响应码或响应消息时为响应 - if (message.getCode() != null) { - return RESPONSE; - } - // 默认为请求消息 - return REQUEST; - } - - /** - * 构建消息体 - */ - private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { - Buffer bodyBuffer = Buffer.buffer(); - if (messageType == RESPONSE) { - // code - bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); - // msg - String msg = message.getMsg() != null ? message.getMsg() : ""; - byte[] msgBytes = StrUtil.utf8Bytes(msg); - bodyBuffer.appendShort((short) msgBytes.length); - bodyBuffer.appendBytes(msgBytes); - // data - if (message.getData() != null) { - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); - } - } else { - // 请求消息只处理 params 参数 - // TODO @haohao:如果为空,是不是得写个长度 0 哈? - if (message.getParams() != null) { - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); - } - } - return bodyBuffer.getBytes(); - } - - /** - * 构建完整消息 - */ - private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { - Buffer buffer = Buffer.buffer(); - // 1. 写入协议头部 - buffer.appendByte(MAGIC_NUMBER); - buffer.appendByte(PROTOCOL_VERSION); - buffer.appendByte(messageType); - // 2. 预留消息长度位置(在 5. 更新消息长度) - int lengthPosition = buffer.length(); - buffer.appendInt(0); - // 3. 写入消息 ID - String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() - : IotDeviceMessageUtils.generateMessageId(); - byte[] messageIdBytes = StrUtil.utf8Bytes(messageId); - buffer.appendShort((short) messageIdBytes.length); - buffer.appendBytes(messageIdBytes); - // 4. 写入方法名 - byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod()); - buffer.appendShort((short) methodBytes.length); - buffer.appendBytes(methodBytes); - // 5. 写入消息体 - buffer.appendBytes(bodyData); - // 6. 更新消息长度 - buffer.setInt(lengthPosition, buffer.length()); - return buffer.getBytes(); - } - - /** - * 解析消息体 - */ - private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, - String messageId, String method) { - if (startIndex >= buffer.length()) { - // 空消息体 - return IotDeviceMessage.of(messageId, method, null, null, null, null); - } - - if (messageType == RESPONSE) { - // 响应消息:解析 code + msg + data - return parseResponseMessage(buffer, startIndex, messageId, method); - } else { - // 请求消息:解析 payload - Object payload = parseJsonData(buffer, startIndex, buffer.length()); - return IotDeviceMessage.of(messageId, method, payload, null, null, null); - } - } - - /** - * 解析响应消息 - */ - private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { - int index = startIndex; - - // 1. 读取响应码 - Integer code = buffer.getInt(index); - index += 4; - - // 2. 读取响应消息 - short msgLength = buffer.getShort(index); - index += 2; - String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; - index += msgLength; - - // 3. 读取响应数据 - Object data = null; - if (index < buffer.length()) { - data = parseJsonData(buffer, index, buffer.length()); - } - - return IotDeviceMessage.of(messageId, method, null, data, code, msg); - } - - /** - * 解析 JSON 数据 - */ - private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { - if (startIndex >= endIndex) { - return null; - } - try { - String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); - return JsonUtils.parseObject(jsonStr, Object.class); - } catch (Exception e) { - log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); - return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); - } - } - - /** - * 快速检测是否为二进制格式 - * - * @param data 数据 - * @return 是否为二进制格式 - */ - public static boolean isBinaryFormatQuick(byte[] data) { - return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java deleted file mode 100644 index 734c041..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器 - * - * 采用纯 JSON 格式传输,格式如下: - * { - * "id": "消息 ID", - * "method": "消息方法", - * "params": {...}, // 请求参数 - * "data": {...}, // 响应结果 - * "code": 200, // 响应错误码 - * "msg": "success", // 响应提示 - * "timestamp": 时间戳 - * } - * - * @author 芋道源码 - */ -@Component -public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TcpJson"; - - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class TcpJsonMessage { - - /** - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 - */ - private String id; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - - /** - * 响应错误码 - */ - private Integer code; - - /** - * 响应提示 - */ - private String msg; - - /** - * 时间戳 - */ - private Long timestamp; - - } - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( - message.getRequestId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg(), - System.currentTimeMillis()); - return JsonUtils.toJsonByte(tcpJsonMessage); - } - - @Override - @SuppressWarnings("DataFlowIssue") - public IotDeviceMessage decode(byte[] bytes) { - String jsonStr = StrUtil.utf8Str(bytes).trim(); - TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class); - Assert.notNull(tcpJsonMessage, "消息不能为空"); - Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); - return IotDeviceMessage.of( - tcpJsonMessage.getId(), - tcpJsonMessage.getMethod(), - tcpJsonMessage.getParams(), - tcpJsonMessage.getData(), - tcpJsonMessage.getCode(), - tcpJsonMessage.getMsg()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 190d43a..2f865d9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/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 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - @Configuration @EnableConfigurationProperties(IotGatewayProperties.class) -@Slf4j public class IotGatewayConfiguration { @Bean @@ -45,75 +58,6 @@ public class IotGatewayConfiguration { } /** - * IoT 网关 EMQX 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") - @Slf4j - public static class EmqxProtocolConfiguration { - - @Bean(name = "emqxVertx", destroyMethod = "close") - public Vertx emqxVertx() { - return Vertx.vertx(); - } - - @Bean - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("emqxVertx") Vertx emqxVertx) { - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); - } - - @Bean - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("emqxVertx") Vertx emqxVertx) { - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); - } - - @Bean - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, - IotMessageBus messageBus) { - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); - } - } - - /** - * IoT 网关 MQTT 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") - @Slf4j - public static class MqttProtocolConfiguration { - - @Bean(name = "mqttVertx", destroyMethod = "close") - public Vertx mqttVertx() { - return Vertx.vertx(); - } - - @Bean - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager, - @Qualifier("mqttVertx") Vertx mqttVertx) { - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, - connectionManager, mqttVertx); - } - - @Bean - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager) { - return new IotMqttDownstreamHandler(messageService, connectionManager); - } - - @Bean - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, - IotMqttDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); - } - - } - - /** * IoT 网关 Modbus TCP 协议配置类 */ @Configuration @@ -188,8 +132,8 @@ public class IotGatewayConfiguration { @Bean public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, - IotModbusTcpDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { + IotModbusTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 27a673f..ce0222c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; -import io.vertx.core.net.KeyCertOptions; -import io.vertx.core.net.TrustOptions; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -33,14 +33,9 @@ public class IotGatewayProperties { private TokenProperties token; /** - * 协议配置(旧版,保持兼容) - */ - private ProtocolProperties protocol; - - /** * 协议实例列表 */ - private List protocols; + private List protocols; @Data public static class RpcProperties { @@ -79,367 +74,155 @@ public class IotGatewayProperties { } + /** + * 协议实例配置 + */ @Data public static class ProtocolProperties { /** - * EMQX 组件配置 - */ - private EmqxProperties emqx; - - /** - * MQTT 组件配置 + * 协议实例 ID,如 "http-alink"、"tcp-binary" */ - private MqttProperties mqtt; - + @NotEmpty(message = "协议实例 ID 不能为空") + private String id; /** - * Modbus TCP 组件配置 + * 是否启用 */ - private ModbusTcpProperties modbusTcp; - - } - - @Data - public static class HttpProperties { - + @NotNull(message = "是否启用不能为空") + private Boolean enabled = true; /** - * 是否开启 + * 协议类型 + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; + @NotEmpty(message = "协议类型不能为空") + private String protocol; /** * 服务端口 - */ - private Integer serverPort; - - /** - * 是否开启 SSL - */ - @NotNull(message = "是否开启 SSL 不能为空") - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslKeyPath; - /** - * SSL 证书路径 - */ - private String sslCertPath; - - } - - @Data - public static class EmqxProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * HTTP 服务端口(默认:8090) - */ - private Integer httpPort = 8090; - - /** - * MQTT 服务器地址 - */ - @NotEmpty(message = "MQTT 服务器地址不能为空") - private String mqttHost; - - /** - * MQTT 服务器端口(默认:1883) - */ - @NotNull(message = "MQTT 服务器端口不能为空") - private Integer mqttPort = 1883; - - /** - * MQTT 用户名 - */ - @NotEmpty(message = "MQTT 用户名不能为空") - private String mqttUsername; - - /** - * MQTT 密码 - */ - @NotEmpty(message = "MQTT 密码不能为空") - private String mqttPassword; - - /** - * MQTT 客户端的 SSL 开关 - */ - @NotNull(message = "MQTT 是否开启 SSL 不能为空") - private Boolean mqttSsl = false; - - /** - * MQTT 客户端 ID(如果为空,系统将自动生成) - */ - @NotEmpty(message = "MQTT 客户端 ID 不能为空") - private String mqttClientId; - - /** - * MQTT 订阅的主题 - */ - @NotEmpty(message = "MQTT 主题不能为空") - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; - - /** - * 默认 QoS 级别 *

- * 0 - 最多一次 - * 1 - 至少一次 - * 2 - 刚好一次 + * 不同协议含义不同: + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口 + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event) */ - private Integer mqttQos = 1; - + @NotNull(message = "服务端口不能为空") + private Integer port; /** - * 连接超时时间(秒) + * 序列化类型(可选) + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum + * + * 为什么是可选的呢? + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 */ - private Integer connectTimeoutSeconds = 10; + private String serialize; - /** - * 重连延迟时间(毫秒) - */ - private Long reconnectDelayMs = 5000L; + // ========== SSL 配置 ========== /** - * 是否启用 Clean Session (清理会话) - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 + * SSL 配置(可选,配置文件中不配置则为 null) */ - private Boolean cleanSession = true; + @Valid + private SslConfig ssl; + + // ========== 各协议配置 ========== /** - * 心跳间隔(秒) - * 用于保持连接活性,及时发现网络中断。 + * HTTP 协议配置 */ - private Integer keepAliveIntervalSeconds = 60; - + @Valid + private IotHttpConfig http; /** - * 最大未确认消息队列大小 - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 + * WebSocket 协议配置 */ - private Integer maxInflightQueue = 10000; + @Valid + private IotWebSocketConfig websocket; /** - * 是否信任所有 SSL 证书 - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! - * 在生产环境中,应设置为 false,并配置正确的信任库。 + * TCP 协议配置 */ - private Boolean trustAll = false; - + @Valid + private IotTcpConfig tcp; /** - * 遗嘱消息配置 (用于网关异常下线时通知其他系统) + * UDP 协议配置 */ - private final Will will = new Will(); + @Valid + private IotUdpConfig udp; /** - * 高级 SSL/TLS 配置 (用于生产环境) + * CoAP 协议配置 */ - private final Ssl sslOptions = new Ssl(); + @Valid + private IotCoapConfig coap; /** - * 遗嘱消息 (Last Will and Testament) + * MQTT 协议配置 */ - @Data - public static class Will { - - /** - * 是否启用遗嘱消息 - */ - private boolean enabled = false; - /** - * 遗嘱消息主题 - */ - private String topic; - /** - * 遗嘱消息内容 - */ - private String payload; - /** - * 遗嘱消息 QoS 等级 - */ - private Integer qos = 1; - /** - * 遗嘱消息是否作为保留消息发布 - */ - private boolean retain = true; - - } - + @Valid + private IotMqttConfig mqtt; /** - * 高级 SSL/TLS 配置 + * EMQX 协议配置 */ - @Data - public static class Ssl { - - /** - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 - */ - private String keyStorePath; - /** - * 密钥库密码 - */ - private String keyStorePassword; - /** - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 - */ - private String trustStorePath; - /** - * 信任库密码 - */ - private String trustStorePassword; - - } + @Valid + private IotEmqxConfig emqx; } + /** + * SSL 配置 + */ @Data - public static class MqttProperties { + public static class SslConfig { /** - * 是否开启 + * 是否启用 SSL */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; + @NotNull(message = "是否启用 SSL 不能为空") + private Boolean ssl = false; /** - * 服务器端口 + * SSL 证书路径 */ - private Integer port = 1883; + @NotEmpty(message = "SSL 证书路径不能为空") + private String sslCertPath; /** - * 最大消息大小(字节) + * SSL 私钥路径 */ - private Integer maxMessageSize = 8192; + @NotEmpty(message = "SSL 私钥路径不能为空") + private String sslKeyPath; /** - * 连接超时时间(秒) + * 密钥库(KeyStore)路径 + *

+ * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证) */ - private Integer connectTimeoutSeconds = 60; + private String keyStorePath; /** - * 保持连接超时时间(秒) + * 密钥库密码 */ - private Integer keepAliveTimeoutSeconds = 300; + private String keyStorePassword; - // NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低) - /** - * 是否启用 SSL - */ - private Boolean sslEnabled = false; /** - * SSL 配置 + * 信任库(TrustStore)路径 + *

+ * 包含服务端信任的 CA 证书,用于验证服务端的身份 */ - private SslOptions sslOptions = new SslOptions(); - + private String trustStorePath; /** - * SSL 配置选项 + * 信任库密码 */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private KeyCertOptions keyCertOptions; - /** - * 信任选项 - */ - private TrustOptions trustOptions; - /** - * SSL 证书路径 - */ - private String certPath; - /** - * SSL 私钥路径 - */ - private String keyPath; - /** - * 信任存储路径 - */ - private String trustStorePath; - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } + private String trustStorePassword; } - // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整 - /** - * 协议实例配置 - */ @Data - public static class ProtocolInstanceProperties { - - /** - * 协议实例 ID,如 "http-alink"、"tcp-binary" - */ - @NotEmpty(message = "协议实例 ID 不能为空") - private String id; - /** - * 是否启用 - */ - @NotNull(message = "是否启用不能为空") - private Boolean enabled = true; - /** - * 协议类型 - * - * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum - */ - @NotEmpty(message = "协议类型不能为空") - private String type; - /** - * 服务端口 - */ - @NotNull(message = "服务端口不能为空") - private Integer port; - /** - * 序列化类型(可选) - * - * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum - * - * 为什么是可选的呢? - * 1. {@link IotProtocolTypeEnum#HTTP}、${@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 - * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 - */ - private String serialize; - - /** - * HTTP 协议配置 - */ - @Valid - private IotHttpConfig http; - - /** - * TCP 协议配置 - */ - @Valid - private IotTcpConfig tcp; - - /** - * UDP 协议配置 - */ - @Valid - private IotUdpConfig udp; - - /** - * CoAP 协议配置 - */ - @Valid - private IotCoapConfig coap; + public static class ProtocolProperties { /** - * WebSocket 协议配置 + * Modbus TCP 组件配置 */ - @Valid - private IotWebSocketConfig websocket; + private ModbusTcpProperties modbusTcp; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 45b6789..3cd00c7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; @@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle { if (running) { return; } - List protocolConfigs = gatewayProperties.getProtocols(); + List protocolConfigs = gatewayProperties.getProtocols(); if (CollUtil.isEmpty(protocolConfigs)) { log.info("[start][没有配置协议实例,跳过启动]"); return; } - for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) { + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { if (BooleanUtil.isFalse(config.getEnabled())) { log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); continue; @@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle { * @return 协议实例 */ @SuppressWarnings({"EnhancedSwitchMigration"}) - private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { - IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType()); + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) { + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); if (protocolType == null) { - log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType()); + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); return null; } switch (protocolType) { @@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle { return createCoapProtocol(config); case WEBSOCKET: return createWebSocketProtocol(config); + case MQTT: + return createMqttProtocol(config); + case EMQX: + return createEmqxProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return HTTP 协议实例 */ - private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotHttpProtocol(config); } @@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return TCP 协议实例 */ - private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotTcpProtocol(config); } @@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return UDP 协议实例 */ - private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotUdpProtocol(config); } @@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return CoAP 协议实例 */ - private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotCoapProtocol(config); } @@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return WebSocket 协议实例 */ - private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotWebSocketProtocol(config); } + /** + * 创建 MQTT 协议实例 + * + * @param config 协议实例配置 + * @return MQTT 协议实例 + */ + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotMqttProtocol(config); + } + + /** + * 创建 EMQX 协议实例 + * + * @param config 协议实例配置 + * @return EMQX 协议实例 + */ + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotEmqxProtocol(config); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java index 28fa998..14fe10d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.config.CoapConfig; import org.eclipse.californium.elements.config.Configuration; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import java.util.concurrent.TimeUnit; @@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol { */ private final IotCoapDownstreamSubscriber downstreamSubscriber; - public IotCoapProtocol(ProtocolInstanceProperties properties) { + public IotCoapProtocol(ProtocolProperties properties) { IotCoapConfig coapConfig = properties.getCoap(); Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); this.properties = properties; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java index a00cce4..12a70d9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java +++ b/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 { Assert.notNull(request, "请求体不能为空"); Assert.notBlank(request.getProductKey(), "productKey 不能为空"); Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(request); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java deleted file mode 100644 index ce10cf7..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java +++ /dev/null @@ -1,104 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 EMQX 认证事件协议服务 - *

- * 为 EMQX 提供 HTTP 接口服务,包括: - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 - * - * @author 芋道源码 - */ -@Slf4j -public class IotEmqxAuthEventProtocol { - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private final String serverId; - - private final Vertx vertx; - - private HttpServer httpServer; - - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties, - Vertx vertx) { - this.emqxProperties = emqxProperties; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); - } - - @PostConstruct - public void start() { - try { - startHttpServer(); - log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - stopHttpServer(); - log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]"); - } - - /** - * 启动 HTTP 服务器 - */ - private void startHttpServer() { - int port = emqxProperties.getHttpPort(); - - // 1. 创建路由 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); - - // 2. 创建处理器,传入 serverId - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); - router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); - router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); - // TODO @haohao:/mqtt/acl 需要处理么? - // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理 - - // 3. 启动 HTTP 服务器 - try { - httpServer = vertx.createHttpServer() - .requestHandler(router) - .listen(port) - .result(); - } catch (Exception e) { - log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e); - throw e; - } - } - - /** - * 停止 HTTP 服务器 - */ - private void stopHttpServer() { - if (httpServer == null) { - return; - } - - try { - httpServer.close().result(); - log.info("[stopHttpServer][HTTP 服务器已停止]"); - } catch (Exception e) { - log.error("[stopHttpServer][HTTP 服务器停止失败]", e); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java new file mode 100644 index 0000000..bc039fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java @@ -0,0 +1,225 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * IoT EMQX 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotEmqxConfig { + + // ========== MQTT Client 配置(连接 EMQX Broker) ========== + + /** + * MQTT 服务器地址 + */ + @NotEmpty(message = "MQTT 服务器地址不能为空") + private String mqttHost; + + /** + * MQTT 服务器端口(默认:1883) + */ + @NotNull(message = "MQTT 服务器端口不能为空") + private Integer mqttPort = 1883; + + /** + * MQTT 用户名 + */ + @NotEmpty(message = "MQTT 用户名不能为空") + private String mqttUsername; + + /** + * MQTT 密码 + */ + @NotEmpty(message = "MQTT 密码不能为空") + private String mqttPassword; + + /** + * MQTT 客户端的 SSL 开关 + */ + @NotNull(message = "MQTT 是否开启 SSL 不能为空") + private Boolean mqttSsl = false; + + /** + * MQTT 客户端 ID + */ + @NotEmpty(message = "MQTT 客户端 ID 不能为空") + private String mqttClientId; + + /** + * MQTT 订阅的主题 + */ + @NotEmpty(message = "MQTT 主题不能为空") + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; + + /** + * 默认 QoS 级别 + *

+ * 0 - 最多一次 + * 1 - 至少一次 + * 2 - 刚好一次 + */ + @NotNull(message = "MQTT QoS 不能为空") + @Min(value = 0, message = "MQTT QoS 不能小于 0") + @Max(value = 2, message = "MQTT QoS 不能大于 2") + private Integer mqttQos = 1; + + /** + * 连接超时时间(秒) + */ + @NotNull(message = "连接超时时间不能为空") + @Min(value = 1, message = "连接超时时间不能小于 1 秒") + private Integer connectTimeoutSeconds = 10; + + /** + * 重连延迟时间(毫秒) + */ + @NotNull(message = "重连延迟时间不能为空") + @Min(value = 0, message = "重连延迟时间不能小于 0 毫秒") + private Long reconnectDelayMs = 5000L; + + /** + * 是否启用 Clean Session (清理会话) + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 + */ + @NotNull(message = "是否启用 Clean Session 不能为空") + private Boolean cleanSession = true; + + /** + * 心跳间隔(秒) + * 用于保持连接活性,及时发现网络中断。 + */ + @NotNull(message = "心跳间隔不能为空") + @Min(value = 1, message = "心跳间隔不能小于 1 秒") + private Integer keepAliveIntervalSeconds = 60; + + /** + * 最大未确认消息队列大小 + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 + */ + @NotNull(message = "最大未确认消息队列大小不能为空") + @Min(value = 1, message = "最大未确认消息队列大小不能小于 1") + private Integer maxInflightQueue = 10000; + + /** + * 是否信任所有 SSL 证书 + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! + * 在生产环境中,应设置为 false,并配置正确的信任库。 + */ + @NotNull(message = "是否信任所有 SSL 证书不能为空") + private Boolean trustAll = false; + + // ========== MQTT Will / SSL 高级配置 ========== + + /** + * 遗嘱消息配置 (用于网关异常下线时通知其他系统) + */ + @Valid + private Will will = new Will(); + + /** + * 高级 SSL/TLS 配置 (用于生产环境) + */ + @Valid + private Ssl sslOptions = new Ssl(); + + // ========== HTTP Hook 配置(网关提供给 EMQX 调用) ========== + + /** + * HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event) + */ + @Valid + private Http http = new Http(); + + /** + * 遗嘱消息 (Last Will and Testament) + */ + @Data + public static class Will { + + /** + * 是否启用遗嘱消息 + */ + private boolean enabled = false; + /** + * 遗嘱消息主题 + */ + private String topic; + /** + * 遗嘱消息内容 + */ + private String payload; + /** + * 遗嘱消息 QoS 等级 + */ + @Min(value = 0, message = "遗嘱消息 QoS 不能小于 0") + @Max(value = 2, message = "遗嘱消息 QoS 不能大于 2") + private Integer qos = 1; + /** + * 遗嘱消息是否作为保留消息发布 + */ + private boolean retain = true; + + } + + /** + * 高级 SSL/TLS 配置 + */ + @Data + public static class Ssl { + + /** + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 + */ + private String keyStorePath; + /** + * 密钥库密码 + */ + private String keyStorePassword; + /** + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 + */ + private String trustStorePath; + /** + * 信任库密码 + */ + private String trustStorePassword; + + } + + /** + * HTTP Hook 服务 SSL 配置 + */ + @Data + public static class Http { + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java new file mode 100644 index 0000000..a9e2517 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java @@ -0,0 +1,531 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IoT 网关 EMQX 协议实现: + *

+ * 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用 + * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * EMQX 配置 + */ + private final IotEmqxConfig emqxConfig; + /** + * 服务器 ID + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * HTTP Hook 服务器 + */ + private HttpServer httpServer; + + /** + * MQTT Client + */ + private volatile MqttClient mqttClient; + /** + * MQTT 重连定时器 ID + */ + private volatile Long reconnectTimerId; + + /** + * 上行消息处理器 + */ + private final IotEmqxUpstreamHandler upstreamHandler; + + /** + * 下行消息订阅者 + */ + private final IotEmqxDownstreamSubscriber downstreamSubscriber; + + public IotEmqxProtocol(ProtocolProperties properties) { + Assert.notNull(properties, "协议实例配置不能为空"); + Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空"); + this.properties = properties; + this.emqxConfig = properties.getEmqx(); + Assert.notNull(emqxConfig.getConnectTimeoutSeconds(), + "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.EMQX; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + try { + // 1.2 启动 HTTP Hook 服务 + startHttpServer(); + + // 1.3 启动 MQTT Client + startMqttClient(); + running = true; + log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); + // 启动失败时,关闭资源 + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 先置为 false:避免 closeHandler 触发重连 + running = false; + stopMqttClientReconnectChecker(); + // 2.2 停止 MQTT Client + stopMqttClient(); + + // 2.3 停止 HTTP Hook 服务 + stopHttpServer(); + + // 2.4 关闭 Vertx + if (vertx != null) { + try { + vertx.close().toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + + log.info("[stop][IoT EMQX 协议 {} 已停止]", getId()); + } + + // ======================================= HTTP Hook Server ======================================= + + /** + * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event) + */ + private void startHttpServer() { + // 1. 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 + + // 2. 创建处理器 + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this); + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); + router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); + + // 3. 启动 HTTP Server(支持 HTTPS) + IotEmqxConfig.Http httpConfig = emqxConfig.getHttp(); + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { + Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空"); + Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空"); + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(httpConfig.getSslKeyPath()) + .setCertPath(httpConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]", + getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())); + } catch (Exception e) { + log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e); + throw new RuntimeException("HTTP Hook 服务启动失败", e); + } + } + + private void stopHttpServer() { + if (httpServer == null) { + return; + } + try { + httpServer.close().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId()); + } catch (Exception e) { + log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e); + } finally { + httpServer = null; + } + } + + // ======================================= MQTT Client ====================================== + + private void startMqttClient() { + // 1.1 创建 MQTT Client + MqttClient client = createMqttClient(); + this.mqttClient = client; + // 1.2 连接 MQTT Broker + if (!connectMqttClient(client)) { + throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败"); + } + + // 2. 启动定时重连检查 + startMqttClientReconnectChecker(); + } + + private void stopMqttClient() { + MqttClient client = this.mqttClient; + this.mqttClient = null; // 先清理引用 + if (client == null) { + return; + } + + // 1. 批量取消订阅(仅在连接时) + if (client.isConnected()) { + List topicList = emqxConfig.getMqttTopics(); + if (CollUtil.isNotEmpty(topicList)) { + try { + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); + } + } + } + + // 2. 断开 MQTT 连接 + try { + client.disconnect().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e); + } + } + + // ======================================= MQTT 基础方法 ====================================== + + /** + * 创建 MQTT 客户端 + * + * @return 新创建的 MqttClient + */ + private MqttClient createMqttClient() { + // 1.1 基础配置 + MqttClientOptions options = new MqttClientOptions() + .setClientId(emqxConfig.getMqttClientId()) + .setUsername(emqxConfig.getMqttUsername()) + .setPassword(emqxConfig.getMqttPassword()) + .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl())) + .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession())) + .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds()) + .setMaxInflightQueue(emqxConfig.getMaxInflightQueue()); + options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒 + options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll())); + // 1.2 配置遗嘱消息 + IotEmqxConfig.Will will = emqxConfig.getWill(); + if (will != null && will.isEnabled()) { + Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空"); + Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空"); + options.setWillFlag(true) + .setWillTopic(will.getTopic()) + .setWillMessageBytes(Buffer.buffer(will.getPayload())) + .setWillQoS(will.getQos()) + .setWillRetain(will.isRetain()); + } + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空) + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) + && Boolean.FALSE.equals(emqxConfig.getTrustAll()) + && sslOptions != null) { + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { + options.setTrustStoreOptions(new JksOptions() + .setPath(sslOptions.getTrustStorePath()) + .setPassword(sslOptions.getTrustStorePassword())); + } + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { + options.setKeyStoreOptions(new JksOptions() + .setPath(sslOptions.getKeyStorePath()) + .setPassword(sslOptions.getKeyStorePassword())); + } + } + + // 2. 创建客户端 + return MqttClient.create(vertx, options); + } + + /** + * 连接 MQTT Broker(同步等待) + * + * @param client MQTT 客户端 + * @return 连接成功返回 true,失败返回 false + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private synchronized boolean connectMqttClient(MqttClient client) { + String host = emqxConfig.getMqttHost(); + int port = emqxConfig.getMqttPort(); + int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds(); + try { + // 1. 连接 Broker + client.connect(port, host).toCompletionStage().toCompletableFuture() + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]", + getId(), host, port); + + // 2. 设置处理器 + setupMqttClientHandlers(client); + subscribeMqttClientTopics(client); + return true; + } catch (Exception e) { + log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e); + return false; + } + } + + /** + * 关闭 MQTT 客户端 + */ + private void closeMqttClient() { + MqttClient oldClient = this.mqttClient; + this.mqttClient = null; // 先清理引用 + if (oldClient == null) { + return; + } + // 尽力释放(无论是否连接都尝试 disconnect) + try { + oldClient.disconnect().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + + // ======================================= MQTT 重连机制 ====================================== + + /** + * 启动 MQTT Client 周期性重连检查器 + */ + private void startMqttClientReconnectChecker() { + long interval = emqxConfig.getReconnectDelayMs(); + this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> { + if (!running) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId()); + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) + vertx.executeBlocking(() -> { + tryReconnectMqttClient(); + return null; + }); + }); + } + + /** + * 停止 MQTT Client 重连检查器 + */ + private void stopMqttClientReconnectChecker() { + if (reconnectTimerId != null && vertx != null) { + try { + vertx.cancelTimer(reconnectTimerId); + } catch (Exception ignored) { + } + reconnectTimerId = null; + } + } + + /** + * 尝试重连 MQTT Client + */ + private synchronized void tryReconnectMqttClient() { + // 1. 前置检查 + if (!running) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + + log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId()); + try { + // 2. 关闭旧客户端 + closeMqttClient(); + + // 3.1 创建新客户端 + MqttClient client = createMqttClient(); + this.mqttClient = client; + // 3.2 连接(失败只打印日志,等下次定时) + if (!connectMqttClient(client)) { + log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId()); + } + } catch (Exception e) { + log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e); + } + } + + // ======================================= MQTT Handler ====================================== + + /** + * 设置 MQTT Client 事件处理器 + */ + private void setupMqttClientHandlers(MqttClient client) { + // 1. 断开重连监听 + client.closeHandler(closeEvent -> { + if (!running) { + return; + } + log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId()); + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) + vertx.executeBlocking(() -> { + tryReconnectMqttClient(); + return null; + }); + }); + + // 2. 异常处理 + client.exceptionHandler(exception -> + log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception)); + + // 3. 上行消息处理 + client.publishHandler(upstreamHandler::handle); + } + + /** + * 订阅 MQTT Client 主题(同步等待) + */ + private void subscribeMqttClientTopics(MqttClient client) { + List topicList = emqxConfig.getMqttTopics(); + if (!client.isConnected()) { + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId()); + return; + } + if (CollUtil.isEmpty(topicList)) { + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId()); + return; + } + // 执行订阅 + Map topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic, + topic -> emqxConfig.getMqttQos()); + try { + client.subscribe(topics).toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size()); + } catch (Exception e) { + log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e); + } + } + + /** + * 发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + */ + public void publishMessage(String topic, byte[] payload) { + if (mqttClient == null || !mqttClient.isConnected()) { + log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId()); + return; + } + MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos()); + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false) + .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); + } + + /** + * 延迟发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + * @param delayMs 延迟时间(毫秒) + */ + public void publishDelayMessage(String topic, byte[] payload, long delayMs) { + vertx.setTimer(delayMs, id -> publishMessage(topic, payload)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java deleted file mode 100644 index 47b2f16..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java +++ /dev/null @@ -1,386 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.JksOptions; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * IoT 网关 EMQX 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotEmqxUpstreamProtocol implements IotProtocol { - - private static final String ID = "emqx"; - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private volatile boolean running = false; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private MqttClient mqttClient; - - private IotEmqxUpstreamHandler upstreamHandler; - - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties, - Vertx vertx) { - this.emqxProperties = emqxProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); - this.vertx = vertx; - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.EMQX; - } - - @Override - @PostConstruct - public void start() { - if (running) { - return; - } - - try { - // 1. 启动 MQTT 客户端 - startMqttClient(); - - // 2. 标记服务为运行状态 - running = true; - log.info("[start][IoT 网关 EMQX 协议启动成功]"); - } catch (Exception e) { - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); - stop(); - - // 异步关闭应用 - Thread shutdownThread = new Thread(() -> { - try { - // 确保日志输出完成,使用更优雅的方式 - log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); - // 等待日志输出完成 - Thread.sleep(1000); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - log.warn("[start][应用关闭被中断]"); - } - System.exit(1); - }); - shutdownThread.setDaemon(true); - shutdownThread.setName("emergency-shutdown"); - shutdownThread.start(); - - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (!running) { - return; - } - - // 1. 停止 MQTT 客户端 - stopMqttClient(); - - // 2. 标记服务为停止状态 - running = false; - log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); - } - - @Override - public boolean isRunning() { - return running; - } - - /** - * 启动 MQTT 客户端 - */ - private void startMqttClient() { - try { - // 1. 初始化消息处理器 - this.upstreamHandler = new IotEmqxUpstreamHandler(this); - - // 2. 创建 MQTT 客户端 - createMqttClient(); - - // 3. 同步连接 MQTT Broker - connectMqttSync(); - } catch (Exception e) { - log.error("[startMqttClient][MQTT 客户端启动失败]", e); - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); - } - } - - /** - * 同步连接 MQTT Broker - */ - private void connectMqttSync() { - String host = emqxProperties.getMqttHost(); - int port = emqxProperties.getMqttPort(); - // 1. 连接 MQTT Broker - CountDownLatch latch = new CountDownLatch(1); - AtomicBoolean success = new AtomicBoolean(false); - mqttClient.connect(port, host, connectResult -> { - if (connectResult.succeeded()) { - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); - setupMqttHandlers(); - subscribeToTopics(); - success.set(true); - } else { - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", - host, port, connectResult.cause()); - } - latch.countDown(); - }); - - // 2. 等待连接结果 - try { - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制 - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); - if (!awaitResult) { - log.error("[connectMqttSync][等待连接结果超时]"); - throw new RuntimeException("连接 MQTT Broker 超时"); - } - if (!success.get()) { - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port)); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("[connectMqttSync][等待连接结果被中断]", e); - throw new RuntimeException("连接 MQTT Broker 被中断", e); - } - } - - /** - * 异步连接 MQTT Broker - */ - private void connectMqttAsync() { - String host = emqxProperties.getMqttHost(); - int port = emqxProperties.getMqttPort(); - mqttClient.connect(port, host, connectResult -> { - if (connectResult.succeeded()) { - log.info("[connectMqttAsync][MQTT 客户端重连成功]"); - setupMqttHandlers(); - subscribeToTopics(); - } else { - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", - host, port, connectResult.cause()); - log.warn("[connectMqttAsync][重连失败,将再次尝试]"); - reconnectWithDelay(); - } - }); - } - - /** - * 延迟重连 - */ - private void reconnectWithDelay() { - if (!running) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - long delay = emqxProperties.getReconnectDelayMs(); - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); - vertx.setTimer(delay, timerId -> { - if (!running) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); - try { - createMqttClient(); - connectMqttAsync(); - } catch (Exception e) { - log.error("[reconnectWithDelay][重连过程中发生异常]", e); - vertx.setTimer(delay, t -> reconnectWithDelay()); - } - }); - } - - /** - * 停止 MQTT 客户端 - */ - private void stopMqttClient() { - if (mqttClient == null) { - return; - } - try { - if (mqttClient.isConnected()) { - // 1. 取消订阅所有主题 - List topicList = emqxProperties.getMqttTopics(); - for (String topic : topicList) { - try { - mqttClient.unsubscribe(topic); - } catch (Exception e) { - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); - } - } - - // 2. 断开 MQTT 客户端连接 - try { - CountDownLatch disconnectLatch = new CountDownLatch(1); - mqttClient.disconnect(ar -> disconnectLatch.countDown()); - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { - log.warn("[stopMqttClient][断开 MQTT 连接超时]"); - } - } catch (Exception e) { - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); - } - } - } catch (Exception e) { - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); - } finally { - mqttClient = null; - } - } - - /** - * 创建 MQTT 客户端 - */ - private void createMqttClient() { - // 1.1 创建基础配置 - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions() - .setClientId(emqxProperties.getMqttClientId()) - .setUsername(emqxProperties.getMqttUsername()) - .setPassword(emqxProperties.getMqttPassword()) - .setSsl(emqxProperties.getMqttSsl()) - .setCleanSession(emqxProperties.getCleanSession()) - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds()) - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue()) - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒 - .setTrustAll(emqxProperties.getTrustAll()); - // 1.2 配置遗嘱消息 - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill(); - if (will.isEnabled()) { - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空"); - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空"); - options.setWillFlag(true) - .setWillTopic(will.getTopic()) - .setWillMessageBytes(Buffer.buffer(will.getPayload())) - .setWillQoS(will.getQos()) - .setWillRetain(will.isRetain()); - } - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效) - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) { - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions(); - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { - options.setTrustStoreOptions(new JksOptions() - .setPath(sslOptions.getTrustStorePath()) - .setPassword(sslOptions.getTrustStorePassword())); - } - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { - options.setKeyStoreOptions(new JksOptions() - .setPath(sslOptions.getKeyStorePath()) - .setPassword(sslOptions.getKeyStorePassword())); - } - } - // 1.4 安全警告日志 - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) { - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]"); - } - - // 2. 创建客户端实例 - this.mqttClient = MqttClient.create(vertx, options); - } - - /** - * 设置 MQTT 处理器 - */ - private void setupMqttHandlers() { - // 1. 设置断开重连监听器 - mqttClient.closeHandler(closeEvent -> { - if (!running) { - return; - } - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); - reconnectWithDelay(); - }); - - // 2. 设置异常处理器 - mqttClient.exceptionHandler(exception -> - log.error("[exceptionHandler][MQTT 客户端异常]", exception)); - - // 3. 设置消息处理器 - mqttClient.publishHandler(upstreamHandler::handle); - } - - /** - * 订阅设备上行消息主题 - */ - private void subscribeToTopics() { - // 1. 校验 MQTT 客户端是否连接 - List topicList = emqxProperties.getMqttTopics(); - if (mqttClient == null || !mqttClient.isConnected()) { - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); - return; - } - - // 2. 批量订阅所有主题 - Map topics = new HashMap<>(); - int qos = emqxProperties.getMqttQos(); - for (String topic : topicList) { - topics.put(topic, qos); - } - mqttClient.subscribe(topics, subscribeResult -> { - if (subscribeResult.succeeded()) { - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); - } else { - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]", - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause()); - } - }); - } - - /** - * 发布消息到 MQTT Broker - * - * @param topic 主题 - * @param payload 消息内容 - */ - public void publishMessage(String topic, byte[] payload) { - if (mqttClient == null || !mqttClient.isConnected()) { - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); - return; - } - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java index 06632b3..db5ea12 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java @@ -1,11 +1,11 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotEmqxDownstreamHandler { - private final IotEmqxUpstreamProtocol protocol; + private final IotEmqxProtocol protocol; private final IotDeviceService deviceService; private final IotDeviceMessageService deviceMessageService; - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) { this.protocol = protocol; this.deviceService = SpringUtil.getBean(IotDeviceService.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { return; } // 2.2 构建载荷 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - // 2.3 发布消息 + + // 3. 发布消息 protocol.publishMessage(topic, payload); } @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java index 4b5bad2..55aaaac 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java @@ -1,10 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import lombok.extern.slf4j.Slf4j; /** @@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber private final IotEmqxDownstreamHandler downstreamHandler; - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { super(protocol, messageBus); this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java index 6b6694f..ccc7a2b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java @@ -1,25 +1,35 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import lombok.extern.slf4j.Slf4j; +import java.util.Locale; + /** * IoT 网关 EMQX 认证事件处理器 *

* 为 EMQX 提供 HTTP 接口服务,包括: - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)} * * @author 芋道源码 */ @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { private static final String RESULT_IGNORE = "ignore"; /** - * EMQX 事件类型常量 + * EMQX 事件类型常量 - 客户端连接 */ private static final String EVENT_CLIENT_CONNECTED = "client.connected"; + /** + * EMQX 事件类型常量 - 客户端断开连接 + */ private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; + /** + * 认证类型标识 - 设备注册 + */ + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; + private final String serverId; - private final IotDeviceMessageService deviceMessageService; + private final IotEmqxProtocol protocol; + private final IotDeviceMessageService deviceMessageService; private final IotDeviceCommonApi deviceApi; - public IotEmqxAuthEventHandler(String serverId) { + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { this.serverId = serverId; + this.protocol = protocol; this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); } + // ========== 认证处理 ========== + /** * EMQX 认证接口 */ public void handleAuth(RoutingContext context) { + JsonObject body = null; try { // 1. 参数校验 - JsonObject body = parseRequestBody(context); + body = parseRequestBody(context); if (body == null) { return; } @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { return; } - // 2. 执行认证 + // 2.1 情况一:判断是否为注册请求 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { + handleDeviceRegister(context, username, password); + return; + } + + // 2.2 情况二:执行认证 boolean authResult = handleDeviceAuth(clientId, username, password); log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); if (authResult) { @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler { sendAuthResponse(context, RESULT_DENY); } } catch (Exception e) { - log.error("[handleAuth][设备认证异常]", e); + log.error("[handleAuth][设备认证异常][body={}]", body, e); sendAuthResponse(context, RESULT_IGNORE); } } /** + * 解析认证接口请求体 + *

+ * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) + * + * @param context 路由上下文 + * @return 请求体JSON对象,解析失败时返回null + */ + private JsonObject parseRequestBody(RoutingContext context) { + try { + JsonObject body = context.body().asJsonObject(); + if (body == null) { + log.info("[parseRequestBody][请求体为空]"); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + return body; + } catch (Exception e) { + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + } + + /** + * 执行设备认证 + * + * @param clientId 客户端ID + * @param username 用户名 + * @param password 密码 + * @return 认证是否成功 + */ + private boolean handleDeviceAuth(String clientId, String username, String password) { + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password)); + result.checkError(); + return BooleanUtil.isTrue(result.getData()); + } catch (Exception e) { + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); + throw e; + } + } + + /** + * 发送 EMQX 认证响应 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 + * + * @param context 路由上下文 + * @param result 认证结果:allow、deny、ignore + */ + private void sendAuthResponse(RoutingContext context, String result) { + // 构建符合 EMQX 官方规范的响应 + JsonObject response = new JsonObject() + .put("result", result) + .put("is_superuser", false); + // 可以根据业务需求添加客户端属性 + // response.put("client_attrs", new JsonObject().put("role", "device")); + // 可以添加认证过期时间(可选) + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); + + // 回复响应 + context.response() + .setStatusCode(SUCCESS_STATUS_CODE) + .putHeader("Content-Type", "application/json; charset=utf-8") + .end(response.encode()); + } + + // ========== ACL 处理 ========== + + /** + * EMQX ACL 接口 + *

+ * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。 + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。 + */ + public void handleAcl(RoutingContext context) { + JsonObject body = null; + try { + // 1.1 解析请求体 + body = parseRequestBody(context); + if (body == null) { + return; + } + String username = body.getString("username"); + String topic = body.getString("topic"); + if (StrUtil.hasBlank(username, topic)) { + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic); + sendAuthResponse(context, RESULT_IGNORE); + return; + } + // 1.2 解析设备身份 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + sendAuthResponse(context, RESULT_IGNORE); + return; + } + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段) + Boolean subscribe = parseAclSubscribeFlag(body); + if (subscribe == null) { + sendAuthResponse(context, RESULT_IGNORE); + return; + } + + // 2. 执行 ACL 校验 + boolean allowed = subscribe + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()) + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY); + } catch (Exception e) { + log.error("[handleAcl][ACL 处理失败][body={}]", body, e); + sendAuthResponse(context, RESULT_IGNORE); + } + } + + /** + * 解析 ACL 动作类型:订阅/发布 + * + * @param body ACL 请求体 + * @return true 订阅;false 发布;null 不识别 + */ + private static Boolean parseAclSubscribeFlag(JsonObject body) { + // 1. action 字段(常见为 publish/subscribe) + String action = body.getString("action"); + if (StrUtil.isNotBlank(action)) { + String lower = action.toLowerCase(Locale.ROOT); + if (lower.contains("sub")) { + return true; + } + if (lower.contains("pub")) { + return false; + } + } + + // 2. access 字段:可能是数字或字符串 + Integer access = body.getInteger("access"); + if (access != null) { + if (access == 1) { + return true; + } + if (access == 2) { + return false; + } + } + String accessText = body.getString("access"); + if (StrUtil.isNotBlank(accessText)) { + String lower = accessText.toLowerCase(Locale.ROOT); + if (lower.contains("sub")) { + return true; + } + if (lower.contains("pub")) { + return false; + } + if (StrUtil.isNumeric(accessText)) { + int value = Integer.parseInt(accessText); + if (value == 1) { + return true; + } + if (value == 2) { + return false; + } + } + } + return null; + } + + // ========== 事件处理 ========== + + /** * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 * 支持的事件类型:client.connected、client.disconnected 等 */ @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler { break; } - // EMQX Webhook 只需要 200 状态码,无需响应体 + // 3. EMQX Webhook 只需要 200 状态码,无需响应体 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); } catch (Exception e) { - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e); - // 即使处理失败,也返回 200 避免EMQX重试 + log.error("[handleEvent][事件处理失败][body={}]", body, e); + // 即使处理失败,也返回 200 避免 EMQX 重试 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); } } /** - * 处理客户端连接事件 - */ - private void handleClientConnected(JsonObject body) { - String username = body.getString("username"); - log.info("[handleClientConnected][设备上线: {}]", username); - handleDeviceStateChange(username, true); - } - - /** - * 处理客户端断开连接事件 - */ - private void handleClientDisconnected(JsonObject body) { - String username = body.getString("username"); - String reason = body.getString("reason"); - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); - handleDeviceStateChange(username, false); - } - - /** - * 解析认证接口请求体 - *

- * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) - * - * @param context 路由上下文 - * @return 请求体JSON对象,解析失败时返回null - */ - private JsonObject parseRequestBody(RoutingContext context) { - try { - JsonObject body = context.body().asJsonObject(); - if (body == null) { - log.info("[parseRequestBody][请求体为空]"); - sendAuthResponse(context, RESULT_IGNORE); - return null; - } - return body; - } catch (Exception e) { - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); - sendAuthResponse(context, RESULT_IGNORE); - return null; - } - } - - /** * 解析事件接口请求体 *

* 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范) @@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler { } /** - * 执行设备认证 - * - * @param clientId 客户端ID - * @param username 用户名 - * @param password 密码 - * @return 认证是否成功 + * 处理客户端连接事件 */ - private boolean handleDeviceAuth(String clientId, String username, String password) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); - throw e; - } + private void handleClientConnected(JsonObject body) { + String username = body.getString("username"); + log.info("[handleClientConnected][设备上线: {}]", username); + handleDeviceStateChange(username, true); + } + + /** + * 处理客户端断开连接事件 + */ + private void handleClientDisconnected(JsonObject body) { + String username = body.getString("username"); + String reason = body.getString("reason"); + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); + handleDeviceStateChange(username, false); } /** @@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler { } } + // ========= 注册处理 ========= + /** - * 发送 EMQX 认证响应 - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 + * 处理设备注册请求(一型一密) * - * @param context 路由上下文 - * @param result 认证结果:allow、deny、ignore + * @param context 路由上下文 + * @param username 用户名 + * @param password 密码(签名) */ - private void sendAuthResponse(RoutingContext context, String result) { - // 构建符合 EMQX 官方规范的响应 - JsonObject response = new JsonObject() - .put("result", result) - .put("is_superuser", false); + private void handleDeviceRegister(RoutingContext context, String username, String password) { + try { + // 1. 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username); + sendAuthResponse(context, RESULT_DENY); + return; + } - // 可以根据业务需求添加客户端属性 - // response.put("client_attrs", new JsonObject().put("role", "device")); + // 2. 调用注册 API + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) + .setSign(password); + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); - // 可以添加认证过期时间(可选) - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); + // 3. 允许连接 + log.info("[handleDeviceRegister][设备注册成功: {}]", username); + sendAuthResponse(context, RESULT_ALLOW); - context.response() - .setStatusCode(SUCCESS_STATUS_CODE) - .putHeader("Content-Type", "application/json; charset=utf-8") - .end(response.encode()); + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅) + sendRegisterResultMessage(username, result.getData()); + } catch (Exception e) { + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage()); + sendAuthResponse(context, RESULT_DENY); + } + } + + /** + * 发送注册结果消息给设备 + *

+ * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。 + * + * @param username 用户名 + * @param result 注册结果 + */ + @SuppressWarnings("DataFlowIssue") + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) { + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "设备信息不能为空"); + try { + // 1.1 构建响应消息 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null); + // 1.2 序列化消息 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON); + // 1.3 构建响应主题 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true); + + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅) + protocol.publishDelayMessage(replyTopic, encodedData, 5000); + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic); + } catch (Exception e) { + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e); + } } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java index 81d8cbb..17d5f85 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler { private final String serverId; - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { + public IotEmqxUpstreamHandler(String serverId) { this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - this.serverId = protocol.getServerId(); + this.serverId = serverId; } /** * 处理 MQTT 发布消息 */ public void handle(MqttPublishMessage mqttMessage) { - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); String topic = mqttMessage.topicName(); byte[] payload = mqttMessage.payload().getBytes(); try { // 1. 解析主题,一次性获取所有信息 String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + String productKey = ArrayUtil.get(topicParts, 2); + String deviceName = ArrayUtil.get(topicParts, 3); + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) { log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); return; } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - - // 3. 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + // 2. 反序列化消息 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); if (message == null) { log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); return; } - // 4. 发送消息到队列 + // 3. 发送消息到队列 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); } catch (Exception e) { log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); } } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java index 968a9ae..bc7f4dc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java +++ b/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; @Data public class IotHttpConfig { - /** - * 是否启用 SSL - */ - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index f3a3c0d..2f92419 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; @@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol { */ private IotHttpDownstreamSubscriber downstreamSubscriber; - public IotHttpProtocol(ProtocolInstanceProperties properties) { + public IotHttpProtocol(ProtocolProperties properties) { this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol { router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); // 1.4 启动 HTTP 服务器 - IotHttpConfig httpConfig = properties.getHttp(); HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); - if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(httpConfig.getSslKeyPath()) - .setCertPath(httpConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } try { @@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol { this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭 Vertx + // 启动失败时关闭资源 + if (httpServer != null) { + httpServer.close(); + httpServer = null; + } if (vertx != null) { vertx.close(); vertx = null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java index 08c60f3..df010f9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java +++ b/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 { Assert.notNull(request, "请求参数不能为空"); Assert.notBlank(request.getProductKey(), "productKey 不能为空"); Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(request); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java new file mode 100644 index 0000000..afbdd93 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT 网关 MQTT 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotMqttConfig { + + /** + * 最大消息大小(字节) + */ + @NotNull(message = "最大消息大小不能为空") + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节") + private Integer maxMessageSize = 8192; + + /** + * 连接超时时间(秒) + */ + @NotNull(message = "连接超时时间不能为空") + @Min(value = 1, message = "连接超时时间不能小于 1 秒") + private Integer connectTimeoutSeconds = 60; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java new file mode 100644 index 0000000..1201fd1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -0,0 +1,345 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.mqtt.MqttEndpoint; +import io.vertx.mqtt.MqttServer; +import io.vertx.mqtt.MqttServerOptions; +import io.vertx.mqtt.MqttTopicSubscription; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +/** + * IoT 网关 MQTT 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttProtocol implements IotProtocol { + + /** + * 注册连接的 clientId 标识 + * + * @see #handleEndpoint(MqttEndpoint) + */ + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * MQTT 服务器 + */ + private MqttServer mqttServer; + + /** + * 连接管理器 + */ + private final IotMqttConnectionManager connectionManager; + /** + * 下行消息订阅者 + */ + private final IotMqttDownstreamSubscriber downstreamSubscriber; + + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttAuthHandler authHandler; + private final IotMqttRegisterHandler registerHandler; + private final IotMqttUpstreamHandler upstreamHandler; + + public IotMqttProtocol(ProtocolProperties properties) { + IotMqttConfig mqttConfig = properties.getMqtt(); + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化连接管理器 + this.connectionManager = new IotMqttConnectionManager(); + + // 初始化 Handler + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MQTT; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotMqttConfig mqttConfig = properties.getMqtt(); + MqttServerOptions options = new MqttServerOptions() + .setPort(properties.getPort()) + .setMaxMessageSize(mqttConfig.getMaxMessageSize()) + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds()); + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 1.3 创建服务器并设置连接处理器 + mqttServer = MqttServer.create(vertx, options); + mqttServer.endpointHandler(this::handleEndpoint); + + // 1.4 启动 MQTT 服务器 + try { + mqttServer.listen().result(); + running = true; + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭资源 + if (mqttServer != null) { + mqttServer.close(); + mqttServer = null; + } + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 关闭 MQTT 服务器 + if (mqttServer != null) { + try { + mqttServer.close().result(); + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e); + } + mqttServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); + } + + // ======================================= MQTT 连接处理 ====================================== + + /** + * 处理 MQTT 连接端点 + * + * @param endpoint MQTT 连接端点 + */ + private void handleEndpoint(MqttEndpoint endpoint) { + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程 + String clientId = endpoint.clientIdentifier(); + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { + // 情况一:设备注册请求 + registerHandler.handleRegister(endpoint); + return; + } else { + // 情况二:普通认证请求 + if (!authHandler.handleAuthenticationRequest(endpoint)) { + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + return; + } + } + + // 2.1 设置异常和关闭处理器 + endpoint.exceptionHandler(ex -> { + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); + endpoint.close(); + }); + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等) + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文 + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); + cleanupConnection(endpoint); + }); + // 2.2 设置心跳处理器 + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); + + // 3.1 设置消息处理器 + endpoint.publishHandler(message -> processMessage(endpoint, message)); + // 3.2 设置 QoS 2 消息的 PUBREL 处理器 + endpoint.publishReleaseHandler(endpoint::publishComplete); + + // 4.1 设置订阅处理器(带 ACL 校验) + endpoint.subscribeHandler(subscribe -> { + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + List grantedQoSLevels = new ArrayList<>(); + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) { + String topicName = sub.topicName(); + // 校验主题是否属于当前设备 + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed( + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) { + grantedQoSLevels.add(sub.qualityOfService()); + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName); + } else { + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName); + grantedQoSLevels.add(MqttQoS.FAILURE); + } + } + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); + }); + // 4.2 设置取消订阅处理器 + endpoint.unsubscribeHandler(unsubscribe -> { + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }); + + // 5. 接受连接 + endpoint.accept(false); + } + + /** + * 处理消息(发布) + * + * @param endpoint MQTT 连接端点 + * @param message 发布消息 + */ + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { + String clientId = endpoint.clientIdentifier(); + try { + // 1. 处理业务消息 + String topic = message.topicName(); + byte[] payload = message.payload().getBytes(); + upstreamHandler.handleBusinessRequest(endpoint, topic, payload); + + // 2. 根据 QoS 级别发送相应的确认消息 + handleQoSAck(endpoint, message); + } catch (Exception e) { + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); + endpoint.close(); + } + } + + /** + * 处理 QoS 确认 + * + * @param endpoint MQTT 连接端点 + * @param message 发布消息 + */ + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) { + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + // QoS 1: 发送 PUBACK 确认 + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + // QoS 2: 发送 PUBREC 确认 + endpoint.publishReceived(message.messageId()); + } + // QoS 0 无需确认 + } + + /** + * 清理连接 + * + * @param endpoint MQTT 连接端点 + */ + private void cleanupConnection(MqttEndpoint endpoint) { + try { + // 1. 发送设备离线消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(endpoint); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", + endpoint.clientIdentifier(), e.getMessage()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted file mode 100644 index 46fbc7c..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java +++ /dev/null @@ -1,117 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.mqtt.MqttServer; -import io.vertx.mqtt.MqttServerOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttUpstreamProtocol implements IotProtocol { - - private static final String ID = "mqtt"; - - private volatile boolean running = false; - - private final IotGatewayProperties.MqttProperties mqttProperties; - - private final IotDeviceMessageService messageService; - - private final IotMqttConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private MqttServer mqttServer; - - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, - IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager, - Vertx vertx) { - this.mqttProperties = mqttProperties; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.MQTT; - } - - @Override - public boolean isRunning() { - return running; - } - - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; - @Override - @PostConstruct - public void start() { - // 创建服务器选项 - MqttServerOptions options = new MqttServerOptions() - .setPort(mqttProperties.getPort()) - .setMaxMessageSize(mqttProperties.getMaxMessageSize()) - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); - - // 配置 SSL(如果启用) - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { - options.setSsl(true) - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); - } - - // 创建服务器并设置连接处理器 - mqttServer = MqttServer.create(vertx, options); - mqttServer.endpointHandler(endpoint -> { - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); - handler.handle(endpoint); - }); - - // 启动服务器 - try { - mqttServer.listen().result(); - running = true; - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (mqttServer != null) { - try { - mqttServer.close().result(); - running = false; - log.info("[stop][IoT 网关 MQTT 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); - } - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java new file mode 100644 index 0000000..18a5a41 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:下行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotMqttDownstreamHandler { + + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttConnectionManager connectionManager; + + /** + * 处理下行消息 + * + * @param message 设备消息 + */ + public void handle(IotDeviceMessage message) { + try { + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + + // 1. 检查设备连接 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( + message.getDeviceId()); + if (connectionInfo == null) { + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + return; + } + + // 2.1 序列化消息 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName()); + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); + // 2.2 构建主题 + Assert.notBlank(message.getMethod(), "消息方法不能为空"); + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), isReply); + Assert.notBlank(topic, "主题不能为空"); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, + MqttQoS.AT_LEAST_ONCE.value(), false); + if (!success) { + throw new RuntimeException("下行消息发送失败"); + } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length); + } catch (Exception e) { + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java index fe9b600..c8aa299 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java @@ -1,15 +1,13 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import lombok.extern.slf4j.Slf4j; /** - * IoT 网关 MQTT 协议:下行消息订阅器 - *

- * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 + * IoT 网关 MQTT 协议:接收下行给设备的消息 * * @author 芋道源码 */ @@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber private final IotMqttDownstreamHandler downstreamHandler; - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, IotMqttDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); @@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber @Override protected void handleMessage(IotDeviceMessage message) { - boolean success = downstreamHandler.handleDownstreamMessage(message); - if (success) { - log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } else { - log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } + downstreamHandler.handle(message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java new file mode 100644 index 0000000..443dc10 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttEndpoint; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议的处理器抽象基类 + *

+ * 提供通用的连接校验、响应发送等功能 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class IotMqttAbstractHandler { + + protected final IotMqttConnectionManager connectionManager; + protected final IotDeviceMessageService deviceMessageService; + + /** + * 发送成功响应到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + @SuppressWarnings("SameParameterValue") + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); + writeResponse(endpoint, productKey, deviceName, method, responseMessage); + } + + /** + * 发送错误响应到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param method 方法名 + * @param errorCode 错误码 + * @param errorMessage 错误消息 + */ + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String requestId, String method, Integer errorCode, String errorMessage) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); + writeResponse(endpoint, productKey, deviceName, method, responseMessage); + } + + /** + * 写入响应消息到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param method 方法名 + * @param responseMessage 响应消息 + */ + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String method, IotDeviceMessage responseMessage) { + try { + // 1.1 序列化消息(根据设备配置的序列化类型) + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName); + // 1.2 构建响应主题 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); + + // 2. 发送响应消息 + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); + } catch (Exception e) { + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java new file mode 100644 index 0000000..4922876 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 网关 MQTT 认证处理器 + *

+ * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttAuthHandler extends IotMqttAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + private final IotDeviceService deviceService; + private final String serverId; + + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + IotDeviceCommonApi deviceApi, + String serverId) { + super(connectionManager, deviceMessageService); + this.deviceApi = deviceApi; + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.serverId = serverId; + } + + /** + * 处理 MQTT 连接(认证)请求 + * + * @param endpoint MQTT 连接端点 + * @return 认证是否成功 + */ + @SuppressWarnings("DataFlowIssue") + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", + clientId, username, connectionManager.getEndpointAddress(endpoint)); + + try { + // 1.1 解析认证参数 + Assert.notBlank(clientId, "clientId 不能为空"); + Assert.notBlank(username, "username 不能为空"); + Assert.notBlank(password, "password 不能为空"); + // 1.2 构建认证参数 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(endpoint, device, clientId); + // 3.2 发送设备上线消息 + sendOnlineMessage(device); + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); + return true; + } catch (Exception e) { + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]", + clientId, username, e.getMessage()); + return false; + } + } + + /** + * 注册连接 + */ + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); + connectionManager.registerConnection(endpoint, connectionInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java new file mode 100644 index 0000000..9640fa2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttRegisterHandler extends IotMqttAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService) { + super(connectionManager, deviceMessageService); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + /** + * 处理注册连接 + *

+ * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册 + * + * @param endpoint MQTT 连接端点 + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DataFlowIssue") + public void handleRegister(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); + String productKey = null; + String deviceName = null; + + try { + // 1.1 校验参数 + Assert.notBlank(clientId, "clientId 不能为空"); + Assert.notBlank(username, "username 不能为空"); + Assert.notBlank(password, "password 不能为空"); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + productKey = deviceInfo.getProductKey(); + deviceName = deviceInfo.getDeviceName(); + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]", + clientId, productKey, deviceName); + // 1.2 构建注册参数 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() + .setProductKey(productKey) + .setDeviceName(deviceName) + .setSign(password); + + // 2. 调用动态注册 API + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 接受连接,并发送成功响应 + endpoint.accept(false); + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData()); + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId); + } catch (Exception e) { + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + // 接受连接,并发送错误响应 + endpoint.accept(false); + sendErrorResponse(endpoint, productKey, deviceName, null, method, + INTERNAL_SERVER_ERROR.getCode(), e.getMessage()); + } finally { + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) + endpoint.close(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java new file mode 100644 index 0000000..00a0c4b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { + + private final String serverId; + + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + String serverId) { + super(connectionManager, deviceMessageService); + this.serverId = serverId; + } + + /** + * 处理业务消息 + * + * @param endpoint MQTT 连接端点 + * @param topic 主题 + * @param payload 消息内容 + */ + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { + String clientId = endpoint.clientIdentifier(); + IotDeviceMessage message = null; + String productKey = null; + String deviceName = null; + + try { + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; + } + // 1.2 解析主题,获取 productKey 和 deviceName + String[] topicParts = topic.split("/"); + productKey = ArrayUtil.get(topicParts, 2); + deviceName = ArrayUtil.get(topicParts, 3); + Assert.notBlank(productKey, "产品 Key 不能为空"); + Assert.notBlank(deviceName, "设备名称不能为空"); + // 1.3 校验设备信息,防止伪造设备消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + Assert.notNull(connectionInfo, "无法获取连接信息"); + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); + + // 2. 反序列化消息 + message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); + if (message == null) { + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + sendErrorResponse(endpoint, productKey, deviceName, null, null, + BAD_REQUEST.getCode(), "消息解码失败"); + return; + } + + // 3. 处理业务消息 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); + } catch (ServiceException e) { + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, + BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage(), e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 082a2ad..ccb9fa5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/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 { } catch (Exception ignored) { // 连接已关闭,忽略异常 } - return realTimeAddress; } @@ -74,24 +73,24 @@ public class IotMqttConnectionManager { * 注册设备连接(包含认证信息) * * @param endpoint MQTT 连接端点 - * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { + Long deviceId = connectionInfo.getDeviceId(); // 如果设备已有其他连接,先清理旧连接 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); if (oldEndpoint != null && oldEndpoint != endpoint) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, getEndpointAddress(oldEndpoint)); - oldEndpoint.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接(避免旧连接处理器干扰) connectionMap.remove(oldEndpoint); + oldEndpoint.close(); } // 注册新连接 connectionMap.put(endpoint, connectionInfo); deviceEndpointMap.put(deviceId, endpoint); - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); } @@ -129,26 +128,11 @@ public class IotMqttConnectionManager { if (endpoint == null) { return null; } - // 获取连接信息 return getConnectionInfo(endpoint); } /** - * 检查设备是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - return deviceEndpointMap.containsKey(deviceId); - } - - /** - * 检查设备是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); - } - - /** * 发送消息到设备 * * @param deviceId 设备 ID @@ -192,28 +176,16 @@ public class IotMqttConnectionManager { * 设备 ID */ private Long deviceId; - /** * 产品 Key */ private String productKey; - /** * 设备名称 */ private String deviceName; /** - * 客户端 ID - */ - private String clientId; - - /** - * 是否已认证 - */ - private boolean authenticated; - - /** * 连接地址 */ private String remoteAddress; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java deleted file mode 100644 index c848833..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java +++ /dev/null @@ -1,132 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.netty.handler.codec.mqtt.MqttQoS; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:下行消息处理器 - *

- * 专门处理下行消息的业务逻辑,包括: - * 1. 消息编码 - * 2. 主题构建 - * 3. 消息发送 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttDownstreamHandler { - - private final IotDeviceMessageService deviceMessageService; - - private final IotMqttConnectionManager connectionManager; - - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotMqttConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.connectionManager = connectionManager; - } - - /** - * 处理下行消息 - * - * @param message 设备消息 - * @return 是否处理成功 - */ - public boolean handleDownstreamMessage(IotDeviceMessage message) { - try { - // 1. 基础校验 - if (message == null || message.getDeviceId() == null) { - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); - return false; - } - - // 2. 检查设备是否在线 - if (connectionManager.isDeviceOffline(message.getDeviceId())) { - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 3. 获取连接信息 - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); - if (connectionInfo == null) { - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 4. 编码消息 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName()); - if (payload == null || payload.length == 0) { - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 5. 发送消息到设备 - return sendMessageToDevice(message, connectionInfo, payload); - } catch (Exception e) { - if (message != null) { - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", - message.getDeviceId(), e.getMessage(), e); - } - return false; - } - } - - /** - * 发送消息到设备 - * - * @param message 设备消息 - * @param connectionInfo 连接信息 - * @param payload 消息负载 - * @return 是否发送成功 - */ - private boolean sendMessageToDevice(IotDeviceMessage message, - IotMqttConnectionManager.ConnectionInfo connectionInfo, - byte[] payload) { - // 1. 构建主题 - String topic = buildDownstreamTopic(message, connectionInfo); - if (StrUtil.isBlank(topic)) { - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", - message.getDeviceId(), message.getMethod()); - return false; - } - - // 2. 发送消息 - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); - if (success) { - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", - message.getDeviceId(), topic, message.getMethod()); - } else { - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", - message.getDeviceId(), topic, message.getMethod()); - } - return success; - } - - /** - * 构建下行消息主题 - * - * @param message 设备消息 - * @param connectionInfo 连接信息 - * @return 主题 - */ - private String buildDownstreamTopic(IotDeviceMessage message, - IotMqttConnectionManager.ConnectionInfo connectionInfo) { - String method = message.getMethod(); - if (StrUtil.isBlank(method)) { - return null; - } - - // 使用工具类构建主题,支持回复消息处理 - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), isReply); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted file mode 100644 index d40dba4..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java +++ /dev/null @@ -1,511 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.netty.handler.codec.mqtt.MqttConnectReturnCode; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.mqtt.MqttEndpoint; -import io.vertx.mqtt.MqttTopicSubscription; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.Map; - -/** - * MQTT 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttUpstreamHandler { - - /** - * 默认编解码类型(MQTT 使用 Alink 协议) - */ - private static final String DEFAULT_CODEC_TYPE = "Alink"; - - /** - * register 请求的 topic 后缀 - */ - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotMqttConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotMqttConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionManager = connectionManager; - this.serverId = protocol.getServerId(); - } - - /** - * 处理 MQTT 连接 - * - * @param endpoint MQTT 连接端点 - */ - public void handle(MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; - - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", - clientId, username, connectionManager.getEndpointAddress(endpoint)); - - // 1. 先进行认证 - if (!authenticateDevice(clientId, username, password, endpoint)) { - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); - return; - } - - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); - - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息) - endpoint.pingHandler(v -> { - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId); - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理 - }); - - // 3. 设置异常和关闭处理器 - endpoint.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); - cleanupConnection(endpoint); - }); - endpoint.closeHandler(v -> { - cleanupConnection(endpoint); - }); - - // 4. 设置消息处理器 - endpoint.publishHandler(mqttMessage -> { - try { - // 4.1 根据 topic 判断是否为 register 请求 - String topic = mqttMessage.topicName(); - byte[] payload = mqttMessage.payload().getBytes(); - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { - // register 请求:使用默认编解码器处理(设备可能未注册) - processRegisterMessage(clientId, topic, payload, endpoint); - } else { - // 业务请求:正常处理 - processMessage(clientId, topic, payload); - } - - // 4.2 根据 QoS 级别发送相应的确认消息 - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { - // QoS 1: 发送 PUBACK 确认 - endpoint.publishAcknowledge(mqttMessage.messageId()); - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { - // QoS 2: 发送 PUBREC 确认 - endpoint.publishReceived(mqttMessage.messageId()); - } - // QoS 0 无需确认 - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); - cleanupConnection(endpoint); - endpoint.close(); - } - }); - - // 5. 设置订阅处理器 - endpoint.subscribeHandler(subscribe -> { - // 提取主题名称列表用于日志显示 - List topicNames = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::topicName) - .collect(java.util.stream.Collectors.toList()); - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); - - // 提取 QoS 列表 - List grantedQoSLevels = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::qualityOfService) - .collect(java.util.stream.Collectors.toList()); - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); - }); - - // 6. 设置取消订阅处理器 - endpoint.unsubscribeHandler(unsubscribe -> { - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); - }); - - // 7. 设置 QoS 2消息的 PUBREL 处理器 - endpoint.publishReleaseHandler(endpoint::publishComplete); - - // 8. 设置断开连接处理器 - endpoint.disconnectHandler(v -> { - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); - cleanupConnection(endpoint); - }); - - // 9. 接受连接 - endpoint.accept(false); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param topic 主题 - * @param payload 消息内容 - */ - private void processMessage(String clientId, String topic, byte[] payload) { - // 1. 基础检查 - if (payload == null || payload.length == 0) { - return; - } - - // 2. 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); - return; - } - - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - try { - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (message == null) { - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - return; - } - - // 4. 处理业务消息(认证已在连接时完成) - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - handleBusinessRequest(message, productKey, deviceName); - } catch (Exception e) { - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - } - } - - /** - * 在 MQTT 连接时进行设备认证 - * - * @param clientId 客户端 ID - * @param username 用户名 - * @param password 密码 - * @param endpoint MQTT 连接端点 - * @return 认证是否成功 - */ - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { - try { - // 1. 参数校验 - if (StrUtil.hasEmpty(clientId, username, password)) { - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - // 2. 构建认证参数 - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() - .setClientId(clientId) - .setUsername(username) - .setPassword(password); - - // 3. 调用设备认证 API - CommonResult authResult = deviceApi.authDevice(authParams); - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, authResult.getMsg()); - return false; - } - - // 4. 获取设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); - if (deviceInfo == null) { - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() - .setProductKey(deviceInfo.getProductKey()) - .setDeviceName(deviceInfo.getDeviceName()); - - CommonResult deviceResult = deviceApi.getDevice(getReqDTO); - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, deviceResult.getMsg()); - return false; - } - - // 5. 注册连接 - IotDeviceRespDTO device = deviceResult.getData(); - registerConnection(endpoint, device, clientId); - - // 6. 发送设备上线消息 - sendOnlineMessage(device); - - return true; - } catch (Exception e) { - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); - return false; - } - } - - /** - * 处理 register 消息(设备动态注册,使用默认编解码器) - * - * @param clientId 客户端 ID - * @param topic 主题 - * @param payload 消息内容 - * @param endpoint MQTT 连接端点 - */ - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { - // 1.1 基础检查 - if (ArrayUtil.isEmpty(payload)) { - return; - } - // 1.2 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); - return; - } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); - if (message == null) { - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - return; - } - } catch (Exception e) { - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - return; - } - - // 3. 处理设备动态注册请求 - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - try { - handleRegisterRequest(message, productKey, deviceName, endpoint); - } catch (Exception e) { - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) - * - * @param message 消息信息 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param endpoint MQTT 连接端点 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); - return; - } - - // 2. 调用动态注册 API - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", - params.getDeviceName(), clientId); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param endpoint MQTT 连接端点 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), - MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", - endpoint.clientIdentifier(), e); - } - } - - /** - * 发送注册错误响应 - * - * @param endpoint MQTT 连接端点 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, String errorMessage) { - try { - // 1. 构建响应消息 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); - - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), - MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", - endpoint.clientIdentifier(), e); - } - } - - /** - * 处理业务请求 - */ - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { - // 发送消息到消息总线 - message.setServerId(serverId); - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); - } - - /** - * 注册连接 - */ - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setAuthenticated(true) - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); - } - } - - /** - * 清理连接 - */ - private void cleanupConnection(MqttEndpoint endpoint) { - try { - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); - if (connectionInfo != null) { - // 发送设备离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); - } - - // 注销连接 - connectionManager.unregisterConnection(endpoint); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage()); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java index 6eb414e..9c8e827 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java @@ -1,4 +1,4 @@ /** - * 提供设备接入的各种协议的实现 + * 设备接入协议:MQTT、EMQX、HTTP、TCP 等协议的实现 */ -package cn.iocoder.yudao.module.iot.gateway.protocol; \ No newline at end of file +package cn.iocoder.yudao.module.iot.gateway.protocol; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java index 16dd3b5..a8c21c8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java +++ b/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 { private Long keepAliveTimeoutMs = 30000L; /** - * 是否启用 SSL - */ - @NotNull(message = "是否启用 SSL 不能为空") - private Boolean sslEnabled = false; - /** - * SSL 证书路径 - */ - private String sslCertPath; - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - - /** * 拆包配置 */ @Valid diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 3a31f50..e864df5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; @@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions; import io.vertx.core.net.PemKeyCertOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT TCP 协议实现 @@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol { */ private final IotTcpFrameCodec frameCodec; - public IotTcpProtocol(ProtocolInstanceProperties properties) { + public IotTcpProtocol(ProtocolProperties properties) { IotTcpConfig tcpConfig = properties.getTcp(); Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol { .setTcpNoDelay(true) .setReuseAddress(true) .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 - if (Boolean.TRUE.equals(tcpConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(tcpConfig.getSslKeyPath()) - .setCertPath(tcpConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java index 6e15e95..269d6b1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java +++ b/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; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT TCP 分隔符帧编解码器 @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { private final byte[] delimiterBytes; public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { - Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); this.delimiterBytes = parseDelimiter(config.getDelimiter()); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java index eda77c4..4bd4549 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java +++ b/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; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT TCP 定长帧编解码器 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java index 4200b6b..08b7c23 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java +++ b/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; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import java.util.concurrent.atomic.AtomicReference; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java index 986bfbe..b3ae6a0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java +++ b/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 { } // 2. 序列化 + 帧编码 - byte[] serializedData = serializer.serialize(message); - Buffer frameData = codec.encode(serializedData); + byte[] payload = serializer.serialize(message); + Buffer frameData = codec.encode(payload); // 3. 发送到设备 boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 93fadd8..5d54758 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/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; import io.vertx.core.net.NetSocket; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); - Assert.hasText(authParams.getUsername(), "username 不能为空"); - Assert.hasText(authParams.getPassword(), "password 不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index 36c5928..b7b72a3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/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 { * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { - // 检查连接数是否已达上限 + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) if (connectionMap.size() >= maxConnections) { throw new IllegalStateException("连接数已达上限: " + maxConnections); } @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { if (oldSocket != null && oldSocket != socket) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, oldSocket.remoteAddress()); - oldSocket.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接 connectionMap.remove(oldSocket); + oldSocket.close(); } // 注册新连接 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java index 647a713..bfed2d9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket; import io.vertx.core.datagram.DatagramSocketOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT UDP 协议实现 @@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol { */ private final IotMessageSerializer serializer; - public IotUdpProtocol(ProtocolInstanceProperties properties) { + public IotUdpProtocol(ProtocolProperties properties) { IotUdpConfig udpConfig = properties.getUdp(); Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); this.properties = properties; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java index dd41a52..7b248ab 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java +++ b/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; import io.vertx.core.datagram.DatagramPacket; import io.vertx.core.datagram.DatagramSocket; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import java.net.InetSocketAddress; import java.util.Map; @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); - Assert.hasText(authParams.getUsername(), "username 不能为空"); - Assert.hasText(authParams.getPassword(), "password 不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java index 8195c99..d807bce 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java +++ b/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; import lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** @@ -47,8 +48,8 @@ public class IotUdpSessionManager { * @param deviceId 设备 ID * @param sessionInfo 会话信息 */ - public void registerSession(Long deviceId, SessionInfo sessionInfo) { - // 检查是否为新设备,且会话数已达上限 + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) if (deviceSessionCache.getIfPresent(deviceId) == null && deviceSessionCache.size() >= maxSessions) { throw new IllegalStateException("会话数已达上限: " + maxSessions); @@ -113,16 +114,21 @@ public class IotUdpSessionManager { } InetSocketAddress address = sessionInfo.getAddress(); try { + // 使用 CompletableFuture 同步等待发送结果 + CompletableFuture future = new CompletableFuture<>(); socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { if (result.succeeded()) { log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", deviceId, buildAddressKey(address), data.length); - return; + future.complete(true); + } else { + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", + deviceId, buildAddressKey(address), result.cause()); + future.complete(false); } - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", - deviceId, buildAddressKey(address), result.cause()); }); - return true; + // 同步等待结果,超时 5 秒 + return future.get(5, TimeUnit.SECONDS); } catch (Exception e) { log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); return false; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java index e64e11d..95f738d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java +++ b/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 { @NotNull(message = "空闲超时时间不能为空") private Integer idleTimeoutSeconds = 60; - /** - * 是否启用 SSL(wss://) - */ - @NotNull(message = "是否启用 SSL 不能为空") - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java index 67d5608..b416900 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java +++ b/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; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; @@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions; import io.vertx.core.net.PemKeyCertOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT WebSocket 协议实现 @@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol { */ private final IotMessageSerializer serializer; - public IotWebSocketProtocol(ProtocolInstanceProperties properties) { + public IotWebSocketProtocol(ProtocolProperties properties) { Assert.notNull(properties, "协议实例配置不能为空"); Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); this.properties = properties; @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol { .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); - if (Boolean.TRUE.equals(wsConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(wsConfig.getSslKeyPath()) - .setCertPath(wsConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java index c838198..48de709 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java +++ b/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 import io.vertx.core.Handler; import io.vertx.core.http.ServerWebSocket; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler { // 1.2 解码消息 message = serializer.deserialize(payload); Assert.notNull(message, "消息反序列化失败"); - Assert.hasText(message.getMethod(), "method 不能为空"); + Assert.notBlank(message.getMethod(), "method 不能为空"); // 2. 根据消息类型路由处理 if (AUTH_METHOD.equals(message.getMethod())) { @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); - Assert.hasText(authParams.getUsername(), "username 不能为空"); - Assert.hasText(authParams.getPassword(), "password 不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java new file mode 100644 index 0000000..cfdda5a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息序列化:将设备消息转换为字节数组(JSON、二进制等格式) + * + * @see cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer + */ +package cn.iocoder.yudao.module.iot.gateway.serialize; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java index c86fc09..7d16a65 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** @@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; public interface IotDeviceMessageService { /** - * 编码消息 + * 序列化消息 * * @param message 消息 * @param productKey 产品 Key * @param deviceName 设备名称 - * @return 编码后的消息内容 + * @return 序列化后的消息内容 */ - byte[] encodeDeviceMessage(IotDeviceMessage message, - String productKey, String deviceName); + byte[] serializeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName); /** - * 编码消息 + * 序列化消息 * - * @param message 消息 - * @param codecType 编解码器类型 - * @return 编码后的消息内容 + * @param message 消息 + * @param serializeType 序列化类型 + * @return 序列化后的消息内容 */ - byte[] encodeDeviceMessage(IotDeviceMessage message, - String codecType); + byte[] serializeDeviceMessage(IotDeviceMessage message, + IotSerializeTypeEnum serializeType); /** - * 解码消息 + * 反序列化消息 * * @param bytes 消息内容 * @param productKey 产品 Key * @param deviceName 设备名称 - * @return 解码后的消息内容 + * @return 反序列化后的消息内容 */ - IotDeviceMessage decodeDeviceMessage(byte[] bytes, - String productKey, String deviceName); + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, + String productKey, String deviceName); /** - * 解码消息 + * 反序列化消息 * - * @param bytes 消息内容 - * @param codecType 编解码器类型 - * @return 解码后的消息内容 + * @param bytes 消息内容 + * @param serializeType 序列化类型 + * @return 反序列化后的消息内容 */ - IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType); /** * 发送消息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java index 014da9a..ee0c4ae 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -1,20 +1,20 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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 @Slf4j public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { - /** - * 编解码器 - */ - private final Map codes; - @Resource private IotDeviceService deviceService; @Resource private IotDeviceMessageProducer deviceMessageProducer; - public IotDeviceMessageServiceImpl(List codes) { - this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type); - } + @Resource + private IotMessageSerializerManager messageSerializerManager; @Override - public byte[] encodeDeviceMessage(IotDeviceMessage message, - String productKey, String deviceName) { + public byte[] serializeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName) { // 1.1 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); if (device == null) { throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); } - // 1.2 获取编解码器 - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); - } + // 1.2 获取序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); + Assert.notNull(serializeType, "设备序列化类型不能为空"); - // 2. 编码消息 - return codec.encode(message); + // 2. 序列化消息 + return serializeDeviceMessage(message, serializeType); } @Override - public byte[] encodeDeviceMessage(IotDeviceMessage message, - String codecType) { - // 1. 获取编解码器 - IotDeviceMessageCodec codec = codes.get(codecType); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + public byte[] serializeDeviceMessage(IotDeviceMessage message, + IotSerializeTypeEnum serializeType) { + // 1. 获取序列化器 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); + if (serializer == null) { + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); } - // 2. 编码消息 - return codec.encode(message); + // 2. 序列化消息 + return serializer.serialize(message); } @Override - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, - String productKey, String deviceName) { + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, + String productKey, String deviceName) { // 1.1 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); if (device == null) { throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); } - // 1.2 获取编解码器 - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); - } + // 1.2 获取序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); + Assert.notNull(serializeType, "设备序列化类型不能为空"); - // 2. 解码消息 - return codec.decode(bytes); + // 2. 反序列化消息 + return deserializeDeviceMessage(bytes, serializeType); } @Override - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { - // 1. 获取编解码器 - IotDeviceMessageCodec codec = codes.get(codecType); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType) { + // 1. 获取序列化器 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); + if (serializer == null) { + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); } - // 2. 解码消息 - return codec.decode(bytes); + // 2. 反序列化消息 + return serializer.deserialize(bytes); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java index 7f72937..249b315 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java +++ b/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 { */ public static final String MQTT_EVENT_PATH = "/mqtt/event"; + /** + * MQTT ACL 接口路径 + * 对应 EMQX HTTP ACL 插件的 ACL 请求接口 + */ + public static final String MQTT_ACL_PATH = "/mqtt/acl"; + // ========== 工具方法 ========== /** @@ -63,4 +69,50 @@ public final class IotMqttTopicUtils { return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; } -} \ No newline at end of file + /** + * 校验主题是否允许订阅 + *

+ * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头, + * 或者是通配符形式 /sys/{productKey}/{deviceName}/# + * + * @param topic 订阅的主题 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 是否允许订阅 + */ + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) { + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { + return false; + } + // 构建设备主题前缀 + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式 + return topic.startsWith(deviceTopicPrefix) + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); + } + + /** + * 校验主题是否允许发布 + *

+ * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,且不允许包含通配符(+/#)。 + * + * @param topic 发布的主题 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 是否允许发布 + */ + // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用; + // TODO @AI:那你改下 mqtt,也调用!!! + public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { + return false; + } + // MQTT publish topic 不允许包含通配符,但这里做一次兜底校验 + if (topic.contains("#") || topic.contains("+")) { + return false; + } + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; + return topic.startsWith(deviceTopicPrefix); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 705c34f..b4f4226 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -48,7 +48,7 @@ yudao: # 针对引入的 HTTP 组件的配置 # ==================================== - id: http-json - type: http + protocol: http port: 8092 enabled: false http: @@ -57,9 +57,9 @@ yudao: # 针对引入的 TCP 组件的配置 # ==================================== - id: tcp-json - type: tcp - port: 8091 enabled: false + protocol: tcp + port: 8091 serialize: json tcp: max-connections: 1000 @@ -79,9 +79,9 @@ yudao: # 针对引入的 UDP 组件的配置 # ==================================== - id: udp-json - type: udp - port: 8093 enabled: false + protocol: udp + port: 8093 serialize: json udp: max-sessions: 1000 # 最大会话数 @@ -92,9 +92,9 @@ yudao: # 针对引入的 WebSocket 组件的配置 # ==================================== - id: websocket-json - type: websocket + enabled: false + protocol: websocket port: 8094 - enabled: true serialize: json websocket: path: /ws @@ -106,21 +106,68 @@ yudao: # 针对引入的 CoAP 组件的配置 # ==================================== - id: coap-json - type: coap + enabled: false + protocol: coap port: 5683 - enabled: true coap: max-message-size: 1024 # 最大消息大小(字节) ack-timeout-ms: 2000 # ACK 超时时间(毫秒) max-retransmit: 4 # 最大重传次数 - - # 协议配置(旧版,保持兼容) - protocol: + # ==================================== + # 针对引入的 MQTT 组件的配置 + # ==================================== + - id: mqtt-json + enabled: true + protocol: mqtt + port: 1883 + serialize: json + mqtt: + max-message-size: 8192 # 最大消息大小(字节) + connect-timeout-seconds: 60 # 连接超时时间(秒) + ssl-enabled: false # 是否启用 SSL # ==================================== # 针对引入的 EMQX 组件的配置 # ==================================== - emqx: + - id: emqx-1 enabled: false + protocol: emqx + port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) + emqx: + mqtt-host: 127.0.0.1 # MQTT Broker 地址 + mqtt-port: 1883 # MQTT Broker 端口 + mqtt-username: admin # MQTT 用户名 + mqtt-password: public # MQTT 密码 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID + mqtt-ssl: false # 是否开启 SSL + mqtt-topics: + - "/sys/#" # 系统主题 + mqtt-qos: 1 # 默认 QoS + clean-session: true # 是否启用 Clean Session (默认: true) + keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) + max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 + connect-timeout-seconds: 10 # 连接超时,单位:秒 + reconnect-delay-ms: 5000 # 重连延迟,单位:毫秒 + # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! + # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true + trust-all: true # 在 dev 环境可以设为 true + # EMQX HTTP Hook 回调网关的 HTTPS 配置(可选) + http: + ssl-enabled: false + # ssl-cert-path: "path/to/server.crt" + # ssl-key-path: "path/to/server.key" + # 遗嘱消息配置 (用于网关异常下线时通知其他系统) + will: + enabled: true # 生产环境强烈建议开启 + topic: "gateway/status/iot-gateway-mqtt" # 遗嘱消息主题 + payload: "offline" # 遗嘱消息负载 + qos: 1 # 遗嘱消息 QoS + retain: true # 遗嘱消息是否保留 + # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) + ssl-options: + key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 + key-store-password: "your-keystore-password" # 客户端证书库密码 + trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 + trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 http-port: 8090 # MQTT HTTP 服务端口 mqtt-host: 127.0.0.1 # MQTT Broker 地址 mqtt-port: 1883 # MQTT Broker 端口 @@ -153,25 +200,8 @@ yudao: # ==================================== # 针对引入的 MQTT 组件的配置 # ==================================== - mqtt: - enabled: false - port: 1883 - max-message-size: 8192 - connect-timeout-seconds: 60 - ssl-enabled: false - # ==================================== - # 针对引入的 MQTT WebSocket 组件的配置 - # ==================================== - mqtt-ws: - enabled: true # 是否启用 MQTT WebSocket - port: 8083 # WebSocket 服务端口 - path: /mqtt # WebSocket 路径 - max-message-size: 8192 # 最大消息大小(字节) - max-frame-size: 65536 # 最大帧大小(字节) - connect-timeout-seconds: 60 # 连接超时时间(秒) - keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) - ssl-enabled: false # 是否启用 SSL(wss://) - sub-protocol: mqtt # WebSocket 子协议 + # 协议配置(旧版,保持兼容) + protocol: modbus-tcp: enabled: true config-refresh-interval: 30 # 配置刷新间隔(秒) @@ -194,7 +224,6 @@ logging: cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG # 根日志级别 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java index 6c852af..8fc4990 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { // 1.1 构建请求 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URI: {}]", uri); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java new file mode 100644 index 0000000..d7d4535 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java @@ -0,0 +1,18 @@ +/** + * IoT 网关 EMQX 协议集成测试包 + * + *

+ * 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。 + * + * @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt + * + *

架构

+ *
+ * +--------+      MQTT       +-------------+     HTTP Hook     +---------+
+ * |  设备  | --------------> | EMQX Broker | ----------------> |  网关   |
+ * +--------+                 +-------------+                   +---------+
+ * 
+ * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java index ea412a2..1759000 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { // 1.1 构建请求 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 + String deviceName = "test-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() .setProductKey(PRODUCT_KEY) - .setDeviceName("test-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); + .setDeviceName(deviceName) + .setSign(sign); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URL: {}]", url); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 67a8ced..45cb7ca 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java @@ -1,16 +1,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testPropertyPost][连接认证成功]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("width", 1) + .put("height", "2") + .build())); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); + + // 2.2 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 直连设备事件上报测试 ===================== @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testEventPost][连接认证成功]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "eat", + MapUtil.builder().put("rice", 3).build(), + System.currentTimeMillis())); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 设备动态注册测试(一型一密) ===================== @@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { /** * 直连设备动态注册测试(一型一密) *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) + * 认证方式: + * - clientId: 任意值 + "|authType=register|" 后缀 + * - username: {deviceName}&{productKey}(与普通认证相同) + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256) *

- * 注意:此接口不需要认证 + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证 */ @Test public void testDeviceRegister() throws Exception { - // 1. 连接并认证(使用已有设备连接) - MqttClient client = connectAndAuth(); - log.info("[testDeviceRegister][连接认证成功]"); - - // 2.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 2.2 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - subscribeReply(client, replyTopic); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/register", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - - // 4. 断开连接 - disconnect(client); + // 1.1 构建注册参数 + String deviceName = "test-mqtt-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + // 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀) + String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|"; + String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName); + log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]", + clientId, username, sign); + // 1.3 创建客户端并连接(连接时服务端自动处理注册) + MqttClientOptions options = new MqttClientOptions() + .setClientId(clientId) + .setUsername(username) + .setPassword(sign) + .setCleanSession(true) + .setKeepAliveInterval(60); + MqttClient client = MqttClient.create(vertx, options); + + try { + // 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应) + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][连接成功,等待注册响应...]"); + + // 3.1 设置消息处理器,接收注册响应 + CompletableFuture responseFuture = new CompletableFuture<>(); + client.publishHandler(message -> { + log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", + message.topicName(), message.payload().toString()); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); + }); + // 3.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName); + subscribe(client, replyTopic); + + // 4. 等待注册响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][注册响应: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } finally { + disconnect(client); + } } // ===================== 订阅下行消息测试 ===================== @@ -237,44 +234,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { */ @Test public void testSubscribe() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 连接并认证 MqttClient client = connectAndAuth(); log.info("[testSubscribe][连接认证成功]"); - // 2. 设置消息处理器 - client.publishHandler(message -> { - log.info("[testSubscribe][收到消息: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - }); - - // 3. 订阅下行主题 - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); - log.info("[testSubscribe][订阅主题: {}]", topic); - - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(subscribeAr -> { - if (subscribeAr.succeeded()) { - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); - // 保持连接 30 秒等待消息 - vertx.setTimer(30000, id -> { - client.disconnect() - .onComplete(disconnectAr -> { - log.info("[testSubscribe][断开连接]"); - latch.countDown(); - }); - }); - } else { - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); - latch.countDown(); - } - }); - - // 4. 等待测试完成 - boolean completed = latch.await(60, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testSubscribe][测试超时]"); + try { + // 2. 设置消息处理器 + client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", + message.topicName(), message.payload().toString())); + + // 3. 订阅下行主题 + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); + log.info("[testSubscribe][订阅主题: {}]", topic); + subscribe(client, topic); + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); + + // 4. 保持连接 30 秒等待消息 + Thread.sleep(30000); + } finally { + disconnect(client); } } @@ -286,7 +264,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -350,34 +307,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); - - // 3. 等待响应(超时返回 null) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); + + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java index 5172067..8e09974 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java +++ b/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; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -13,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoAdd][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - - // 2.3 构建请求消息 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/add", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoAdd][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + try { + // 2.1 构建子设备认证信息 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() + .setClientId(subAuthInfo.getClientId()) + .setUsername(subAuthInfo.getUsername()) + .setPassword(subAuthInfo.getPassword()); + + // 2.2 构建请求消息 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO() + .setSubDevices(Collections.singletonList(subDeviceAuth)); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), + params); + + // 2.3 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/add", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoAdd][响应消息: {}]", response); + } finally { + disconnect(client); + } } /** @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoDelete][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建请求消息 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/delete", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoDelete][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + try { + // 2.1 构建请求消息 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO() + .setSubDevices(Collections.singletonList( + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), + params); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/delete", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoDelete][响应消息: {}]", response); + } finally { + disconnect(client); + } } /** @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoGet][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建请求消息 - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/get", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoGet][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + try { + // 2.1 构建请求消息 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), + params); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/get", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoGet][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 子设备注册测试 ===================== @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testSubDeviceRegister][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建请求消息 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei-mqtt"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + try { + // 2.1 构建请求消息 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() + .setProductKey(SUB_DEVICE_PRODUCT_KEY) + .setDeviceName("mougezishebei-mqtt"); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), + Collections.singletonList(subDevice)); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 批量上报测试 ===================== @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testPropertyPackPost][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - - // 2.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil - .builder() - .put("statusReport", gatewayEvent) - .build(); - - // 2.4 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - - // 2.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil - .builder() - .put("healthCheck", subDeviceEvent) - .build(); - - // 2.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - - // 2.7 构建请求消息 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPackPost][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + try { + // 2.1 构建【网关设备】自身属性 + Map gatewayProperties = MapUtil.builder() + .put("temperature", 25.5) + .build(); + + // 2.2 构建【网关设备】自身事件 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); + Map gatewayEvents = MapUtil + .builder() + .put("statusReport", gatewayEvent) + .build(); + + // 2.3 构建【网关子设备】属性 + Map subDeviceProperties = MapUtil.builder() + .put("power", 100) + .build(); + + // 2.4 构建【网关子设备】事件 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); + Map subDeviceEvents = MapUtil + .builder() + .put("healthCheck", subDeviceEvent) + .build(); + + // 2.5 构建子设备数据 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); + + // 2.6 构建请求消息 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO() + .setProperties(gatewayProperties) + .setEvents(gatewayEvents) + .setSubDevices(ListUtil.of(subDeviceData)); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), + params); + + // 2.7 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPackPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 辅助方法 ===================== @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); - - // 3. 等待响应(超时返回 null) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); + + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java index c14d2c6..ca01b85 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java @@ -1,15 +1,14 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { log.info("[testPropertyPost][连接认证成功]"); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("power", 100) + .put("status", "online") + .put("temperature", 36.5) + .build())); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 子设备事件上报测试 ===================== @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { log.info("[testEventPost][连接认证成功]"); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "alarm", + MapUtil.builder() + .put("level", "warning") + .put("message", "temperature too high") + .put("threshold", 40) + .put("current", 42) + .build(), + System.currentTimeMillis())); + + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 辅助方法 ===================== @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); - - // 3. 等待响应(超时返回 null) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); + + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index 192dce3..778c72f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1. 构建注册消息 + String deviceName = "test-tcp-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() .setProductKey(PRODUCT_KEY) - .setDeviceName("test-tcp-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java index 74169b2..ef7f2ff 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1. 构建注册消息 + String deviceName = "test-udp-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() .setProductKey(PRODUCT_KEY) - .setDeviceName("test-udp-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java index 15eed61..ba80ed1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-ws-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); // 1.2 序列化