diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-biz/pom.xml index a0fe16d..02644cb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-biz/pom.xml @@ -101,18 +101,6 @@ spring-boot-starter-amqp true - - - - - - - - - - - - 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 db0a862..ee4680e 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 @@ -58,7 +58,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/enums/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java new file mode 100644 index 0000000..5fbd713 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 协议类型枚举 + * + * 用于定义传输层协议类型 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotProtocolTypeEnum implements ArrayValuable { + + TCP("tcp"), + UDP("udp"), + WEBSOCKET("websocket"), + HTTP("http"), + MQTT("mqtt"), + EMQX("emqx"), + COAP("coap"), + MODBUS_TCP("modbus_tcp"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); + + /** + * 类型 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotProtocolTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java new file mode 100644 index 0000000..0f9400f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 序列化类型枚举 + * + * 用于定义设备消息的序列化格式 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotSerializeTypeEnum implements ArrayValuable { + + JSON("json"), + BINARY("binary"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new); + + /** + * 类型 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotSerializeTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java index c621467..646eb36 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java @@ -24,4 +24,14 @@ public interface IotMessageBus { */ void register(IotMessageSubscriber subscriber); + /** + * 取消注册消息订阅者 + * + * @param subscriber 订阅者 + */ + default void unregister(IotMessageSubscriber subscriber) { + // TODO 芋艿:暂时不实现,需求量不大,但是 + // throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现"); + } + } \ 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/messagebus/core/IotMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java index 23a0553..fb5c712 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java @@ -26,4 +26,16 @@ public interface IotMessageSubscriber { */ void onMessage(T message); + /** + * 启动订阅 + */ + default void start() { + } + + /** + * 停止订阅 + */ + default void stop() { + } + } \ 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/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java index feed3eb..813b360 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -60,7 +60,7 @@ public class IotDeviceMessage { */ private String serverId; - // ========== codec(编解码)字段 ========== + // ========== serialize(序列化)相关字段 ========== /** * 请求编号 @@ -94,7 +94,7 @@ public class IotDeviceMessage { */ private String msg; - // ========== 基础方法:只传递"codec(编解码)字段" ========== + // ========== 基础方法:只传递"serialize(序列化)相关字段" ========== public static IotDeviceMessage requestOf(String method) { return requestOf(null, method, null); 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/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/simple/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java deleted file mode 100644 index 5bd676a..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO @芋艿:实现一个 alink 的 xml 版本 - */ -package cn.iocoder.yudao.module.iot.gateway.codec.simple; \ 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 7d62ce2..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 = "TCP_JSON"; - - @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 a4e93a8..3c62c0d 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 @@ -1,254 +1,23 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; -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.tcp.IotTcpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; 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 { - /** - * IoT 网关 HTTP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true") - @Slf4j - public static class HttpProtocolConfiguration { - - @Bean(name = "httpVertx", destroyMethod = "close") - public Vertx httpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("httpVertx") Vertx httpVertx) { - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx); - } - - @Bean - public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, - IotMessageBus messageBus) { - return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); - } - } - - /** - * 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); - } + @Bean + public IotMessageSerializerManager iotMessageSerializerManager() { + return new IotMessageSerializerManager(); } - /** - * IoT 网关 TCP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true") - @Slf4j - public static class TcpProtocolConfiguration { - - @Bean(name = "tcpVertx", destroyMethod = "close") - public Vertx tcpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - @Qualifier("tcpVertx") Vertx tcpVertx) { - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), - deviceService, messageService, connectionManager, tcpVertx); - } - - @Bean - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, 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 网关 UDP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true") - @Slf4j - public static class UdpProtocolConfiguration { - - @Bean(name = "udpVertx", destroyMethod = "close") - public Vertx udpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - @Qualifier("udpVertx") Vertx udpVertx) { - return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(), - deviceService, messageService, sessionManager, udpVertx); - } - - @Bean - public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - IotMessageBus messageBus) { - return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus); - } - - } - - /** - * IoT 网关 CoAP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true") - @Slf4j - public static class CoapProtocolConfiguration { - - @Bean - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) { - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap()); - } - - @Bean - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol, - IotMessageBus messageBus) { - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus); - } - - } - - /** - * IoT 网关 WebSocket 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true") - @Slf4j - public static class WebSocketProtocolConfiguration { - - @Bean(name = "websocketVertx", destroyMethod = "close") - public Vertx websocketVertx() { - return Vertx.vertx(); - } - - @Bean - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - @Qualifier("websocketVertx") Vertx websocketVertx) { - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(), - deviceService, messageService, connectionManager, websocketVertx); - } - - @Bean - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - IotMessageBus messageBus) { - return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); - } - + @Bean + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) { + return new IotProtocolManager(gatewayProperties); } } 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 9a86ee6..6019b0f 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 @@ -1,5 +1,14 @@ 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 jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -24,9 +33,9 @@ public class IotGatewayProperties { private TokenProperties token; /** - * 协议配置 + * 协议实例列表 */ - private ProtocolProperties protocol; + private List protocols; @Data public static class RpcProperties { @@ -65,581 +74,145 @@ public class IotGatewayProperties { } + /** + * 协议实例配置 + */ @Data public static class ProtocolProperties { /** - * HTTP 组件配置 - */ - private HttpProperties http; - - /** - * EMQX 组件配置 - */ - private EmqxProperties emqx; - - /** - * TCP 组件配置 + * 协议实例 ID,如 "http-alink"、"tcp-binary" */ - private TcpProperties tcp; - - /** - * MQTT 组件配置 - */ - private MqttProperties mqtt; - + @NotEmpty(message = "协议实例 ID 不能为空") + private String id; /** - * MQTT WebSocket 组件配置 + * 是否启用 */ - private MqttWsProperties mqttWs; - + @NotNull(message = "是否启用不能为空") + private Boolean enabled = true; /** - * UDP 组件配置 + * 协议类型 + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum */ - private UdpProperties udp; - - /** - * CoAP 组件配置 - */ - private CoapProperties coap; - - /** - * WebSocket 组件配置 - */ - private WebSocketProperties websocket; - - } - - @Data - public static class HttpProperties { - - /** - * 是否开启 - */ - @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 - 刚好一次 - */ - private Integer mqttQos = 1; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 10; - - /** - * 重连延迟时间(毫秒) - */ - private Long reconnectDelayMs = 5000L; - - /** - * 是否启用 Clean Session (清理会话) - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 - */ - private Boolean cleanSession = true; - - /** - * 心跳间隔(秒) - * 用于保持连接活性,及时发现网络中断。 - */ - private Integer keepAliveIntervalSeconds = 60; - - /** - * 最大未确认消息队列大小 - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 - */ - private Integer maxInflightQueue = 10000; - - /** - * 是否信任所有 SSL 证书 - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! - * 在生产环境中,应设置为 false,并配置正确的信任库。 - */ - private Boolean trustAll = false; - - /** - * 遗嘱消息配置 (用于网关异常下线时通知其他系统) - */ - private final Will will = new Will(); - - /** - * 高级 SSL/TLS 配置 (用于生产环境) + * 不同协议含义不同: + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口 + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event) */ - private final Ssl sslOptions = new Ssl(); - + @NotNull(message = "服务端口不能为空") + private Integer port; /** - * 遗嘱消息 (Last Will and Testament) + * 序列化类型(可选) + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum + * + * 为什么是可选的呢? + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 */ - @Data - public static class Will { - - /** - * 是否启用遗嘱消息 - */ - private boolean enabled = false; - /** - * 遗嘱消息主题 - */ - private String topic; - /** - * 遗嘱消息内容 - */ - private String payload; - /** - * 遗嘱消息 QoS 等级 - */ - private Integer qos = 1; - /** - * 遗嘱消息是否作为保留消息发布 - */ - private boolean retain = true; + private String serialize; - } + // ========== SSL 配置 ========== /** - * 高级 SSL/TLS 配置 + * SSL 配置(可选,配置文件中不配置则为 null) */ - @Data - public static class Ssl { + @Valid + private SslConfig ssl; - /** - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 - */ - private String keyStorePath; - /** - * 密钥库密码 - */ - private String keyStorePassword; - /** - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 - */ - private String trustStorePath; - /** - * 信任库密码 - */ - private String trustStorePassword; - - } - - } - - @Data - public static class TcpProperties { + // ========== 各协议配置 ========== /** - * 是否开启 + * HTTP 协议配置 */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - + @Valid + private IotHttpConfig http; /** - * 服务器端口 + * WebSocket 协议配置 */ - private Integer port = 8091; + @Valid + private IotWebSocketConfig websocket; /** - * 心跳超时时间(毫秒) + * TCP 协议配置 */ - private Long keepAliveTimeoutMs = 30000L; - + @Valid + private IotTcpConfig tcp; /** - * 最大连接数 + * UDP 协议配置 */ - private Integer maxConnections = 1000; + @Valid + private IotUdpConfig udp; /** - * 是否启用SSL + * CoAP 协议配置 */ - private Boolean sslEnabled = false; + @Valid + private IotCoapConfig coap; /** - * SSL证书路径 + * MQTT 协议配置 */ - private String sslCertPath; - + @Valid + private IotMqttConfig mqtt; /** - * SSL私钥路径 + * EMQX 协议配置 */ - private String sslKeyPath; + @Valid + private IotEmqxConfig emqx; } + /** + * SSL 配置 + */ @Data - public static class MqttProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口 - */ - private Integer port = 1883; - - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; + public static class SslConfig { /** * 是否启用 SSL */ - private Boolean sslEnabled = false; - /** - * SSL 配置 - */ - private SslOptions sslOptions = new SslOptions(); - - /** - * SSL 配置选项 - */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private io.vertx.core.net.KeyCertOptions keyCertOptions; - /** - * 信任选项 - */ - private io.vertx.core.net.TrustOptions trustOptions; - /** - * SSL 证书路径 - */ - private String certPath; - /** - * SSL 私钥路径 - */ - private String keyPath; - /** - * 信任存储路径 - */ - private String trustStorePath; - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } - - } - - @Data - public static class MqttWsProperties { + @NotNull(message = "是否启用 SSL 不能为空") + private Boolean ssl = false; /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * WebSocket 服务器端口(默认:8083) - */ - private Integer port = 8083; - - /** - * WebSocket 路径(默认:/mqtt) - */ - @NotEmpty(message = "WebSocket 路径不能为空") - private String path = "/mqtt"; - - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; - - /** - * 是否启用 SSL(wss://) - */ - private Boolean sslEnabled = false; - - /** - * SSL 配置 - */ - private SslOptions sslOptions = new SslOptions(); - - /** - * WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1") - */ - @NotEmpty(message = "WebSocket 子协议不能为空") - private String subProtocol = "mqtt"; - - /** - * 最大帧大小(字节) - */ - private Integer maxFrameSize = 65536; - - /** - * SSL 配置选项 - */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private io.vertx.core.net.KeyCertOptions keyCertOptions; - - /** - * 信任选项 - */ - private io.vertx.core.net.TrustOptions trustOptions; - - /** - * SSL 证书路径 - */ - private String certPath; - - /** - * SSL 私钥路径 - */ - private String keyPath; - - /** - * 信任存储路径 - */ - private String trustStorePath; - - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } - - } - - @Data - public static class UdpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务端口(默认 8093) - */ - private Integer port = 8093; - - /** - * 接收缓冲区大小(默认 64KB) + * SSL 证书路径 */ - private Integer receiveBufferSize = 65536; + @NotEmpty(message = "SSL 证书路径不能为空") + private String sslCertPath; /** - * 发送缓冲区大小(默认 64KB) + * SSL 私钥路径 */ - private Integer sendBufferSize = 65536; + @NotEmpty(message = "SSL 私钥路径不能为空") + private String sslKeyPath; /** - * 会话超时时间(毫秒,默认 60 秒) + * 密钥库(KeyStore)路径 *

- * 用于清理不活跃的设备地址映射 - */ - private Long sessionTimeoutMs = 60000L; - - /** - * 会话清理间隔(毫秒,默认 30 秒) - */ - private Long sessionCleanIntervalMs = 30000L; - - } - - @Data - public static class CoapProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务端口(CoAP 默认端口 5683) - */ - @NotNull(message = "服务端口不能为空") - private Integer port = 5683; - - /** - * 最大消息大小(字节) - */ - @NotNull(message = "最大消息大小不能为空") - private Integer maxMessageSize = 1024; - - /** - * ACK 超时时间(毫秒) - */ - @NotNull(message = "ACK 超时时间不能为空") - private Integer ackTimeout = 2000; - - /** - * 最大重传次数 - */ - @NotNull(message = "最大重传次数不能为空") - private Integer maxRetransmit = 4; - - } - - @Data - public static class WebSocketProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口(默认:8094) - */ - private Integer port = 8094; - - /** - * WebSocket 路径(默认:/ws) - */ - @NotEmpty(message = "WebSocket 路径不能为空") - private String path = "/ws"; - - /** - * 最大消息大小(字节,默认 64KB) - */ - private Integer maxMessageSize = 65536; - - /** - * 最大帧大小(字节,默认 64KB) + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证) */ - private Integer maxFrameSize = 65536; - + private String keyStorePath; /** - * 空闲超时时间(秒,默认 60) + * 密钥库密码 */ - private Integer idleTimeoutSeconds = 60; + private String keyStorePassword; /** - * 是否启用 SSL(wss://) - */ - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 + * 信任库(TrustStore)路径 + *

+ * 包含服务端信任的 CA 证书,用于验证服务端的身份 */ - private String sslCertPath; - + private String trustStorePath; /** - * SSL 私钥路径 + * 信任库密码 */ - private String sslKeyPath; + private String trustStorePassword; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java new file mode 100644 index 0000000..bdfc28b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol; + +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; + +/** + * IoT 协议接口 + * + * 定义传输层协议的生命周期管理 + * + * @author 芋道源码 + */ +public interface IotProtocol { + + /** + * 获取协议实例 ID + * + * @return 协议实例 ID,如 "http-alink"、"tcp-binary" + */ + String getId(); + + /** + * 获取服务器 ID(用于消息追踪,全局唯一) + * + * @return 服务器 ID + */ + String getServerId(); + + /** + * 获取协议类型 + * + * @return 协议类型枚举 + */ + IotProtocolTypeEnum getType(); + + /** + * 启动协议服务 + */ + void start(); + + /** + * 停止协议服务 + */ + void stop(); + + /** + * 检查协议服务是否正在运行 + * + * @return 是否正在运行 + */ + boolean isRunning(); + +} 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/IotProtocolDownstreamSubscriber.java index 61bf123..2e2150f 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/IotProtocolDownstreamSubscriber.java @@ -1,68 +1,79 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; +package cn.iocoder.yudao.module.iot.gateway.protocol; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; 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.router.IotEmqxDownstreamHandler; -import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; /** - * IoT 网关 EMQX 订阅者:接收下行给设备的消息 + * IoT 协议下行消息订阅者抽象类 + * + * 负责接收来自消息总线的下行消息,并委托给子类进行业务处理 * * @author 芋道源码 */ +@AllArgsConstructor @Slf4j -public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber { +public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber { - private final IotEmqxDownstreamHandler downstreamHandler; + private final IotProtocol protocol; private final IotMessageBus messageBus; - private final IotEmqxUpstreamProtocol protocol; - - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { - this.protocol = protocol; - this.messageBus = messageBus; - this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); - } - - @PostConstruct - public void init() { - messageBus.register(this); - } - @Override public String getTopic() { return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); } + /** + * 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + */ @Override public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group return getTopic(); } @Override + public void start() { + messageBus.register(this); + log.info("[start][{} 下行消息订阅成功,Topic:{}]", protocol.getType().name(), getTopic()); + } + + @Override + public void stop() { + messageBus.unregister(this); + log.info("[stop][{} 下行消息订阅已停止,Topic:{}]", protocol.getType().name(), getTopic()); + } + + @Override public void onMessage(IotDeviceMessage message) { log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", message.getId(), message.getMethod(), message.getDeviceId()); try { // 1. 校验 String method = message.getMethod(); - if (method == null) { + if (StrUtil.isBlank(method)) { log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", message.getId(), message.getDeviceId()); return; } // 2. 处理下行消息 - downstreamHandler.handle(message); + handleMessage(message); } catch (Exception e) { log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", message.getId(), message.getMethod(), message.getDeviceId(), e); } } -} \ No newline at end of file + /** + * 处理下行消息 + * + * @param message 下行消息 + */ + protected abstract void handleMessage(IotDeviceMessage message); + +} 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 new file mode 100644 index 0000000..3cd00c7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol; + +import cn.hutool.core.collection.CollUtil; +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; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.SmartLifecycle; + +import java.util.ArrayList; +import java.util.List; + +/** + * IoT 协议管理器:负责根据配置创建和管理协议实例 + * + * @author 芋道源码 + */ +@Slf4j +public class IotProtocolManager implements SmartLifecycle { + + private final IotGatewayProperties gatewayProperties; + + /** + * 协议实例列表 + */ + private final List protocols = new ArrayList<>(); + + @Getter + private volatile boolean running = false; + + public IotProtocolManager(IotGatewayProperties gatewayProperties) { + this.gatewayProperties = gatewayProperties; + } + + @Override + public void start() { + if (running) { + return; + } + List protocolConfigs = gatewayProperties.getProtocols(); + if (CollUtil.isEmpty(protocolConfigs)) { + log.info("[start][没有配置协议实例,跳过启动]"); + return; + } + + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { + if (BooleanUtil.isFalse(config.getEnabled())) { + log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); + continue; + } + IotProtocol protocol = createProtocol(config); + if (protocol == null) { + continue; + } + protocol.start(); + protocols.add(protocol); + } + running = true; + log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size()); + } + + @Override + public void stop() { + if (!running) { + return; + } + for (IotProtocol protocol : protocols) { + try { + protocol.stop(); + } catch (Exception e) { + log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e); + } + } + protocols.clear(); + running = false; + log.info("[stop][协议管理器已停止]"); + } + + /** + * 创建协议实例 + * + * @param config 协议实例配置 + * @return 协议实例 + */ + @SuppressWarnings({"EnhancedSwitchMigration"}) + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) { + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); + if (protocolType == null) { + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); + return null; + } + switch (protocolType) { + case HTTP: + return createHttpProtocol(config); + case TCP: + return createTcpProtocol(config); + case UDP: + return createUdpProtocol(config); + case COAP: + 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)); + } + } + + /** + * 创建 HTTP 协议实例 + * + * @param config 协议实例配置 + * @return HTTP 协议实例 + */ + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotHttpProtocol(config); + } + + /** + * 创建 TCP 协议实例 + * + * @param config 协议实例配置 + * @return TCP 协议实例 + */ + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotTcpProtocol(config); + } + + /** + * 创建 UDP 协议实例 + * + * @param config 协议实例配置 + * @return UDP 协议实例 + */ + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotUdpProtocol(config); + } + + /** + * 创建 CoAP 协议实例 + * + * @param config 协议实例配置 + * @return CoAP 协议实例 + */ + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotCoapProtocol(config); + } + + /** + * 创建 WebSocket 协议实例 + * + * @param config 协议实例配置 + * @return WebSocket 协议实例 + */ + 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/IotCoapConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java new file mode 100644 index 0000000..45fe300 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT CoAP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotCoapConfig { + + /** + * 最大消息大小(字节) + */ + @NotNull(message = "最大消息大小不能为空") + @Min(value = 64, message = "最大消息大小必须大于 64 字节") + private Integer maxMessageSize = 1024; + + /** + * ACK 超时时间(毫秒) + */ + @NotNull(message = "ACK 超时时间不能为空") + @Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒") + private Integer ackTimeoutMs = 2000; + + /** + * 最大重传次数 + */ + @NotNull(message = "最大重传次数不能为空") + @Min(value = 0, message = "最大重传次数必须大于等于 0") + private Integer maxRetransmit = 4; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java deleted file mode 100644 index d01cdc4..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 CoAP 订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotCoapDownstreamSubscriber implements IotMessageSubscriber { - - private final IotCoapUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) - log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); - } - -} 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 new file mode 100644 index 0000000..14fe10d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java @@ -0,0 +1,173 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; + +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.coap.handler.downstream.IotCoapDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamTopicResource; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +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 cn.hutool.core.lang.Assert; + +import java.util.concurrent.TimeUnit; + +/** + * IoT CoAP 协议实现 + *

