Commit ff2618bbf1f29b107936936ff9a420d7b83e4b8e

Authored by 芋道源码
Committed by Gitee
2 parents f4ba6e75 eb9d27ae

!1513 feat(iot):【协议改造】http、websocket、tcp、udp、coap、mqtt 初步改造

Merge pull request !1513 from 芋道源码/feature/iot-protocol
Showing 138 changed files with 7708 additions and 7679 deletions

Too many changes.

To preserve performance only 100 of 138 files are displayed.

yudao-module-iot/yudao-module-iot-biz/pom.xml
... ... @@ -101,18 +101,6 @@
101 101 <artifactId>spring-boot-starter-amqp</artifactId>
102 102 <optional>true</optional>
103 103 </dependency>
104   -
105   - <!-- IoT 网络组件:接收来自设备的上行数据 -->
106   - <!-- <dependency>-->
107   - <!-- <groupId>cn.iocoder.boot</groupId>-->
108   - <!-- <artifactId>yudao-module-iot-net-component-http</artifactId>-->
109   - <!-- <version>${revision}</version>-->
110   - <!-- </dependency>-->
111   - <!-- <dependency>-->
112   - <!-- <groupId>cn.iocoder.boot</groupId>-->
113   - <!-- <artifactId>yudao-module-iot-net-component-emqx</artifactId>-->
114   - <!-- <version>${revision}</version>-->
115   - <!-- </dependency>-->
116 104 </dependencies>
117 105  
118 106 </project>
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
... ... @@ -58,7 +58,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
58 58 return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
59 59 IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
60 60 if (product != null) {
61   - deviceDTO.setCodecType(product.getCodecType());
  61 + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType());
62 62 }
63 63 }));
64 64 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
... ... @@ -67,10 +67,15 @@ public class IotProductRespVO {
67 67 @DictFormat(DictTypeConstants.NET_TYPE)
68 68 private Integer netType;
69 69  
70   - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
71   - @ExcelProperty(value = "数据格式", converter = DictConvert.class)
72   - @DictFormat(DictTypeConstants.CODEC_TYPE)
73   - private String codecType;
  70 + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
  71 + @ExcelProperty(value = "协议类型", converter = DictConvert.class)
  72 + @DictFormat(DictTypeConstants.PROTOCOL_TYPE)
  73 + private String protocolType;
  74 +
  75 + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
  76 + @ExcelProperty(value = "序列化类型", converter = DictConvert.class)
  77 + @DictFormat(DictTypeConstants.SERIALIZE_TYPE)
  78 + private String serializeType;
74 79  
75 80 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
76 81 @ExcelProperty("创建时间")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
4 6 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
5 7 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
6 8 import io.swagger.v3.oas.annotations.media.Schema;
... ... @@ -44,9 +46,15 @@ public class IotProductSaveReqVO {
44 46 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
45 47 private Integer netType;
46 48  
47   - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
48   - @NotEmpty(message = "数据格式不能为空")
49   - private String codecType;
  49 + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
  50 + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}")
  51 + @NotEmpty(message = "协议类型不能为空")
  52 + private String protocolType;
  53 +
  54 + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
  55 + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}")
  56 + @NotEmpty(message = "序列化类型不能为空")
  57 + private String serializeType;
50 58  
51 59 @Schema(description = "是否开启动态注册", example = "false")
52 60 @NotNull(message = "是否开启动态注册不能为空")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
... ... @@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO {
78 78 */
79 79 private Integer netType;
80 80 /**
81   - * 数据格式(编解码器类型)
  81 + * 协议类型
82 82 * <p>
83   - * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE}
84   - *
85   - * 目的:用于 gateway-server 解析消息格式
  83 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum}
86 84 */
87   - private String codecType;
  85 + private String protocolType;
  86 + /**
  87 + * 序列化类型
  88 + * <p>
  89 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum}
  90 + */
  91 + private String serializeType;
88 92  
89 93 }
90 94 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java
... ... @@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums;
8 8 public class DictTypeConstants {
9 9  
10 10 public static final String NET_TYPE = "iot_net_type";
11   - public static final String LOCATION_TYPE = "iot_location_type";
12   - public static final String CODEC_TYPE = "iot_codec_type";
  11 + public static final String PROTOCOL_TYPE = "iot_protocol_type";
  12 + public static final String SERIALIZE_TYPE = "iot_serialize_type";
13 13  
14 14 public static final String PRODUCT_STATUS = "iot_product_status";
15 15 public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type";
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
... ... @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
29 29 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
30 30 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
31 31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  32 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
32 33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
33 34 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
34 35 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
... ... @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
819 820 if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
820 821 throw exception(DEVICE_REGISTER_DISABLED);
821 822 }
822   - // 1.3 验证 productSecret
823   - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
  823 + // 1.3 【重要!!!】验证签名
  824 + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(),
  825 + product.getProductSecret(), reqDTO.getSign())) {
824 826 throw exception(DEVICE_REGISTER_SECRET_INVALID);
825 827 }
826 828 return TenantUtils.execute(product.getTenantId(), () -> {
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java
... ... @@ -34,8 +34,12 @@ public class IotDeviceRespDTO {
34 34 */
35 35 private Long productId;
36 36 /**
37   - * 编解码器类型
  37 + * 协议类型
38 38 */
39   - private String codecType;
  39 + private String protocolType;
  40 + /**
  41 + * 序列化类型
  42 + */
  43 + private String serializeType;
40 44  
41 45 }
42 46 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.Getter;
  6 +import lombok.RequiredArgsConstructor;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * IoT 协议类型枚举
  12 + *
  13 + * 用于定义传输层协议类型
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@RequiredArgsConstructor
  18 +@Getter
  19 +public enum IotProtocolTypeEnum implements ArrayValuable<String> {
  20 +
  21 + TCP("tcp"),
  22 + UDP("udp"),
  23 + WEBSOCKET("websocket"),
  24 + HTTP("http"),
  25 + MQTT("mqtt"),
  26 + EMQX("emqx"),
  27 + COAP("coap"),
  28 + MODBUS_TCP("modbus_tcp");
  29 +
  30 + public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);
  31 +
  32 + /**
  33 + * 类型
  34 + */
  35 + private final String type;
  36 +
  37 + @Override
  38 + public String[] array() {
  39 + return ARRAYS;
  40 + }
  41 +
  42 + public static IotProtocolTypeEnum of(String type) {
  43 + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values());
  44 + }
  45 +
  46 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.Getter;
  6 +import lombok.RequiredArgsConstructor;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * IoT 序列化类型枚举
  12 + *
  13 + * 用于定义设备消息的序列化格式
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@RequiredArgsConstructor
  18 +@Getter
  19 +public enum IotSerializeTypeEnum implements ArrayValuable<String> {
  20 +
  21 + JSON("json"),
  22 + BINARY("binary");
  23 +
  24 + public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new);
  25 +
  26 + /**
  27 + * 类型
  28 + */
  29 + private final String type;
  30 +
  31 + @Override
  32 + public String[] array() {
  33 + return ARRAYS;
  34 + }
  35 +
  36 + public static IotSerializeTypeEnum of(String type) {
  37 + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values());
  38 + }
  39 +
  40 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java
... ... @@ -24,4 +24,14 @@ public interface IotMessageBus {
24 24 */
25 25 void register(IotMessageSubscriber<?> subscriber);
26 26  
  27 + /**
  28 + * 取消注册消息订阅者
  29 + *
  30 + * @param subscriber 订阅者
  31 + */
  32 + default void unregister(IotMessageSubscriber<?> subscriber) {
  33 + // TODO 芋艿:暂时不实现,需求量不大,但是
  34 + // throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现");
  35 + }
  36 +
27 37 }
28 38 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java
... ... @@ -26,4 +26,16 @@ public interface IotMessageSubscriber&lt;T&gt; {
26 26 */
27 27 void onMessage(T message);
28 28  
  29 + /**
  30 + * 启动订阅
  31 + */
  32 + default void start() {
  33 + }
  34 +
  35 + /**
  36 + * 停止订阅
  37 + */
  38 + default void stop() {
  39 + }
  40 +
29 41 }
30 42 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
... ... @@ -60,7 +60,7 @@ public class IotDeviceMessage {
60 60 */
61 61 private String serverId;
62 62  
63   - // ========== codec(编解码)字段 ==========
  63 + // ========== serialize(序列化)相关字段 ==========
64 64  
65 65 /**
66 66 * 请求编号
... ... @@ -94,7 +94,7 @@ public class IotDeviceMessage {
94 94 */
95 95 private String msg;
96 96  
97   - // ========== 基础方法:只传递"codec(编解码)字段" ==========
  97 + // ========== 基础方法:只传递"serialize(序列化)相关字段" ==========
98 98  
99 99 public static IotDeviceMessage requestOf(String method) {
100 100 return requestOf(null, method, null);
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
... ... @@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO {
27 27 private String deviceName;
28 28  
29 29 /**
30   - * 产品密钥
  30 + * 注册签名
  31 + *
  32 + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
31 33 */
32   - @NotEmpty(message = "产品密钥不能为空")
33   - private String productSecret;
  34 + @NotEmpty(message = "签名不能为空")
  35 + private String sign;
34 36  
35 37 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.crypto.digest.DigestUtil;
  5 +import cn.hutool.crypto.digest.HmacAlgorithm;
  6 +
  7 +/**
  8 + * IoT 产品【动态注册】认证工具类
  9 + * <p>
  10 + * 用于一型一密场景,使用 productSecret 生成签名
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class IotProductAuthUtils {
  15 +
  16 + /**
  17 + * 生成设备动态注册签名
  18 + *
  19 + * @param productKey 产品标识
  20 + * @param deviceName 设备名称
  21 + * @param productSecret 产品密钥
  22 + * @return 签名
  23 + */
  24 + public static String buildSign(String productKey, String deviceName, String productSecret) {
  25 + String content = buildContent(productKey, deviceName);
  26 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
  27 + .digestHex(content);
  28 + }
  29 +
  30 + /**
  31 + * 验证设备动态注册签名
  32 + *
  33 + * @param productKey 产品标识
  34 + * @param deviceName 设备名称
  35 + * @param productSecret 产品密钥
  36 + * @param sign 待验证的签名
  37 + * @return 是否验证通过
  38 + */
  39 + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
  40 + String expectedSign = buildSign(productKey, deviceName, productSecret);
  41 + return expectedSign.equals(sign);
  42 + }
  43 +
  44 + /**
  45 + * 构建签名内容
  46 + *
  47 + * @param productKey 产品标识
  48 + * @param deviceName 设备名称
  49 + * @return 签名内容
  50 + */
  51 + private static String buildContent(String productKey, String deviceName) {
  52 + return "deviceName" + deviceName + "productKey" + productKey;
  53 + }
  54 +
  55 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.codec.alink;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
8   -import lombok.AllArgsConstructor;
9   -import lombok.Data;
10   -import lombok.NoArgsConstructor;
11   -import org.springframework.stereotype.Component;
12   -
13   -/**
14   - * 阿里云 Alink {@link IotDeviceMessage} 的编解码器
15   - *
16   - * @author 芋道源码
17   - */
18   -@Component
19   -public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
20   -
21   - public static final String TYPE = "Alink";
22   -
23   - @Data
24   - @NoArgsConstructor
25   - @AllArgsConstructor
26   - private static class AlinkMessage {
27   -
28   - public static final String VERSION_1 = "1.0";
29   -
30   - /**
31   - * 消息 ID,且每个消息 ID 在当前设备具有唯一性
32   - */
33   - private String id;
34   -
35   - /**
36   - * 版本号
37   - */
38   - private String version;
39   -
40   - /**
41   - * 请求方法
42   - */
43   - private String method;
44   -
45   - /**
46   - * 请求参数
47   - */
48   - private Object params;
49   -
50   - /**
51   - * 响应结果
52   - */
53   - private Object data;
54   - /**
55   - * 响应错误码
56   - */
57   - private Integer code;
58   - /**
59   - * 响应提示
60   - *
61   - * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。
62   - */
63   - private String msg;
64   -
65   - }
66   -
67   - @Override
68   - public String type() {
69   - return TYPE;
70   - }
71   -
72   - @Override
73   - public byte[] encode(IotDeviceMessage message) {
74   - AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
75   - message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg());
76   - return JsonUtils.toJsonByte(alinkMessage);
77   - }
78   -
79   - @Override
80   - @SuppressWarnings("DataFlowIssue")
81   - public IotDeviceMessage decode(byte[] bytes) {
82   - AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class);
83   - Assert.notNull(alinkMessage, "消息不能为空");
84   - Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0");
85   - return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(),
86   - alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
87   - }
88   -
89   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java deleted
1   -/**
2   - * 提供设备接入的各种数据(请求、响应)的编解码
3   - */
4   -package cn.iocoder.yudao.module.iot.gateway.codec;
5 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java deleted
1   -/**
2   - * TODO @芋艿:实现一个 alink 的 xml 版本
3   - */
4   -package cn.iocoder.yudao.module.iot.gateway.codec.simple;
5 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
8   -import lombok.AllArgsConstructor;
9   -import lombok.Data;
10   -import lombok.NoArgsConstructor;
11   -import org.springframework.stereotype.Component;
12   -
13   -/**
14   - * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
15   - *
16   - * 采用纯 JSON 格式传输,格式如下:
17   - * {
18   - * "id": "消息 ID",
19   - * "method": "消息方法",
20   - * "params": {...}, // 请求参数
21   - * "data": {...}, // 响应结果
22   - * "code": 200, // 响应错误码
23   - * "msg": "success", // 响应提示
24   - * "timestamp": 时间戳
25   - * }
26   - *
27   - * @author 芋道源码
28   - */
29   -@Component
30   -public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
31   -
32   - public static final String TYPE = "TCP_JSON";
33   -
34   - @Data
35   - @NoArgsConstructor
36   - @AllArgsConstructor
37   - private static class TcpJsonMessage {
38   -
39   - /**
40   - * 消息 ID,且每个消息 ID 在当前设备具有唯一性
41   - */
42   - private String id;
43   -
44   - /**
45   - * 请求方法
46   - */
47   - private String method;
48   -
49   - /**
50   - * 请求参数
51   - */
52   - private Object params;
53   -
54   - /**
55   - * 响应结果
56   - */
57   - private Object data;
58   -
59   - /**
60   - * 响应错误码
61   - */
62   - private Integer code;
63   -
64   - /**
65   - * 响应提示
66   - */
67   - private String msg;
68   -
69   - /**
70   - * 时间戳
71   - */
72   - private Long timestamp;
73   -
74   - }
75   -
76   - @Override
77   - public String type() {
78   - return TYPE;
79   - }
80   -
81   - @Override
82   - public byte[] encode(IotDeviceMessage message) {
83   - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
84   - message.getRequestId(),
85   - message.getMethod(),
86   - message.getParams(),
87   - message.getData(),
88   - message.getCode(),
89   - message.getMsg(),
90   - System.currentTimeMillis());
91   - return JsonUtils.toJsonByte(tcpJsonMessage);
92   - }
93   -
94   - @Override
95   - @SuppressWarnings("DataFlowIssue")
96   - public IotDeviceMessage decode(byte[] bytes) {
97   - String jsonStr = StrUtil.utf8Str(bytes).trim();
98   - TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
99   - Assert.notNull(tcpJsonMessage, "消息不能为空");
100   - Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
101   - return IotDeviceMessage.of(
102   - tcpJsonMessage.getId(),
103   - tcpJsonMessage.getMethod(),
104   - tcpJsonMessage.getParams(),
105   - tcpJsonMessage.getData(),
106   - tcpJsonMessage.getCode(),
107   - tcpJsonMessage.getMsg());
108   - }
109   -
110   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
1 1 package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
11   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
16   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
17   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
18   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
19   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
20   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
21   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
22   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
23   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
24   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
25   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
26   -import io.vertx.core.Vertx;
27   -import lombok.extern.slf4j.Slf4j;
28   -import org.springframework.beans.factory.annotation.Qualifier;
29   -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
  4 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
30 5 import org.springframework.boot.context.properties.EnableConfigurationProperties;
31 6 import org.springframework.context.annotation.Bean;
32 7 import org.springframework.context.annotation.Configuration;
33 8  
34 9 @Configuration
35 10 @EnableConfigurationProperties(IotGatewayProperties.class)
36   -@Slf4j
37 11 public class IotGatewayConfiguration {
38 12  
39   - /**
40   - * IoT 网关 HTTP 协议配置类
41   - */
42   - @Configuration
43   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true")
44   - @Slf4j
45   - public static class HttpProtocolConfiguration {
46   -
47   - @Bean(name = "httpVertx", destroyMethod = "close")
48   - public Vertx httpVertx() {
49   - return Vertx.vertx();
50   - }
51   -
52   - @Bean
53   - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
54   - @Qualifier("httpVertx") Vertx httpVertx) {
55   - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
56   - }
57   -
58   - @Bean
59   - public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol,
60   - IotMessageBus messageBus) {
61   - return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus);
62   - }
63   - }
64   -
65   - /**
66   - * IoT 网关 EMQX 协议配置类
67   - */
68   - @Configuration
69   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
70   - @Slf4j
71   - public static class EmqxProtocolConfiguration {
72   -
73   - @Bean(name = "emqxVertx", destroyMethod = "close")
74   - public Vertx emqxVertx() {
75   - return Vertx.vertx();
76   - }
77   -
78   - @Bean
79   - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
80   - @Qualifier("emqxVertx") Vertx emqxVertx) {
81   - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
82   - }
83   -
84   - @Bean
85   - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
86   - @Qualifier("emqxVertx") Vertx emqxVertx) {
87   - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
88   - }
89   -
90   - @Bean
91   - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
92   - IotMessageBus messageBus) {
93   - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
94   - }
  13 + @Bean
  14 + public IotMessageSerializerManager iotMessageSerializerManager() {
  15 + return new IotMessageSerializerManager();
95 16 }
96 17  
97   - /**
98   - * IoT 网关 TCP 协议配置类
99   - */
100   - @Configuration
101   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true")
102   - @Slf4j
103   - public static class TcpProtocolConfiguration {
104   -
105   - @Bean(name = "tcpVertx", destroyMethod = "close")
106   - public Vertx tcpVertx() {
107   - return Vertx.vertx();
108   - }
109   -
110   - @Bean
111   - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
112   - IotDeviceService deviceService,
113   - IotDeviceMessageService messageService,
114   - IotTcpConnectionManager connectionManager,
115   - @Qualifier("tcpVertx") Vertx tcpVertx) {
116   - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
117   - deviceService, messageService, connectionManager, tcpVertx);
118   - }
119   -
120   - @Bean
121   - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
122   - IotDeviceMessageService messageService,
123   - IotTcpConnectionManager connectionManager,
124   - IotMessageBus messageBus) {
125   - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
126   - }
127   -
128   - }
129   -
130   - /**
131   - * IoT 网关 MQTT 协议配置类
132   - */
133   - @Configuration
134   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
135   - @Slf4j
136   - public static class MqttProtocolConfiguration {
137   -
138   - @Bean(name = "mqttVertx", destroyMethod = "close")
139   - public Vertx mqttVertx() {
140   - return Vertx.vertx();
141   - }
142   -
143   - @Bean
144   - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
145   - IotDeviceMessageService messageService,
146   - IotMqttConnectionManager connectionManager,
147   - @Qualifier("mqttVertx") Vertx mqttVertx) {
148   - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService,
149   - connectionManager, mqttVertx);
150   - }
151   -
152   - @Bean
153   - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService,
154   - IotMqttConnectionManager connectionManager) {
155   - return new IotMqttDownstreamHandler(messageService, connectionManager);
156   - }
157   -
158   - @Bean
159   - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol,
160   - IotMqttDownstreamHandler downstreamHandler,
161   - IotMessageBus messageBus) {
162   - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus);
163   - }
164   -
165   - }
166   -
167   - /**
168   - * IoT 网关 UDP 协议配置类
169   - */
170   - @Configuration
171   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
172   - @Slf4j
173   - public static class UdpProtocolConfiguration {
174   -
175   - @Bean(name = "udpVertx", destroyMethod = "close")
176   - public Vertx udpVertx() {
177   - return Vertx.vertx();
178   - }
179   -
180   - @Bean
181   - public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
182   - IotDeviceService deviceService,
183   - IotDeviceMessageService messageService,
184   - IotUdpSessionManager sessionManager,
185   - @Qualifier("udpVertx") Vertx udpVertx) {
186   - return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
187   - deviceService, messageService, sessionManager, udpVertx);
188   - }
189   -
190   - @Bean
191   - public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
192   - IotDeviceMessageService messageService,
193   - IotUdpSessionManager sessionManager,
194   - IotMessageBus messageBus) {
195   - return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
196   - }
197   -
198   - }
199   -
200   - /**
201   - * IoT 网关 CoAP 协议配置类
202   - */
203   - @Configuration
204   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
205   - @Slf4j
206   - public static class CoapProtocolConfiguration {
207   -
208   - @Bean
209   - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
210   - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
211   - }
212   -
213   - @Bean
214   - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
215   - IotMessageBus messageBus) {
216   - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
217   - }
218   -
219   - }
220   -
221   - /**
222   - * IoT 网关 WebSocket 协议配置类
223   - */
224   - @Configuration
225   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
226   - @Slf4j
227   - public static class WebSocketProtocolConfiguration {
228   -
229   - @Bean(name = "websocketVertx", destroyMethod = "close")
230   - public Vertx websocketVertx() {
231   - return Vertx.vertx();
232   - }
233   -
234   - @Bean
235   - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
236   - IotDeviceService deviceService,
237   - IotDeviceMessageService messageService,
238   - IotWebSocketConnectionManager connectionManager,
239   - @Qualifier("websocketVertx") Vertx websocketVertx) {
240   - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
241   - deviceService, messageService, connectionManager, websocketVertx);
242   - }
243   -
244   - @Bean
245   - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
246   - IotDeviceMessageService messageService,
247   - IotWebSocketConnectionManager connectionManager,
248   - IotMessageBus messageBus) {
249   - return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
250   - }
251   -
  18 + @Bean
  19 + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) {
  20 + return new IotProtocolManager(gatewayProperties);
252 21 }
253 22  
254 23 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
1 1 package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
  11 +import jakarta.validation.Valid;
3 12 import jakarta.validation.constraints.NotEmpty;
4 13 import jakarta.validation.constraints.NotNull;
5 14 import lombok.Data;
... ... @@ -24,9 +33,9 @@ public class IotGatewayProperties {
24 33 private TokenProperties token;
25 34  
26 35 /**
27   - * 协议配置
  36 + * 协议实例列表
28 37 */
29   - private ProtocolProperties protocol;
  38 + private List<ProtocolProperties> protocols;
30 39  
31 40 @Data
32 41 public static class RpcProperties {
... ... @@ -65,581 +74,145 @@ public class IotGatewayProperties {
65 74  
66 75 }
67 76  
  77 + /**
  78 + * 协议实例配置
  79 + */
68 80 @Data
69 81 public static class ProtocolProperties {
70 82  
71 83 /**
72   - * HTTP 组件配置
73   - */
74   - private HttpProperties http;
75   -
76   - /**
77   - * EMQX 组件配置
78   - */
79   - private EmqxProperties emqx;
80   -
81   - /**
82   - * TCP 组件配置
  84 + * 协议实例 ID,如 "http-alink"、"tcp-binary"
83 85 */
84   - private TcpProperties tcp;
85   -
86   - /**
87   - * MQTT 组件配置
88   - */
89   - private MqttProperties mqtt;
90   -
  86 + @NotEmpty(message = "协议实例 ID 不能为空")
  87 + private String id;
91 88 /**
92   - * MQTT WebSocket 组件配置
  89 + * 是否启用
93 90 */
94   - private MqttWsProperties mqttWs;
95   -
  91 + @NotNull(message = "是否启用不能为空")
  92 + private Boolean enabled = true;
96 93 /**
97   - * UDP 组件配置
  94 + * 协议类型
  95 + *
  96 + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
98 97 */
99   - private UdpProperties udp;
100   -
101   - /**
102   - * CoAP 组件配置
103   - */
104   - private CoapProperties coap;
105   -
106   - /**
107   - * WebSocket 组件配置
108   - */
109   - private WebSocketProperties websocket;
110   -
111   - }
112   -
113   - @Data
114   - public static class HttpProperties {
115   -
116   - /**
117   - * 是否开启
118   - */
119   - @NotNull(message = "是否开启不能为空")
120   - private Boolean enabled;
  98 + @NotEmpty(message = "协议类型不能为空")
  99 + private String protocol;
121 100 /**
122 101 * 服务端口
123   - */
124   - private Integer serverPort;
125   -
126   - /**
127   - * 是否开启 SSL
128   - */
129   - @NotNull(message = "是否开启 SSL 不能为空")
130   - private Boolean sslEnabled = false;
131   -
132   - /**
133   - * SSL 证书路径
134   - */
135   - private String sslKeyPath;
136   - /**
137   - * SSL 证书路径
138   - */
139   - private String sslCertPath;
140   -
141   - }
142   -
143   - @Data
144   - public static class EmqxProperties {
145   -
146   - /**
147   - * 是否开启
148   - */
149   - @NotNull(message = "是否开启不能为空")
150   - private Boolean enabled;
151   -
152   - /**
153   - * HTTP 服务端口(默认:8090)
154   - */
155   - private Integer httpPort = 8090;
156   -
157   - /**
158   - * MQTT 服务器地址
159   - */
160   - @NotEmpty(message = "MQTT 服务器地址不能为空")
161   - private String mqttHost;
162   -
163   - /**
164   - * MQTT 服务器端口(默认:1883)
165   - */
166   - @NotNull(message = "MQTT 服务器端口不能为空")
167   - private Integer mqttPort = 1883;
168   -
169   - /**
170   - * MQTT 用户名
171   - */
172   - @NotEmpty(message = "MQTT 用户名不能为空")
173   - private String mqttUsername;
174   -
175   - /**
176   - * MQTT 密码
177   - */
178   - @NotEmpty(message = "MQTT 密码不能为空")
179   - private String mqttPassword;
180   -
181   - /**
182   - * MQTT 客户端的 SSL 开关
183   - */
184   - @NotNull(message = "MQTT 是否开启 SSL 不能为空")
185   - private Boolean mqttSsl = false;
186   -
187   - /**
188   - * MQTT 客户端 ID(如果为空,系统将自动生成)
189   - */
190   - @NotEmpty(message = "MQTT 客户端 ID 不能为空")
191   - private String mqttClientId;
192   -
193   - /**
194   - * MQTT 订阅的主题
195   - */
196   - @NotEmpty(message = "MQTT 主题不能为空")
197   - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
198   -
199   - /**
200   - * 默认 QoS 级别
201 102 * <p>
202   - * 0 - 最多一次
203   - * 1 - 至少一次
204   - * 2 - 刚好一次
205   - */
206   - private Integer mqttQos = 1;
207   -
208   - /**
209   - * 连接超时时间(秒)
210   - */
211   - private Integer connectTimeoutSeconds = 10;
212   -
213   - /**
214   - * 重连延迟时间(毫秒)
215   - */
216   - private Long reconnectDelayMs = 5000L;
217   -
218   - /**
219   - * 是否启用 Clean Session (清理会话)
220   - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。
221   - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
222   - */
223   - private Boolean cleanSession = true;
224   -
225   - /**
226   - * 心跳间隔(秒)
227   - * 用于保持连接活性,及时发现网络中断。
228   - */
229   - private Integer keepAliveIntervalSeconds = 60;
230   -
231   - /**
232   - * 最大未确认消息队列大小
233   - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
234   - */
235   - private Integer maxInflightQueue = 10000;
236   -
237   - /**
238   - * 是否信任所有 SSL 证书
239   - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
240   - * 在生产环境中,应设置为 false,并配置正确的信任库。
241   - */
242   - private Boolean trustAll = false;
243   -
244   - /**
245   - * 遗嘱消息配置 (用于网关异常下线时通知其他系统)
246   - */
247   - private final Will will = new Will();
248   -
249   - /**
250   - * 高级 SSL/TLS 配置 (用于生产环境)
  103 + * 不同协议含义不同:
  104 + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口
  105 + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event)
251 106 */
252   - private final Ssl sslOptions = new Ssl();
253   -
  107 + @NotNull(message = "服务端口不能为空")
  108 + private Integer port;
254 109 /**
255   - * 遗嘱消息 (Last Will and Testament)
  110 + * 序列化类型(可选)
  111 + *
  112 + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
  113 + *
  114 + * 为什么是可选的呢?
  115 + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
  116 + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析
256 117 */
257   - @Data
258   - public static class Will {
259   -
260   - /**
261   - * 是否启用遗嘱消息
262   - */
263   - private boolean enabled = false;
264   - /**
265   - * 遗嘱消息主题
266   - */
267   - private String topic;
268   - /**
269   - * 遗嘱消息内容
270   - */
271   - private String payload;
272   - /**
273   - * 遗嘱消息 QoS 等级
274   - */
275   - private Integer qos = 1;
276   - /**
277   - * 遗嘱消息是否作为保留消息发布
278   - */
279   - private boolean retain = true;
  118 + private String serialize;
280 119  
281   - }
  120 + // ========== SSL 配置 ==========
282 121  
283 122 /**
284   - * 高级 SSL/TLS 配置
  123 + * SSL 配置(可选,配置文件中不配置则为 null)
285 124 */
286   - @Data
287   - public static class Ssl {
  125 + @Valid
  126 + private SslConfig ssl;
288 127  
289   - /**
290   - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks
291   - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
292   - */
293   - private String keyStorePath;
294   - /**
295   - * 密钥库密码
296   - */
297   - private String keyStorePassword;
298   - /**
299   - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks
300   - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
301   - */
302   - private String trustStorePath;
303   - /**
304   - * 信任库密码
305   - */
306   - private String trustStorePassword;
307   -
308   - }
309   -
310   - }
311   -
312   - @Data
313   - public static class TcpProperties {
  128 + // ========== 各协议配置 ==========
314 129  
315 130 /**
316   - * 是否开启
  131 + * HTTP 协议配置
317 132 */
318   - @NotNull(message = "是否开启不能为空")
319   - private Boolean enabled;
320   -
  133 + @Valid
  134 + private IotHttpConfig http;
321 135 /**
322   - * 服务器端口
  136 + * WebSocket 协议配置
323 137 */
324   - private Integer port = 8091;
  138 + @Valid
  139 + private IotWebSocketConfig websocket;
325 140  
326 141 /**
327   - * 心跳超时时间(毫秒)
  142 + * TCP 协议配置
328 143 */
329   - private Long keepAliveTimeoutMs = 30000L;
330   -
  144 + @Valid
  145 + private IotTcpConfig tcp;
331 146 /**
332   - * 最大连接数
  147 + * UDP 协议配置
333 148 */
334   - private Integer maxConnections = 1000;
  149 + @Valid
  150 + private IotUdpConfig udp;
335 151  
336 152 /**
337   - * 是否启用SSL
  153 + * CoAP 协议配置
338 154 */
339   - private Boolean sslEnabled = false;
  155 + @Valid
  156 + private IotCoapConfig coap;
340 157  
341 158 /**
342   - * SSL证书路径
  159 + * MQTT 协议配置
343 160 */
344   - private String sslCertPath;
345   -
  161 + @Valid
  162 + private IotMqttConfig mqtt;
346 163 /**
347   - * SSL私钥路径
  164 + * EMQX 协议配置
348 165 */
349   - private String sslKeyPath;
  166 + @Valid
  167 + private IotEmqxConfig emqx;
350 168  
351 169 }
352 170  
  171 + /**
  172 + * SSL 配置
  173 + */
353 174 @Data
354   - public static class MqttProperties {
355   -
356   - /**
357   - * 是否开启
358   - */
359   - @NotNull(message = "是否开启不能为空")
360   - private Boolean enabled;
361   -
362   - /**
363   - * 服务器端口
364   - */
365   - private Integer port = 1883;
366   -
367   - /**
368   - * 最大消息大小(字节)
369   - */
370   - private Integer maxMessageSize = 8192;
371   -
372   - /**
373   - * 连接超时时间(秒)
374   - */
375   - private Integer connectTimeoutSeconds = 60;
376   - /**
377   - * 保持连接超时时间(秒)
378   - */
379   - private Integer keepAliveTimeoutSeconds = 300;
  175 + public static class SslConfig {
380 176  
381 177 /**
382 178 * 是否启用 SSL
383 179 */
384   - private Boolean sslEnabled = false;
385   - /**
386   - * SSL 配置
387   - */
388   - private SslOptions sslOptions = new SslOptions();
389   -
390   - /**
391   - * SSL 配置选项
392   - */
393   - @Data
394   - public static class SslOptions {
395   -
396   - /**
397   - * 密钥证书选项
398   - */
399   - private io.vertx.core.net.KeyCertOptions keyCertOptions;
400   - /**
401   - * 信任选项
402   - */
403   - private io.vertx.core.net.TrustOptions trustOptions;
404   - /**
405   - * SSL 证书路径
406   - */
407   - private String certPath;
408   - /**
409   - * SSL 私钥路径
410   - */
411   - private String keyPath;
412   - /**
413   - * 信任存储路径
414   - */
415   - private String trustStorePath;
416   - /**
417   - * 信任存储密码
418   - */
419   - private String trustStorePassword;
420   -
421   - }
422   -
423   - }
424   -
425   - @Data
426   - public static class MqttWsProperties {
  180 + @NotNull(message = "是否启用 SSL 不能为空")
  181 + private Boolean ssl = false;
427 182  
428 183 /**
429   - * 是否开启
430   - */
431   - @NotNull(message = "是否开启不能为空")
432   - private Boolean enabled;
433   -
434   - /**
435   - * WebSocket 服务器端口(默认:8083)
436   - */
437   - private Integer port = 8083;
438   -
439   - /**
440   - * WebSocket 路径(默认:/mqtt)
441   - */
442   - @NotEmpty(message = "WebSocket 路径不能为空")
443   - private String path = "/mqtt";
444   -
445   - /**
446   - * 最大消息大小(字节)
447   - */
448   - private Integer maxMessageSize = 8192;
449   -
450   - /**
451   - * 连接超时时间(秒)
452   - */
453   - private Integer connectTimeoutSeconds = 60;
454   -
455   - /**
456   - * 保持连接超时时间(秒)
457   - */
458   - private Integer keepAliveTimeoutSeconds = 300;
459   -
460   - /**
461   - * 是否启用 SSL(wss://)
462   - */
463   - private Boolean sslEnabled = false;
464   -
465   - /**
466   - * SSL 配置
467   - */
468   - private SslOptions sslOptions = new SslOptions();
469   -
470   - /**
471   - * WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1")
472   - */
473   - @NotEmpty(message = "WebSocket 子协议不能为空")
474   - private String subProtocol = "mqtt";
475   -
476   - /**
477   - * 最大帧大小(字节)
478   - */
479   - private Integer maxFrameSize = 65536;
480   -
481   - /**
482   - * SSL 配置选项
483   - */
484   - @Data
485   - public static class SslOptions {
486   -
487   - /**
488   - * 密钥证书选项
489   - */
490   - private io.vertx.core.net.KeyCertOptions keyCertOptions;
491   -
492   - /**
493   - * 信任选项
494   - */
495   - private io.vertx.core.net.TrustOptions trustOptions;
496   -
497   - /**
498   - * SSL 证书路径
499   - */
500   - private String certPath;
501   -
502   - /**
503   - * SSL 私钥路径
504   - */
505   - private String keyPath;
506   -
507   - /**
508   - * 信任存储路径
509   - */
510   - private String trustStorePath;
511   -
512   - /**
513   - * 信任存储密码
514   - */
515   - private String trustStorePassword;
516   -
517   - }
518   -
519   - }
520   -
521   - @Data
522   - public static class UdpProperties {
523   -
524   - /**
525   - * 是否开启
526   - */
527   - @NotNull(message = "是否开启不能为空")
528   - private Boolean enabled;
529   -
530   - /**
531   - * 服务端口(默认 8093)
532   - */
533   - private Integer port = 8093;
534   -
535   - /**
536   - * 接收缓冲区大小(默认 64KB)
  184 + * SSL 证书路径
537 185 */
538   - private Integer receiveBufferSize = 65536;
  186 + @NotEmpty(message = "SSL 证书路径不能为空")
  187 + private String sslCertPath;
539 188  
540 189 /**
541   - * 发送缓冲区大小(默认 64KB)
  190 + * SSL 私钥路径
542 191 */
543   - private Integer sendBufferSize = 65536;
  192 + @NotEmpty(message = "SSL 私钥路径不能为空")
  193 + private String sslKeyPath;
544 194  
545 195 /**
546   - * 会话超时时间(毫秒,默认 60 秒)
  196 + * 密钥库(KeyStore)路径
547 197 * <p>
548   - * 用于清理不活跃的设备地址映射
549   - */
550   - private Long sessionTimeoutMs = 60000L;
551   -
552   - /**
553   - * 会话清理间隔(毫秒,默认 30 秒)
554   - */
555   - private Long sessionCleanIntervalMs = 30000L;
556   -
557   - }
558   -
559   - @Data
560   - public static class CoapProperties {
561   -
562   - /**
563   - * 是否开启
564   - */
565   - @NotNull(message = "是否开启不能为空")
566   - private Boolean enabled;
567   -
568   - /**
569   - * 服务端口(CoAP 默认端口 5683)
570   - */
571   - @NotNull(message = "服务端口不能为空")
572   - private Integer port = 5683;
573   -
574   - /**
575   - * 最大消息大小(字节)
576   - */
577   - @NotNull(message = "最大消息大小不能为空")
578   - private Integer maxMessageSize = 1024;
579   -
580   - /**
581   - * ACK 超时时间(毫秒)
582   - */
583   - @NotNull(message = "ACK 超时时间不能为空")
584   - private Integer ackTimeout = 2000;
585   -
586   - /**
587   - * 最大重传次数
588   - */
589   - @NotNull(message = "最大重传次数不能为空")
590   - private Integer maxRetransmit = 4;
591   -
592   - }
593   -
594   - @Data
595   - public static class WebSocketProperties {
596   -
597   - /**
598   - * 是否开启
599   - */
600   - @NotNull(message = "是否开启不能为空")
601   - private Boolean enabled;
602   -
603   - /**
604   - * 服务器端口(默认:8094)
605   - */
606   - private Integer port = 8094;
607   -
608   - /**
609   - * WebSocket 路径(默认:/ws)
610   - */
611   - @NotEmpty(message = "WebSocket 路径不能为空")
612   - private String path = "/ws";
613   -
614   - /**
615   - * 最大消息大小(字节,默认 64KB)
616   - */
617   - private Integer maxMessageSize = 65536;
618   -
619   - /**
620   - * 最大帧大小(字节,默认 64KB)
  198 + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)
621 199 */
622   - private Integer maxFrameSize = 65536;
623   -
  200 + private String keyStorePath;
624 201 /**
625   - * 空闲超时时间(秒,默认 60)
  202 + * 密钥库密码
626 203 */
627   - private Integer idleTimeoutSeconds = 60;
  204 + private String keyStorePassword;
628 205  
629 206 /**
630   - * 是否启用 SSL(wss://)
631   - */
632   - private Boolean sslEnabled = false;
633   -
634   - /**
635   - * SSL 证书路径
  207 + * 信任库(TrustStore)路径
  208 + * <p>
  209 + * 包含服务端信任的 CA 证书,用于验证服务端的身份
636 210 */
637   - private String sslCertPath;
638   -
  211 + private String trustStorePath;
639 212 /**
640   - * SSL 私钥路径
  213 + * 信任库密码
641 214 */
642   - private String sslKeyPath;
  215 + private String trustStorePassword;
643 216  
644 217 }
645 218  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  4 +
  5 +/**
  6 + * IoT 协议接口
  7 + *
  8 + * 定义传输层协议的生命周期管理
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public interface IotProtocol {
  13 +
  14 + /**
  15 + * 获取协议实例 ID
  16 + *
  17 + * @return 协议实例 ID,如 "http-alink"、"tcp-binary"
  18 + */
  19 + String getId();
  20 +
  21 + /**
  22 + * 获取服务器 ID(用于消息追踪,全局唯一)
  23 + *
  24 + * @return 服务器 ID
  25 + */
  26 + String getServerId();
  27 +
  28 + /**
  29 + * 获取协议类型
  30 + *
  31 + * @return 协议类型枚举
  32 + */
  33 + IotProtocolTypeEnum getType();
  34 +
  35 + /**
  36 + * 启动协议服务
  37 + */
  38 + void start();
  39 +
  40 + /**
  41 + * 停止协议服务
  42 + */
  43 + void stop();
  44 +
  45 + /**
  46 + * 检查协议服务是否正在运行
  47 + *
  48 + * @return 是否正在运行
  49 + */
  50 + boolean isRunning();
  51 +
  52 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol;
2 2  
  3 +import cn.hutool.core.util.StrUtil;
3 4 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
8   -import jakarta.annotation.PostConstruct;
  8 +import lombok.AllArgsConstructor;
9 9 import lombok.extern.slf4j.Slf4j;
10 10  
11 11 /**
12   - * IoT 网关 EMQX 订阅者:接收下行给设备的消息
  12 + * IoT 协议下行消息订阅者抽象类
  13 + *
  14 + * 负责接收来自消息总线的下行消息,并委托给子类进行业务处理
13 15 *
14 16 * @author 芋道源码
15 17 */
  18 +@AllArgsConstructor
16 19 @Slf4j
17   -public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
  20 +public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
18 21  
19   - private final IotEmqxDownstreamHandler downstreamHandler;
  22 + private final IotProtocol protocol;
20 23  
21 24 private final IotMessageBus messageBus;
22 25  
23   - private final IotEmqxUpstreamProtocol protocol;
24   -
25   - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
26   - this.protocol = protocol;
27   - this.messageBus = messageBus;
28   - this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
29   - }
30   -
31   - @PostConstruct
32   - public void init() {
33   - messageBus.register(this);
34   - }
35   -
36 26 @Override
37 27 public String getTopic() {
38 28 return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
39 29 }
40 30  
  31 + /**
  32 + * 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
  33 + */
41 34 @Override
42 35 public String getGroup() {
43   - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
44 36 return getTopic();
45 37 }
46 38  
47 39 @Override
  40 + public void start() {
  41 + messageBus.register(this);
  42 + log.info("[start][{} 下行消息订阅成功,Topic:{}]", protocol.getType().name(), getTopic());
  43 + }
  44 +
  45 + @Override
  46 + public void stop() {
  47 + messageBus.unregister(this);
  48 + log.info("[stop][{} 下行消息订阅已停止,Topic:{}]", protocol.getType().name(), getTopic());
  49 + }
  50 +
  51 + @Override
48 52 public void onMessage(IotDeviceMessage message) {
49 53 log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
50 54 message.getId(), message.getMethod(), message.getDeviceId());
51 55 try {
52 56 // 1. 校验
53 57 String method = message.getMethod();
54   - if (method == null) {
  58 + if (StrUtil.isBlank(method)) {
55 59 log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
56 60 message.getId(), message.getDeviceId());
57 61 return;
58 62 }
59 63  
60 64 // 2. 处理下行消息
61   - downstreamHandler.handle(message);
  65 + handleMessage(message);
62 66 } catch (Exception e) {
63 67 log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
64 68 message.getId(), message.getMethod(), message.getDeviceId(), e);
65 69 }
66 70 }
67 71  
68   -}
69 72 \ No newline at end of file
  73 + /**
  74 + * 处理下行消息
  75 + *
  76 + * @param message 下行消息
  77 + */
  78 + protected abstract void handleMessage(IotDeviceMessage message);
  79 +
  80 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.BooleanUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
  14 +import lombok.Getter;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +import org.springframework.context.SmartLifecycle;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.List;
  20 +
  21 +/**
  22 + * IoT 协议管理器:负责根据配置创建和管理协议实例
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@Slf4j
  27 +public class IotProtocolManager implements SmartLifecycle {
  28 +
  29 + private final IotGatewayProperties gatewayProperties;
  30 +
  31 + /**
  32 + * 协议实例列表
  33 + */
  34 + private final List<IotProtocol> protocols = new ArrayList<>();
  35 +
  36 + @Getter
  37 + private volatile boolean running = false;
  38 +
  39 + public IotProtocolManager(IotGatewayProperties gatewayProperties) {
  40 + this.gatewayProperties = gatewayProperties;
  41 + }
  42 +
  43 + @Override
  44 + public void start() {
  45 + if (running) {
  46 + return;
  47 + }
  48 + List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
  49 + if (CollUtil.isEmpty(protocolConfigs)) {
  50 + log.info("[start][没有配置协议实例,跳过启动]");
  51 + return;
  52 + }
  53 +
  54 + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
  55 + if (BooleanUtil.isFalse(config.getEnabled())) {
  56 + log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
  57 + continue;
  58 + }
  59 + IotProtocol protocol = createProtocol(config);
  60 + if (protocol == null) {
  61 + continue;
  62 + }
  63 + protocol.start();
  64 + protocols.add(protocol);
  65 + }
  66 + running = true;
  67 + log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size());
  68 + }
  69 +
  70 + @Override
  71 + public void stop() {
  72 + if (!running) {
  73 + return;
  74 + }
  75 + for (IotProtocol protocol : protocols) {
  76 + try {
  77 + protocol.stop();
  78 + } catch (Exception e) {
  79 + log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e);
  80 + }
  81 + }
  82 + protocols.clear();
  83 + running = false;
  84 + log.info("[stop][协议管理器已停止]");
  85 + }
  86 +
  87 + /**
  88 + * 创建协议实例
  89 + *
  90 + * @param config 协议实例配置
  91 + * @return 协议实例
  92 + */
  93 + @SuppressWarnings({"EnhancedSwitchMigration"})
  94 + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) {
  95 + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol());
  96 + if (protocolType == null) {
  97 + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
  98 + return null;
  99 + }
  100 + switch (protocolType) {
  101 + case HTTP:
  102 + return createHttpProtocol(config);
  103 + case TCP:
  104 + return createTcpProtocol(config);
  105 + case UDP:
  106 + return createUdpProtocol(config);
  107 + case COAP:
  108 + return createCoapProtocol(config);
  109 + case WEBSOCKET:
  110 + return createWebSocketProtocol(config);
  111 + case MQTT:
  112 + return createMqttProtocol(config);
  113 + case EMQX:
  114 + return createEmqxProtocol(config);
  115 + default:
  116 + throw new IllegalArgumentException(String.format(
  117 + "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
  118 + }
  119 + }
  120 +
  121 + /**
  122 + * 创建 HTTP 协议实例
  123 + *
  124 + * @param config 协议实例配置
  125 + * @return HTTP 协议实例
  126 + */
  127 + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
  128 + return new IotHttpProtocol(config);
  129 + }
  130 +
  131 + /**
  132 + * 创建 TCP 协议实例
  133 + *
  134 + * @param config 协议实例配置
  135 + * @return TCP 协议实例
  136 + */
  137 + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
  138 + return new IotTcpProtocol(config);
  139 + }
  140 +
  141 + /**
  142 + * 创建 UDP 协议实例
  143 + *
  144 + * @param config 协议实例配置
  145 + * @return UDP 协议实例
  146 + */
  147 + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
  148 + return new IotUdpProtocol(config);
  149 + }
  150 +
  151 + /**
  152 + * 创建 CoAP 协议实例
  153 + *
  154 + * @param config 协议实例配置
  155 + * @return CoAP 协议实例
  156 + */
  157 + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
  158 + return new IotCoapProtocol(config);
  159 + }
  160 +
  161 + /**
  162 + * 创建 WebSocket 协议实例
  163 + *
  164 + * @param config 协议实例配置
  165 + * @return WebSocket 协议实例
  166 + */
  167 + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
  168 + return new IotWebSocketProtocol(config);
  169 + }
  170 +
  171 + /**
  172 + * 创建 MQTT 协议实例
  173 + *
  174 + * @param config 协议实例配置
  175 + * @return MQTT 协议实例
  176 + */
  177 + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) {
  178 + return new IotMqttProtocol(config);
  179 + }
  180 +
  181 + /**
  182 + * 创建 EMQX 协议实例
  183 + *
  184 + * @param config 协议实例配置
  185 + * @return EMQX 协议实例
  186 + */
  187 + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) {
  188 + return new IotEmqxProtocol(config);
  189 + }
  190 +
  191 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT CoAP 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotCoapConfig {
  14 +
  15 + /**
  16 + * 最大消息大小(字节)
  17 + */
  18 + @NotNull(message = "最大消息大小不能为空")
  19 + @Min(value = 64, message = "最大消息大小必须大于 64 字节")
  20 + private Integer maxMessageSize = 1024;
  21 +
  22 + /**
  23 + * ACK 超时时间(毫秒)
  24 + */
  25 + @NotNull(message = "ACK 超时时间不能为空")
  26 + @Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒")
  27 + private Integer ackTimeoutMs = 2000;
  28 +
  29 + /**
  30 + * 最大重传次数
  31 + */
  32 + @NotNull(message = "最大重传次数不能为空")
  33 + @Min(value = 0, message = "最大重传次数必须大于等于 0")
  34 + private Integer maxRetransmit = 4;
  35 +
  36 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
2   -
3   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import jakarta.annotation.PostConstruct;
8   -import lombok.RequiredArgsConstructor;
9   -import lombok.extern.slf4j.Slf4j;
10   -
11   -/**
12   - * IoT 网关 CoAP 订阅者:接收下行给设备的消息
13   - *
14   - * @author 芋道源码
15   - */
16   -@RequiredArgsConstructor
17   -@Slf4j
18   -public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
19   -
20   - private final IotCoapUpstreamProtocol protocol;
21   -
22   - private final IotMessageBus messageBus;
23   -
24   - @PostConstruct
25   - public void init() {
26   - messageBus.register(this);
27   - }
28   -
29   - @Override
30   - public String getTopic() {
31   - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
32   - }
33   -
34   - @Override
35   - public String getGroup() {
36   - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
37   - return getTopic();
38   - }
39   -
40   - @Override
41   - public void onMessage(IotDeviceMessage message) {
42   - // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
43   - log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
44   - }
45   -
46   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthResource;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterHandler;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterResource;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubResource;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamHandler;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamTopicResource;
  18 +import lombok.Getter;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.eclipse.californium.core.CoapResource;
  21 +import org.eclipse.californium.core.CoapServer;
  22 +import org.eclipse.californium.core.config.CoapConfig;
  23 +import org.eclipse.californium.elements.config.Configuration;
  24 +import cn.hutool.core.lang.Assert;
  25 +
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +/**
  29 + * IoT CoAP 协议实现
  30 + * <p>
  31 + * 基于 Eclipse Californium 实现,支持:
  32 + * 1. 认证:POST /auth
  33 + * 2. 设备动态注册:POST /auth/register/device
  34 + * 3. 子设备动态注册:POST /auth/register/sub-device/{productKey}/{deviceName}
  35 + * 4. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  36 + * 5. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  37 + *
  38 + * @author 芋道源码
  39 + */
  40 +@Slf4j
  41 +public class IotCoapProtocol implements IotProtocol {
  42 +
  43 + /**
  44 + * 协议配置
  45 + */
  46 + private final ProtocolProperties properties;
  47 + /**
  48 + * 服务器 ID(用于消息追踪,全局唯一)
  49 + */
  50 + @Getter
  51 + private final String serverId;
  52 +
  53 + /**
  54 + * 运行状态
  55 + */
  56 + @Getter
  57 + private volatile boolean running = false;
  58 +
  59 + /**
  60 + * CoAP 服务器
  61 + */
  62 + private CoapServer coapServer;
  63 +
  64 + /**
  65 + * 下行消息订阅者
  66 + */
  67 + private final IotCoapDownstreamSubscriber downstreamSubscriber;
  68 +
  69 + public IotCoapProtocol(ProtocolProperties properties) {
  70 + IotCoapConfig coapConfig = properties.getCoap();
  71 + Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
  72 + this.properties = properties;
  73 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  74 +
  75 + // 初始化下行消息订阅者
  76 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  77 + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
  78 + }
  79 +
  80 + @Override
  81 + public String getId() {
  82 + return properties.getId();
  83 + }
  84 +
  85 + @Override
  86 + public IotProtocolTypeEnum getType() {
  87 + return IotProtocolTypeEnum.COAP;
  88 + }
  89 +
  90 + @Override
  91 + public void start() {
  92 + if (running) {
  93 + log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId());
  94 + return;
  95 + }
  96 +
  97 + IotCoapConfig coapConfig = properties.getCoap();
  98 + try {
  99 + // 1.1 创建 CoAP 配置
  100 + Configuration config = Configuration.createStandardWithoutFile();
  101 + config.set(CoapConfig.COAP_PORT, properties.getPort());
  102 + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
  103 + config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS);
  104 + config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit());
  105 + // 1.2 创建 CoAP 服务器
  106 + coapServer = new CoapServer(config);
  107 +
  108 + // 2.1 添加 /auth 认证资源
  109 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId);
  110 + IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler);
  111 + coapServer.add(authResource);
  112 + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
  113 + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
  114 + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
  115 + // 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源
  116 + IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler();
  117 + IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler);
  118 + authResource.add(new CoapResource("register") {{
  119 + add(registerResource);
  120 + add(registerSubResource);
  121 + }});
  122 + // 2.4 添加 /topic 根资源(用于上行消息)
  123 + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId);
  124 + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler);
  125 + coapServer.add(topicResource);
  126 +
  127 + // 3. 启动服务器
  128 + coapServer.start();
  129 + running = true;
  130 + log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{},serverId:{}]",
  131 + getId(), properties.getPort(), serverId);
  132 +
  133 + // 4. 启动下行消息订阅者
  134 + this.downstreamSubscriber.start();
  135 + } catch (Exception e) {
  136 + log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
  137 + if (coapServer != null) {
  138 + coapServer.destroy();
  139 + coapServer = null;
  140 + }
  141 + throw e;
  142 + }
  143 + }
  144 +
  145 + @Override
  146 + public void stop() {
  147 + if (!running) {
  148 + return;
  149 + }
  150 + // 1. 停止下行消息订阅者
  151 + try {
  152 + downstreamSubscriber.stop();
  153 + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
  154 + } catch (Exception e) {
  155 + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  156 + }
  157 +
  158 + // 2. 关闭 CoAP 服务器
  159 + if (coapServer != null) {
  160 + try {
  161 + coapServer.stop();
  162 + coapServer.destroy();
  163 + coapServer = null;
  164 + log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId());
  165 + } catch (Exception e) {
  166 + log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e);
  167 + }
  168 + }
  169 + running = false;
  170 + log.info("[stop][IoT CoAP 协议 {} 已停止]", getId());
  171 + }
  172 +
  173 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
2   -
3   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler;
11   -import jakarta.annotation.PostConstruct;
12   -import jakarta.annotation.PreDestroy;
13   -import lombok.Getter;
14   -import lombok.extern.slf4j.Slf4j;
15   -import org.eclipse.californium.core.CoapResource;
16   -import org.eclipse.californium.core.CoapServer;
17   -import org.eclipse.californium.core.config.CoapConfig;
18   -import org.eclipse.californium.elements.config.Configuration;
19   -
20   -import java.util.concurrent.TimeUnit;
21   -
22   -/**
23   - * IoT 网关 CoAP 协议:接收设备上行消息
24   - *
25   - * 基于 Eclipse Californium 实现,支持:
26   - * 1. 认证:POST /auth
27   - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
28   - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
29   - *
30   - * @author 芋道源码
31   - */
32   -@Slf4j
33   -public class IotCoapUpstreamProtocol {
34   -
35   - private final IotGatewayProperties.CoapProperties coapProperties;
36   -
37   - private CoapServer coapServer;
38   -
39   - @Getter
40   - private final String serverId;
41   -
42   - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
43   - this.coapProperties = coapProperties;
44   - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
45   - }
46   -
47   - @PostConstruct
48   - public void start() {
49   - try {
50   - // 1.1 创建网络配置(Californium 3.x API)
51   - Configuration config = Configuration.createStandardWithoutFile();
52   - config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
53   - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
54   - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
55   - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
56   - // 1.2 创建 CoAP 服务器
57   - coapServer = new CoapServer(config);
58   -
59   - // 2.1 添加 /auth 认证资源
60   - IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
61   - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
62   - coapServer.add(authResource);
63   - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
64   - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
65   - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
66   - authResource.add(new CoapResource("register") {{
67   - add(registerResource);
68   - }});
69   - // 2.3 添加 /topic 根资源(用于上行消息)
70   - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
71   - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
72   - coapServer.add(topicResource);
73   -
74   - // 3. 启动服务器
75   - coapServer.start();
76   - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
77   - } catch (Exception e) {
78   - log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
79   - throw e;
80   - }
81   - }
82   -
83   - @PreDestroy
84   - public void stop() {
85   - if (coapServer != null) {
86   - try {
87   - coapServer.stop();
88   - log.info("[stop][IoT 网关 CoAP 协议已停止]");
89   - } catch (Exception e) {
90   - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
91   - }
92   - }
93   - }
94   -
95   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 网关 CoAP 订阅者:接收下行给设备的消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +
  17 + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
  18 + super(protocol, messageBus);
  19 + }
  20 +
  21 + @Override
  22 + protected void handleMessage(IotDeviceMessage message) {
  23 + // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
  24 + log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
  25 + }
  26 +
  27 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ArrayUtil;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.extra.spring.SpringUtil;
  8 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  9 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  10 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  12 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.eclipse.californium.core.coap.CoAP;
  15 +import org.eclipse.californium.core.coap.MediaTypeRegistry;
  16 +import org.eclipse.californium.core.coap.Option;
  17 +import org.eclipse.californium.core.server.resources.CoapExchange;
  18 +
  19 +import java.util.List;
  20 +
  21 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  22 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +
  24 +/**
  25 + * IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@Slf4j
  30 +public abstract class IotCoapAbstractHandler {
  31 +
  32 + /**
  33 + * 自定义 CoAP Option 编号,用于携带 Token
  34 + * <p>
  35 + * CoAP Option 范围 2048-65535 属于实验/自定义范围
  36 + */
  37 + public static final int OPTION_TOKEN = 2088;
  38 +
  39 + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  40 +
  41 + /**
  42 + * 处理 CoAP 请求(模板方法)
  43 + *
  44 + * @param exchange CoAP 交换对象
  45 + */
  46 + public final void handle(CoapExchange exchange) {
  47 + try {
  48 + // 1. 前置处理
  49 + beforeHandle(exchange);
  50 +
  51 + // 2. 执行业务逻辑
  52 + CommonResult<Object> result = handle0(exchange);
  53 + writeResponse(exchange, result);
  54 + } catch (ServiceException e) {
  55 + // 业务异常,返回对应的错误码和消息
  56 + writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage()));
  57 + } catch (IllegalArgumentException e) {
  58 + // 参数校验异常(hutool Assert 抛出),返回 BAD_REQUEST
  59 + writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
  60 + } catch (Exception e) {
  61 + // 其他未知异常,返回 INTERNAL_SERVER_ERROR
  62 + log.error("[handle][CoAP 请求处理异常]", e);
  63 + writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR));
  64 + }
  65 + }
  66 +
  67 + /**
  68 + * 处理 CoAP 请求(子类实现)
  69 + *
  70 + * @param exchange CoAP 交换对象
  71 + * @return 处理结果
  72 + */
  73 + protected abstract CommonResult<Object> handle0(CoapExchange exchange);
  74 +
  75 + /**
  76 + * 前置处理:认证等
  77 + *
  78 + * @param exchange CoAP 交换对象
  79 + */
  80 + private void beforeHandle(CoapExchange exchange) {
  81 + // 1.1 如果不需要认证,则不走前置处理
  82 + if (!requiresAuthentication()) {
  83 + return;
  84 + }
  85 + // 1.2 从自定义 Option 获取 token
  86 + String token = getTokenFromOption(exchange);
  87 + if (StrUtil.isEmpty(token)) {
  88 + throw exception(UNAUTHORIZED);
  89 + }
  90 + // 1.3 校验 token
  91 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
  92 + if (deviceInfo == null) {
  93 + throw exception(UNAUTHORIZED);
  94 + }
  95 +
  96 + // 2.1 解析 productKey 和 deviceName
  97 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  98 + String productKey = getProductKey(uriPath);
  99 + String deviceName = getDeviceName(uriPath);
  100 + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) {
  101 + throw exception(BAD_REQUEST);
  102 + }
  103 + // 2.2 校验设备信息是否匹配
  104 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
  105 + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
  106 + throw exception(FORBIDDEN);
  107 + }
  108 + }
  109 +
  110 + // ========== Token 相关方法 ==========
  111 +
  112 + /**
  113 + * 是否需要认证(子类可覆盖)
  114 + * <p>
  115 + * 默认不需要认证
  116 + *
  117 + * @return 是否需要认证
  118 + */
  119 + protected boolean requiresAuthentication() {
  120 + return false;
  121 + }
  122 +
  123 + /**
  124 + * 从 URI 路径中获取 productKey(子类实现)
  125 + * <p>
  126 + * 默认抛出异常,需要认证的子类必须实现此方法
  127 + *
  128 + * @param uriPath URI 路径
  129 + * @return productKey
  130 + */
  131 + protected String getProductKey(List<String> uriPath) {
  132 + throw new UnsupportedOperationException("子类需要实现 getProductKey 方法");
  133 + }
  134 +
  135 + /**
  136 + * 从 URI 路径中获取 deviceName(子类实现)
  137 + * <p>
  138 + * 默认抛出异常,需要认证的子类必须实现此方法
  139 + *
  140 + * @param uriPath URI 路径
  141 + * @return deviceName
  142 + */
  143 + protected String getDeviceName(List<String> uriPath) {
  144 + throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法");
  145 + }
  146 +
  147 + /**
  148 + * 从自定义 CoAP Option 中获取 Token
  149 + *
  150 + * @param exchange CoAP 交换对象
  151 + * @return Token 值,如果不存在则返回 null
  152 + */
  153 + protected String getTokenFromOption(CoapExchange exchange) {
  154 + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
  155 + o -> o.getNumber() == OPTION_TOKEN);
  156 + return option != null ? new String(option.getValue()) : null;
  157 + }
  158 +
  159 + // ========== 序列化相关方法 ==========
  160 +
  161 + /**
  162 + * 解析请求体为指定类型
  163 + *
  164 + * @param exchange CoAP 交换对象
  165 + * @param clazz 目标类型
  166 + * @param <T> 目标类型泛型
  167 + * @return 解析后的对象,解析失败返回 null
  168 + */
  169 + protected <T> T deserializeRequest(CoapExchange exchange, Class<T> clazz) {
  170 + byte[] payload = exchange.getRequestPayload();
  171 + if (ArrayUtil.isEmpty(payload)) {
  172 + return null;
  173 + }
  174 + return JsonUtils.parseObject(payload, clazz);
  175 + }
  176 +
  177 + private static String serializeResponse(Object data) {
  178 + return JsonUtils.toJsonString(data);
  179 + }
  180 +
  181 + protected void writeResponse(CoapExchange exchange, CommonResult<?> data) {
  182 + String json = serializeResponse(data);
  183 + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
  184 + }
  185 +
  186 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.BooleanUtil;
  6 +import cn.hutool.extra.spring.SpringUtil;
  7 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  12 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.eclipse.californium.core.server.resources.CoapExchange;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  18 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
  19 +
  20 +/**
  21 + * IoT 网关 CoAP 协议的【认证】处理器
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@Slf4j
  26 +public class IotCoapAuthHandler extends IotCoapAbstractHandler {
  27 +
  28 + private final String serverId;
  29 +
  30 + private final IotDeviceTokenService deviceTokenService;
  31 + private final IotDeviceCommonApi deviceApi;
  32 + private final IotDeviceMessageService deviceMessageService;
  33 +
  34 + public IotCoapAuthHandler(String serverId) {
  35 + this.serverId = serverId;
  36 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  37 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  38 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  39 + }
  40 +
  41 + @Override
  42 + @SuppressWarnings("DuplicatedCode")
  43 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  44 + // 1. 解析参数
  45 + IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class);
  46 + Assert.notNull(request, "请求体不能为空");
  47 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
  48 + Assert.notBlank(request.getUsername(), "username 不能为空");
  49 + Assert.notBlank(request.getPassword(), "password 不能为空");
  50 +
  51 + // 2.1 执行认证
  52 + CommonResult<Boolean> result = deviceApi.authDevice(request);
  53 + result.checkError();
  54 + if (BooleanUtil.isFalse(result.getData())) {
  55 + throw exception(DEVICE_AUTH_FAIL);
  56 + }
  57 + // 2.2 生成 Token
  58 + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
  59 + Assert.notNull(deviceInfo, "设备信息不能为空");
  60 + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  61 + Assert.notBlank(token, "生成 token 不能为空");
  62 +
  63 + // 3. 执行上线
  64 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
  65 + deviceMessageService.sendDeviceMessage(message,
  66 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
  67 +
  68 + // 4. 构建响应数据
  69 + return CommonResult.success(MapUtil.of("token", token));
  70 + }
  71 +
  72 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
2 2  
3   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
4 3 import lombok.extern.slf4j.Slf4j;
5 4 import org.eclipse.californium.core.CoapResource;
6 5 import org.eclipse.californium.core.server.resources.CoapExchange;
... ... @@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource {
17 16  
18 17 public static final String PATH = "auth";
19 18  
20   - private final IotCoapUpstreamProtocol protocol;
21 19 private final IotCoapAuthHandler authHandler;
22 20  
23   - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
24   - IotCoapAuthHandler authHandler) {
  21 + public IotCoapAuthResource(IotCoapAuthHandler authHandler) {
25 22 super(PATH);
26   - this.protocol = protocol;
27 23 this.authHandler = authHandler;
28 24 log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
29 25 }
... ... @@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource {
31 27 @Override
32 28 public void handlePOST(CoapExchange exchange) {
33 29 log.debug("[handlePOST][收到 /auth POST 请求]");
34   - authHandler.handle(exchange, protocol);
  30 + authHandler.handle(exchange);
35 31 }
36 32  
37 33 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.eclipse.californium.core.server.resources.CoapExchange;
  11 +
  12 +/**
  13 + * IoT 网关 CoAP 协议的【设备动态注册】处理器
  14 + * <p>
  15 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  16 + *
  17 + * @author 芋道源码
  18 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  19 + */
  20 +@Slf4j
  21 +public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
  22 +
  23 + private final IotDeviceCommonApi deviceApi;
  24 +
  25 + public IotCoapRegisterHandler() {
  26 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  27 + }
  28 +
  29 + @Override
  30 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  31 + // 1. 解析参数
  32 + IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class);
  33 + Assert.notNull(request, "请求体不能为空");
  34 + Assert.notBlank(request.getProductKey(), "productKey 不能为空");
  35 + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
  36 + Assert.notBlank(request.getSign(), "sign 不能为空");
  37 +
  38 + // 2. 调用动态注册
  39 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
  40 + result.checkError();
  41 +
  42 + // 3. 构建响应数据
  43 + return CommonResult.success(result.getData());
  44 + }
  45 +
  46 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
2 2  
3 3 import lombok.extern.slf4j.Slf4j;
4 4 import org.eclipse.californium.core.CoapResource;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  7 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  11 +import lombok.Data;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.eclipse.californium.core.server.resources.CoapExchange;
  14 +
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  18 +
  19 +/**
  20 + * IoT 网关 CoAP 协议的【子设备动态注册】处理器
  21 + * <p>
  22 + * 用于子设备的动态注册,需要网关认证
  23 + *
  24 + * @author 芋道源码
  25 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  26 + */
  27 +@Slf4j
  28 +public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler {
  29 +
  30 + private final IotDeviceCommonApi deviceApi;
  31 +
  32 + public IotCoapRegisterSubHandler() {
  33 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  34 + }
  35 +
  36 + @Override
  37 + @SuppressWarnings("DuplicatedCode")
  38 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  39 + // 1.1 解析通用参数(从 URI 路径获取网关设备信息)
  40 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  41 + String productKey = getProductKey(uriPath);
  42 + String deviceName = getDeviceName(uriPath);
  43 + // 1.2 解析子设备列表
  44 + SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class);
  45 + Assert.notNull(request, "请求参数不能为空");
  46 + Assert.notEmpty(request.getParams(), "params 不能为空");
  47 +
  48 + // 2. 调用子设备动态注册
  49 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
  50 + .setGatewayProductKey(productKey)
  51 + .setGatewayDeviceName(deviceName)
  52 + .setSubDevices(request.getParams());
  53 + CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
  54 + result.checkError();
  55 +
  56 + // 3. 返回结果
  57 + return success(result.getData());
  58 + }
  59 +
  60 + @Override
  61 + protected boolean requiresAuthentication() {
  62 + return true;
  63 + }
  64 +
  65 + @Override
  66 + protected String getProductKey(List<String> uriPath) {
  67 + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
  68 + return CollUtil.get(uriPath, 3);
  69 + }
  70 +
  71 + @Override
  72 + protected String getDeviceName(List<String> uriPath) {
  73 + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
  74 + return CollUtil.get(uriPath, 4);
  75 + }
  76 +
  77 + @Data
  78 + public static class SubDeviceRegisterRequest {
  79 +
  80 + private List<IotSubDeviceRegisterReqDTO> params;
  81 +
  82 + }
  83 +
  84 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.eclipse.californium.core.CoapResource;
  5 +import org.eclipse.californium.core.server.resources.CoapExchange;
  6 +import org.eclipse.californium.core.server.resources.Resource;
  7 +
  8 +/**
  9 + * IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName})
  10 + * <p>
  11 + * 用于子设备的动态注册,需要网关认证
  12 + * <p>
  13 + * 支持动态路径匹配:productKey 和 deviceName 是网关设备的标识
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +public class IotCoapRegisterSubResource extends CoapResource {
  19 +
  20 + public static final String PATH = "sub-device";
  21 +
  22 + private final IotCoapRegisterSubHandler registerSubHandler;
  23 +
  24 + /**
  25 + * 创建根资源(/auth/register/sub-device)
  26 + */
  27 + public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) {
  28 + this(PATH, registerSubHandler);
  29 + log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH);
  30 + }
  31 +
  32 + /**
  33 + * 创建子资源(动态路径)
  34 + */
  35 + private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) {
  36 + super(name);
  37 + this.registerSubHandler = registerSubHandler;
  38 + }
  39 +
  40 + @Override
  41 + public Resource getChild(String name) {
  42 + // 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径
  43 + return new IotCoapRegisterSubResource(name, registerSubHandler);
  44 + }
  45 +
  46 + @Override
  47 + public void handlePOST(CoapExchange exchange) {
  48 + log.debug("[handlePOST][收到子设备动态注册请求]");
  49 + registerSubHandler.handle(exchange);
  50 + }
  51 +
  52 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.text.StrPool;
  7 +import cn.hutool.extra.spring.SpringUtil;
  8 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.eclipse.californium.core.server.resources.CoapExchange;
  13 +
  14 +import java.util.List;
  15 +
  16 +/**
  17 + * IoT 网关 CoAP 协议的【上行】处理器
  18 + *
  19 + * 处理设备通过 CoAP 协议发送的上行消息,包括:
  20 + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  21 + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  22 + *
  23 + * Token 通过自定义 CoAP Option 2088 携带
  24 + *
  25 + * @author 芋道源码
  26 + */
  27 +@Slf4j
  28 +public class IotCoapUpstreamHandler extends IotCoapAbstractHandler {
  29 +
  30 + private final String serverId;
  31 +
  32 + private final IotDeviceMessageService deviceMessageService;
  33 +
  34 + public IotCoapUpstreamHandler(String serverId) {
  35 + this.serverId = serverId;
  36 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  37 + }
  38 +
  39 + @Override
  40 + @SuppressWarnings("DuplicatedCode")
  41 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  42 + // 1.1 解析通用参数
  43 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  44 + String productKey = getProductKey(uriPath);
  45 + String deviceName = getDeviceName(uriPath);
  46 + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
  47 + // 1.2 解析消息
  48 + IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class);
  49 + Assert.notNull(message, "请求参数不能为空");
  50 + Assert.equals(method, message.getMethod(), "method 不匹配");
  51 +
  52 + // 2. 发送消息
  53 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
  54 +
  55 + // 3. 返回结果
  56 + return CommonResult.success(MapUtil.of("messageId", message.getId()));
  57 + }
  58 +
  59 + @Override
  60 + protected boolean requiresAuthentication() {
  61 + return true;
  62 + }
  63 +
  64 + @Override
  65 + protected String getProductKey(List<String> uriPath) {
  66 + // 路径格式:/topic/sys/{productKey}/{deviceName}/...
  67 + return CollUtil.get(uriPath, 2);
  68 + }
  69 +
  70 + @Override
  71 + protected String getDeviceName(List<String> uriPath) {
  72 + // 路径格式:/topic/sys/{productKey}/{deviceName}/...
  73 + return CollUtil.get(uriPath, 3);
  74 + }
  75 +
  76 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
2 2  
3   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
4 3 import lombok.extern.slf4j.Slf4j;
5 4 import org.eclipse.californium.core.CoapResource;
6 5 import org.eclipse.californium.core.server.resources.CoapExchange;
... ... @@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
20 19  
21 20 public static final String PATH = "topic";
22 21  
23   - private final IotCoapUpstreamProtocol protocol;
  22 + private final String serverId;
24 23 private final IotCoapUpstreamHandler upstreamHandler;
25 24  
26 25 /**
27 26 * 创建根资源(/topic)
28 27 */
29   - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
  28 + public IotCoapUpstreamTopicResource(String serverId,
30 29 IotCoapUpstreamHandler upstreamHandler) {
31   - this(PATH, protocol, upstreamHandler);
  30 + this(PATH, serverId, upstreamHandler);
32 31 log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
33 32 }
34 33  
... ... @@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
36 35 * 创建子资源(动态路径)
37 36 */
38 37 private IotCoapUpstreamTopicResource(String name,
39   - IotCoapUpstreamProtocol protocol,
  38 + String serverId,
40 39 IotCoapUpstreamHandler upstreamHandler) {
41 40 super(name);
42   - this.protocol = protocol;
  41 + this.serverId = serverId;
43 42 this.upstreamHandler = upstreamHandler;
44 43 }
45 44  
46 45 @Override
47 46 public Resource getChild(String name) {
48 47 // 递归创建动态子资源,支持任意深度路径
49   - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
  48 + return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler);
50 49 }
51 50  
52 51 @Override
53 52 public void handleGET(CoapExchange exchange) {
54   - upstreamHandler.handle(exchange, protocol);
  53 + upstreamHandler.handle(exchange);
55 54 }
56 55  
57 56 @Override
58 57 public void handlePOST(CoapExchange exchange) {
59   - upstreamHandler.handle(exchange, protocol);
  58 + upstreamHandler.handle(exchange);
60 59 }
61 60  
62 61 @Override
63 62 public void handlePUT(CoapExchange exchange) {
64   - upstreamHandler.handle(exchange, protocol);
  63 + upstreamHandler.handle(exchange);
65 64 }
66 65  
67 66 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java
... ... @@ -2,12 +2,5 @@
2 2 * CoAP 协议实现包
3 3 * <p>
4 4 * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
5   - * <p>
6   - * URI 路径:
7   - * - 认证:POST /auth
8   - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
9   - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
10   - * <p>
11   - * Token 通过 CoAP Option 2088 携带
12 5 */
13 6 package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.BooleanUtil;
6   -import cn.hutool.core.util.StrUtil;
7   -import cn.hutool.extra.spring.SpringUtil;
8   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
12   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
13   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
16   -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
17   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
18   -import lombok.extern.slf4j.Slf4j;
19   -import org.eclipse.californium.core.coap.CoAP;
20   -import org.eclipse.californium.core.server.resources.CoapExchange;
21   -
22   -import java.util.Map;
23   -
24   -/**
25   - * IoT 网关 CoAP 协议的【认证】处理器
26   - *
27   - * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler}
28   - *
29   - * @author 芋道源码
30   - */
31   -@Slf4j
32   -public class IotCoapAuthHandler {
33   -
34   - private final IotDeviceTokenService deviceTokenService;
35   - private final IotDeviceCommonApi deviceApi;
36   - private final IotDeviceMessageService deviceMessageService;
37   -
38   - public IotCoapAuthHandler() {
39   - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
40   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
41   - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
42   - }
43   -
44   - /**
45   - * 处理认证请求
46   - *
47   - * @param exchange CoAP 交换对象
48   - * @param protocol 协议对象
49   - */
50   - @SuppressWarnings("unchecked")
51   - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
52   - try {
53   - // 1.1 解析请求体
54   - byte[] payload = exchange.getRequestPayload();
55   - if (payload == null || payload.length == 0) {
56   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
57   - return;
58   - }
59   - Map<String, Object> body;
60   - try {
61   - body = JsonUtils.parseObject(new String(payload), Map.class);
62   - } catch (Exception e) {
63   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
64   - return;
65   - }
66   - // 1.2 解析参数
67   - String clientId = MapUtil.getStr(body, "clientId");
68   - if (StrUtil.isEmpty(clientId)) {
69   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
70   - return;
71   - }
72   - String username = MapUtil.getStr(body, "username");
73   - if (StrUtil.isEmpty(username)) {
74   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
75   - return;
76   - }
77   - String password = MapUtil.getStr(body, "password");
78   - if (StrUtil.isEmpty(password)) {
79   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
80   - return;
81   - }
82   -
83   - // 2.1 执行认证
84   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
85   - .setClientId(clientId).setUsername(username).setPassword(password));
86   - if (result.isError()) {
87   - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg());
88   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
89   - return;
90   - }
91   - if (!BooleanUtil.isTrue(result.getData())) {
92   - log.warn("[handle][认证失败,clientId: {}]", clientId);
93   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
94   - return;
95   - }
96   - // 2.2 生成 Token
97   - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
98   - Assert.notNull(deviceInfo, "设备信息不能为空");
99   - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
100   - Assert.notBlank(token, "生成 token 不能为空");
101   -
102   - // 3. 执行上线
103   - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
104   - deviceMessageService.sendDeviceMessage(message,
105   - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
106   -
107   - // 4. 返回成功响应
108   - log.info("[handle][认证成功,productKey: {}, deviceName: {}]",
109   - deviceInfo.getProductKey(), deviceInfo.getDeviceName());
110   - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
111   - } catch (Exception e) {
112   - log.error("[handle][认证处理异常]", e);
113   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
114   - }
115   - }
116   -
117   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.hutool.extra.spring.SpringUtil;
6   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
9   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
11   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
12   -import lombok.extern.slf4j.Slf4j;
13   -import org.eclipse.californium.core.coap.CoAP;
14   -import org.eclipse.californium.core.server.resources.CoapExchange;
15   -
16   -import java.util.Map;
17   -
18   -/**
19   - * IoT 网关 CoAP 协议的【设备动态注册】处理器
20   - * <p>
21   - * 用于直连设备/网关的一型一密动态注册,不需要认证
22   - *
23   - * @author 芋道源码
24   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
25   - * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler
26   - */
27   -@Slf4j
28   -public class IotCoapRegisterHandler {
29   -
30   - private final IotDeviceCommonApi deviceApi;
31   -
32   - public IotCoapRegisterHandler() {
33   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
34   - }
35   -
36   - /**
37   - * 处理设备动态注册请求
38   - *
39   - * @param exchange CoAP 交换对象
40   - */
41   - @SuppressWarnings("unchecked")
42   - public void handle(CoapExchange exchange) {
43   - try {
44   - // 1.1 解析请求体
45   - byte[] payload = exchange.getRequestPayload();
46   - if (payload == null || payload.length == 0) {
47   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
48   - return;
49   - }
50   - Map<String, Object> body;
51   - try {
52   - body = JsonUtils.parseObject(new String(payload), Map.class);
53   - } catch (Exception e) {
54   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
55   - return;
56   - }
57   -
58   - // 1.2 解析参数
59   - String productKey = MapUtil.getStr(body, "productKey");
60   - if (StrUtil.isEmpty(productKey)) {
61   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
62   - return;
63   - }
64   - String deviceName = MapUtil.getStr(body, "deviceName");
65   - if (StrUtil.isEmpty(deviceName)) {
66   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
67   - return;
68   - }
69   - String productSecret = MapUtil.getStr(body, "productSecret");
70   - if (StrUtil.isEmpty(productSecret)) {
71   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
72   - return;
73   - }
74   -
75   - // 2. 调用动态注册
76   - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
77   - .setProductKey(productKey)
78   - .setDeviceName(deviceName)
79   - .setProductSecret(productSecret);
80   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
81   - if (result.isError()) {
82   - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]",
83   - productKey, deviceName, result.getMsg());
84   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
85   - "设备动态注册失败:" + result.getMsg());
86   - return;
87   - }
88   -
89   - // 3. 返回成功响应
90   - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName);
91   - IotCoapUtils.respondSuccess(exchange, result.getData());
92   - } catch (Exception e) {
93   - log.error("[handle][设备动态注册处理异常]", e);
94   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
95   - }
96   - }
97   -
98   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2   -
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.text.StrPool;
6   -import cn.hutool.core.util.ArrayUtil;
7   -import cn.hutool.core.util.ObjUtil;
8   -import cn.hutool.core.util.StrUtil;
9   -import cn.hutool.extra.spring.SpringUtil;
10   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
11   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
14   -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
15   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
16   -import lombok.extern.slf4j.Slf4j;
17   -import org.eclipse.californium.core.coap.CoAP;
18   -import org.eclipse.californium.core.server.resources.CoapExchange;
19   -
20   -import java.util.List;
21   -
22   -/**
23   - * IoT 网关 CoAP 协议的【上行】处理器
24   - *
25   - * 处理设备通过 CoAP 协议发送的上行消息,包括:
26   - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
27   - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
28   - *
29   - * Token 通过自定义 CoAP Option 2088 携带
30   - *
31   - * @author 芋道源码
32   - */
33   -@Slf4j
34   -public class IotCoapUpstreamHandler {
35   -
36   - private final IotDeviceTokenService deviceTokenService;
37   - private final IotDeviceMessageService deviceMessageService;
38   -
39   - public IotCoapUpstreamHandler() {
40   - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
41   - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
42   - }
43   -
44   - /**
45   - * 处理 CoAP 请求
46   - *
47   - * @param exchange CoAP 交换对象
48   - * @param protocol 协议对象
49   - */
50   - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
51   - try {
52   - // 1. 解析通用参数
53   - List<String> uriPath = exchange.getRequestOptions().getUriPath();
54   - String productKey = CollUtil.get(uriPath, 2);
55   - String deviceName = CollUtil.get(uriPath, 3);
56   - byte[] payload = exchange.getRequestPayload();
57   - if (StrUtil.isEmpty(productKey)) {
58   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
59   - return;
60   - }
61   - if (StrUtil.isEmpty(deviceName)) {
62   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
63   - return;
64   - }
65   - if (ArrayUtil.isEmpty(payload)) {
66   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
67   - return;
68   - }
69   -
70   - // 2. 认证:从自定义 Option 获取 token
71   - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
72   - if (StrUtil.isEmpty(token)) {
73   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
74   - return;
75   - }
76   - // 验证 token
77   - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
78   - if (deviceInfo == null) {
79   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
80   - return;
81   - }
82   - // 验证设备信息匹配
83   - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
84   - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
85   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
86   - return;
87   - }
88   -
89   - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接
90   - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
91   - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
92   -
93   - // 2.2 解码消息
94   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
95   - if (ObjUtil.notEqual(method, message.getMethod())) {
96   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
97   - return;
98   - }
99   - // 2.3 发送消息到消息总线
100   - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
101   -
102   - // 3. 返回成功响应
103   - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
104   - } catch (Exception e) {
105   - log.error("[handle][CoAP 请求处理异常]", e);
106   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
107   - }
108   - }
109   -
110   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
2   -
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import org.eclipse.californium.core.coap.CoAP;
7   -import org.eclipse.californium.core.coap.MediaTypeRegistry;
8   -import org.eclipse.californium.core.coap.Option;
9   -import org.eclipse.californium.core.server.resources.CoapExchange;
10   -
11   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
12   -
13   -/**
14   - * IoT CoAP 协议工具类
15   - *
16   - * @author 芋道源码
17   - */
18   -public class IotCoapUtils {
19   -
20   - /**
21   - * 自定义 CoAP Option 编号,用于携带 Token
22   - * <p>
23   - * CoAP Option 范围 2048-65535 属于实验/自定义范围
24   - */
25   - public static final int OPTION_TOKEN = 2088;
26   -
27   - /**
28   - * 返回成功响应
29   - *
30   - * @param exchange CoAP 交换对象
31   - * @param data 响应数据
32   - */
33   - public static void respondSuccess(CoapExchange exchange, Object data) {
34   - CommonResult<Object> result = CommonResult.success(data);
35   - String json = JsonUtils.toJsonString(result);
36   - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
37   - }
38   -
39   - /**
40   - * 返回错误响应
41   - *
42   - * @param exchange CoAP 交换对象
43   - * @param code CoAP 响应码
44   - * @param message 错误消息
45   - */
46   - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
47   - int errorCode = mapCoapCodeToErrorCode(code);
48   - CommonResult<Object> result = CommonResult.error(errorCode, message);
49   - String json = JsonUtils.toJsonString(result);
50   - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
51   - }
52   -
53   - /**
54   - * 从自定义 CoAP Option 中获取 Token
55   - *
56   - * @param exchange CoAP 交换对象
57   - * @param optionNumber Option 编号
58   - * @return Token 值,如果不存在则返回 null
59   - */
60   - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
61   - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
62   - o -> o.getNumber() == optionNumber);
63   - return option != null ? new String(option.getValue()) : null;
64   - }
65   -
66   - /**
67   - * 将 CoAP 响应码映射到业务错误码
68   - *
69   - * @param code CoAP 响应码
70   - * @return 业务错误码
71   - */
72   - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
73   - if (code == CoAP.ResponseCode.BAD_REQUEST) {
74   - return BAD_REQUEST.getCode();
75   - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
76   - return UNAUTHORIZED.getCode();
77   - } else if (code == CoAP.ResponseCode.FORBIDDEN) {
78   - return FORBIDDEN.getCode();
79   - } else {
80   - return INTERNAL_SERVER_ERROR.getCode();
81   - }
82   - }
83   -
84   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
2   -
3   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler;
6   -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
7   -import io.vertx.core.Vertx;
8   -import io.vertx.core.http.HttpServer;
9   -import io.vertx.ext.web.Router;
10   -import io.vertx.ext.web.handler.BodyHandler;
11   -import jakarta.annotation.PostConstruct;
12   -import jakarta.annotation.PreDestroy;
13   -import lombok.extern.slf4j.Slf4j;
14   -
15   -/**
16   - * IoT 网关 EMQX 认证事件协议服务
17   - * <p>
18   - * 为 EMQX 提供 HTTP 接口服务,包括:
19   - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
20   - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
21   - *
22   - * @author 芋道源码
23   - */
24   -@Slf4j
25   -public class IotEmqxAuthEventProtocol {
26   -
27   - private final IotGatewayProperties.EmqxProperties emqxProperties;
28   -
29   - private final String serverId;
30   -
31   - private final Vertx vertx;
32   -
33   - private HttpServer httpServer;
34   -
35   - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
36   - Vertx vertx) {
37   - this.emqxProperties = emqxProperties;
38   - this.vertx = vertx;
39   - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
40   - }
41   -
42   - @PostConstruct
43   - public void start() {
44   - try {
45   - startHttpServer();
46   - log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
47   - } catch (Exception e) {
48   - log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e);
49   - throw e;
50   - }
51   - }
52   -
53   - @PreDestroy
54   - public void stop() {
55   - stopHttpServer();
56   - log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
57   - }
58   -
59   - /**
60   - * 启动 HTTP 服务器
61   - */
62   - private void startHttpServer() {
63   - int port = emqxProperties.getHttpPort();
64   -
65   - // 1. 创建路由
66   - Router router = Router.router(vertx);
67   - router.route().handler(BodyHandler.create());
68   -
69   - // 2. 创建处理器,传入 serverId
70   - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
71   - router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
72   - router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
73   - // TODO @haohao:/mqtt/acl 需要处理么?
74   - // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理
75   -
76   - // 3. 启动 HTTP 服务器
77   - try {
78   - httpServer = vertx.createHttpServer()
79   - .requestHandler(router)
80   - .listen(port)
81   - .result();
82   - } catch (Exception e) {
83   - log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e);
84   - throw e;
85   - }
86   - }
87   -
88   - /**
89   - * 停止 HTTP 服务器
90   - */
91   - private void stopHttpServer() {
92   - if (httpServer == null) {
93   - return;
94   - }
95   -
96   - try {
97   - httpServer.close().result();
98   - log.info("[stopHttpServer][HTTP 服务器已停止]");
99   - } catch (Exception e) {
100   - log.error("[stopHttpServer][HTTP 服务器停止失败]", e);
101   - }
102   - }
103   -
104   -}
105 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  2 +
  3 +import jakarta.validation.Valid;
  4 +import jakarta.validation.constraints.Max;
  5 +import jakarta.validation.constraints.Min;
  6 +import jakarta.validation.constraints.NotEmpty;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import lombok.Data;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * IoT EMQX 协议配置
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class IotEmqxConfig {
  19 +
  20 + // ========== MQTT Client 配置(连接 EMQX Broker) ==========
  21 +
  22 + /**
  23 + * MQTT 服务器地址
  24 + */
  25 + @NotEmpty(message = "MQTT 服务器地址不能为空")
  26 + private String mqttHost;
  27 +
  28 + /**
  29 + * MQTT 服务器端口(默认:1883)
  30 + */
  31 + @NotNull(message = "MQTT 服务器端口不能为空")
  32 + private Integer mqttPort = 1883;
  33 +
  34 + /**
  35 + * MQTT 用户名
  36 + */
  37 + @NotEmpty(message = "MQTT 用户名不能为空")
  38 + private String mqttUsername;
  39 +
  40 + /**
  41 + * MQTT 密码
  42 + */
  43 + @NotEmpty(message = "MQTT 密码不能为空")
  44 + private String mqttPassword;
  45 +
  46 + /**
  47 + * MQTT 客户端的 SSL 开关
  48 + */
  49 + @NotNull(message = "MQTT 是否开启 SSL 不能为空")
  50 + private Boolean mqttSsl = false;
  51 +
  52 + /**
  53 + * MQTT 客户端 ID
  54 + */
  55 + @NotEmpty(message = "MQTT 客户端 ID 不能为空")
  56 + private String mqttClientId;
  57 +
  58 + /**
  59 + * MQTT 订阅的主题
  60 + */
  61 + @NotEmpty(message = "MQTT 主题不能为空")
  62 + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
  63 +
  64 + /**
  65 + * 默认 QoS 级别
  66 + * <p>
  67 + * 0 - 最多一次
  68 + * 1 - 至少一次
  69 + * 2 - 刚好一次
  70 + */
  71 + @NotNull(message = "MQTT QoS 不能为空")
  72 + @Min(value = 0, message = "MQTT QoS 不能小于 0")
  73 + @Max(value = 2, message = "MQTT QoS 不能大于 2")
  74 + private Integer mqttQos = 1;
  75 +
  76 + /**
  77 + * 连接超时时间(秒)
  78 + */
  79 + @NotNull(message = "连接超时时间不能为空")
  80 + @Min(value = 1, message = "连接超时时间不能小于 1 秒")
  81 + private Integer connectTimeoutSeconds = 10;
  82 +
  83 + /**
  84 + * 重连延迟时间(毫秒)
  85 + */
  86 + @NotNull(message = "重连延迟时间不能为空")
  87 + @Min(value = 0, message = "重连延迟时间不能小于 0 毫秒")
  88 + private Long reconnectDelayMs = 5000L;
  89 +
  90 + /**
  91 + * 是否启用 Clean Session (清理会话)
  92 + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。
  93 + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
  94 + */
  95 + @NotNull(message = "是否启用 Clean Session 不能为空")
  96 + private Boolean cleanSession = true;
  97 +
  98 + /**
  99 + * 心跳间隔(秒)
  100 + * 用于保持连接活性,及时发现网络中断。
  101 + */
  102 + @NotNull(message = "心跳间隔不能为空")
  103 + @Min(value = 1, message = "心跳间隔不能小于 1 秒")
  104 + private Integer keepAliveIntervalSeconds = 60;
  105 +
  106 + /**
  107 + * 最大未确认消息队列大小
  108 + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
  109 + */
  110 + @NotNull(message = "最大未确认消息队列大小不能为空")
  111 + @Min(value = 1, message = "最大未确认消息队列大小不能小于 1")
  112 + private Integer maxInflightQueue = 10000;
  113 +
  114 + /**
  115 + * 是否信任所有 SSL 证书
  116 + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
  117 + * 在生产环境中,应设置为 false,并配置正确的信任库。
  118 + */
  119 + @NotNull(message = "是否信任所有 SSL 证书不能为空")
  120 + private Boolean trustAll = false;
  121 +
  122 + // ========== MQTT Will / SSL 高级配置 ==========
  123 +
  124 + /**
  125 + * 遗嘱消息配置 (用于网关异常下线时通知其他系统)
  126 + */
  127 + @Valid
  128 + private Will will = new Will();
  129 +
  130 + /**
  131 + * 高级 SSL/TLS 配置 (用于生产环境)
  132 + */
  133 + @Valid
  134 + private Ssl sslOptions = new Ssl();
  135 +
  136 + // ========== HTTP Hook 配置(网关提供给 EMQX 调用) ==========
  137 +
  138 + /**
  139 + * HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event)
  140 + */
  141 + @Valid
  142 + private Http http = new Http();
  143 +
  144 + /**
  145 + * 遗嘱消息 (Last Will and Testament)
  146 + */
  147 + @Data
  148 + public static class Will {
  149 +
  150 + /**
  151 + * 是否启用遗嘱消息
  152 + */
  153 + private boolean enabled = false;
  154 + /**
  155 + * 遗嘱消息主题
  156 + */
  157 + private String topic;
  158 + /**
  159 + * 遗嘱消息内容
  160 + */
  161 + private String payload;
  162 + /**
  163 + * 遗嘱消息 QoS 等级
  164 + */
  165 + @Min(value = 0, message = "遗嘱消息 QoS 不能小于 0")
  166 + @Max(value = 2, message = "遗嘱消息 QoS 不能大于 2")
  167 + private Integer qos = 1;
  168 + /**
  169 + * 遗嘱消息是否作为保留消息发布
  170 + */
  171 + private boolean retain = true;
  172 +
  173 + }
  174 +
  175 + /**
  176 + * 高级 SSL/TLS 配置
  177 + */
  178 + @Data
  179 + public static class Ssl {
  180 +
  181 + /**
  182 + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks
  183 + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
  184 + */
  185 + private String keyStorePath;
  186 + /**
  187 + * 密钥库密码
  188 + */
  189 + private String keyStorePassword;
  190 + /**
  191 + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks
  192 + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
  193 + */
  194 + private String trustStorePath;
  195 + /**
  196 + * 信任库密码
  197 + */
  198 + private String trustStorePassword;
  199 +
  200 + }
  201 +
  202 + /**
  203 + * HTTP Hook 服务 SSL 配置
  204 + */
  205 + @Data
  206 + public static class Http {
  207 +
  208 + /**
  209 + * 是否启用 SSL
  210 + */
  211 + private Boolean sslEnabled = false;
  212 +
  213 + /**
  214 + * SSL 证书路径
  215 + */
  216 + private String sslCertPath;
  217 +
  218 + /**
  219 + * SSL 私钥路径
  220 + */
  221 + private String sslKeyPath;
  222 +
  223 + }
  224 +
  225 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.extra.spring.SpringUtil;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  9 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  16 +import io.netty.handler.codec.mqtt.MqttQoS;
  17 +import io.vertx.core.Vertx;
  18 +import io.vertx.core.buffer.Buffer;
  19 +import io.vertx.core.http.HttpServer;
  20 +import io.vertx.core.http.HttpServerOptions;
  21 +import io.vertx.core.net.JksOptions;
  22 +import io.vertx.core.net.PemKeyCertOptions;
  23 +import io.vertx.ext.web.Router;
  24 +import io.vertx.ext.web.handler.BodyHandler;
  25 +import io.vertx.mqtt.MqttClient;
  26 +import io.vertx.mqtt.MqttClientOptions;
  27 +import lombok.Getter;
  28 +import lombok.extern.slf4j.Slf4j;
  29 +
  30 +import java.util.List;
  31 +import java.util.Map;
  32 +import java.util.concurrent.TimeUnit;
  33 +
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  35 +
  36 +/**
  37 + * IoT 网关 EMQX 协议实现:
  38 + * <p>
  39 + * 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用
  40 + * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker
  41 + *
  42 + * @author 芋道源码
  43 + */
  44 +@Slf4j
  45 +public class IotEmqxProtocol implements IotProtocol {
  46 +
  47 + /**
  48 + * 协议配置
  49 + */
  50 + private final ProtocolProperties properties;
  51 + /**
  52 + * EMQX 配置
  53 + */
  54 + private final IotEmqxConfig emqxConfig;
  55 + /**
  56 + * 服务器 ID
  57 + */
  58 + @Getter
  59 + private final String serverId;
  60 +
  61 + /**
  62 + * 运行状态
  63 + */
  64 + @Getter
  65 + private volatile boolean running = false;
  66 +
  67 + /**
  68 + * Vert.x 实例
  69 + */
  70 + private Vertx vertx;
  71 + /**
  72 + * HTTP Hook 服务器
  73 + */
  74 + private HttpServer httpServer;
  75 +
  76 + /**
  77 + * MQTT Client
  78 + */
  79 + private volatile MqttClient mqttClient;
  80 + /**
  81 + * MQTT 重连定时器 ID
  82 + */
  83 + private volatile Long reconnectTimerId;
  84 +
  85 + /**
  86 + * 上行消息处理器
  87 + */
  88 + private final IotEmqxUpstreamHandler upstreamHandler;
  89 +
  90 + /**
  91 + * 下行消息订阅者
  92 + */
  93 + private final IotEmqxDownstreamSubscriber downstreamSubscriber;
  94 +
  95 + public IotEmqxProtocol(ProtocolProperties properties) {
  96 + Assert.notNull(properties, "协议实例配置不能为空");
  97 + Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空");
  98 + this.properties = properties;
  99 + this.emqxConfig = properties.getEmqx();
  100 + Assert.notNull(emqxConfig.getConnectTimeoutSeconds(),
  101 + "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
  102 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  103 + this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
  104 +
  105 + // 初始化下行消息订阅者
  106 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  107 + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
  108 + }
  109 +
  110 + @Override
  111 + public String getId() {
  112 + return properties.getId();
  113 + }
  114 +
  115 + @Override
  116 + public IotProtocolTypeEnum getType() {
  117 + return IotProtocolTypeEnum.EMQX;
  118 + }
  119 +
  120 + @Override
  121 + public void start() {
  122 + if (running) {
  123 + log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId());
  124 + return;
  125 + }
  126 +
  127 + // 1.1 创建 Vertx 实例
  128 + this.vertx = Vertx.vertx();
  129 +
  130 + try {
  131 + // 1.2 启动 HTTP Hook 服务
  132 + startHttpServer();
  133 +
  134 + // 1.3 启动 MQTT Client
  135 + startMqttClient();
  136 + running = true;
  137 + log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]",
  138 + getId(), properties.getPort(), serverId);
  139 +
  140 + // 2. 启动下行消息订阅者
  141 + this.downstreamSubscriber.start();
  142 + } catch (Exception e) {
  143 + log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
  144 + // 启动失败时,关闭资源
  145 + stop0();
  146 + throw e;
  147 + }
  148 + }
  149 +
  150 + @Override
  151 + public void stop() {
  152 + if (!running) {
  153 + return;
  154 + }
  155 + stop0();
  156 + }
  157 +
  158 + private void stop0() {
  159 + // 1. 停止下行消息订阅者
  160 + try {
  161 + downstreamSubscriber.stop();
  162 + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
  163 + } catch (Exception e) {
  164 + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
  165 + }
  166 +
  167 + // 2.1 先置为 false:避免 closeHandler 触发重连
  168 + running = false;
  169 + stopMqttClientReconnectChecker();
  170 + // 2.2 停止 MQTT Client
  171 + stopMqttClient();
  172 +
  173 + // 2.3 停止 HTTP Hook 服务
  174 + stopHttpServer();
  175 +
  176 + // 2.4 关闭 Vertx
  177 + if (vertx != null) {
  178 + try {
  179 + vertx.close().toCompletionStage().toCompletableFuture()
  180 + .get(10, TimeUnit.SECONDS);
  181 + log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId());
  182 + } catch (Exception e) {
  183 + log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e);
  184 + }
  185 + vertx = null;
  186 + }
  187 +
  188 + log.info("[stop][IoT EMQX 协议 {} 已停止]", getId());
  189 + }
  190 +
  191 + // ======================================= HTTP Hook Server =======================================
  192 +
  193 + /**
  194 + * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)
  195 + */
  196 + private void startHttpServer() {
  197 + // 1. 创建路由
  198 + Router router = Router.router(vertx);
  199 + router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击
  200 +
  201 + // 2. 创建处理器
  202 + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this);
  203 + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
  204 + router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl);
  205 + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
  206 +
  207 + // 3. 启动 HTTP Server(支持 HTTPS)
  208 + IotEmqxConfig.Http httpConfig = emqxConfig.getHttp();
  209 + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
  210 + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
  211 + Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空");
  212 + Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空");
  213 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  214 + .setKeyPath(httpConfig.getSslKeyPath())
  215 + .setCertPath(httpConfig.getSslCertPath());
  216 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  217 + }
  218 + try {
  219 + httpServer = vertx.createHttpServer(options)
  220 + .requestHandler(router)
  221 + .listen()
  222 + .toCompletionStage().toCompletableFuture()
  223 + .get(10, TimeUnit.SECONDS);
  224 + log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]",
  225 + getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled()));
  226 + } catch (Exception e) {
  227 + log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e);
  228 + throw new RuntimeException("HTTP Hook 服务启动失败", e);
  229 + }
  230 + }
  231 +
  232 + private void stopHttpServer() {
  233 + if (httpServer == null) {
  234 + return;
  235 + }
  236 + try {
  237 + httpServer.close().toCompletionStage().toCompletableFuture()
  238 + .get(5, TimeUnit.SECONDS);
  239 + log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId());
  240 + } catch (Exception e) {
  241 + log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e);
  242 + } finally {
  243 + httpServer = null;
  244 + }
  245 + }
  246 +
  247 + // ======================================= MQTT Client ======================================
  248 +
  249 + private void startMqttClient() {
  250 + // 1.1 创建 MQTT Client
  251 + MqttClient client = createMqttClient();
  252 + this.mqttClient = client;
  253 + // 1.2 连接 MQTT Broker
  254 + if (!connectMqttClient(client)) {
  255 + throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败");
  256 + }
  257 +
  258 + // 2. 启动定时重连检查
  259 + startMqttClientReconnectChecker();
  260 + }
  261 +
  262 + private void stopMqttClient() {
  263 + MqttClient client = this.mqttClient;
  264 + this.mqttClient = null; // 先清理引用
  265 + if (client == null) {
  266 + return;
  267 + }
  268 +
  269 + // 1. 批量取消订阅(仅在连接时)
  270 + if (client.isConnected()) {
  271 + List<String> topicList = emqxConfig.getMqttTopics();
  272 + if (CollUtil.isNotEmpty(topicList)) {
  273 + try {
  274 + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture()
  275 + .get(5, TimeUnit.SECONDS);
  276 + } catch (Exception e) {
  277 + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e);
  278 + }
  279 + }
  280 + }
  281 +
  282 + // 2. 断开 MQTT 连接
  283 + try {
  284 + client.disconnect().toCompletionStage().toCompletableFuture()
  285 + .get(5, TimeUnit.SECONDS);
  286 + } catch (Exception e) {
  287 + log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e);
  288 + }
  289 + }
  290 +
  291 + // ======================================= MQTT 基础方法 ======================================
  292 +
  293 + /**
  294 + * 创建 MQTT 客户端
  295 + *
  296 + * @return 新创建的 MqttClient
  297 + */
  298 + private MqttClient createMqttClient() {
  299 + // 1.1 基础配置
  300 + MqttClientOptions options = new MqttClientOptions()
  301 + .setClientId(emqxConfig.getMqttClientId())
  302 + .setUsername(emqxConfig.getMqttUsername())
  303 + .setPassword(emqxConfig.getMqttPassword())
  304 + .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl()))
  305 + .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession()))
  306 + .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds())
  307 + .setMaxInflightQueue(emqxConfig.getMaxInflightQueue());
  308 + options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒
  309 + options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll()));
  310 + // 1.2 配置遗嘱消息
  311 + IotEmqxConfig.Will will = emqxConfig.getWill();
  312 + if (will != null && will.isEnabled()) {
  313 + Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空");
  314 + Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空");
  315 + options.setWillFlag(true)
  316 + .setWillTopic(will.getTopic())
  317 + .setWillMessageBytes(Buffer.buffer(will.getPayload()))
  318 + .setWillQoS(will.getQos())
  319 + .setWillRetain(will.isRetain());
  320 + }
  321 + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空)
  322 + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions();
  323 + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl())
  324 + && Boolean.FALSE.equals(emqxConfig.getTrustAll())
  325 + && sslOptions != null) {
  326 + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
  327 + options.setTrustStoreOptions(new JksOptions()
  328 + .setPath(sslOptions.getTrustStorePath())
  329 + .setPassword(sslOptions.getTrustStorePassword()));
  330 + }
  331 + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
  332 + options.setKeyStoreOptions(new JksOptions()
  333 + .setPath(sslOptions.getKeyStorePath())
  334 + .setPassword(sslOptions.getKeyStorePassword()));
  335 + }
  336 + }
  337 +
  338 + // 2. 创建客户端
  339 + return MqttClient.create(vertx, options);
  340 + }
  341 +
  342 + /**
  343 + * 连接 MQTT Broker(同步等待)
  344 + *
  345 + * @param client MQTT 客户端
  346 + * @return 连接成功返回 true,失败返回 false
  347 + */
  348 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
  349 + private synchronized boolean connectMqttClient(MqttClient client) {
  350 + String host = emqxConfig.getMqttHost();
  351 + int port = emqxConfig.getMqttPort();
  352 + int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds();
  353 + try {
  354 + // 1. 连接 Broker
  355 + client.connect(port, host).toCompletionStage().toCompletableFuture()
  356 + .get(timeoutSeconds, TimeUnit.SECONDS);
  357 + log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]",
  358 + getId(), host, port);
  359 +
  360 + // 2. 设置处理器
  361 + setupMqttClientHandlers(client);
  362 + subscribeMqttClientTopics(client);
  363 + return true;
  364 + } catch (Exception e) {
  365 + log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e);
  366 + return false;
  367 + }
  368 + }
  369 +
  370 + /**
  371 + * 关闭 MQTT 客户端
  372 + */
  373 + private void closeMqttClient() {
  374 + MqttClient oldClient = this.mqttClient;
  375 + this.mqttClient = null; // 先清理引用
  376 + if (oldClient == null) {
  377 + return;
  378 + }
  379 + // 尽力释放(无论是否连接都尝试 disconnect)
  380 + try {
  381 + oldClient.disconnect().toCompletionStage().toCompletableFuture()
  382 + .get(5, TimeUnit.SECONDS);
  383 + } catch (Exception ignored) {
  384 + }
  385 + }
  386 +
  387 + // ======================================= MQTT 重连机制 ======================================
  388 +
  389 + /**
  390 + * 启动 MQTT Client 周期性重连检查器
  391 + */
  392 + private void startMqttClientReconnectChecker() {
  393 + long interval = emqxConfig.getReconnectDelayMs();
  394 + this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> {
  395 + if (!running) {
  396 + return;
  397 + }
  398 + if (mqttClient != null && mqttClient.isConnected()) {
  399 + return;
  400 + }
  401 + log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId());
  402 + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待)
  403 + vertx.executeBlocking(() -> {
  404 + tryReconnectMqttClient();
  405 + return null;
  406 + });
  407 + });
  408 + }
  409 +
  410 + /**
  411 + * 停止 MQTT Client 重连检查器
  412 + */
  413 + private void stopMqttClientReconnectChecker() {
  414 + if (reconnectTimerId != null && vertx != null) {
  415 + try {
  416 + vertx.cancelTimer(reconnectTimerId);
  417 + } catch (Exception ignored) {
  418 + }
  419 + reconnectTimerId = null;
  420 + }
  421 + }
  422 +
  423 + /**
  424 + * 尝试重连 MQTT Client
  425 + */
  426 + private synchronized void tryReconnectMqttClient() {
  427 + // 1. 前置检查
  428 + if (!running) {
  429 + return;
  430 + }
  431 + if (mqttClient != null && mqttClient.isConnected()) {
  432 + return;
  433 + }
  434 +
  435 + log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId());
  436 + try {
  437 + // 2. 关闭旧客户端
  438 + closeMqttClient();
  439 +
  440 + // 3.1 创建新客户端
  441 + MqttClient client = createMqttClient();
  442 + this.mqttClient = client;
  443 + // 3.2 连接(失败只打印日志,等下次定时)
  444 + if (!connectMqttClient(client)) {
  445 + log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId());
  446 + }
  447 + } catch (Exception e) {
  448 + log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e);
  449 + }
  450 + }
  451 +
  452 + // ======================================= MQTT Handler ======================================
  453 +
  454 + /**
  455 + * 设置 MQTT Client 事件处理器
  456 + */
  457 + private void setupMqttClientHandlers(MqttClient client) {
  458 + // 1. 断开重连监听
  459 + client.closeHandler(closeEvent -> {
  460 + if (!running) {
  461 + return;
  462 + }
  463 + log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId());
  464 + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待)
  465 + vertx.executeBlocking(() -> {
  466 + tryReconnectMqttClient();
  467 + return null;
  468 + });
  469 + });
  470 +
  471 + // 2. 异常处理
  472 + client.exceptionHandler(exception ->
  473 + log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception));
  474 +
  475 + // 3. 上行消息处理
  476 + client.publishHandler(upstreamHandler::handle);
  477 + }
  478 +
  479 + /**
  480 + * 订阅 MQTT Client 主题(同步等待)
  481 + */
  482 + private void subscribeMqttClientTopics(MqttClient client) {
  483 + List<String> topicList = emqxConfig.getMqttTopics();
  484 + if (!client.isConnected()) {
  485 + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId());
  486 + return;
  487 + }
  488 + if (CollUtil.isEmpty(topicList)) {
  489 + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId());
  490 + return;
  491 + }
  492 + // 执行订阅
  493 + Map<String, Integer> topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic,
  494 + topic -> emqxConfig.getMqttQos());
  495 + try {
  496 + client.subscribe(topics).toCompletionStage().toCompletableFuture()
  497 + .get(10, TimeUnit.SECONDS);
  498 + log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size());
  499 + } catch (Exception e) {
  500 + log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e);
  501 + }
  502 + }
  503 +
  504 + /**
  505 + * 发布消息到 MQTT Broker
  506 + *
  507 + * @param topic 主题
  508 + * @param payload 消息内容
  509 + */
  510 + public void publishMessage(String topic, byte[] payload) {
  511 + if (mqttClient == null || !mqttClient.isConnected()) {
  512 + log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId());
  513 + return;
  514 + }
  515 + MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos());
  516 + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false)
  517 + .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e));
  518 + }
  519 +
  520 + /**
  521 + * 延迟发布消息到 MQTT Broker
  522 + *
  523 + * @param topic 主题
  524 + * @param payload 消息内容
  525 + * @param delayMs 延迟时间(毫秒)
  526 + */
  527 + public void publishDelayMessage(String topic, byte[] payload, long delayMs) {
  528 + vertx.setTimer(delayMs, id -> publishMessage(topic, payload));
  529 + }
  530 +
  531 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
6   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler;
8   -import io.netty.handler.codec.mqtt.MqttQoS;
9   -import io.vertx.core.Vertx;
10   -import io.vertx.core.buffer.Buffer;
11   -import io.vertx.core.net.JksOptions;
12   -import io.vertx.mqtt.MqttClient;
13   -import io.vertx.mqtt.MqttClientOptions;
14   -import jakarta.annotation.PostConstruct;
15   -import jakarta.annotation.PreDestroy;
16   -import lombok.Getter;
17   -import lombok.extern.slf4j.Slf4j;
18   -
19   -import java.util.HashMap;
20   -import java.util.List;
21   -import java.util.Map;
22   -import java.util.concurrent.CountDownLatch;
23   -import java.util.concurrent.atomic.AtomicBoolean;
24   -
25   -/**
26   - * IoT 网关 EMQX 协议:接收设备上行消息
27   - *
28   - * @author 芋道源码
29   - */
30   -@Slf4j
31   -public class IotEmqxUpstreamProtocol {
32   -
33   - private final IotGatewayProperties.EmqxProperties emqxProperties;
34   -
35   - private volatile boolean isRunning = false;
36   -
37   - private final Vertx vertx;
38   -
39   - @Getter
40   - private final String serverId;
41   -
42   - private MqttClient mqttClient;
43   -
44   - private IotEmqxUpstreamHandler upstreamHandler;
45   -
46   - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
47   - Vertx vertx) {
48   - this.emqxProperties = emqxProperties;
49   - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
50   - this.vertx = vertx;
51   - }
52   -
53   - @PostConstruct
54   - public void start() {
55   - if (isRunning) {
56   - return;
57   - }
58   -
59   - try {
60   - // 1. 启动 MQTT 客户端
61   - startMqttClient();
62   -
63   - // 2. 标记服务为运行状态
64   - isRunning = true;
65   - log.info("[start][IoT 网关 EMQX 协议启动成功]");
66   - } catch (Exception e) {
67   - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
68   - stop();
69   -
70   - // 异步关闭应用
71   - Thread shutdownThread = new Thread(() -> {
72   - try {
73   - // 确保日志输出完成,使用更优雅的方式
74   - log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
75   - // 等待日志输出完成
76   - Thread.sleep(1000);
77   - } catch (InterruptedException ie) {
78   - Thread.currentThread().interrupt();
79   - log.warn("[start][应用关闭被中断]");
80   - }
81   - System.exit(1);
82   - });
83   - shutdownThread.setDaemon(true);
84   - shutdownThread.setName("emergency-shutdown");
85   - shutdownThread.start();
86   -
87   - throw e;
88   - }
89   - }
90   -
91   - @PreDestroy
92   - public void stop() {
93   - if (!isRunning) {
94   - return;
95   - }
96   -
97   - // 1. 停止 MQTT 客户端
98   - stopMqttClient();
99   -
100   - // 2. 标记服务为停止状态
101   - isRunning = false;
102   - log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
103   - }
104   -
105   - /**
106   - * 启动 MQTT 客户端
107   - */
108   - private void startMqttClient() {
109   - try {
110   - // 1. 初始化消息处理器
111   - this.upstreamHandler = new IotEmqxUpstreamHandler(this);
112   -
113   - // 2. 创建 MQTT 客户端
114   - createMqttClient();
115   -
116   - // 3. 同步连接 MQTT Broker
117   - connectMqttSync();
118   - } catch (Exception e) {
119   - log.error("[startMqttClient][MQTT 客户端启动失败]", e);
120   - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e);
121   - }
122   - }
123   -
124   - /**
125   - * 同步连接 MQTT Broker
126   - */
127   - private void connectMqttSync() {
128   - String host = emqxProperties.getMqttHost();
129   - int port = emqxProperties.getMqttPort();
130   - // 1. 连接 MQTT Broker
131   - CountDownLatch latch = new CountDownLatch(1);
132   - AtomicBoolean success = new AtomicBoolean(false);
133   - mqttClient.connect(port, host, connectResult -> {
134   - if (connectResult.succeeded()) {
135   - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
136   - setupMqttHandlers();
137   - subscribeToTopics();
138   - success.set(true);
139   - } else {
140   - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
141   - host, port, connectResult.cause());
142   - }
143   - latch.countDown();
144   - });
145   -
146   - // 2. 等待连接结果
147   - try {
148   - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制
149   - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
150   - if (!awaitResult) {
151   - log.error("[connectMqttSync][等待连接结果超时]");
152   - throw new RuntimeException("连接 MQTT Broker 超时");
153   - }
154   - if (!success.get()) {
155   - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port));
156   - }
157   - } catch (InterruptedException e) {
158   - Thread.currentThread().interrupt();
159   - log.error("[connectMqttSync][等待连接结果被中断]", e);
160   - throw new RuntimeException("连接 MQTT Broker 被中断", e);
161   - }
162   - }
163   -
164   - /**
165   - * 异步连接 MQTT Broker
166   - */
167   - private void connectMqttAsync() {
168   - String host = emqxProperties.getMqttHost();
169   - int port = emqxProperties.getMqttPort();
170   - mqttClient.connect(port, host, connectResult -> {
171   - if (connectResult.succeeded()) {
172   - log.info("[connectMqttAsync][MQTT 客户端重连成功]");
173   - setupMqttHandlers();
174   - subscribeToTopics();
175   - } else {
176   - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]",
177   - host, port, connectResult.cause());
178   - log.warn("[connectMqttAsync][重连失败,将再次尝试]");
179   - reconnectWithDelay();
180   - }
181   - });
182   - }
183   -
184   - /**
185   - * 延迟重连
186   - */
187   - private void reconnectWithDelay() {
188   - if (!isRunning) {
189   - return;
190   - }
191   - if (mqttClient != null && mqttClient.isConnected()) {
192   - return;
193   - }
194   -
195   - long delay = emqxProperties.getReconnectDelayMs();
196   - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay);
197   - vertx.setTimer(delay, timerId -> {
198   - if (!isRunning) {
199   - return;
200   - }
201   - if (mqttClient != null && mqttClient.isConnected()) {
202   - return;
203   - }
204   -
205   - log.info("[reconnectWithDelay][开始重连 MQTT Broker]");
206   - try {
207   - createMqttClient();
208   - connectMqttAsync();
209   - } catch (Exception e) {
210   - log.error("[reconnectWithDelay][重连过程中发生异常]", e);
211   - vertx.setTimer(delay, t -> reconnectWithDelay());
212   - }
213   - });
214   - }
215   -
216   - /**
217   - * 停止 MQTT 客户端
218   - */
219   - private void stopMqttClient() {
220   - if (mqttClient == null) {
221   - return;
222   - }
223   - try {
224   - if (mqttClient.isConnected()) {
225   - // 1. 取消订阅所有主题
226   - List<String> topicList = emqxProperties.getMqttTopics();
227   - for (String topic : topicList) {
228   - try {
229   - mqttClient.unsubscribe(topic);
230   - } catch (Exception e) {
231   - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e);
232   - }
233   - }
234   -
235   - // 2. 断开 MQTT 客户端连接
236   - try {
237   - CountDownLatch disconnectLatch = new CountDownLatch(1);
238   - mqttClient.disconnect(ar -> disconnectLatch.countDown());
239   - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) {
240   - log.warn("[stopMqttClient][断开 MQTT 连接超时]");
241   - }
242   - } catch (Exception e) {
243   - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e);
244   - }
245   - }
246   - } catch (Exception e) {
247   - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e);
248   - } finally {
249   - mqttClient = null;
250   - }
251   - }
252   -
253   - /**
254   - * 创建 MQTT 客户端
255   - */
256   - private void createMqttClient() {
257   - // 1.1 创建基础配置
258   - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions()
259   - .setClientId(emqxProperties.getMqttClientId())
260   - .setUsername(emqxProperties.getMqttUsername())
261   - .setPassword(emqxProperties.getMqttPassword())
262   - .setSsl(emqxProperties.getMqttSsl())
263   - .setCleanSession(emqxProperties.getCleanSession())
264   - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds())
265   - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue())
266   - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒
267   - .setTrustAll(emqxProperties.getTrustAll());
268   - // 1.2 配置遗嘱消息
269   - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill();
270   - if (will.isEnabled()) {
271   - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空");
272   - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空");
273   - options.setWillFlag(true)
274   - .setWillTopic(will.getTopic())
275   - .setWillMessageBytes(Buffer.buffer(will.getPayload()))
276   - .setWillQoS(will.getQos())
277   - .setWillRetain(will.isRetain());
278   - }
279   - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效)
280   - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
281   - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions();
282   - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
283   - options.setTrustStoreOptions(new JksOptions()
284   - .setPath(sslOptions.getTrustStorePath())
285   - .setPassword(sslOptions.getTrustStorePassword()));
286   - }
287   - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
288   - options.setKeyStoreOptions(new JksOptions()
289   - .setPath(sslOptions.getKeyStorePath())
290   - .setPassword(sslOptions.getKeyStorePassword()));
291   - }
292   - }
293   - // 1.4 安全警告日志
294   - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
295   - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]");
296   - }
297   -
298   - // 2. 创建客户端实例
299   - this.mqttClient = MqttClient.create(vertx, options);
300   - }
301   -
302   - /**
303   - * 设置 MQTT 处理器
304   - */
305   - private void setupMqttHandlers() {
306   - // 1. 设置断开重连监听器
307   - mqttClient.closeHandler(closeEvent -> {
308   - if (!isRunning) {
309   - return;
310   - }
311   - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
312   - reconnectWithDelay();
313   - });
314   -
315   - // 2. 设置异常处理器
316   - mqttClient.exceptionHandler(exception ->
317   - log.error("[exceptionHandler][MQTT 客户端异常]", exception));
318   -
319   - // 3. 设置消息处理器
320   - mqttClient.publishHandler(upstreamHandler::handle);
321   - }
322   -
323   - /**
324   - * 订阅设备上行消息主题
325   - */
326   - private void subscribeToTopics() {
327   - // 1. 校验 MQTT 客户端是否连接
328   - List<String> topicList = emqxProperties.getMqttTopics();
329   - if (mqttClient == null || !mqttClient.isConnected()) {
330   - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]");
331   - return;
332   - }
333   -
334   - // 2. 批量订阅所有主题
335   - Map<String, Integer> topics = new HashMap<>();
336   - int qos = emqxProperties.getMqttQos();
337   - for (String topic : topicList) {
338   - topics.put(topic, qos);
339   - }
340   - mqttClient.subscribe(topics, subscribeResult -> {
341   - if (subscribeResult.succeeded()) {
342   - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size());
343   - } else {
344   - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]",
345   - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause());
346   - }
347   - });
348   - }
349   -
350   - /**
351   - * 发布消息到 MQTT Broker
352   - *
353   - * @param topic 主题
354   - * @param payload 消息内容
355   - */
356   - public void publishMessage(String topic, byte[] payload) {
357   - if (mqttClient == null || !mqttClient.isConnected()) {
358   - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]");
359   - return;
360   - }
361   - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos());
362   - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false);
363   - }
364   -
365   -}
366 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
9 9 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
10 10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11 11 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
... ... @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j;
21 21 @Slf4j
22 22 public class IotEmqxDownstreamHandler {
23 23  
24   - private final IotEmqxUpstreamProtocol protocol;
  24 + private final IotEmqxProtocol protocol;
25 25  
26 26 private final IotDeviceService deviceService;
27 27  
28 28 private final IotDeviceMessageService deviceMessageService;
29 29  
30   - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
  30 + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) {
31 31 this.protocol = protocol;
32 32 this.deviceService = SpringUtil.getBean(IotDeviceService.class);
33 33 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
... ... @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler {
53 53 return;
54 54 }
55 55 // 2.2 构建载荷
56   - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
  56 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
57 57 deviceInfo.getDeviceName());
58   - // 2.3 发布消息
  58 +
  59 + // 3. 发布消息
59 60 protocol.publishMessage(topic, payload);
60 61 }
61 62  
... ... @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler {
74 75 return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
75 76 }
76 77  
77   -}
78 78 \ No newline at end of file
  79 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 网关 EMQX 订阅者:接收下行给设备的消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +
  17 + private final IotEmqxDownstreamHandler downstreamHandler;
  18 +
  19 + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) {
  20 + super(protocol, messageBus);
  21 + this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
  22 + }
  23 +
  24 + @Override
  25 + protected void handleMessage(IotDeviceMessage message) {
  26 + downstreamHandler.handle(message);
  27 + }
  28 +
  29 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.core.util.BooleanUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.hutool.extra.spring.SpringUtil;
6 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 12 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  13 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
11 15 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
12 17 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  18 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
13 19 import io.vertx.core.json.JsonObject;
14 20 import io.vertx.ext.web.RoutingContext;
15 21 import lombok.extern.slf4j.Slf4j;
16 22  
  23 +import java.util.Locale;
  24 +
17 25 /**
18 26 * IoT 网关 EMQX 认证事件处理器
19 27 * <p>
20 28 * 为 EMQX 提供 HTTP 接口服务,包括:
21   - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
22   - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
  29 + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)}
  30 + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)}
  31 + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)}
  32 + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)}
23 33 *
24 34 * @author 芋道源码
25 35 */
... ... @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler {
45 55 private static final String RESULT_IGNORE = "ignore";
46 56  
47 57 /**
48   - * EMQX 事件类型常量
  58 + * EMQX 事件类型常量 - 客户端连接
49 59 */
50 60 private static final String EVENT_CLIENT_CONNECTED = "client.connected";
  61 + /**
  62 + * EMQX 事件类型常量 - 客户端断开连接
  63 + */
51 64 private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
52 65  
  66 + /**
  67 + * 认证类型标识 - 设备注册
  68 + */
  69 + private static final String AUTH_TYPE_REGISTER = "|authType=register|";
  70 +
53 71 private final String serverId;
54 72  
55   - private final IotDeviceMessageService deviceMessageService;
  73 + private final IotEmqxProtocol protocol;
56 74  
  75 + private final IotDeviceMessageService deviceMessageService;
57 76 private final IotDeviceCommonApi deviceApi;
58 77  
59   - public IotEmqxAuthEventHandler(String serverId) {
  78 + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
60 79 this.serverId = serverId;
  80 + this.protocol = protocol;
61 81 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
62 82 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
63 83 }
64 84  
  85 + // ========== 认证处理 ==========
  86 +
65 87 /**
66 88 * EMQX 认证接口
67 89 */
68 90 public void handleAuth(RoutingContext context) {
  91 + JsonObject body = null;
69 92 try {
70 93 // 1. 参数校验
71   - JsonObject body = parseRequestBody(context);
  94 + body = parseRequestBody(context);
72 95 if (body == null) {
73 96 return;
74 97 }
... ... @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler {
82 105 return;
83 106 }
84 107  
85   - // 2. 执行认证
  108 + // 2.1 情况一:判断是否为注册请求
  109 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
  110 + handleDeviceRegister(context, username, password);
  111 + return;
  112 + }
  113 +
  114 + // 2.2 情况二:执行认证
86 115 boolean authResult = handleDeviceAuth(clientId, username, password);
87 116 log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
88 117 if (authResult) {
... ... @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler {
91 120 sendAuthResponse(context, RESULT_DENY);
92 121 }
93 122 } catch (Exception e) {
94   - log.error("[handleAuth][设备认证异常]", e);
  123 + log.error("[handleAuth][设备认证异常][body={}]", body, e);
95 124 sendAuthResponse(context, RESULT_IGNORE);
96 125 }
97 126 }
98 127  
99 128 /**
  129 + * 解析认证接口请求体
  130 + * <p>
  131 + * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
  132 + *
  133 + * @param context 路由上下文
  134 + * @return 请求体JSON对象,解析失败时返回null
  135 + */
  136 + private JsonObject parseRequestBody(RoutingContext context) {
  137 + try {
  138 + JsonObject body = context.body().asJsonObject();
  139 + if (body == null) {
  140 + log.info("[parseRequestBody][请求体为空]");
  141 + sendAuthResponse(context, RESULT_IGNORE);
  142 + return null;
  143 + }
  144 + return body;
  145 + } catch (Exception e) {
  146 + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
  147 + sendAuthResponse(context, RESULT_IGNORE);
  148 + return null;
  149 + }
  150 + }
  151 +
  152 + /**
  153 + * 执行设备认证
  154 + *
  155 + * @param clientId 客户端ID
  156 + * @param username 用户名
  157 + * @param password 密码
  158 + * @return 认证是否成功
  159 + */
  160 + private boolean handleDeviceAuth(String clientId, String username, String password) {
  161 + try {
  162 + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
  163 + .setClientId(clientId).setUsername(username).setPassword(password));
  164 + result.checkError();
  165 + return BooleanUtil.isTrue(result.getData());
  166 + } catch (Exception e) {
  167 + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
  168 + throw e;
  169 + }
  170 + }
  171 +
  172 + /**
  173 + * 发送 EMQX 认证响应
  174 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
  175 + *
  176 + * @param context 路由上下文
  177 + * @param result 认证结果:allow、deny、ignore
  178 + */
  179 + private void sendAuthResponse(RoutingContext context, String result) {
  180 + // 构建符合 EMQX 官方规范的响应
  181 + JsonObject response = new JsonObject()
  182 + .put("result", result)
  183 + .put("is_superuser", false);
  184 + // 可以根据业务需求添加客户端属性
  185 + // response.put("client_attrs", new JsonObject().put("role", "device"));
  186 + // 可以添加认证过期时间(可选)
  187 + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
  188 +
  189 + // 回复响应
  190 + context.response()
  191 + .setStatusCode(SUCCESS_STATUS_CODE)
  192 + .putHeader("Content-Type", "application/json; charset=utf-8")
  193 + .end(response.encode());
  194 + }
  195 +
  196 + // ========== ACL 处理 ==========
  197 +
  198 + /**
  199 + * EMQX ACL 接口
  200 + * <p>
  201 + * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。
  202 + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。
  203 + */
  204 + public void handleAcl(RoutingContext context) {
  205 + JsonObject body = null;
  206 + try {
  207 + // 1.1 解析请求体
  208 + body = parseRequestBody(context);
  209 + if (body == null) {
  210 + return;
  211 + }
  212 + String username = body.getString("username");
  213 + String topic = body.getString("topic");
  214 + if (StrUtil.hasBlank(username, topic)) {
  215 + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic);
  216 + sendAuthResponse(context, RESULT_IGNORE);
  217 + return;
  218 + }
  219 + // 1.2 解析设备身份
  220 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  221 + if (deviceInfo == null) {
  222 + sendAuthResponse(context, RESULT_IGNORE);
  223 + return;
  224 + }
  225 + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段)
  226 + Boolean subscribe = parseAclSubscribeFlag(body);
  227 + if (subscribe == null) {
  228 + sendAuthResponse(context, RESULT_IGNORE);
  229 + return;
  230 + }
  231 +
  232 + // 2. 执行 ACL 校验
  233 + boolean allowed = subscribe
  234 + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName())
  235 + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  236 + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY);
  237 + } catch (Exception e) {
  238 + log.error("[handleAcl][ACL 处理失败][body={}]", body, e);
  239 + sendAuthResponse(context, RESULT_IGNORE);
  240 + }
  241 + }
  242 +
  243 + /**
  244 + * 解析 ACL 动作类型:订阅/发布
  245 + *
  246 + * @param body ACL 请求体
  247 + * @return true 订阅;false 发布;null 不识别
  248 + */
  249 + private static Boolean parseAclSubscribeFlag(JsonObject body) {
  250 + // 1. action 字段(常见为 publish/subscribe)
  251 + String action = body.getString("action");
  252 + if (StrUtil.isNotBlank(action)) {
  253 + String lower = action.toLowerCase(Locale.ROOT);
  254 + if (lower.contains("sub")) {
  255 + return true;
  256 + }
  257 + if (lower.contains("pub")) {
  258 + return false;
  259 + }
  260 + }
  261 +
  262 + // 2. access 字段:可能是数字或字符串
  263 + Integer access = body.getInteger("access");
  264 + if (access != null) {
  265 + if (access == 1) {
  266 + return true;
  267 + }
  268 + if (access == 2) {
  269 + return false;
  270 + }
  271 + }
  272 + String accessText = body.getString("access");
  273 + if (StrUtil.isNotBlank(accessText)) {
  274 + String lower = accessText.toLowerCase(Locale.ROOT);
  275 + if (lower.contains("sub")) {
  276 + return true;
  277 + }
  278 + if (lower.contains("pub")) {
  279 + return false;
  280 + }
  281 + if (StrUtil.isNumeric(accessText)) {
  282 + int value = Integer.parseInt(accessText);
  283 + if (value == 1) {
  284 + return true;
  285 + }
  286 + if (value == 2) {
  287 + return false;
  288 + }
  289 + }
  290 + }
  291 + return null;
  292 + }
  293 +
  294 + // ========== 事件处理 ==========
  295 +
  296 + /**
100 297 * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
101 298 * 支持的事件类型:client.connected、client.disconnected 等
102 299 */
... ... @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler {
124 321 break;
125 322 }
126 323  
127   - // EMQX Webhook 只需要 200 状态码,无需响应体
  324 + // 3. EMQX Webhook 只需要 200 状态码,无需响应体
128 325 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
129 326 } catch (Exception e) {
130   - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
131   - // 即使处理失败,也返回 200 避免EMQX重试
  327 + log.error("[handleEvent][事件处理失败][body={}]", body, e);
  328 + // 即使处理失败,也返回 200 避免 EMQX 重试
132 329 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
133 330 }
134 331 }
135 332  
136 333 /**
137   - * 处理客户端连接事件
138   - */
139   - private void handleClientConnected(JsonObject body) {
140   - String username = body.getString("username");
141   - log.info("[handleClientConnected][设备上线: {}]", username);
142   - handleDeviceStateChange(username, true);
143   - }
144   -
145   - /**
146   - * 处理客户端断开连接事件
147   - */
148   - private void handleClientDisconnected(JsonObject body) {
149   - String username = body.getString("username");
150   - String reason = body.getString("reason");
151   - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
152   - handleDeviceStateChange(username, false);
153   - }
154   -
155   - /**
156   - * 解析认证接口请求体
157   - * <p>
158   - * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
159   - *
160   - * @param context 路由上下文
161   - * @return 请求体JSON对象,解析失败时返回null
162   - */
163   - private JsonObject parseRequestBody(RoutingContext context) {
164   - try {
165   - JsonObject body = context.body().asJsonObject();
166   - if (body == null) {
167   - log.info("[parseRequestBody][请求体为空]");
168   - sendAuthResponse(context, RESULT_IGNORE);
169   - return null;
170   - }
171   - return body;
172   - } catch (Exception e) {
173   - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
174   - sendAuthResponse(context, RESULT_IGNORE);
175   - return null;
176   - }
177   - }
178   -
179   - /**
180 334 * 解析事件接口请求体
181 335 * <p>
182 336 * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范)
... ... @@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler {
201 355 }
202 356  
203 357 /**
204   - * 执行设备认证
205   - *
206   - * @param clientId 客户端ID
207   - * @param username 用户名
208   - * @param password 密码
209   - * @return 认证是否成功
  358 + * 处理客户端连接事件
210 359 */
211   - private boolean handleDeviceAuth(String clientId, String username, String password) {
212   - try {
213   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
214   - .setClientId(clientId).setUsername(username).setPassword(password));
215   - result.checkError();
216   - return BooleanUtil.isTrue(result.getData());
217   - } catch (Exception e) {
218   - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
219   - throw e;
220   - }
  360 + private void handleClientConnected(JsonObject body) {
  361 + String username = body.getString("username");
  362 + log.info("[handleClientConnected][设备上线: {}]", username);
  363 + handleDeviceStateChange(username, true);
  364 + }
  365 +
  366 + /**
  367 + * 处理客户端断开连接事件
  368 + */
  369 + private void handleClientDisconnected(JsonObject body) {
  370 + String username = body.getString("username");
  371 + String reason = body.getString("reason");
  372 + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
  373 + handleDeviceStateChange(username, false);
221 374 }
222 375  
223 376 /**
... ... @@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler {
247 400 }
248 401 }
249 402  
  403 + // ========= 注册处理 =========
  404 +
250 405 /**
251   - * 发送 EMQX 认证响应
252   - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
  406 + * 处理设备注册请求(一型一密)
253 407 *
254   - * @param context 路由上下文
255   - * @param result 认证结果:allow、deny、ignore
  408 + * @param context 路由上下文
  409 + * @param username 用户名
  410 + * @param password 密码(签名)
256 411 */
257   - private void sendAuthResponse(RoutingContext context, String result) {
258   - // 构建符合 EMQX 官方规范的响应
259   - JsonObject response = new JsonObject()
260   - .put("result", result)
261   - .put("is_superuser", false);
  412 + private void handleDeviceRegister(RoutingContext context, String username, String password) {
  413 + try {
  414 + // 1. 解析设备信息
  415 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  416 + if (deviceInfo == null) {
  417 + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username);
  418 + sendAuthResponse(context, RESULT_DENY);
  419 + return;
  420 + }
262 421  
263   - // 可以根据业务需求添加客户端属性
264   - // response.put("client_attrs", new JsonObject().put("role", "device"));
  422 + // 2. 调用注册 API
  423 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
  424 + .setProductKey(deviceInfo.getProductKey())
  425 + .setDeviceName(deviceInfo.getDeviceName())
  426 + .setSign(password);
  427 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  428 + result.checkError();
265 429  
266   - // 可以添加认证过期时间(可选)
267   - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
  430 + // 3. 允许连接
  431 + log.info("[handleDeviceRegister][设备注册成功: {}]", username);
  432 + sendAuthResponse(context, RESULT_ALLOW);
268 433  
269   - context.response()
270   - .setStatusCode(SUCCESS_STATUS_CODE)
271   - .putHeader("Content-Type", "application/json; charset=utf-8")
272   - .end(response.encode());
  434 + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅)
  435 + sendRegisterResultMessage(username, result.getData());
  436 + } catch (Exception e) {
  437 + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage());
  438 + sendAuthResponse(context, RESULT_DENY);
  439 + }
  440 + }
  441 +
  442 + /**
  443 + * 发送注册结果消息给设备
  444 + * <p>
  445 + * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。
  446 + *
  447 + * @param username 用户名
  448 + * @param result 注册结果
  449 + */
  450 + @SuppressWarnings("DataFlowIssue")
  451 + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) {
  452 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  453 + Assert.notNull(deviceInfo, "设备信息不能为空");
  454 + try {
  455 + // 1.1 构建响应消息
  456 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
  457 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null);
  458 + // 1.2 序列化消息
  459 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage,
  460 + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON);
  461 + // 1.3 构建响应主题
  462 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method,
  463 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true);
  464 +
  465 + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅)
  466 + protocol.publishDelayMessage(replyTopic, encodedData, 5000);
  467 + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic);
  468 + } catch (Exception e) {
  469 + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e);
  470 + }
273 471 }
274 472  
275   -}
276 473 \ No newline at end of file
  474 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2  
  3 +import cn.hutool.core.util.ArrayUtil;
3 4 import cn.hutool.core.util.StrUtil;
4 5 import cn.hutool.extra.spring.SpringUtil;
5 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
7 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 8 import io.vertx.mqtt.messages.MqttPublishMessage;
9 9 import lombok.extern.slf4j.Slf4j;
... ... @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler {
20 20  
21 21 private final String serverId;
22 22  
23   - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
  23 + public IotEmqxUpstreamHandler(String serverId) {
24 24 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
25   - this.serverId = protocol.getServerId();
  25 + this.serverId = serverId;
26 26 }
27 27  
28 28 /**
29 29 * 处理 MQTT 发布消息
30 30 */
31 31 public void handle(MqttPublishMessage mqttMessage) {
32   - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
  32 + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
33 33 String topic = mqttMessage.topicName();
34 34 byte[] payload = mqttMessage.payload().getBytes();
35 35 try {
36 36 // 1. 解析主题,一次性获取所有信息
37 37 String[] topicParts = topic.split("/");
38   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
  38 + String productKey = ArrayUtil.get(topicParts, 2);
  39 + String deviceName = ArrayUtil.get(topicParts, 3);
  40 + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) {
39 41 log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
40 42 return;
41 43 }
42 44  
43   - String productKey = topicParts[2];
44   - String deviceName = topicParts[3];
45   -
46   - // 3. 解码消息
47   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
  45 + // 2. 反序列化消息
  46 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
48 47 if (message == null) {
49 48 log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
50 49 return;
51 50 }
52 51  
53   - // 4. 发送消息到队列
  52 + // 3. 发送消息到队列
54 53 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
55 54 } catch (Exception e) {
56 55 log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
57 56 }
58 57 }
59 58  
60   -}
61 59 \ No newline at end of file
  60 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT HTTP 协议配置
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +@Data
  11 +public class IotHttpConfig {
  12 +
  13 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http;
2   -
3   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import jakarta.annotation.PostConstruct;
8   -import lombok.RequiredArgsConstructor;
9   -import lombok.extern.slf4j.Slf4j;
10   -
11   -/**
12   - * IoT 网关 HTTP 订阅者:接收下行给设备的消息
13   - *
14   - * @author 芋道源码
15   - */
16   -@RequiredArgsConstructor
17   -@Slf4j
18   -public class IotHttpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
19   -
20   - private final IotHttpUpstreamProtocol protocol;
21   -
22   - private final IotMessageBus messageBus;
23   -
24   - @PostConstruct
25   - public void init() {
26   - messageBus.register(this);
27   - }
28   -
29   - @Override
30   - public String getTopic() {
31   - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
32   - }
33   -
34   - @Override
35   - public String getGroup() {
36   - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
37   - return getTopic();
38   - }
39   -
40   - @Override
41   - public void onMessage(IotDeviceMessage message) {
42   - log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message);
43   - }
44   -
45   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  8 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler;
  15 +import io.vertx.core.Vertx;
  16 +import io.vertx.core.http.HttpServer;
  17 +import io.vertx.core.http.HttpServerOptions;
  18 +import io.vertx.core.net.PemKeyCertOptions;
  19 +import io.vertx.ext.web.Router;
  20 +import io.vertx.ext.web.handler.BodyHandler;
  21 +import lombok.Getter;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +
  24 +/**
  25 + * IoT HTTP 协议实现
  26 + * <p>
  27 + * 基于 Vert.x 实现 HTTP 服务器,接收设备上行消息
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@Slf4j
  32 +public class IotHttpProtocol implements IotProtocol {
  33 +
  34 + /**
  35 + * 协议配置
  36 + */
  37 + private final ProtocolProperties properties;
  38 + /**
  39 + * 服务器 ID(用于消息追踪,全局唯一)
  40 + */
  41 + @Getter
  42 + private final String serverId;
  43 +
  44 + /**
  45 + * 运行状态
  46 + */
  47 + @Getter
  48 + private volatile boolean running = false;
  49 +
  50 + /**
  51 + * Vert.x 实例
  52 + */
  53 + private Vertx vertx;
  54 + /**
  55 + * HTTP 服务器
  56 + */
  57 + private HttpServer httpServer;
  58 +
  59 + /**
  60 + * 下行消息订阅者
  61 + */
  62 + private IotHttpDownstreamSubscriber downstreamSubscriber;
  63 +
  64 + public IotHttpProtocol(ProtocolProperties properties) {
  65 + this.properties = properties;
  66 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  67 +
  68 + // 初始化下行消息订阅者
  69 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  70 + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
  71 + }
  72 +
  73 + @Override
  74 + public String getId() {
  75 + return properties.getId();
  76 + }
  77 +
  78 + @Override
  79 + public IotProtocolTypeEnum getType() {
  80 + return IotProtocolTypeEnum.HTTP;
  81 + }
  82 +
  83 + @Override
  84 + public void start() {
  85 + if (running) {
  86 + log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId());
  87 + return;
  88 + }
  89 +
  90 + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理)
  91 + this.vertx = Vertx.vertx();
  92 +
  93 + // 1.2 创建路由
  94 + Router router = Router.router(vertx);
  95 + router.route().handler(BodyHandler.create());
  96 +
  97 + // 1.3 创建处理器,添加路由处理器
  98 + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
  99 + router.post(IotHttpAuthHandler.PATH).handler(authHandler);
  100 + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
  101 + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
  102 + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
  103 + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
  104 + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
  105 + router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
  106 +
  107 + // 1.4 启动 HTTP 服务器
  108 + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
  109 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  110 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
  111 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  112 + .setKeyPath(sslConfig.getSslKeyPath())
  113 + .setCertPath(sslConfig.getSslCertPath());
  114 + options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  115 + }
  116 + try {
  117 + httpServer = vertx.createHttpServer(options)
  118 + .requestHandler(router)
  119 + .listen()
  120 + .result();
  121 + running = true;
  122 + log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{},serverId:{}]",
  123 + getId(), properties.getPort(), serverId);
  124 +
  125 + // 2. 启动下行消息订阅者
  126 + this.downstreamSubscriber.start();
  127 + } catch (Exception e) {
  128 + log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
  129 + // 启动失败时关闭资源
  130 + if (httpServer != null) {
  131 + httpServer.close();
  132 + httpServer = null;
  133 + }
  134 + if (vertx != null) {
  135 + vertx.close();
  136 + vertx = null;
  137 + }
  138 + throw e;
  139 + }
  140 + }
  141 +
  142 + @Override
  143 + public void stop() {
  144 + if (!running) {
  145 + return;
  146 + }
  147 + // 1. 停止下行消息订阅者
  148 + if (downstreamSubscriber != null) {
  149 + try {
  150 + downstreamSubscriber.stop();
  151 + log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId());
  152 + } catch (Exception e) {
  153 + log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  154 + }
  155 + downstreamSubscriber = null;
  156 + }
  157 +
  158 + // 2.1 关闭 HTTP 服务器
  159 + if (httpServer != null) {
  160 + try {
  161 + httpServer.close().result();
  162 + log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId());
  163 + } catch (Exception e) {
  164 + log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e);
  165 + }
  166 + httpServer = null;
  167 + }
  168 + // 2.2 关闭 Vertx 实例
  169 + if (vertx != null) {
  170 + try {
  171 + vertx.close().result();
  172 + log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId());
  173 + } catch (Exception e) {
  174 + log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e);
  175 + }
  176 + vertx = null;
  177 + }
  178 + running = false;
  179 + log.info("[stop][IoT HTTP 协议 {} 已停止]", getId());
  180 + }
  181 +
  182 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http;
2   -
3   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
9   -import io.vertx.core.Vertx;
10   -import io.vertx.core.http.HttpServer;
11   -import io.vertx.core.http.HttpServerOptions;
12   -import io.vertx.core.net.PemKeyCertOptions;
13   -import io.vertx.ext.web.Router;
14   -import io.vertx.ext.web.handler.BodyHandler;
15   -import jakarta.annotation.PostConstruct;
16   -import jakarta.annotation.PreDestroy;
17   -import lombok.Getter;
18   -import lombok.extern.slf4j.Slf4j;
19   -
20   -/**
21   - * IoT 网关 HTTP 协议:接收设备上行消息
22   - *
23   - * @author 芋道源码
24   - */
25   -@Slf4j
26   -public class IotHttpUpstreamProtocol {
27   -
28   - private final IotGatewayProperties.HttpProperties httpProperties;
29   -
30   - private final Vertx vertx;
31   -
32   - private HttpServer httpServer;
33   -
34   - @Getter
35   - private final String serverId;
36   -
37   - public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) {
38   - this.httpProperties = httpProperties;
39   - this.vertx = vertx;
40   - this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
41   - }
42   -
43   - @PostConstruct
44   - public void start() {
45   - // 创建路由
46   - Router router = Router.router(vertx);
47   - router.route().handler(BodyHandler.create());
48   -
49   - // 创建处理器,添加路由处理器
50   - IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
51   - router.post(IotHttpAuthHandler.PATH).handler(authHandler);
52   - IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
53   - router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
54   - IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
55   - router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
56   - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
57   - router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
58   -
59   - // 启动 HTTP 服务器
60   - HttpServerOptions options = new HttpServerOptions()
61   - .setPort(httpProperties.getServerPort());
62   - if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) {
63   - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath())
64   - .setCertPath(httpProperties.getSslCertPath());
65   - options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
66   - }
67   - try {
68   - httpServer = vertx.createHttpServer(options)
69   - .requestHandler(router)
70   - .listen()
71   - .result();
72   - log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort());
73   - } catch (Exception e) {
74   - log.error("[start][IoT 网关 HTTP 协议启动失败]", e);
75   - throw e;
76   - }
77   - }
78   -
79   - @PreDestroy
80   - public void stop() {
81   - if (httpServer != null) {
82   - try {
83   - httpServer.close().result();
84   - log.info("[stop][IoT 网关 HTTP 协议已停止]");
85   - } catch (Exception e) {
86   - log.error("[stop][IoT 网关 HTTP 协议停止失败]", e);
87   - }
88   - }
89   - }
90   -
91   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 网关 HTTP 订阅者:接收下行给设备的消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +
  15 +@Slf4j
  16 +public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  17 +
  18 + public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
  19 + super(protocol, messageBus);
  20 + }
  21 +
  22 + @Override
  23 + protected void handleMessage(IotDeviceMessage message) {
  24 + log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message);
  25 + }
  26 +
  27 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3 3 import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ArrayUtil;
4 5 import cn.hutool.core.util.ObjUtil;
5 6 import cn.hutool.core.util.StrUtil;
6 7 import cn.hutool.extra.spring.SpringUtil;
... ... @@ -13,12 +14,10 @@ import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
13 14 import io.vertx.core.Handler;
14 15 import io.vertx.core.http.HttpHeaders;
15 16 import io.vertx.ext.web.RoutingContext;
16   -import lombok.RequiredArgsConstructor;
17 17 import lombok.extern.slf4j.Slf4j;
18 18 import org.springframework.http.MediaType;
19 19  
20   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
21   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  20 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
22 21 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
23 22 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
24 23  
... ... @@ -27,7 +26,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
27 26 *
28 27 * @author 芋道源码
29 28 */
30   -@RequiredArgsConstructor
31 29 @Slf4j
32 30 public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> {
33 31  
... ... @@ -43,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
43 41 CommonResult<Object> result = handle0(context);
44 42 writeResponse(context, result);
45 43 } catch (ServiceException e) {
  44 + // 已知异常,返回对应的错误码和错误信息
46 45 writeResponse(context, CommonResult.error(e.getCode(), e.getMessage()));
  46 + } catch (IllegalArgumentException e) {
  47 + // 参数校验异常,返回 400 错误
  48 + writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
47 49 } catch (Exception e) {
  50 + // 其他未知异常,返回 500 错误
48 51 log.error("[handle][path({}) 处理异常]", context.request().path(), e);
49 52 writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR));
50 53 }
51 54 }
52 55  
  56 + /**
  57 + * 处理 HTTP 请求(子类实现)
  58 + *
  59 + * @param context RoutingContext 对象
  60 + * @return 处理结果
  61 + */
53 62 protected abstract CommonResult<Object> handle0(RoutingContext context);
54 63  
  64 + /**
  65 + * 前置处理:认证等
  66 + *
  67 + * @param context RoutingContext 对象
  68 + */
55 69 private void beforeHandle(RoutingContext context) {
56 70 // 如果不需要认证,则不走前置处理
57 71 String path = context.request().path();
... ... @@ -83,12 +97,26 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
83 97 }
84 98 }
85 99  
  100 + // ========== 序列化相关方法 ==========
  101 +
  102 + protected static <T> T deserializeRequest(RoutingContext context, Class<T> clazz) {
  103 + byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null;
  104 + if (ArrayUtil.isEmpty(body)) {
  105 + throw invalidParamException("请求体不能为空");
  106 + }
  107 + return JsonUtils.parseObject(body, clazz);
  108 + }
  109 +
  110 + private static String serializeResponse(Object data) {
  111 + return JsonUtils.toJsonString(data);
  112 + }
  113 +
86 114 @SuppressWarnings("deprecation")
87   - public static void writeResponse(RoutingContext context, Object data) {
  115 + public static void writeResponse(RoutingContext context, CommonResult<?> data) {
88 116 context.response()
89 117 .setStatusCode(200)
90 118 .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
91   - .end(JsonUtils.toJsonString(data));
  119 + .end(serializeResponse(data));
92 120 }
93 121  
94 122 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.map.MapUtil;
5 5 import cn.hutool.core.util.BooleanUtil;
6   -import cn.hutool.core.util.StrUtil;
7 6 import cn.hutool.extra.spring.SpringUtil;
8 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
10 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11 10 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 11 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
14 13 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
15 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
16   -import io.vertx.core.json.JsonObject;
17 15 import io.vertx.ext.web.RoutingContext;
18 16  
19 17 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
20   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
21 18 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
22 19 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
23 20  
... ... @@ -32,7 +29,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
32 29  
33 30 public static final String PATH = "/auth";
34 31  
35   - private final IotHttpUpstreamProtocol protocol;
  32 + private final String serverId;
36 33  
37 34 private final IotDeviceTokenService deviceTokenService;
38 35  
... ... @@ -40,42 +37,31 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
40 37  
41 38 private final IotDeviceMessageService deviceMessageService;
42 39  
43   - public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) {
44   - this.protocol = protocol;
  40 + public IotHttpAuthHandler(IotHttpProtocol protocol) {
  41 + this.serverId = protocol.getServerId();
45 42 this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
46 43 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
47 44 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
48 45 }
49 46  
50 47 @Override
  48 + @SuppressWarnings("DuplicatedCode")
51 49 public CommonResult<Object> handle0(RoutingContext context) {
52 50 // 1. 解析参数
53   - JsonObject body = context.body().asJsonObject();
54   - if (body == null) {
55   - throw invalidParamException("请求体不能为空");
56   - }
57   - String clientId = body.getString("clientId");
58   - if (StrUtil.isEmpty(clientId)) {
59   - throw invalidParamException("clientId 不能为空");
60   - }
61   - String username = body.getString("username");
62   - if (StrUtil.isEmpty(username)) {
63   - throw invalidParamException("username 不能为空");
64   - }
65   - String password = body.getString("password");
66   - if (StrUtil.isEmpty(password)) {
67   - throw invalidParamException("password 不能为空");
68   - }
  51 + IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class);
  52 + Assert.notNull(request, "请求参数不能为空");
  53 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
  54 + Assert.notBlank(request.getUsername(), "username 不能为空");
  55 + Assert.notBlank(request.getPassword(), "password 不能为空");
69 56  
70 57 // 2.1 执行认证
71   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
72   - .setClientId(clientId).setUsername(username).setPassword(password));
  58 + CommonResult<Boolean> result = deviceApi.authDevice(request);
73 59 result.checkError();
74   - if (!BooleanUtil.isTrue(result.getData())) {
  60 + if (BooleanUtil.isFalse(result.getData())) {
75 61 throw exception(DEVICE_AUTH_FAIL);
76 62 }
77 63 // 2.2 生成 Token
78   - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
  64 + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
79 65 Assert.notNull(deviceInfo, "设备信息不能为空");
80 66 String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
81 67 Assert.notBlank(token, "生成 token 不能为空位");
... ... @@ -83,7 +69,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
83 69 // 3. 执行上线
84 70 IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
85 71 deviceMessageService.sendDeviceMessage(message,
86   - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
  72 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
87 73  
88 74 // 构建响应数据
89 75 return success(MapUtil.of("token", token));
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3   -import cn.hutool.core.util.StrUtil;
  3 +import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 7 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
9   -import io.vertx.core.json.JsonObject;
10 9 import io.vertx.ext.web.RoutingContext;
11 10  
12   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
13 11 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
14 12  
15 13 /**
... ... @@ -33,27 +31,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
33 31 @Override
34 32 public CommonResult<Object> handle0(RoutingContext context) {
35 33 // 1. 解析参数
36   - JsonObject body = context.body().asJsonObject();
37   - if (body == null) {
38   - throw invalidParamException("请求体不能为空");
39   - }
40   - String productKey = body.getString("productKey");
41   - if (StrUtil.isEmpty(productKey)) {
42   - throw invalidParamException("productKey 不能为空");
43   - }
44   - String deviceName = body.getString("deviceName");
45   - if (StrUtil.isEmpty(deviceName)) {
46   - throw invalidParamException("deviceName 不能为空");
47   - }
48   - String productSecret = body.getString("productSecret");
49   - if (StrUtil.isEmpty(productSecret)) {
50   - throw invalidParamException("productSecret 不能为空");
51   - }
  34 + IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class);
  35 + Assert.notNull(request, "请求参数不能为空");
  36 + Assert.notBlank(request.getProductKey(), "productKey 不能为空");
  37 + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
  38 + Assert.notBlank(request.getSign(), "sign 不能为空");
52 39  
53 40 // 2. 调用动态注册
54   - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
55   - .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
56   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
  41 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
57 42 result.checkError();
58 43  
59 44 // 3. 返回结果
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.extra.spring.SpringUtil;
4 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
8 9 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
9   -import io.vertx.core.json.JsonObject;
10 10 import io.vertx.ext.web.RoutingContext;
  11 +import lombok.Data;
11 12  
12 13 import java.util.List;
13 14  
14   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
15 15 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
16 16  
17 17 /**
... ... @@ -39,29 +39,31 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
39 39  
40 40 @Override
41 41 public CommonResult<Object> handle0(RoutingContext context) {
42   - // 1. 解析通用参数
  42 + // 1.1 解析通用参数
43 43 String productKey = context.pathParam("productKey");
44 44 String deviceName = context.pathParam("deviceName");
  45 + // 1.2 解析子设备列表
  46 + SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class);
  47 + Assert.notNull(request, "请求参数不能为空");
  48 + Assert.notEmpty(request.getParams(), "params 不能为空");
45 49  
46   - // 2. 解析子设备列表
47   - JsonObject body = context.body().asJsonObject();
48   - if (body == null) {
49   - throw invalidParamException("请求体不能为空");
50   - }
51   - if (body.getJsonArray("params") == null) {
52   - throw invalidParamException("params 不能为空");
53   - }
54   - List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
55   - body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
56   -
57   - // 3. 调用子设备动态注册
  50 + // 2. 调用子设备动态注册
58 51 IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
59   - .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
  52 + .setGatewayProductKey(productKey)
  53 + .setGatewayDeviceName(deviceName)
  54 + .setSubDevices(request.getParams());
60 55 CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
61 56 result.checkError();
62 57  
63   - // 4. 返回结果
  58 + // 3. 返回结果
64 59 return success(result.getData());
65 60 }
66 61  
  62 + @Data
  63 + public static class SubDeviceRegisterRequest {
  64 +
  65 + private List<IotSubDeviceRegisterReqDTO> params;
  66 +
  67 + }
  68 +
67 69 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.map.MapUtil;
... ... @@ -6,55 +6,47 @@ import cn.hutool.core.text.StrPool;
6 6 import cn.hutool.extra.spring.SpringUtil;
7 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
10 10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11 11 import io.vertx.ext.web.RoutingContext;
12   -import lombok.RequiredArgsConstructor;
13 12 import lombok.extern.slf4j.Slf4j;
14 13  
15   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
16   -
17 14 /**
18 15 * IoT 网关 HTTP 协议的【上行】处理器
19 16 *
20 17 * @author 芋道源码
21 18 */
22   -@RequiredArgsConstructor
23 19 @Slf4j
24 20 public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
25 21  
26 22 public static final String PATH = "/topic/sys/:productKey/:deviceName/*";
27 23  
28   - private final IotHttpUpstreamProtocol protocol;
  24 + private final String serverId;
29 25  
30 26 private final IotDeviceMessageService deviceMessageService;
31 27  
32   - public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) {
33   - this.protocol = protocol;
  28 + public IotHttpUpstreamHandler(IotHttpProtocol protocol) {
  29 + this.serverId = protocol.getServerId();
34 30 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
35 31 }
36 32  
37 33 @Override
38 34 protected CommonResult<Object> handle0(RoutingContext context) {
39   - // 1. 解析通用参数
  35 + // 1.1 解析通用参数
40 36 String productKey = context.pathParam("productKey");
41 37 String deviceName = context.pathParam("deviceName");
42 38 String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
43   -
44   - // 2.1 解析消息
45   - if (context.body().buffer() == null) {
46   - throw invalidParamException("请求体不能为空");
47   - }
48   - byte[] bytes = context.body().buffer().getBytes();
49   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
50   - productKey, deviceName);
  39 + // 1.2 根据 Content-Type 反序列化消息
  40 + IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class);
  41 + Assert.notNull(message, "请求参数不能为空");
51 42 Assert.equals(method, message.getMethod(), "method 不匹配");
52   - // 2.2 发送消息
  43 +
  44 + // 2. 发送消息
53 45 deviceMessageService.sendDeviceMessage(message,
54   - productKey, deviceName, protocol.getServerId());
  46 + productKey, deviceName, serverId);
55 47  
56 48 // 3. 返回结果
57 49 return CommonResult.success(MapUtil.of("messageId", message.getId()));
58 50 }
59 51  
60   -}
61 52 \ No newline at end of file
  53 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT 网关 MQTT 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotMqttConfig {
  14 +
  15 + /**
  16 + * 最大消息大小(字节)
  17 + */
  18 + @NotNull(message = "最大消息大小不能为空")
  19 + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节")
  20 + private Integer maxMessageSize = 8192;
  21 +
  22 + /**
  23 + * 连接超时时间(秒)
  24 + */
  25 + @NotNull(message = "连接超时时间不能为空")
  26 + @Min(value = 1, message = "连接超时时间不能小于 1 秒")
  27 + private Integer connectTimeoutSeconds = 60;
  28 +
  29 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2   -
3   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
8   -import jakarta.annotation.PostConstruct;
9   -import lombok.extern.slf4j.Slf4j;
10   -
11   -/**
12   - * IoT 网关 MQTT 协议:下行消息订阅器
13   - * <p>
14   - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理
15   - *
16   - * @author 芋道源码
17   - */
18   -@Slf4j
19   -public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
20   -
21   - private final IotMqttUpstreamProtocol upstreamProtocol;
22   -
23   - private final IotMqttDownstreamHandler downstreamHandler;
24   -
25   - private final IotMessageBus messageBus;
26   -
27   - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol,
28   - IotMqttDownstreamHandler downstreamHandler,
29   - IotMessageBus messageBus) {
30   - this.upstreamProtocol = upstreamProtocol;
31   - this.downstreamHandler = downstreamHandler;
32   - this.messageBus = messageBus;
33   - }
34   -
35   - @PostConstruct
36   - public void subscribe() {
37   - messageBus.register(this);
38   - log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic());
39   - }
40   -
41   - @Override
42   - public String getTopic() {
43   - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId());
44   - }
45   -
46   - @Override
47   - public String getGroup() {
48   - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
49   - return getTopic();
50   - }
51   -
52   - @Override
53   - public void onMessage(IotDeviceMessage message) {
54   - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
55   - message.getId(), message.getMethod(), message.getDeviceId());
56   - try {
57   - // 1. 校验
58   - String method = message.getMethod();
59   - if (method == null) {
60   - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
61   - message.getId(), message.getDeviceId());
62   - return;
63   - }
64   -
65   - // 2. 委托给下行处理器处理业务逻辑
66   - boolean success = downstreamHandler.handleDownstreamMessage(message);
67   - if (success) {
68   - log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]",
69   - message.getId(), message.getMethod(), message.getDeviceId());
70   - } else {
71   - log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]",
72   - message.getId(), message.getMethod(), message.getDeviceId());
73   - }
74   - } catch (Exception e) {
75   - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
76   - message.getId(), message.getMethod(), message.getDeviceId(), e);
77   - }
78   - }
79   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  11 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  12 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  20 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  21 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  22 +import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
  23 +import io.netty.handler.codec.mqtt.MqttQoS;
  24 +import io.vertx.core.Vertx;
  25 +import io.vertx.core.net.PemKeyCertOptions;
  26 +import io.vertx.mqtt.MqttEndpoint;
  27 +import io.vertx.mqtt.MqttServer;
  28 +import io.vertx.mqtt.MqttServerOptions;
  29 +import io.vertx.mqtt.MqttTopicSubscription;
  30 +import io.vertx.mqtt.messages.MqttPublishMessage;
  31 +import lombok.Getter;
  32 +import lombok.extern.slf4j.Slf4j;
  33 +
  34 +import java.util.ArrayList;
  35 +import java.util.List;
  36 +
  37 +/**
  38 + * IoT 网关 MQTT 协议:接收设备上行消息
  39 + *
  40 + * @author 芋道源码
  41 + */
  42 +@Slf4j
  43 +public class IotMqttProtocol implements IotProtocol {
  44 +
  45 + /**
  46 + * 注册连接的 clientId 标识
  47 + *
  48 + * @see #handleEndpoint(MqttEndpoint)
  49 + */
  50 + private static final String AUTH_TYPE_REGISTER = "|authType=register|";
  51 +
  52 + /**
  53 + * 协议配置
  54 + */
  55 + private final ProtocolProperties properties;
  56 + /**
  57 + * 服务器 ID(用于消息追踪,全局唯一)
  58 + */
  59 + @Getter
  60 + private final String serverId;
  61 +
  62 + /**
  63 + * 运行状态
  64 + */
  65 + @Getter
  66 + private volatile boolean running = false;
  67 +
  68 + /**
  69 + * Vert.x 实例
  70 + */
  71 + private Vertx vertx;
  72 + /**
  73 + * MQTT 服务器
  74 + */
  75 + private MqttServer mqttServer;
  76 +
  77 + /**
  78 + * 连接管理器
  79 + */
  80 + private final IotMqttConnectionManager connectionManager;
  81 + /**
  82 + * 下行消息订阅者
  83 + */
  84 + private final IotMqttDownstreamSubscriber downstreamSubscriber;
  85 +
  86 + private final IotDeviceMessageService deviceMessageService;
  87 +
  88 + private final IotMqttAuthHandler authHandler;
  89 + private final IotMqttRegisterHandler registerHandler;
  90 + private final IotMqttUpstreamHandler upstreamHandler;
  91 +
  92 + public IotMqttProtocol(ProtocolProperties properties) {
  93 + IotMqttConfig mqttConfig = properties.getMqtt();
  94 + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空");
  95 + this.properties = properties;
  96 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  97 +
  98 + // 初始化连接管理器
  99 + this.connectionManager = new IotMqttConnectionManager();
  100 +
  101 + // 初始化 Handler
  102 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  103 + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  104 + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
  105 + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
  106 + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
  107 +
  108 + // 初始化下行消息订阅者
  109 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  110 + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
  111 + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
  112 + }
  113 +
  114 + @Override
  115 + public String getId() {
  116 + return properties.getId();
  117 + }
  118 +
  119 + @Override
  120 + public IotProtocolTypeEnum getType() {
  121 + return IotProtocolTypeEnum.MQTT;
  122 + }
  123 +
  124 + @Override
  125 + public void start() {
  126 + if (running) {
  127 + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId());
  128 + return;
  129 + }
  130 +
  131 + // 1.1 创建 Vertx 实例
  132 + this.vertx = Vertx.vertx();
  133 +
  134 + // 1.2 创建服务器选项
  135 + IotMqttConfig mqttConfig = properties.getMqtt();
  136 + MqttServerOptions options = new MqttServerOptions()
  137 + .setPort(properties.getPort())
  138 + .setMaxMessageSize(mqttConfig.getMaxMessageSize())
  139 + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds());
  140 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  141 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
  142 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  143 + .setKeyPath(sslConfig.getSslKeyPath())
  144 + .setCertPath(sslConfig.getSslCertPath());
  145 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  146 + }
  147 +
  148 + // 1.3 创建服务器并设置连接处理器
  149 + mqttServer = MqttServer.create(vertx, options);
  150 + mqttServer.endpointHandler(this::handleEndpoint);
  151 +
  152 + // 1.4 启动 MQTT 服务器
  153 + try {
  154 + mqttServer.listen().result();
  155 + running = true;
  156 + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]",
  157 + getId(), properties.getPort(), serverId);
  158 +
  159 + // 2. 启动下行消息订阅者
  160 + this.downstreamSubscriber.start();
  161 + } catch (Exception e) {
  162 + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e);
  163 + // 启动失败时关闭资源
  164 + if (mqttServer != null) {
  165 + mqttServer.close();
  166 + mqttServer = null;
  167 + }
  168 + if (vertx != null) {
  169 + vertx.close();
  170 + vertx = null;
  171 + }
  172 + throw e;
  173 + }
  174 + }
  175 +
  176 + @Override
  177 + public void stop() {
  178 + if (!running) {
  179 + return;
  180 + }
  181 + // 1. 停止下行消息订阅者
  182 + try {
  183 + downstreamSubscriber.stop();
  184 + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
  185 + } catch (Exception e) {
  186 + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
  187 + }
  188 +
  189 + // 2.1 关闭 MQTT 服务器
  190 + if (mqttServer != null) {
  191 + try {
  192 + mqttServer.close().result();
  193 + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId());
  194 + } catch (Exception e) {
  195 + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e);
  196 + }
  197 + mqttServer = null;
  198 + }
  199 + // 2.2 关闭 Vertx 实例
  200 + if (vertx != null) {
  201 + try {
  202 + vertx.close().result();
  203 + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId());
  204 + } catch (Exception e) {
  205 + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e);
  206 + }
  207 + vertx = null;
  208 + }
  209 + running = false;
  210 + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId());
  211 + }
  212 +
  213 + // ======================================= MQTT 连接处理 ======================================
  214 +
  215 + /**
  216 + * 处理 MQTT 连接端点
  217 + *
  218 + * @param endpoint MQTT 连接端点
  219 + */
  220 + private void handleEndpoint(MqttEndpoint endpoint) {
  221 + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程
  222 + String clientId = endpoint.clientIdentifier();
  223 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
  224 + // 情况一:设备注册请求
  225 + registerHandler.handleRegister(endpoint);
  226 + return;
  227 + } else {
  228 + // 情况二:普通认证请求
  229 + if (!authHandler.handleAuthenticationRequest(endpoint)) {
  230 + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
  231 + return;
  232 + }
  233 + }
  234 +
  235 + // 2.1 设置异常和关闭处理器
  236 + endpoint.exceptionHandler(ex -> {
  237 + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]",
  238 + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
  239 + endpoint.close();
  240 + });
  241 + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等)
  242 + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文
  243 + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
  244 + cleanupConnection(endpoint);
  245 + });
  246 + // 2.2 设置心跳处理器
  247 + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId));
  248 +
  249 + // 3.1 设置消息处理器
  250 + endpoint.publishHandler(message -> processMessage(endpoint, message));
  251 + // 3.2 设置 QoS 2 消息的 PUBREL 处理器
  252 + endpoint.publishReleaseHandler(endpoint::publishComplete);
  253 +
  254 + // 4.1 设置订阅处理器(带 ACL 校验)
  255 + endpoint.subscribeHandler(subscribe -> {
  256 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  257 + List<MqttQoS> grantedQoSLevels = new ArrayList<>();
  258 + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) {
  259 + String topicName = sub.topicName();
  260 + // 校验主题是否属于当前设备
  261 + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed(
  262 + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) {
  263 + grantedQoSLevels.add(sub.qualityOfService());
  264 + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName);
  265 + } else {
  266 + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName);
  267 + grantedQoSLevels.add(MqttQoS.FAILURE);
  268 + }
  269 + }
  270 + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
  271 + });
  272 + // 4.2 设置取消订阅处理器
  273 + endpoint.unsubscribeHandler(unsubscribe -> {
  274 + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
  275 + endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
  276 + });
  277 +
  278 + // 5. 接受连接
  279 + endpoint.accept(false);
  280 + }
  281 +
  282 + /**
  283 + * 处理消息(发布)
  284 + *
  285 + * @param endpoint MQTT 连接端点
  286 + * @param message 发布消息
  287 + */
  288 + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
  289 + String clientId = endpoint.clientIdentifier();
  290 + try {
  291 + // 1. 处理业务消息
  292 + String topic = message.topicName();
  293 + byte[] payload = message.payload().getBytes();
  294 + upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
  295 +
  296 + // 2. 根据 QoS 级别发送相应的确认消息
  297 + handleQoSAck(endpoint, message);
  298 + } catch (Exception e) {
  299 + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
  300 + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
  301 + endpoint.close();
  302 + }
  303 + }
  304 +
  305 + /**
  306 + * 处理 QoS 确认
  307 + *
  308 + * @param endpoint MQTT 连接端点
  309 + * @param message 发布消息
  310 + */
  311 + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) {
  312 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
  313 + // QoS 1: 发送 PUBACK 确认
  314 + endpoint.publishAcknowledge(message.messageId());
  315 + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
  316 + // QoS 2: 发送 PUBREC 确认
  317 + endpoint.publishReceived(message.messageId());
  318 + }
  319 + // QoS 0 无需确认
  320 + }
  321 +
  322 + /**
  323 + * 清理连接
  324 + *
  325 + * @param endpoint MQTT 连接端点
  326 + */
  327 + private void cleanupConnection(MqttEndpoint endpoint) {
  328 + try {
  329 + // 1. 发送设备离线消息
  330 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  331 + if (connectionInfo != null) {
  332 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  333 + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
  334 + connectionInfo.getDeviceName(), serverId);
  335 + }
  336 +
  337 + // 2. 注销连接
  338 + connectionManager.unregisterConnection(endpoint);
  339 + } catch (Exception e) {
  340 + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
  341 + endpoint.clientIdentifier(), e.getMessage());
  342 + }
  343 + }
  344 +
  345 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2   -
3   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler;
7   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8   -import io.vertx.core.Vertx;
9   -import io.vertx.mqtt.MqttServer;
10   -import io.vertx.mqtt.MqttServerOptions;
11   -import jakarta.annotation.PostConstruct;
12   -import jakarta.annotation.PreDestroy;
13   -import lombok.Getter;
14   -import lombok.extern.slf4j.Slf4j;
15   -
16   -/**
17   - * IoT 网关 MQTT 协议:接收设备上行消息
18   - *
19   - * @author 芋道源码
20   - */
21   -@Slf4j
22   -public class IotMqttUpstreamProtocol {
23   -
24   - private final IotGatewayProperties.MqttProperties mqttProperties;
25   -
26   - private final IotDeviceMessageService messageService;
27   -
28   - private final IotMqttConnectionManager connectionManager;
29   -
30   - private final Vertx vertx;
31   -
32   - @Getter
33   - private final String serverId;
34   -
35   - private MqttServer mqttServer;
36   -
37   - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties,
38   - IotDeviceMessageService messageService,
39   - IotMqttConnectionManager connectionManager,
40   - Vertx vertx) {
41   - this.mqttProperties = mqttProperties;
42   - this.messageService = messageService;
43   - this.connectionManager = connectionManager;
44   - this.vertx = vertx;
45   - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort());
46   - }
47   -
48   - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈;
49   - @PostConstruct
50   - public void start() {
51   - // 创建服务器选项
52   - MqttServerOptions options = new MqttServerOptions()
53   - .setPort(mqttProperties.getPort())
54   - .setMaxMessageSize(mqttProperties.getMaxMessageSize())
55   - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds());
56   -
57   - // 配置 SSL(如果启用)
58   - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) {
59   - options.setSsl(true)
60   - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions())
61   - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions());
62   - }
63   -
64   - // 创建服务器并设置连接处理器
65   - mqttServer = MqttServer.create(vertx, options);
66   - mqttServer.endpointHandler(endpoint -> {
67   - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager);
68   - handler.handle(endpoint);
69   - });
70   -
71   - // 启动服务器
72   - try {
73   - mqttServer.listen().result();
74   - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort());
75   - } catch (Exception e) {
76   - log.error("[start][IoT 网关 MQTT 协议启动失败]", e);
77   - throw e;
78   - }
79   - }
80   -
81   - @PreDestroy
82   - public void stop() {
83   - if (mqttServer != null) {
84   - try {
85   - mqttServer.close().result();
86   - log.info("[stop][IoT 网关 MQTT 协议已停止]");
87   - } catch (Exception e) {
88   - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e);
89   - }
90   - }
91   - }
92   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  7 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  8 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  9 +import io.netty.handler.codec.mqtt.MqttQoS;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +/**
  14 + * IoT 网关 MQTT 协议:下行消息处理器
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Slf4j
  19 +@RequiredArgsConstructor
  20 +public class IotMqttDownstreamHandler {
  21 +
  22 + private final IotDeviceMessageService deviceMessageService;
  23 +
  24 + private final IotMqttConnectionManager connectionManager;
  25 +
  26 + /**
  27 + * 处理下行消息
  28 + *
  29 + * @param message 设备消息
  30 + */
  31 + public void handle(IotDeviceMessage message) {
  32 + try {
  33 + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
  34 + message.getDeviceId(), message.getMethod(), message.getId());
  35 +
  36 + // 1. 检查设备连接
  37 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
  38 + message.getDeviceId());
  39 + if (connectionInfo == null) {
  40 + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
  41 + message.getDeviceId(), message.getMethod(), message.getId());
  42 + return;
  43 + }
  44 +
  45 + // 2.1 序列化消息
  46 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(),
  47 + connectionInfo.getDeviceName());
  48 + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空");
  49 + // 2.2 构建主题
  50 + Assert.notBlank(message.getMethod(), "消息方法不能为空");
  51 + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
  52 + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(),
  53 + connectionInfo.getDeviceName(), isReply);
  54 + Assert.notBlank(topic, "主题不能为空");
  55 +
  56 + // 3. 发送到设备
  57 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload,
  58 + MqttQoS.AT_LEAST_ONCE.value(), false);
  59 + if (!success) {
  60 + throw new RuntimeException("下行消息发送失败");
  61 + }
  62 + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]",
  63 + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length);
  64 + } catch (Exception e) {
  65 + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
  66 + message.getDeviceId(), message.getMethod(), message, e);
  67 + }
  68 + }
  69 +
  70 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 网关 MQTT 协议:接收下行给设备的消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +
  17 + private final IotMqttDownstreamHandler downstreamHandler;
  18 +
  19 + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol,
  20 + IotMqttDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
  24 + }
  25 +
  26 + @Override
  27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
  29 + }
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  5 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  6 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  7 +import io.netty.handler.codec.mqtt.MqttQoS;
  8 +import io.vertx.core.buffer.Buffer;
  9 +import io.vertx.mqtt.MqttEndpoint;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +/**
  14 + * IoT 网关 MQTT 协议的处理器抽象基类
  15 + * <p>
  16 + * 提供通用的连接校验、响应发送等功能
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Slf4j
  21 +@RequiredArgsConstructor
  22 +public abstract class IotMqttAbstractHandler {
  23 +
  24 + protected final IotMqttConnectionManager connectionManager;
  25 + protected final IotDeviceMessageService deviceMessageService;
  26 +
  27 + /**
  28 + * 发送成功响应到设备
  29 + *
  30 + * @param endpoint MQTT 连接端点
  31 + * @param productKey 产品 Key
  32 + * @param deviceName 设备名称
  33 + * @param requestId 请求 ID
  34 + * @param method 方法名
  35 + * @param data 响应数据
  36 + */
  37 + @SuppressWarnings("SameParameterValue")
  38 + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  39 + String requestId, String method, Object data) {
  40 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null);
  41 + writeResponse(endpoint, productKey, deviceName, method, responseMessage);
  42 + }
  43 +
  44 + /**
  45 + * 发送错误响应到设备
  46 + *
  47 + * @param endpoint MQTT 连接端点
  48 + * @param productKey 产品 Key
  49 + * @param deviceName 设备名称
  50 + * @param requestId 请求 ID
  51 + * @param method 方法名
  52 + * @param errorCode 错误码
  53 + * @param errorMessage 错误消息
  54 + */
  55 + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  56 + String requestId, String method, Integer errorCode, String errorMessage) {
  57 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage);
  58 + writeResponse(endpoint, productKey, deviceName, method, responseMessage);
  59 + }
  60 +
  61 + /**
  62 + * 写入响应消息到设备
  63 + *
  64 + * @param endpoint MQTT 连接端点
  65 + * @param productKey 产品 Key
  66 + * @param deviceName 设备名称
  67 + * @param method 方法名
  68 + * @param responseMessage 响应消息
  69 + */
  70 + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  71 + String method, IotDeviceMessage responseMessage) {
  72 + try {
  73 + // 1.1 序列化消息(根据设备配置的序列化类型)
  74 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName);
  75 + // 1.2 构建响应主题
  76 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true);
  77 +
  78 + // 2. 发送响应消息
  79 + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false);
  80 + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode());
  81 + } catch (Exception e) {
  82 + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e);
  83 + }
  84 + }
  85 +
  86 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.hutool.core.util.BooleanUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  14 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  15 +import io.vertx.mqtt.MqttEndpoint;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import cn.hutool.core.lang.Assert;
  18 +
  19 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
  20 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  21 +
  22 +/**
  23 + * IoT 网关 MQTT 认证处理器
  24 + * <p>
  25 + * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@Slf4j
  30 +public class IotMqttAuthHandler extends IotMqttAbstractHandler {
  31 +
  32 + private final IotDeviceCommonApi deviceApi;
  33 + private final IotDeviceService deviceService;
  34 + private final String serverId;
  35 +
  36 + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager,
  37 + IotDeviceMessageService deviceMessageService,
  38 + IotDeviceCommonApi deviceApi,
  39 + String serverId) {
  40 + super(connectionManager, deviceMessageService);
  41 + this.deviceApi = deviceApi;
  42 + this.deviceService = SpringUtil.getBean(IotDeviceService.class);
  43 + this.serverId = serverId;
  44 + }
  45 +
  46 + /**
  47 + * 处理 MQTT 连接(认证)请求
  48 + *
  49 + * @param endpoint MQTT 连接端点
  50 + * @return 认证是否成功
  51 + */
  52 + @SuppressWarnings("DataFlowIssue")
  53 + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
  54 + String clientId = endpoint.clientIdentifier();
  55 + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
  56 + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
  57 + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
  58 + clientId, username, connectionManager.getEndpointAddress(endpoint));
  59 +
  60 + try {
  61 + // 1.1 解析认证参数
  62 + Assert.notBlank(clientId, "clientId 不能为空");
  63 + Assert.notBlank(username, "username 不能为空");
  64 + Assert.notBlank(password, "password 不能为空");
  65 + // 1.2 构建认证参数
  66 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
  67 + .setClientId(clientId)
  68 + .setUsername(username)
  69 + .setPassword(password);
  70 +
  71 + // 2.1 执行认证
  72 + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
  73 + authResult.checkError();
  74 + if (BooleanUtil.isFalse(authResult.getData())) {
  75 + throw exception(DEVICE_AUTH_FAIL);
  76 + }
  77 + // 2.2 解析设备信息
  78 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  79 + Assert.notNull(deviceInfo, "解析设备信息失败");
  80 + // 2.3 获取设备信息
  81 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  82 + Assert.notNull(device, "设备不存在");
  83 +
  84 + // 3.1 注册连接
  85 + registerConnection(endpoint, device, clientId);
  86 + // 3.2 发送设备上线消息
  87 + sendOnlineMessage(device);
  88 + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
  89 + return true;
  90 + } catch (Exception e) {
  91 + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]",
  92 + clientId, username, e.getMessage());
  93 + return false;
  94 + }
  95 + }
  96 +
  97 + /**
  98 + * 注册连接
  99 + */
  100 + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
  101 + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
  102 + .setDeviceId(device.getId())
  103 + .setProductKey(device.getProductKey())
  104 + .setDeviceName(device.getDeviceName())
  105 + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
  106 + connectionManager.registerConnection(endpoint, connectionInfo);
  107 + }
  108 +
  109 + /**
  110 + * 发送设备上线消息
  111 + */
  112 + private void sendOnlineMessage(IotDeviceRespDTO device) {
  113 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  114 + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
  115 + device.getDeviceName(), serverId);
  116 + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
  117 + }
  118 +
  119 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  14 +import io.vertx.mqtt.MqttEndpoint;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  18 +
  19 +/**
  20 + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Slf4j
  25 +public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
  26 +
  27 + private final IotDeviceCommonApi deviceApi;
  28 +
  29 + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
  30 + IotDeviceMessageService deviceMessageService) {
  31 + super(connectionManager, deviceMessageService);
  32 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  33 + }
  34 +
  35 + /**
  36 + * 处理注册连接
  37 + * <p>
  38 + * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册
  39 + *
  40 + * @param endpoint MQTT 连接端点
  41 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  42 + */
  43 + @SuppressWarnings("DataFlowIssue")
  44 + public void handleRegister(MqttEndpoint endpoint) {
  45 + String clientId = endpoint.clientIdentifier();
  46 + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
  47 + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
  48 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
  49 + String productKey = null;
  50 + String deviceName = null;
  51 +
  52 + try {
  53 + // 1.1 校验参数
  54 + Assert.notBlank(clientId, "clientId 不能为空");
  55 + Assert.notBlank(username, "username 不能为空");
  56 + Assert.notBlank(password, "password 不能为空");
  57 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  58 + Assert.notNull(deviceInfo, "解析设备信息失败");
  59 + productKey = deviceInfo.getProductKey();
  60 + deviceName = deviceInfo.getDeviceName();
  61 + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]",
  62 + clientId, productKey, deviceName);
  63 + // 1.2 构建注册参数
  64 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
  65 + .setProductKey(productKey)
  66 + .setDeviceName(deviceName)
  67 + .setSign(password);
  68 +
  69 + // 2. 调用动态注册 API
  70 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  71 + result.checkError();
  72 +
  73 + // 3. 接受连接,并发送成功响应
  74 + endpoint.accept(false);
  75 + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData());
  76 + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId);
  77 + } catch (Exception e) {
  78 + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
  79 + // 接受连接,并发送错误响应
  80 + endpoint.accept(false);
  81 + sendErrorResponse(endpoint, productKey, deviceName, null, method,
  82 + INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
  83 + } finally {
  84 + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接)
  85 + endpoint.close();
  86 + }
  87 + }
  88 +
  89 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ArrayUtil;
  5 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  8 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  9 +import io.vertx.mqtt.MqttEndpoint;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  13 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  14 +
  15 +/**
  16 + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Slf4j
  21 +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
  22 +
  23 + private final String serverId;
  24 +
  25 + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
  26 + IotDeviceMessageService deviceMessageService,
  27 + String serverId) {
  28 + super(connectionManager, deviceMessageService);
  29 + this.serverId = serverId;
  30 + }
  31 +
  32 + /**
  33 + * 处理业务消息
  34 + *
  35 + * @param endpoint MQTT 连接端点
  36 + * @param topic 主题
  37 + * @param payload 消息内容
  38 + */
  39 + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) {
  40 + String clientId = endpoint.clientIdentifier();
  41 + IotDeviceMessage message = null;
  42 + String productKey = null;
  43 + String deviceName = null;
  44 +
  45 + try {
  46 + // 1.1 基础检查
  47 + if (ArrayUtil.isEmpty(payload)) {
  48 + return;
  49 + }
  50 + // 1.2 解析主题,获取 productKey 和 deviceName
  51 + String[] topicParts = topic.split("/");
  52 + productKey = ArrayUtil.get(topicParts, 2);
  53 + deviceName = ArrayUtil.get(topicParts, 3);
  54 + Assert.notBlank(productKey, "产品 Key 不能为空");
  55 + Assert.notBlank(deviceName, "设备名称不能为空");
  56 + // 1.3 校验设备信息,防止伪造设备消息
  57 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  58 + Assert.notNull(connectionInfo, "无法获取连接信息");
  59 + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
  60 + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
  61 +
  62 + // 2. 反序列化消息
  63 + message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
  64 + if (message == null) {
  65 + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  66 + sendErrorResponse(endpoint, productKey, deviceName, null, null,
  67 + BAD_REQUEST.getCode(), "消息解码失败");
  68 + return;
  69 + }
  70 +
  71 + // 3. 处理业务消息
  72 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
  73 + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic);
  74 + } catch (ServiceException e) {
  75 + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]",
  76 + clientId, topic, e.getMessage());
  77 + String requestId = message != null ? message.getRequestId() : null;
  78 + String method = message != null ? message.getMethod() : null;
  79 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
  80 + } catch (IllegalArgumentException e) {
  81 + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
  82 + clientId, topic, e.getMessage());
  83 + String requestId = message != null ? message.getRequestId() : null;
  84 + String method = message != null ? message.getMethod() : null;
  85 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  86 + BAD_REQUEST.getCode(), e.getMessage());
  87 + } catch (Exception e) {
  88 + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
  89 + clientId, topic, e.getMessage(), e);
  90 + String requestId = message != null ? message.getRequestId() : null;
  91 + String method = message != null ? message.getMethod() : null;
  92 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  93 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  94 + }
  95 + }
  96 +
  97 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
... ... @@ -66,7 +66,6 @@ public class IotMqttConnectionManager {
66 66 } catch (Exception ignored) {
67 67 // 连接已关闭,忽略异常
68 68 }
69   -
70 69 return realTimeAddress;
71 70 }
72 71  
... ... @@ -74,24 +73,24 @@ public class IotMqttConnectionManager {
74 73 * 注册设备连接(包含认证信息)
75 74 *
76 75 * @param endpoint MQTT 连接端点
77   - * @param deviceId 设备 ID
78 76 * @param connectionInfo 连接信息
79 77 */
80   - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
  78 + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) {
  79 + Long deviceId = connectionInfo.getDeviceId();
81 80 // 如果设备已有其他连接,先清理旧连接
82 81 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
83 82 if (oldEndpoint != null && oldEndpoint != endpoint) {
84 83 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
85 84 deviceId, getEndpointAddress(oldEndpoint));
86   - oldEndpoint.close();
87   - // 清理旧连接的映射
  85 + // 先清理映射,再关闭连接(避免旧连接处理器干扰)
88 86 connectionMap.remove(oldEndpoint);
  87 + oldEndpoint.close();
89 88 }
90 89  
91 90 // 注册新连接
92 91 connectionMap.put(endpoint, connectionInfo);
93 92 deviceEndpointMap.put(deviceId, endpoint);
94   - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
  93 + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]",
95 94 deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
96 95 }
97 96  
... ... @@ -129,26 +128,11 @@ public class IotMqttConnectionManager {
129 128 if (endpoint == null) {
130 129 return null;
131 130 }
132   -
133 131 // 获取连接信息
134 132 return getConnectionInfo(endpoint);
135 133 }
136 134  
137 135 /**
138   - * 检查设备是否在线
139   - */
140   - public boolean isDeviceOnline(Long deviceId) {
141   - return deviceEndpointMap.containsKey(deviceId);
142   - }
143   -
144   - /**
145   - * 检查设备是否离线
146   - */
147   - public boolean isDeviceOffline(Long deviceId) {
148   - return !isDeviceOnline(deviceId);
149   - }
150   -
151   - /**
152 136 * 发送消息到设备
153 137 *
154 138 * @param deviceId 设备 ID
... ... @@ -192,28 +176,16 @@ public class IotMqttConnectionManager {
192 176 * 设备 ID
193 177 */
194 178 private Long deviceId;
195   -
196 179 /**
197 180 * 产品 Key
198 181 */
199 182 private String productKey;
200   -
201 183 /**
202 184 * 设备名称
203 185 */
204 186 private String deviceName;
205 187  
206 188 /**
207   - * 客户端 ID
208   - */
209   - private String clientId;
210   -
211   - /**
212   - * 是否已认证
213   - */
214   - private boolean authenticated;
215   -
216   - /**
217 189 * 连接地址
218 190 */
219 191 private String remoteAddress;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2   -
3   -import cn.hutool.core.util.StrUtil;
4   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
7   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8   -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
9   -import io.netty.handler.codec.mqtt.MqttQoS;
10   -import lombok.extern.slf4j.Slf4j;
11   -
12   -/**
13   - * IoT 网关 MQTT 协议:下行消息处理器
14   - * <p>
15   - * 专门处理下行消息的业务逻辑,包括:
16   - * 1. 消息编码
17   - * 2. 主题构建
18   - * 3. 消息发送
19   - *
20   - * @author 芋道源码
21   - */
22   -@Slf4j
23   -public class IotMqttDownstreamHandler {
24   -
25   - private final IotDeviceMessageService deviceMessageService;
26   -
27   - private final IotMqttConnectionManager connectionManager;
28   -
29   - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService,
30   - IotMqttConnectionManager connectionManager) {
31   - this.deviceMessageService = deviceMessageService;
32   - this.connectionManager = connectionManager;
33   - }
34   -
35   - /**
36   - * 处理下行消息
37   - *
38   - * @param message 设备消息
39   - * @return 是否处理成功
40   - */
41   - public boolean handleDownstreamMessage(IotDeviceMessage message) {
42   - try {
43   - // 1. 基础校验
44   - if (message == null || message.getDeviceId() == null) {
45   - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
46   - return false;
47   - }
48   -
49   - // 2. 检查设备是否在线
50   - if (connectionManager.isDeviceOffline(message.getDeviceId())) {
51   - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId());
52   - return false;
53   - }
54   -
55   - // 3. 获取连接信息
56   - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
57   - if (connectionInfo == null) {
58   - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId());
59   - return false;
60   - }
61   -
62   - // 4. 编码消息
63   - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(),
64   - connectionInfo.getDeviceName());
65   - if (payload == null || payload.length == 0) {
66   - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());
67   - return false;
68   - }
69   -
70   - // 5. 发送消息到设备
71   - return sendMessageToDevice(message, connectionInfo, payload);
72   - } catch (Exception e) {
73   - if (message != null) {
74   - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",
75   - message.getDeviceId(), e.getMessage(), e);
76   - }
77   - return false;
78   - }
79   - }
80   -
81   - /**
82   - * 发送消息到设备
83   - *
84   - * @param message 设备消息
85   - * @param connectionInfo 连接信息
86   - * @param payload 消息负载
87   - * @return 是否发送成功
88   - */
89   - private boolean sendMessageToDevice(IotDeviceMessage message,
90   - IotMqttConnectionManager.ConnectionInfo connectionInfo,
91   - byte[] payload) {
92   - // 1. 构建主题
93   - String topic = buildDownstreamTopic(message, connectionInfo);
94   - if (StrUtil.isBlank(topic)) {
95   - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]",
96   - message.getDeviceId(), message.getMethod());
97   - return false;
98   - }
99   -
100   - // 2. 发送消息
101   - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false);
102   - if (success) {
103   - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]",
104   - message.getDeviceId(), topic, message.getMethod());
105   - } else {
106   - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]",
107   - message.getDeviceId(), topic, message.getMethod());
108   - }
109   - return success;
110   - }
111   -
112   - /**
113   - * 构建下行消息主题
114   - *
115   - * @param message 设备消息
116   - * @param connectionInfo 连接信息
117   - * @return 主题
118   - */
119   - private String buildDownstreamTopic(IotDeviceMessage message,
120   - IotMqttConnectionManager.ConnectionInfo connectionInfo) {
121   - String method = message.getMethod();
122   - if (StrUtil.isBlank(method)) {
123   - return null;
124   - }
125   -
126   - // 使用工具类构建主题,支持回复消息处理
127   - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
128   - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(),
129   - connectionInfo.getDeviceName(), isReply);
130   - }
131   -
132   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.ArrayUtil;
5   -import cn.hutool.core.util.BooleanUtil;
6   -import cn.hutool.core.util.StrUtil;
7   -import cn.hutool.extra.spring.SpringUtil;
8   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
12   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
13   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
14   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
15   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
16   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
17   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
18   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
19   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
20   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
21   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
22   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
23   -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
24   -import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
25   -import io.netty.handler.codec.mqtt.MqttQoS;
26   -import io.vertx.mqtt.MqttEndpoint;
27   -import io.vertx.mqtt.MqttTopicSubscription;
28   -import lombok.extern.slf4j.Slf4j;
29   -
30   -import java.util.List;
31   -import java.util.Map;
32   -
33   -/**
34   - * MQTT 上行消息处理器
35   - *
36   - * @author 芋道源码
37   - */
38   -@Slf4j
39   -public class IotMqttUpstreamHandler {
40   -
41   - /**
42   - * 默认编解码类型(MQTT 使用 Alink 协议)
43   - */
44   - private static final String DEFAULT_CODEC_TYPE = "Alink";
45   -
46   - /**
47   - * register 请求的 topic 后缀
48   - */
49   - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
50   -
51   - private final IotDeviceMessageService deviceMessageService;
52   -
53   - private final IotMqttConnectionManager connectionManager;
54   -
55   - private final IotDeviceCommonApi deviceApi;
56   -
57   - private final String serverId;
58   -
59   - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol,
60   - IotDeviceMessageService deviceMessageService,
61   - IotMqttConnectionManager connectionManager) {
62   - this.deviceMessageService = deviceMessageService;
63   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
64   - this.connectionManager = connectionManager;
65   - this.serverId = protocol.getServerId();
66   - }
67   -
68   - /**
69   - * 处理 MQTT 连接
70   - *
71   - * @param endpoint MQTT 连接端点
72   - */
73   - public void handle(MqttEndpoint endpoint) {
74   - String clientId = endpoint.clientIdentifier();
75   - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
76   - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
77   -
78   - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
79   - clientId, username, connectionManager.getEndpointAddress(endpoint));
80   -
81   - // 1. 先进行认证
82   - if (!authenticateDevice(clientId, username, password, endpoint)) {
83   - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username);
84   - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
85   - return;
86   - }
87   -
88   - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
89   -
90   - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
91   - endpoint.pingHandler(v -> {
92   - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
93   - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理
94   - });
95   -
96   - // 3. 设置异常和关闭处理器
97   - endpoint.exceptionHandler(ex -> {
98   - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
99   - cleanupConnection(endpoint);
100   - });
101   - endpoint.closeHandler(v -> {
102   - cleanupConnection(endpoint);
103   - });
104   -
105   - // 4. 设置消息处理器
106   - endpoint.publishHandler(mqttMessage -> {
107   - try {
108   - // 4.1 根据 topic 判断是否为 register 请求
109   - String topic = mqttMessage.topicName();
110   - byte[] payload = mqttMessage.payload().getBytes();
111   - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
112   - // register 请求:使用默认编解码器处理(设备可能未注册)
113   - processRegisterMessage(clientId, topic, payload, endpoint);
114   - } else {
115   - // 业务请求:正常处理
116   - processMessage(clientId, topic, payload);
117   - }
118   -
119   - // 4.2 根据 QoS 级别发送相应的确认消息
120   - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
121   - // QoS 1: 发送 PUBACK 确认
122   - endpoint.publishAcknowledge(mqttMessage.messageId());
123   - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
124   - // QoS 2: 发送 PUBREC 确认
125   - endpoint.publishReceived(mqttMessage.messageId());
126   - }
127   - // QoS 0 无需确认
128   - } catch (Exception e) {
129   - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
130   - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
131   - cleanupConnection(endpoint);
132   - endpoint.close();
133   - }
134   - });
135   -
136   - // 5. 设置订阅处理器
137   - endpoint.subscribeHandler(subscribe -> {
138   - // 提取主题名称列表用于日志显示
139   - List<String> topicNames = subscribe.topicSubscriptions().stream()
140   - .map(MqttTopicSubscription::topicName)
141   - .collect(java.util.stream.Collectors.toList());
142   - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
143   -
144   - // 提取 QoS 列表
145   - List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream()
146   - .map(MqttTopicSubscription::qualityOfService)
147   - .collect(java.util.stream.Collectors.toList());
148   - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
149   - });
150   -
151   - // 6. 设置取消订阅处理器
152   - endpoint.unsubscribeHandler(unsubscribe -> {
153   - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
154   - endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
155   - });
156   -
157   - // 7. 设置 QoS 2消息的 PUBREL 处理器
158   - endpoint.publishReleaseHandler(endpoint::publishComplete);
159   -
160   - // 8. 设置断开连接处理器
161   - endpoint.disconnectHandler(v -> {
162   - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
163   - cleanupConnection(endpoint);
164   - });
165   -
166   - // 9. 接受连接
167   - endpoint.accept(false);
168   - }
169   -
170   - /**
171   - * 处理消息
172   - *
173   - * @param clientId 客户端 ID
174   - * @param topic 主题
175   - * @param payload 消息内容
176   - */
177   - private void processMessage(String clientId, String topic, byte[] payload) {
178   - // 1. 基础检查
179   - if (payload == null || payload.length == 0) {
180   - return;
181   - }
182   -
183   - // 2. 解析主题,获取 productKey 和 deviceName
184   - String[] topicParts = topic.split("/");
185   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
186   - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
187   - return;
188   - }
189   -
190   - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
191   - String productKey = topicParts[2];
192   - String deviceName = topicParts[3];
193   - try {
194   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
195   - if (message == null) {
196   - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
197   - return;
198   - }
199   -
200   - // 4. 处理业务消息(认证已在连接时完成)
201   - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
202   - productKey, deviceName, message.getMethod());
203   - handleBusinessRequest(message, productKey, deviceName);
204   - } catch (Exception e) {
205   - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
206   - clientId, topic, e.getMessage(), e);
207   - }
208   - }
209   -
210   - /**
211   - * 在 MQTT 连接时进行设备认证
212   - *
213   - * @param clientId 客户端 ID
214   - * @param username 用户名
215   - * @param password 密码
216   - * @param endpoint MQTT 连接端点
217   - * @return 认证是否成功
218   - */
219   - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) {
220   - try {
221   - // 1. 参数校验
222   - if (StrUtil.hasEmpty(clientId, username, password)) {
223   - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username);
224   - return false;
225   - }
226   -
227   - // 2. 构建认证参数
228   - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
229   - .setClientId(clientId)
230   - .setUsername(username)
231   - .setPassword(password);
232   -
233   - // 3. 调用设备认证 API
234   - CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
235   - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
236   - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]",
237   - clientId, username, authResult.getMsg());
238   - return false;
239   - }
240   -
241   - // 4. 获取设备信息
242   - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
243   - if (deviceInfo == null) {
244   - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
245   - return false;
246   - }
247   -
248   - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO()
249   - .setProductKey(deviceInfo.getProductKey())
250   - .setDeviceName(deviceInfo.getDeviceName());
251   -
252   - CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO);
253   - if (!deviceResult.isSuccess() || deviceResult.getData() == null) {
254   - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]",
255   - clientId, username, deviceResult.getMsg());
256   - return false;
257   - }
258   -
259   - // 5. 注册连接
260   - IotDeviceRespDTO device = deviceResult.getData();
261   - registerConnection(endpoint, device, clientId);
262   -
263   - // 6. 发送设备上线消息
264   - sendOnlineMessage(device);
265   -
266   - return true;
267   - } catch (Exception e) {
268   - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e);
269   - return false;
270   - }
271   - }
272   -
273   - /**
274   - * 处理 register 消息(设备动态注册,使用默认编解码器)
275   - *
276   - * @param clientId 客户端 ID
277   - * @param topic 主题
278   - * @param payload 消息内容
279   - * @param endpoint MQTT 连接端点
280   - */
281   - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
282   - // 1.1 基础检查
283   - if (ArrayUtil.isEmpty(payload)) {
284   - return;
285   - }
286   - // 1.2 解析主题,获取 productKey 和 deviceName
287   - String[] topicParts = topic.split("/");
288   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
289   - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
290   - return;
291   - }
292   - String productKey = topicParts[2];
293   - String deviceName = topicParts[3];
294   -
295   - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
296   - IotDeviceMessage message;
297   - try {
298   - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
299   - if (message == null) {
300   - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
301   - return;
302   - }
303   - } catch (Exception e) {
304   - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
305   - clientId, topic, e.getMessage(), e);
306   - return;
307   - }
308   -
309   - // 3. 处理设备动态注册请求
310   - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
311   - productKey, deviceName, message.getMethod());
312   - try {
313   - handleRegisterRequest(message, productKey, deviceName, endpoint);
314   - } catch (Exception e) {
315   - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
316   - clientId, topic, e.getMessage(), e);
317   - }
318   - }
319   -
320   - /**
321   - * 处理设备动态注册请求(一型一密,不需要 deviceSecret)
322   - *
323   - * @param message 消息信息
324   - * @param productKey 产品 Key
325   - * @param deviceName 设备名称
326   - * @param endpoint MQTT 连接端点
327   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
328   - */
329   - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) {
330   - String clientId = endpoint.clientIdentifier();
331   - try {
332   - // 1. 解析注册参数
333   - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
334   - if (params == null) {
335   - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
336   - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整");
337   - return;
338   - }
339   -
340   - // 2. 调用动态注册 API
341   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
342   - if (result.isError()) {
343   - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
344   - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg());
345   - return;
346   - }
347   -
348   - // 3. 发送成功响应(包含 deviceSecret)
349   - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData());
350   - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
351   - params.getDeviceName(), clientId);
352   - } catch (Exception e) {
353   - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
354   - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常");
355   - }
356   - }
357   -
358   - /**
359   - * 解析注册参数
360   - *
361   - * @param params 参数对象(通常为 Map 类型)
362   - * @return 注册参数 DTO,解析失败时返回 null
363   - */
364   - @SuppressWarnings({"unchecked", "DuplicatedCode"})
365   - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
366   - if (params == null) {
367   - return null;
368   - }
369   - try {
370   - // 参数默认为 Map 类型,直接转换
371   - if (params instanceof Map) {
372   - Map<String, Object> paramMap = (Map<String, Object>) params;
373   - return new IotDeviceRegisterReqDTO()
374   - .setProductKey(MapUtil.getStr(paramMap, "productKey"))
375   - .setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
376   - .setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
377   - }
378   - // 如果已经是目标类型,直接返回
379   - if (params instanceof IotDeviceRegisterReqDTO) {
380   - return (IotDeviceRegisterReqDTO) params;
381   - }
382   -
383   - // 其他情况尝试 JSON 转换
384   - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
385   - } catch (Exception e) {
386   - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
387   - return null;
388   - }
389   - }
390   -
391   - /**
392   - * 发送注册成功响应(包含 deviceSecret)
393   - *
394   - * @param endpoint MQTT 连接端点
395   - * @param productKey 产品 Key
396   - * @param deviceName 设备名称
397   - * @param requestId 请求 ID
398   - * @param registerResp 注册响应
399   - */
400   - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
401   - String requestId, IotDeviceRegisterRespDTO registerResp) {
402   - try {
403   - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
404   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
405   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
406   -
407   - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
408   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
409   -
410   - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
411   - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
412   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
413   - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
414   - MqttQoS.AT_LEAST_ONCE, false, false);
415   - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic);
416   - } catch (Exception e) {
417   - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]",
418   - endpoint.clientIdentifier(), e);
419   - }
420   - }
421   -
422   - /**
423   - * 发送注册错误响应
424   - *
425   - * @param endpoint MQTT 连接端点
426   - * @param productKey 产品 Key
427   - * @param deviceName 设备名称
428   - * @param requestId 请求 ID
429   - * @param errorMessage 错误消息
430   - */
431   - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
432   - String requestId, String errorMessage) {
433   - try {
434   - // 1. 构建响应消息
435   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
436   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage);
437   -
438   - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
439   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
440   -
441   - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
442   - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
443   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
444   - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
445   - MqttQoS.AT_LEAST_ONCE, false, false);
446   - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic);
447   - } catch (Exception e) {
448   - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]",
449   - endpoint.clientIdentifier(), e);
450   - }
451   - }
452   -
453   - /**
454   - * 处理业务请求
455   - */
456   - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) {
457   - // 发送消息到消息总线
458   - message.setServerId(serverId);
459   - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
460   - }
461   -
462   - /**
463   - * 注册连接
464   - */
465   - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
466   - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
467   - .setDeviceId(device.getId())
468   - .setProductKey(device.getProductKey())
469   - .setDeviceName(device.getDeviceName())
470   - .setClientId(clientId)
471   - .setAuthenticated(true)
472   - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
473   - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
474   - }
475   -
476   - /**
477   - * 发送设备上线消息
478   - */
479   - private void sendOnlineMessage(IotDeviceRespDTO device) {
480   - try {
481   - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
482   - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
483   - device.getDeviceName(), serverId);
484   - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
485   - } catch (Exception e) {
486   - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage());
487   - }
488   - }
489   -
490   - /**
491   - * 清理连接
492   - */
493   - private void cleanupConnection(MqttEndpoint endpoint) {
494   - try {
495   - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
496   - if (connectionInfo != null) {
497   - // 发送设备离线消息
498   - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
499   - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
500   - connectionInfo.getDeviceName(), serverId);
501   - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
502   - }
503   -
504   - // 注销连接
505   - connectionManager.unregisterConnection(endpoint);
506   - } catch (Exception e) {
507   - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
508   - }
509   - }
510   -
511   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java
1 1 /**
2   - * 提供设备接入的各种协议的实现
  2 + * 设备接入协议:MQTT、EMQX、HTTP、TCP 等协议的实现
3 3 */
4   -package cn.iocoder.yudao.module.iot.gateway.protocol;
5 4 \ No newline at end of file
  5 +package cn.iocoder.yudao.module.iot.gateway.protocol;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  4 +import jakarta.validation.Valid;
  5 +import jakarta.validation.constraints.Min;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +/**
  10 + * IoT TCP 协议配置
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +public class IotTcpConfig {
  16 +
  17 + /**
  18 + * 最大连接数
  19 + */
  20 + @NotNull(message = "最大连接数不能为空")
  21 + @Min(value = 1, message = "最大连接数必须大于 0")
  22 + private Integer maxConnections = 1000;
  23 + /**
  24 + * 心跳超时时间(毫秒)
  25 + */
  26 + @NotNull(message = "心跳超时时间不能为空")
  27 + @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒")
  28 + private Long keepAliveTimeoutMs = 30000L;
  29 +
  30 + /**
  31 + * 拆包配置
  32 + */
  33 + @Valid
  34 + private CodecConfig codec;
  35 +
  36 + /**
  37 + * TCP 拆包配置
  38 + */
  39 + @Data
  40 + public static class CodecConfig {
  41 +
  42 + /**
  43 + * 拆包类型
  44 + *
  45 + * @see IotTcpCodecTypeEnum
  46 + */
  47 + @NotNull(message = "拆包类型不能为空")
  48 + private String type;
  49 +
  50 + /**
  51 + * LENGTH_FIELD: 长度字段偏移量
  52 + * <p>
  53 + * 表示长度字段在消息中的起始位置(从 0 开始)
  54 + */
  55 + private Integer lengthFieldOffset;
  56 + /**
  57 + * LENGTH_FIELD: 长度字段长度(字节数)
  58 + * <p>
  59 + * 常见值:1(最大 255)、2(最大 65535)、4(最大 2GB)
  60 + */
  61 + private Integer lengthFieldLength;
  62 + /**
  63 + * LENGTH_FIELD: 长度调整值
  64 + * <p>
  65 + * 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度
  66 + */
  67 + private Integer lengthAdjustment = 0;
  68 + /**
  69 + * LENGTH_FIELD: 跳过的初始字节数
  70 + * <p>
  71 + * 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength
  72 + */
  73 + private Integer initialBytesToStrip = 0;
  74 +
  75 + /**
  76 + * DELIMITER: 分隔符
  77 + * <p>
  78 + * 支持转义字符:\n(换行)、\r(回车)、\r\n(回车换行)
  79 + */
  80 + private String delimiter;
  81 +
  82 + /**
  83 + * FIXED_LENGTH: 固定消息长度(字节)
  84 + * <p>
  85 + * 每条消息的固定长度
  86 + */
  87 + private Integer fixedLength;
  88 +
  89 + }
  90 +
  91 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
2   -
3   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
9   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10   -import jakarta.annotation.PostConstruct;
11   -import lombok.RequiredArgsConstructor;
12   -import lombok.extern.slf4j.Slf4j;
13   -
14   -/**
15   - * IoT 网关 TCP 下游订阅者:接收下行给设备的消息
16   - *
17   - * @author 芋道源码
18   - */
19   -@Slf4j
20   -@RequiredArgsConstructor
21   -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
22   -
23   - private final IotTcpUpstreamProtocol protocol;
24   -
25   - private final IotDeviceMessageService messageService;
26   -
27   - private final IotTcpConnectionManager connectionManager;
28   -
29   - private final IotMessageBus messageBus;
30   -
31   - private IotTcpDownstreamHandler downstreamHandler;
32   -
33   - @PostConstruct
34   - public void init() {
35   - // 初始化下游处理器
36   - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager);
37   - // 注册下游订阅者
38   - messageBus.register(this);
39   - log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
40   - protocol.getServerId(), getTopic());
41   - }
42   -
43   - @Override
44   - public String getTopic() {
45   - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
46   - }
47   -
48   - @Override
49   - public String getGroup() {
50   - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
51   - return getTopic();
52   - }
53   -
54   - @Override
55   - public void onMessage(IotDeviceMessage message) {
56   - try {
57   - downstreamHandler.handle(message);
58   - } catch (Exception e) {
59   - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
60   - message.getDeviceId(), message.getMethod(), message.getId(), e);
61   - }
62   - }
63   -
64   -}
65 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  7 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  8 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  9 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
  17 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  18 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
  19 +import io.vertx.core.Vertx;
  20 +import io.vertx.core.net.NetServer;
  21 +import io.vertx.core.net.NetServerOptions;
  22 +import io.vertx.core.net.PemKeyCertOptions;
  23 +import lombok.Getter;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +import cn.hutool.core.lang.Assert;
  26 +
  27 +/**
  28 + * IoT TCP 协议实现
  29 + * <p>
  30 + * 基于 Vert.x 实现 TCP 服务器,接收设备上行消息
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@Slf4j
  35 +public class IotTcpProtocol implements IotProtocol {
  36 +
  37 + /**
  38 + * 协议配置
  39 + */
  40 + private final ProtocolProperties properties;
  41 + /**
  42 + * 服务器 ID(用于消息追踪,全局唯一)
  43 + */
  44 + @Getter
  45 + private final String serverId;
  46 +
  47 + /**
  48 + * 运行状态
  49 + */
  50 + @Getter
  51 + private volatile boolean running = false;
  52 +
  53 + /**
  54 + * Vert.x 实例
  55 + */
  56 + private Vertx vertx;
  57 + /**
  58 + * TCP 服务器
  59 + */
  60 + private NetServer tcpServer;
  61 + /**
  62 + * TCP 连接管理器
  63 + */
  64 + private final IotTcpConnectionManager connectionManager;
  65 +
  66 + /**
  67 + * 下行消息订阅者
  68 + */
  69 + private final IotTcpDownstreamSubscriber downstreamSubscriber;
  70 +
  71 + /**
  72 + * 消息序列化器
  73 + */
  74 + private final IotMessageSerializer serializer;
  75 + /**
  76 + * TCP 帧编解码器
  77 + */
  78 + private final IotTcpFrameCodec frameCodec;
  79 +
  80 + public IotTcpProtocol(ProtocolProperties properties) {
  81 + IotTcpConfig tcpConfig = properties.getTcp();
  82 + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空");
  83 + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空");
  84 + this.properties = properties;
  85 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  86 +
  87 + // 初始化序列化器
  88 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize());
  89 + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize());
  90 + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class);
  91 + this.serializer = serializerManager.get(serializeType);
  92 + // 初始化帧编解码器
  93 + this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec());
  94 +
  95 + // 初始化连接管理器
  96 + this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections());
  97 +
  98 + // 初始化下行消息订阅者
  99 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  100 + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer);
  101 + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus);
  102 + }
  103 +
  104 + @Override
  105 + public String getId() {
  106 + return properties.getId();
  107 + }
  108 +
  109 + @Override
  110 + public IotProtocolTypeEnum getType() {
  111 + return IotProtocolTypeEnum.TCP;
  112 + }
  113 +
  114 + @Override
  115 + public void start() {
  116 + if (running) {
  117 + log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId());
  118 + return;
  119 + }
  120 +
  121 + // 1.1 创建 Vertx 实例
  122 + this.vertx = Vertx.vertx();
  123 +
  124 + // 1.2 创建服务器选项
  125 + IotTcpConfig tcpConfig = properties.getTcp();
  126 + NetServerOptions options = new NetServerOptions()
  127 + .setPort(properties.getPort())
  128 + .setTcpKeepAlive(true)
  129 + .setTcpNoDelay(true)
  130 + .setReuseAddress(true)
  131 + .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时
  132 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  133 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
  134 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  135 + .setKeyPath(sslConfig.getSslKeyPath())
  136 + .setCertPath(sslConfig.getSslCertPath());
  137 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  138 + }
  139 +
  140 + // 1.3 创建服务器并设置连接处理器
  141 + tcpServer = vertx.createNetServer(options);
  142 + tcpServer.connectHandler(socket -> {
  143 + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(serverId, frameCodec, serializer, connectionManager);
  144 + handler.handle(socket);
  145 + });
  146 +
  147 + // 1.4 启动 TCP 服务器
  148 + try {
  149 + tcpServer.listen().result();
  150 + running = true;
  151 + log.info("[start][IoT TCP 协议 {} 启动成功,端口:{},serverId:{}]",
  152 + getId(), properties.getPort(), serverId);
  153 +
  154 + // 2. 启动下行消息订阅者
  155 + this.downstreamSubscriber.start();
  156 + } catch (Exception e) {
  157 + log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e);
  158 + // 启动失败时关闭资源
  159 + if (tcpServer != null) {
  160 + tcpServer.close();
  161 + tcpServer = null;
  162 + }
  163 + if (vertx != null) {
  164 + vertx.close();
  165 + vertx = null;
  166 + }
  167 + throw e;
  168 + }
  169 + }
  170 +
  171 + @Override
  172 + public void stop() {
  173 + if (!running) {
  174 + return;
  175 + }
  176 + // 1. 停止下行消息订阅者
  177 + try {
  178 + downstreamSubscriber.stop();
  179 + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId());
  180 + } catch (Exception e) {
  181 + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  182 + }
  183 +
  184 + // 2.1 关闭 TCP 服务器
  185 + if (tcpServer != null) {
  186 + try {
  187 + tcpServer.close().result();
  188 + log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId());
  189 + } catch (Exception e) {
  190 + log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e);
  191 + }
  192 + tcpServer = null;
  193 + }
  194 + // 2.2 关闭 Vertx 实例
  195 + if (vertx != null) {
  196 + try {
  197 + vertx.close().result();
  198 + log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId());
  199 + } catch (Exception e) {
  200 + log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e);
  201 + }
  202 + vertx = null;
  203 + }
  204 + running = false;
  205 + log.info("[stop][IoT TCP 协议 {} 已停止]", getId());
  206 + }
  207 +
  208 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
2   -
3   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
7   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
8   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
9   -import io.vertx.core.Vertx;
10   -import io.vertx.core.net.NetServer;
11   -import io.vertx.core.net.NetServerOptions;
12   -import io.vertx.core.net.PemKeyCertOptions;
13   -import jakarta.annotation.PostConstruct;
14   -import jakarta.annotation.PreDestroy;
15   -import lombok.Getter;
16   -import lombok.extern.slf4j.Slf4j;
17   -
18   -/**
19   - * IoT 网关 TCP 协议:接收设备上行消息
20   - *
21   - * @author 芋道源码
22   - */
23   -@Slf4j
24   -public class IotTcpUpstreamProtocol {
25   -
26   - private final IotGatewayProperties.TcpProperties tcpProperties;
27   -
28   - private final IotDeviceService deviceService;
29   -
30   - private final IotDeviceMessageService messageService;
31   -
32   - private final IotTcpConnectionManager connectionManager;
33   -
34   - private final Vertx vertx;
35   -
36   - @Getter
37   - private final String serverId;
38   -
39   - private NetServer tcpServer;
40   -
41   - public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
42   - IotDeviceService deviceService,
43   - IotDeviceMessageService messageService,
44   - IotTcpConnectionManager connectionManager,
45   - Vertx vertx) {
46   - this.tcpProperties = tcpProperties;
47   - this.deviceService = deviceService;
48   - this.messageService = messageService;
49   - this.connectionManager = connectionManager;
50   - this.vertx = vertx;
51   - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
52   - }
53   -
54   - @PostConstruct
55   - public void start() {
56   - // 创建服务器选项
57   - NetServerOptions options = new NetServerOptions()
58   - .setPort(tcpProperties.getPort())
59   - .setTcpKeepAlive(true)
60   - .setTcpNoDelay(true)
61   - .setReuseAddress(true);
62   - // 配置 SSL(如果启用)
63   - if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
64   - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
65   - .setKeyPath(tcpProperties.getSslKeyPath())
66   - .setCertPath(tcpProperties.getSslCertPath());
67   - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
68   - }
69   -
70   - // 创建服务器并设置连接处理器
71   - tcpServer = vertx.createNetServer(options);
72   - tcpServer.connectHandler(socket -> {
73   - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService,
74   - connectionManager);
75   - handler.handle(socket);
76   - });
77   -
78   - // 启动服务器
79   - try {
80   - tcpServer.listen().result();
81   - log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort());
82   - } catch (Exception e) {
83   - log.error("[start][IoT 网关 TCP 协议启动失败]", e);
84   - throw e;
85   - }
86   - }
87   -
88   - @PreDestroy
89   - public void stop() {
90   - if (tcpServer != null) {
91   - try {
92   - tcpServer.close().result();
93   - log.info("[stop][IoT 网关 TCP 协议已停止]");
94   - } catch (Exception e) {
95   - log.error("[stop][IoT 网关 TCP 协议停止失败]", e);
96   - }
97   - }
98   - }
99   -}
100 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec;
  7 +import lombok.AllArgsConstructor;
  8 +import lombok.Getter;
  9 +
  10 +/**
  11 + * IoT TCP 拆包类型枚举
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@AllArgsConstructor
  16 +@Getter
  17 +public enum IotTcpCodecTypeEnum {
  18 +
  19 + /**
  20 + * 基于固定长度的拆包
  21 + */
  22 + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec.class),
  23 +
  24 + /**
  25 + * 基于分隔符的拆包
  26 + */
  27 + DELIMITER("delimiter", IotTcpDelimiterFrameCodec.class),
  28 +
  29 + /**
  30 + * 基于长度字段的拆包
  31 + */
  32 + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec.class),
  33 + ;
  34 +
  35 + /**
  36 + * 类型标识
  37 + */
  38 + private final String type;
  39 + /**
  40 + * 编解码器类
  41 + */
  42 + private final Class<? extends IotTcpFrameCodec> codecClass;
  43 +
  44 + /**
  45 + * 根据类型获取枚举
  46 + *
  47 + * @param type 类型标识
  48 + * @return 枚举值
  49 + */
  50 + public static IotTcpCodecTypeEnum of(String type) {
  51 + return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values());
  52 + }
  53 +
  54 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
  2 +
  3 +import io.vertx.core.Handler;
  4 +import io.vertx.core.buffer.Buffer;
  5 +import io.vertx.core.parsetools.RecordParser;
  6 +
  7 +/**
  8 + * IoT TCP 帧编解码器接口
  9 + * <p>
  10 + * 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public interface IotTcpFrameCodec {
  15 +
  16 + /**
  17 + * 获取编解码器类型
  18 + *
  19 + * @return 编解码器类型
  20 + */
  21 + IotTcpCodecTypeEnum getType();
  22 +
  23 + /**
  24 + * 创建解码器(RecordParser)
  25 + * <p>
  26 + * 每个连接调用一次,返回的 parser 需绑定到 socket.handler()
  27 + *
  28 + * @param handler 消息处理器,当收到完整的消息帧后回调
  29 + * @return RecordParser 实例
  30 + */
  31 + RecordParser createDecodeParser(Handler<Buffer> handler);
  32 +
  33 + /**
  34 + * 编码消息(加帧)
  35 + * <p>
  36 + * 根据不同的编解码类型添加帧头/分隔符
  37 + *
  38 + * @param data 原始数据
  39 + * @return 编码后的数据(带帧头/分隔符)
  40 + */
  41 + Buffer encode(byte[] data);
  42 +
  43 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ReflectUtil;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  6 +
  7 +/**
  8 + * IoT TCP 帧编解码器工厂
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public class IotTcpFrameCodecFactory {
  13 +
  14 + /**
  15 + * 根据配置创建编解码器
  16 + *
  17 + * @param config 拆包配置
  18 + * @return 编解码器实例,如果配置为空则返回 null
  19 + */
  20 + public static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) {
  21 + Assert.notNull(config, "CodecConfig 不能为空");
  22 + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType());
  23 + Assert.notNull(type, "不支持的 CodecType 类型:" + config.getType());
  24 + return ReflectUtil.newInstance(type.getCodecClass(), config);
  25 + }
  26 +
  27 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  7 +import io.vertx.core.Handler;
  8 +import io.vertx.core.buffer.Buffer;
  9 +import io.vertx.core.parsetools.RecordParser;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import cn.hutool.core.lang.Assert;
  12 +
  13 +/**
  14 + * IoT TCP 分隔符帧编解码器
  15 + * <p>
  16 + * 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符
  17 + * <p>
  18 + * 支持的分隔符:
  19 + * <ul>
  20 + * <li>\n - 换行符</li>
  21 + * <li>\r - 回车符</li>
  22 + * <li>\r\n - 回车换行</li>
  23 + * <li>自定义字符串</li>
  24 + * </ul>
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@Slf4j
  29 +public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
  30 +
  31 + /**
  32 + * 最大记录大小(64KB),防止 DoS 攻击
  33 + */
  34 + private static final int MAX_RECORD_SIZE = 65536;
  35 +
  36 + /**
  37 + * 解析后的分隔符字节数组
  38 + */
  39 + private final byte[] delimiterBytes;
  40 +
  41 + public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
  42 + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
  43 + this.delimiterBytes = parseDelimiter(config.getDelimiter());
  44 + }
  45 +
  46 + @Override
  47 + public IotTcpCodecTypeEnum getType() {
  48 + return IotTcpCodecTypeEnum.DELIMITER;
  49 + }
  50 +
  51 + @Override
  52 + public RecordParser createDecodeParser(Handler<Buffer> handler) {
  53 + RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes));
  54 + parser.maxRecordSize(MAX_RECORD_SIZE); // 设置最大记录大小,防止 DoS 攻击
  55 + // 处理完整消息(不包含分隔符)
  56 + parser.handler(handler);
  57 + parser.exceptionHandler(ex -> {
  58 + throw new RuntimeException("[createDecodeParser][解析异常]", ex);
  59 + });
  60 + return parser;
  61 + }
  62 +
  63 + @Override
  64 + public Buffer encode(byte[] data) {
  65 + Buffer buffer = Buffer.buffer();
  66 + buffer.appendBytes(data);
  67 + buffer.appendBytes(delimiterBytes);
  68 + return buffer;
  69 + }
  70 +
  71 + /**
  72 + * 解析分隔符字符串为字节数组
  73 + * <p>
  74 + * 支持转义字符:\n、\r、\r\n、\t
  75 + *
  76 + * @param delimiter 分隔符字符串
  77 + * @return 分隔符字节数组
  78 + */
  79 + private byte[] parseDelimiter(String delimiter) {
  80 + // 处理转义字符
  81 + String parsed = delimiter
  82 + .replace("\\r\\n", "\r\n")
  83 + .replace("\\r", "\r")
  84 + .replace("\\n", "\n")
  85 + .replace("\\t", "\t");
  86 + return StrUtil.utf8Bytes(parsed);
  87 + }
  88 +
  89 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  6 +import io.vertx.core.Handler;
  7 +import io.vertx.core.buffer.Buffer;
  8 +import io.vertx.core.parsetools.RecordParser;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import cn.hutool.core.lang.Assert;
  11 +
  12 +/**
  13 + * IoT TCP 定长帧编解码器
  14 + * <p>
  15 + * 基于固定长度的拆包策略,每条消息固定字节数
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Slf4j
  20 +public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec {
  21 +
  22 + /**
  23 + * 固定消息长度
  24 + */
  25 + private final int fixedLength;
  26 +
  27 + public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) {
  28 + Assert.notNull(config.getFixedLength(), "fixedLength 不能为空");
  29 + this.fixedLength = config.getFixedLength();
  30 + }
  31 +
  32 + @Override
  33 + public IotTcpCodecTypeEnum getType() {
  34 + return IotTcpCodecTypeEnum.FIXED_LENGTH;
  35 + }
  36 +
  37 + @Override
  38 + public RecordParser createDecodeParser(Handler<Buffer> handler) {
  39 + RecordParser parser = RecordParser.newFixed(fixedLength);
  40 + parser.handler(handler);
  41 + parser.exceptionHandler(ex -> {
  42 + throw new RuntimeException("[createDecodeParser][解析异常]", ex);
  43 + });
  44 + return parser;
  45 + }
  46 +
  47 + @Override
  48 + public Buffer encode(byte[] data) {
  49 + // 校验数据长度不能超过固定长度
  50 + if (data.length > fixedLength) {
  51 + throw new IllegalArgumentException(String.format(
  52 + "数据长度 %d 超过固定长度 %d", data.length, fixedLength));
  53 + }
  54 + Buffer buffer = Buffer.buffer(fixedLength);
  55 + buffer.appendBytes(data);
  56 + // 如果数据不足固定长度,填充 0(RecordParser.newFixed 解码时按固定长度读取,所以发送端需要填充)
  57 + if (data.length < fixedLength) {
  58 + byte[] padding = new byte[fixedLength - data.length];
  59 + buffer.appendBytes(padding);
  60 + }
  61 + return buffer;
  62 + }
  63 +
  64 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  6 +import io.vertx.core.Handler;
  7 +import io.vertx.core.buffer.Buffer;
  8 +import io.vertx.core.parsetools.RecordParser;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import cn.hutool.core.lang.Assert;
  11 +
  12 +import java.util.concurrent.atomic.AtomicReference;
  13 +
  14 +/**
  15 + * IoT TCP 长度字段帧编解码器
  16 + * <p>
  17 + * 基于长度字段的拆包策略,消息格式:[长度字段][消息体]
  18 + * <p>
  19 + * 参数说明:
  20 + * <ul>
  21 + * <li>lengthFieldOffset: 长度字段在消息中的偏移量</li>
  22 + * <li>lengthFieldLength: 长度字段的字节数(1/2/4)</li>
  23 + * <li>lengthAdjustment: 长度调整值,用于调整长度字段的实际含义</li>
  24 + * <li>initialBytesToStrip: 解码后跳过的字节数</li>
  25 + * </ul>
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@Slf4j
  30 +public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec {
  31 +
  32 + /**
  33 + * 最大帧长度(64KB),防止 DoS 攻击
  34 + */
  35 + private static final int MAX_FRAME_LENGTH = 65536;
  36 +
  37 + private final int lengthFieldOffset;
  38 + private final int lengthFieldLength;
  39 + private final int lengthAdjustment;
  40 + private final int initialBytesToStrip;
  41 +
  42 + /**
  43 + * 头部长度 = 长度字段偏移量 + 长度字段长度
  44 + */
  45 + private final int headerLength;
  46 +
  47 + public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) {
  48 + Assert.notNull(config.getLengthFieldOffset(), "lengthFieldOffset 不能为空");
  49 + Assert.notNull(config.getLengthFieldLength(), "lengthFieldLength 不能为空");
  50 + Assert.notNull(config.getLengthAdjustment(), "lengthAdjustment 不能为空");
  51 + Assert.notNull(config.getInitialBytesToStrip(), "initialBytesToStrip 不能为空");
  52 + this.lengthFieldOffset = config.getLengthFieldOffset();
  53 + this.lengthFieldLength = config.getLengthFieldLength();
  54 + this.lengthAdjustment = config.getLengthAdjustment();
  55 + this.initialBytesToStrip = config.getInitialBytesToStrip();
  56 + this.headerLength = lengthFieldOffset + lengthFieldLength;
  57 + }
  58 +
  59 + @Override
  60 + public IotTcpCodecTypeEnum getType() {
  61 + return IotTcpCodecTypeEnum.LENGTH_FIELD;
  62 + }
  63 +
  64 + @Override
  65 + public RecordParser createDecodeParser(Handler<Buffer> handler) {
  66 + // 创建状态机:先读取头部,再读取消息体
  67 + RecordParser parser = RecordParser.newFixed(headerLength);
  68 + parser.maxRecordSize(MAX_FRAME_LENGTH); // 设置最大记录大小,防止 DoS 攻击
  69 + final AtomicReference<Integer> bodyLength = new AtomicReference<>(null); // 消息体长度,null 表示读取头部阶段
  70 + final AtomicReference<Buffer> headerBuffer = new AtomicReference<>(null); // 头部消息
  71 +
  72 + // 处理读取到的数据
  73 + parser.handler(buffer -> {
  74 + if (bodyLength.get() == null) {
  75 + // 阶段 1: 读取头部,解析长度字段
  76 + headerBuffer.set(buffer.copy());
  77 + int length = readLength(buffer, lengthFieldOffset, lengthFieldLength);
  78 + int frameBodyLength = length + lengthAdjustment;
  79 + // 检查帧长度是否合法
  80 + if (frameBodyLength < 0) {
  81 + throw new IllegalStateException(String.format(
  82 + "[createDecodeParser][帧长度异常,length: %d, frameBodyLength: %d]",
  83 + length, frameBodyLength));
  84 + }
  85 + // 消息体为空,抛出异常
  86 + if (frameBodyLength == 0) {
  87 + throw new IllegalStateException("[createDecodeParser][消息体不能为空]");
  88 + }
  89 +
  90 + // 【重要】切换到读取消息体模式
  91 + bodyLength.set(frameBodyLength);
  92 + parser.fixedSizeMode(frameBodyLength);
  93 + } else {
  94 + // 阶段 2: 读取消息体,组装完整帧
  95 + Buffer frame = processFrame(headerBuffer.get(), buffer);
  96 + // 重置状态,准备读取下一帧
  97 + bodyLength.set(null);
  98 + headerBuffer.set(null);
  99 + parser.fixedSizeMode(headerLength);
  100 +
  101 + // 【重要】处理完整消息
  102 + handler.handle(frame);
  103 + }
  104 + });
  105 + parser.exceptionHandler(ex -> {
  106 + throw new RuntimeException("[createDecodeParser][解析异常]", ex);
  107 + });
  108 + return parser;
  109 + }
  110 +
  111 + @Override
  112 + public Buffer encode(byte[] data) {
  113 + Buffer buffer = Buffer.buffer();
  114 + // 计算要写入的长度值
  115 + int lengthValue = data.length - lengthAdjustment;
  116 + // 写入偏移量前的填充字节(如果有)
  117 + for (int i = 0; i < lengthFieldOffset; i++) {
  118 + buffer.appendByte((byte) 0);
  119 + }
  120 + // 写入长度字段
  121 + writeLength(buffer, lengthValue, lengthFieldLength);
  122 + // 写入消息体
  123 + buffer.appendBytes(data);
  124 + return buffer;
  125 + }
  126 +
  127 + /**
  128 + * 从 Buffer 中读取长度字段
  129 + */
  130 + @SuppressWarnings("EnhancedSwitchMigration")
  131 + private int readLength(Buffer buffer, int offset, int length) {
  132 + switch (length) {
  133 + case 1:
  134 + return buffer.getUnsignedByte(offset);
  135 + case 2:
  136 + return buffer.getUnsignedShort(offset);
  137 + case 4:
  138 + return buffer.getInt(offset);
  139 + default:
  140 + throw new IllegalArgumentException("不支持的长度字段长度: " + length);
  141 + }
  142 + }
  143 +
  144 + /**
  145 + * 向 Buffer 中写入长度字段
  146 + */
  147 + private void writeLength(Buffer buffer, int length, int fieldLength) {
  148 + switch (fieldLength) {
  149 + case 1:
  150 + buffer.appendByte((byte) length);
  151 + break;
  152 + case 2:
  153 + buffer.appendShort((short) length);
  154 + break;
  155 + case 4:
  156 + buffer.appendInt(length);
  157 + break;
  158 + default:
  159 + throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength);
  160 + }
  161 + }
  162 +
  163 + /**
  164 + * 处理帧数据(根据 initialBytesToStrip 跳过指定字节)
  165 + */
  166 + private Buffer processFrame(Buffer header, Buffer body) {
  167 + Buffer fullFrame = Buffer.buffer();
  168 + if (header != null) {
  169 + fullFrame.appendBuffer(header);
  170 + }
  171 + if (body != null) {
  172 + fullFrame.appendBuffer(body);
  173 + }
  174 + // 根据 initialBytesToStrip 跳过指定字节
  175 + if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) {
  176 + return fullFrame.slice(initialBytesToStrip, fullFrame.length());
  177 + }
  178 + return fullFrame;
  179 + }
  180 +
  181 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
4 5 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
5   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  6 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  7 +import io.vertx.core.buffer.Buffer;
6 8 import lombok.RequiredArgsConstructor;
7 9 import lombok.extern.slf4j.Slf4j;
8 10  
... ... @@ -15,36 +17,44 @@ import lombok.extern.slf4j.Slf4j;
15 17 @RequiredArgsConstructor
16 18 public class IotTcpDownstreamHandler {
17 19  
18   - private final IotDeviceMessageService deviceMessageService;
19   -
20 20 private final IotTcpConnectionManager connectionManager;
21 21  
22 22 /**
  23 + * TCP 帧编解码器(处理粘包/拆包)
  24 + */
  25 + private final IotTcpFrameCodec codec;
  26 + /**
  27 + * 消息序列化器(处理业务消息序列化/反序列化)
  28 + */
  29 + private final IotMessageSerializer serializer;
  30 +
  31 + /**
23 32 * 处理下行消息
24 33 */
25 34 public void handle(IotDeviceMessage message) {
26 35 try {
27 36 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
28 37 message.getDeviceId(), message.getMethod(), message.getId());
29   -
30   - // 1. 获取连接信息(包含 codecType)
  38 + // 1. 检查设备连接
31 39 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
32 40 message.getDeviceId());
33 41 if (connectionInfo == null) {
34   - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
  42 + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
  43 + message.getDeviceId(), message.getMethod(), message.getId());
35 44 return;
36 45 }
37 46  
38   - // 2. 使用连接时的 codecType 编码消息,并发送到设备
39   - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
40   - boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
41   - if (success) {
42   - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
43   - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
44   - } else {
45   - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
46   - message.getDeviceId(), message.getMethod(), message.getId());
  47 + // 2. 序列化 + 帧编码
  48 + byte[] payload = serializer.serialize(message);
  49 + Buffer frameData = codec.encode(payload);
  50 +
  51 + // 3. 发送到设备
  52 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes());
  53 + if (!success) {
  54 + throw new RuntimeException("下行消息发送失败");
47 55 }
  56 + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
  57 + message.getDeviceId(), message.getMethod(), message.getId(), frameData.length());
48 58 } catch (Exception e) {
49 59 log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
50 60 message.getDeviceId(), message.getMethod(), message, e);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +
  17 + private final IotTcpDownstreamHandler downstreamHandler;
  18 +
  19 + public IotTcpDownstreamSubscriber(IotProtocol protocol,
  20 + IotTcpDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
  24 + }
  25 +
  26 + @Override
  27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
  29 + }
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream;
  2 +
  3 +import cn.hutool.core.util.BooleanUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  6 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  7 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  8 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  11 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  12 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  13 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  14 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  15 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  16 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
  19 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  20 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  21 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  22 +import io.vertx.core.Handler;
  23 +import io.vertx.core.buffer.Buffer;
  24 +import io.vertx.core.net.NetSocket;
  25 +import io.vertx.core.parsetools.RecordParser;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +import cn.hutool.core.lang.Assert;
  28 +
  29 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  30 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  31 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
  32 +
  33 +/**
  34 + * TCP 上行消息处理器
  35 + *
  36 + * @author 芋道源码
  37 + */
  38 +@Slf4j
  39 +public class IotTcpUpstreamHandler implements Handler<NetSocket> {
  40 +
  41 + private static final String AUTH_METHOD = "auth";
  42 +
  43 + private final String serverId;
  44 +
  45 + /**
  46 + * TCP 帧编解码器(处理粘包/拆包)
  47 + */
  48 + private final IotTcpFrameCodec codec;
  49 + /**
  50 + * 消息序列化器(处理业务消息序列化/反序列化)
  51 + */
  52 + private final IotMessageSerializer serializer;
  53 + /**
  54 + * TCP 连接管理器
  55 + */
  56 + private final IotTcpConnectionManager connectionManager;
  57 +
  58 + private final IotDeviceMessageService deviceMessageService;
  59 + private final IotDeviceService deviceService;
  60 + private final IotDeviceCommonApi deviceApi;
  61 +
  62 + public IotTcpUpstreamHandler(String serverId,
  63 + IotTcpFrameCodec codec,
  64 + IotMessageSerializer serializer,
  65 + IotTcpConnectionManager connectionManager) {
  66 + Assert.notNull(codec, "TCP FrameCodec 必须配置");
  67 + Assert.notNull(serializer, "消息序列化器必须配置");
  68 + Assert.notNull(connectionManager, "连接管理器不能为空");
  69 + this.serverId = serverId;
  70 + this.codec = codec;
  71 + this.serializer = serializer;
  72 + this.connectionManager = connectionManager;
  73 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  74 + this.deviceService = SpringUtil.getBean(IotDeviceService.class);
  75 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  76 + }
  77 +
  78 + @Override
  79 + @SuppressWarnings("DuplicatedCode")
  80 + public void handle(NetSocket socket) {
  81 + String remoteAddress = String.valueOf(socket.remoteAddress());
  82 + log.debug("[handle][设备连接,地址: {}]", remoteAddress);
  83 +
  84 + // 1. 设置异常和关闭处理器
  85 + socket.exceptionHandler(ex -> {
  86 + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex);
  87 + socket.close();
  88 + });
  89 + socket.closeHandler(v -> {
  90 + log.debug("[handle][连接关闭,地址: {}]", remoteAddress);
  91 + cleanupConnection(socket);
  92 + });
  93 +
  94 + // 2.1 设置消息处理器
  95 + Handler<Buffer> messageHandler = buffer -> {
  96 + try {
  97 + processMessage(buffer, socket);
  98 + } catch (Exception e) {
  99 + log.error("[handle][消息处理失败,地址: {}]", remoteAddress, e);
  100 + socket.close();
  101 + }
  102 + };
  103 + // 2.2 使用拆包器处理粘包/拆包
  104 + RecordParser parser = codec.createDecodeParser(messageHandler);
  105 + socket.handler(parser);
  106 + log.debug("[handle][启用 {} 拆包器,地址: {}]", codec.getType(), remoteAddress);
  107 + }
  108 +
  109 + /**
  110 + * 处理消息
  111 + *
  112 + * @param buffer 消息
  113 + * @param socket 网络连接
  114 + */
  115 + private void processMessage(Buffer buffer, NetSocket socket) {
  116 + IotDeviceMessage message = null;
  117 + try {
  118 + // 1. 反序列化消息
  119 + message = serializer.deserialize(buffer.getBytes());
  120 + if (message == null) {
  121 + sendErrorResponse(socket, null, null, BAD_REQUEST.getCode(), "消息反序列化失败");
  122 + return;
  123 + }
  124 +
  125 + // 2. 根据消息类型路由处理
  126 + if (AUTH_METHOD.equals(message.getMethod())) {
  127 + // 认证请求
  128 + handleAuthenticationRequest(message, socket);
  129 + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
  130 + // 设备动态注册请求
  131 + handleRegisterRequest(message, socket);
  132 + } else {
  133 + // 业务消息
  134 + handleBusinessRequest(message, socket);
  135 + }
  136 + } catch (ServiceException e) {
  137 + // 业务异常,返回对应的错误码和错误信息
  138 + log.warn("[processMessage][业务异常,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage());
  139 + String requestId = message != null ? message.getRequestId() : null;
  140 + String method = message != null ? message.getMethod() : null;
  141 + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage());
  142 + } catch (IllegalArgumentException e) {
  143 + // 参数校验失败,返回 400
  144 + log.warn("[processMessage][参数校验失败,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage());
  145 + String requestId = message != null ? message.getRequestId() : null;
  146 + String method = message != null ? message.getMethod() : null;
  147 + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage());
  148 + } catch (Exception e) {
  149 + // 其他异常,返回 500,并重新抛出让上层关闭连接
  150 + log.error("[processMessage][处理消息失败,地址: {}]", socket.remoteAddress(), e);
  151 + String requestId = message != null ? message.getRequestId() : null;
  152 + String method = message != null ? message.getMethod() : null;
  153 + sendErrorResponse(socket, requestId, method,
  154 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  155 + throw e;
  156 + }
  157 + }
  158 +
  159 + /**
  160 + * 处理认证请求
  161 + *
  162 + * @param message 消息信息
  163 + * @param socket 网络连接
  164 + */
  165 + @SuppressWarnings("DuplicatedCode")
  166 + private void handleAuthenticationRequest(IotDeviceMessage message, NetSocket socket) {
  167 + // 1. 解析认证参数
  168 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
  169 + Assert.notNull(authParams, "认证参数不能为空");
  170 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  171 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
  172 +
  173 + // 2.1 执行认证
  174 + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
  175 + authResult.checkError();
  176 + if (BooleanUtil.isFalse(authResult.getData())) {
  177 + throw exception(DEVICE_AUTH_FAIL);
  178 + }
  179 + // 2.2 解析设备信息
  180 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
  181 + Assert.notNull(deviceInfo, "解析设备信息失败");
  182 + // 2.3 获取设备信息
  183 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  184 + Assert.notNull(device, "设备不存在");
  185 +
  186 + // 3.1 注册连接
  187 + registerConnection(socket, device);
  188 + // 3.2 发送上线消息
  189 + sendOnlineMessage(device);
  190 + // 3.3 发送成功响应
  191 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功");
  192 + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName());
  193 + }
  194 +
  195 + /**
  196 + * 处理设备动态注册请求(一型一密,不需要认证)
  197 + *
  198 + * @param message 消息信息
  199 + * @param socket 网络连接
  200 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  201 + */
  202 + @SuppressWarnings("DuplicatedCode")
  203 + private void handleRegisterRequest(IotDeviceMessage message, NetSocket socket) {
  204 + // 1. 解析注册参数
  205 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
  206 + Assert.notNull(params, "注册参数不能为空");
  207 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  208 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  209 + Assert.notBlank(params.getSign(), "sign 不能为空");
  210 +
  211 + // 2. 调用动态注册
  212 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  213 + result.checkError();
  214 +
  215 + // 3. 发送成功响应
  216 + sendSuccessResponse(socket, message.getRequestId(),
  217 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData());
  218 + log.info("[handleRegisterRequest][注册成功,地址: {},设备名: {}]",
  219 + socket.remoteAddress(), params.getDeviceName());
  220 + }
  221 +
  222 + /**
  223 + * 处理业务请求
  224 + *
  225 + * @param message 消息信息
  226 + * @param socket 网络连接
  227 + */
  228 + private void handleBusinessRequest(IotDeviceMessage message, NetSocket socket) {
  229 + // 1. 获取认证信息并处理业务消息
  230 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
  231 + if (connectionInfo == null) {
  232 + log.error("[handleBusinessRequest][无法获取连接信息,地址: {}]", socket.remoteAddress());
  233 + sendErrorResponse(socket, message.getRequestId(), message.getMethod(),
  234 + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息");
  235 + return;
  236 + }
  237 +
  238 + // 2. 发送消息到消息总线
  239 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(),
  240 + connectionInfo.getDeviceName(), serverId);
  241 + log.info("[handleBusinessRequest][发送消息到消息总线,地址: {},消息: {}]", socket.remoteAddress(), message);
  242 + }
  243 +
  244 + /**
  245 + * 注册连接信息
  246 + *
  247 + * @param socket 网络连接
  248 + * @param device 设备
  249 + */
  250 + private void registerConnection(NetSocket socket, IotDeviceRespDTO device) {
  251 + IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo()
  252 + .setDeviceId(device.getId())
  253 + .setProductKey(device.getProductKey())
  254 + .setDeviceName(device.getDeviceName());
  255 + connectionManager.registerConnection(socket, device.getId(), connectionInfo);
  256 + }
  257 +
  258 + /**
  259 + * 发送设备上线消息
  260 + *
  261 + * @param device 设备信息
  262 + */
  263 + private void sendOnlineMessage(IotDeviceRespDTO device) {
  264 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  265 + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
  266 + device.getDeviceName(), serverId);
  267 + }
  268 +
  269 + /**
  270 + * 清理连接
  271 + *
  272 + * @param socket 网络连接
  273 + */
  274 + private void cleanupConnection(NetSocket socket) {
  275 + // 1. 发送离线消息
  276 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
  277 + if (connectionInfo != null) {
  278 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  279 + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
  280 + connectionInfo.getDeviceName(), serverId);
  281 + }
  282 +
  283 + // 2. 注销连接
  284 + connectionManager.unregisterConnection(socket);
  285 + }
  286 +
  287 + // ===================== 发送响应消息 =====================
  288 +
  289 + /**
  290 + * 发送成功响应
  291 + *
  292 + * @param socket 网络连接
  293 + * @param requestId 请求 ID
  294 + * @param method 方法名
  295 + * @param data 响应数据
  296 + */
  297 + private void sendSuccessResponse(NetSocket socket, String requestId, String method, Object data) {
  298 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null);
  299 + writeResponse(socket, responseMessage);
  300 + }
  301 +
  302 + /**
  303 + * 发送错误响应
  304 + *
  305 + * @param socket 网络连接
  306 + * @param requestId 请求 ID
  307 + * @param method 方法名
  308 + * @param code 错误码
  309 + * @param msg 错误消息
  310 + */
  311 + private void sendErrorResponse(NetSocket socket, String requestId, String method, Integer code, String msg) {
  312 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg);
  313 + writeResponse(socket, responseMessage);
  314 + }
  315 +
  316 + /**
  317 + * 写入响应到 Socket
  318 + *
  319 + * @param socket 网络连接
  320 + * @param responseMessage 响应消息
  321 + */
  322 + private void writeResponse(NetSocket socket, IotDeviceMessage responseMessage) {
  323 + byte[] serializedData = serializer.serialize(responseMessage);
  324 + Buffer frameData = codec.encode(serializedData);
  325 + socket.write(frameData);
  326 + }
  327 +
  328 +}
0 329 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
... ... @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer;
4 4 import io.vertx.core.net.NetSocket;
5 5 import lombok.Data;
6 6 import lombok.extern.slf4j.Slf4j;
7   -import org.springframework.stereotype.Component;
8 7  
9 8 import java.util.Map;
10 9 import java.util.concurrent.ConcurrentHashMap;
... ... @@ -20,10 +19,14 @@ import java.util.concurrent.ConcurrentHashMap;
20 19 * @author 芋道源码
21 20 */
22 21 @Slf4j
23   -@Component
24 22 public class IotTcpConnectionManager {
25 23  
26 24 /**
  25 + * 最大连接数
  26 + */
  27 + private final int maxConnections;
  28 +
  29 + /**
27 30 * 连接信息映射:NetSocket -> 连接信息
28 31 */
29 32 private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
... ... @@ -33,6 +36,10 @@ public class IotTcpConnectionManager {
33 36 */
34 37 private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
35 38  
  39 + public IotTcpConnectionManager(int maxConnections) {
  40 + this.maxConnections = maxConnections;
  41 + }
  42 +
36 43 /**
37 44 * 注册设备连接(包含认证信息)
38 45 *
... ... @@ -40,15 +47,19 @@ public class IotTcpConnectionManager {
40 47 * @param deviceId 设备 ID
41 48 * @param connectionInfo 连接信息
42 49 */
43   - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
  50 + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
  51 + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性)
  52 + if (connectionMap.size() >= maxConnections) {
  53 + throw new IllegalStateException("连接数已达上限: " + maxConnections);
  54 + }
44 55 // 如果设备已有其他连接,先清理旧连接
45 56 NetSocket oldSocket = deviceSocketMap.get(deviceId);
46 57 if (oldSocket != null && oldSocket != socket) {
47 58 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
48 59 deviceId, oldSocket.remoteAddress());
49   - oldSocket.close();
50   - // 清理旧连接的映射
  60 + // 先清理映射,再关闭连接
51 61 connectionMap.remove(oldSocket);
  62 + oldSocket.close();
52 63 }
53 64  
54 65 // 注册新连接
... ... @@ -69,26 +80,12 @@ public class IotTcpConnectionManager {
69 80 return;
70 81 }
71 82 Long deviceId = connectionInfo.getDeviceId();
72   - deviceSocketMap.remove(deviceId);
  83 + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接
  84 + deviceSocketMap.remove(deviceId, socket);
73 85 log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress());
74 86 }
75 87  
76 88 /**
77   - * 检查连接是否已认证
78   - */
79   - public boolean isAuthenticated(NetSocket socket) {
80   - ConnectionInfo info = connectionMap.get(socket);
81   - return info != null;
82   - }
83   -
84   - /**
85   - * 检查连接是否未认证
86   - */
87   - public boolean isNotAuthenticated(NetSocket socket) {
88   - return !isAuthenticated(socket);
89   - }
90   -
91   - /**
92 89 * 获取连接信息
93 90 */
94 91 public ConnectionInfo getConnectionInfo(NetSocket socket) {
... ... @@ -144,15 +141,6 @@ public class IotTcpConnectionManager {
144 141 */
145 142 private String deviceName;
146 143  
147   - /**
148   - * 客户端 ID
149   - */
150   - private String clientId;
151   - /**
152   - * 消息编解码类型(认证后确定)
153   - */
154   - private String codecType;
155   -
156 144 }
157 145  
158 146 }
159 147 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.BooleanUtil;
5   -import cn.hutool.core.util.IdUtil;
6   -import cn.hutool.core.util.StrUtil;
7   -import cn.hutool.extra.spring.SpringUtil;
8   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
12   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
13   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
14   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
15   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
16   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
17   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
18   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
19   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
20   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
21   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
22   -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
23   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
24   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
25   -import io.vertx.core.Handler;
26   -import io.vertx.core.buffer.Buffer;
27   -import io.vertx.core.net.NetSocket;
28   -import lombok.extern.slf4j.Slf4j;
29   -
30   -import java.util.Map;
31   -
32   -/**
33   - * TCP 上行消息处理器
34   - *
35   - * @author 芋道源码
36   - */
37   -@Slf4j
38   -public class IotTcpUpstreamHandler implements Handler<NetSocket> {
39   -
40   - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
41   - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
42   -
43   - private static final String AUTH_METHOD = "auth";
44   -
45   - private final IotDeviceMessageService deviceMessageService;
46   -
47   - private final IotDeviceService deviceService;
48   -
49   - private final IotTcpConnectionManager connectionManager;
50   -
51   - private final IotDeviceCommonApi deviceApi;
52   -
53   - private final String serverId;
54   -
55   - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol,
56   - IotDeviceMessageService deviceMessageService,
57   - IotDeviceService deviceService,
58   - IotTcpConnectionManager connectionManager) {
59   - this.deviceMessageService = deviceMessageService;
60   - this.deviceService = deviceService;
61   - this.connectionManager = connectionManager;
62   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
63   - this.serverId = protocol.getServerId();
64   - }
65   -
66   - @Override
67   - public void handle(NetSocket socket) {
68   - String clientId = IdUtil.simpleUUID();
69   - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
70   -
71   - // 设置异常和关闭处理器
72   - socket.exceptionHandler(ex -> {
73   - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
74   - cleanupConnection(socket);
75   - });
76   - socket.closeHandler(v -> {
77   - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
78   - cleanupConnection(socket);
79   - });
80   -
81   - // 设置消息处理器
82   - socket.handler(buffer -> {
83   - // TODO @AI:TODO @芋艿:这里应该有拆粘包的问题;
84   - try {
85   - processMessage(clientId, buffer, socket);
86   - } catch (Exception e) {
87   - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
88   - clientId, socket.remoteAddress(), e.getMessage());
89   - cleanupConnection(socket);
90   - socket.close();
91   - }
92   - });
93   - }
94   -
95   - /**
96   - * 处理消息
97   - *
98   - * @param clientId 客户端 ID
99   - * @param buffer 消息
100   - * @param socket 网络连接
101   - * @throws Exception 消息解码失败时抛出异常
102   - */
103   - private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception {
104   - // 1. 基础检查
105   - if (buffer == null || buffer.length() == 0) {
106   - return;
107   - }
108   -
109   - // 2. 获取消息格式类型
110   - String codecType = getMessageCodecType(buffer, socket);
111   -
112   - // 3. 解码消息
113   - IotDeviceMessage message;
114   - try {
115   - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
116   - if (message == null) {
117   - throw new Exception("解码后消息为空");
118   - }
119   - } catch (Exception e) {
120   - // 消息格式错误时抛出异常,由上层处理连接断开
121   - throw new Exception("消息解码失败: " + e.getMessage(), e);
122   - }
123   -
124   - // 4. 根据消息类型路由处理
125   - try {
126   - if (AUTH_METHOD.equals(message.getMethod())) {
127   - // 认证请求
128   - handleAuthenticationRequest(clientId, message, codecType, socket);
129   - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
130   - // 设备动态注册请求
131   - handleRegisterRequest(clientId, message, codecType, socket);
132   - } else {
133   - // 业务消息
134   - handleBusinessRequest(clientId, message, codecType, socket);
135   - }
136   - } catch (Exception e) {
137   - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]",
138   - clientId, message.getMethod(), e);
139   - // 发送错误响应,避免客户端一直等待
140   - try {
141   - sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType);
142   - } catch (Exception responseEx) {
143   - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx);
144   - }
145   - }
146   - }
147   -
148   - /**
149   - * 处理认证请求
150   - *
151   - * @param clientId 客户端 ID
152   - * @param message 消息信息
153   - * @param codecType 消息编解码类型
154   - * @param socket 网络连接
155   - */
156   - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType,
157   - NetSocket socket) {
158   - try {
159   - // 1.1 解析认证参数
160   - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
161   - if (authParams == null) {
162   - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId);
163   - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType);
164   - return;
165   - }
166   - // 1.2 执行认证
167   - if (!validateDeviceAuth(authParams)) {
168   - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
169   - clientId, authParams.getUsername());
170   - sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType);
171   - return;
172   - }
173   -
174   - // 2.1 解析设备信息
175   - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
176   - if (deviceInfo == null) {
177   - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
178   - return;
179   - }
180   - // 2.2 获取设备信息
181   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
182   - deviceInfo.getDeviceName());
183   - if (device == null) {
184   - sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType);
185   - return;
186   - }
187   -
188   - // 3.1 注册连接
189   - registerConnection(socket, device, clientId, codecType);
190   - // 3.2 发送上线消息
191   - sendOnlineMessage(device);
192   - // 3.3 发送成功响应
193   - sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType);
194   - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
195   - device.getId(), device.getDeviceName());
196   - } catch (Exception e) {
197   - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
198   - sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType);
199   - }
200   - }
201   -
202   - /**
203   - * 处理设备动态注册请求(一型一密,不需要认证)
204   - *
205   - * @param clientId 客户端 ID
206   - * @param message 消息信息
207   - * @param codecType 消息编解码类型
208   - * @param socket 网络连接
209   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
210   - */
211   - private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType,
212   - NetSocket socket) {
213   - try {
214   - // 1. 解析注册参数
215   - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
216   - if (params == null) {
217   - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
218   - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType);
219   - return;
220   - }
221   -
222   - // 2. 调用动态注册
223   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
224   - if (result.isError()) {
225   - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
226   - sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType);
227   - return;
228   - }
229   -
230   - // 3. 发送成功响应(包含 deviceSecret)
231   - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType);
232   - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]",
233   - clientId, params.getDeviceName());
234   - } catch (Exception e) {
235   - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
236   - sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType);
237   - }
238   - }
239   -
240   - /**
241   - * 处理业务请求
242   - *
243   - * @param clientId 客户端 ID
244   - * @param message 消息信息
245   - * @param codecType 消息编解码类型
246   - * @param socket 网络连接
247   - */
248   - private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) {
249   - try {
250   - // 1. 检查认证状态
251   - if (connectionManager.isNotAuthenticated(socket)) {
252   - log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId);
253   - sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType);
254   - return;
255   - }
256   -
257   - // 2. 获取认证信息并处理业务消息
258   - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
259   -
260   - // 3. 发送消息到消息总线
261   - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(),
262   - connectionInfo.getDeviceName(), serverId);
263   - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}",
264   - clientId, message.toString());
265   - } catch (Exception e) {
266   - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e);
267   - }
268   - }
269   -
270   - /**
271   - * 获取消息编解码类型
272   - *
273   - * @param buffer 消息
274   - * @param socket 网络连接
275   - * @return 消息编解码类型
276   - */
277   - private String getMessageCodecType(Buffer buffer, NetSocket socket) {
278   - // 1. 如果已认证,优先使用缓存的编解码类型
279   - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
280   - if (connectionInfo != null
281   - && StrUtil.isNotBlank(connectionInfo.getCodecType())) {
282   - return connectionInfo.getCodecType();
283   - }
284   -
285   - // 2. 未认证时检测消息格式类型
286   - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY
287   - : CODEC_TYPE_JSON;
288   - }
289   -
290   - /**
291   - * 注册连接信息
292   - *
293   - * @param socket 网络连接
294   - * @param device 设备
295   - * @param clientId 客户端 ID
296   - * @param codecType 消息编解码类型
297   - */
298   - private void registerConnection(NetSocket socket, IotDeviceRespDTO device,
299   - String clientId, String codecType) {
300   - IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo()
301   - .setDeviceId(device.getId())
302   - .setProductKey(device.getProductKey())
303   - .setDeviceName(device.getDeviceName())
304   - .setClientId(clientId)
305   - .setCodecType(codecType);
306   - // 注册连接
307   - connectionManager.registerConnection(socket, device.getId(), connectionInfo);
308   - }
309   -
310   - /**
311   - * 发送设备上线消息
312   - *
313   - * @param device 设备信息
314   - */
315   - private void sendOnlineMessage(IotDeviceRespDTO device) {
316   - try {
317   - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
318   - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
319   - device.getDeviceName(), serverId);
320   - } catch (Exception e) {
321   - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
322   - }
323   - }
324   -
325   - /**
326   - * 清理连接
327   - *
328   - * @param socket 网络连接
329   - */
330   - private void cleanupConnection(NetSocket socket) {
331   - try {
332   - // 1. 发送离线消息(如果已认证)
333   - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
334   - if (connectionInfo != null) {
335   - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
336   - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
337   - connectionInfo.getDeviceName(), serverId);
338   - }
339   -
340   - // 2. 注销连接
341   - connectionManager.unregisterConnection(socket);
342   - } catch (Exception e) {
343   - log.error("[cleanupConnection][清理连接失败]", e);
344   - }
345   - }
346   -
347   - /**
348   - * 发送响应消息
349   - *
350   - * @param socket 网络连接
351   - * @param success 是否成功
352   - * @param message 消息
353   - * @param requestId 请求 ID
354   - * @param codecType 消息编解码类型
355   - */
356   - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) {
357   - try {
358   - Object responseData = MapUtil.builder()
359   - .put("success", success)
360   - .put("message", message)
361   - .build();
362   -
363   - int code = success ? 0 : 401;
364   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData,
365   - code, message);
366   -
367   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
368   - socket.write(Buffer.buffer(encodedData));
369   -
370   - } catch (Exception e) {
371   - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e);
372   - }
373   - }
374   -
375   - /**
376   - * 验证设备认证信息
377   - *
378   - * @param authParams 认证参数
379   - * @return 是否认证成功
380   - */
381   - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
382   - try {
383   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
384   - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
385   - .setPassword(authParams.getPassword()));
386   - result.checkError();
387   - return BooleanUtil.isTrue(result.getData());
388   - } catch (Exception e) {
389   - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e);
390   - return false;
391   - }
392   - }
393   -
394   - /**
395   - * 发送错误响应
396   - *
397   - * @param socket 网络连接
398   - * @param requestId 请求 ID
399   - * @param errorMessage 错误消息
400   - * @param codecType 消息编解码类型
401   - */
402   - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) {
403   - sendResponse(socket, false, errorMessage, requestId, codecType);
404   - }
405   -
406   - /**
407   - * 发送成功响应
408   - *
409   - * @param socket 网络连接
410   - * @param requestId 请求 ID
411   - * @param message 消息
412   - * @param codecType 消息编解码类型
413   - */
414   - @SuppressWarnings("SameParameterValue")
415   - private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) {
416   - sendResponse(socket, true, message, requestId, codecType);
417   - }
418   -
419   - /**
420   - * 解析认证参数
421   - *
422   - * @param params 参数对象(通常为 Map 类型)
423   - * @return 认证参数 DTO,解析失败时返回 null
424   - */
425   - @SuppressWarnings({"unchecked", "DuplicatedCode"})
426   - private IotDeviceAuthReqDTO parseAuthParams(Object params) {
427   - if (params == null) {
428   - return null;
429   - }
430   - try {
431   - // 参数默认为 Map 类型,直接转换
432   - if (params instanceof Map) {
433   - Map<String, Object> paramMap = (Map<String, Object>) params;
434   - return new IotDeviceAuthReqDTO()
435   - .setClientId(MapUtil.getStr(paramMap, "clientId"))
436   - .setUsername(MapUtil.getStr(paramMap, "username"))
437   - .setPassword(MapUtil.getStr(paramMap, "password"));
438   - }
439   - // 如果已经是目标类型,直接返回
440   - if (params instanceof IotDeviceAuthReqDTO) {
441   - return (IotDeviceAuthReqDTO) params;
442   - }
443   -
444   - // 其他情况尝试 JSON 转换
445   - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
446   - } catch (Exception e) {
447   - log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
448   - return null;
449   - }
450   - }
451   -
452   - /**
453   - * 解析注册参数
454   - *
455   - * @param params 参数对象(通常为 Map 类型)
456   - * @return 注册参数 DTO,解析失败时返回 null
457   - */
458   - @SuppressWarnings({"unchecked", "DuplicatedCode"})
459   - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
460   - if (params == null) {
461   - return null;
462   - }
463   - try {
464   - // 参数默认为 Map 类型,直接转换
465   - if (params instanceof Map) {
466   - Map<String, Object> paramMap = (Map<String, Object>) params;
467   - return new IotDeviceRegisterReqDTO()
468   - .setProductKey(MapUtil.getStr(paramMap, "productKey"))
469   - .setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
470   - .setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
471   - }
472   - // 如果已经是目标类型,直接返回
473   - if (params instanceof IotDeviceRegisterReqDTO) {
474   - return (IotDeviceRegisterReqDTO) params;
475   - }
476   -
477   - // 其他情况尝试 JSON 转换
478   - String jsonStr = JsonUtils.toJsonString(params);
479   - return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class);
480   - } catch (Exception e) {
481   - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
482   - return null;
483   - }
484   - }
485   -
486   - /**
487   - * 发送注册成功响应(包含 deviceSecret)
488   - *
489   - * @param socket 网络连接
490   - * @param requestId 请求 ID
491   - * @param registerResp 注册响应
492   - * @param codecType 消息编解码类型
493   - */
494   - private void sendRegisterSuccessResponse(NetSocket socket, String requestId,
495   - IotDeviceRegisterRespDTO registerResp, String codecType) {
496   - try {
497   - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
498   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
499   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
500   - // 2. 发送响应
501   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
502   - socket.write(Buffer.buffer(encodedData));
503   - } catch (Exception e) {
504   - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e);
505   - }
506   - }
507   -
508   -}
509 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT UDP 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotUdpConfig {
  14 +
  15 + /**
  16 + * 最大会话数
  17 + */
  18 + @NotNull(message = "最大会话数不能为空")
  19 + @Min(value = 1, message = "最大会话数必须大于 0")
  20 + private Integer maxSessions = 1000;
  21 + /**
  22 + * 会话超时时间(毫秒)
  23 + * <p>
  24 + * 基于 Guava Cache 的 expireAfterAccess 实现自动过期清理
  25 + */
  26 + @NotNull(message = "会话超时时间不能为空")
  27 + @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒")
  28 + private Long sessionTimeoutMs = 60000L;
  29 +
  30 + /**
  31 + * 接收缓冲区大小(字节)
  32 + */
  33 + @NotNull(message = "接收缓冲区大小不能为空")
  34 + @Min(value = 1024, message = "接收缓冲区大小必须大于 1024 字节")
  35 + private Integer receiveBufferSize = 65536;
  36 + /**
  37 + * 发送缓冲区大小(字节)
  38 + */
  39 + @NotNull(message = "发送缓冲区大小不能为空")
  40 + @Min(value = 1024, message = "发送缓冲区大小必须大于 1024 字节")
  41 + private Integer sendBufferSize = 65536;
  42 +
  43 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
2   -
3   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler;
9   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10   -import jakarta.annotation.PostConstruct;
11   -import lombok.RequiredArgsConstructor;
12   -import lombok.extern.slf4j.Slf4j;
13   -
14   -/**
15   - * IoT 网关 UDP 下游订阅者:接收下行给设备的消息
16   - *
17   - * @author 芋道源码
18   - */
19   -@Slf4j
20   -@RequiredArgsConstructor
21   -public class IotUdpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
22   -
23   - private final IotUdpUpstreamProtocol protocol;
24   -
25   - private final IotDeviceMessageService messageService;
26   -
27   - private final IotUdpSessionManager sessionManager;
28   -
29   - private final IotMessageBus messageBus;
30   -
31   - private IotUdpDownstreamHandler downstreamHandler;
32   -
33   - @PostConstruct
34   - public void init() {
35   - // 初始化下游处理器
36   - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol);
37   - // 注册下游订阅者
38   - messageBus.register(this);
39   - log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
40   - protocol.getServerId(), getTopic());
41   - }
42   -
43   - @Override
44   - public String getTopic() {
45   - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
46   - }
47   -
48   - @Override
49   - public String getGroup() {
50   - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
51   - return getTopic();
52   - }
53   -
54   - @Override
55   - public void onMessage(IotDeviceMessage message) {
56   - try {
57   - downstreamHandler.handle(message);
58   - } catch (Exception e) {
59   - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
60   - message.getDeviceId(), message.getMethod(), message.getId(), e);
61   - }
62   - }
63   -
64   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpUpstreamHandler;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  14 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
  15 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  16 +import io.vertx.core.Vertx;
  17 +import io.vertx.core.datagram.DatagramSocket;
  18 +import io.vertx.core.datagram.DatagramSocketOptions;
  19 +import lombok.Getter;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import cn.hutool.core.lang.Assert;
  22 +
  23 +/**
  24 + * IoT UDP 协议实现
  25 + * <p>
  26 + * 基于 Vert.x 实现 UDP 服务器,接收设备上行消息
  27 + *
  28 + * @author 芋道源码
  29 + */
  30 +@Slf4j
  31 +public class IotUdpProtocol implements IotProtocol {
  32 +
  33 + /**
  34 + * 协议配置
  35 + */
  36 + private final ProtocolProperties properties;
  37 + /**
  38 + * 服务器 ID(用于消息追踪,全局唯一)
  39 + */
  40 + @Getter
  41 + private final String serverId;
  42 +
  43 + /**
  44 + * 运行状态
  45 + */
  46 + @Getter
  47 + private volatile boolean running = false;
  48 +
  49 + /**
  50 + * Vert.x 实例
  51 + */
  52 + private Vertx vertx;
  53 + /**
  54 + * UDP 服务器
  55 + */
  56 + @Getter
  57 + private DatagramSocket udpSocket;
  58 + /**
  59 + * UDP 会话管理器
  60 + */
  61 + private final IotUdpSessionManager sessionManager;
  62 +
  63 + /**
  64 + * 下行消息订阅者
  65 + */
  66 + private final IotUdpDownstreamSubscriber downstreamSubscriber;
  67 +
  68 + /**
  69 + * 消息序列化器
  70 + */
  71 + private final IotMessageSerializer serializer;
  72 +
  73 + public IotUdpProtocol(ProtocolProperties properties) {
  74 + IotUdpConfig udpConfig = properties.getUdp();
  75 + Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空");
  76 + this.properties = properties;
  77 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  78 +
  79 + // 初始化序列化器
  80 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize());
  81 + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize());
  82 + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class);
  83 + this.serializer = serializerManager.get(serializeType);
  84 +
  85 + // 初始化会话管理器
  86 + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs());
  87 +
  88 + // 初始化下行消息订阅者
  89 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  90 + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer);
  91 + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus);
  92 + }
  93 +
  94 + @Override
  95 + public String getId() {
  96 + return properties.getId();
  97 + }
  98 +
  99 + @Override
  100 + public IotProtocolTypeEnum getType() {
  101 + return IotProtocolTypeEnum.UDP;
  102 + }
  103 +
  104 + @Override
  105 + public void start() {
  106 + if (running) {
  107 + log.warn("[start][IoT UDP 协议 {} 已经在运行中]", getId());
  108 + return;
  109 + }
  110 +
  111 + // 1.1 创建 Vertx 实例
  112 + this.vertx = Vertx.vertx();
  113 +
  114 + // 1.2 创建 UDP Socket 选项
  115 + IotUdpConfig udpConfig = properties.getUdp();
  116 + DatagramSocketOptions options = new DatagramSocketOptions()
  117 + .setReceiveBufferSize(udpConfig.getReceiveBufferSize())
  118 + .setSendBufferSize(udpConfig.getSendBufferSize())
  119 + .setReuseAddress(true);
  120 +
  121 + // 1.3 创建 UDP Socket
  122 + udpSocket = vertx.createDatagramSocket(options);
  123 +
  124 + // 1.4 创建上行消息处理器
  125 + IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer);
  126 +
  127 + // 1.5 启动 UDP 服务器(阻塞式)
  128 + try {
  129 + udpSocket.listen(properties.getPort(), "0.0.0.0").result();
  130 + // 设置数据包处理器
  131 + udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket));
  132 + running = true;
  133 + log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]",
  134 + getId(), properties.getPort(), serverId);
  135 +
  136 + // 2. 启动下行消息订阅者
  137 + this.downstreamSubscriber.start();
  138 + } catch (Exception e) {
  139 + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e);
  140 + // 启动失败时关闭资源
  141 + if (udpSocket != null) {
  142 + udpSocket.close();
  143 + udpSocket = null;
  144 + }
  145 + if (vertx != null) {
  146 + vertx.close();
  147 + vertx = null;
  148 + }
  149 + throw e;
  150 + }
  151 + }
  152 +
  153 + @Override
  154 + public void stop() {
  155 + if (!running) {
  156 + return;
  157 + }
  158 + // 1. 停止下行消息订阅者
  159 + try {
  160 + downstreamSubscriber.stop();
  161 + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId());
  162 + } catch (Exception e) {
  163 + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  164 + }
  165 +
  166 + // 2.1 关闭 UDP Socket
  167 + if (udpSocket != null) {
  168 + try {
  169 + udpSocket.close().result();
  170 + log.info("[stop][IoT UDP 协议 {} 服务器已停止]", getId());
  171 + } catch (Exception e) {
  172 + log.error("[stop][IoT UDP 协议 {} 服务器停止失败]", getId(), e);
  173 + }
  174 + udpSocket = null;
  175 + }
  176 + // 2.2 关闭 Vertx 实例
  177 + if (vertx != null) {
  178 + try {
  179 + vertx.close().result();
  180 + log.info("[stop][IoT UDP 协议 {} Vertx 已关闭]", getId());
  181 + } catch (Exception e) {
  182 + log.error("[stop][IoT UDP 协议 {} Vertx 关闭失败]", getId(), e);
  183 + }
  184 + vertx = null;
  185 + }
  186 + running = false;
  187 + log.info("[stop][IoT UDP 协议 {} 已停止]", getId());
  188 + }
  189 +
  190 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
2   -
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
5   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler;
10   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
11   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
12   -import io.vertx.core.Vertx;
13   -import io.vertx.core.datagram.DatagramSocket;
14   -import io.vertx.core.datagram.DatagramSocketOptions;
15   -import jakarta.annotation.PostConstruct;
16   -import jakarta.annotation.PreDestroy;
17   -import lombok.Getter;
18   -import lombok.extern.slf4j.Slf4j;
19   -
20   -import java.util.List;
21   -
22   -/**
23   - * IoT 网关 UDP 协议:接收设备上行消息
24   - * <p>
25   - * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能:
26   - * 1. 监听 UDP 端口,接收设备消息
27   - * 2. 定期清理不活跃的设备地址映射
28   - * 3. 提供 UDP Socket 用于下行消息发送
29   - *
30   - * @author 芋道源码
31   - */
32   -@Slf4j
33   -public class IotUdpUpstreamProtocol {
34   -
35   - private final IotGatewayProperties.UdpProperties udpProperties;
36   -
37   - private final IotDeviceService deviceService;
38   -
39   - private final IotDeviceMessageService messageService;
40   -
41   - private final IotUdpSessionManager sessionManager;
42   -
43   - private final Vertx vertx;
44   -
45   - @Getter
46   - private final String serverId;
47   -
48   - @Getter
49   - private DatagramSocket udpSocket;
50   -
51   - /**
52   - * 会话清理定时器 ID
53   - */
54   - private Long cleanTimerId;
55   -
56   - private IotUdpUpstreamHandler upstreamHandler;
57   -
58   - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties,
59   - IotDeviceService deviceService,
60   - IotDeviceMessageService messageService,
61   - IotUdpSessionManager sessionManager,
62   - Vertx vertx) {
63   - this.udpProperties = udpProperties;
64   - this.deviceService = deviceService;
65   - this.messageService = messageService;
66   - this.sessionManager = sessionManager;
67   - this.vertx = vertx;
68   - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort());
69   - }
70   -
71   - @PostConstruct
72   - public void start() {
73   - // 1. 初始化上行消息处理器
74   - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager);
75   -
76   - // 2. 创建 UDP Socket 选项
77   - DatagramSocketOptions options = new DatagramSocketOptions()
78   - .setReceiveBufferSize(udpProperties.getReceiveBufferSize())
79   - .setSendBufferSize(udpProperties.getSendBufferSize())
80   - .setReuseAddress(true);
81   -
82   - // 3. 创建 UDP Socket
83   - udpSocket = vertx.createDatagramSocket(options);
84   -
85   - // 4. 监听端口
86   - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> {
87   - if (result.failed()) {
88   - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause());
89   - return;
90   - }
91   - // 设置数据包处理器
92   - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket));
93   - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]",
94   - udpProperties.getPort(), udpProperties.getReceiveBufferSize(),
95   - udpProperties.getSendBufferSize());
96   -
97   - // 5. 启动会话清理定时器
98   - startSessionCleanTimer();
99   - });
100   - }
101   -
102   - @PreDestroy
103   - public void stop() {
104   - // 1. 取消会话清理定时器
105   - if (cleanTimerId != null) {
106   - vertx.cancelTimer(cleanTimerId);
107   - cleanTimerId = null;
108   - log.info("[stop][会话清理定时器已取消]");
109   - }
110   -
111   - // 2. 关闭 UDP Socket
112   - if (udpSocket != null) {
113   - try {
114   - udpSocket.close().result();
115   - log.info("[stop][IoT 网关 UDP 协议已停止]");
116   - } catch (Exception e) {
117   - log.error("[stop][IoT 网关 UDP 协议停止失败]", e);
118   - }
119   - }
120   - }
121   -
122   - /**
123   - * 启动会话清理定时器
124   - */
125   - private void startSessionCleanTimer() {
126   - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> {
127   - try {
128   - // 1. 清理超时的设备地址映射,并获取离线设备列表
129   - List<Long> offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs());
130   -
131   - // 2. 为每个离线设备发送离线消息
132   - for (Long deviceId : offlineDeviceIds) {
133   - sendOfflineMessage(deviceId);
134   - }
135   - if (CollUtil.isNotEmpty(offlineDeviceIds)) {
136   - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size());
137   - }
138   - } catch (Exception e) {
139   - log.error("[cleanExpiredMappings][清理超时会话失败]", e);
140   - }
141   - });
142   - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]",
143   - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs());
144   - }
145   -
146   - /**
147   - * 发送设备离线消息
148   - *
149   - * @param deviceId 设备 ID
150   - */
151   - private void sendOfflineMessage(Long deviceId) {
152   - try {
153   - // 获取设备信息
154   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId);
155   - if (device == null) {
156   - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId);
157   - return;
158   - }
159   -
160   - // 发送离线消息
161   - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
162   - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(),
163   - device.getDeviceName(), serverId);
164   - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]",
165   - deviceId, device.getDeviceName());
166   - } catch (Exception e) {
167   - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e);
168   - }
169   - }
170   -
171   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
6   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  6 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
7 7 import io.vertx.core.datagram.DatagramSocket;
  8 +import lombok.RequiredArgsConstructor;
8 9 import lombok.extern.slf4j.Slf4j;
9 10  
10 11 /**
... ... @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j;
13 14 * @author 芋道源码
14 15 */
15 16 @Slf4j
  17 +@RequiredArgsConstructor
16 18 public class IotUdpDownstreamHandler {
17 19  
18   - private final IotDeviceMessageService deviceMessageService;
  20 + private final IotUdpProtocol protocol;
19 21  
20 22 private final IotUdpSessionManager sessionManager;
21 23  
22   - private final IotUdpUpstreamProtocol protocol;
23   -
24   - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService,
25   - IotUdpSessionManager sessionManager,
26   - IotUdpUpstreamProtocol protocol) {
27   - this.deviceMessageService = deviceMessageService;
28   - this.sessionManager = sessionManager;
29   - this.protocol = protocol;
30   - }
  24 + /**
  25 + * 消息序列化器(处理业务消息序列化/反序列化)
  26 + */
  27 + private final IotMessageSerializer serializer;
31 28  
32 29 /**
33 30 * 处理下行消息
34   - *
35   - * @param message 下行消息
36 31 */
37 32 public void handle(IotDeviceMessage message) {
38 33 try {
39 34 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
40 35 message.getDeviceId(), message.getMethod(), message.getId());
41   -
42   - // 1. 获取会话信息(包含 codecType)
43   - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId());
  36 + // 1. 检查设备会话
  37 + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSession(message.getDeviceId());
44 38 if (sessionInfo == null) {
45   - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
  39 + log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
  40 + message.getDeviceId(), message.getMethod(), message.getId());
46 41 return;
47 42 }
48   -
49   - // 2. 使用会话中的 codecType 编码消息,并发送到设备
50   - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType());
51 43 DatagramSocket socket = protocol.getUdpSocket();
52 44 if (socket == null) {
53 45 log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId());
54 46 return;
55 47 }
56   - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket);
57   - if (success) {
58   - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
59   - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
60   - } else {
61   - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
62   - message.getDeviceId(), message.getMethod(), message.getId());
  48 +
  49 + // 2. 序列化消息
  50 + byte[] serializedData = serializer.serialize(message);
  51 +
  52 + // 3. 发送到设备
  53 + boolean success = sessionManager.sendToDevice(message.getDeviceId(), serializedData, socket);
  54 + if (!success) {
  55 + throw new RuntimeException("下行消息发送失败");
63 56 }
  57 + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
  58 + message.getDeviceId(), message.getMethod(), message.getId(), serializedData.length);
64 59 } catch (Exception e) {
65 60 log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
66 61 message.getDeviceId(), message.getMethod(), message, e);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 网关 UDP 下游订阅者:接收下行给设备的消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +
  17 + private final IotUdpDownstreamHandler downstreamHandler;
  18 +
  19 + public IotUdpDownstreamSubscriber(IotProtocol protocol,
  20 + IotUdpDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
  24 + }
  25 +
  26 + @Override
  27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
  29 + }
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.ArrayUtil;
  5 +import cn.hutool.core.util.BooleanUtil;
  6 +import cn.hutool.core.util.IdUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import cn.hutool.extra.spring.SpringUtil;
  9 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  10 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  11 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  12 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  13 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  15 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  16 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  17 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  18 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  19 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  20 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  21 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
  22 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  23 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  24 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  25 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  26 +import io.vertx.core.buffer.Buffer;
  27 +import io.vertx.core.datagram.DatagramPacket;
  28 +import io.vertx.core.datagram.DatagramSocket;
  29 +import lombok.extern.slf4j.Slf4j;
  30 +import cn.hutool.core.lang.Assert;
  31 +
  32 +import java.net.InetSocketAddress;
  33 +import java.util.Map;
  34 +
  35 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  36 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  37 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
  38 +
  39 +/**
  40 + * UDP 上行消息处理器
  41 + * <p>
  42 + * 采用无状态 Token 机制(每次请求携带 token):
  43 + * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password
  44 + * 2. 返回 Token:服务端验证后返回 JWT token
  45 + * 3. 后续请求:每次请求在 params 中携带 token
  46 + * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证
  47 + *
  48 + * @author 芋道源码
  49 + */
  50 +@Slf4j
  51 +public class IotUdpUpstreamHandler {
  52 +
  53 + private static final String AUTH_METHOD = "auth";
  54 +
  55 + /**
  56 + * Token 参数 Key
  57 + */
  58 + private static final String PARAM_KEY_TOKEN = "token";
  59 + /**
  60 + * Body 参数 Key(实际请求内容)
  61 + */
  62 + private static final String PARAM_KEY_BODY = "body";
  63 +
  64 + private final String serverId;
  65 +
  66 + /**
  67 + * 消息序列化器(处理业务消息序列化/反序列化)
  68 + */
  69 + private final IotMessageSerializer serializer;
  70 + /**
  71 + * UDP 会话管理器
  72 + */
  73 + private final IotUdpSessionManager sessionManager;
  74 +
  75 + private final IotDeviceMessageService deviceMessageService;
  76 + private final IotDeviceService deviceService;
  77 + private final IotDeviceTokenService deviceTokenService;
  78 + private final IotDeviceCommonApi deviceApi;
  79 +
  80 + public IotUdpUpstreamHandler(String serverId,
  81 + IotUdpSessionManager sessionManager,
  82 + IotMessageSerializer serializer) {
  83 + Assert.notNull(serializer, "消息序列化器必须配置");
  84 + Assert.notNull(sessionManager, "会话管理器不能为空");
  85 + this.serverId = serverId;
  86 + this.sessionManager = sessionManager;
  87 + this.serializer = serializer;
  88 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  89 + this.deviceService = SpringUtil.getBean(IotDeviceService.class);
  90 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  91 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  92 + }
  93 +
  94 + // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现
  95 + /**
  96 + * 处理 UDP 数据包
  97 + *
  98 + * @param packet 数据包
  99 + * @param socket UDP Socket
  100 + */
  101 + public void handle(DatagramPacket packet, DatagramSocket socket) {
  102 + InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port());
  103 + Buffer data = packet.data();
  104 + String addressKey = sessionManager.buildAddressKey(senderAddress);
  105 + log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length());
  106 + processMessage(data, senderAddress, socket);
  107 + }
  108 +
  109 + /**
  110 + * 处理消息
  111 + *
  112 + * @param buffer 消息
  113 + * @param senderAddress 发送者地址
  114 + * @param socket UDP Socket
  115 + */
  116 + private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) {
  117 + String addressKey = sessionManager.buildAddressKey(senderAddress);
  118 + // 1.1 基础检查
  119 + if (ArrayUtil.isEmpty(buffer)) {
  120 + return;
  121 + }
  122 + // 1.2 反序列化消息
  123 + IotDeviceMessage message = serializer.deserialize(buffer.getBytes());
  124 + if (message == null) {
  125 + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), "消息反序列化失败");
  126 + return;
  127 + }
  128 +
  129 + // 2. 根据消息类型路由处理
  130 + try {
  131 + if (AUTH_METHOD.equals(message.getMethod())) {
  132 + // 认证请求
  133 + handleAuthenticationRequest(message, senderAddress, socket);
  134 + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
  135 + // 设备动态注册请求
  136 + handleRegisterRequest(message, senderAddress, socket);
  137 + } else {
  138 + // 业务消息
  139 + handleBusinessRequest(message, senderAddress, socket);
  140 + }
  141 + } catch (ServiceException e) {
  142 + // 业务异常,返回对应的错误码和错误信息
  143 + log.warn("[processMessage][业务异常,来源: {},requestId: {},method: {},错误: {}]",
  144 + addressKey, message.getRequestId(), message.getMethod(), e.getMessage());
  145 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  146 + e.getCode(), e.getMessage());
  147 + } catch (IllegalArgumentException e) {
  148 + // 参数校验失败,返回 400
  149 + log.warn("[processMessage][参数校验失败,来源: {},requestId: {},method: {},错误: {}]",
  150 + addressKey, message.getRequestId(), message.getMethod(), e.getMessage());
  151 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  152 + BAD_REQUEST.getCode(), e.getMessage());
  153 + } catch (Exception e) {
  154 + // 其他异常,返回 500
  155 + log.error("[processMessage][处理消息失败,来源: {},requestId: {},method: {}]",
  156 + addressKey, message.getRequestId(), message.getMethod(), e);
  157 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  158 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  159 + }
  160 + }
  161 +
  162 + /**
  163 + * 处理认证请求
  164 + *
  165 + * @param message 消息信息
  166 + * @param senderAddress 发送者地址
  167 + * @param socket UDP Socket
  168 + */
  169 + @SuppressWarnings("DuplicatedCode")
  170 + private void handleAuthenticationRequest(IotDeviceMessage message, InetSocketAddress senderAddress,
  171 + DatagramSocket socket) {
  172 + String clientId = IdUtil.simpleUUID();
  173 + // 1. 解析认证参数
  174 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
  175 + Assert.notNull(authParams, "认证参数不能为空");
  176 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  177 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
  178 +
  179 + // 2.1 执行认证
  180 + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
  181 + authResult.checkError();
  182 + if (!BooleanUtil.isTrue(authResult.getData())) {
  183 + throw exception(DEVICE_AUTH_FAIL);
  184 + }
  185 + // 2.2 解析设备信息
  186 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
  187 + Assert.notNull(deviceInfo, "解析设备信息失败");
  188 + // 2.3 获取设备信息
  189 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
  190 + deviceInfo.getDeviceName());
  191 + Assert.notNull(device, "设备不存在");
  192 +
  193 + // 3. 生成 JWT Token(无状态)
  194 + String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName());
  195 +
  196 + // 4.1 注册会话
  197 + registerSession(senderAddress, device, clientId);
  198 + // 4.2 发送上线消息
  199 + sendOnlineMessage(device);
  200 + // 4.3 发送成功响应(包含 token)
  201 + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD,
  202 + MapUtil.of("token", token));
  203 + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]",
  204 + device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress));
  205 + }
  206 +
  207 + /**
  208 + * 处理设备动态注册请求(一型一密,不需要认证)
  209 + *
  210 + * @param message 消息信息
  211 + * @param senderAddress 发送者地址
  212 + * @param socket UDP Socket
  213 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  214 + */
  215 + @SuppressWarnings("DuplicatedCode")
  216 + private void handleRegisterRequest(IotDeviceMessage message, InetSocketAddress senderAddress,
  217 + DatagramSocket socket) {
  218 + // 1. 解析注册参数
  219 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
  220 + Assert.notNull(params, "注册参数不能为空");
  221 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  222 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  223 + Assert.notBlank(params.getSign(), "sign 不能为空");
  224 +
  225 + // 2. 调用动态注册
  226 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  227 + result.checkError();
  228 +
  229 + // 3. 发送成功响应
  230 + sendSuccessResponse(socket, senderAddress, message.getRequestId(),
  231 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData());
  232 + log.info("[handleRegisterRequest][注册成功,来源: {},设备名: {}]",
  233 + sessionManager.buildAddressKey(senderAddress), params.getDeviceName());
  234 + }
  235 +
  236 + /**
  237 + * 处理业务请求
  238 + * <p>
  239 + * 请求参数格式:
  240 + * - token:JWT 令牌
  241 + * - body:实际请求内容(可以是 Map、List 或其他类型)
  242 + *
  243 + * @param message 消息信息
  244 + * @param senderAddress 发送者地址
  245 + * @param socket UDP Socket
  246 + */
  247 + @SuppressWarnings("unchecked")
  248 + private void handleBusinessRequest(IotDeviceMessage message, InetSocketAddress senderAddress,
  249 + DatagramSocket socket) {
  250 + String addressKey = sessionManager.buildAddressKey(senderAddress);
  251 + // 1.1 从消息中提取 token 和 body
  252 + String token = null;
  253 + Object body = null;
  254 + if (message.getParams() instanceof Map) {
  255 + Map<String, Object> paramsMap = (Map<String, Object>) message.getParams();
  256 + token = (String) paramsMap.get(PARAM_KEY_TOKEN);
  257 + body = paramsMap.get(PARAM_KEY_BODY);
  258 + }
  259 + if (StrUtil.isBlank(token)) {
  260 + log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey);
  261 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  262 + UNAUTHORIZED.getCode(), "请先进行认证");
  263 + return;
  264 + }
  265 + // 1.2 验证 token,获取设备信息
  266 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
  267 + if (deviceInfo == null) {
  268 + log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey);
  269 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  270 + UNAUTHORIZED.getCode(), "token 无效或已过期");
  271 + return;
  272 + }
  273 + // 1.3 获取设备详细信息
  274 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
  275 + deviceInfo.getDeviceName());
  276 + if (device == null) {
  277 + log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]",
  278 + addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  279 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  280 + BAD_REQUEST.getCode(), "设备不存在");
  281 + return;
  282 + }
  283 +
  284 + // 2. 更新会话地址(如有变化)
  285 + sessionManager.updateSessionAddress(device.getId(), senderAddress);
  286 +
  287 + // 3. 将 body 设置为实际的 params,发送消息到消息总线
  288 + message.setParams(body);
  289 + deviceMessageService.sendDeviceMessage(message, device.getProductKey(),
  290 + device.getDeviceName(), serverId);
  291 + log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]",
  292 + device.getId(), message.getMethod(), addressKey);
  293 + }
  294 +
  295 + /**
  296 + * 注册会话信息
  297 + *
  298 + * @param address 设备地址
  299 + * @param device 设备
  300 + * @param clientId 客户端 ID
  301 + */
  302 + private void registerSession(InetSocketAddress address, IotDeviceRespDTO device, String clientId) {
  303 + IotUdpSessionManager.SessionInfo sessionInfo = new IotUdpSessionManager.SessionInfo()
  304 + .setDeviceId(device.getId())
  305 + .setProductKey(device.getProductKey())
  306 + .setDeviceName(device.getDeviceName())
  307 + .setAddress(address);
  308 + sessionManager.registerSession(device.getId(), sessionInfo);
  309 + }
  310 +
  311 + /**
  312 + * 发送设备上线消息
  313 + *
  314 + * @param device 设备信息
  315 + */
  316 + private void sendOnlineMessage(IotDeviceRespDTO device) {
  317 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  318 + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
  319 + device.getDeviceName(), serverId);
  320 + }
  321 +
  322 + // ===================== 发送响应消息 =====================
  323 +
  324 + /**
  325 + * 发送成功响应
  326 + *
  327 + * @param socket UDP Socket
  328 + * @param address 目标地址
  329 + * @param requestId 请求 ID
  330 + * @param method 方法名
  331 + * @param data 响应数据
  332 + */
  333 + private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address,
  334 + String requestId, String method, Object data) {
  335 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null);
  336 + writeResponse(socket, address, responseMessage);
  337 + }
  338 +
  339 + /**
  340 + * 发送错误响应
  341 + *
  342 + * @param socket UDP Socket
  343 + * @param address 目标地址
  344 + * @param requestId 请求 ID
  345 + * @param method 方法名
  346 + * @param code 错误码
  347 + * @param msg 错误消息
  348 + */
  349 + private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address,
  350 + String requestId, String method, Integer code, String msg) {
  351 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg);
  352 + writeResponse(socket, address, responseMessage);
  353 + }
  354 +
  355 + /**
  356 + * 写入响应到 Socket
  357 + *
  358 + * @param socket UDP Socket
  359 + * @param address 目标地址
  360 + * @param responseMessage 响应消息
  361 + */
  362 + private void writeResponse(DatagramSocket socket, InetSocketAddress address, IotDeviceMessage responseMessage) {
  363 + try {
  364 + byte[] serializedData = serializer.serialize(responseMessage);
  365 + socket.send(Buffer.buffer(serializedData), address.getPort(), address.getHostString(), result -> {
  366 + if (result.failed()) {
  367 + log.error("[writeResponse][发送响应失败,地址: {}]",
  368 + sessionManager.buildAddressKey(address), result.cause());
  369 + }
  370 + });
  371 + } catch (Exception e) {
  372 + log.error("[writeResponse][发送响应异常,地址: {}]",
  373 + sessionManager.buildAddressKey(address), e);
  374 + }
  375 + }
  376 +
  377 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import com.google.common.cache.Cache;
  5 +import com.google.common.cache.CacheBuilder;
3 6 import io.vertx.core.buffer.Buffer;
4 7 import io.vertx.core.datagram.DatagramSocket;
5 8 import lombok.Data;
6 9 import lombok.extern.slf4j.Slf4j;
7   -import org.springframework.stereotype.Component;
8 10  
9 11 import java.net.InetSocketAddress;
10   -import java.time.LocalDateTime;
11   -import java.util.ArrayList;
12   -import java.util.Iterator;
13   -import java.util.List;
14   -import java.util.Map;
15   -import java.util.concurrent.ConcurrentHashMap;
  12 +import java.util.concurrent.CompletableFuture;
  13 +import java.util.concurrent.TimeUnit;
16 14  
17 15 /**
18 16 * IoT 网关 UDP 会话管理器
19 17 * <p>
20   - * 采用无状态设计,SessionManager 主要用于:
21   - * 1. 管理设备地址映射(用于下行消息发送)
22   - * 2. 定期清理不活跃的设备地址映射
23   - * <p>
24   - * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态
  18 + * 基于 Guava Cache 实现会话的自动过期清理:
  19 + * 1. 管理设备会话信息(设备 ID -> 地址映射)
  20 + * 2. 自动清理超时会话(expireAfterAccess)
  21 + * 3. 限制最大会话数(maximumSize)
25 22 *
26 23 * @author 芋道源码
27 24 */
28 25 @Slf4j
29   -@Component
30 26 public class IotUdpSessionManager {
31 27  
32 28 /**
33   - * 设备 ID -> 会话信息(包含地址和 codecType)
  29 + * 设备会话缓存:设备 ID -> 会话信息
  30 + * <p>
  31 + * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间
34 32 */
35   - private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>();
  33 + private final Cache<Long, SessionInfo> deviceSessionCache;
36 34  
37   - /**
38   - * 设备地址 Key -> 最后活跃时间(用于清理)
39   - */
40   - private final Map<String, LocalDateTime> lastActiveTimeMap = new ConcurrentHashMap<>();
  35 + private final int maxSessions;
41 36  
42   - /**
43   - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步)
44   - */
45   - private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>();
46   -
47   - /**
48   - * 更新设备会话(每次收到上行消息时调用)
49   - *
50   - * @param deviceId 设备 ID
51   - * @param address 设备地址
52   - * @param codecType 消息编解码类型
53   - */
54   - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) {
55   - String addressKey = buildAddressKey(address);
56   - // 更新设备会话映射
57   - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType));
58   - lastActiveTimeMap.put(addressKey, LocalDateTime.now());
59   - addressDeviceMap.put(addressKey, deviceId);
60   - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType);
  37 + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) {
  38 + this.maxSessions = maxSessions;
  39 + this.deviceSessionCache = CacheBuilder.newBuilder()
  40 + .maximumSize(maxSessions)
  41 + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS)
  42 + .build();
61 43 }
62 44  
63 45 /**
64   - * 更新设备地址(兼容旧接口,默认不更新 codecType)
  46 + * 注册设备会话
65 47 *
66   - * @param deviceId 设备 ID
67   - * @param address 设备地址
  48 + * @param deviceId 设备 ID
  49 + * @param sessionInfo 会话信息
68 50 */
69   - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) {
70   - SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
71   - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null;
72   - updateDeviceSession(deviceId, address, codecType);
  51 + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) {
  52 + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性)
  53 + if (deviceSessionCache.getIfPresent(deviceId) == null
  54 + && deviceSessionCache.size() >= maxSessions) {
  55 + throw new IllegalStateException("会话数已达上限: " + maxSessions);
  56 + }
  57 + // 注册会话
  58 + deviceSessionCache.put(deviceId, sessionInfo);
  59 + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]",
  60 + deviceId, buildAddressKey(sessionInfo.getAddress()),
  61 + sessionInfo.getProductKey(), sessionInfo.getDeviceName());
73 62 }
74 63  
75 64 /**
76   - * 获取设备会话信息
  65 + * 获取会话信息
  66 + * <p>
  67 + * 注意:调用此方法会自动刷新会话的过期时间
77 68 *
78 69 * @param deviceId 设备 ID
79   - * @return 会话信息
  70 + * @return 会话信息,不存在则返回 null
80 71 */
81   - public SessionInfo getSessionInfo(Long deviceId) {
82   - return deviceSessionMap.get(deviceId);
  72 + public SessionInfo getSession(Long deviceId) {
  73 + return deviceSessionCache.getIfPresent(deviceId);
83 74 }
84 75  
85 76 /**
86   - * 检查设备是否在线(即是否有地址映射)
  77 + * 更新设备会话地址(设备地址变更时调用)
  78 + * <p>
  79 + * 注意:getIfPresent 已自动刷新过期时间,无需重新 put
87 80 *
88   - * @param deviceId 设备 ID
89   - * @return 是否在线
  81 + * @param deviceId 设备 ID
  82 + * @param newAddress 新地址
90 83 */
91   - public boolean isDeviceOnline(Long deviceId) {
92   - return deviceSessionMap.containsKey(deviceId);
93   - }
  84 + public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) {
  85 + // 地址未变化,无需更新
  86 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId);
  87 + if (sessionInfo == null) {
  88 + return;
  89 + }
  90 + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) {
  91 + return;
  92 + }
94 93  
95   - /**
96   - * 检查设备是否离线
97   - *
98   - * @param deviceId 设备 ID
99   - * @return 是否离线
100   - */
101   - public boolean isDeviceOffline(Long deviceId) {
102   - return !isDeviceOnline(deviceId);
  94 + // 更新地址
  95 + String oldAddressKey = buildAddressKey(sessionInfo.getAddress());
  96 + sessionInfo.setAddress(newAddress);
  97 + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]",
  98 + deviceId, oldAddressKey, buildAddressKey(newAddress));
103 99 }
104 100  
105 101 /**
... ... @@ -111,24 +107,28 @@ public class IotUdpSessionManager {
111 107 * @return 是否发送成功
112 108 */
113 109 public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) {
114   - SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
  110 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId);
115 111 if (sessionInfo == null || sessionInfo.getAddress() == null) {
116 112 log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId);
117 113 return false;
118 114 }
119   -
120 115 InetSocketAddress address = sessionInfo.getAddress();
121 116 try {
  117 + // 使用 CompletableFuture 同步等待发送结果
  118 + CompletableFuture<Boolean> future = new CompletableFuture<>();
122 119 socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
123 120 if (result.succeeded()) {
124 121 log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
125 122 deviceId, buildAddressKey(address), data.length);
  123 + future.complete(true);
126 124 } else {
127 125 log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
128 126 deviceId, buildAddressKey(address), result.cause());
  127 + future.complete(false);
129 128 }
130 129 });
131   - return true;
  130 + // 同步等待结果,超时 5 秒
  131 + return future.get(5, TimeUnit.SECONDS);
132 132 } catch (Exception e) {
133 133 log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
134 134 return false;
... ... @@ -136,44 +136,7 @@ public class IotUdpSessionManager {
136 136 }
137 137  
138 138 /**
139   - * 定期清理不活跃的设备地址映射
140   - *
141   - * @param timeoutMs 超时时间(毫秒)
142   - * @return 清理的设备 ID 列表(用于发送离线消息)
143   - */
144   - public List<Long> cleanExpiredMappings(long timeoutMs) {
145   - List<Long> offlineDeviceIds = new ArrayList<>();
146   - LocalDateTime now = LocalDateTime.now();
147   - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000);
148   - Iterator<Map.Entry<String, LocalDateTime>> iterator = lastActiveTimeMap.entrySet().iterator();
149   - while (iterator.hasNext()) {
150   - // 未过期,跳过
151   - Map.Entry<String, LocalDateTime> entry = iterator.next();
152   - if (entry.getValue().isAfter(expireTime)) {
153   - continue;
154   - }
155   - // 过期处理:记录离线设备 ID
156   - String addressKey = entry.getKey();
157   - Long deviceId = addressDeviceMap.remove(addressKey);
158   - if (deviceId == null) {
159   - iterator.remove();
160   - continue;
161   - }
162   - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId);
163   - if (sessionInfo == null) {
164   - iterator.remove();
165   - continue;
166   - }
167   - offlineDeviceIds.add(deviceId);
168   - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]",
169   - deviceId, addressKey, entry.getValue());
170   - iterator.remove();
171   - }
172   - return offlineDeviceIds;
173   - }
174   -
175   - /**
176   - * 构建地址 Key
  139 + * 构建地址 Key(用于日志输出)
177 140 *
178 141 * @param address 地址
179 142 * @return 地址 Key
... ... @@ -189,14 +152,22 @@ public class IotUdpSessionManager {
189 152 public static class SessionInfo {
190 153  
191 154 /**
192   - * 设备地址
  155 + * 设备 ID
193 156 */
194   - private InetSocketAddress address;
  157 + private Long deviceId;
  158 + /**
  159 + * 产品 Key
  160 + */
  161 + private String productKey;
  162 + /**
  163 + * 设备名称
  164 + */
  165 + private String deviceName;
195 166  
196 167 /**
197   - * 消息编解码类型
  168 + * 设备地址
198 169 */
199   - private String codecType;
  170 + private InetSocketAddress address;
200 171  
201 172 }
202 173  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.BooleanUtil;
5   -import cn.hutool.core.util.StrUtil;
6   -import cn.hutool.extra.spring.SpringUtil;
7   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
10   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
12   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
13   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
14   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
15   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
16   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
17   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
18   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
19   -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
20   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
21   -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
22   -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
23   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
24   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
25   -import io.vertx.core.buffer.Buffer;
26   -import io.vertx.core.datagram.DatagramPacket;
27   -import io.vertx.core.datagram.DatagramSocket;
28   -import lombok.extern.slf4j.Slf4j;
29   -
30   -import java.net.InetSocketAddress;
31   -import java.util.Map;
32   -
33   -/**
34   - * UDP 上行消息处理器
35   - * <p>
36   - * 采用无状态 Token 机制(每次请求携带 token):
37   - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password
38   - * 2. 返回 Token:服务端验证后返回 JWT token
39   - * 3. 后续请求:每次请求在 params 中携带 token
40   - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证
41   - *
42   - * @author 芋道源码
43   - */
44   -@Slf4j
45   -public class IotUdpUpstreamHandler {
46   -
47   - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
48   - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
49   -
50   - private static final String AUTH_METHOD = "auth";
51   - /**
52   - * Token 参数 Key
53   - */
54   - private static final String PARAM_KEY_TOKEN = "token";
55   - /**
56   - * Body 参数 Key(实际请求内容)
57   - */
58   - private static final String PARAM_KEY_BODY = "body";
59   -
60   - private final IotDeviceMessageService deviceMessageService;
61   -
62   - private final IotDeviceService deviceService;
63   -
64   - private final IotUdpSessionManager sessionManager;
65   -
66   - private final IotDeviceTokenService deviceTokenService;
67   -
68   - private final IotDeviceCommonApi deviceApi;
69   -
70   - private final String serverId;
71   -
72   - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol,
73   - IotDeviceMessageService deviceMessageService,
74   - IotDeviceService deviceService,
75   - IotUdpSessionManager sessionManager) {
76   - this.deviceMessageService = deviceMessageService;
77   - this.deviceService = deviceService;
78   - this.sessionManager = sessionManager;
79   - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
80   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
81   - this.serverId = protocol.getServerId();
82   - }
83   -
84   - /**
85   - * 处理 UDP 数据包
86   - *
87   - * @param packet 数据包
88   - * @param socket UDP Socket
89   - */
90   - public void handle(DatagramPacket packet, DatagramSocket socket) {
91   - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port());
92   - Buffer data = packet.data();
93   - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]",
94   - sessionManager.buildAddressKey(senderAddress), data.length());
95   - try {
96   - processMessage(data, senderAddress, socket);
97   - } catch (Exception e) {
98   - log.error("[handle][处理消息失败,来源: {},错误: {}]",
99   - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e);
100   - // UDP 无连接,不需要断开连接,只记录错误
101   - }
102   - }
103   -
104   - /**
105   - * 处理消息
106   - *
107   - * @param buffer 消息
108   - * @param senderAddress 发送者地址
109   - * @param socket UDP Socket
110   - */
111   - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) {
112   - // 1. 基础检查
113   - if (buffer == null || buffer.length() == 0) {
114   - return;
115   - }
116   -
117   - // 2. 获取消息格式类型
118   - String codecType = getMessageCodecType(buffer);
119   -
120   - // 3. 解码消息
121   - IotDeviceMessage message;
122   - try {
123   - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
124   - if (message == null) {
125   - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress));
126   - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType);
127   - return;
128   - }
129   - } catch (Exception e) {
130   - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e);
131   - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType);
132   - return;
133   - }
134   -
135   - // 4. 根据消息类型路由处理
136   - try {
137   - if (AUTH_METHOD.equals(message.getMethod())) {
138   - // 认证请求
139   - handleAuthenticationRequest(message, codecType, senderAddress, socket);
140   - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
141   - // 设备动态注册请求
142   - handleRegisterRequest(message, codecType, senderAddress, socket);
143   - } else {
144   - // 业务消息
145   - handleBusinessRequest(message, codecType, senderAddress, socket);
146   - }
147   - } catch (Exception e) {
148   - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]",
149   - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e);
150   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType);
151   - }
152   - }
153   -
154   - /**
155   - * 处理认证请求
156   - *
157   - * @param message 消息信息
158   - * @param codecType 消息编解码类型
159   - * @param senderAddress 发送者地址
160   - * @param socket UDP Socket
161   - */
162   - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType,
163   - InetSocketAddress senderAddress, DatagramSocket socket) {
164   - String addressKey = sessionManager.buildAddressKey(senderAddress);
165   - try {
166   - // 1.1 解析认证参数
167   - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
168   - if (authParams == null) {
169   - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey);
170   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType);
171   - return;
172   - }
173   - // 1.2 执行认证
174   - if (!validateDeviceAuth(authParams)) {
175   - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]",
176   - addressKey, authParams.getUsername());
177   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType);
178   - return;
179   - }
180   -
181   - // 2.1 解析设备信息
182   - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
183   - if (deviceInfo == null) {
184   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType);
185   - return;
186   - }
187   - // 2.2 获取设备信息
188   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
189   - deviceInfo.getDeviceName());
190   - if (device == null) {
191   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType);
192   - return;
193   - }
194   -
195   - // 3.1 生成 JWT Token(无状态)
196   - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName());
197   -
198   - // 3.2 更新设备会话信息(用于下行消息,保存 codecType)
199   - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType);
200   -
201   - // 3.3 发送上线消息
202   - sendOnlineMessage(device);
203   -
204   - // 3.4 发送成功响应(包含 token)
205   - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType);
206   - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]",
207   - device.getId(), device.getDeviceName(), addressKey);
208   - } catch (Exception e) {
209   - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e);
210   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType);
211   - }
212   - }
213   -
214   - /**
215   - * 处理设备动态注册请求(一型一密,不需要 Token)
216   - *
217   - * @param message 消息信息
218   - * @param codecType 消息编解码类型
219   - * @param senderAddress 发送者地址
220   - * @param socket UDP Socket
221   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
222   - */
223   - private void handleRegisterRequest(IotDeviceMessage message, String codecType,
224   - InetSocketAddress senderAddress, DatagramSocket socket) {
225   - String addressKey = sessionManager.buildAddressKey(senderAddress);
226   - try {
227   - // 1. 解析注册参数
228   - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
229   - if (params == null) {
230   - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey);
231   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType);
232   - return;
233   - }
234   -
235   - // 2. 调用动态注册
236   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
237   - if (result.isError()) {
238   - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg());
239   - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType);
240   - return;
241   - }
242   -
243   - // 3. 发送成功响应(包含 deviceSecret)
244   - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType);
245   - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]",
246   - params.getDeviceName(), addressKey);
247   - } catch (Exception e) {
248   - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e);
249   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType);
250   - }
251   - }
252   -
253   - /**
254   - * 处理业务请求
255   - * <p>
256   - * 请求参数格式:
257   - * - token:JWT 令牌
258   - * - body:实际请求内容(可以是 Map、List 或其他类型)
259   - *
260   - * @param message 消息信息
261   - * @param codecType 消息编解码类型
262   - * @param senderAddress 发送者地址
263   - * @param socket UDP Socket
264   - */
265   - @SuppressWarnings("unchecked")
266   - private void handleBusinessRequest(IotDeviceMessage message, String codecType,
267   - InetSocketAddress senderAddress, DatagramSocket socket) {
268   - String addressKey = sessionManager.buildAddressKey(senderAddress);
269   - try {
270   - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]})
271   - String token = null;
272   - Object body = null;
273   - if (message.getParams() instanceof Map) {
274   - Map<String, Object> paramsMap = (Map<String, Object>) message.getParams();
275   - token = (String) paramsMap.get(PARAM_KEY_TOKEN);
276   - body = paramsMap.get(PARAM_KEY_BODY);
277   - }
278   - if (StrUtil.isBlank(token)) {
279   - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey);
280   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType);
281   - return;
282   - }
283   - // 1.2 验证 token,获取设备信息
284   - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
285   - if (deviceInfo == null) {
286   - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey);
287   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType);
288   - return;
289   - }
290   -
291   - // 2. 获取设备详细信息
292   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
293   - deviceInfo.getDeviceName());
294   - if (device == null) {
295   - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]",
296   - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
297   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType);
298   - return;
299   - }
300   -
301   - // 3. 更新设备会话信息(保持最新,保存 codecType)
302   - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType);
303   -
304   - // 4. 将 body 设置为实际的 params,发送消息到消息总线
305   - message.setParams(body);
306   - deviceMessageService.sendDeviceMessage(message, device.getProductKey(),
307   - device.getDeviceName(), serverId);
308   - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]",
309   - device.getId(), message.getMethod(), addressKey);
310   - } catch (Exception e) {
311   - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e);
312   - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType);
313   - }
314   - }
315   -
316   - /**
317   - * 获取消息编解码类型
318   - *
319   - * @param buffer 消息
320   - * @return 消息编解码类型
321   - */
322   - private String getMessageCodecType(Buffer buffer) {
323   - // 检测消息格式类型
324   - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY
325   - : CODEC_TYPE_JSON;
326   - }
327   -
328   - /**
329   - * 发送设备上线消息
330   - *
331   - * @param device 设备信息
332   - */
333   - private void sendOnlineMessage(IotDeviceRespDTO device) {
334   - try {
335   - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
336   - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
337   - device.getDeviceName(), serverId);
338   - } catch (Exception e) {
339   - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
340   - }
341   - }
342   -
343   - /**
344   - * 验证设备认证信息
345   - *
346   - * @param authParams 认证参数
347   - * @return 是否认证成功
348   - */
349   - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
350   - try {
351   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
352   - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
353   - .setPassword(authParams.getPassword()));
354   - result.checkError();
355   - return BooleanUtil.isTrue(result.getData());
356   - } catch (Exception e) {
357   - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e);
358   - return false;
359   - }
360   - }
361   -
362   - /**
363   - * 发送认证成功响应(包含 token)
364   - *
365   - * @param socket UDP Socket
366   - * @param address 目标地址
367   - * @param requestId 请求 ID
368   - * @param token JWT Token
369   - * @param codecType 消息编解码类型
370   - */
371   - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address,
372   - String requestId, String token, String codecType) {
373   - try {
374   - // 构建响应数据
375   - Object responseData = MapUtil.builder()
376   - .put("success", true)
377   - .put("token", token)
378   - .put("message", "认证成功")
379   - .build();
380   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功");
381   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
382   - // 发送响应
383   - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
384   - if (result.failed()) {
385   - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]",
386   - sessionManager.buildAddressKey(address), result.cause());
387   - }
388   - });
389   - } catch (Exception e) {
390   - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]",
391   - sessionManager.buildAddressKey(address), e);
392   - }
393   - }
394   -
395   - /**
396   - * 发送注册成功响应(包含 deviceSecret)
397   - *
398   - * @param socket UDP Socket
399   - * @param address 目标地址
400   - * @param requestId 请求 ID
401   - * @param registerResp 注册响应
402   - * @param codecType 消息编解码类型
403   - */
404   - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address,
405   - String requestId, IotDeviceRegisterRespDTO registerResp,
406   - String codecType) {
407   - try {
408   - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
409   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
410   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
411   - // 2. 发送响应
412   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
413   - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
414   - if (result.failed()) {
415   - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]",
416   - sessionManager.buildAddressKey(address), result.cause());
417   - }
418   - });
419   - } catch (Exception e) {
420   - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]",
421   - sessionManager.buildAddressKey(address), e);
422   - }
423   - }
424   -
425   - /**
426   - * 发送错误响应
427   - *
428   - * @param socket UDP Socket
429   - * @param address 目标地址
430   - * @param requestId 请求 ID
431   - * @param errorMessage 错误消息
432   - * @param codecType 消息编解码类型
433   - */
434   - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address,
435   - String requestId, String errorMessage, String codecType) {
436   - sendResponse(socket, address, false, errorMessage, requestId, codecType);
437   - }
438   -
439   - /**
440   - * 发送响应消息
441   - *
442   - * @param socket UDP Socket
443   - * @param address 目标地址
444   - * @param success 是否成功
445   - * @param message 消息
446   - * @param requestId 请求 ID
447   - * @param codecType 消息编解码类型
448   - */
449   - @SuppressWarnings("SameParameterValue")
450   - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success,
451   - String message, String requestId, String codecType) {
452   - try {
453   - // 构建响应数据
454   - Object responseData = MapUtil.builder()
455   - .put("success", success)
456   - .put("message", message)
457   - .build();
458   - int code = success ? 0 : 401;
459   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
460   - "response", responseData, code, message);
461   -
462   - // 发送响应
463   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
464   - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> {
465   - if (ar.failed()) {
466   - log.error("[sendResponse][发送响应失败,地址: {}]",
467   - sessionManager.buildAddressKey(address), ar.cause());
468   - }
469   - });
470   - } catch (Exception e) {
471   - log.error("[sendResponse][发送响应异常,地址: {}]",
472   - sessionManager.buildAddressKey(address), e);
473   - }
474   - }
475   -
476   - /**
477   - * 解析认证参数
478   - *
479   - * @param params 参数对象(通常为 Map 类型)
480   - * @return 认证参数 DTO,解析失败时返回 null
481   - */
482   - @SuppressWarnings("unchecked")
483   - private IotDeviceAuthReqDTO parseAuthParams(Object params) {
484   - if (params == null) {
485   - return null;
486   - }
487   - try {
488   - // 参数默认为 Map 类型,直接转换
489   - if (params instanceof Map) {
490   - Map<String, Object> paramMap = (Map<String, Object>) params;
491   - return new IotDeviceAuthReqDTO()
492   - .setClientId(MapUtil.getStr(paramMap, "clientId"))
493   - .setUsername(MapUtil.getStr(paramMap, "username"))
494   - .setPassword(MapUtil.getStr(paramMap, "password"));
495   - }
496   - // 如果已经是目标类型,直接返回
497   - if (params instanceof IotDeviceAuthReqDTO) {
498   - return (IotDeviceAuthReqDTO) params;
499   - }
500   -
501   - // 其他情况尝试 JSON 转换
502   - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
503   - } catch (Exception e) {
504   - log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
505   - return null;
506   - }
507   - }
508   -
509   - /**
510   - * 解析注册参数
511   - *
512   - * @param params 参数对象(通常为 Map 类型)
513   - * @return 注册参数 DTO,解析失败时返回 null
514   - */
515   - @SuppressWarnings({"unchecked", "DuplicatedCode"})
516   - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
517   - if (params == null) {
518   - return null;
519   - }
520   - try {
521   - // 参数默认为 Map 类型,直接转换
522   - if (params instanceof Map) {
523   - Map<String, Object> paramMap = (Map<String, Object>) params;
524   - return new IotDeviceRegisterReqDTO()
525   - .setProductKey(MapUtil.getStr(paramMap, "productKey"))
526   - .setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
527   - .setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
528   - }
529   - // 如果已经是目标类型,直接返回
530   - if (params instanceof IotDeviceRegisterReqDTO) {
531   - return (IotDeviceRegisterReqDTO) params;
532   - }
533   -
534   - // 其他情况尝试 JSON 转换
535   - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
536   - } catch (Exception e) {
537   - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
538   - return null;
539   - }
540   - }
541   -
542   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT WebSocket 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotWebSocketConfig {
  14 +
  15 + /**
  16 + * WebSocket 路径(默认:/ws)
  17 + */
  18 + @NotEmpty(message = "WebSocket 路径不能为空")
  19 + private String path = "/ws";
  20 +
  21 + /**
  22 + * 最大消息大小(字节,默认 64KB)
  23 + */
  24 + @NotNull(message = "最大消息大小不能为空")
  25 + private Integer maxMessageSize = 65536;
  26 + /**
  27 + * 最大帧大小(字节,默认 64KB)
  28 + */
  29 + @NotNull(message = "最大帧大小不能为空")
  30 + private Integer maxFrameSize = 65536;
  31 +
  32 + /**
  33 + * 空闲超时时间(秒,默认 60)
  34 + */
  35 + @NotNull(message = "空闲超时时间不能为空")
  36 + private Integer idleTimeoutSeconds = 60;
  37 +
  38 +}
... ...