+ * 基于 Eclipse Californium 实现,支持: + * 1. 认证:POST /auth + * 2. 设备动态注册:POST /auth/register/device + * 3. 子设备动态注册:POST /auth/register/sub-device/{productKey}/{deviceName} + * 4. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post + * 5. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * CoAP 服务器 + */ + private CoapServer coapServer; + + /** + * 下行消息订阅者 + */ + private final IotCoapDownstreamSubscriber downstreamSubscriber; + + public IotCoapProtocol(ProtocolProperties properties) { + IotCoapConfig coapConfig = properties.getCoap(); + Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.COAP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId()); + return; + } + + IotCoapConfig coapConfig = properties.getCoap(); + try { + // 1.1 创建 CoAP 配置 + Configuration config = Configuration.createStandardWithoutFile(); + config.set(CoapConfig.COAP_PORT, properties.getPort()); + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); + config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS); + config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit()); + // 1.2 创建 CoAP 服务器 + coapServer = new CoapServer(config); + + // 2.1 添加 /auth 认证资源 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId); + IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler); + coapServer.add(authResource); + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); + // 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源 + IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler(); + IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler); + authResource.add(new CoapResource("register") {{ + add(registerResource); + add(registerSubResource); + }}); + // 2.4 添加 /topic 根资源(用于上行消息) + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId); + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler); + coapServer.add(topicResource); + + // 3. 启动服务器 + coapServer.start(); + running = true; + log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 4. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); + if (coapServer != null) { + coapServer.destroy(); + coapServer = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2. 关闭 CoAP 服务器 + if (coapServer != null) { + try { + coapServer.stop(); + coapServer.destroy(); + coapServer = null; + log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e); + } + } + running = false; + log.info("[stop][IoT CoAP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted file mode 100644 index e259aa6..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -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.coap.router.IotCoapAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -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 java.util.concurrent.TimeUnit; - -/** - * IoT 网关 CoAP 协议:接收设备上行消息 - * - * 基于 Eclipse Californium 实现,支持: - * 1. 认证:POST /auth - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamProtocol { - - private final IotGatewayProperties.CoapProperties coapProperties; - - private CoapServer coapServer; - - @Getter - private final String serverId; - - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { - this.coapProperties = coapProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); - } - - @PostConstruct - public void start() { - try { - // 1.1 创建网络配置(Californium 3.x API) - Configuration config = Configuration.createStandardWithoutFile(); - config.set(CoapConfig.COAP_PORT, coapProperties.getPort()); - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize()); - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS); - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit()); - // 1.2 创建 CoAP 服务器 - coapServer = new CoapServer(config); - - // 2.1 添加 /auth 认证资源 - IotCoapAuthHandler authHandler = new IotCoapAuthHandler(); - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); - coapServer.add(authResource); - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); - authResource.add(new CoapResource("register") {{ - add(registerResource); - }}); - // 2.3 添加 /topic 根资源(用于上行消息) - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(); - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); - coapServer.add(topicResource); - - // 3. 启动服务器 - coapServer.start(); - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 CoAP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (coapServer != null) { - try { - coapServer.stop(); - log.info("[stop][IoT 网关 CoAP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java new file mode 100644 index 0000000..188d2e6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.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.coap.IotCoapProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 CoAP 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) + log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java new file mode 100644 index 0000000..994fb14 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Option; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class IotCoapAbstractHandler { + + /** + * 自定义 CoAP Option 编号,用于携带 Token + *

+ * CoAP Option 范围 2048-65535 属于实验/自定义范围 + */ + public static final int OPTION_TOKEN = 2088; + + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + + /** + * 处理 CoAP 请求(模板方法) + * + * @param exchange CoAP 交换对象 + */ + public final void handle(CoapExchange exchange) { + try { + // 1. 前置处理 + beforeHandle(exchange); + + // 2. 执行业务逻辑 + CommonResult result = handle0(exchange); + writeResponse(exchange, result); + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和消息 + writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage())); + } catch (IllegalArgumentException e) { + // 参数校验异常(hutool Assert 抛出),返回 BAD_REQUEST + writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); + } catch (Exception e) { + // 其他未知异常,返回 INTERNAL_SERVER_ERROR + log.error("[handle][CoAP 请求处理异常]", e); + writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR)); + } + } + + /** + * 处理 CoAP 请求(子类实现) + * + * @param exchange CoAP 交换对象 + * @return 处理结果 + */ + protected abstract CommonResult handle0(CoapExchange exchange); + + /** + * 前置处理:认证等 + * + * @param exchange CoAP 交换对象 + */ + private void beforeHandle(CoapExchange exchange) { + // 1.1 如果不需要认证,则不走前置处理 + if (!requiresAuthentication()) { + return; + } + // 1.2 从自定义 Option 获取 token + String token = getTokenFromOption(exchange); + if (StrUtil.isEmpty(token)) { + throw exception(UNAUTHORIZED); + } + // 1.3 校验 token + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); + if (deviceInfo == null) { + throw exception(UNAUTHORIZED); + } + + // 2.1 解析 productKey 和 deviceName + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) { + throw exception(BAD_REQUEST); + } + // 2.2 校验设备信息是否匹配 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { + throw exception(FORBIDDEN); + } + } + + // ========== Token 相关方法 ========== + + /** + * 是否需要认证(子类可覆盖) + *

+ * 默认不需要认证 + * + * @return 是否需要认证 + */ + protected boolean requiresAuthentication() { + return false; + } + + /** + * 从 URI 路径中获取 productKey(子类实现) + *

+ * 默认抛出异常,需要认证的子类必须实现此方法 + * + * @param uriPath URI 路径 + * @return productKey + */ + protected String getProductKey(List uriPath) { + throw new UnsupportedOperationException("子类需要实现 getProductKey 方法"); + } + + /** + * 从 URI 路径中获取 deviceName(子类实现) + *

+ * 默认抛出异常,需要认证的子类必须实现此方法 + * + * @param uriPath URI 路径 + * @return deviceName + */ + protected String getDeviceName(List uriPath) { + throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法"); + } + + /** + * 从自定义 CoAP Option 中获取 Token + * + * @param exchange CoAP 交换对象 + * @return Token 值,如果不存在则返回 null + */ + protected String getTokenFromOption(CoapExchange exchange) { + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), + o -> o.getNumber() == OPTION_TOKEN); + return option != null ? new String(option.getValue()) : null; + } + + // ========== 序列化相关方法 ========== + + /** + * 解析请求体为指定类型 + * + * @param exchange CoAP 交换对象 + * @param clazz 目标类型 + * @param 目标类型泛型 + * @return 解析后的对象,解析失败返回 null + */ + protected T deserializeRequest(CoapExchange exchange, Class clazz) { + byte[] payload = exchange.getRequestPayload(); + if (ArrayUtil.isEmpty(payload)) { + return null; + } + return JsonUtils.parseObject(payload, clazz); + } + + private static String serializeResponse(Object data) { + return JsonUtils.toJsonString(data); + } + + protected void writeResponse(CoapExchange exchange, CommonResult data) { + String json = serializeResponse(data); + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java new file mode 100644 index 0000000..0b1914e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +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.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * IoT 网关 CoAP 协议的【认证】处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapAuthHandler extends IotCoapAbstractHandler { + + private final String serverId; + + private final IotDeviceTokenService deviceTokenService; + private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService deviceMessageService; + + public IotCoapAuthHandler(String serverId) { + this.serverId = serverId; + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1. 解析参数 + IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "请求体不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult result = deviceApi.authDevice(request); + result.checkError(); + if (BooleanUtil.isFalse(result.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 生成 Token + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); + Assert.notNull(deviceInfo, "设备信息不能为空"); + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notBlank(token, "生成 token 不能为空"); + + // 3. 执行上线 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(message, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); + + // 4. 构建响应数据 + return CommonResult.success(MapUtil.of("token", token)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java index 9d0d90c..95b6fef 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource { public static final String PATH = "auth"; - private final IotCoapUpstreamProtocol protocol; private final IotCoapAuthHandler authHandler; - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol, - IotCoapAuthHandler authHandler) { + public IotCoapAuthResource(IotCoapAuthHandler authHandler) { super(PATH); - this.protocol = protocol; this.authHandler = authHandler; log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH); } @@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource { @Override public void handlePOST(CoapExchange exchange) { log.debug("[handlePOST][收到 /auth POST 请求]"); - authHandler.handle(exchange, protocol); + authHandler.handle(exchange); } } 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 new file mode 100644 index 0000000..12a70d9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.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.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +/** + * IoT 网关 CoAP 协议的【设备动态注册】处理器 + *

+ * 用于直连设备/网关的一型一密动态注册,不需要认证 + * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +@Slf4j +public class IotCoapRegisterHandler extends IotCoapAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotCoapRegisterHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + protected CommonResult handle0(CoapExchange exchange) { + // 1. 解析参数 + IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class); + Assert.notNull(request, "请求体不能为空"); + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(request); + result.checkError(); + + // 3. 构建响应数据 + return CommonResult.success(result.getData()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java index 05fd1ec..f8f6b0c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java new file mode 100644 index 0000000..8827cc3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.collection.CollUtil; +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.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 网关 CoAP 协议的【子设备动态注册】处理器 + *

+ * 用于子设备的动态注册,需要网关认证 + * + * @author 芋道源码 + * @see 阿里云 - 动态注册子设备 + */ +@Slf4j +public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotCoapRegisterSubHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1.1 解析通用参数(从 URI 路径获取网关设备信息) + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + // 1.2 解析子设备列表 + SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notEmpty(request.getParams(), "params 不能为空"); + + // 2. 调用子设备动态注册 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() + .setGatewayProductKey(productKey) + .setGatewayDeviceName(deviceName) + .setSubDevices(request.getParams()); + CommonResult> result = deviceApi.registerSubDevices(reqDTO); + result.checkError(); + + // 3. 返回结果 + return success(result.getData()); + } + + @Override + protected boolean requiresAuthentication() { + return true; + } + + @Override + protected String getProductKey(List uriPath) { + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} + return CollUtil.get(uriPath, 3); + } + + @Override + protected String getDeviceName(List uriPath) { + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} + return CollUtil.get(uriPath, 4); + } + + @Data + public static class SubDeviceRegisterRequest { + + private List params; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java new file mode 100644 index 0000000..3cc42b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; + +/** + * IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName}) + *

+ * 用于子设备的动态注册,需要网关认证 + *

+ * 支持动态路径匹配:productKey 和 deviceName 是网关设备的标识 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapRegisterSubResource extends CoapResource { + + public static final String PATH = "sub-device"; + + private final IotCoapRegisterSubHandler registerSubHandler; + + /** + * 创建根资源(/auth/register/sub-device) + */ + public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) { + this(PATH, registerSubHandler); + log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH); + } + + /** + * 创建子资源(动态路径) + */ + private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) { + super(name); + this.registerSubHandler = registerSubHandler; + } + + @Override + public Resource getChild(String name) { + // 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径 + return new IotCoapRegisterSubResource(name, registerSubHandler); + } + + @Override + public void handlePOST(CoapExchange exchange) { + log.debug("[handlePOST][收到子设备动态注册请求]"); + registerSubHandler.handle(exchange); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java new file mode 100644 index 0000000..d9e349b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +/** + * IoT 网关 CoAP 协议的【上行】处理器 + * + * 处理设备通过 CoAP 协议发送的上行消息,包括: + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post + * + * Token 通过自定义 CoAP Option 2088 携带 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapUpstreamHandler extends IotCoapAbstractHandler { + + private final String serverId; + + private final IotDeviceMessageService deviceMessageService; + + public IotCoapUpstreamHandler(String serverId) { + this.serverId = serverId; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1.1 解析通用参数 + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); + // 1.2 解析消息 + IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class); + Assert.notNull(message, "请求参数不能为空"); + Assert.equals(method, message.getMethod(), "method 不匹配"); + + // 2. 发送消息 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + + // 3. 返回结果 + return CommonResult.success(MapUtil.of("messageId", message.getId())); + } + + @Override + protected boolean requiresAuthentication() { + return true; + } + + @Override + protected String getProductKey(List uriPath) { + // 路径格式:/topic/sys/{productKey}/{deviceName}/... + return CollUtil.get(uriPath, 2); + } + + @Override + protected String getDeviceName(List uriPath) { + // 路径格式:/topic/sys/{productKey}/{deviceName}/... + return CollUtil.get(uriPath, 3); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java index 1c69448..65185b5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource { public static final String PATH = "topic"; - private final IotCoapUpstreamProtocol protocol; + private final String serverId; private final IotCoapUpstreamHandler upstreamHandler; /** * 创建根资源(/topic) */ - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol, + public IotCoapUpstreamTopicResource(String serverId, IotCoapUpstreamHandler upstreamHandler) { - this(PATH, protocol, upstreamHandler); + this(PATH, serverId, upstreamHandler); log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH); } @@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource { * 创建子资源(动态路径) */ private IotCoapUpstreamTopicResource(String name, - IotCoapUpstreamProtocol protocol, + String serverId, IotCoapUpstreamHandler upstreamHandler) { super(name); - this.protocol = protocol; + this.serverId = serverId; this.upstreamHandler = upstreamHandler; } @Override public Resource getChild(String name) { // 递归创建动态子资源,支持任意深度路径 - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler); + return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler); } @Override public void handleGET(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } @Override public void handlePOST(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } @Override public void handlePUT(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java index 94536a6..3de662a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java @@ -2,12 +2,5 @@ * CoAP 协议实现包 *

* 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能 - *

- * URI 路径: - * - 认证:POST /auth - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - *

- * Token 通过 CoAP Option 2088 携带 */ package cn.iocoder.yudao.module.iot.gateway.protocol.coap; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted file mode 100644 index 43fb776..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -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.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【认证】处理器 - * - * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler} - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapAuthHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceCommonApi deviceApi; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapAuthHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理认证请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - // 1.2 解析参数 - String clientId = MapUtil.getStr(body, "clientId"); - if (StrUtil.isEmpty(clientId)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); - return; - } - String username = MapUtil.getStr(body, "username"); - if (StrUtil.isEmpty(username)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); - return; - } - String password = MapUtil.getStr(body, "password"); - if (StrUtil.isEmpty(password)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); - return; - } - - // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - if (result.isError()) { - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); - return; - } - if (!BooleanUtil.isTrue(result.getData())) { - log.warn("[handle][认证失败,clientId: {}]", clientId); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); - return; - } - // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); - Assert.notNull(deviceInfo, "设备信息不能为空"); - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - Assert.notBlank(token, "生成 token 不能为空"); - - // 3. 执行上线 - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); - - // 4. 返回成功响应 - log.info("[handle][认证成功,productKey: {}, deviceName: {}]", - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token)); - } catch (Exception e) { - log.error("[handle][认证处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted file mode 100644 index 8ffbe4f..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.map.MapUtil; -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.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【设备动态注册】处理器 - *

- * 用于直连设备/网关的一型一密动态注册,不需要认证 - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler - */ -@Slf4j -public class IotCoapRegisterHandler { - - private final IotDeviceCommonApi deviceApi; - - public IotCoapRegisterHandler() { - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - } - - /** - * 处理设备动态注册请求 - * - * @param exchange CoAP 交换对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - - // 1.2 解析参数 - String productKey = MapUtil.getStr(body, "productKey"); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - String deviceName = MapUtil.getStr(body, "deviceName"); - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - String productSecret = MapUtil.getStr(body, "productSecret"); - if (StrUtil.isEmpty(productSecret)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空"); - return; - } - - // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey) - .setDeviceName(deviceName) - .setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); - if (result.isError()) { - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]", - productKey, deviceName, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, - "设备动态注册失败:" + result.getMsg()); - return; - } - - // 3. 返回成功响应 - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName); - IotCoapUtils.respondSuccess(exchange, result.getData()); - } catch (Exception e) { - log.error("[handle][设备动态注册处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted file mode 100644 index d33eb46..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.StrPool; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjUtil; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.List; - -/** - * IoT 网关 CoAP 协议的【上行】处理器 - * - * 处理设备通过 CoAP 协议发送的上行消息,包括: - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * Token 通过自定义 CoAP Option 2088 携带 - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapUpstreamHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理 CoAP 请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1. 解析通用参数 - List uriPath = exchange.getRequestOptions().getUriPath(); - String productKey = CollUtil.get(uriPath, 2); - String deviceName = CollUtil.get(uriPath, 3); - byte[] payload = exchange.getRequestPayload(); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - if (ArrayUtil.isEmpty(payload)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - - // 2. 认证:从自定义 Option 获取 token - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN); - if (StrUtil.isEmpty(token)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空"); - return; - } - // 验证 token - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期"); - return; - } - // 验证设备信息匹配 - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配"); - return; - } - - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接 - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post] - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); - - // 2.2 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (ObjUtil.notEqual(method, message.getMethod())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配"); - return; - } - // 2.3 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); - - // 3. 返回成功响应 - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId())); - } catch (Exception e) { - log.error("[handle][CoAP 请求处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java deleted file mode 100644 index 9d5cdf3..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; - -/** - * IoT CoAP 协议工具类 - * - * @author 芋道源码 - */ -public class IotCoapUtils { - - /** - * 自定义 CoAP Option 编号,用于携带 Token - *

- * CoAP Option 范围 2048-65535 属于实验/自定义范围 - */ - public static final int OPTION_TOKEN = 2088; - - /** - * 返回成功响应 - * - * @param exchange CoAP 交换对象 - * @param data 响应数据 - */ - public static void respondSuccess(CoapExchange exchange, Object data) { - CommonResult result = CommonResult.success(data); - String json = JsonUtils.toJsonString(result); - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 返回错误响应 - * - * @param exchange CoAP 交换对象 - * @param code CoAP 响应码 - * @param message 错误消息 - */ - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) { - int errorCode = mapCoapCodeToErrorCode(code); - CommonResult result = CommonResult.error(errorCode, message); - String json = JsonUtils.toJsonString(result); - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 从自定义 CoAP Option 中获取 Token - * - * @param exchange CoAP 交换对象 - * @param optionNumber Option 编号 - * @return Token 值,如果不存在则返回 null - */ - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) { - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), - o -> o.getNumber() == optionNumber); - return option != null ? new String(option.getValue()) : null; - } - - /** - * 将 CoAP 响应码映射到业务错误码 - * - * @param code CoAP 响应码 - * @return 业务错误码 - */ - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) { - if (code == CoAP.ResponseCode.BAD_REQUEST) { - return BAD_REQUEST.getCode(); - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) { - return UNAUTHORIZED.getCode(); - } else if (code == CoAP.ResponseCode.FORBIDDEN) { - return FORBIDDEN.getCode(); - } else { - return INTERNAL_SERVER_ERROR.getCode(); - } - } - -} 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 a888158..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,365 +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.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -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 { - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private volatile boolean isRunning = 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; - } - - @PostConstruct - public void start() { - if (isRunning) { - return; - } - - try { - // 1. 启动 MQTT 客户端 - startMqttClient(); - - // 2. 标记服务为运行状态 - isRunning = 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; - } - } - - @PreDestroy - public void stop() { - if (!isRunning) { - return; - } - - // 1. 停止 MQTT 客户端 - stopMqttClient(); - - // 2. 标记服务为停止状态 - isRunning = false; - log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); - } - - /** - * 启动 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 (!isRunning) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - long delay = emqxProperties.getReconnectDelayMs(); - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); - vertx.setTimer(delay, timerId -> { - if (!isRunning) { - 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 (!isRunning) { - 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/handler/downstream/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 new file mode 100644 index 0000000..55aaaac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java @@ -0,0 +1,29 @@ +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.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + private final IotEmqxDownstreamHandler downstreamHandler; + + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} 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 new file mode 100644 index 0000000..bc7f4dc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import lombok.Data; + +/** + * IoT HTTP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotHttpConfig { + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java deleted file mode 100644 index 585bbdd..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 HTTP 订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotHttpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotHttpUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); - } - -} 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 new file mode 100644 index 0000000..2f92419 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -0,0 +1,182 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +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; +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; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT HTTP 协议实现 + *

+ * 基于 Vert.x 实现 HTTP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotHttpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * HTTP 服务器 + */ + private HttpServer httpServer; + + /** + * 下行消息订阅者 + */ + private IotHttpDownstreamSubscriber downstreamSubscriber; + + public IotHttpProtocol(ProtocolProperties properties) { + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.HTTP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + this.vertx = Vertx.vertx(); + + // 1.2 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + + // 1.3 创建处理器,添加路由处理器 + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); + router.post(IotHttpAuthHandler.PATH).handler(authHandler); + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); + router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); + + // 1.4 启动 HTTP 服务器 + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); + options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .result(); + running = true; + log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭资源 + if (httpServer != null) { + httpServer.close(); + httpServer = null; + } + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭 HTTP 服务器 + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e); + } + httpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT HTTP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java deleted file mode 100644 index 54cb2da..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java +++ /dev/null @@ -1,91 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -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.http.router.IotHttpAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 HTTP 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotHttpUpstreamProtocol { - - private final IotGatewayProperties.HttpProperties httpProperties; - - private final Vertx vertx; - - private HttpServer httpServer; - - @Getter - private final String serverId; - - public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) { - this.httpProperties = httpProperties; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort()); - } - - @PostConstruct - public void start() { - // 创建路由 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); - - // 创建处理器,添加路由处理器 - IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); - router.post(IotHttpAuthHandler.PATH).handler(authHandler); - IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); - router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); - IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); - router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); - router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); - - // 启动 HTTP 服务器 - HttpServerOptions options = new HttpServerOptions() - .setPort(httpProperties.getServerPort()); - if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath()) - .setCertPath(httpProperties.getSslCertPath()); - options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - try { - httpServer = vertx.createHttpServer(options) - .requestHandler(router) - .listen() - .result(); - log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 HTTP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - log.info("[stop][IoT 网关 HTTP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 HTTP 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java new file mode 100644 index 0000000..bfac16c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.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 lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 HTTP 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ + +@Slf4j +public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java index 850fde1..c403ee9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java @@ -1,6 +1,7 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -13,12 +14,10 @@ import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; @@ -27,7 +26,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public abstract class IotHttpAbstractHandler implements Handler { @@ -43,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler CommonResult result = handle0(context); writeResponse(context, result); } catch (ServiceException e) { + // 已知异常,返回对应的错误码和错误信息 writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); + } catch (IllegalArgumentException e) { + // 参数校验异常,返回 400 错误 + writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); } catch (Exception e) { + // 其他未知异常,返回 500 错误 log.error("[handle][path({}) 处理异常]", context.request().path(), e); writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); } } + /** + * 处理 HTTP 请求(子类实现) + * + * @param context RoutingContext 对象 + * @return 处理结果 + */ protected abstract CommonResult handle0(RoutingContext context); + /** + * 前置处理:认证等 + * + * @param context RoutingContext 对象 + */ private void beforeHandle(RoutingContext context) { // 如果不需要认证,则不走前置处理 String path = context.request().path(); @@ -83,12 +97,26 @@ public abstract class IotHttpAbstractHandler implements Handler } } + // ========== 序列化相关方法 ========== + + protected static T deserializeRequest(RoutingContext context, Class clazz) { + byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null; + if (ArrayUtil.isEmpty(body)) { + throw invalidParamException("请求体不能为空"); + } + return JsonUtils.parseObject(body, clazz); + } + + private static String serializeResponse(Object data) { + return JsonUtils.toJsonString(data); + } + @SuppressWarnings("deprecation") - public static void writeResponse(RoutingContext context, Object data) { + public static void writeResponse(RoutingContext context, CommonResult data) { context.response() .setStatusCode(200) .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(data)); + .end(serializeResponse(data)); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java index 148756c..21aa5a8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java @@ -1,23 +1,20 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; 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.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; @@ -32,7 +29,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { public static final String PATH = "/auth"; - private final IotHttpUpstreamProtocol protocol; + private final String serverId; private final IotDeviceTokenService deviceTokenService; @@ -40,42 +37,31 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { private final IotDeviceMessageService deviceMessageService; - public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { - this.protocol = protocol; + public IotHttpAuthHandler(IotHttpProtocol protocol) { + this.serverId = protocol.getServerId(); this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); } @Override + @SuppressWarnings("DuplicatedCode") public CommonResult handle0(RoutingContext context) { // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String clientId = body.getString("clientId"); - if (StrUtil.isEmpty(clientId)) { - throw invalidParamException("clientId 不能为空"); - } - String username = body.getString("username"); - if (StrUtil.isEmpty(username)) { - throw invalidParamException("username 不能为空"); - } - String password = body.getString("password"); - if (StrUtil.isEmpty(password)) { - throw invalidParamException("password 不能为空"); - } + IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); + CommonResult result = deviceApi.authDevice(request); result.checkError(); - if (!BooleanUtil.isTrue(result.getData())) { + if (BooleanUtil.isFalse(result.getData())) { throw exception(DEVICE_AUTH_FAIL); } // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); Assert.notNull(deviceInfo, "设备信息不能为空"); String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); Assert.notBlank(token, "生成 token 不能为空位"); @@ -83,7 +69,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { // 3. 执行上线 IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); // 构建响应数据 return success(MapUtil.of("token", token)); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/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 51459df..df010f9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/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 @@ -1,15 +1,13 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; -import cn.hutool.core.util.StrUtil; +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.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -33,27 +31,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String productKey = body.getString("productKey"); - if (StrUtil.isEmpty(productKey)) { - throw invalidParamException("productKey 不能为空"); - } - String deviceName = body.getString("deviceName"); - if (StrUtil.isEmpty(deviceName)) { - throw invalidParamException("deviceName 不能为空"); - } - String productSecret = body.getString("productSecret"); - if (StrUtil.isEmpty(productSecret)) { - throw invalidParamException("productSecret 不能为空"); - } + IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); + CommonResult result = deviceApi.registerDevice(request); result.checkError(); // 3. 返回结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java index 32a6144..4693220 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java @@ -1,17 +1,17 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.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.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; +import lombok.Data; import java.util.List; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -39,29 +39,31 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 + // 1.1 解析通用参数 String productKey = context.pathParam("productKey"); String deviceName = context.pathParam("deviceName"); + // 1.2 解析子设备列表 + SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notEmpty(request.getParams(), "params 不能为空"); - // 2. 解析子设备列表 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - if (body.getJsonArray("params") == null) { - throw invalidParamException("params 不能为空"); - } - List subDevices = JsonUtils.parseArray( - body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class); - - // 3. 调用子设备动态注册 + // 2. 调用子设备动态注册 IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() - .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices); + .setGatewayProductKey(productKey) + .setGatewayDeviceName(deviceName) + .setSubDevices(request.getParams()); CommonResult> result = deviceApi.registerSubDevices(reqDTO); result.checkError(); - // 4. 返回结果 + // 3. 返回结果 return success(result.getData()); } + @Data + public static class SubDeviceRegisterRequest { + + private List params; + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java index 5289e03..aa408dc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; @@ -6,55 +6,47 @@ import cn.hutool.core.text.StrPool; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; - /** * IoT 网关 HTTP 协议的【上行】处理器 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { public static final String PATH = "/topic/sys/:productKey/:deviceName/*"; - private final IotHttpUpstreamProtocol protocol; + private final String serverId; private final IotDeviceMessageService deviceMessageService; - public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { - this.protocol = protocol; + public IotHttpUpstreamHandler(IotHttpProtocol protocol) { + this.serverId = protocol.getServerId(); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); } @Override protected CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 + // 1.1 解析通用参数 String productKey = context.pathParam("productKey"); String deviceName = context.pathParam("deviceName"); String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); - - // 2.1 解析消息 - if (context.body().buffer() == null) { - throw invalidParamException("请求体不能为空"); - } - byte[] bytes = context.body().buffer().getBytes(); - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, - productKey, deviceName); + // 1.2 根据 Content-Type 反序列化消息 + IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class); + Assert.notNull(message, "请求参数不能为空"); Assert.equals(method, message.getMethod(), "method 不匹配"); - // 2.2 发送消息 + + // 2. 发送消息 deviceMessageService.sendDeviceMessage(message, - productKey, deviceName, protocol.getServerId()); + productKey, deviceName, serverId); // 3. 返回结果 return CommonResult.success(MapUtil.of("messageId", message.getId())); } -} \ 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/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/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java deleted file mode 100644 index 3b62368..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -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.router.IotMqttDownstreamHandler; -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:下行消息订阅器 - *

- * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttDownstreamSubscriber implements IotMessageSubscriber { - - private final IotMqttUpstreamProtocol upstreamProtocol; - - private final IotMqttDownstreamHandler downstreamHandler; - - private final IotMessageBus messageBus; - - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol, - IotMqttDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - this.upstreamProtocol = upstreamProtocol; - this.downstreamHandler = downstreamHandler; - this.messageBus = messageBus; - } - - @PostConstruct - public void subscribe() { - messageBus.register(this); - log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - try { - // 1. 校验 - String method = message.getMethod(); - if (method == null) { - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", - message.getId(), message.getDeviceId()); - return; - } - - // 2. 委托给下行处理器处理业务逻辑 - boolean success = downstreamHandler.handleDownstreamMessage(message); - if (success) { - log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } else { - log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId(), e); - } - } -} 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 fc0b667..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,92 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -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.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 { - - 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()); - } - - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; - @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(); - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (mqttServer != null) { - try { - mqttServer.close().result(); - 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/handler/downstream/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 new file mode 100644 index 0000000..c8aa299 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java @@ -0,0 +1,31 @@ +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.IotMqttProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + private final IotMqttDownstreamHandler downstreamHandler; + + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, + IotMqttDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + 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 new file mode 100644 index 0000000..a8c21c8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT TCP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotTcpConfig { + + /** + * 最大连接数 + */ + @NotNull(message = "最大连接数不能为空") + @Min(value = 1, message = "最大连接数必须大于 0") + private Integer maxConnections = 1000; + /** + * 心跳超时时间(毫秒) + */ + @NotNull(message = "心跳超时时间不能为空") + @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒") + private Long keepAliveTimeoutMs = 30000L; + + /** + * 拆包配置 + */ + @Valid + private CodecConfig codec; + + /** + * TCP 拆包配置 + */ + @Data + public static class CodecConfig { + + /** + * 拆包类型 + * + * @see IotTcpCodecTypeEnum + */ + @NotNull(message = "拆包类型不能为空") + private String type; + + /** + * LENGTH_FIELD: 长度字段偏移量 + *

+ * 表示长度字段在消息中的起始位置(从 0 开始) + */ + private Integer lengthFieldOffset; + /** + * LENGTH_FIELD: 长度字段长度(字节数) + *

+ * 常见值:1(最大 255)、2(最大 65535)、4(最大 2GB) + */ + private Integer lengthFieldLength; + /** + * LENGTH_FIELD: 长度调整值 + *

+ * 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度 + */ + private Integer lengthAdjustment = 0; + /** + * LENGTH_FIELD: 跳过的初始字节数 + *

+ * 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength + */ + private Integer initialBytesToStrip = 0; + + /** + * DELIMITER: 分隔符 + *

+ * 支持转义字符:\n(换行)、\r(回车)、\r\n(回车换行) + */ + private String delimiter; + + /** + * FIXED_LENGTH: 固定消息长度(字节) + *

+ * 每条消息的固定长度 + */ + private Integer fixedLength; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java deleted file mode 100644 index 3f0cc02..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -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.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotTcpUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotTcpConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotTcpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), 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/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java new file mode 100644 index 0000000..e864df5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +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.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.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +/** + * IoT TCP 协议实现 + *

+ * 基于 Vert.x 实现 TCP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * TCP 服务器 + */ + private NetServer tcpServer; + /** + * TCP 连接管理器 + */ + private final IotTcpConnectionManager connectionManager; + + /** + * 下行消息订阅者 + */ + private final IotTcpDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + /** + * TCP 帧编解码器 + */ + private final IotTcpFrameCodec frameCodec; + + public IotTcpProtocol(ProtocolProperties properties) { + IotTcpConfig tcpConfig = properties.getTcp(); + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + // 初始化帧编解码器 + this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); + + // 初始化连接管理器 + this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.TCP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotTcpConfig tcpConfig = properties.getTcp(); + NetServerOptions options = new NetServerOptions() + .setPort(properties.getPort()) + .setTcpKeepAlive(true) + .setTcpNoDelay(true) + .setReuseAddress(true) + .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 + 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 创建服务器并设置连接处理器 + tcpServer = vertx.createNetServer(options); + tcpServer.connectHandler(socket -> { + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(serverId, frameCodec, serializer, connectionManager); + handler.handle(socket); + }); + + // 1.4 启动 TCP 服务器 + try { + tcpServer.listen().result(); + running = true; + log.info("[start][IoT TCP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭资源 + if (tcpServer != null) { + tcpServer.close(); + tcpServer = 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 TCP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 关闭 TCP 服务器 + if (tcpServer != null) { + try { + tcpServer.close().result(); + log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e); + } + tcpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT TCP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java deleted file mode 100644 index 791c6cb..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ /dev/null @@ -1,99 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -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.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.net.NetServer; -import io.vertx.core.net.NetServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 TCP 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotTcpUpstreamProtocol { - - private final IotGatewayProperties.TcpProperties tcpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotTcpConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private NetServer tcpServer; - - public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - Vertx vertx) { - this.tcpProperties = tcpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); - } - - @PostConstruct - public void start() { - // 创建服务器选项 - NetServerOptions options = new NetServerOptions() - .setPort(tcpProperties.getPort()) - .setTcpKeepAlive(true) - .setTcpNoDelay(true) - .setReuseAddress(true); - // 配置 SSL(如果启用) - if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(tcpProperties.getSslKeyPath()) - .setCertPath(tcpProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 创建服务器并设置连接处理器 - tcpServer = vertx.createNetServer(options); - tcpServer.connectHandler(socket -> { - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, - connectionManager); - handler.handle(socket); - }); - - // 启动服务器 - try { - tcpServer.listen().result(); - log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 TCP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (tcpServer != null) { - try { - tcpServer.close().result(); - log.info("[stop][IoT 网关 TCP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 TCP 协议停止失败]", 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/tcp/codec/IotTcpCodecTypeEnum.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java new file mode 100644 index 0000000..344001f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * IoT TCP 拆包类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum IotTcpCodecTypeEnum { + + /** + * 基于固定长度的拆包 + */ + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec.class), + + /** + * 基于分隔符的拆包 + */ + DELIMITER("delimiter", IotTcpDelimiterFrameCodec.class), + + /** + * 基于长度字段的拆包 + */ + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec.class), + ; + + /** + * 类型标识 + */ + private final String type; + /** + * 编解码器类 + */ + private final Class codecClass; + + /** + * 根据类型获取枚举 + * + * @param type 类型标识 + * @return 枚举值 + */ + public static IotTcpCodecTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java new file mode 100644 index 0000000..d002d20 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; + +/** + * IoT TCP 帧编解码器接口 + *

+ * 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力 + * + * @author 芋道源码 + */ +public interface IotTcpFrameCodec { + + /** + * 获取编解码器类型 + * + * @return 编解码器类型 + */ + IotTcpCodecTypeEnum getType(); + + /** + * 创建解码器(RecordParser) + *

+ * 每个连接调用一次,返回的 parser 需绑定到 socket.handler() + * + * @param handler 消息处理器,当收到完整的消息帧后回调 + * @return RecordParser 实例 + */ + RecordParser createDecodeParser(Handler handler); + + /** + * 编码消息(加帧) + *

+ * 根据不同的编解码类型添加帧头/分隔符 + * + * @param data 原始数据 + * @return 编码后的数据(带帧头/分隔符) + */ + Buffer encode(byte[] data); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java new file mode 100644 index 0000000..a40783a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; + +/** + * IoT TCP 帧编解码器工厂 + * + * @author 芋道源码 + */ +public class IotTcpFrameCodecFactory { + + /** + * 根据配置创建编解码器 + * + * @param config 拆包配置 + * @return 编解码器实例,如果配置为空则返回 null + */ + public static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) { + Assert.notNull(config, "CodecConfig 不能为空"); + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType()); + Assert.notNull(type, "不支持的 CodecType 类型:" + config.getType()); + return ReflectUtil.newInstance(type.getCodecClass(), config); + } + +} 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 new file mode 100644 index 0000000..269d6b1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +/** + * IoT TCP 分隔符帧编解码器 + *

+ * 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符 + *

+ * 支持的分隔符: + *

    + *
  • \n - 换行符
  • + *
  • \r - 回车符
  • + *
  • \r\n - 回车换行
  • + *
  • 自定义字符串
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { + + /** + * 最大记录大小(64KB),防止 DoS 攻击 + */ + private static final int MAX_RECORD_SIZE = 65536; + + /** + * 解析后的分隔符字节数组 + */ + private final byte[] delimiterBytes; + + public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); + this.delimiterBytes = parseDelimiter(config.getDelimiter()); + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.DELIMITER; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes)); + parser.maxRecordSize(MAX_RECORD_SIZE); // 设置最大记录大小,防止 DoS 攻击 + // 处理完整消息(不包含分隔符) + parser.handler(handler); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(); + buffer.appendBytes(data); + buffer.appendBytes(delimiterBytes); + return buffer; + } + + /** + * 解析分隔符字符串为字节数组 + *

+ * 支持转义字符:\n、\r、\r\n、\t + * + * @param delimiter 分隔符字符串 + * @return 分隔符字节数组 + */ + private byte[] parseDelimiter(String delimiter) { + // 处理转义字符 + String parsed = delimiter + .replace("\\r\\n", "\r\n") + .replace("\\r", "\r") + .replace("\\n", "\n") + .replace("\\t", "\t"); + return StrUtil.utf8Bytes(parsed); + } + +} 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 new file mode 100644 index 0000000..4bd4549 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +/** + * IoT TCP 定长帧编解码器 + *

+ * 基于固定长度的拆包策略,每条消息固定字节数 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { + + /** + * 固定消息长度 + */ + private final int fixedLength; + + public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) { + Assert.notNull(config.getFixedLength(), "fixedLength 不能为空"); + this.fixedLength = config.getFixedLength(); + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.FIXED_LENGTH; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + RecordParser parser = RecordParser.newFixed(fixedLength); + parser.handler(handler); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + // 校验数据长度不能超过固定长度 + if (data.length > fixedLength) { + throw new IllegalArgumentException(String.format( + "数据长度 %d 超过固定长度 %d", data.length, fixedLength)); + } + Buffer buffer = Buffer.buffer(fixedLength); + buffer.appendBytes(data); + // 如果数据不足固定长度,填充 0(RecordParser.newFixed 解码时按固定长度读取,所以发送端需要填充) + if (data.length < fixedLength) { + byte[] padding = new byte[fixedLength - data.length]; + buffer.appendBytes(padding); + } + return buffer; + } + +} 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 new file mode 100644 index 0000000..08b7c23 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * IoT TCP 长度字段帧编解码器 + *

+ * 基于长度字段的拆包策略,消息格式:[长度字段][消息体] + *

+ * 参数说明: + *

    + *
  • lengthFieldOffset: 长度字段在消息中的偏移量
  • + *
  • lengthFieldLength: 长度字段的字节数(1/2/4)
  • + *
  • lengthAdjustment: 长度调整值,用于调整长度字段的实际含义
  • + *
  • initialBytesToStrip: 解码后跳过的字节数
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { + + /** + * 最大帧长度(64KB),防止 DoS 攻击 + */ + private static final int MAX_FRAME_LENGTH = 65536; + + private final int lengthFieldOffset; + private final int lengthFieldLength; + private final int lengthAdjustment; + private final int initialBytesToStrip; + + /** + * 头部长度 = 长度字段偏移量 + 长度字段长度 + */ + private final int headerLength; + + public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) { + Assert.notNull(config.getLengthFieldOffset(), "lengthFieldOffset 不能为空"); + Assert.notNull(config.getLengthFieldLength(), "lengthFieldLength 不能为空"); + Assert.notNull(config.getLengthAdjustment(), "lengthAdjustment 不能为空"); + Assert.notNull(config.getInitialBytesToStrip(), "initialBytesToStrip 不能为空"); + this.lengthFieldOffset = config.getLengthFieldOffset(); + this.lengthFieldLength = config.getLengthFieldLength(); + this.lengthAdjustment = config.getLengthAdjustment(); + this.initialBytesToStrip = config.getInitialBytesToStrip(); + this.headerLength = lengthFieldOffset + lengthFieldLength; + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.LENGTH_FIELD; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + // 创建状态机:先读取头部,再读取消息体 + RecordParser parser = RecordParser.newFixed(headerLength); + parser.maxRecordSize(MAX_FRAME_LENGTH); // 设置最大记录大小,防止 DoS 攻击 + final AtomicReference bodyLength = new AtomicReference<>(null); // 消息体长度,null 表示读取头部阶段 + final AtomicReference headerBuffer = new AtomicReference<>(null); // 头部消息 + + // 处理读取到的数据 + parser.handler(buffer -> { + if (bodyLength.get() == null) { + // 阶段 1: 读取头部,解析长度字段 + headerBuffer.set(buffer.copy()); + int length = readLength(buffer, lengthFieldOffset, lengthFieldLength); + int frameBodyLength = length + lengthAdjustment; + // 检查帧长度是否合法 + if (frameBodyLength < 0) { + throw new IllegalStateException(String.format( + "[createDecodeParser][帧长度异常,length: %d, frameBodyLength: %d]", + length, frameBodyLength)); + } + // 消息体为空,抛出异常 + if (frameBodyLength == 0) { + throw new IllegalStateException("[createDecodeParser][消息体不能为空]"); + } + + // 【重要】切换到读取消息体模式 + bodyLength.set(frameBodyLength); + parser.fixedSizeMode(frameBodyLength); + } else { + // 阶段 2: 读取消息体,组装完整帧 + Buffer frame = processFrame(headerBuffer.get(), buffer); + // 重置状态,准备读取下一帧 + bodyLength.set(null); + headerBuffer.set(null); + parser.fixedSizeMode(headerLength); + + // 【重要】处理完整消息 + handler.handle(frame); + } + }); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(); + // 计算要写入的长度值 + int lengthValue = data.length - lengthAdjustment; + // 写入偏移量前的填充字节(如果有) + for (int i = 0; i < lengthFieldOffset; i++) { + buffer.appendByte((byte) 0); + } + // 写入长度字段 + writeLength(buffer, lengthValue, lengthFieldLength); + // 写入消息体 + buffer.appendBytes(data); + return buffer; + } + + /** + * 从 Buffer 中读取长度字段 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private int readLength(Buffer buffer, int offset, int length) { + switch (length) { + case 1: + return buffer.getUnsignedByte(offset); + case 2: + return buffer.getUnsignedShort(offset); + case 4: + return buffer.getInt(offset); + default: + throw new IllegalArgumentException("不支持的长度字段长度: " + length); + } + } + + /** + * 向 Buffer 中写入长度字段 + */ + private void writeLength(Buffer buffer, int length, int fieldLength) { + switch (fieldLength) { + case 1: + buffer.appendByte((byte) length); + break; + case 2: + buffer.appendShort((short) length); + break; + case 4: + buffer.appendInt(length); + break; + default: + throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength); + } + } + + /** + * 处理帧数据(根据 initialBytesToStrip 跳过指定字节) + */ + private Buffer processFrame(Buffer header, Buffer body) { + Buffer fullFrame = Buffer.buffer(); + if (header != null) { + fullFrame.appendBuffer(header); + } + if (body != null) { + fullFrame.appendBuffer(body); + } + // 根据 initialBytesToStrip 跳过指定字节 + if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) { + return fullFrame.slice(initialBytesToStrip, fullFrame.length()); + } + return fullFrame; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/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 374e752..b3ae6a0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/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 @@ -1,8 +1,10 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import io.vertx.core.buffer.Buffer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,36 +17,44 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class IotTcpDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; - private final IotTcpConnectionManager connectionManager; /** + * TCP 帧编解码器(处理粘包/拆包) + */ + private final IotTcpFrameCodec codec; + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + + /** * 处理下行消息 */ public void handle(IotDeviceMessage message) { try { log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取连接信息(包含 codecType) + // 1. 检查设备连接 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( message.getDeviceId()); if (connectionInfo == null) { - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); return; } - // 2. 使用连接时的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + // 2. 序列化 + 帧编码 + byte[] payload = serializer.serialize(message); + Buffer frameData = codec.encode(payload); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), frameData.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/tcp/handler/downstream/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java new file mode 100644 index 0000000..7a29e6c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.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 lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + private final IotTcpDownstreamHandler downstreamHandler; + + public IotTcpDownstreamSubscriber(IotProtocol protocol, + IotTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} 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 new file mode 100644 index 0000000..5d54758 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -0,0 +1,328 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +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.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.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; +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; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * TCP 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpUpstreamHandler implements Handler { + + private static final String AUTH_METHOD = "auth"; + + private final String serverId; + + /** + * TCP 帧编解码器(处理粘包/拆包) + */ + private final IotTcpFrameCodec codec; + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * TCP 连接管理器 + */ + private final IotTcpConnectionManager connectionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceCommonApi deviceApi; + + public IotTcpUpstreamHandler(String serverId, + IotTcpFrameCodec codec, + IotMessageSerializer serializer, + IotTcpConnectionManager connectionManager) { + Assert.notNull(codec, "TCP FrameCodec 必须配置"); + Assert.notNull(serializer, "消息序列化器必须配置"); + Assert.notNull(connectionManager, "连接管理器不能为空"); + this.serverId = serverId; + this.codec = codec; + this.serializer = serializer; + this.connectionManager = connectionManager; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + public void handle(NetSocket socket) { + String remoteAddress = String.valueOf(socket.remoteAddress()); + log.debug("[handle][设备连接,地址: {}]", remoteAddress); + + // 1. 设置异常和关闭处理器 + socket.exceptionHandler(ex -> { + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); + socket.close(); + }); + socket.closeHandler(v -> { + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); + cleanupConnection(socket); + }); + + // 2.1 设置消息处理器 + Handler messageHandler = buffer -> { + try { + processMessage(buffer, socket); + } catch (Exception e) { + log.error("[handle][消息处理失败,地址: {}]", remoteAddress, e); + socket.close(); + } + }; + // 2.2 使用拆包器处理粘包/拆包 + RecordParser parser = codec.createDecodeParser(messageHandler); + socket.handler(parser); + log.debug("[handle][启用 {} 拆包器,地址: {}]", codec.getType(), remoteAddress); + } + + /** + * 处理消息 + * + * @param buffer 消息 + * @param socket 网络连接 + */ + private void processMessage(Buffer buffer, NetSocket socket) { + IotDeviceMessage message = null; + try { + // 1. 反序列化消息 + message = serializer.deserialize(buffer.getBytes()); + if (message == null) { + sendErrorResponse(socket, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); + return; + } + + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(message, socket); + } else { + // 业务消息 + handleBusinessRequest(message, socket); + } + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和错误信息 + log.warn("[processMessage][业务异常,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[processMessage][参数校验失败,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500,并重新抛出让上层关闭连接 + log.error("[processMessage][处理消息失败,地址: {}]", socket.remoteAddress(), e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + throw e; + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param socket 网络连接 + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, NetSocket socket) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "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(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(socket, device); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param socket 网络连接 + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, NetSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,地址: {},设备名: {}]", + socket.remoteAddress(), params.getDeviceName()); + } + + /** + * 处理业务请求 + * + * @param message 消息信息 + * @param socket 网络连接 + */ + private void handleBusinessRequest(IotDeviceMessage message, NetSocket socket) { + // 1. 获取认证信息并处理业务消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo == null) { + log.error("[handleBusinessRequest][无法获取连接信息,地址: {}]", socket.remoteAddress()); + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); + return; + } + + // 2. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,地址: {},消息: {}]", socket.remoteAddress(), message); + } + + /** + * 注册连接信息 + * + * @param socket 网络连接 + * @param device 设备 + */ + private void registerConnection(NetSocket socket, IotDeviceRespDTO device) { + IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()); + connectionManager.registerConnection(socket, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } + + /** + * 清理连接 + * + * @param socket 网络连接 + */ + private void cleanupConnection(NetSocket socket) { + // 1. 发送离线消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(socket); + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送成功响应 + * + * @param socket 网络连接 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + private void sendSuccessResponse(NetSocket socket, String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, responseMessage); + } + + /** + * 发送错误响应 + * + * @param socket 网络连接 + * @param requestId 请求 ID + * @param method 方法名 + * @param code 错误码 + * @param msg 错误消息 + */ + private void sendErrorResponse(NetSocket socket, String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, responseMessage); + } + + /** + * 写入响应到 Socket + * + * @param socket 网络连接 + * @param responseMessage 响应消息 + */ + private void writeResponse(NetSocket socket, IotDeviceMessage responseMessage) { + byte[] serializedData = serializer.serialize(responseMessage); + Buffer frameData = codec.encode(serializedData); + socket.write(frameData); + } + +} \ 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/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 c0f2cf7..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 @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,10 +19,14 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -@Component public class IotTcpConnectionManager { /** + * 最大连接数 + */ + private final int maxConnections; + + /** * 连接信息映射:NetSocket -> 连接信息 */ private final Map connectionMap = new ConcurrentHashMap<>(); @@ -33,6 +36,10 @@ public class IotTcpConnectionManager { */ private final Map deviceSocketMap = new ConcurrentHashMap<>(); + public IotTcpConnectionManager(int maxConnections) { + this.maxConnections = maxConnections; + } + /** * 注册设备连接(包含认证信息) * @@ -40,15 +47,19 @@ 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); + } // 如果设备已有其他连接,先清理旧连接 NetSocket oldSocket = deviceSocketMap.get(deviceId); if (oldSocket != null && oldSocket != socket) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, oldSocket.remoteAddress()); - oldSocket.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接 connectionMap.remove(oldSocket); + oldSocket.close(); } // 注册新连接 @@ -69,26 +80,12 @@ public class IotTcpConnectionManager { return; } Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 + deviceSocketMap.remove(deviceId, socket); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } /** - * 检查连接是否已认证 - */ - public boolean isAuthenticated(NetSocket socket) { - ConnectionInfo info = connectionMap.get(socket); - return info != null; - } - - /** - * 检查连接是否未认证 - */ - public boolean isNotAuthenticated(NetSocket socket) { - return !isAuthenticated(socket); - } - - /** * 获取连接信息 */ public ConnectionInfo getConnectionInfo(NetSocket socket) { @@ -144,15 +141,6 @@ public class IotTcpConnectionManager { */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; - } } \ 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/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java deleted file mode 100644 index 4a20f46..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ /dev/null @@ -1,508 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; -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.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.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -/** - * TCP 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotTcpUpstreamHandler implements Handler { - - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotTcpConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotTcpConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.connectionManager = connectionManager; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - @Override - public void handle(NetSocket socket) { - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - - // 设置异常和关闭处理器 - socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - - // 设置消息处理器 - socket.handler(buffer -> { - // TODO @AI:TODO @芋艿:这里应该有拆粘包的问题; - try { - processMessage(clientId, buffer, socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param buffer 消息 - * @param socket 网络连接 - * @throws Exception 消息解码失败时抛出异常 - */ - private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception { - // 1. 基础检查 - if (buffer == null || buffer.length() == 0) { - return; - } - - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer, socket); - - // 3. 解码消息 - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - if (message == null) { - throw new Exception("解码后消息为空"); - } - } catch (Exception e) { - // 消息格式错误时抛出异常,由上层处理连接断开 - throw new Exception("消息解码失败: " + e.getMessage(), e); - } - - // 4. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, message, codecType, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, message, codecType, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, message, codecType, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, message.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - try { - sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } - } - } - - /** - * 处理认证请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 - */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType, - NetSocket socket) { - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId, codecType); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要认证) - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType, - NetSocket socket) { - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType); - } - } - - /** - * 处理业务请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 - */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) { - try { - // 1. 检查认证状态 - if (connectionManager.isNotAuthenticated(socket)) { - log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType); - return; - } - - // 2. 获取认证信息并处理业务消息 - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - - // 3. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); - } - } - - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @param socket 网络连接 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer, NetSocket socket) { - // 1. 如果已认证,优先使用缓存的编解码类型 - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null - && StrUtil.isNotBlank(connectionInfo.getCodecType())) { - return connectionInfo.getCodecType(); - } - - // 2. 未认证时检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - - /** - * 注册连接信息 - * - * @param socket 网络连接 - * @param device 设备 - * @param clientId 客户端 ID - * @param codecType 消息编解码类型 - */ - private void registerConnection(NetSocket socket, IotDeviceRespDTO device, - String clientId, String codecType) { - IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(codecType); - // 注册连接 - connectionManager.registerConnection(socket, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 清理连接 - * - * @param socket 网络连接 - */ - private void cleanupConnection(NetSocket socket) { - try { - // 1. 发送离线消息(如果已认证) - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(socket); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败]", e); - } - } - - /** - * 发送响应消息 - * - * @param socket 网络连接 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - * @param codecType 消息编解码类型 - */ - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { - try { - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, - code, message); - - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.write(Buffer.buffer(encodedData)); - - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送错误响应 - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 - */ - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) { - sendResponse(socket, false, errorMessage, requestId, codecType); - } - - /** - * 发送成功响应 - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param message 消息 - * @param codecType 消息编解码类型 - */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) { - sendResponse(socket, true, message, requestId, codecType); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @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 转换 - String jsonStr = JsonUtils.toJsonString(params); - return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - * @param codecType 消息编解码类型 - */ - private void sendRegisterSuccessResponse(NetSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp, String codecType) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.write(Buffer.buffer(encodedData)); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, 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/udp/IotUdpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java new file mode 100644 index 0000000..95a6291 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT UDP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotUdpConfig { + + /** + * 最大会话数 + */ + @NotNull(message = "最大会话数不能为空") + @Min(value = 1, message = "最大会话数必须大于 0") + private Integer maxSessions = 1000; + /** + * 会话超时时间(毫秒) + *

+ * 基于 Guava Cache 的 expireAfterAccess 实现自动过期清理 + */ + @NotNull(message = "会话超时时间不能为空") + @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒") + private Long sessionTimeoutMs = 60000L; + + /** + * 接收缓冲区大小(字节) + */ + @NotNull(message = "接收缓冲区大小不能为空") + @Min(value = 1024, message = "接收缓冲区大小必须大于 1024 字节") + private Integer receiveBufferSize = 65536; + /** + * 发送缓冲区大小(字节) + */ + @NotNull(message = "发送缓冲区大小不能为空") + @Min(value = 1024, message = "发送缓冲区大小必须大于 1024 字节") + private Integer sendBufferSize = 65536; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java deleted file mode 100644 index 87f8785..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -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.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotUdpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotUdpUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final IotMessageBus messageBus; - - private IotUdpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} 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 new file mode 100644 index 0000000..bfed2d9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -0,0 +1,190 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; + +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.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; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import io.vertx.core.Vertx; +import io.vertx.core.datagram.DatagramSocket; +import io.vertx.core.datagram.DatagramSocketOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +/** + * IoT UDP 协议实现 + *

+ * 基于 Vert.x 实现 UDP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * UDP 服务器 + */ + @Getter + private DatagramSocket udpSocket; + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; + + /** + * 下行消息订阅者 + */ + private final IotUdpDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + public IotUdpProtocol(ProtocolProperties properties) { + IotUdpConfig udpConfig = properties.getUdp(); + Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + + // 初始化会话管理器 + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs()); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.UDP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT UDP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建 UDP Socket 选项 + IotUdpConfig udpConfig = properties.getUdp(); + DatagramSocketOptions options = new DatagramSocketOptions() + .setReceiveBufferSize(udpConfig.getReceiveBufferSize()) + .setSendBufferSize(udpConfig.getSendBufferSize()) + .setReuseAddress(true); + + // 1.3 创建 UDP Socket + udpSocket = vertx.createDatagramSocket(options); + + // 1.4 创建上行消息处理器 + IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer); + + // 1.5 启动 UDP 服务器(阻塞式) + try { + udpSocket.listen(properties.getPort(), "0.0.0.0").result(); + // 设置数据包处理器 + udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); + running = true; + log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭资源 + if (udpSocket != null) { + udpSocket.close(); + udpSocket = 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 UDP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 关闭 UDP Socket + if (udpSocket != null) { + try { + udpSocket.close().result(); + log.info("[stop][IoT UDP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 服务器停止失败]", getId(), e); + } + udpSocket = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT UDP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT UDP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted file mode 100644 index 7448683..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java +++ /dev/null @@ -1,171 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.collection.CollUtil; -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.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.datagram.DatagramSocket; -import io.vertx.core.datagram.DatagramSocketOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * IoT 网关 UDP 协议:接收设备上行消息 - *

- * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能: - * 1. 监听 UDP 端口,接收设备消息 - * 2. 定期清理不活跃的设备地址映射 - * 3. 提供 UDP Socket 用于下行消息发送 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamProtocol { - - private final IotGatewayProperties.UdpProperties udpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - @Getter - private DatagramSocket udpSocket; - - /** - * 会话清理定时器 ID - */ - private Long cleanTimerId; - - private IotUdpUpstreamHandler upstreamHandler; - - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - Vertx vertx) { - this.udpProperties = udpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.sessionManager = sessionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); - } - - @PostConstruct - public void start() { - // 1. 初始化上行消息处理器 - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager); - - // 2. 创建 UDP Socket 选项 - DatagramSocketOptions options = new DatagramSocketOptions() - .setReceiveBufferSize(udpProperties.getReceiveBufferSize()) - .setSendBufferSize(udpProperties.getSendBufferSize()) - .setReuseAddress(true); - - // 3. 创建 UDP Socket - udpSocket = vertx.createDatagramSocket(options); - - // 4. 监听端口 - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> { - if (result.failed()) { - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause()); - return; - } - // 设置数据包处理器 - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", - udpProperties.getPort(), udpProperties.getReceiveBufferSize(), - udpProperties.getSendBufferSize()); - - // 5. 启动会话清理定时器 - startSessionCleanTimer(); - }); - } - - @PreDestroy - public void stop() { - // 1. 取消会话清理定时器 - if (cleanTimerId != null) { - vertx.cancelTimer(cleanTimerId); - cleanTimerId = null; - log.info("[stop][会话清理定时器已取消]"); - } - - // 2. 关闭 UDP Socket - if (udpSocket != null) { - try { - udpSocket.close().result(); - log.info("[stop][IoT 网关 UDP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 UDP 协议停止失败]", e); - } - } - } - - /** - * 启动会话清理定时器 - */ - private void startSessionCleanTimer() { - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> { - try { - // 1. 清理超时的设备地址映射,并获取离线设备列表 - List offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs()); - - // 2. 为每个离线设备发送离线消息 - for (Long deviceId : offlineDeviceIds) { - sendOfflineMessage(deviceId); - } - if (CollUtil.isNotEmpty(offlineDeviceIds)) { - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size()); - } - } catch (Exception e) { - log.error("[cleanExpiredMappings][清理超时会话失败]", e); - } - }); - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs()); - } - - /** - * 发送设备离线消息 - * - * @param deviceId 设备 ID - */ - private void sendOfflineMessage(Long deviceId) { - try { - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); - if (device == null) { - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); - return; - } - - // 发送离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", - deviceId, device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java index 6aeb2cb..6caf71a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java @@ -1,10 +1,11 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import io.vertx.core.datagram.DatagramSocket; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j +@RequiredArgsConstructor public class IotUdpDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; + private final IotUdpProtocol protocol; private final IotUdpSessionManager sessionManager; - private final IotUdpUpstreamProtocol protocol; - - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotUdpSessionManager sessionManager, - IotUdpUpstreamProtocol protocol) { - this.deviceMessageService = deviceMessageService; - this.sessionManager = sessionManager; - this.protocol = protocol; - } + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; /** * 处理下行消息 - * - * @param message 下行消息 */ public void handle(IotDeviceMessage message) { try { log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取会话信息(包含 codecType) - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); + // 1. 检查设备会话 + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSession(message.getDeviceId()); if (sessionInfo == null) { - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); + log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); return; } - - // 2. 使用会话中的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); DatagramSocket socket = protocol.getUdpSocket(); if (socket == null) { log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); return; } - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + + // 2. 序列化消息 + byte[] serializedData = serializer.serialize(message); + + // 3. 发送到设备 + boolean success = sessionManager.sendToDevice(message.getDeviceId(), serializedData, socket); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), serializedData.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/udp/handler/downstream/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java new file mode 100644 index 0000000..ea0bc99 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.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 lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + private final IotUdpDownstreamHandler downstreamHandler; + + public IotUdpDownstreamSubscriber(IotProtocol protocol, + IotUdpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} 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 new file mode 100644 index 0000000..7b248ab --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java @@ -0,0 +1,377 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +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.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.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.datagram.DatagramPacket; +import io.vertx.core.datagram.DatagramSocket; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +import java.net.InetSocketAddress; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * UDP 上行消息处理器 + *

+ * 采用无状态 Token 机制(每次请求携带 token): + * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password + * 2. 返回 Token:服务端验证后返回 JWT token + * 3. 后续请求:每次请求在 params 中携带 token + * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpUpstreamHandler { + + private static final String AUTH_METHOD = "auth"; + + /** + * Token 参数 Key + */ + private static final String PARAM_KEY_TOKEN = "token"; + /** + * Body 参数 Key(实际请求内容) + */ + private static final String PARAM_KEY_BODY = "body"; + + private final String serverId; + + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceTokenService deviceTokenService; + private final IotDeviceCommonApi deviceApi; + + public IotUdpUpstreamHandler(String serverId, + IotUdpSessionManager sessionManager, + IotMessageSerializer serializer) { + Assert.notNull(serializer, "消息序列化器必须配置"); + Assert.notNull(sessionManager, "会话管理器不能为空"); + this.serverId = serverId; + this.sessionManager = sessionManager; + this.serializer = serializer; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + } + + // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现 + /** + * 处理 UDP 数据包 + * + * @param packet 数据包 + * @param socket UDP Socket + */ + public void handle(DatagramPacket packet, DatagramSocket socket) { + InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); + Buffer data = packet.data(); + String addressKey = sessionManager.buildAddressKey(senderAddress); + log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); + processMessage(data, senderAddress, socket); + } + + /** + * 处理消息 + * + * @param buffer 消息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { + String addressKey = sessionManager.buildAddressKey(senderAddress); + // 1.1 基础检查 + if (ArrayUtil.isEmpty(buffer)) { + return; + } + // 1.2 反序列化消息 + IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); + if (message == null) { + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); + return; + } + + // 2. 根据消息类型路由处理 + try { + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(message, senderAddress, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(message, senderAddress, socket); + } else { + // 业务消息 + handleBusinessRequest(message, senderAddress, socket); + } + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和错误信息 + log.warn("[processMessage][业务异常,来源: {},requestId: {},method: {},错误: {}]", + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[processMessage][参数校验失败,来源: {},requestId: {},method: {},错误: {}]", + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500 + log.error("[processMessage][处理消息失败,来源: {},requestId: {},method: {}]", + addressKey, message.getRequestId(), message.getMethod(), e); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + String clientId = IdUtil.simpleUUID(); + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (!BooleanUtil.isTrue(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3. 生成 JWT Token(无状态) + String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); + + // 4.1 注册会话 + registerSession(senderAddress, device, clientId); + // 4.2 发送上线消息 + sendOnlineMessage(device); + // 4.3 发送成功响应(包含 token) + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, + MapUtil.of("token", token)); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", + device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应 + sendSuccessResponse(socket, senderAddress, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,来源: {},设备名: {}]", + sessionManager.buildAddressKey(senderAddress), params.getDeviceName()); + } + + /** + * 处理业务请求 + *

+ * 请求参数格式: + * - token:JWT 令牌 + * - body:实际请求内容(可以是 Map、List 或其他类型) + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + @SuppressWarnings("unchecked") + private void handleBusinessRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + String addressKey = sessionManager.buildAddressKey(senderAddress); + // 1.1 从消息中提取 token 和 body + String token = null; + Object body = null; + if (message.getParams() instanceof Map) { + Map paramsMap = (Map) message.getParams(); + token = (String) paramsMap.get(PARAM_KEY_TOKEN); + body = paramsMap.get(PARAM_KEY_BODY); + } + if (StrUtil.isBlank(token)) { + log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "请先进行认证"); + return; + } + // 1.2 验证 token,获取设备信息 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); + if (deviceInfo == null) { + log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "token 无效或已过期"); + return; + } + // 1.3 获取设备详细信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", + addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + BAD_REQUEST.getCode(), "设备不存在"); + return; + } + + // 2. 更新会话地址(如有变化) + sessionManager.updateSessionAddress(device.getId(), senderAddress); + + // 3. 将 body 设置为实际的 params,发送消息到消息总线 + message.setParams(body); + deviceMessageService.sendDeviceMessage(message, device.getProductKey(), + device.getDeviceName(), serverId); + log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", + device.getId(), message.getMethod(), addressKey); + } + + /** + * 注册会话信息 + * + * @param address 设备地址 + * @param device 设备 + * @param clientId 客户端 ID + */ + private void registerSession(InetSocketAddress address, IotDeviceRespDTO device, String clientId) { + IotUdpSessionManager.SessionInfo sessionInfo = new IotUdpSessionManager.SessionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setAddress(address); + sessionManager.registerSession(device.getId(), sessionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送成功响应 + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, address, responseMessage); + } + + /** + * 发送错误响应 + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param method 方法名 + * @param code 错误码 + * @param msg 错误消息 + */ + private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, address, responseMessage); + } + + /** + * 写入响应到 Socket + * + * @param socket UDP Socket + * @param address 目标地址 + * @param responseMessage 响应消息 + */ + private void writeResponse(DatagramSocket socket, InetSocketAddress address, IotDeviceMessage responseMessage) { + try { + byte[] serializedData = serializer.serialize(responseMessage); + socket.send(Buffer.buffer(serializedData), address.getPort(), address.getHostString(), result -> { + if (result.failed()) { + log.error("[writeResponse][发送响应失败,地址: {}]", + sessionManager.buildAddressKey(address), result.cause()); + } + }); + } catch (Exception e) { + log.error("[writeResponse][发送响应异常,地址: {}]", + sessionManager.buildAddressKey(address), e); + } + } + +} 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 79a5bf0..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 @@ -1,105 +1,101 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager; +import cn.hutool.core.util.ObjUtil; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import io.vertx.core.buffer.Buffer; import io.vertx.core.datagram.DatagramSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.net.InetSocketAddress; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关 UDP 会话管理器 *

- * 采用无状态设计,SessionManager 主要用于: - * 1. 管理设备地址映射(用于下行消息发送) - * 2. 定期清理不活跃的设备地址映射 - *

- * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态 + * 基于 Guava Cache 实现会话的自动过期清理: + * 1. 管理设备会话信息(设备 ID -> 地址映射) + * 2. 自动清理超时会话(expireAfterAccess) + * 3. 限制最大会话数(maximumSize) * * @author 芋道源码 */ @Slf4j -@Component public class IotUdpSessionManager { /** - * 设备 ID -> 会话信息(包含地址和 codecType) + * 设备会话缓存:设备 ID -> 会话信息 + *

+ * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间 */ - private final Map deviceSessionMap = new ConcurrentHashMap<>(); + private final Cache deviceSessionCache; - /** - * 设备地址 Key -> 最后活跃时间(用于清理) - */ - private final Map lastActiveTimeMap = new ConcurrentHashMap<>(); + private final int maxSessions; - /** - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) - */ - private final Map addressDeviceMap = new ConcurrentHashMap<>(); - - /** - * 更新设备会话(每次收到上行消息时调用) - * - * @param deviceId 设备 ID - * @param address 设备地址 - * @param codecType 消息编解码类型 - */ - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { - String addressKey = buildAddressKey(address); - // 更新设备会话映射 - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); - lastActiveTimeMap.put(addressKey, LocalDateTime.now()); - addressDeviceMap.put(addressKey, deviceId); - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) { + this.maxSessions = maxSessions; + this.deviceSessionCache = CacheBuilder.newBuilder() + .maximumSize(maxSessions) + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS) + .build(); } /** - * 更新设备地址(兼容旧接口,默认不更新 codecType) + * 注册设备会话 * - * @param deviceId 设备 ID - * @param address 设备地址 + * @param deviceId 设备 ID + * @param sessionInfo 会话信息 */ - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null; - updateDeviceSession(deviceId, address, codecType); + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) + if (deviceSessionCache.getIfPresent(deviceId) == null + && deviceSessionCache.size() >= maxSessions) { + throw new IllegalStateException("会话数已达上限: " + maxSessions); + } + // 注册会话 + deviceSessionCache.put(deviceId, sessionInfo); + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]", + deviceId, buildAddressKey(sessionInfo.getAddress()), + sessionInfo.getProductKey(), sessionInfo.getDeviceName()); } /** - * 获取设备会话信息 + * 获取会话信息 + *

+ * 注意:调用此方法会自动刷新会话的过期时间 * * @param deviceId 设备 ID - * @return 会话信息 + * @return 会话信息,不存在则返回 null */ - public SessionInfo getSessionInfo(Long deviceId) { - return deviceSessionMap.get(deviceId); + public SessionInfo getSession(Long deviceId) { + return deviceSessionCache.getIfPresent(deviceId); } /** - * 检查设备是否在线(即是否有地址映射) + * 更新设备会话地址(设备地址变更时调用) + *

+ * 注意:getIfPresent 已自动刷新过期时间,无需重新 put * - * @param deviceId 设备 ID - * @return 是否在线 + * @param deviceId 设备 ID + * @param newAddress 新地址 */ - public boolean isDeviceOnline(Long deviceId) { - return deviceSessionMap.containsKey(deviceId); - } + public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { + // 地址未变化,无需更新 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); + if (sessionInfo == null) { + return; + } + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) { + return; + } - /** - * 检查设备是否离线 - * - * @param deviceId 设备 ID - * @return 是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); + // 更新地址 + String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); + sessionInfo.setAddress(newAddress); + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]", + deviceId, oldAddressKey, buildAddressKey(newAddress)); } /** @@ -111,24 +107,28 @@ public class IotUdpSessionManager { * @return 是否发送成功 */ public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); if (sessionInfo == null || sessionInfo.getAddress() == null) { log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); return false; } - 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); + future.complete(true); } else { log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", deviceId, buildAddressKey(address), result.cause()); + future.complete(false); } }); - return true; + // 同步等待结果,超时 5 秒 + return future.get(5, TimeUnit.SECONDS); } catch (Exception e) { log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); return false; @@ -136,44 +136,7 @@ public class IotUdpSessionManager { } /** - * 定期清理不活跃的设备地址映射 - * - * @param timeoutMs 超时时间(毫秒) - * @return 清理的设备 ID 列表(用于发送离线消息) - */ - public List cleanExpiredMappings(long timeoutMs) { - List offlineDeviceIds = new ArrayList<>(); - LocalDateTime now = LocalDateTime.now(); - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); - Iterator> iterator = lastActiveTimeMap.entrySet().iterator(); - while (iterator.hasNext()) { - // 未过期,跳过 - Map.Entry entry = iterator.next(); - if (entry.getValue().isAfter(expireTime)) { - continue; - } - // 过期处理:记录离线设备 ID - String addressKey = entry.getKey(); - Long deviceId = addressDeviceMap.remove(addressKey); - if (deviceId == null) { - iterator.remove(); - continue; - } - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); - if (sessionInfo == null) { - iterator.remove(); - continue; - } - offlineDeviceIds.add(deviceId); - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", - deviceId, addressKey, entry.getValue()); - iterator.remove(); - } - return offlineDeviceIds; - } - - /** - * 构建地址 Key + * 构建地址 Key(用于日志输出) * * @param address 地址 * @return 地址 Key @@ -189,14 +152,22 @@ public class IotUdpSessionManager { public static class SessionInfo { /** - * 设备地址 + * 设备 ID */ - private InetSocketAddress address; + private Long deviceId; + /** + * 产品 Key + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; /** - * 消息编解码类型 + * 设备地址 */ - private String codecType; + private InetSocketAddress address; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted file mode 100644 index 872a615..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java +++ /dev/null @@ -1,542 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; - -import cn.hutool.core.map.MapUtil; -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.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.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.datagram.DatagramPacket; -import io.vertx.core.datagram.DatagramSocket; -import lombok.extern.slf4j.Slf4j; - -import java.net.InetSocketAddress; -import java.util.Map; - -/** - * UDP 上行消息处理器 - *

- * 采用无状态 Token 机制(每次请求携带 token): - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password - * 2. 返回 Token:服务端验证后返回 JWT token - * 3. 后续请求:每次请求在 params 中携带 token - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamHandler { - - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - /** - * Token 参数 Key - */ - private static final String PARAM_KEY_TOKEN = "token"; - /** - * Body 参数 Key(实际请求内容) - */ - private static final String PARAM_KEY_BODY = "body"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotUdpSessionManager sessionManager; - - private final IotDeviceTokenService deviceTokenService; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotUdpSessionManager sessionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.sessionManager = sessionManager; - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - /** - * 处理 UDP 数据包 - * - * @param packet 数据包 - * @param socket UDP Socket - */ - public void handle(DatagramPacket packet, DatagramSocket socket) { - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); - Buffer data = packet.data(); - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", - sessionManager.buildAddressKey(senderAddress), data.length()); - try { - processMessage(data, senderAddress, socket); - } catch (Exception e) { - log.error("[handle][处理消息失败,来源: {},错误: {}]", - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e); - // UDP 无连接,不需要断开连接,只记录错误 - } - } - - /** - * 处理消息 - * - * @param buffer 消息 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { - // 1. 基础检查 - if (buffer == null || buffer.length() == 0) { - return; - } - - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer); - - // 3. 解码消息 - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - if (message == null) { - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress)); - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType); - return; - } - } catch (Exception e) { - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e); - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType); - return; - } - - // 4. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(message, codecType, senderAddress, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(message, codecType, senderAddress, socket); - } else { - // 业务消息 - handleBusinessRequest(message, codecType, senderAddress, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]", - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType); - } - } - - /** - * 处理认证请求 - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]", - addressKey, authParams.getUsername()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3.1 生成 JWT Token(无状态) - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); - - // 3.2 更新设备会话信息(用于下行消息,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 3.3 发送上线消息 - sendOnlineMessage(device); - - // 3.4 发送成功响应(包含 token) - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", - device.getId(), device.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 Token) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType); - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]", - params.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType); - } - } - - /** - * 处理业务请求 - *

- * 请求参数格式: - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - @SuppressWarnings("unchecked") - private void handleBusinessRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}) - String token = null; - Object body = null; - if (message.getParams() instanceof Map) { - Map paramsMap = (Map) message.getParams(); - token = (String) paramsMap.get(PARAM_KEY_TOKEN); - body = paramsMap.get(PARAM_KEY_BODY); - } - if (StrUtil.isBlank(token)) { - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType); - return; - } - // 1.2 验证 token,获取设备信息 - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); - return; - } - - // 2. 获取设备详细信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3. 更新设备会话信息(保持最新,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 4. 将 body 设置为实际的 params,发送消息到消息总线 - message.setParams(body); - deviceMessageService.sendDeviceMessage(message, device.getProductKey(), - device.getDeviceName(), serverId); - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", - device.getId(), message.getMethod(), addressKey); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType); - } - } - - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer) { - // 检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送认证成功响应(包含 token) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param token JWT Token - * @param codecType 消息编解码类型 - */ - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String token, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", true) - .put("token", token) - .put("message", "认证成功") - .build(); - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - // 发送响应 - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param registerResp 注册响应 - * @param codecType 消息编解码类型 - */ - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, IotDeviceRegisterRespDTO registerResp, - String codecType) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送错误响应 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 - */ - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String errorMessage, String codecType) { - sendResponse(socket, address, false, errorMessage, requestId, codecType); - } - - /** - * 发送响应消息 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - * @param codecType 消息编解码类型 - */ - @SuppressWarnings("SameParameterValue") - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, - String message, String requestId, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - "response", responseData, code, message); - - // 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { - if (ar.failed()) { - log.error("[sendResponse][发送响应失败,地址: {}]", - sessionManager.buildAddressKey(address), ar.cause()); - } - }); - } catch (Exception e) { - log.error("[sendResponse][发送响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @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; - } - } - -} 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 new file mode 100644 index 0000000..95f738d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT WebSocket 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotWebSocketConfig { + + /** + * WebSocket 路径(默认:/ws) + */ + @NotEmpty(message = "WebSocket 路径不能为空") + private String path = "/ws"; + + /** + * 最大消息大小(字节,默认 64KB) + */ + @NotNull(message = "最大消息大小不能为空") + private Integer maxMessageSize = 65536; + /** + * 最大帧大小(字节,默认 64KB) + */ + @NotNull(message = "最大帧大小不能为空") + private Integer maxFrameSize = 65536; + + /** + * 空闲超时时间(秒,默认 60) + */ + @NotNull(message = "空闲超时时间不能为空") + private Integer idleTimeoutSeconds = 60; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java deleted file mode 100644 index 47abb33..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -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.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotWebSocketDownstreamSubscriber implements IotMessageSubscriber { - - private final IotWebSocketUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotWebSocketDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotWebSocketDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][WebSocket 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} 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 new file mode 100644 index 0000000..b416900 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; + +import cn.hutool.core.util.ObjUtil; +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.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.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import cn.hutool.core.lang.Assert; + +/** + * IoT WebSocket 协议实现 + *

+ * 基于 Vert.x 实现 WebSocket 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * WebSocket 服务器 + */ + private HttpServer httpServer; + /** + * WebSocket 连接管理器 + */ + private final IotWebSocketConnectionManager connectionManager; + + /** + * 下行消息订阅者 + */ + private final IotWebSocketDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + public IotWebSocketProtocol(ProtocolProperties properties) { + Assert.notNull(properties, "协议实例配置不能为空"); + Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + + // 初始化连接管理器 + this.connectionManager = new IotWebSocketConnectionManager(); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); + this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.WEBSOCKET; + } + + @Override + @SuppressWarnings("deprecation") + public void start() { + if (running) { + log.warn("[start][IoT WebSocket 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotWebSocketConfig wsConfig = properties.getWebsocket(); + HttpServerOptions options = new HttpServerOptions() + .setPort(properties.getPort()) + .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) + .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) + .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); + 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 创建服务器并设置 WebSocket 处理器 + httpServer = vertx.createHttpServer(options); + httpServer.webSocketHandler(socket -> { + // 验证路径 + if (ObjUtil.notEqual(wsConfig.getPath(), socket.path())) { + log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", + socket.path(), wsConfig.getPath()); + socket.reject(); + return; + } + // 创建上行处理器 + IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(serverId, serializer, connectionManager); + handler.handle(socket); + }); + + // 1.4 启动服务器 + try { + httpServer.listen().result(); + running = true; + log.info("[start][IoT WebSocket 协议 {} 启动成功,端口:{},路径:{},serverId:{}]", + getId(), properties.getPort(), wsConfig.getPath(), serverId); + + // 2. 启动下行消息订阅者 + downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT WebSocket 协议 {} 启动失败]", getId(), e); + if (httpServer != null) { + httpServer.close(); + httpServer = 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 WebSocket 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 关闭 WebSocket 服务器 + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT WebSocket 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 服务器停止失败]", getId(), e); + } + httpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT WebSocket 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT WebSocket 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java deleted file mode 100644 index 9c612ac..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.util.ObjUtil; -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.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 WebSocket 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamProtocol { - - private final IotGatewayProperties.WebSocketProperties wsProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private HttpServer httpServer; - - public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - Vertx vertx) { - this.wsProperties = wsProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort()); - } - - @PostConstruct - @SuppressWarnings("deprecation") - public void start() { - // 1.1 创建服务器选项 - HttpServerOptions options = new HttpServerOptions() - .setPort(wsProperties.getPort()) - .setIdleTimeout(wsProperties.getIdleTimeoutSeconds()) - .setMaxWebSocketFrameSize(wsProperties.getMaxFrameSize()) - .setMaxWebSocketMessageSize(wsProperties.getMaxMessageSize()); - // 1.2 配置 SSL(如果启用) - if (Boolean.TRUE.equals(wsProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(wsProperties.getSslKeyPath()) - .setCertPath(wsProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 2. 创建服务器并设置 WebSocket 处理器 - httpServer = vertx.createHttpServer(options); - httpServer.webSocketHandler(socket -> { - // 验证路径 - if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) { - log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", - socket.path(), wsProperties.getPath()); - socket.reject(); - return; - } - // 创建上行处理器 - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, - messageService, deviceService, connectionManager); - handler.handle(socket); - }); - - // 3. 启动服务器 - try { - httpServer.listen().result(); - log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath()); - } catch (Exception e) { - log.error("[start][IoT 网关 WebSocket 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - log.info("[stop][IoT 网关 WebSocket 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java index 05e3c8c..6391fd4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,7 +16,7 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class IotWebSocketDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; + private final IotMessageSerializer serializer; private final IotWebSocketConnectionManager connectionManager; @@ -36,17 +36,17 @@ public class IotWebSocketDownstreamHandler { return; } - // 2. 编码消息并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - String jsonMessage = StrUtil.utf8Str(bytes); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + // 2. 序列化 + byte[] bytes = serializer.serialize(message); + String bytesContent = StrUtil.utf8Str(bytes); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(connectionInfo.getDeviceId(), bytesContent); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.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/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java new file mode 100644 index 0000000..efe5f43 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.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.websocket.IotWebSocketProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + private final IotWebSocketDownstreamHandler downstreamHandler; + + public IotWebSocketDownstreamSubscriber(IotWebSocketProtocol protocol, + IotWebSocketDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} 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 new file mode 100644 index 0000000..48de709 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java @@ -0,0 +1,305 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream; + +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.exception.ServiceException; +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.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.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Handler; +import io.vertx.core.http.ServerWebSocket; +import lombok.extern.slf4j.Slf4j; +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; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + + +/** + * WebSocket 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketUpstreamHandler implements Handler { + + private static final String AUTH_METHOD = "auth"; + + private final String serverId; + + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * 连接管理器 + */ + private final IotWebSocketConnectionManager connectionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceCommonApi deviceApi; + + public IotWebSocketUpstreamHandler(String serverId, + IotMessageSerializer serializer, + IotWebSocketConnectionManager connectionManager) { + this.serverId = serverId; + this.serializer = serializer; + this.connectionManager = connectionManager; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + public void handle(ServerWebSocket socket) { + String remoteAddress = String.valueOf(socket.remoteAddress()); + log.debug("[handle][设备连接,地址: {}]", remoteAddress); + + // 1. 设置异常和关闭处理器 + socket.exceptionHandler(ex -> { + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); + socket.close(); + }); + socket.closeHandler(v -> { + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); + cleanupConnection(socket); + }); + + // 2. 设置消息处理器(仅支持文本帧) + socket.textMessageHandler(message -> { + try { + processMessage(StrUtil.utf8Bytes(message), socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,地址: {},错误: {}]", remoteAddress, e.getMessage()); + socket.close(); + } + }); + } + + /** + * 处理消息 + * + * @param payload 消息负载 + * @param socket WebSocket 连接 + */ + private void processMessage(byte[] payload, ServerWebSocket socket) { + IotDeviceMessage message = null; + try { + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; + } + // 1.2 解码消息 + message = serializer.deserialize(payload); + Assert.notNull(message, "消息反序列化失败"); + Assert.notBlank(message.getMethod(), "method 不能为空"); + + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + handleAuthenticationRequest(message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + handleRegisterRequest(message, socket); + } else { + handleBusinessRequest(message, socket); + } + } catch (ServiceException e) { + log.warn("[processMessage][业务异常,错误: {}]", e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("[processMessage][参数校验失败,错误: {}]", e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + log.error("[processMessage][处理消息失败]", e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, INTERNAL_SERVER_ERROR.getCode(), + INTERNAL_SERVER_ERROR.getMsg()); + throw e; + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param socket WebSocket 连接 + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "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(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(socket, device); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param socket WebSocket 连接 + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应(包含 deviceSecret) + sendSuccessResponse(socket, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,设备名: {}]", params.getDeviceName()); + } + + /** + * 处理业务请求 + * + * @param message 消息信息 + * @param socket WebSocket 连接 + */ + private void handleBusinessRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 获取认证信息并处理业务消息 + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo == null) { + log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息]"); + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); + return; + } + + // 2. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,消息: {}]", message); + } + + /** + * 注册连接信息 + * + * @param socket WebSocket 连接 + * @param device 设备 + */ + private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device) { + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()); + connectionManager.registerConnection(socket, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); + } + } + + /** + * 清理连接 + * + * @param socket WebSocket 连接 + */ + private void cleanupConnection(ServerWebSocket socket) { + try { + // 1. 发送离线消息(如果已认证) + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(socket); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败]", e); + } + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送响应消息 + * + * @param socket WebSocket 连接 + * @param requestId 请求 ID + * @param method 请求方法 + * @param data 响应数据 + */ + private void sendSuccessResponse(ServerWebSocket socket, String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, responseMessage); + } + + private void sendErrorResponse(ServerWebSocket socket, String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, responseMessage); + } + + /** + * 写入响应消息 + */ + private void writeResponse(ServerWebSocket socket, IotDeviceMessage responseMessage) { + byte[] payload = serializer.serialize(responseMessage); + socket.writeTextMessage(StrUtil.utf8Str(payload)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java index 128b360..8b09da0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java @@ -4,7 +4,6 @@ import io.vertx.core.http.ServerWebSocket; import lombok.Data; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,7 +19,6 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -@Component public class IotWebSocketConnectionManager { /** @@ -69,7 +67,8 @@ public class IotWebSocketConnectionManager { return; } Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 + deviceSocketMap.remove(deviceId, socket); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } @@ -135,15 +134,6 @@ public class IotWebSocketConnectionManager { */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; - } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java deleted file mode 100644 index 630246a..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java +++ /dev/null @@ -1,471 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; -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.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.codec.alink.IotAlinkDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Handler; -import io.vertx.core.http.ServerWebSocket; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - - -/** - * WebSocket 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamHandler implements Handler { - - /** - * 默认消息编解码类型 - */ - private static final String CODEC_TYPE = IotAlinkDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotWebSocketUpstreamHandler(IotWebSocketUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotWebSocketConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.connectionManager = connectionManager; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - @Override - public void handle(ServerWebSocket socket) { - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - - // 1. 设置异常和关闭处理器 - socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - - // 2. 设置文本消息处理器 - socket.textMessageHandler(message -> { - try { - processMessage(clientId, message, socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param message 消息(JSON 字符串) - * @param socket WebSocket 连接 - * @throws Exception 消息解码失败时抛出异常 - */ - private void processMessage(String clientId, String message, ServerWebSocket socket) throws Exception { - // 1.1 基础检查 - if (StrUtil.isBlank(message)) { - return; - } - // 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE; - IotDeviceMessage deviceMessage; - try { - deviceMessage = deviceMessageService.decodeDeviceMessage( - StrUtil.utf8Bytes(message), codecType); - if (deviceMessage == null) { - throw new Exception("解码后消息为空"); - } - } catch (Exception e) { - throw new Exception("消息解码失败: " + e.getMessage(), e); - } - - // 2. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(deviceMessage.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, deviceMessage, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(deviceMessage.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, deviceMessage, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, deviceMessage, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, deviceMessage.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - try { - sendErrorResponse(socket, deviceMessage.getRequestId(), "消息处理失败"); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } - } - } - - /** - * 处理认证请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整"); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败"); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败"); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在"); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功"); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常"); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要认证) - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null - || StrUtil.hasEmpty(params.getProductKey(), params.getDeviceName(), params.getProductSecret())) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整"); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常"); - } - } - - /** - * 处理业务请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 获取认证信息并处理业务消息 - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo == null) { - log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "连接未认证"); - return; - } - - // 2. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); - } - } - - /** - * 注册连接信息 - * - * @param socket WebSocket 连接 - * @param device 设备 - * @param clientId 客户端 ID - */ - private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device, String clientId) { - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(CODEC_TYPE); - // 注册连接 - connectionManager.registerConnection(socket, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 清理连接 - * - * @param socket WebSocket 连接 - */ - private void cleanupConnection(ServerWebSocket socket) { - try { - // 1. 发送离线消息(如果已认证) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(socket); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败]", e); - } - } - - /** - * 发送响应消息 - * - * @param socket WebSocket 连接 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - */ - private void sendResponse(ServerWebSocket socket, boolean success, String message, String requestId) { - try { - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); - - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送错误响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendErrorResponse(ServerWebSocket socket, String requestId, String errorMessage) { - sendResponse(socket, false, errorMessage, requestId); - } - - /** - * 发送成功响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param message 消息 - */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(ServerWebSocket socket, String requestId, String message) { - sendResponse(socket, true, message, requestId); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - 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 socket WebSocket 连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(ServerWebSocket socket, 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, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); - } - } - -} 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/serialize/IotMessageSerializer.java index 94dd309..095dae0 100644 --- 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/serialize/IotMessageSerializer.java @@ -1,33 +1,38 @@ -package cn.iocoder.yudao.module.iot.gateway.codec; +package cn.iocoder.yudao.module.iot.gateway.serialize; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** - * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器 + * IoT 设备消息序列化器接口 + * + * 用于序列化和反序列化设备消息 * * @author 芋道源码 */ -public interface IotDeviceMessageCodec { +public interface IotMessageSerializer { /** - * 编码消息 + * 序列化消息 * * @param message 消息 * @return 编码后的消息内容 */ - byte[] encode(IotDeviceMessage message); + byte[] serialize(IotDeviceMessage message); /** - * 解码消息 + * 反序列化消息 * * @param bytes 消息内容 * @return 解码后的消息内容 */ - IotDeviceMessage decode(byte[] bytes); + IotDeviceMessage deserialize(byte[] bytes); /** - * @return 数据格式(编码器类型) + * 获取序列化类型 + * + * @return 序列化类型枚举 */ - String type(); + IotSerializeTypeEnum getType(); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java new file mode 100644 index 0000000..0c072a5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize; + +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import lombok.extern.slf4j.Slf4j; + +import java.util.EnumMap; +import java.util.Map; + +/** + * IoT 序列化器管理器 + * + * 负责根据枚举创建和管理序列化器实例 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMessageSerializerManager { + + private final Map serializerMap = new EnumMap<>(IotSerializeTypeEnum.class); + + public IotMessageSerializerManager() { + // 遍历枚举,创建对应的序列化器 + for (IotSerializeTypeEnum type : IotSerializeTypeEnum.values()) { + IotMessageSerializer serializer = createSerializer(type); + serializerMap.put(type, serializer); + log.info("[IotSerializerManager][序列化器 {} 创建成功]", type); + } + } + + /** + * 根据类型创建序列化器 + * + * @param type 序列化类型 + * @return 序列化器实例 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private IotMessageSerializer createSerializer(IotSerializeTypeEnum type) { + switch (type) { + case JSON: + return new IotJsonSerializer(); + case BINARY: + return new IotBinarySerializer(); + default: + throw new IllegalArgumentException("未知的序列化类型:" + type); + } + } + + /** + * 获取序列化器 + * + * @param type 序列化类型 + * @return 序列化器实例 + */ + public IotMessageSerializer get(IotSerializeTypeEnum type) { + return serializerMap.get(type); + } + +} 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/serialize/binary/IotBinarySerializer.java index 05098cc..7227c4d 100644 --- 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/serialize/binary/IotBinarySerializer.java @@ -1,20 +1,20 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; +package cn.iocoder.yudao.module.iot.gateway.serialize.binary; 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.enums.IotSerializeTypeEnum; 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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; 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} 编解码器 - *

+ * 二进制格式的消息序列化器 + * * 二进制协议格式(所有数值使用大端序): * *

@@ -28,20 +28,15 @@ import java.nio.charset.StandardCharsets;
  * |                        消息体数据(变长)                              |
  * +--------+--------+--------+--------+--------+--------+--------+--------+
  * 
- *

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

- * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 * * @author 芋道源码 */ @Slf4j -@Component -public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TCP_BINARY"; +public class IotBinarySerializer implements IotMessageSerializer { /** * 协议魔术字,用于协议识别 @@ -74,12 +69,12 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; @Override - public String type() { - return TYPE; + public IotSerializeTypeEnum getType() { + return IotSerializeTypeEnum.BINARY; } @Override - public byte[] encode(IotDeviceMessage message) { + public byte[] serialize(IotDeviceMessage message) { Assert.notNull(message, "消息不能为空"); Assert.notBlank(message.getMethod(), "消息方法不能为空"); try { @@ -90,19 +85,19 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // 3. 构建完整消息 return buildCompleteMessage(message, messageType, bodyData); } catch (Exception e) { - log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); - throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); + log.error("[encode][二进制消息编码失败,消息: {}]", message, e); + throw new RuntimeException("二进制消息编码失败: " + e.getMessage(), e); } } @Override - public IotDeviceMessage decode(byte[] bytes) { + public IotDeviceMessage deserialize(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); @@ -113,9 +108,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // 3. 读取消息类型 byte messageType = buffer.getByte(index++); - // 直接验证消息类型,无需抽取方法 - Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, - "无效的消息类型: " + messageType); + Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, "无效的消息类型: " + messageType); // 4. 读取消息长度 int messageLength = buffer.getInt(index); @@ -138,27 +131,28 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // 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); + log.error("[decode][二进制消息解码失败,数据长度: {}]", bytes.length, e); + throw new RuntimeException("二进制消息解码失败: " + e.getMessage(), e); } } /** - * 确定消息类型 - * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 + * 快速检测是否为二进制格式 + * + * @param data 数据 + * @return 是否为二进制格式 */ + public static boolean isBinaryFormat(byte[] data) { + return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; + } + 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) { @@ -175,7 +169,6 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { } } else { // 请求消息只处理 params 参数 - // TODO @haohao:如果为空,是不是得写个长度 0 哈? if (message.getParams() != null) { bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); } @@ -183,16 +176,13 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { 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. 更新消息长度) + // 2. 预留消息长度位置 int lengthPosition = buffer.length(); buffer.appendInt(0); // 3. 写入消息 ID @@ -212,29 +202,20 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { 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; @@ -257,9 +238,6 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return IotDeviceMessage.of(messageId, method, null, data, code, msg); } - /** - * 解析 JSON 数据 - */ private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { if (startIndex >= endIndex) { return null; @@ -273,14 +251,4 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { } } - /** - * 快速检测是否为二进制格式 - * - * @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/serialize/json/IotJsonSerializer.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java new file mode 100644 index 0000000..7fa6570 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize.json; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +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.gateway.serialize.IotMessageSerializer; + +/** + * JSON 格式的消息序列化器 + * + * 直接使用 JsonUtils 序列化/反序列化 {@link IotDeviceMessage},不包装额外字段 + * + * @author 芋道源码 + */ +public class IotJsonSerializer implements IotMessageSerializer { + + @Override + public IotSerializeTypeEnum getType() { + return IotSerializeTypeEnum.JSON; + } + + @Override + public byte[] serialize(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + return JsonUtils.toJsonByte(message); + } + + @Override + public IotDeviceMessage deserialize(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + IotDeviceMessage message = JsonUtils.parseObject(bytes, IotDeviceMessage.class); + Assert.notNull(message, "消息解码失败"); + return message; + } + +} 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 691e5cf..9c6b500 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 @@ -42,98 +42,132 @@ yudao: secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位 expiration: 7d - # 协议配置 - protocol: + # 协议实例列表 + protocols: # ==================================== # 针对引入的 HTTP 组件的配置 # ==================================== - http: - enabled: true - server-port: 8092 - # ==================================== - # 针对引入的 EMQX 组件的配置 - # ==================================== - emqx: + - id: http-json + protocol: http + port: 8092 enabled: false - http-port: 8090 # MQTT HTTP 服务端口 - 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/#" # 系统主题 - clean-session: true # 是否启用 Clean Session (默认: true) - keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) - max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 - connect-timeout-seconds: 10 # 连接超时,单位:秒 - # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! - # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true - trust-all: true # 在 dev 环境可以设为 true - # 遗嘱消息配置 (用于网关异常下线时通知其他系统) - will: - enabled: true # 生产环境强烈建议开启 - topic: "gateway/status/${yudao.iot.gateway.emqx.mqtt-client-id}" # 遗嘱消息主题 - 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: + ssl-enabled: false # ==================================== # 针对引入的 TCP 组件的配置 # ==================================== - tcp: + - id: tcp-json enabled: false + protocol: tcp port: 8091 - keep-alive-timeout-ms: 30000 - max-connections: 1000 - ssl-enabled: false - ssl-cert-path: "classpath:certs/client.jks" - ssl-key-path: "classpath:certs/client.jks" + serialize: json + tcp: + max-connections: 1000 + keep-alive-timeout-ms: 30000 + ssl-enabled: false + codec: + type: delimiter # 拆包类型:length_field / delimiter / fixed_length + delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符) +# type: length_field # 拆包类型:length_field / delimiter / fixed_length +# length-field-offset: 0 # 长度字段偏移量 +# length-field-length: 4 # 长度字段长度 +# length-adjustment: 0 # 长度调整值 +# initial-bytes-to-strip: 4 # 初始跳过的字节数 +# type: fixed_length # 拆包类型:length_field / delimiter / fixed_length +# fixed-length: 256 # 固定长度 # ==================================== # 针对引入的 UDP 组件的配置 # ==================================== - udp: - enabled: false # 是否启用 UDP - port: 8093 # UDP 服务端口 - receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB) - send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB) - session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒) - session-clean-interval-ms: 30000 # 会话清理间隔(毫秒,默认 30 秒) + - id: udp-json + enabled: false + protocol: udp + port: 8093 + serialize: json + udp: + max-sessions: 1000 # 最大会话数 + session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期 + receive-buffer-size: 65536 # 接收缓冲区大小(字节) + send-buffer-size: 65536 # 发送缓冲区大小(字节) # ==================================== - # 针对引入的 MQTT 组件的配置 + # 针对引入的 WebSocket 组件的配置 # ==================================== - mqtt: + - id: websocket-json enabled: false - port: 1883 - max-message-size: 8192 - connect-timeout-seconds: 60 - ssl-enabled: false + protocol: websocket + port: 8094 + serialize: json + websocket: + path: /ws + max-message-size: 65536 # 最大消息大小(字节,默认 64KB) + max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) + idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) + ssl-enabled: false # 是否启用 SSL(wss://) # ==================================== # 针对引入的 CoAP 组件的配置 # ==================================== - coap: - enabled: false # 是否启用 CoAP 协议 - port: 5683 # CoAP 服务端口(默认 5683) - max-message-size: 1024 # 最大消息大小(字节) - ack-timeout: 2000 # ACK 超时时间(毫秒) - max-retransmit: 4 # 最大重传次数 + - id: coap-json + enabled: false + protocol: coap + port: 5683 + coap: + max-message-size: 1024 # 最大消息大小(字节) + ack-timeout-ms: 2000 # ACK 超时时间(毫秒) + max-retransmit: 4 # 最大重传次数 # ==================================== - # 针对引入的 WebSocket 组件的配置 + # 针对引入的 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 组件的配置 # ==================================== - websocket: - enabled: false # 是否启用 WebSocket 协议 - port: 8094 # WebSocket 服务端口(默认 8094) - path: /ws # WebSocket 路径(默认 /ws) - max-message-size: 65536 # 最大消息大小(字节,默认 64KB) - max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) - idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) - ssl-enabled: false # 是否启用 SSL(wss://) + - 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 证书库密码 --- #################### 日志相关配置 #################### @@ -153,7 +187,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 583763e..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,7 +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.gateway.protocol.coap.util.IotCoapUtils; +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; @@ -23,6 +23,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 直连设备 CoAP 协议集成测试(手动测试) * @@ -134,7 +136,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -177,7 +179,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -202,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/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java index ca581cb..f350325 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java @@ -13,7 +13,6 @@ 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.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -30,6 +29,8 @@ import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.Map; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 网关设备 CoAP 协议集成测试(手动测试) * @@ -158,7 +159,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -201,7 +202,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -242,7 +243,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -289,7 +290,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -362,7 +363,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java index 7aed8ec..4d909a2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 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.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -22,6 +21,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 网关子设备 CoAP 协议集成测试(手动测试) * @@ -137,7 +138,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -185,7 +186,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 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 8dd36cc..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 @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -11,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; @@ -92,9 +92,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") @@ -126,9 +124,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") .put("params", IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), @@ -163,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { // 1.1 构建请求 String url = String.format("http://%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][请求 URL: {}]", url); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java index 354c4d6..779c588 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -20,7 +19,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.List; import java.util.Map; @@ -121,9 +119,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.4 输出请求 @@ -155,9 +151,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.3 输出请求 @@ -187,9 +181,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { // 1.2 构建请求参数(目前为空,预留扩展) IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.3 输出请求 @@ -208,8 +200,6 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { // ===================== 子设备注册测试 ===================== - // TODO @芋艿:待测试 - /** * 子设备动态注册测试 *

@@ -227,9 +217,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); subDevice.setDeviceName("mougezishebei"); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) - .put("version", "1.0") .put("params", Collections.singletonList(subDevice)) .build()); // 1.3 输出请求 @@ -263,9 +251,9 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { .put("temperature", 25.5) .build(); // 1.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); + 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(); @@ -274,26 +262,24 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { .put("power", 100) .build(); // 1.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); Map subDeviceEvents = MapUtil.builder() .put("healthCheck", subDeviceEvent) .build(); // 1.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); // 1.7 构建请求参数 IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.8 输出请求 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java index cfebdbe..f6b9399 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -94,9 +93,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") @@ -130,9 +127,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") .put("params", IotDeviceEventPostReqDTO.of( "alarm", MapUtil.builder() 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 4b6936c..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 @@ -1,8 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.HexUtil; -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; @@ -10,32 +8,35 @@ 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.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +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; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 直连设备 TCP 协议集成测试(手动测试) * *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台 * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * *

使用步骤: *

    *
  1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. *
  5. 运行以下测试方法: *
      *
    • {@link #testAuth()} - 设备认证
    • @@ -58,10 +59,31 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== -// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); - private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) + ); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -69,6 +91,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final String DEVICE_NAME = "small"; private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -76,28 +117,21 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); + + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -112,29 +146,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-tcp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } + // 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(deviceName) + .setSign(sign); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); + + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } finally { + socket.close(); } } @@ -145,35 +175,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPost][认证响应: {}]", authResponse); - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } + .build())); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -184,95 +204,87 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testEventPost][认证响应: {}]", authResponse); - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } + System.currentTimeMillis())); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** * 执行设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]", - responseBytes.length, - String.format("%02X", responseBytes[0]), - HexUtil.encodeHexStr(responseBytes)); - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应 * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器 + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码 + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java index b417ceb..5bb113b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java @@ -13,35 +13,36 @@ 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.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +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; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; import java.util.Collections; -import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关设备 TCP 协议集成测试(手动测试) * *

      测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 TCP 协议管理子设备拓扑关系 * - *

      支持两种编解码格式: - *

        - *
      • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
      • - *
      • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
      • - *
      - * *

      使用步骤: *

        *
      1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
      2. - *
      3. 修改 {@link #CODEC} 选择测试的编解码格式
      4. *
      5. 运行以下测试方法: *
          *
        • {@link #testAuth()} - 网关设备认证
        • @@ -66,10 +67,31 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) + ); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -83,6 +105,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { private static final String SUB_DEVICE_NAME = "chazuo-it"; private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -90,29 +131,22 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); + + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -123,9 +157,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoAdd() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoAdd][认证响应: {}]", authResponse); @@ -140,24 +173,16 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { // 2.2 构建请求参数 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( + IotDeviceMessage request = IotDeviceMessage.requestOf( IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } + params); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoAdd][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -166,35 +191,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoDelete() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoDelete][认证响应: {}]", authResponse); - // 2.1 构建请求参数 + // 2. 构建请求参数 IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } + params); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoDelete][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -203,33 +218,23 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoGet() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoGet][认证响应: {}]", authResponse); - // 2.1 构建请求参数 + // 2. 构建请求参数 IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } + params); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoGet][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -240,35 +245,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testSubDeviceRegister() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testSubDeviceRegister][认证响应: {}]", authResponse); - // 2.1 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建请求参数 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() + .setProductKey(SUB_DEVICE_PRODUCT_KEY) + .setDeviceName("mougezishebei"); + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } + Collections.singletonList(subDevice)); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -279,9 +274,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPackPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPackPost][认证响应: {}]", authResponse); @@ -291,9 +285,9 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { .put("temperature", 25.5) .build(); // 2.2 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); + 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(); @@ -302,97 +296,95 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { .put("power", 100) .build(); // 2.4 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); + 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(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + 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(); params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } + params); + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPackPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** * 执行网关设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑) * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑) + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码 + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", + SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java index c918b47..22b654a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java @@ -1,23 +1,31 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; 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.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +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; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关子设备 TCP 协议集成测试(手动测试) @@ -26,17 +34,10 @@ import java.net.Socket; * *

          重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 * - *

          支持两种编解码格式: - *

            - *
          • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
          • - *
          • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
          • - *
          - * *

          使用步骤: *

            *
          1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
          2. *
          3. 确保子设备已通过 {@link IotGatewayDeviceTcpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
          4. - *
          5. 修改 {@link #CODEC} 选择测试的编解码格式
          6. *
          7. 运行以下测试方法: *
              *
            • {@link #testAuth()} - 子设备认证
            • @@ -58,10 +59,31 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== + + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) + ); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -69,6 +91,25 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { private static final String DEVICE_NAME = "chazuo-it"; private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -76,28 +117,21 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); + + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -108,37 +142,27 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPost][认证响应: {}]", authResponse); - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") .put("temperature", 36.5) - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + .build())); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -149,16 +173,14 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testEventPost][认证响应: {}]", authResponse); - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceEventPostReqDTO.of( "alarm", @@ -168,78 +190,77 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { .put("threshold", 40) .put("current", 42) .build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + System.currentTimeMillis())); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } + + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** * 执行子设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑) * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑) + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码 + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", + SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md deleted file mode 100644 index d6b2b3f..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md +++ /dev/null @@ -1,193 +0,0 @@ -# TCP 二进制协议数据包格式说明 - -## 1. 协议概述 - -TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。 - -### 1.1 协议特点 - -- **高效传输**:完全二进制格式,减少数据传输量 -- **版本控制**:内置协议版本号,支持协议升级 -- **类型安全**:明确的消息类型标识 -- **简洁设计**:去除冗余字段,协议更加精简 -- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容 - -## 2. 协议格式 - -### 2.1 整体结构 - -``` -+--------+--------+--------+---------------------------+--------+--------+ -| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) | -+--------+--------+--------+---------------------------+--------+--------+ -| 消息 ID 长度(2字节) | 消息 ID (变长字符串) | -+--------+--------+--------+--------+--------+--------+--------+--------+ -| 方法名长度(2字节) | 方法名(变长字符串) | -+--------+--------+--------+--------+--------+--------+--------+--------+ -| 消息体数据(变长) | -+--------+--------+--------+--------+--------+--------+--------+--------+ -``` - -### 2.2 字段详细说明 - -| 字段 | 长度 | 类型 | 说明 | -|------|------|------|------| -| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 | -| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 | -| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 | -| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) | -| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 | -| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) | -| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 | -| 方法名 | 变长 | string | 消息方法名(UTF-8编码) | -| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 | - -**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 - -### 2.3 协议常量定义 - -```java -// 协议标识 -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; // 固定头部长度 -private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度 -``` - -## 3. 消息类型和格式 - -### 3.1 请求消息 (REQUEST - 0x01) - -请求消息用于设备向服务器发送数据或请求。 - -#### 3.1.1 消息体格式 -``` -消息体 = params 数据(JSON格式) -``` - -#### 3.1.2 示例:设备认证请求 - -**消息内容:** -- 消息 ID: `auth_1704067200000_123` -- 方法名: `auth` -- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}` - -**二进制数据包结构:** -``` -7E // 魔术字 (0x7E) -01 // 版本号 (0x01) -01 // 消息类型 (REQUEST) -00 00 00 89 // 消息长度 (137字节) -00 19 // 消息 ID 长度 (25字节) -61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123" -36 37 32 30 30 30 30 30 5F 31 -32 33 -00 04 // 方法名长度 (4字节) -61 75 74 68 // 方法名: "auth" -7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据 -22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001", -30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName", -6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"} -64 75 63 74 4B 65 79 5F 64 65 -76 69 63 65 4E 61 6D 65 22 2C -22 70 61 73 73 77 6F 72 64 22 -3A 22 64 65 76 69 63 65 5F 70 -61 73 73 77 6F 72 64 22 7D -``` - -#### 3.1.3 示例:属性数据上报 - -**消息内容:** -- 消息 ID: `property_1704067200000_456` -- 方法名: `thing.property.post` -- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}` - -### 3.2 响应消息 (RESPONSE - 0x02) - -响应消息用于服务器向设备回复请求结果。 - -#### 3.2.1 消息体格式 -``` -消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON) -``` - -#### 3.2.2 字段说明 - -| 字段 | 长度 | 类型 | 说明 | -|------|------|------|------| -| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 | -| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 | -| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) | -| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) | - -#### 3.2.3 示例:认证成功响应 - -**消息内容:** -- 消息 ID: `auth_response_1704067200000_123` -- 方法名: `auth` -- 响应码: `0` -- 响应消息: `认证成功` -- 响应数据: `{"success":true,"message":"认证成功"}` - -**二进制数据包结构:** -``` -7E // 魔术字 (0x7E) -01 // 版本号 (0x01) -02 // 消息类型 (RESPONSE) -00 00 00 A4 // 消息长度 (164字节) -00 22 // 消息 ID 长度 (34字节) -61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123" -6E 73 65 5F 31 37 30 34 30 36 -37 32 30 30 30 30 30 5F 31 32 -33 -00 04 // 方法名长度 (4字节) -61 75 74 68 // 方法名: "auth" -00 00 00 00 // 响应码 (0 = 成功) -00 0C // 响应消息长度 (12字节) -E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8) -8A 9F -7B 22 73 75 63 63 65 73 73 22 // JSON响应数据 -3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"} -73 61 67 65 22 3A 22 E8 AE A4 -E8 AF 81 E6 88 90 E5 8A 9F 22 -7D -``` - -## 4. 编解码器标识 - -```java -public static final String TYPE = "TCP_BINARY"; -``` - -## 5. 协议优势 - -- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量 -- **解析高效**:直接二进制操作,减少字符串转换开销 -- **类型安全**:明确的消息类型和字段定义 -- **设计简洁**:去除冗余字段,协议更加精简高效 -- **版本控制**:内置版本号支持协议升级 - -## 6. 与 JSON 协议对比 - -| 特性 | 二进制协议 | JSON协议 | -|------|-------------|--------| -| 数据大小 | 小(节省30-50%) | 大 | -| 解析性能 | 高 | 中等 | -| 网络开销 | 低 | 高 | -| 可读性 | 差 | 优秀 | -| 调试难度 | 高 | 低 | -| 扩展性 | 良好 | 优秀 | - -**推荐场景**: -- ✅ **高频数据传输**:传感器数据实时上报 -- ✅ **带宽受限环境**:移动网络、卫星通信 -- ✅ **性能要求高**:需要低延迟、高吞吐的场景 -- ✅ **设备资源有限**:嵌入式设备、低功耗设备 -- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议 -- ❌ **快速原型开发**:开发效率低 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md deleted file mode 100644 index 09ef50c..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md +++ /dev/null @@ -1,191 +0,0 @@ -# TCP JSON 格式协议说明 - -## 1. 协议概述 - -TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点: - -- **标准化**:使用标准 JSON 格式,易于解析和处理 -- **可读性**:人类可读,便于调试和维护 -- **扩展性**:可以轻松添加新字段,向后兼容 -- **跨平台**:JSON 格式支持所有主流编程语言 -- **安全优化**:移除冗余的 deviceId 字段,提高安全性 - -## 2. 消息格式 - -### 2.1 基础消息结构 - -```json -{ - "id": "消息唯一标识", - "method": "消息方法", - "params": { - // 请求参数 - }, - "data": { - // 响应数据 - }, - "code": 响应码, - "msg": "响应消息", - "timestamp": 时间戳 -} -``` - -**⚠️ 重要说明**: -- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID -- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息 - -### 2.2 字段详细说明 - -| 字段名 | 类型 | 必填 | 用途 | 说明 | -|--------|------|------|------|------| -| id | String | 是 | 所有消息 | 消息唯一标识 | -| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` | -| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 | -| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 | -| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 | -| msg | String | 否 | 响应消息 | 响应提示信息 | -| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 | - -### 2.3 消息分类 - -#### 2.3.1 请求消息(上行) -- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段 -- **方向**:设备 → 服务器 -- **用途**:设备认证、数据上报、状态更新等 - -#### 2.3.2 响应消息(下行) -- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段 -- **方向**:服务器 → 设备 -- **用途**:认证结果、指令响应、错误提示等 - -## 3. 消息示例 - -### 3.1 设备认证 (auth) - -#### 认证请求格式 -**消息方向**:设备 → 服务器 - -```json -{ - "id": "auth_1704067200000_123", - "method": "auth", - "params": { - "clientId": "device_001", - "username": "productKey_deviceName", - "password": "设备密码" - }, - "timestamp": 1704067200000 -} -``` - -**认证参数说明:** - -| 字段名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| clientId | String | 是 | 客户端唯一标识,用于连接管理 | -| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | -| password | String | 是 | 设备密码,在设备管理平台配置 | - -#### 认证响应格式 -**消息方向**:服务器 → 设备 - -**认证成功响应:** -```json -{ - "id": "response_auth_1704067200000_123", - "method": "auth", - "data": { - "success": true, - "message": "认证成功" - }, - "code": 0, - "msg": "认证成功", - "timestamp": 1704067200001 -} -``` - -**认证失败响应:** -```json -{ - "id": "response_auth_1704067200000_123", - "method": "auth", - "data": { - "success": false, - "message": "认证失败:用户名或密码错误" - }, - "code": 401, - "msg": "认证失败", - "timestamp": 1704067200001 -} -``` - -### 3.2 属性数据上报 (thing.property.post) - -**消息方向**:设备 → 服务器 - -**示例:温度传感器数据上报** -```json -{ - "id": "property_1704067200000_456", - "method": "thing.property.post", - "params": { - "temperature": 25.5, - "humidity": 60.2, - "pressure": 1013.25, - "battery": 85, - "signal_strength": -65 - }, - "timestamp": 1704067200000 -} -``` - -### 3.3 设备状态更新 (thing.state.update) - -**消息方向**:设备 → 服务器 - -**示例:心跳请求** -```json -{ - "id": "heartbeat_1704067200000_321", - "method": "thing.state.update", - "params": { - "state": "online", - "uptime": 86400, - "memory_usage": 65.2, - "cpu_usage": 12.8 - }, - "timestamp": 1704067200000 -} -``` - -## 4. 编解码器标识 - -```java -public static final String TYPE = "TCP_JSON"; -``` - -## 5. 协议优势 - -- **开发效率高**:JSON 格式,开发和调试简单 -- **跨语言支持**:所有主流语言都支持 JSON -- **可读性优秀**:可以直接查看消息内容 -- **扩展性强**:可以轻松添加新字段 -- **安全性高**:移除 deviceId 字段,防止伪造攻击 - -## 6. 与二进制协议对比 - -| 特性 | JSON协议 | 二进制协议 | -|------|----------|------------| -| 开发难度 | 低 | 高 | -| 调试难度 | 低 | 高 | -| 可读性 | 优秀 | 差 | -| 数据大小 | 中等 | 小(节省30-50%) | -| 解析性能 | 中等 | 高 | -| 学习成本 | 低 | 高 | - -**推荐场景**: -- ✅ **开发调试阶段**:调试友好,开发效率高 -- ✅ **快速原型开发**:实现简单,快速迭代 -- ✅ **多语言集成**:广泛的语言支持 -- ❌ **高频数据传输**:建议使用二进制协议 -- ❌ **带宽受限环境**:建议使用二进制协议 \ No newline at end of file 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 9d507cc..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 @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; 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; @@ -9,9 +8,9 @@ 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.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +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; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,16 +26,9 @@ import java.util.Map; * *

              测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 * - *

              支持两种编解码格式: - *

                - *
              • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
              • - *
              • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
              • - *
              - * *

              使用步骤: *

                *
              1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
              2. - *
              3. 修改 {@link #CODEC} 选择测试的编解码格式
              4. *
              5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
              6. *
              7. 运行以下测试方法: *
                  @@ -58,10 +50,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -72,7 +66,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { /** * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo"; + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc3MDUyNTA0MywiZGV2aWNlTmFtZSI6InNtYWxsIn0.W9Mo-Oe1ZNLDkINndKieUeW1XhDzhVp0W0zTAwO6hJM"; // ===================== 认证测试 ===================== @@ -81,30 +75,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); } // ===================== 动态注册测试 ===================== @@ -118,30 +100,21 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } - } + // 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(deviceName) + .setSign(sign); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); + + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } // ===================== 直连设备属性上报测试 ===================== @@ -151,31 +124,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + .build()))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPost][响应消息: {}]", response); } // ===================== 直连设备事件上报测试 ===================== @@ -185,31 +144,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + System.currentTimeMillis()))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testEventPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== @@ -232,30 +177,36 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { } /** - * 发送 UDP 请求并接收响应 + * 发送 UDP 消息并接收响应 * - * @param socket UDP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param request 请求消息 + * @return 响应消息 */ - public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception { - InetAddress address = InetAddress.getByName(SERVER_HOST); - - // 发送请求 - DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); - socket.send(sendPacket); + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + // 1. 序列化请求 + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); - // 接收响应 - byte[] receiveData = new byte[4096]; - DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); - try { - socket.receive(receivePacket); - byte[] response = new byte[receivePacket.getLength()]; - System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength()); - return response; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; + // 2. 发送请求 + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + // 3. 接收响应 + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", responseBytes.length); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java index 20c2933..0acdeae 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; 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,36 +12,27 @@ 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.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.InetAddress; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - /** * IoT 网关设备 UDP 协议集成测试(手动测试) * *

                  测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 * - *

                  支持两种编解码格式: - *

                    - *
                  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                  • - *
                  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                  • - *
                  - * *

                  使用步骤: *

                    *
                  1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                  2. - *
                  3. 修改 {@link #CODEC} 选择测试的编解码格式
                  4. *
                  5. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
                  6. *
                  7. 运行以下测试方法: *
                      @@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd @Disabled public class IotGatewayDeviceUdpProtocolIntegrationTest { + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); } // ===================== 拓扑管理测试 ===================== /** * 添加子设备拓扑关系测试 - *

                      - * 网关设备向平台上报需要绑定的子设备信息 */ @Test public void testTopoAdd() throws Exception { - // 1.1 构建子设备认证信息 + // 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()); - // 1.2 构建请求参数 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - withToken(params), - null, null, null); - // 1.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } - } + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), withToken(params)); + + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoAdd][响应消息: {}]", response); } /** * 删除子设备拓扑关系测试 - *

                      - * 网关设备向平台上报需要解绑的子设备信息 */ @Test public void testTopoDelete() throws Exception { - // 1.1 构建请求参数 + // 1. 构建请求参数 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(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } - } + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), withToken(params)); + + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoDelete][响应消息: {}]", response); } /** * 获取子设备拓扑关系测试 - *

                      - * 网关设备向平台查询已绑定的子设备列表 */ @Test public void testTopoGet() throws Exception { - // 1.1 构建请求参数(目前为空,预留扩展) + // 1. 构建请求参数 IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } - } + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), withToken(params)); + + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoGet][响应消息: {}]", response); } // ===================== 子设备注册测试 ===================== /** * 子设备动态注册测试 - *

                      - * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

                      - * 注意:此接口需要网关 Token 认证 */ @Test public void testSubDeviceRegister() throws Exception { - // 1.1 构建请求参数 + // 1. 构建请求参数 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - withToken(Collections.singletonList(subDevice)), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + withToken(Collections.singletonList(subDevice))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); } // ===================== 批量上报测试 ===================== /** * 批量上报属性测试(网关 + 子设备) - *

                      - * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 */ @Test public void testPropertyPackPost() throws Exception { @@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - withToken(params), - null, null, null); - // 1.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } - } + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), withToken(params)); + + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPackPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== /** * 构建带 token 的 params - *

                      - * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map */ private Map withToken(Object params) { Map result = new HashMap<>(); @@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { return result; } + /** + * 发送 UDP 消息并接收响应 + */ + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java index 100c276..fe7f7f8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java @@ -1,26 +1,24 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; 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.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.InetAddress; import java.util.HashMap; import java.util.Map; -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - /** * IoT 网关子设备 UDP 协议集成测试(手动测试) * @@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd *

                      重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 *

                      网关设备转发子设备请求时,Token 使用子设备自己的信息。 * - *

                      支持两种编解码格式: - *

                        - *
                      • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                      • - *
                      • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                      • - *
                      - * *

                      使用步骤: *

                        *
                      1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                      2. *
                      3. 确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
                      4. - *
                      5. 修改 {@link #CODEC} 选择测试的编解码格式
                      6. *
                      7. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
                      8. *
                      9. 运行以下测试方法: *
                          @@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd @Disabled public class IotGatewaySubDeviceUdpProtocolIntegrationTest { + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); } // ===================== 子设备属性上报测试 ===================== @@ -115,33 +98,19 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") .put("temperature", 36.5) - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + .build()))); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPost][响应消息: {}]", response); } // ===================== 子设备事件上报测试 ===================== @@ -151,9 +120,8 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( "alarm", @@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { .put("threshold", 40) .put("current", 42) .build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + System.currentTimeMillis()))); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testEventPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== /** * 构建带 token 的 params - *

                          - * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map */ private Map withToken(Object params) { Map result = new HashMap<>(); @@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { return result; } + /** + * 发送 UDP 消息并接收响应 + */ + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } + } + } + } 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 ca79c42..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,8 +10,9 @@ 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.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -61,7 +62,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // ===================== 编解码器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -95,10 +96,10 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -109,7 +110,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -131,16 +132,19 @@ 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 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -151,7 +155,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testDeviceRegister][响应消息: {}]", responseMessage); log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } else { @@ -186,16 +190,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .put("height", "2") .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -229,16 +233,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { MapUtil.builder().put("rice", 3).build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testEventPost][响应消息: {}]", responseMessage); } else { log.warn("[testEventPost][未收到响应]"); @@ -308,13 +312,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java index a44f2f6..20d66fa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java @@ -14,8 +14,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.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -67,9 +67,9 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器选择 ===================== + // ===================== 序列化器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -110,10 +110,10 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -124,7 +124,7 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -164,16 +164,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), params, null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); + // 2.3 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoAdd][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoAdd][响应消息: {}]", responseMessage); } else { log.warn("[testTopoAdd][未收到响应]"); @@ -205,16 +205,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), params, null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoDelete][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoDelete][响应消息: {}]", responseMessage); } else { log.warn("[testTopoDelete][未收到响应]"); @@ -244,16 +244,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), params, null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoGet][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoGet][响应消息: {}]", responseMessage); } else { log.warn("[testTopoGet][未收到响应]"); @@ -287,16 +287,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), Collections.singletonList(subDevice), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testSubDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage); } else { log.warn("[testSubDeviceRegister][未收到响应]"); @@ -358,16 +358,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), params, null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); + // 2.7 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPackPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPackPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPackPost][未收到响应]"); @@ -438,13 +438,13 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java index 04bf3d5..f792288 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java @@ -9,8 +9,8 @@ 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.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -60,9 +60,9 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器选择 ===================== + // ===================== 序列化器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -96,10 +96,10 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -110,7 +110,7 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -146,16 +146,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .put("temperature", 36.5) .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -195,16 +195,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testEventPost][响应消息: {}]", responseMessage); } else { log.warn("[testEventPost][未收到响应]"); @@ -274,13 +274,13 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; }