【同步】BOOT 和 CLOUD 的功能

This commit is contained in:
YunaiV
2026-01-29 22:14:05 +08:00
parent 17a1af1069
commit fa72dc4e59
155 changed files with 13546 additions and 3184 deletions

View File

@@ -75,6 +75,8 @@
<netty.version>4.2.9.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<vertx.version>4.5.22</vertx.version>
<okhttp.version>4.12.0</okhttp.version>
<californium.version>3.12.0</californium.version>
<!-- 三方云服务相关 -->
<awssdk.version>2.40.15</awssdk.version>
<justauth.version>1.16.7</justauth.version>
@@ -348,7 +350,6 @@
<artifactId>yudao-spring-boot-starter-mq</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
@@ -611,6 +612,50 @@
<version>${reflections.version}</version>
</dependency>
<!-- Vert.x -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
<version>${vertx.version}</version>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>${mqtt.version}</version>
</dependency>
<!-- OkHttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${okhttp.version}</version>
<scope>test</scope>
</dependency>
<!-- CoAP - Eclipse Californium -->
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
<version>${californium.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
@@ -618,6 +663,24 @@
<version>${awssdk.version}</version>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${justauth-starter.version}</version>
<exclusions>
<!-- 移除,避免和项目里的 hutool-all 冲突 -->
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
@@ -646,17 +709,6 @@
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${justauth-starter.version}</version>
</dependency>
<!-- 积木报表-->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
@@ -678,30 +730,6 @@
</exclusion>
</exclusions>
</dependency>
<!-- Vert.x -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
<version>${vertx.version}</version>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>${mqtt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -65,4 +66,47 @@ public class MapUtils {
return map;
}
/**
* 从 Map 中获取 BigDecimal 值
*
* @param map Map 数据源
* @param key 键名
* @return BigDecimal 值,解析失败或值为 null 时返回 null
*/
public static BigDecimal getBigDecimal(Map<String, ?> map, String key) {
return getBigDecimal(map, key, null);
}
/**
* 从 Map 中获取 BigDecimal 值
*
* @param map Map 数据源
* @param key 键名
* @param defaultValue 默认值
* @return BigDecimal 值,解析失败或值为 null 时返回默认值
*/
public static BigDecimal getBigDecimal(Map<String, ?> map, String key, BigDecimal defaultValue) {
if (map == null) {
return defaultValue;
}
Object value = map.get(key);
if (value == null) {
return defaultValue;
}
if (value instanceof BigDecimal) {
return (BigDecimal) value;
}
if (value instanceof Number) {
return BigDecimal.valueOf(((Number) value).doubleValue());
}
if (value instanceof String) {
try {
return new BigDecimal((String) value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return defaultValue;
}
}

View File

@@ -229,4 +229,53 @@ public class JsonUtils {
return JSONUtil.isTypeJSONObject(str);
}
/**
* 将 Object 转换为目标类型
* <p>
* 避免先转 jsonString 再 parseObject 的性能损耗
*
* @param obj 源对象(可以是 Map、POJO 等)
* @param clazz 目标类型
* @return 转换后的对象
*/
public static <T> T convertObject(Object obj, Class<T> clazz) {
if (obj == null) {
return null;
}
if (clazz.isInstance(obj)) {
return clazz.cast(obj);
}
return objectMapper.convertValue(obj, clazz);
}
/**
* 将 Object 转换为目标类型(支持泛型)
*
* @param obj 源对象
* @param typeReference 目标类型引用
* @return 转换后的对象
*/
public static <T> T convertObject(Object obj, TypeReference<T> typeReference) {
if (obj == null) {
return null;
}
return objectMapper.convertValue(obj, typeReference);
}
/**
* 将 Object 转换为 List 类型
* <p>
* 避免先转 jsonString 再 parseArray 的性能损耗
*
* @param obj 源对象(可以是 List、数组等
* @param clazz 目标元素类型
* @return 转换后的 List
*/
public static <T> List<T> convertList(Object obj, Class<T> clazz) {
if (obj == null) {
return new ArrayList<>();
}
return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
}
}

View File

@@ -15,6 +15,7 @@ import java.util.function.Consumer;
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
* 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
*
* @param <T> 数据类型
*/
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
@@ -122,6 +123,12 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> orderByAsc(SFunction<X, ?> column) {
super.orderByAsc(true, column);
return this;
}
@Override
public MPJLambdaWrapperX<T> last(String lastSql) {
super.last(lastSql);

View File

@@ -15,7 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.List;
@@ -30,12 +30,12 @@ public class BpmFlowableConfiguration {
/**
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
*
* <p>
* 如果不创建会导致项目启动时Flowable 报错的问题
*/
@Bean(name = "applicationTaskExecutor")
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
public AsyncListenableTaskExecutor taskExecutor() {
public AsyncTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(8);

View File

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import lombok.Setter;
import org.flowable.bpmn.model.*;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
@@ -90,4 +91,21 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
}
// ========== 屏蔽解析器覆写 ==========
@Override
public void setCollectionExpression(Expression collectionExpression) {
// 保持自定义变量名,忽略解析器写入的 collection 表达式
}
@Override
public void setCollectionVariable(String collectionVariable) {
// 保持自定义变量名,忽略解析器写入的 collection 变量名
}
@Override
public void setCollectionElementVariable(String collectionElementVariable) {
// 保持自定义变量名,忽略解析器写入的单元素变量名
}
}

View File

@@ -70,7 +70,7 @@ public class ApiAccessLogRespVO {
@Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "操作分类", converter = DictConvert.class)
@DictFormat(cn.iocoder.yudao.module.infra.enums.DictTypeConstants.OPERATE_TYPE)
@DictFormat(DictTypeConstants.OPERATE_TYPE)
private Integer operateType;
@Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@@ -26,13 +26,26 @@ public interface ErrorCodeConstants {
// ========== 设备 1-050-003-000 ============
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "子设备,不允许删除");
ErrorCode DEVICE_GATEWAY_HAS_SUB = new ErrorCode(1_050_003_002, "网关设备存在已绑定的子设备,不允许删除");
ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在");
ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备");
ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
// 拓扑管理相关错误码 1-050-003-100
ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
// 设备注册相关错误码 1-050-003-200
ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");

View File

@@ -1,45 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.device;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
// TODO @芋艿:需要添加对应的 DTO以及上下行的链路网关、网关服务、设备等
/**
* IoT 设备消息标识符枚举
*/
@Deprecated
@Getter
@RequiredArgsConstructor
public enum IotDeviceMessageIdentifierEnum {
PROPERTY_GET("get"), // 下行
PROPERTY_SET("set"), // 下行
PROPERTY_REPORT("report"), // 上行
STATE_ONLINE("online"), // 上行
STATE_OFFLINE("offline"), // 上行
CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景
CONFIG_SET("set"), // 下行
SERVICE_INVOKE("${identifier}"), // 下行
SERVICE_REPLY_SUFFIX("_reply"), // 芋艿TODO 芋艿:【讨论】上行 or 下行
OTA_UPGRADE("upgrade"), // 下行
OTA_PULL("pull"), // 上行
OTA_PROGRESS("progress"), // 上行
OTA_REPORT("report"), // 上行
REGISTER_REGISTER("register"), // 上行
REGISTER_REGISTER_SUB("register_sub"), // 上行
REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行
TOPOLOGY_ADD("topology_add"), // 下行;
;
/**
* 标志符
*/
private final String identifier;
}

View File

@@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.device;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 设备消息类型枚举
*/
@Deprecated
@Getter
@RequiredArgsConstructor
public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {
STATE("state"), // 设备状态
PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
OTA("ota"), // 设备 OTA可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
/**
* 属性
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
}

View File

@@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IoT 定位方式枚举类
*
* @author alwayssuper
*/
@AllArgsConstructor
@Getter
public enum IotLocationTypeEnum implements ArrayValuable<Integer> {
IP(1, "IP 定位"),
DEVICE(2, "设备上报"),
MANUAL(3, "手动定位");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new);
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import java.util.List;
/**
* IoT 设备通用 API
@@ -28,4 +34,20 @@ public interface IotDeviceCommonApi {
*/
CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
/**
* 直连/网关设备动态注册(一型一密)
*
* @param reqDTO 动态注册请求
* @return 注册结果(包含 DeviceSecret
*/
CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
/**
* 网关子设备动态注册(网关代理转发)
*
* @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
* @return 注册结果列表
*/
CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
}

View File

@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 设备认证 Request DTO
@@ -9,6 +11,8 @@ import lombok.Data;
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceAuthReqDTO {
/**

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* IoT 子设备动态注册 Request DTO
* <p>
* 额外包含了网关设备的标识信息
*
* @author 芋道源码
*/
@Data
public class IotSubDeviceRegisterFullReqDTO {
/**
* 网关设备 ProductKey
*/
@NotEmpty(message = "网关产品标识不能为空")
private String gatewayProductKey;
/**
* 网关设备 DeviceName
*/
@NotEmpty(message = "网关设备名称不能为空")
private String gatewayDeviceName;
/**
* 子设备注册列表
*/
@NotNull(message = "子设备注册列表不能为空")
private List<IotSubDeviceRegisterReqDTO> subDevices;
}

View File

@@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
// TODO 芋艿:要不要加个 ping 消息;
// ========== 拓扑管理 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships
TOPO_ADD("thing.topo.add", "添加拓扑关系", true),
TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true),
TOPO_GET("thing.topo.get", "获取拓扑关系", true),
TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
// ========== 设备注册 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
// ========== 设备属性 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
PROPERTY_POST("thing.property.post", "属性上报", true),
PROPERTY_SET("thing.property.set", "属性设置", false),
PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
// ========== 设备事件 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
@@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
;
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)

View File

@@ -1,37 +0,0 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 设备消息类型枚举
*/
@Getter
@RequiredArgsConstructor
public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {
STATE("state"), // 设备状态
// PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
OTA("ota"), // 设备 OTA可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
/**
* 属性
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.iot.core.topic;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 设备标识
*
* 用于标识一个设备的基本信息productKey + deviceName
*
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceIdentity {
/**
* 产品标识
*/
@NotEmpty(message = "产品标识不能为空")
private String productKey;
/**
* 设备名称
*/
@NotEmpty(message = "设备名称不能为空")
private String deviceName;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
/**
* IoT 设备动态注册 Request DTO
* <p>
* 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@Data
public class IotDeviceRegisterReqDTO {
/**
* 产品标识
*/
@NotEmpty(message = "产品标识不能为空")
private String productKey;
/**
* 设备名称
*/
@NotEmpty(message = "设备名称不能为空")
private String deviceName;
/**
* 产品密钥
*/
@NotEmpty(message = "产品密钥不能为空")
private String productSecret;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 设备动态注册 Response DTO
* <p>
* 用于直连设备/网关的一型一密动态注册响应
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceRegisterRespDTO {
/**
* 产品标识
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 设备密钥
*/
private String deviceSecret;
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
/**
* IoT 子设备动态注册 Request DTO
* <p>
* 用于 thing.auth.register.sub 消息的 params 数组元素
*
* 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
@Data
public class IotSubDeviceRegisterReqDTO {
/**
* 子设备 ProductKey
*/
@NotEmpty(message = "产品标识不能为空")
private String productKey;
/**
* 子设备 DeviceName
*/
@NotEmpty(message = "设备名称不能为空")
private String deviceName;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 子设备动态注册 Response DTO
* <p>
* 用于 thing.auth.register.sub 响应的设备信息
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotSubDeviceRegisterRespDTO {
/**
* 子设备 ProductKey
*/
private String productKey;
/**
* 子设备 DeviceName
*/
private String deviceName;
/**
* 分配的 DeviceSecret
*/
private String deviceSecret;
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.iot.core.topic.event;
import lombok.Data;
/**
* IoT 设备事件上报 Request DTO
* <p>
* 用于 thing.event.post 消息的 params 参数
*
* @author 芋道源码
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
*/
@Data
public class IotDeviceEventPostReqDTO {
/**
* 事件标识符
*/
private String identifier;
/**
* 事件输出参数
*/
private Object value;
/**
* 上报时间(毫秒时间戳,可选)
*/
private Long time;
/**
* 创建事件上报 DTO
*
* @param identifier 事件标识符
* @param value 事件值
* @return DTO 对象
*/
public static IotDeviceEventPostReqDTO of(String identifier, Object value) {
return of(identifier, value, null);
}
/**
* 创建事件上报 DTO带时间
*
* @param identifier 事件标识符
* @param value 事件值
* @param time 上报时间
* @return DTO 对象
*/
public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) {
return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time);
}
}

View File

@@ -0,0 +1,8 @@
/**
* IoT Topic 消息体 DTO 定义
* <p>
* 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范
*
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/alink-protocol-1">阿里云 Alink 协议</a>
*/
package cn.iocoder.yudao.module.iot.core.topic;

View File

@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.iot.core.topic.property;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* IoT 设备属性批量上报 Request DTO
* <p>
* 用于 thing.event.property.pack.post 消息的 params 参数
*
* @author 芋道源码
* @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
*/
@Data
public class IotDevicePropertyPackPostReqDTO {
/**
* 网关自身属性
* <p>
* key: 属性标识符
* value: 属性值
*/
private Map<String, Object> properties;
/**
* 网关自身事件
* <p>
* key: 事件标识符
* value: 事件值对象(包含 value 和 time
*/
private Map<String, EventValue> events;
/**
* 子设备数据列表
*/
private List<SubDeviceData> subDevices;
/**
* 事件值对象
*/
@Data
public static class EventValue {
/**
* 事件参数
*/
private Object value;
/**
* 上报时间(毫秒时间戳)
*/
private Long time;
}
/**
* 子设备数据
*/
@Data
public static class SubDeviceData {
/**
* 子设备标识
*/
private IotDeviceIdentity identity;
/**
* 子设备属性
* <p>
* key: 属性标识符
* value: 属性值
*/
private Map<String, Object> properties;
/**
* 子设备事件
* <p>
* key: 事件标识符
* value: 事件值对象(包含 value 和 time
*/
private Map<String, EventValue> events;
}
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.iot.core.topic.property;
import java.util.HashMap;
import java.util.Map;
/**
* IoT 设备属性上报 Request DTO
* <p>
* 用于 thing.property.post 消息的 params 参数
* <p>
* 本质是一个 Mapkey 为属性标识符value 为属性值
*
* @author 芋道源码
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-attributes">阿里云 - 设备上报属性</a>
*/
public class IotDevicePropertyPostReqDTO extends HashMap<String, Object> {
public IotDevicePropertyPostReqDTO() {
super();
}
public IotDevicePropertyPostReqDTO(Map<String, Object> properties) {
super(properties);
}
/**
* 创建属性上报 DTO
*
* @param properties 属性数据
* @return DTO 对象
*/
public static IotDevicePropertyPostReqDTO of(Map<String, Object> properties) {
return new IotDevicePropertyPostReqDTO(properties);
}
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* IoT 设备拓扑添加 Request DTO
* <p>
* 用于 thing.topo.add 消息的 params 参数
*
* @author 芋道源码
* @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
*/
@Data
public class IotDeviceTopoAddReqDTO {
/**
* 子设备认证信息列表
* <p>
* 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
*/
@NotEmpty(message = "子设备认证信息列表不能为空")
private List<IotDeviceAuthReqDTO> subDevices;
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* IoT 设备拓扑关系变更通知 Request DTO
* <p>
* 用于 thing.topo.change 下行消息的 params 参数
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceTopoChangeReqDTO {
public static final Integer STATUS_CREATE = 0;
public static final Integer STATUS_DELETE = 1;
/**
* 拓扑关系状态
*/
private Integer status;
/**
* 子设备列表
*/
private List<IotDeviceIdentity> subList;
public static IotDeviceTopoChangeReqDTO ofCreate(List<IotDeviceIdentity> subList) {
return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList);
}
public static IotDeviceTopoChangeReqDTO ofDelete(List<IotDeviceIdentity> subList) {
return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList);
}
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* IoT 设备拓扑删除 Request DTO
* <p>
* 用于 thing.topo.delete 消息的 params 参数
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
*/
@Data
public class IotDeviceTopoDeleteReqDTO {
/**
* 子设备标识列表
*/
@Valid
@NotEmpty(message = "子设备标识列表不能为空")
private List<IotDeviceIdentity> subDevices;
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import lombok.Data;
/**
* IoT 设备拓扑关系获取 Request DTO
* <p>
* 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
*/
@Data
public class IotDeviceTopoGetReqDTO {
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.Data;
import java.util.List;
/**
* IoT 设备拓扑关系获取 Response DTO
* <p>
* 用于 thing.topo.get 响应
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
*/
@Data
public class IotDeviceTopoGetRespDTO {
/**
* 子设备列表
*/
private List<IotDeviceIdentity> subDevices;
}

View File

@@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
/**
* IoT 设备【认证】的工具类,参考阿里云
@@ -13,73 +13,40 @@ import lombok.NoArgsConstructor;
*/
public class IotDeviceAuthUtils {
/**
* 认证信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AuthInfo {
/**
* 客户端 ID
*/
private String clientId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
/**
* 设备信息
*/
@Data
public static class DeviceInfo {
private String productKey;
private String deviceName;
}
public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) {
public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
String clientId = buildClientId(productKey, deviceName);
String username = buildUsername(productKey, deviceName);
String content = "clientId" + clientId +
"deviceName" + deviceName +
"deviceSecret" + deviceSecret +
"productKey" + productKey;
String password = buildPassword(deviceSecret, content);
return new AuthInfo(clientId, username, password);
String password = buildPassword(deviceSecret,
buildContent(clientId, productKey, deviceName, deviceSecret));
return new IotDeviceAuthReqDTO(clientId, username, password);
}
private static String buildClientId(String productKey, String deviceName) {
public static String buildClientId(String productKey, String deviceName) {
return String.format("%s.%s", productKey, deviceName);
}
private static String buildUsername(String productKey, String deviceName) {
public static String buildUsername(String productKey, String deviceName) {
return String.format("%s&%s", deviceName, productKey);
}
private static String buildPassword(String deviceSecret, String content) {
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes())
public static String buildPassword(String deviceSecret, String content) {
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret))
.digestHex(content);
}
public static DeviceInfo parseUsername(String username) {
private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
return "clientId" + clientId +
"deviceName" + deviceName +
"deviceSecret" + deviceSecret +
"productKey" + productKey;
}
public static IotDeviceIdentity parseUsername(String username) {
String[] usernameParts = username.split("&");
if (usernameParts.length != 2) {
return null;
}
return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]);
return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
}
}

View File

@@ -72,7 +72,7 @@ public class IotDeviceMessageUtils {
/**
* 判断消息中是否包含指定的标识符
*
* <p>
* 对于不同消息类型的处理:
* - EVENT_POST/SERVICE_INVOKE检查 params.identifier 是否匹配
* - STATE_UPDATE检查 params.state 是否匹配
@@ -99,6 +99,17 @@ public class IotDeviceMessageUtils {
return false;
}
/**
* 判断消息中是否不包含指定的标识符
*
* @param message 消息
* @param identifier 要检查的标识符
* @return 是否不包含
*/
public static boolean notContainsIdentifier(IotDeviceMessage message, String identifier) {
return !containsIdentifier(message, identifier);
}
/**
* 将 params 解析为 Map
*
@@ -144,20 +155,19 @@ public class IotDeviceMessageUtils {
return null;
}
// 策略1如果 params 不是 Map直接返回该值适用于简单的单属性消息
// 策略 1如果 params 不是 Map直接返回该值适用于简单的单属性消息
if (!(params instanceof Map)) {
return params;
}
// 策略 2直接通过标识符获取属性值
Map<String, Object> paramsMap = (Map<String, Object>) params;
// 策略2直接通过标识符获取属性值
Object directValue = paramsMap.get(identifier);
if (directValue != null) {
return directValue;
}
// 策略3从 properties 字段中获取(适用于标准属性上报消息)
// 策略 3从 properties 字段中获取(适用于标准属性上报消息)
Object properties = paramsMap.get("properties");
if (properties instanceof Map) {
Map<String, Object> propertiesMap = (Map<String, Object>) properties;
@@ -167,7 +177,7 @@ public class IotDeviceMessageUtils {
}
}
// 策略4从 data 字段中获取(适用于某些消息格式)
// 策略 4从 data 字段中获取(适用于某些消息格式)
Object data = paramsMap.get("data");
if (data instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) data;
@@ -177,13 +187,13 @@ public class IotDeviceMessageUtils {
}
}
// 策略5从 value 字段中获取(适用于单值消息)
// 策略 5从 value 字段中获取(适用于单值消息)
Object value = paramsMap.get("value");
if (value != null) {
return value;
}
// 策略6如果 Map 只有两个字段且包含 identifier返回另一个字段的值
// 策略 6如果 Map 只有两个字段且包含 identifier返回另一个字段的值
if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
if (!"identifier".equals(entry.getKey())) {
@@ -196,6 +206,43 @@ public class IotDeviceMessageUtils {
return null;
}
/**
* 从服务调用消息中提取输入参数
* <p>
* 服务调用消息的 params 结构通常为:
* {
* "identifier": "serviceIdentifier",
* "inputData": { ... } 或 "inputParams": { ... }
* }
*
* @param message 设备消息
* @return 输入参数 Map如果未找到则返回 null
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> extractServiceInputParams(IotDeviceMessage message) {
// 1. 参数校验
Object params = message.getParams();
if (params == null) {
return null;
}
if (!(params instanceof Map)) {
return null;
}
Map<String, Object> paramsMap = (Map<String, Object>) params;
// 尝试从 inputData 字段获取
Object inputData = paramsMap.get("inputData");
if (inputData instanceof Map) {
return (Map<String, Object>) inputData;
}
// 尝试从 inputParams 字段获取
Object inputParams = paramsMap.get("inputParams");
if (inputParams instanceof Map) {
return (Map<String, Object>) inputParams;
}
return null;
}
// ========== Topic 相关 ==========
public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {

View File

@@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link IotDeviceMessageUtils} 的单元测试
@@ -138,4 +138,72 @@ public class IotDeviceMessageUtilsTest {
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result); // 应该返回直接标识符的值
}
// ========== notContainsIdentifier 测试 ==========
/**
* 测试 notContainsIdentifier 与 containsIdentifier 的互补性
* **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
* **Validates: Requirements 4.1**
*/
@Test
public void testNotContainsIdentifier_complementary_whenContains() {
// 准备参数:消息包含指定标识符
IotDeviceMessage message = new IotDeviceMessage();
message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25);
message.setParams(params);
String identifier = "temperature";
// 调用 & 断言:验证互补性
boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
assertTrue(containsResult);
assertFalse(notContainsResult);
assertEquals(!containsResult, notContainsResult);
}
/**
* 测试 notContainsIdentifier 与 containsIdentifier 的互补性
* **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
* **Validates: Requirements 4.1**
*/
@Test
public void testNotContainsIdentifier_complementary_whenNotContains() {
// 准备参数:消息不包含指定标识符
IotDeviceMessage message = new IotDeviceMessage();
message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25);
message.setParams(params);
String identifier = "humidity";
// 调用 & 断言:验证互补性
boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
assertFalse(containsResult);
assertTrue(notContainsResult);
assertEquals(!containsResult, notContainsResult);
}
/**
* 测试 notContainsIdentifier 与 containsIdentifier 的互补性 - 空参数场景
* **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
* **Validates: Requirements 4.1**
*/
@Test
public void testNotContainsIdentifier_complementary_nullParams() {
// 准备参数params 为 null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
String identifier = "temperature";
// 调用 & 断言:验证互补性
boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
assertFalse(containsResult);
assertTrue(notContainsResult);
assertEquals(!containsResult, notContainsResult);
}
}

View File

@@ -48,6 +48,12 @@
<artifactId>vertx-mqtt</artifactId>
</dependency>
<!-- CoAP 相关 - Eclipse Californium -->
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
</dependency>
<!-- 测试相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>

View File

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.codec;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
/**
* {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器
* {@link IotDeviceMessage} 的编解码器
*
* @author 芋道源码
*/

View File

@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
@Component
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
private static final String TYPE = "Alink";
public static final String TYPE = "Alink";
@Data
@NoArgsConstructor

View File

@@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* TCP 二进制格式 {@link IotDeviceMessage} 编解码器
* TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器
* <p>
* 二进制协议格式(所有数值使用大端序):
*

View File

@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* TCP JSON 格式 {@link IotDeviceMessage} 编解码器
* TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
*
* 采用纯 JSON 格式传输,格式如下:
* {

View File

@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
@@ -10,13 +12,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
@@ -40,9 +44,15 @@ public class IotGatewayConfiguration {
@Slf4j
public static class HttpProtocolConfiguration {
@Bean(name = "httpVertx", destroyMethod = "close")
public Vertx httpVertx() {
return Vertx.vertx();
}
@Bean
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) {
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp());
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("httpVertx") Vertx httpVertx) {
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
}
@Bean
@@ -110,11 +120,9 @@ public class IotGatewayConfiguration {
@Bean
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotDeviceService deviceService,
IotTcpConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,
messageBus);
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}
@@ -157,39 +165,88 @@ public class IotGatewayConfiguration {
}
/**
* IoT 网关 MQTT WebSocket 协议配置类
* IoT 网关 UDP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt-ws", name = "enabled", havingValue = "true")
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
@Slf4j
public static class MqttWsProtocolConfiguration {
public static class UdpProtocolConfiguration {
@Bean(name = "mqttWsVertx", destroyMethod = "close")
public Vertx mqttWsVertx() {
@Bean(name = "udpVertx", destroyMethod = "close")
public Vertx udpVertx() {
return Vertx.vertx();
}
@Bean
public IotMqttWsUpstreamProtocol iotMqttWsUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceMessageService messageService,
IotMqttWsConnectionManager connectionManager,
@Qualifier("mqttWsVertx") Vertx mqttWsVertx) {
return new IotMqttWsUpstreamProtocol(gatewayProperties.getProtocol().getMqttWs(),
messageService, connectionManager, mqttWsVertx);
public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
@Qualifier("udpVertx") Vertx udpVertx) {
return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
deviceService, messageService, sessionManager, udpVertx);
}
@Bean
public IotMqttWsDownstreamHandler iotMqttWsDownstreamHandler(IotDeviceMessageService messageService,
IotDeviceService deviceService,
IotMqttWsConnectionManager connectionManager) {
return new IotMqttWsDownstreamHandler(messageService, deviceService, connectionManager);
public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
IotMessageBus messageBus) {
return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
}
}
/**
* IoT 网关 CoAP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
@Slf4j
public static class CoapProtocolConfiguration {
@Bean
public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
}
@Bean
public IotMqttWsDownstreamSubscriber iotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol mqttWsUpstreamProtocol,
IotMqttWsDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
return new IotMqttWsDownstreamSubscriber(mqttWsUpstreamProtocol, downstreamHandler, messageBus);
public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
IotMessageBus messageBus) {
return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
}
}
/**
* IoT 网关 WebSocket 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
@Slf4j
public static class WebSocketProtocolConfiguration {
@Bean(name = "websocketVertx", destroyMethod = "close")
public Vertx websocketVertx() {
return Vertx.vertx();
}
@Bean
public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
@Qualifier("websocketVertx") Vertx websocketVertx) {
return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
deviceService, messageService, connectionManager, websocketVertx);
}
@Bean
public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}

View File

@@ -93,6 +93,21 @@ public class IotGatewayProperties {
*/
private MqttWsProperties mqttWs;
/**
* UDP 组件配置
*/
private UdpProperties udp;
/**
* CoAP 组件配置
*/
private CoapProperties coap;
/**
* WebSocket 组件配置
*/
private WebSocketProperties websocket;
}
@Data
@@ -503,4 +518,129 @@ public class IotGatewayProperties {
}
@Data
public static class UdpProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务端口(默认 8093
*/
private Integer port = 8093;
/**
* 接收缓冲区大小(默认 64KB
*/
private Integer receiveBufferSize = 65536;
/**
* 发送缓冲区大小(默认 64KB
*/
private Integer sendBufferSize = 65536;
/**
* 会话超时时间(毫秒,默认 60 秒)
* <p>
* 用于清理不活跃的设备地址映射
*/
private Long sessionTimeoutMs = 60000L;
/**
* 会话清理间隔(毫秒,默认 30 秒)
*/
private Long sessionCleanIntervalMs = 30000L;
}
@Data
public static class CoapProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务端口CoAP 默认端口 5683
*/
@NotNull(message = "服务端口不能为空")
private Integer port = 5683;
/**
* 最大消息大小(字节)
*/
@NotNull(message = "最大消息大小不能为空")
private Integer maxMessageSize = 1024;
/**
* ACK 超时时间(毫秒)
*/
@NotNull(message = "ACK 超时时间不能为空")
private Integer ackTimeout = 2000;
/**
* 最大重传次数
*/
@NotNull(message = "最大重传次数不能为空")
private Integer maxRetransmit = 4;
}
@Data
public static class WebSocketProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务器端口默认8094
*/
private Integer port = 8094;
/**
* WebSocket 路径(默认:/ws
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/ws";
/**
* 最大消息大小(字节,默认 64KB
*/
private Integer maxMessageSize = 65536;
/**
* 最大帧大小(字节,默认 64KB
*/
private Integer maxFrameSize = 65536;
/**
* 空闲超时时间(秒,默认 60
*/
private Integer idleTimeoutSeconds = 60;
/**
* 是否启用 SSLwss://
*/
private Boolean sslEnabled = false;
/**
* SSL 证书路径
*/
private String sslCertPath;
/**
* SSL 私钥路径
*/
private String sslKeyPath;
}
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 CoAP 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotCoapUpstreamProtocol protocol;
private final IotMessageBus messageBus;
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
}
}

View File

@@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.*;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关 CoAP 协议:接收设备上行消息
*
* 基于 Eclipse Californium 实现,支持:
* 1. 认证POST /auth
* 2. 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 3. 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapUpstreamProtocol {
private final IotGatewayProperties.CoapProperties coapProperties;
private CoapServer coapServer;
@Getter
private final String serverId;
public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
this.coapProperties = coapProperties;
this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
}
@PostConstruct
public void start() {
try {
// 1.1 创建网络配置Californium 3.x API
Configuration config = Configuration.createStandardWithoutFile();
config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
// 1.2 创建 CoAP 服务器
coapServer = new CoapServer(config);
// 2.1 添加 /auth 认证资源
IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
coapServer.add(authResource);
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
authResource.add(new CoapResource("register") {{
add(registerResource);
}});
// 2.3 添加 /topic 根资源(用于上行消息)
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
coapServer.add(topicResource);
// 3. 启动服务器
coapServer.start();
log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
} catch (Exception e) {
log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (coapServer != null) {
try {
coapServer.stop();
log.info("[stop][IoT 网关 CoAP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
}
}
}
}

View File

@@ -0,0 +1,13 @@
/**
* CoAP 协议实现包
* <p>
* 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
* <p>
* URI 路径:
* - 认证POST /auth
* - 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* - 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
* <p>
* Token 通过 CoAP Option 2088 携带
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;

View File

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

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
/**
* IoT 网关 CoAP 协议的认证资源(/auth
*
* 设备通过此资源进行认证,获取 Token
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapAuthResource extends CoapResource {
public static final String PATH = "auth";
private final IotCoapUpstreamProtocol protocol;
private final IotCoapAuthHandler authHandler;
public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
IotCoapAuthHandler authHandler) {
super(PATH);
this.protocol = protocol;
this.authHandler = authHandler;
log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
}
@Override
public void handlePOST(CoapExchange exchange) {
log.debug("[handlePOST][收到 /auth POST 请求]");
authHandler.handle(exchange, protocol);
}
}

View File

@@ -0,0 +1,98 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.Map;
/**
* IoT 网关 CoAP 协议的【设备动态注册】处理器
* <p>
* 用于直连设备/网关的一型一密动态注册,不需要认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
* @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler
*/
@Slf4j
public class IotCoapRegisterHandler {
private final IotDeviceCommonApi deviceApi;
public IotCoapRegisterHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
/**
* 处理设备动态注册请求
*
* @param exchange CoAP 交换对象
*/
@SuppressWarnings("unchecked")
public void handle(CoapExchange exchange) {
try {
// 1.1 解析请求体
byte[] payload = exchange.getRequestPayload();
if (payload == null || payload.length == 0) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
return;
}
Map<String, Object> body;
try {
body = JsonUtils.parseObject(new String(payload), Map.class);
} catch (Exception e) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
return;
}
// 1.2 解析参数
String productKey = MapUtil.getStr(body, "productKey");
if (StrUtil.isEmpty(productKey)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
return;
}
String deviceName = MapUtil.getStr(body, "deviceName");
if (StrUtil.isEmpty(deviceName)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
return;
}
String productSecret = MapUtil.getStr(body, "productSecret");
if (StrUtil.isEmpty(productSecret)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
return;
}
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey)
.setDeviceName(deviceName)
.setProductSecret(productSecret);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
if (result.isError()) {
log.warn("[handle][设备动态注册失败productKey: {}, deviceName: {}, 错误: {}]",
productKey, deviceName, result.getMsg());
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
"设备动态注册失败:" + result.getMsg());
return;
}
// 3. 返回成功响应
log.info("[handle][设备动态注册成功productKey: {}, deviceName: {}]", productKey, deviceName);
IotCoapUtils.respondSuccess(exchange, result.getData());
} catch (Exception e) {
log.error("[handle][设备动态注册处理异常]", e);
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
}
}
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
/**
* IoT 网关 CoAP 协议的设备动态注册资源(/auth/register/device
* <p>
* 用于直连设备/网关的一型一密动态注册,不需要认证
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapRegisterResource extends CoapResource {
public static final String PATH = "device";
private final IotCoapRegisterHandler registerHandler;
public IotCoapRegisterResource(IotCoapRegisterHandler registerHandler) {
super(PATH);
this.registerHandler = registerHandler;
log.info("[IotCoapRegisterResource][创建 CoAP 设备动态注册资源: /auth/register/{}]", PATH);
}
@Override
public void handlePOST(CoapExchange exchange) {
log.debug("[handlePOST][收到设备动态注册请求]");
registerHandler.handle(exchange);
}
}

View File

@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
/**
* IoT 网关 CoAP 协议的【上行】处理器
*
* 处理设备通过 CoAP 协议发送的上行消息,包括:
* 1. 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 2. 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* Token 通过自定义 CoAP Option 2088 携带
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapUpstreamHandler {
private final IotDeviceTokenService deviceTokenService;
private final IotDeviceMessageService deviceMessageService;
public IotCoapUpstreamHandler() {
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
/**
* 处理 CoAP 请求
*
* @param exchange CoAP 交换对象
* @param protocol 协议对象
*/
public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
try {
// 1. 解析通用参数
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = CollUtil.get(uriPath, 2);
String deviceName = CollUtil.get(uriPath, 3);
byte[] payload = exchange.getRequestPayload();
if (StrUtil.isEmpty(productKey)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
return;
}
if (StrUtil.isEmpty(deviceName)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
return;
}
if (ArrayUtil.isEmpty(payload)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
return;
}
// 2. 认证:从自定义 Option 获取 token
String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
if (StrUtil.isEmpty(token)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
return;
}
// 验证 token
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
if (deviceInfo == null) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
return;
}
// 验证设备信息匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
return;
}
// 2.1 解析 methoddeviceName 后面的路径,用 . 拼接
// 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
// 2.2 解码消息
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
if (ObjUtil.notEqual(method, message.getMethod())) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
return;
}
// 2.3 发送消息到消息总线
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
// 3. 返回成功响应
IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
} catch (Exception e) {
log.error("[handle][CoAP 请求处理异常]", e);
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
}
}
}

View File

@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
/**
* IoT 网关 CoAP 协议的【上行】Topic 资源
*
* 支持任意深度的路径匹配:
* - /topic/sys/{productKey}/{deviceName}/thing/property/post
* - /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapUpstreamTopicResource extends CoapResource {
public static final String PATH = "topic";
private final IotCoapUpstreamProtocol protocol;
private final IotCoapUpstreamHandler upstreamHandler;
/**
* 创建根资源(/topic
*/
public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
IotCoapUpstreamHandler upstreamHandler) {
this(PATH, protocol, upstreamHandler);
log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
}
/**
* 创建子资源(动态路径)
*/
private IotCoapUpstreamTopicResource(String name,
IotCoapUpstreamProtocol protocol,
IotCoapUpstreamHandler upstreamHandler) {
super(name);
this.protocol = protocol;
this.upstreamHandler = upstreamHandler;
}
@Override
public Resource getChild(String name) {
// 递归创建动态子资源,支持任意深度路径
return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
}
@Override
public void handleGET(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
}
@Override
public void handlePOST(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
}
@Override
public void handlePUT(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
}
}

View File

@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.server.resources.CoapExchange;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
/**
* IoT CoAP 协议工具类
*
* @author 芋道源码
*/
public class IotCoapUtils {
/**
* 自定义 CoAP Option 编号,用于携带 Token
* <p>
* CoAP Option 范围 2048-65535 属于实验/自定义范围
*/
public static final int OPTION_TOKEN = 2088;
/**
* 返回成功响应
*
* @param exchange CoAP 交换对象
* @param data 响应数据
*/
public static void respondSuccess(CoapExchange exchange, Object data) {
CommonResult<Object> result = CommonResult.success(data);
String json = JsonUtils.toJsonString(result);
exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
}
/**
* 返回错误响应
*
* @param exchange CoAP 交换对象
* @param code CoAP 响应码
* @param message 错误消息
*/
public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
int errorCode = mapCoapCodeToErrorCode(code);
CommonResult<Object> result = CommonResult.error(errorCode, message);
String json = JsonUtils.toJsonString(result);
exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
}
/**
* 从自定义 CoAP Option 中获取 Token
*
* @param exchange CoAP 交换对象
* @param optionNumber Option 编号
* @return Token 值,如果不存在则返回 null
*/
public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
o -> o.getNumber() == optionNumber);
return option != null ? new String(option.getValue()) : null;
}
/**
* 将 CoAP 响应码映射到业务错误码
*
* @param code CoAP 响应码
* @return 业务错误码
*/
public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
if (code == CoAP.ResponseCode.BAD_REQUEST) {
return BAD_REQUEST.getCode();
} else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
return UNAUTHORIZED.getCode();
} else if (code == CoAP.ResponseCode.FORBIDDEN) {
return FORBIDDEN.getCode();
} else {
return INTERNAL_SERVER_ERROR.getCode();
}
}
}

View File

@@ -0,0 +1 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;

View File

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.json.JsonObject;
@@ -103,7 +104,7 @@ public class IotEmqxAuthEventHandler {
JsonObject body = null;
try {
// 1. 解析请求体
body = parseRequestBody(context);
body = parseEventRequestBody(context);
if (body == null) {
return;
}
@@ -152,7 +153,9 @@ public class IotEmqxAuthEventHandler {
}
/**
* 解析请求体
* 解析认证接口请求体
* <p>
* 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
*
* @param context 路由上下文
* @return 请求体JSON对象解析失败时返回null
@@ -173,6 +176,30 @@ public class IotEmqxAuthEventHandler {
}
}
/**
* 解析事件接口请求体
* <p>
* 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范)
*
* @param context 路由上下文
* @return 请求体JSON对象解析失败时返回null
*/
private JsonObject parseEventRequestBody(RoutingContext context) {
try {
JsonObject body = context.body().asJsonObject();
if (body == null) {
log.info("[parseEventRequestBody][请求体为空]");
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
return null;
}
return body;
} catch (Exception e) {
log.error("[parseEventRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
return null;
}
}
/**
* 执行设备认证
*
@@ -201,7 +228,7 @@ public class IotEmqxAuthEventHandler {
*/
private void handleDeviceStateChange(String username, boolean online) {
// 1. 解析设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
return;

View File

@@ -3,8 +3,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
@@ -22,31 +23,36 @@ import lombok.extern.slf4j.Slf4j;
* @author 芋道源码
*/
@Slf4j
public class IotHttpUpstreamProtocol extends AbstractVerticle {
public class IotHttpUpstreamProtocol {
private final IotGatewayProperties.HttpProperties httpProperties;
private final Vertx vertx;
private HttpServer httpServer;
@Getter
private final String serverId;
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties) {
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) {
this.httpProperties = httpProperties;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
}
@Override
@PostConstruct
public void start() {
// 创建路由
Vertx vertx = Vertx.vertx();
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
@@ -70,7 +76,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
}
}
@Override
@PreDestroy
public void stop() {
if (httpServer != null) {

View File

@@ -0,0 +1,6 @@
/**
* HTTP 协议实现包
* <p>
* 提供基于 Vert.x HTTP Server 的 IoT 设备连接和消息处理功能
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.http;

View File

@@ -7,7 +7,8 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
private void beforeHandle(RoutingContext context) {
// 如果不需要认证,则不走前置处理
String path = context.request().path();
if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) {
if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
return;
}
@@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
}
// 校验 token
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token);
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
Assert.notNull(deviceInfo, "设备信息不能为空");
// 校验设备信息是否匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())

View File

@@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -51,6 +51,9 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
String clientId = body.getString("clientId");
if (StrUtil.isEmpty(clientId)) {
throw invalidParamException("clientId 不能为空");
@@ -72,7 +75,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username);
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空位");

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* IoT 网关 HTTP 协议的【设备动态注册】处理器
* <p>
* 用于直连设备/网关的一型一密动态注册,不需要认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
public static final String PATH = "/auth/register/device";
private final IotDeviceCommonApi deviceApi;
public IotHttpRegisterHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
String productKey = body.getString("productKey");
if (StrUtil.isEmpty(productKey)) {
throw invalidParamException("productKey 不能为空");
}
String deviceName = body.getString("deviceName");
if (StrUtil.isEmpty(deviceName)) {
throw invalidParamException("deviceName 不能为空");
}
String productSecret = body.getString("productSecret");
if (StrUtil.isEmpty(productSecret)) {
throw invalidParamException("productSecret 不能为空");
}
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
result.checkError();
// 3. 返回结果
return success(result.getData());
}
}

View File

@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* IoT 网关 HTTP 协议的【子设备动态注册】处理器
* <p>
* 用于子设备的动态注册,需要网关认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
/**
* 路径:/auth/register/sub-device/:productKey/:deviceName
* <p>
* productKey 和 deviceName 是网关设备的标识
*/
public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
private final IotDeviceCommonApi deviceApi;
public IotHttpRegisterSubHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析通用参数
String productKey = context.pathParam("productKey");
String deviceName = context.pathParam("deviceName");
// 2. 解析子设备列表
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
if (body.getJsonArray("params") == null) {
throw invalidParamException("params 不能为空");
}
List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
// 3. 调用子设备动态注册
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
.setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
result.checkError();
// 4. 返回结果
return success(result.getData());
}
}

View File

@@ -12,6 +12,8 @@ import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* IoT 网关 HTTP 协议的【上行】处理器
*
@@ -40,6 +42,9 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
// 2.1 解析消息
if (context.body().buffer() == null) {
throw invalidParamException("请求体不能为空");
}
byte[] bytes = context.body().buffer().getBytes();
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
productKey, deviceName);

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager;
import cn.hutool.core.util.StrUtil;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttEndpoint;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -87,9 +88,9 @@ public class IotMqttConnectionManager {
connectionMap.remove(oldEndpoint);
}
// 注册新连接
connectionMap.put(endpoint, connectionInfo);
deviceEndpointMap.put(deviceId, endpoint);
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {}product key: {}device name: {}]",
deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
}
@@ -101,13 +102,12 @@ public class IotMqttConnectionManager {
*/
public void unregisterConnection(MqttEndpoint endpoint) {
ConnectionInfo connectionInfo = connectionMap.remove(endpoint);
if (connectionInfo != null) {
Long deviceId = connectionInfo.getDeviceId();
deviceEndpointMap.remove(deviceId);
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId,
getEndpointAddress(endpoint));
if (connectionInfo == null) {
return;
}
Long deviceId = connectionInfo.getDeviceId();
deviceEndpointMap.remove(deviceId);
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, getEndpointAddress(endpoint));
}
/**
@@ -123,7 +123,7 @@ public class IotMqttConnectionManager {
* @param deviceId 设备 ID
* @return 连接信息
*/
public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
// 通过设备 ID 获取连接端点
MqttEndpoint endpoint = getDeviceEndpoint(deviceId);
if (endpoint == null) {
@@ -166,7 +166,7 @@ public class IotMqttConnectionManager {
}
try {
endpoint.publish(topic, io.vertx.core.buffer.Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
endpoint.publish(topic, Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {}QoS: {}]", deviceId, topic, qos);
return true;
} catch (Exception e) {

View File

@@ -1,18 +1,26 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.mqtt.MqttEndpoint;
@@ -20,6 +28,7 @@ import io.vertx.mqtt.MqttTopicSubscription;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* MQTT 上行消息处理器
@@ -29,6 +38,16 @@ import java.util.List;
@Slf4j
public class IotMqttUpstreamHandler {
/**
* 默认编解码类型MQTT 使用 Alink 协议)
*/
private static final String DEFAULT_CODEC_TYPE = "Alink";
/**
* register 请求的 topic 后缀
*/
private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
private final IotDeviceMessageService deviceMessageService;
private final IotMqttConnectionManager connectionManager;
@@ -84,20 +103,28 @@ public class IotMqttUpstreamHandler {
});
// 4. 设置消息处理器
endpoint.publishHandler(message -> {
endpoint.publishHandler(mqttMessage -> {
try {
processMessage(clientId, message.topicName(), message.payload().getBytes());
// 4.1 根据 topic 判断是否为 register 请求
String topic = mqttMessage.topicName();
byte[] payload = mqttMessage.payload().getBytes();
if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
// register 请求:使用默认编解码器处理(设备可能未注册)
processRegisterMessage(clientId, topic, payload, endpoint);
} else {
// 业务请求:正常处理
processMessage(clientId, topic, payload);
}
// 根据 QoS 级别发送相应的确认消息
if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
// 4.2 根据 QoS 级别发送相应的确认消息
if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
// QoS 1: 发送 PUBACK 确认
endpoint.publishAcknowledge(message.messageId());
} else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
endpoint.publishAcknowledge(mqttMessage.messageId());
} else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
// QoS 2: 发送 PUBREC 确认
endpoint.publishReceived(message.messageId());
endpoint.publishReceived(mqttMessage.messageId());
}
// QoS 0 无需确认
} catch (Exception e) {
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
@@ -160,10 +187,9 @@ public class IotMqttUpstreamHandler {
return;
}
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName
String productKey = topicParts[2];
String deviceName = topicParts[3];
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName
try {
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
if (message == null) {
@@ -171,10 +197,9 @@ public class IotMqttUpstreamHandler {
return;
}
// 4. 处理业务消息(认证已在连接时完成)
log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
productKey, deviceName, message.getMethod());
// 4. 处理业务消息(认证已在连接时完成)
handleBusinessRequest(message, productKey, deviceName);
} catch (Exception e) {
log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
@@ -214,7 +239,7 @@ public class IotMqttUpstreamHandler {
}
// 4. 获取设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
return false;
@@ -245,6 +270,186 @@ public class IotMqttUpstreamHandler {
}
}
/**
* 处理 register 消息(设备动态注册,使用默认编解码器)
*
* @param clientId 客户端 ID
* @param topic 主题
* @param payload 消息内容
* @param endpoint MQTT 连接端点
*/
private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
// 1.1 基础检查
if (ArrayUtil.isEmpty(payload)) {
return;
}
// 1.2 解析主题,获取 productKey 和 deviceName
String[] topicParts = topic.split("/");
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
return;
}
String productKey = topicParts[2];
String deviceName = topicParts[3];
// 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType
IotDeviceMessage message;
try {
message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
if (message == null) {
log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
return;
}
} catch (Exception e) {
log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage(), e);
return;
}
// 3. 处理设备动态注册请求
log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
productKey, deviceName, message.getMethod());
try {
handleRegisterRequest(message, productKey, deviceName, endpoint);
} catch (Exception e) {
log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage(), e);
}
}
/**
* 处理设备动态注册请求(一型一密,不需要 deviceSecret
*
* @param message 消息信息
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param endpoint MQTT 连接端点
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier();
try {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
if (params == null) {
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整");
return;
}
// 2. 调用动态注册 API
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
if (result.isError()) {
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg());
return;
}
// 3. 发送成功响应(包含 deviceSecret
sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData());
log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
params.getDeviceName(), clientId);
} catch (Exception e) {
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常");
}
}
/**
* 解析注册参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 注册参数 DTO解析失败时返回 null
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceRegisterReqDTO()
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceRegisterReqDTO) {
return (IotDeviceRegisterReqDTO) params;
}
// 其他情况尝试 JSON 转换
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
} catch (Exception e) {
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
return null;
}
}
/**
* 发送注册成功响应(包含 deviceSecret
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param requestId 请求 ID
* @param registerResp 注册响应
*/
private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String requestId, IotDeviceRegisterRespDTO registerResp) {
try {
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
// 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
// 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
MqttQoS.AT_LEAST_ONCE, false, false);
log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic);
} catch (Exception e) {
log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]",
endpoint.clientIdentifier(), e);
}
}
/**
* 发送注册错误响应
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param requestId 请求 ID
* @param errorMessage 错误消息
*/
private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String requestId, String errorMessage) {
try {
// 1. 构建响应消息
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage);
// 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
// 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
MqttQoS.AT_LEAST_ONCE, false, false);
log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic);
} catch (Exception e) {
log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]",
endpoint.clientIdentifier(), e);
}
}
/**
* 处理业务请求
*/
@@ -257,9 +462,7 @@ public class IotMqttUpstreamHandler {
/**
* 注册连接
*/
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device,
String clientId) {
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
.setDeviceId(device.getId())
.setProductKey(device.getProductKey())
@@ -267,7 +470,6 @@ public class IotMqttUpstreamHandler {
.setClientId(clientId)
.setAuthenticated(true)
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
}
@@ -296,15 +498,13 @@ public class IotMqttUpstreamHandler {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]",
connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
}
// 注销连接
connectionManager.unregisterConnection(endpoint);
} catch (Exception e) {
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
endpoint.clientIdentifier(), e.getMessage());
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
}
}

View File

@@ -1,79 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
/**
* IoT MQTT WebSocket 下行消息订阅器
* <p>
* 订阅消息总线的设备下行消息,并通过 WebSocket 发送到设备
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttWsDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotMqttWsUpstreamProtocol upstreamProtocol;
private final IotMqttWsDownstreamHandler downstreamHandler;
private final IotMessageBus messageBus;
public IotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol upstreamProtocol,
IotMqttWsDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
this.upstreamProtocol = upstreamProtocol;
this.downstreamHandler = downstreamHandler;
this.messageBus = messageBus;
}
@PostConstruct
public void init() {
messageBus.register(this);
log.info("[init][MQTT WebSocket 下行消息订阅器已启动topic: {}]", getTopic());
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
log.debug("[onMessage][收到下行消息deviceId: {}method: {}]",
message.getDeviceId(), message.getMethod());
try {
// 1. 校验
String method = message.getMethod();
if (StrUtil.isBlank(method)) {
log.warn("[onMessage][消息方法为空deviceId: {}]", message.getDeviceId());
return;
}
// 2. 委托给下行处理器处理业务逻辑
boolean success = downstreamHandler.handleDownstreamMessage(message);
if (success) {
log.debug("[onMessage][下行消息处理成功deviceId: {}method: {}]",
message.getDeviceId(), message.getMethod());
} else {
log.warn("[onMessage][下行消息处理失败deviceId: {}method: {}]",
message.getDeviceId(), message.getMethod());
}
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败deviceId: {}method: {}]",
message.getDeviceId(), message.getMethod(), e);
}
}
}

View File

@@ -1,146 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.ServerWebSocket;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT WebSocket 协议:接收设备上行消息
* <p>
* 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持:
* - 标准 MQTT 3.1.1 协议
* - WebSocket 协议升级
* - SSL/TLS 加密wss://
* - 设备认证与连接管理
* - QoS 0/1/2 消息质量保证
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttWsUpstreamProtocol {
private final IotGatewayProperties.MqttWsProperties mqttWsProperties;
private final IotDeviceMessageService messageService;
private final IotMqttWsConnectionManager connectionManager;
private final Vertx vertx;
@Getter
private final String serverId;
private HttpServer httpServer;
public IotMqttWsUpstreamProtocol(IotGatewayProperties.MqttWsProperties mqttWsProperties,
IotDeviceMessageService messageService,
IotMqttWsConnectionManager connectionManager,
Vertx vertx) {
this.mqttWsProperties = mqttWsProperties;
this.messageService = messageService;
this.connectionManager = connectionManager;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(mqttWsProperties.getPort());
}
@PostConstruct
public void start() {
// 创建 HTTP 服务器选项
HttpServerOptions options = new HttpServerOptions()
.setPort(mqttWsProperties.getPort())
.setIdleTimeout(mqttWsProperties.getKeepAliveTimeoutSeconds())
.setMaxWebSocketFrameSize(mqttWsProperties.getMaxFrameSize())
.setMaxWebSocketMessageSize(mqttWsProperties.getMaxMessageSize())
// 配置 WebSocket 子协议支持
.addWebSocketSubProtocol(mqttWsProperties.getSubProtocol());
// 配置 SSL如果启用
if (Boolean.TRUE.equals(mqttWsProperties.getSslEnabled())) {
options.setSsl(true)
.setKeyCertOptions(mqttWsProperties.getSslOptions().getKeyCertOptions())
.setTrustOptions(mqttWsProperties.getSslOptions().getTrustOptions());
log.info("[start][MQTT WebSocket 已启用 SSL/TLS (wss://)]");
}
// 创建 HTTP 服务器
httpServer = vertx.createHttpServer(options);
// 设置 WebSocket 处理器
httpServer.webSocketHandler(this::handleWebSocketConnection);
// 启动服务器
try {
httpServer.listen().result();
log.info("[start][IoT 网关 MQTT WebSocket 协议启动成功,端口: {},路径: {},支持子协议: {}]",
mqttWsProperties.getPort(), mqttWsProperties.getPath(),
"mqtt, mqttv3.1, " + mqttWsProperties.getSubProtocol());
} catch (Exception e) {
log.error("[start][IoT 网关 MQTT WebSocket 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (httpServer != null) {
try {
// 关闭所有连接
connectionManager.closeAllConnections();
// 关闭服务器
httpServer.close().result();
log.info("[stop][IoT 网关 MQTT WebSocket 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 MQTT WebSocket 协议停止失败]", e);
}
}
}
/**
* 处理 WebSocket 连接请求
*
* @param socket WebSocket 连接
*/
private void handleWebSocketConnection(ServerWebSocket socket) {
String path = socket.path();
String subProtocol = socket.subProtocol();
log.info("[handleWebSocketConnection][收到 WebSocket 连接请求path: {}subProtocol: {}remoteAddress: {}]",
path, subProtocol, socket.remoteAddress());
// 验证路径
if (!mqttWsProperties.getPath().equals(path)) {
log.warn("[handleWebSocketConnection][WebSocket 路径不匹配拒绝连接path: {},期望: {}]",
path, mqttWsProperties.getPath());
socket.close();
return;
}
// 验证子协议
// Vert.x 已经自动进行了子协议协商,这里只需要验证是否为 MQTT 相关协议
if (subProtocol != null && !subProtocol.startsWith("mqtt")) {
log.warn("[handleWebSocketConnection][WebSocket 子协议不支持拒绝连接subProtocol: {}]", subProtocol);
socket.close();
return;
}
log.info("[handleWebSocketConnection][WebSocket 连接已接受remoteAddress: {}subProtocol: {}]",
socket.remoteAddress(), subProtocol);
// 创建处理器并处理连接
IotMqttWsUpstreamHandler handler = new IotMqttWsUpstreamHandler(
this, messageService, connectionManager);
handler.handle(socket);
}
}

View File

@@ -1,259 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager;
import cn.hutool.core.collection.CollUtil;
import io.vertx.core.http.ServerWebSocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* IoT MQTT WebSocket 连接管理器
*
* @author 芋道源码
*/
@Slf4j
@Component
public class IotMqttWsConnectionManager {
/**
* 存储设备连接
* Key: 设备标识deviceKey
* Value: WebSocket 连接
*/
private final Map<String, ServerWebSocket> connections = new ConcurrentHashMap<>();
/**
* 存储设备标识与 Socket ID 的映射
* Key: 设备标识deviceKey
* Value: Socket IDUUID
*/
private final Map<String, String> deviceKeyToSocketId = new ConcurrentHashMap<>();
/**
* 存储 Socket ID 与设备标识的映射
* Key: Socket IDUUID
* Value: 设备标识deviceKey
*/
private final Map<String, String> socketIdToDeviceKey = new ConcurrentHashMap<>();
/**
* 存储设备订阅的主题
* Key: 设备标识deviceKey
* Value: 订阅的主题集合
*/
private final Map<String, Set<String>> deviceSubscriptions = new ConcurrentHashMap<>();
/**
* 添加连接
*
* @param deviceKey 设备标识
* @param socket WebSocket 连接
* @param socketId Socket IDUUID
*/
public void addConnection(String deviceKey, ServerWebSocket socket, String socketId) {
connections.put(deviceKey, socket);
deviceKeyToSocketId.put(deviceKey, socketId);
socketIdToDeviceKey.put(socketId, deviceKey);
log.info("[addConnection][设备连接已添加deviceKey: {}socketId: {},当前连接数: {}]",
deviceKey, socketId, connections.size());
}
/**
* 移除连接
*
* @param deviceKey 设备标识
*/
public void removeConnection(String deviceKey) {
ServerWebSocket socket = connections.remove(deviceKey);
String socketId = deviceKeyToSocketId.remove(deviceKey);
if (socketId != null) {
socketIdToDeviceKey.remove(socketId);
}
if (socket != null) {
log.info("[removeConnection][设备连接已移除deviceKey: {}socketId: {},当前连接数: {}]",
deviceKey, socketId, connections.size());
}
}
/**
* 根据 Socket ID 移除连接
*
* @param socketId WebSocket 文本框架 ID
*/
public void removeConnectionBySocketId(String socketId) {
String deviceKey = socketIdToDeviceKey.remove(socketId);
if (deviceKey != null) {
connections.remove(deviceKey);
log.info("[removeConnectionBySocketId][设备连接已移除socketId: {}deviceKey: {},当前连接数: {}]",
socketId, deviceKey, connections.size());
}
}
/**
* 获取连接
*
* @param deviceKey 设备标识
* @return WebSocket 连接
*/
public ServerWebSocket getConnection(String deviceKey) {
return connections.get(deviceKey);
}
/**
* 根据 Socket ID 获取设备标识
*
* @param socketId WebSocket 文本框架 ID
* @return 设备标识
*/
public String getDeviceKeyBySocketId(String socketId) {
return socketIdToDeviceKey.get(socketId);
}
/**
* 检查设备是否在线
*
* @param deviceKey 设备标识
* @return 是否在线
*/
public boolean isOnline(String deviceKey) {
return connections.containsKey(deviceKey);
}
/**
* 获取当前连接数
*
* @return 连接数
*/
public int getConnectionCount() {
return connections.size();
}
/**
* 关闭所有连接
*/
public void closeAllConnections() {
connections.forEach((deviceKey, socket) -> {
try {
socket.close();
log.info("[closeAllConnections][关闭设备连接deviceKey: {}]", deviceKey);
} catch (Exception e) {
log.error("[closeAllConnections][关闭设备连接失败deviceKey: {}]", deviceKey, e);
}
});
connections.clear();
deviceKeyToSocketId.clear();
socketIdToDeviceKey.clear();
deviceSubscriptions.clear();
log.info("[closeAllConnections][所有连接已关闭]");
}
// ==================== 订阅管理方法 ====================
/**
* 添加订阅
*
* @param deviceKey 设备标识
* @param topic 订阅主题
*/
public void addSubscription(String deviceKey, String topic) {
deviceSubscriptions.computeIfAbsent(deviceKey, k -> new CopyOnWriteArraySet<>()).add(topic);
log.debug("[addSubscription][设备订阅主题deviceKey: {}topic: {}]", deviceKey, topic);
}
/**
* 移除订阅
*
* @param deviceKey 设备标识
* @param topic 订阅主题
*/
public void removeSubscription(String deviceKey, String topic) {
Set<String> topics = deviceSubscriptions.get(deviceKey);
if (topics != null) {
topics.remove(topic);
log.debug("[removeSubscription][设备取消订阅deviceKey: {}topic: {}]", deviceKey, topic);
}
}
/**
* 检查设备是否订阅了指定主题
* 支持 MQTT 通配符匹配(+ 和 #
*
* @param deviceKey 设备标识
* @param topic 发布主题
* @return 是否匹配
*/
public boolean isSubscribed(String deviceKey, String topic) {
Set<String> subscriptions = deviceSubscriptions.get(deviceKey);
if (CollUtil.isEmpty(subscriptions)) {
return false;
}
// 检查是否有匹配的订阅
for (String subscription : subscriptions) {
if (topicMatches(subscription, topic)) {
return true;
}
}
return false;
}
/**
* 获取设备的所有订阅
*
* @param deviceKey 设备标识
* @return 订阅主题集合
*/
public Set<String> getSubscriptions(String deviceKey) {
return deviceSubscriptions.get(deviceKey);
}
// TODO @haohao这个方法是不是也可以考虑抽到 IotMqttTopicUtils 里面去哈;感觉更简洁一点?
/**
* MQTT 主题匹配
* 支持通配符:
* - +:匹配单层主题
* - #:匹配多层主题(必须在末尾)
*
* @param subscription 订阅主题(可能包含通配符)
* @param topic 发布主题(不包含通配符)
* @return 是否匹配
*/
private boolean topicMatches(String subscription, String topic) {
// 完全匹配
if (subscription.equals(topic)) {
return true;
}
// 不包含通配符
// TODO @haohao这里要不要枚举下哈+ #
if (!subscription.contains("+") && !subscription.contains("#")) {
return false;
}
String[] subscriptionParts = subscription.split("/");
String[] topicParts = topic.split("/");
int i = 0;
for (; i < subscriptionParts.length && i < topicParts.length; i++) {
String subPart = subscriptionParts[i];
String topicPart = topicParts[i];
// # 匹配剩余所有层级,且必须在末尾
if (subPart.equals("#")) {
return i == subscriptionParts.length - 1;
}
// 不是通配符且不匹配
if (!subPart.equals("+") && !subPart.equals(topicPart)) {
return false;
}
}
// 检查是否都匹配完
return i == subscriptionParts.length && i == topicParts.length;
}
}

View File

@@ -1,15 +0,0 @@
/**
* IoT 网关 MQTT WebSocket 协议实现
* <p>
* 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持:
* - 标准 MQTT 3.1.1 协议
* - WebSocket 协议升级
* - SSL/TLS 加密wss://
* - 设备认证与连接管理
* - QoS 0/1/2 消息质量保证
* - 双向消息通信(上行/下行)
*
* @author 芋道源码
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;

View File

@@ -1,221 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.ServerWebSocket;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IoT MQTT WebSocket 下行消息处理器
* <p>
* 处理从消息总线发送到设备的消息,包括:
* - 属性设置
* - 服务调用
* - 事件通知
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttWsDownstreamHandler {
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceService deviceService;
private final IotMqttWsConnectionManager connectionManager;
/**
* 消息 ID 生成器(用于发布消息)
*/
private final AtomicInteger messageIdGenerator = new AtomicInteger(1);
public IotMqttWsDownstreamHandler(IotDeviceMessageService deviceMessageService,
IotDeviceService deviceService,
IotMqttWsConnectionManager connectionManager) {
this.deviceMessageService = deviceMessageService;
this.deviceService = deviceService;
this.connectionManager = connectionManager;
}
/**
* 处理下行消息
*
* @param message 设备消息
* @return 是否处理成功
*/
public boolean handleDownstreamMessage(IotDeviceMessage message) {
try {
// 1. 基础校验
if (message == null || message.getDeviceId() == null) {
log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
return false;
}
// 2. 获取设备信息
IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
if (deviceInfo == null) {
log.warn("[handleDownstreamMessage][设备不存在,设备 ID{}]", message.getDeviceId());
return false;
}
// 3. 构建设备标识
String deviceKey = deviceInfo.getProductKey() + ":" + deviceInfo.getDeviceName();
// 4. 检查设备是否在线
if (!connectionManager.isOnline(deviceKey)) {
log.warn("[handleDownstreamMessage][设备离线无法发送消息deviceKey: {}]", deviceKey);
return false;
}
// 5. 构建主题
String topic = buildDownstreamTopic(message, deviceInfo);
if (StrUtil.isBlank(topic)) {
log.warn("[handleDownstreamMessage][主题构建失败,设备 ID{},方法:{}]",
message.getDeviceId(), message.getMethod());
return false;
}
// 6. 检查设备是否订阅了该主题
if (!connectionManager.isSubscribed(deviceKey, topic)) {
log.warn("[handleDownstreamMessage][设备未订阅该主题deviceKey: {}topic: {}]", deviceKey, topic);
return false;
}
// 8. 编码消息
byte[] payload = deviceMessageService.encodeDeviceMessage(message,
deviceInfo.getProductKey(), deviceInfo.getDeviceName());
if (payload == null || payload.length == 0) {
log.warn("[handleDownstreamMessage][消息编码失败,设备 ID{}]", message.getDeviceId());
return false;
}
// 9. 发送消息到设备
return sendMessageToDevice(deviceKey, topic, payload, 1);
} catch (Exception e) {
if (message != null) {
log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID{},错误:{}]",
message.getDeviceId(), e.getMessage(), e);
}
return false;
}
}
/**
* 构建下行消息主题
*
* @param message 设备消息
* @param deviceInfo 设备信息
* @return 主题
*/
private String buildDownstreamTopic(IotDeviceMessage message, IotDeviceRespDTO deviceInfo) {
String method = message.getMethod();
if (StrUtil.isBlank(method)) {
return null;
}
// 使用工具类构建主题,支持回复消息处理
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
return IotMqttTopicUtils.buildTopicByMethod(method, deviceInfo.getProductKey(),
deviceInfo.getDeviceName(), isReply);
}
/**
* 发送消息到设备
*
* @param deviceKey 设备标识productKey:deviceName
* @param topic 主题
* @param payload 消息内容
* @param qos QoS 级别
* @return 是否发送成功
*/
private boolean sendMessageToDevice(String deviceKey, String topic, byte[] payload, int qos) {
// 获取设备连接
ServerWebSocket socket = connectionManager.getConnection(deviceKey);
if (socket == null) {
log.warn("[sendMessageToDevice][设备未连接deviceKey: {}]", deviceKey);
return false;
}
try {
int messageId = qos > 0 ? generateMessageId() : 0;
// 手动编码 MQTT PUBLISH 消息
io.netty.buffer.ByteBuf byteBuf = io.netty.buffer.Unpooled.buffer();
// 固定头:消息类型(PUBLISH=3) + DUP(0) + QoS + RETAIN
int fixedHeaderByte1 = 0x30 | (qos << 1); // PUBLISH类型
byteBuf.writeByte(fixedHeaderByte1);
// 计算剩余长度
int topicLength = topic.getBytes().length;
int remainingLength = 2 + topicLength + (qos > 0 ? 2 : 0) + payload.length;
// 写入剩余长度(简化版本,假设小于 128 字节)
if (remainingLength < 128) {
byteBuf.writeByte(remainingLength);
} else {
// 处理大于 127 的情况
int x = remainingLength;
do {
int encodedByte = x % 128;
x = x / 128;
if (x > 0) {
encodedByte = encodedByte | 128;
}
byteBuf.writeByte(encodedByte);
} while (x > 0);
}
// 可变头:主题名称
byteBuf.writeShort(topicLength);
byteBuf.writeBytes(topic.getBytes());
// 可变头:消息 ID仅 QoS > 0 时)
if (qos > 0) {
byteBuf.writeShort(messageId);
}
// 有效载荷
byteBuf.writeBytes(payload);
// 发送
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
byteBuf.release();
socket.writeBinaryMessage(Buffer.buffer(bytes));
log.info("[sendMessageToDevice][消息已发送到设备deviceKey: {}topic: {}qos: {}messageId: {}]",
deviceKey, topic, qos, messageId);
return true;
} catch (Exception e) {
log.error("[sendMessageToDevice][发送消息到设备失败deviceKey: {}topic: {}]", deviceKey, topic, e);
return false;
}
}
/**
* 生成消息 ID
*
* @return 消息 ID
*/
private int generateMessageId() {
int id = messageIdGenerator.getAndIncrement();
// MQTT 消息 ID 范围是 1-65535
// TODO @haohao并发可能有问题
if (id > 65535) {
messageIdGenerator.set(1);
return 1;
}
return id;
}
}

View File

@@ -1,753 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.mqtt.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.ServerWebSocket;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IoT MQTT WebSocket 上行消息处理器
* <p>
* 处理来自设备的 MQTT 消息,包括:
* - CONNECT设备连接认证
* - PUBLISH设备发布消息
* - SUBSCRIBE设备订阅主题
* - UNSUBSCRIBE设备取消订阅
* - PINGREQ心跳请求
* - DISCONNECT设备断开连接
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttWsUpstreamHandler {
private final IotMqttWsUpstreamProtocol upstreamProtocol;
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService messageService;
private final IotMqttWsConnectionManager connectionManager;
/**
* 存储 WebSocket 连接到 Socket ID 的映射
* Key: WebSocket 对象
* Value: Socket IDUUID
*/
private final ConcurrentHashMap<ServerWebSocket, String> socketIdMap = new ConcurrentHashMap<>();
/**
* 存储 Socket ID 对应的设备信息
* Key: Socket IDUUID
* Value: 设备信息
*/
private final ConcurrentHashMap<String, IotDeviceRespDTO> socketDeviceMap = new ConcurrentHashMap<>();
/**
* 存储设备的消息 ID 生成器(用于 QoS > 0 的消息)
*/
private final ConcurrentHashMap<String, AtomicInteger> deviceMessageIdMap = new ConcurrentHashMap<>();
/**
* MQTT 解码通道(用于解析 WebSocket 中的 MQTT 二进制消息)
*/
private final ThreadLocal<EmbeddedChannel> decoderChannelThreadLocal = ThreadLocal
.withInitial(() -> new EmbeddedChannel(new MqttDecoder()));
/**
* MQTT 编码通道(用于编码 MQTT 响应消息)
*/
private final ThreadLocal<EmbeddedChannel> encoderChannelThreadLocal = ThreadLocal
.withInitial(() -> new EmbeddedChannel(MqttEncoder.INSTANCE));
public IotMqttWsUpstreamHandler(IotMqttWsUpstreamProtocol upstreamProtocol,
IotDeviceMessageService messageService,
IotMqttWsConnectionManager connectionManager) {
this.upstreamProtocol = upstreamProtocol;
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.messageService = messageService;
this.connectionManager = connectionManager;
}
/**
* 处理 WebSocket 连接
*
* @param socket WebSocket 连接
*/
public void handle(ServerWebSocket socket) {
// 生成唯一的 Socket ID因为 MQTT 使用二进制协议textHandlerID() 会返回 null
String socketId = IdUtil.simpleUUID();
socketIdMap.put(socket, socketId);
log.info("[handle][WebSocket 连接建立socketId: {}remoteAddress: {}]",
socketId, socket.remoteAddress());
// 设置二进制数据处理器
socket.binaryMessageHandler(buffer -> {
try {
handleMqttMessage(socket, buffer);
} catch (Exception e) {
log.error("[handle][处理 MQTT 消息异常socketId: {}]", socketId, e);
socket.close();
}
});
// 设置关闭处理器
socket.closeHandler(v -> {
socketIdMap.remove(socket);
IotDeviceRespDTO device = socketDeviceMap.remove(socketId);
if (device != null) {
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
connectionManager.removeConnection(deviceKey);
deviceMessageIdMap.remove(deviceKey);
// 发送设备离线消息
sendOfflineMessage(device);
log.info("[handle][WebSocket 连接关闭deviceKey: {}socketId: {}]", deviceKey, socketId);
}
});
// 设置异常处理器
socket.exceptionHandler(e -> {
log.error("[handle][WebSocket 连接异常socketId: {}]", socketId, e);
socketIdMap.remove(socket);
IotDeviceRespDTO device = socketDeviceMap.remove(socketId);
if (device != null) {
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
connectionManager.removeConnection(deviceKey);
deviceMessageIdMap.remove(deviceKey);
}
socket.close();
});
}
/**
* 处理 MQTT 消息
*
* @param socket WebSocket 连接
* @param buffer 消息缓冲区
*/
private void handleMqttMessage(ServerWebSocket socket, Buffer buffer) {
String socketId = socketIdMap.get(socket);
ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer.getBytes());
try {
// 使用 EmbeddedChannel 解码 MQTT 消息
EmbeddedChannel decoderChannel = decoderChannelThreadLocal.get();
decoderChannel.writeInbound(byteBuf.retain());
// 读取解码后的消息
MqttMessage mqttMessage = decoderChannel.readInbound();
if (mqttMessage == null) {
log.warn("[handleMqttMessage][MQTT 消息解码失败socketId: {}]", socketId);
return;
}
MqttMessageType messageType = mqttMessage.fixedHeader().messageType();
log.debug("[handleMqttMessage][收到 MQTT 消息,类型: {}socketId: {}]", messageType, socketId);
// 根据消息类型分发处理
switch (messageType) {
case CONNECT:
handleConnect(socket, (MqttConnectMessage) mqttMessage);
break;
case PUBLISH:
handlePublish(socket, (MqttPublishMessage) mqttMessage);
break;
case PUBACK:
handlePubAck(socket, mqttMessage);
break;
case PUBREC:
handlePubRec(socket, mqttMessage);
break;
case PUBREL:
handlePubRel(socket, mqttMessage);
break;
case PUBCOMP:
handlePubComp(socket, mqttMessage);
break;
case SUBSCRIBE:
handleSubscribe(socket, (MqttSubscribeMessage) mqttMessage);
break;
case UNSUBSCRIBE:
handleUnsubscribe(socket, (MqttUnsubscribeMessage) mqttMessage);
break;
case PINGREQ:
handlePingReq(socket);
break;
case DISCONNECT:
handleDisconnect(socket);
break;
default:
log.warn("[handleMqttMessage][不支持的消息类型: {}socketId: {}]", messageType, socketId);
}
} catch (DecoderException e) {
log.error("[handleMqttMessage][MQTT 消息解码异常socketId: {}]", socketId, e);
socket.close();
} catch (Exception e) {
log.error("[handleMqttMessage][处理 MQTT 消息失败socketId: {}]", socketId, e);
socket.close();
} finally {
byteBuf.release();
}
}
/**
* 处理 CONNECT 消息(设备认证)
*/
private void handleConnect(ServerWebSocket socket, MqttConnectMessage message) {
String socketId = socketIdMap.get(socket);
try {
// 1. 解析 CONNECT 消息
MqttConnectPayload payload = message.payload();
String clientId = payload.clientIdentifier();
String username = payload.userName();
String password = payload.passwordInBytes() != null
? new String(payload.passwordInBytes(), StandardCharsets.UTF_8)
: null;
log.info("[handleConnect][收到 CONNECT 消息clientId: {}username: {}socketId: {}]",
clientId, username, socketId);
// 2. 设备认证
IotDeviceRespDTO device = authenticateDevice(clientId, username, password);
if (device == null) {
log.warn("[handleConnect][设备认证失败clientId: {}socketId: {}]", clientId, socketId);
sendConnAck(socket, MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
socket.close();
return;
}
// 3. 保存设备信息
socketDeviceMap.put(socketId, device);
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
connectionManager.addConnection(deviceKey, socket, socketId);
deviceMessageIdMap.put(deviceKey, new AtomicInteger(1));
log.info("[handleConnect][设备认证成功deviceId: {}deviceKey: {}socketId: {}]",
device.getId(), deviceKey, socketId);
// 4. 发送 CONNACK
sendConnAck(socket, MqttConnectReturnCode.CONNECTION_ACCEPTED);
// 5. 发送设备上线消息
sendOnlineMessage(device);
} catch (Exception e) {
log.error("[handleConnect][处理 CONNECT 消息失败socketId: {}]", socketId, e);
sendConnAck(socket, MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE);
socket.close();
}
}
/**
* 处理 PUBLISH 消息(设备发布消息)
*/
private void handlePublish(ServerWebSocket socket, MqttPublishMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handlePublish][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
try {
// 1. 解析 PUBLISH 消息
MqttFixedHeader fixedHeader = message.fixedHeader();
MqttPublishVariableHeader variableHeader = message.variableHeader();
ByteBuf payload = message.payload();
String topic = variableHeader.topicName();
int messageId = variableHeader.packetId();
MqttQoS qos = fixedHeader.qosLevel();
log.debug("[handlePublish][收到 PUBLISH 消息topic: {}messageId: {}QoS: {}deviceId: {}]",
topic, messageId, qos, device.getId());
// 2. 读取 payload
byte[] payloadBytes = new byte[payload.readableBytes()];
payload.readBytes(payloadBytes);
// 3. 解码并发送消息
IotDeviceMessage deviceMessage = messageService.decodeDeviceMessage(payloadBytes,
device.getProductKey(), device.getDeviceName());
if (deviceMessage != null) {
deviceMessage.setServerId(upstreamProtocol.getServerId());
messageService.sendDeviceMessage(deviceMessage, device.getProductKey(),
device.getDeviceName(), upstreamProtocol.getServerId());
log.info("[handlePublish][设备消息已发送method: {}deviceId: {}]",
deviceMessage.getMethod(), device.getId());
}
// 4. 根据 QoS 级别发送相应的确认消息
if (qos == MqttQoS.AT_LEAST_ONCE) {
// QoS 1发送 PUBACK
sendPubAck(socket, messageId);
} else if (qos == MqttQoS.EXACTLY_ONCE) {
// QoS 2发送 PUBREC
sendPubRec(socket, messageId);
}
// QoS 0 无需确认
} catch (Exception e) {
log.error("[handlePublish][处理 PUBLISH 消息失败deviceId: {}]", device.getId(), e);
}
}
/**
* 处理 PUBACK 消息QoS 1 确认)
*/
private void handlePubAck(ServerWebSocket socket, MqttMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handlePubAck][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
log.debug("[handlePubAck][收到 PUBACKmessageId: {}deviceId: {}]", messageId, device.getId());
}
/**
* 处理 PUBREC 消息QoS 2 第一步确认)
*/
private void handlePubRec(ServerWebSocket socket, MqttMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handlePubRec][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
log.debug("[handlePubRec][收到 PUBRECmessageId: {}deviceId: {}]", messageId, device.getId());
// 发送 PUBREL
sendPubRel(socket, messageId);
}
/**
* 处理 PUBREL 消息QoS 2 第二步)
*/
private void handlePubRel(ServerWebSocket socket, MqttMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handlePubRel][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
log.debug("[handlePubRel][收到 PUBRELmessageId: {}deviceId: {}]", messageId, device.getId());
// 发送 PUBCOMP
sendPubComp(socket, messageId);
}
/**
* 处理 PUBCOMP 消息QoS 2 完成确认)
*/
private void handlePubComp(ServerWebSocket socket, MqttMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handlePubComp][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
log.debug("[handlePubComp][收到 PUBCOMPmessageId: {}deviceId: {}]", messageId, device.getId());
}
/**
* 处理 SUBSCRIBE 消息(设备订阅主题)
*/
private void handleSubscribe(ServerWebSocket socket, MqttSubscribeMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handleSubscribe][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
try {
// 1. 解析 SUBSCRIBE 消息
int messageId = message.variableHeader().messageId();
MqttSubscribePayload payload = message.payload();
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
log.info("[handleSubscribe][设备订阅请求deviceKey: {}messageId: {},主题数量: {}]",
deviceKey, messageId, payload.topicSubscriptions().size());
// 2. 构建 QoS 列表并记录订阅信息
int[] grantedQosList = new int[payload.topicSubscriptions().size()];
for (int i = 0; i < payload.topicSubscriptions().size(); i++) {
MqttTopicSubscription subscription = payload.topicSubscriptions().get(i);
String topic = subscription.topicFilter();
grantedQosList[i] = subscription.qualityOfService().value();
// 记录订阅信息到连接管理器
connectionManager.addSubscription(deviceKey, topic);
log.info("[handleSubscribe][订阅主题: {}QoS: {}deviceKey: {}]",
topic, subscription.qualityOfService(), deviceKey);
}
// 3. 发送 SUBACK
sendSubAck(socket, messageId, grantedQosList);
} catch (Exception e) {
log.error("[handleSubscribe][处理 SUBSCRIBE 消息失败deviceId: {}]", device.getId(), e);
}
}
/**
* 处理 UNSUBSCRIBE 消息(设备取消订阅)
*/
private void handleUnsubscribe(ServerWebSocket socket, MqttUnsubscribeMessage message) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handleUnsubscribe][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
try {
// 1. 解析 UNSUBSCRIBE 消息
int messageId = message.variableHeader().messageId();
MqttUnsubscribePayload payload = message.payload();
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
log.info("[handleUnsubscribe][设备取消订阅deviceKey: {}messageId: {},主题数量: {}]",
deviceKey, messageId, payload.topics().size());
// 2. 移除订阅信息
for (String topic : payload.topics()) {
connectionManager.removeSubscription(deviceKey, topic);
log.info("[handleUnsubscribe][取消订阅主题: {}deviceKey: {}]", topic, deviceKey);
}
// 3. 发送 UNSUBACK
sendUnsubAck(socket, messageId);
} catch (Exception e) {
log.error("[handleUnsubscribe][处理 UNSUBSCRIBE 消息失败deviceId: {}]", device.getId(), e);
}
}
/**
* 处理 PINGREQ 消息(心跳请求)
*/
private void handlePingReq(ServerWebSocket socket) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
if (device == null) {
log.warn("[handlePingReq][设备未认证socketId: {}]", socketId);
socket.close();
return;
}
log.debug("[handlePingReq][收到心跳请求deviceId: {}]", device.getId());
// 发送 PINGRESP
sendPingResp(socket);
}
/**
* 处理 DISCONNECT 消息(设备断开连接)
*/
private void handleDisconnect(ServerWebSocket socket) {
String socketId = socketIdMap.get(socket);
IotDeviceRespDTO device = socketDeviceMap.remove(socketId);
if (device != null) {
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
connectionManager.removeConnection(deviceKey);
deviceMessageIdMap.remove(deviceKey);
sendOfflineMessage(device);
log.info("[handleDisconnect][设备主动断开连接deviceKey: {}]", deviceKey);
}
socket.close();
}
// ==================== 设备认证和状态相关方法 ====================
/**
* 设备认证
*/
private IotDeviceRespDTO authenticateDevice(String clientId, String username, String password) {
try {
// 1. 参数校验
if (StrUtil.hasEmpty(clientId, username, password)) {
log.warn("[authenticateDevice][认证参数不完整clientId: {}username: {}]", clientId, username);
return null;
}
// 2. 构建认证参数并调用 API
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
.setClientId(clientId)
.setUsername(username)
.setPassword(password);
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
log.warn("[authenticateDevice][设备认证失败clientId: {}]", clientId);
return null;
}
// 3. 获取设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[authenticateDevice][用户名格式不正确username: {}]", username);
return null;
}
IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO()
.setProductKey(deviceInfo.getProductKey())
.setDeviceName(deviceInfo.getDeviceName());
CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO);
if (!deviceResult.isSuccess() || deviceResult.getData() == null) {
log.warn("[authenticateDevice][获取设备信息失败username: {}]", username);
return null;
}
return deviceResult.getData();
} catch (Exception e) {
log.error("[authenticateDevice][设备认证异常clientId: {}]", clientId, e);
return null;
}
}
/**
* 发送设备上线消息
*/
private void sendOnlineMessage(IotDeviceRespDTO device) {
try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
messageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
device.getDeviceName(), upstreamProtocol.getServerId());
log.info("[sendOnlineMessage][设备上线deviceId: {}]", device.getId());
} catch (Exception e) {
log.error("[sendOnlineMessage][发送设备上线消息失败deviceId: {}]", device.getId(), e);
}
}
/**
* 发送设备离线消息
*/
private void sendOfflineMessage(IotDeviceRespDTO device) {
try {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
messageService.sendDeviceMessage(offlineMessage, device.getProductKey(),
device.getDeviceName(), upstreamProtocol.getServerId());
log.info("[sendOfflineMessage][设备离线deviceId: {}]", device.getId());
} catch (Exception e) {
log.error("[sendOfflineMessage][发送设备离线消息失败deviceId: {}]", device.getId(), e);
}
}
// ==================== 发送响应消息的辅助方法 ====================
/**
* 发送 CONNACK 消息
*/
private void sendConnAck(ServerWebSocket socket, MqttConnectReturnCode returnCode) {
try {
// 构建 CONNACK 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttConnAckVariableHeader variableHeader = new MqttConnAckVariableHeader(returnCode, false);
MqttConnAckMessage connAckMessage = new MqttConnAckMessage(fixedHeader, variableHeader);
// 编码并发送
sendMqttMessage(socket, connAckMessage);
log.debug("[sendConnAck][发送 CONNACK 消息returnCode: {}]", returnCode);
} catch (Exception e) {
log.error("[sendConnAck][发送 CONNACK 消息失败]", e);
}
}
/**
* 发送 PUBACK 消息QoS 1 确认)
*/
private void sendPubAck(ServerWebSocket socket, int messageId) {
try {
// 构建 PUBACK 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttMessage pubAckMessage = new MqttMessage(fixedHeader, variableHeader);
// 编码并发送
sendMqttMessage(socket, pubAckMessage);
log.debug("[sendPubAck][发送 PUBACK 消息messageId: {}]", messageId);
} catch (Exception e) {
log.error("[sendPubAck][发送 PUBACK 消息失败messageId: {}]", messageId, e);
}
}
/**
* 发送 PUBREC 消息QoS 2 第一步确认)
*/
private void sendPubRec(ServerWebSocket socket, int messageId) {
try {
// 构建 PUBREC 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttMessage pubRecMessage = new MqttMessage(fixedHeader, variableHeader);
// 编码并发送
sendMqttMessage(socket, pubRecMessage);
log.debug("[sendPubRec][发送 PUBREC 消息messageId: {}]", messageId);
} catch (Exception e) {
log.error("[sendPubRec][发送 PUBREC 消息失败messageId: {}]", messageId, e);
}
}
/**
* 发送 PUBREL 消息QoS 2 第二步)
*/
private void sendPubRel(ServerWebSocket socket, int messageId) {
try {
// 构建 PUBREL 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttMessage pubRelMessage = new MqttMessage(fixedHeader, variableHeader);
// 编码并发送
sendMqttMessage(socket, pubRelMessage);
log.debug("[sendPubRel][发送 PUBREL 消息messageId: {}]", messageId);
} catch (Exception e) {
log.error("[sendPubRel][发送 PUBREL 消息失败messageId: {}]", messageId, e);
}
}
/**
* 发送 PUBCOMP 消息QoS 2 完成确认)
*/
private void sendPubComp(ServerWebSocket socket, int messageId) {
try {
// 构建 PUBCOMP 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttMessage pubCompMessage = new MqttMessage(fixedHeader, variableHeader);
// 编码并发送
sendMqttMessage(socket, pubCompMessage);
log.debug("[sendPubComp][发送 PUBCOMP 消息messageId: {}]", messageId);
} catch (Exception e) {
log.error("[sendPubComp][发送 PUBCOMP 消息失败messageId: {}]", messageId, e);
}
}
/**
* 发送 SUBACK 消息
*/
private void sendSubAck(ServerWebSocket socket, int messageId, int[] grantedQosList) {
try {
// 构建 SUBACK 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttSubAckPayload payload = new MqttSubAckPayload(grantedQosList);
MqttSubAckMessage subAckMessage = new MqttSubAckMessage(fixedHeader, variableHeader, payload);
// 编码并发送
sendMqttMessage(socket, subAckMessage);
log.debug("[sendSubAck][发送 SUBACK 消息messageId: {},主题数量: {}]", messageId, grantedQosList.length);
} catch (Exception e) {
log.error("[sendSubAck][发送 SUBACK 消息失败messageId: {}]", messageId, e);
}
}
/**
* 发送 UNSUBACK 消息
*/
private void sendUnsubAck(ServerWebSocket socket, int messageId) {
try {
// 构建 UNSUBACK 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttUnsubAckMessage unsubAckMessage = new MqttUnsubAckMessage(fixedHeader, variableHeader);
// 编码并发送
sendMqttMessage(socket, unsubAckMessage);
log.debug("[sendUnsubAck][发送 UNSUBACK 消息messageId: {}]", messageId);
} catch (Exception e) {
log.error("[sendUnsubAck][发送 UNSUBACK 消息失败messageId: {}]", messageId, e);
}
}
/**
* 发送 PINGRESP 消息
*/
private void sendPingResp(ServerWebSocket socket) {
try {
// 构建 PINGRESP 消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(
MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessage pingRespMessage = new MqttMessage(fixedHeader);
// 编码并发送
sendMqttMessage(socket, pingRespMessage);
log.debug("[sendPingResp][发送 PINGRESP 消息]");
} catch (Exception e) {
log.error("[sendPingResp][发送 PINGRESP 消息失败]", e);
}
}
/**
* 发送 MQTT 消息到 WebSocket
*/
private void sendMqttMessage(ServerWebSocket socket, MqttMessage mqttMessage) {
ByteBuf byteBuf = null;
try {
// 使用 EmbeddedChannel 编码 MQTT 消息
EmbeddedChannel encoderChannel = encoderChannelThreadLocal.get();
encoderChannel.writeOutbound(mqttMessage);
// 读取编码后的 ByteBuf
byteBuf = encoderChannel.readOutbound();
if (byteBuf != null) {
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
socket.writeBinaryMessage(Buffer.buffer(bytes));
}
} finally {
if (byteBuf != null) {
byteBuf.release();
}
}
}
}

View File

@@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@@ -25,8 +24,6 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic
private final IotDeviceMessageService messageService;
private final IotDeviceService deviceService;
private final IotTcpConnectionManager connectionManager;
private final IotMessageBus messageBus;
@@ -36,8 +33,8 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic
@PostConstruct
public void init() {
// 初始化下游处理器
this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager);
this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager);
// 注册下游订阅者
messageBus.register(this);
log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {}Topic: {}]",
protocol.getServerId(), getTopic());

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -50,9 +51,9 @@ public class IotTcpConnectionManager {
connectionMap.remove(oldSocket);
}
// 注册新连接
connectionMap.put(socket, connectionInfo);
deviceSocketMap.put(deviceId, socket);
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {}product key: {}device name: {}]",
deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
}
@@ -64,12 +65,12 @@ public class IotTcpConnectionManager {
*/
public void unregisterConnection(NetSocket socket) {
ConnectionInfo connectionInfo = connectionMap.remove(socket);
if (connectionInfo != null) {
Long deviceId = connectionInfo.getDeviceId();
deviceSocketMap.remove(deviceId);
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
deviceId, socket.remoteAddress());
if (connectionInfo == null) {
return;
}
Long deviceId = connectionInfo.getDeviceId();
deviceSocketMap.remove(deviceId);
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress());
}
/**
@@ -77,7 +78,7 @@ public class IotTcpConnectionManager {
*/
public boolean isAuthenticated(NetSocket socket) {
ConnectionInfo info = connectionMap.get(socket);
return info != null && info.isAuthenticated();
return info != null;
}
/**
@@ -95,17 +96,11 @@ public class IotTcpConnectionManager {
}
/**
* 检查设备是否在线
* 根据设备 ID 获取连接信息
*/
public boolean isDeviceOnline(Long deviceId) {
return deviceSocketMap.containsKey(deviceId);
}
/**
* 检查设备是否离线
*/
public boolean isDeviceOffline(Long deviceId) {
return !isDeviceOnline(deviceId);
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
NetSocket socket = deviceSocketMap.get(deviceId);
return socket != null ? connectionMap.get(socket) : null;
}
/**
@@ -119,7 +114,7 @@ public class IotTcpConnectionManager {
}
try {
socket.write(io.vertx.core.buffer.Buffer.buffer(data));
socket.write(Buffer.buffer(data));
log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length);
return true;
} catch (Exception e) {
@@ -157,11 +152,6 @@ public class IotTcpConnectionManager {
* 消息编解码类型(认证后确定)
*/
private String codecType;
// TODO @haohao有没可能不要 authenticated 字段,通过 deviceId 或者其他的?进一步简化,想的是哈。
/**
* 是否已认证
*/
private boolean authenticated;
}

View File

@@ -0,0 +1,6 @@
/**
* TCP 协议实现包
* <p>
* 提供基于 Vert.x TCP Server 的 IoT 设备连接和消息处理功能
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;

View File

@@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -19,8 +17,6 @@ public class IotTcpDownstreamHandler {
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceService deviceService;
private final IotTcpConnectionManager connectionManager;
/**
@@ -31,21 +27,16 @@ public class IotTcpDownstreamHandler {
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
// 1.1 获取设备信息
IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
if (deviceInfo == null) {
log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId());
return;
}
// 1.2 检查设备是否在线
if (connectionManager.isDeviceOffline(message.getDeviceId())) {
log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
// 1. 获取连接信息(包含 codecType
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
message.getDeviceId());
if (connectionInfo == null) {
log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
return;
}
// 2. 根据产品 Key 和设备名称编码消息并发送到设备
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
deviceInfo.getDeviceName());
// 2. 使用连接时的 codecType 编码消息并发送到设备
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
if (success) {
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",

View File

@@ -10,7 +10,11 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
@@ -23,6 +27,8 @@ import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* TCP 上行消息处理器
*
@@ -74,6 +80,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
// 设置消息处理器
socket.handler(buffer -> {
// TODO @AITODO @芋艿:这里应该有拆粘包的问题;
try {
processMessage(clientId, buffer, socket);
} catch (Exception e) {
@@ -119,6 +126,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
if (AUTH_METHOD.equals(message.getMethod())) {
// 认证请求
handleAuthenticationRequest(clientId, message, codecType, socket);
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
// 设备动态注册请求
handleRegisterRequest(clientId, message, codecType, socket);
} else {
// 业务消息
handleBusinessRequest(clientId, message, codecType, socket);
@@ -162,7 +172,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
}
// 2.1 解析设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
if (deviceInfo == null) {
sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
return;
@@ -189,6 +199,44 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
}
}
/**
* 处理设备动态注册请求(一型一密,不需要认证)
*
* @param clientId 客户端 ID
* @param message 消息信息
* @param codecType 消息编解码类型
* @param socket 网络连接
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType,
NetSocket socket) {
try {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
if (params == null) {
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType);
return;
}
// 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
if (result.isError()) {
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType);
return;
}
// 3. 发送成功响应(包含 deviceSecret
sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType);
log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]",
clientId, params.getDeviceName());
} catch (Exception e) {
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType);
}
}
/**
* 处理业务请求
*
@@ -229,8 +277,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
private String getMessageCodecType(Buffer buffer, NetSocket socket) {
// 1. 如果已认证,优先使用缓存的编解码类型
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
if (connectionInfo != null && connectionInfo.isAuthenticated() &&
StrUtil.isNotBlank(connectionInfo.getCodecType())) {
if (connectionInfo != null
&& StrUtil.isNotBlank(connectionInfo.getCodecType())) {
return connectionInfo.getCodecType();
}
@@ -254,8 +302,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
.setProductKey(device.getProductKey())
.setDeviceName(device.getDeviceName())
.setClientId(clientId)
.setCodecType(codecType)
.setAuthenticated(true);
.setCodecType(codecType);
// 注册连接
connectionManager.registerConnection(socket, device.getId(), connectionInfo);
}
@@ -375,34 +422,87 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
* @param params 参数对象(通常为 Map 类型)
* @return 认证参数 DTO解析失败时返回 null
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "DuplicatedCode"})
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof java.util.Map) {
java.util.Map<String, Object> paramMap = (java.util.Map<String, Object>) params;
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceAuthReqDTO()
.setClientId(MapUtil.getStr(paramMap, "clientId"))
.setUsername(MapUtil.getStr(paramMap, "username"))
.setPassword(MapUtil.getStr(paramMap, "password"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceAuthReqDTO) {
return (IotDeviceAuthReqDTO) params;
}
// 其他情况尝试 JSON 转换
String jsonStr = JsonUtils.toJsonString(params);
return JsonUtils.parseObject(jsonStr, IotDeviceAuthReqDTO.class);
return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
} catch (Exception e) {
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
return null;
}
}
/**
* 解析注册参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 注册参数 DTO解析失败时返回 null
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceRegisterReqDTO()
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceRegisterReqDTO) {
return (IotDeviceRegisterReqDTO) params;
}
// 其他情况尝试 JSON 转换
String jsonStr = JsonUtils.toJsonString(params);
return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class);
} catch (Exception e) {
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
return null;
}
}
/**
* 发送注册成功响应(包含 deviceSecret
*
* @param socket 网络连接
* @param requestId 请求 ID
* @param registerResp 注册响应
* @param codecType 消息编解码类型
*/
private void sendRegisterSuccessResponse(NetSocket socket, String requestId,
IotDeviceRegisterRespDTO registerResp, String codecType) {
try {
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
// 2. 发送响应
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
socket.write(Buffer.buffer(encodedData));
} catch (Exception e) {
log.error("[sendRegisterSuccessResponse][发送注册成功响应失败requestId: {}]", requestId, e);
}
}
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 UDP 下游订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class IotUdpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotUdpUpstreamProtocol protocol;
private final IotDeviceMessageService messageService;
private final IotUdpSessionManager sessionManager;
private final IotMessageBus messageBus;
private IotUdpDownstreamHandler downstreamHandler;
@PostConstruct
public void init() {
// 初始化下游处理器
this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol);
// 注册下游订阅者
messageBus.register(this);
log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {}Topic: {}]",
protocol.getServerId(), getTopic());
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
try {
downstreamHandler.handle(message);
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId(), e);
}
}
}

View File

@@ -0,0 +1,171 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.core.datagram.DatagramSocket;
import io.vertx.core.datagram.DatagramSocketOptions;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* IoT 网关 UDP 协议:接收设备上行消息
* <p>
* 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能:
* 1. 监听 UDP 端口,接收设备消息
* 2. 定期清理不活跃的设备地址映射
* 3. 提供 UDP Socket 用于下行消息发送
*
* @author 芋道源码
*/
@Slf4j
public class IotUdpUpstreamProtocol {
private final IotGatewayProperties.UdpProperties udpProperties;
private final IotDeviceService deviceService;
private final IotDeviceMessageService messageService;
private final IotUdpSessionManager sessionManager;
private final Vertx vertx;
@Getter
private final String serverId;
@Getter
private DatagramSocket udpSocket;
/**
* 会话清理定时器 ID
*/
private Long cleanTimerId;
private IotUdpUpstreamHandler upstreamHandler;
public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
Vertx vertx) {
this.udpProperties = udpProperties;
this.deviceService = deviceService;
this.messageService = messageService;
this.sessionManager = sessionManager;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort());
}
@PostConstruct
public void start() {
// 1. 初始化上行消息处理器
this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager);
// 2. 创建 UDP Socket 选项
DatagramSocketOptions options = new DatagramSocketOptions()
.setReceiveBufferSize(udpProperties.getReceiveBufferSize())
.setSendBufferSize(udpProperties.getSendBufferSize())
.setReuseAddress(true);
// 3. 创建 UDP Socket
udpSocket = vertx.createDatagramSocket(options);
// 4. 监听端口
udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> {
if (result.failed()) {
log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause());
return;
}
// 设置数据包处理器
udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket));
log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]",
udpProperties.getPort(), udpProperties.getReceiveBufferSize(),
udpProperties.getSendBufferSize());
// 5. 启动会话清理定时器
startSessionCleanTimer();
});
}
@PreDestroy
public void stop() {
// 1. 取消会话清理定时器
if (cleanTimerId != null) {
vertx.cancelTimer(cleanTimerId);
cleanTimerId = null;
log.info("[stop][会话清理定时器已取消]");
}
// 2. 关闭 UDP Socket
if (udpSocket != null) {
try {
udpSocket.close().result();
log.info("[stop][IoT 网关 UDP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 UDP 协议停止失败]", e);
}
}
}
/**
* 启动会话清理定时器
*/
private void startSessionCleanTimer() {
cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> {
try {
// 1. 清理超时的设备地址映射,并获取离线设备列表
List<Long> offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs());
// 2. 为每个离线设备发送离线消息
for (Long deviceId : offlineDeviceIds) {
sendOfflineMessage(deviceId);
}
if (CollUtil.isNotEmpty(offlineDeviceIds)) {
log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size());
}
} catch (Exception e) {
log.error("[cleanExpiredMappings][清理超时会话失败]", e);
}
});
log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms超时{} ms]",
udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs());
}
/**
* 发送设备离线消息
*
* @param deviceId 设备 ID
*/
private void sendOfflineMessage(Long deviceId) {
try {
// 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId);
if (device == null) {
log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId);
return;
}
// 发送离线消息
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
messageService.sendDeviceMessage(offlineMessage, device.getProductKey(),
device.getDeviceName(), serverId);
log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]",
deviceId, device.getDeviceName());
} catch (Exception e) {
log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e);
}
}
}

View File

@@ -0,0 +1,203 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.datagram.DatagramSocket;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT 网关 UDP 会话管理器
* <p>
* 采用无状态设计SessionManager 主要用于:
* 1. 管理设备地址映射(用于下行消息发送)
* 2. 定期清理不活跃的设备地址映射
* <p>
* 注意UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态
*
* @author 芋道源码
*/
@Slf4j
@Component
public class IotUdpSessionManager {
/**
* 设备 ID -> 会话信息(包含地址和 codecType
*/
private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>();
/**
* 设备地址 Key -> 最后活跃时间(用于清理)
*/
private final Map<String, LocalDateTime> lastActiveTimeMap = new ConcurrentHashMap<>();
/**
* 设备地址 Key -> 设备 ID反向映射用于清理时同步
*/
private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>();
/**
* 更新设备会话(每次收到上行消息时调用)
*
* @param deviceId 设备 ID
* @param address 设备地址
* @param codecType 消息编解码类型
*/
public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) {
String addressKey = buildAddressKey(address);
// 更新设备会话映射
deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType));
lastActiveTimeMap.put(addressKey, LocalDateTime.now());
addressDeviceMap.put(addressKey, deviceId);
log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {}codecType: {}]", deviceId, addressKey, codecType);
}
/**
* 更新设备地址(兼容旧接口,默认不更新 codecType
*
* @param deviceId 设备 ID
* @param address 设备地址
*/
public void updateDeviceAddress(Long deviceId, InetSocketAddress address) {
SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null;
updateDeviceSession(deviceId, address, codecType);
}
/**
* 获取设备会话信息
*
* @param deviceId 设备 ID
* @return 会话信息
*/
public SessionInfo getSessionInfo(Long deviceId) {
return deviceSessionMap.get(deviceId);
}
/**
* 检查设备是否在线(即是否有地址映射)
*
* @param deviceId 设备 ID
* @return 是否在线
*/
public boolean isDeviceOnline(Long deviceId) {
return deviceSessionMap.containsKey(deviceId);
}
/**
* 检查设备是否离线
*
* @param deviceId 设备 ID
* @return 是否离线
*/
public boolean isDeviceOffline(Long deviceId) {
return !isDeviceOnline(deviceId);
}
/**
* 发送消息到设备
*
* @param deviceId 设备 ID
* @param data 数据
* @param socket UDP Socket
* @return 是否发送成功
*/
public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) {
SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
if (sessionInfo == null || sessionInfo.getAddress() == null) {
log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId);
return false;
}
InetSocketAddress address = sessionInfo.getAddress();
try {
socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
if (result.succeeded()) {
log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
deviceId, buildAddressKey(address), data.length);
} else {
log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
deviceId, buildAddressKey(address), result.cause());
}
});
return true;
} catch (Exception e) {
log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
return false;
}
}
/**
* 定期清理不活跃的设备地址映射
*
* @param timeoutMs 超时时间(毫秒)
* @return 清理的设备 ID 列表(用于发送离线消息)
*/
public List<Long> cleanExpiredMappings(long timeoutMs) {
List<Long> offlineDeviceIds = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000);
Iterator<Map.Entry<String, LocalDateTime>> iterator = lastActiveTimeMap.entrySet().iterator();
while (iterator.hasNext()) {
// 未过期,跳过
Map.Entry<String, LocalDateTime> entry = iterator.next();
if (entry.getValue().isAfter(expireTime)) {
continue;
}
// 过期处理:记录离线设备 ID
String addressKey = entry.getKey();
Long deviceId = addressDeviceMap.remove(addressKey);
if (deviceId == null) {
iterator.remove();
continue;
}
SessionInfo sessionInfo = deviceSessionMap.remove(deviceId);
if (sessionInfo == null) {
iterator.remove();
continue;
}
offlineDeviceIds.add(deviceId);
log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]",
deviceId, addressKey, entry.getValue());
iterator.remove();
}
return offlineDeviceIds;
}
/**
* 构建地址 Key
*
* @param address 地址
* @return 地址 Key
*/
public String buildAddressKey(InetSocketAddress address) {
return address.getHostString() + ":" + address.getPort();
}
/**
* 会话信息
*/
@Data
public static class SessionInfo {
/**
* 设备地址
*/
private InetSocketAddress address;
/**
* 消息编解码类型
*/
private String codecType;
}
}

View File

@@ -0,0 +1,6 @@
/**
* UDP 协议实现包
* <p>
* 提供基于 Vert.x DatagramSocket 的 IoT 设备连接和消息处理功能
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.datagram.DatagramSocket;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 UDP 下行消息处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotUdpDownstreamHandler {
private final IotDeviceMessageService deviceMessageService;
private final IotUdpSessionManager sessionManager;
private final IotUdpUpstreamProtocol protocol;
public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService,
IotUdpSessionManager sessionManager,
IotUdpUpstreamProtocol protocol) {
this.deviceMessageService = deviceMessageService;
this.sessionManager = sessionManager;
this.protocol = protocol;
}
/**
* 处理下行消息
*
* @param message 下行消息
*/
public void handle(IotDeviceMessage message) {
try {
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
// 1. 获取会话信息(包含 codecType
IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId());
if (sessionInfo == null) {
log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
return;
}
// 2. 使用会话中的 codecType 编码消息,并发送到设备
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType());
DatagramSocket socket = protocol.getUdpSocket();
if (socket == null) {
log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId());
return;
}
boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket);
if (success) {
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
} else {
log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
}
} catch (Exception e) {
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
message.getDeviceId(), message.getMethod(), message, e);
}
}
}

View File

@@ -0,0 +1,542 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.datagram.DatagramPacket;
import io.vertx.core.datagram.DatagramSocket;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Map;
/**
* UDP 上行消息处理器
* <p>
* 采用无状态 Token 机制(每次请求携带 token
* 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password
* 2. 返回 Token服务端验证后返回 JWT token
* 3. 后续请求:每次请求在 params 中携带 token
* 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证
*
* @author 芋道源码
*/
@Slf4j
public class IotUdpUpstreamHandler {
private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
private static final String AUTH_METHOD = "auth";
/**
* Token 参数 Key
*/
private static final String PARAM_KEY_TOKEN = "token";
/**
* Body 参数 Key实际请求内容
*/
private static final String PARAM_KEY_BODY = "body";
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceService deviceService;
private final IotUdpSessionManager sessionManager;
private final IotDeviceTokenService deviceTokenService;
private final IotDeviceCommonApi deviceApi;
private final String serverId;
public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol,
IotDeviceMessageService deviceMessageService,
IotDeviceService deviceService,
IotUdpSessionManager sessionManager) {
this.deviceMessageService = deviceMessageService;
this.deviceService = deviceService;
this.sessionManager = sessionManager;
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.serverId = protocol.getServerId();
}
/**
* 处理 UDP 数据包
*
* @param packet 数据包
* @param socket UDP Socket
*/
public void handle(DatagramPacket packet, DatagramSocket socket) {
InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port());
Buffer data = packet.data();
log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]",
sessionManager.buildAddressKey(senderAddress), data.length());
try {
processMessage(data, senderAddress, socket);
} catch (Exception e) {
log.error("[handle][处理消息失败,来源: {},错误: {}]",
sessionManager.buildAddressKey(senderAddress), e.getMessage(), e);
// UDP 无连接,不需要断开连接,只记录错误
}
}
/**
* 处理消息
*
* @param buffer 消息
* @param senderAddress 发送者地址
* @param socket UDP Socket
*/
private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) {
// 1. 基础检查
if (buffer == null || buffer.length() == 0) {
return;
}
// 2. 获取消息格式类型
String codecType = getMessageCodecType(buffer);
// 3. 解码消息
IotDeviceMessage message;
try {
message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
if (message == null) {
log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress));
sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType);
return;
}
} catch (Exception e) {
log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e);
sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType);
return;
}
// 4. 根据消息类型路由处理
try {
if (AUTH_METHOD.equals(message.getMethod())) {
// 认证请求
handleAuthenticationRequest(message, codecType, senderAddress, socket);
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
// 设备动态注册请求
handleRegisterRequest(message, codecType, senderAddress, socket);
} else {
// 业务消息
handleBusinessRequest(message, codecType, senderAddress, socket);
}
} catch (Exception e) {
log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]",
sessionManager.buildAddressKey(senderAddress), message.getMethod(), e);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType);
}
}
/**
* 处理认证请求
*
* @param message 消息信息
* @param codecType 消息编解码类型
* @param senderAddress 发送者地址
* @param socket UDP Socket
*/
private void handleAuthenticationRequest(IotDeviceMessage message, String codecType,
InetSocketAddress senderAddress, DatagramSocket socket) {
String addressKey = sessionManager.buildAddressKey(senderAddress);
try {
// 1.1 解析认证参数
IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
if (authParams == null) {
log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType);
return;
}
// 1.2 执行认证
if (!validateDeviceAuth(authParams)) {
log.warn("[handleAuthenticationRequest][认证失败,来源: {}username: {}]",
addressKey, authParams.getUsername());
sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType);
return;
}
// 2.1 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
if (deviceInfo == null) {
sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType);
return;
}
// 2.2 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
deviceInfo.getDeviceName());
if (device == null) {
sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType);
return;
}
// 3.1 生成 JWT Token无状态
String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName());
// 3.2 更新设备会话信息(用于下行消息,保存 codecType
sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType);
// 3.3 发送上线消息
sendOnlineMessage(device);
// 3.4 发送成功响应(包含 token
sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType);
log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]",
device.getId(), device.getDeviceName(), addressKey);
} catch (Exception e) {
log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType);
}
}
/**
* 处理设备动态注册请求(一型一密,不需要 Token
*
* @param message 消息信息
* @param codecType 消息编解码类型
* @param senderAddress 发送者地址
* @param socket UDP Socket
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
private void handleRegisterRequest(IotDeviceMessage message, String codecType,
InetSocketAddress senderAddress, DatagramSocket socket) {
String addressKey = sessionManager.buildAddressKey(senderAddress);
try {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
if (params == null) {
log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType);
return;
}
// 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
if (result.isError()) {
log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg());
sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType);
return;
}
// 3. 发送成功响应(包含 deviceSecret
sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType);
log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]",
params.getDeviceName(), addressKey);
} catch (Exception e) {
log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType);
}
}
/**
* 处理业务请求
* <p>
* 请求参数格式:
* - tokenJWT 令牌
* - body实际请求内容可以是 Map、List 或其他类型)
*
* @param message 消息信息
* @param codecType 消息编解码类型
* @param senderAddress 发送者地址
* @param socket UDP Socket
*/
@SuppressWarnings("unchecked")
private void handleBusinessRequest(IotDeviceMessage message, String codecType,
InetSocketAddress senderAddress, DatagramSocket socket) {
String addressKey = sessionManager.buildAddressKey(senderAddress);
try {
// 1.1 从消息中提取 token 和 body格式{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}
String token = null;
Object body = null;
if (message.getParams() instanceof Map) {
Map<String, Object> paramsMap = (Map<String, Object>) message.getParams();
token = (String) paramsMap.get(PARAM_KEY_TOKEN);
body = paramsMap.get(PARAM_KEY_BODY);
}
if (StrUtil.isBlank(token)) {
log.warn("[handleBusinessRequest][缺少 token来源: {}]", addressKey);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType);
return;
}
// 1.2 验证 token获取设备信息
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
if (deviceInfo == null) {
log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType);
return;
}
// 2. 获取设备详细信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
deviceInfo.getDeviceName());
if (device == null) {
log.warn("[handleBusinessRequest][设备不存在,来源: {}productKey: {}deviceName: {}]",
addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType);
return;
}
// 3. 更新设备会话信息(保持最新,保存 codecType
sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType);
// 4. 将 body 设置为实际的 params发送消息到消息总线
message.setParams(body);
deviceMessageService.sendDeviceMessage(message, device.getProductKey(),
device.getDeviceName(), serverId);
log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]",
device.getId(), message.getMethod(), addressKey);
} catch (Exception e) {
log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e);
sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType);
}
}
/**
* 获取消息编解码类型
*
* @param buffer 消息
* @return 消息编解码类型
*/
private String getMessageCodecType(Buffer buffer) {
// 检测消息格式类型
return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY
: CODEC_TYPE_JSON;
}
/**
* 发送设备上线消息
*
* @param device 设备信息
*/
private void sendOnlineMessage(IotDeviceRespDTO device) {
try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
device.getDeviceName(), serverId);
} catch (Exception e) {
log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
}
}
/**
* 验证设备认证信息
*
* @param authParams 认证参数
* @return 是否认证成功
*/
private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
try {
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
.setPassword(authParams.getPassword()));
result.checkError();
return BooleanUtil.isTrue(result.getData());
} catch (Exception e) {
log.error("[validateDeviceAuth][设备认证异常username: {}]", authParams.getUsername(), e);
return false;
}
}
/**
* 发送认证成功响应(包含 token
*
* @param socket UDP Socket
* @param address 目标地址
* @param requestId 请求 ID
* @param token JWT Token
* @param codecType 消息编解码类型
*/
private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address,
String requestId, String token, String codecType) {
try {
// 构建响应数据
Object responseData = MapUtil.builder()
.put("success", true)
.put("token", token)
.put("message", "认证成功")
.build();
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功");
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
// 发送响应
socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
if (result.failed()) {
log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]",
sessionManager.buildAddressKey(address), result.cause());
}
});
} catch (Exception e) {
log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]",
sessionManager.buildAddressKey(address), e);
}
}
/**
* 发送注册成功响应(包含 deviceSecret
*
* @param socket UDP Socket
* @param address 目标地址
* @param requestId 请求 ID
* @param registerResp 注册响应
* @param codecType 消息编解码类型
*/
private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address,
String requestId, IotDeviceRegisterRespDTO registerResp,
String codecType) {
try {
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
// 2. 发送响应
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
if (result.failed()) {
log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]",
sessionManager.buildAddressKey(address), result.cause());
}
});
} catch (Exception e) {
log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]",
sessionManager.buildAddressKey(address), e);
}
}
/**
* 发送错误响应
*
* @param socket UDP Socket
* @param address 目标地址
* @param requestId 请求 ID
* @param errorMessage 错误消息
* @param codecType 消息编解码类型
*/
private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address,
String requestId, String errorMessage, String codecType) {
sendResponse(socket, address, false, errorMessage, requestId, codecType);
}
/**
* 发送响应消息
*
* @param socket UDP Socket
* @param address 目标地址
* @param success 是否成功
* @param message 消息
* @param requestId 请求 ID
* @param codecType 消息编解码类型
*/
@SuppressWarnings("SameParameterValue")
private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success,
String message, String requestId, String codecType) {
try {
// 构建响应数据
Object responseData = MapUtil.builder()
.put("success", success)
.put("message", message)
.build();
int code = success ? 0 : 401;
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
"response", responseData, code, message);
// 发送响应
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> {
if (ar.failed()) {
log.error("[sendResponse][发送响应失败,地址: {}]",
sessionManager.buildAddressKey(address), ar.cause());
}
});
} catch (Exception e) {
log.error("[sendResponse][发送响应异常,地址: {}]",
sessionManager.buildAddressKey(address), e);
}
}
/**
* 解析认证参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 认证参数 DTO解析失败时返回 null
*/
@SuppressWarnings("unchecked")
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceAuthReqDTO()
.setClientId(MapUtil.getStr(paramMap, "clientId"))
.setUsername(MapUtil.getStr(paramMap, "username"))
.setPassword(MapUtil.getStr(paramMap, "password"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceAuthReqDTO) {
return (IotDeviceAuthReqDTO) params;
}
// 其他情况尝试 JSON 转换
return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
} catch (Exception e) {
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
return null;
}
}
/**
* 解析注册参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 注册参数 DTO解析失败时返回 null
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceRegisterReqDTO()
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceRegisterReqDTO) {
return (IotDeviceRegisterReqDTO) params;
}
// 其他情况尝试 JSON 转换
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
} catch (Exception e) {
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
return null;
}
}
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class IotWebSocketDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotWebSocketUpstreamProtocol protocol;
private final IotDeviceMessageService messageService;
private final IotWebSocketConnectionManager connectionManager;
private final IotMessageBus messageBus;
private IotWebSocketDownstreamHandler downstreamHandler;
@PostConstruct
public void init() {
// 初始化下游处理器
this.downstreamHandler = new IotWebSocketDownstreamHandler(messageService, connectionManager);
// 注册下游订阅者
messageBus.register(this);
log.info("[init][WebSocket 下游订阅者初始化完成,服务器 ID: {}Topic: {}]",
protocol.getServerId(), getTopic());
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
try {
downstreamHandler.handle(message);
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId(), e);
}
}
}

View File

@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 WebSocket 协议:接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotWebSocketUpstreamProtocol {
private final IotGatewayProperties.WebSocketProperties wsProperties;
private final IotDeviceService deviceService;
private final IotDeviceMessageService messageService;
private final IotWebSocketConnectionManager connectionManager;
private final Vertx vertx;
@Getter
private final String serverId;
private HttpServer httpServer;
public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
Vertx vertx) {
this.wsProperties = wsProperties;
this.deviceService = deviceService;
this.messageService = messageService;
this.connectionManager = connectionManager;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort());
}
@PostConstruct
@SuppressWarnings("deprecation")
public void start() {
// 1.1 创建服务器选项
HttpServerOptions options = new HttpServerOptions()
.setPort(wsProperties.getPort())
.setIdleTimeout(wsProperties.getIdleTimeoutSeconds())
.setMaxWebSocketFrameSize(wsProperties.getMaxFrameSize())
.setMaxWebSocketMessageSize(wsProperties.getMaxMessageSize());
// 1.2 配置 SSL如果启用
if (Boolean.TRUE.equals(wsProperties.getSslEnabled())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(wsProperties.getSslKeyPath())
.setCertPath(wsProperties.getSslCertPath());
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
// 2. 创建服务器并设置 WebSocket 处理器
httpServer = vertx.createHttpServer(options);
httpServer.webSocketHandler(socket -> {
// 验证路径
if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) {
log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]",
socket.path(), wsProperties.getPath());
socket.reject();
return;
}
// 创建上行处理器
IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this,
messageService, deviceService, connectionManager);
handler.handle(socket);
});
// 3. 启动服务器
try {
httpServer.listen().result();
log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath());
} catch (Exception e) {
log.error("[start][IoT 网关 WebSocket 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (httpServer != null) {
try {
httpServer.close().result();
log.info("[stop][IoT 网关 WebSocket 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e);
}
}
}
}

View File

@@ -0,0 +1,149 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager;
import io.vertx.core.http.ServerWebSocket;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT 网关 WebSocket 连接管理器
* <p>
* 统一管理 WebSocket 连接的认证状态、设备会话和消息发送功能:
* 1. 管理 WebSocket 连接的认证状态
* 2. 管理设备会话和在线状态
* 3. 管理消息发送到设备
*
* @author 芋道源码
*/
@Slf4j
@Component
public class IotWebSocketConnectionManager {
/**
* 连接信息映射ServerWebSocket -> 连接信息
*/
private final Map<ServerWebSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
/**
* 设备 ID -> ServerWebSocket 的映射
*/
private final Map<Long, ServerWebSocket> deviceSocketMap = new ConcurrentHashMap<>();
/**
* 注册设备连接(包含认证信息)
*
* @param socket WebSocket 连接
* @param deviceId 设备 ID
* @param connectionInfo 连接信息
*/
public void registerConnection(ServerWebSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
// 如果设备已有其他连接,先清理旧连接
ServerWebSocket oldSocket = deviceSocketMap.get(deviceId);
if (oldSocket != null && oldSocket != socket) {
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
deviceId, oldSocket.remoteAddress());
oldSocket.close();
// 清理旧连接的映射
connectionMap.remove(oldSocket);
}
// 注册新连接
connectionMap.put(socket, connectionInfo);
deviceSocketMap.put(deviceId, socket);
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {}product key: {}device name: {}]",
deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
}
/**
* 注销设备连接
*
* @param socket WebSocket 连接
*/
public void unregisterConnection(ServerWebSocket socket) {
ConnectionInfo connectionInfo = connectionMap.remove(socket);
if (connectionInfo == null) {
return;
}
Long deviceId = connectionInfo.getDeviceId();
deviceSocketMap.remove(deviceId);
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
deviceId, socket.remoteAddress());
}
/**
* 获取连接信息
*/
public ConnectionInfo getConnectionInfo(ServerWebSocket socket) {
return connectionMap.get(socket);
}
/**
* 根据设备 ID 获取连接信息
*/
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
ServerWebSocket socket = deviceSocketMap.get(deviceId);
return socket != null ? connectionMap.get(socket) : null;
}
/**
* 发送消息到设备(文本消息)
*
* @param deviceId 设备 ID
* @param message JSON 消息
* @return 是否发送成功
*/
public boolean sendToDevice(Long deviceId, String message) {
ServerWebSocket socket = deviceSocketMap.get(deviceId);
if (socket == null) {
log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId);
return false;
}
try {
socket.writeTextMessage(message);
log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, message.length());
return true;
} catch (Exception e) {
log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e);
// 发送失败时清理连接
unregisterConnection(socket);
return false;
}
}
/**
* 连接信息(包含认证信息)
*/
@Data
@Accessors(chain = true)
public static class ConnectionInfo {
/**
* 设备 ID
*/
private Long deviceId;
/**
* 产品 Key
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 客户端 ID
*/
private String clientId;
/**
* 消息编解码类型(认证后确定)
*/
private String codecType;
}
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 WebSocket 下行消息处理器
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class IotWebSocketDownstreamHandler {
private final IotDeviceMessageService deviceMessageService;
private final IotWebSocketConnectionManager connectionManager;
/**
* 处理下行消息
*/
public void handle(IotDeviceMessage message) {
try {
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
// 1. 获取连接信息
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
message.getDeviceId());
if (connectionInfo == null) {
log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
return;
}
// 2. 编码消息并发送到设备
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
String jsonMessage = StrUtil.utf8Str(bytes);
boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage);
if (success) {
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
} else {
log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
}
} catch (Exception e) {
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
message.getDeviceId(), message.getMethod(), message, e);
}
}
}

View File

@@ -0,0 +1,471 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Handler;
import io.vertx.core.http.ServerWebSocket;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* WebSocket 上行消息处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
/**
* 默认消息编解码类型
*/
private static final String CODEC_TYPE = IotAlinkDeviceMessageCodec.TYPE;
private static final String AUTH_METHOD = "auth";
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceService deviceService;
private final IotWebSocketConnectionManager connectionManager;
private final IotDeviceCommonApi deviceApi;
private final String serverId;
public IotWebSocketUpstreamHandler(IotWebSocketUpstreamProtocol protocol,
IotDeviceMessageService deviceMessageService,
IotDeviceService deviceService,
IotWebSocketConnectionManager connectionManager) {
this.deviceMessageService = deviceMessageService;
this.deviceService = deviceService;
this.connectionManager = connectionManager;
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.serverId = protocol.getServerId();
}
@Override
public void handle(ServerWebSocket socket) {
String clientId = IdUtil.simpleUUID();
log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
// 1. 设置异常和关闭处理器
socket.exceptionHandler(ex -> {
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
cleanupConnection(socket);
});
socket.closeHandler(v -> {
log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
cleanupConnection(socket);
});
// 2. 设置文本消息处理器
socket.textMessageHandler(message -> {
try {
processMessage(clientId, message, socket);
} catch (Exception e) {
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
clientId, socket.remoteAddress(), e.getMessage());
cleanupConnection(socket);
socket.close();
}
});
}
/**
* 处理消息
*
* @param clientId 客户端 ID
* @param message 消息JSON 字符串)
* @param socket WebSocket 连接
* @throws Exception 消息解码失败时抛出异常
*/
private void processMessage(String clientId, String message, ServerWebSocket socket) throws Exception {
// 1.1 基础检查
if (StrUtil.isBlank(message)) {
return;
}
// 1.2 解码消息(已认证连接使用其 codecType未认证连接使用默认 CODEC_TYPE
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE;
IotDeviceMessage deviceMessage;
try {
deviceMessage = deviceMessageService.decodeDeviceMessage(
StrUtil.utf8Bytes(message), codecType);
if (deviceMessage == null) {
throw new Exception("解码后消息为空");
}
} catch (Exception e) {
throw new Exception("消息解码失败: " + e.getMessage(), e);
}
// 2. 根据消息类型路由处理
try {
if (AUTH_METHOD.equals(deviceMessage.getMethod())) {
// 认证请求
handleAuthenticationRequest(clientId, deviceMessage, socket);
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(deviceMessage.getMethod())) {
// 设备动态注册请求
handleRegisterRequest(clientId, deviceMessage, socket);
} else {
// 业务消息
handleBusinessRequest(clientId, deviceMessage, socket);
}
} catch (Exception e) {
log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]",
clientId, deviceMessage.getMethod(), e);
// 发送错误响应,避免客户端一直等待
try {
sendErrorResponse(socket, deviceMessage.getRequestId(), "消息处理失败");
} catch (Exception responseEx) {
log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx);
}
}
}
/**
* 处理认证请求
*
* @param clientId 客户端 ID
* @param message 消息信息
* @param socket WebSocket 连接
*/
private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) {
try {
// 1.1 解析认证参数
IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
if (authParams == null) {
log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId);
sendErrorResponse(socket, message.getRequestId(), "认证参数不完整");
return;
}
// 1.2 执行认证
if (!validateDeviceAuth(authParams)) {
log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {}username: {}]",
clientId, authParams.getUsername());
sendErrorResponse(socket, message.getRequestId(), "认证失败");
return;
}
// 2.1 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
if (deviceInfo == null) {
sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败");
return;
}
// 2.2 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
deviceInfo.getDeviceName());
if (device == null) {
sendErrorResponse(socket, message.getRequestId(), "设备不存在");
return;
}
// 3.1 注册连接
registerConnection(socket, device, clientId);
// 3.2 发送上线消息
sendOnlineMessage(device);
// 3.3 发送成功响应
sendSuccessResponse(socket, message.getRequestId(), "认证成功");
log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
device.getId(), device.getDeviceName());
} catch (Exception e) {
log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
sendErrorResponse(socket, message.getRequestId(), "认证处理异常");
}
}
/**
* 处理设备动态注册请求(一型一密,不需要认证)
*
* @param clientId 客户端 ID
* @param message 消息信息
* @param socket WebSocket 连接
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) {
try {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
if (params == null
|| StrUtil.hasEmpty(params.getProductKey(), params.getDeviceName(), params.getProductSecret())) {
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
sendErrorResponse(socket, message.getRequestId(), "注册参数不完整");
return;
}
// 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
if (result.isError()) {
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
sendErrorResponse(socket, message.getRequestId(), result.getMsg());
return;
}
// 3. 发送成功响应(包含 deviceSecret
sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData());
log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]",
clientId, params.getDeviceName());
} catch (Exception e) {
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
sendErrorResponse(socket, message.getRequestId(), "注册处理异常");
}
}
/**
* 处理业务请求
*
* @param clientId 客户端 ID
* @param message 消息信息
* @param socket WebSocket 连接
*/
private void handleBusinessRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) {
try {
// 1. 获取认证信息并处理业务消息
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
if (connectionInfo == null) {
log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息,客户端 ID: {}]", clientId);
sendErrorResponse(socket, message.getRequestId(), "连接未认证");
return;
}
// 2. 发送消息到消息总线
deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}",
clientId, message.toString());
} catch (Exception e) {
log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e);
}
}
/**
* 注册连接信息
*
* @param socket WebSocket 连接
* @param device 设备
* @param clientId 客户端 ID
*/
private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device, String clientId) {
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo()
.setDeviceId(device.getId())
.setProductKey(device.getProductKey())
.setDeviceName(device.getDeviceName())
.setClientId(clientId)
.setCodecType(CODEC_TYPE);
// 注册连接
connectionManager.registerConnection(socket, device.getId(), connectionInfo);
}
/**
* 发送设备上线消息
*
* @param device 设备信息
*/
private void sendOnlineMessage(IotDeviceRespDTO device) {
try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
device.getDeviceName(), serverId);
} catch (Exception e) {
log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
}
}
/**
* 清理连接
*
* @param socket WebSocket 连接
*/
private void cleanupConnection(ServerWebSocket socket) {
try {
// 1. 发送离线消息(如果已认证)
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
if (connectionInfo != null) {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
}
// 2. 注销连接
connectionManager.unregisterConnection(socket);
} catch (Exception e) {
log.error("[cleanupConnection][清理连接失败]", e);
}
}
/**
* 发送响应消息
*
* @param socket WebSocket 连接
* @param success 是否成功
* @param message 消息
* @param requestId 请求 ID
*/
private void sendResponse(ServerWebSocket socket, boolean success, String message, String requestId) {
try {
Object responseData = MapUtil.builder()
.put("success", success)
.put("message", message)
.build();
int code = success ? 0 : 401;
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message);
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE);
socket.writeTextMessage(StrUtil.utf8Str(encodedData));
} catch (Exception e) {
log.error("[sendResponse][发送响应失败requestId: {}]", requestId, e);
}
}
/**
* 验证设备认证信息
*
* @param authParams 认证参数
* @return 是否认证成功
*/
private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
try {
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
.setPassword(authParams.getPassword()));
result.checkError();
return BooleanUtil.isTrue(result.getData());
} catch (Exception e) {
log.error("[validateDeviceAuth][设备认证异常username: {}]", authParams.getUsername(), e);
return false;
}
}
/**
* 发送错误响应
*
* @param socket WebSocket 连接
* @param requestId 请求 ID
* @param errorMessage 错误消息
*/
private void sendErrorResponse(ServerWebSocket socket, String requestId, String errorMessage) {
sendResponse(socket, false, errorMessage, requestId);
}
/**
* 发送成功响应
*
* @param socket WebSocket 连接
* @param requestId 请求 ID
* @param message 消息
*/
@SuppressWarnings("SameParameterValue")
private void sendSuccessResponse(ServerWebSocket socket, String requestId, String message) {
sendResponse(socket, true, message, requestId);
}
/**
* 解析认证参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 认证参数 DTO解析失败时返回 null
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceAuthReqDTO()
.setClientId(MapUtil.getStr(paramMap, "clientId"))
.setUsername(MapUtil.getStr(paramMap, "username"))
.setPassword(MapUtil.getStr(paramMap, "password"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceAuthReqDTO) {
return (IotDeviceAuthReqDTO) params;
}
// 其他情况尝试 JSON 转换
return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
} catch (Exception e) {
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
return null;
}
}
/**
* 解析注册参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 注册参数 DTO解析失败时返回 null
*/
@SuppressWarnings("unchecked")
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceRegisterReqDTO()
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceRegisterReqDTO) {
return (IotDeviceRegisterReqDTO) params;
}
// 其他情况尝试 JSON 转换
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
} catch (Exception e) {
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
return null;
}
}
/**
* 发送注册成功响应(包含 deviceSecret
*
* @param socket WebSocket 连接
* @param requestId 请求 ID
* @param registerResp 注册响应
*/
private void sendRegisterSuccessResponse(ServerWebSocket socket, String requestId,
IotDeviceRegisterRespDTO registerResp) {
try {
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
// 2. 发送响应
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE);
socket.writeTextMessage(StrUtil.utf8Str(encodedData));
} catch (Exception e) {
log.error("[sendRegisterSuccessResponse][发送注册成功响应失败requestId: {}]", requestId, e);
}
}
}

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.iot.gateway.service.auth;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
/**
* IoT 设备 Token Service 接口
@@ -24,7 +24,7 @@ public interface IotDeviceTokenService {
* @param token 设备 Token
* @return 设备信息
*/
IotDeviceAuthUtils.DeviceInfo verifyToken(String token);
IotDeviceIdentity verifyToken(String token);
/**
* 解析用户名
@@ -32,6 +32,6 @@ public interface IotDeviceTokenService {
* @param username 用户名
* @return 设备信息
*/
IotDeviceAuthUtils.DeviceInfo parseUsername(String username);
IotDeviceIdentity parseUsername(String username);
}

View File

@@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import jakarta.annotation.Resource;
@@ -48,7 +49,7 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
}
@Override
public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) {
public IotDeviceIdentity verifyToken(String token) {
Assert.notBlank(token, "token 不能为空");
// 校验 JWT Token
boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes());
@@ -68,11 +69,11 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
String deviceName = payload.getStr("deviceName");
Assert.notBlank(productKey, "productKey 不能为空");
Assert.notBlank(deviceName, "deviceName 不能为空");
return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName);
return new IotDeviceIdentity(productKey, deviceName);
}
@Override
public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) {
public IotDeviceIdentity parseUsername(String username) {
return IotDeviceAuthUtils.parseUsername(username);
}

View File

@@ -6,6 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
@@ -18,6 +22,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
/**
@@ -54,6 +60,16 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { });
}
@Override
public CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO) {
return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { });
}
@Override
public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
return doPost("/register-sub", reqDTO, new ParameterizedTypeReference<>() { });
}
private <T, R> CommonResult<R> doPost(String url, T body,
ParameterizedTypeReference<CommonResult<R>> responseType) {
try {

View File

@@ -96,6 +96,16 @@ yudao:
ssl-cert-path: "classpath:certs/client.jks"
ssl-key-path: "classpath:certs/client.jks"
# ====================================
# 针对引入的 UDP 组件的配置
# ====================================
udp:
enabled: false # 是否启用 UDP
port: 8093 # UDP 服务端口
receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB
send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB
session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒)
session-clean-interval-ms: 30000 # 会话清理间隔(毫秒,默认 30 秒)
# ====================================
# 针对引入的 MQTT 组件的配置
# ====================================
mqtt:
@@ -105,18 +115,25 @@ yudao:
connect-timeout-seconds: 60
ssl-enabled: false
# ====================================
# 针对引入的 MQTT WebSocket 组件的配置
# 针对引入的 CoAP 组件的配置
# ====================================
mqtt-ws:
enabled: false # 是否启用 MQTT WebSocket
port: 8083 # WebSocket 服务端口
path: /mqtt # WebSocket 路径
max-message-size: 8192 # 最大消息大小(字节
max-frame-size: 65536 # 最大帧大小(字节)
connect-timeout-seconds: 60 # 连接超时时间(秒)
keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒)
coap:
enabled: false # 是否启用 CoAP 协议
port: 5683 # CoAP 服务端口(默认 5683
max-message-size: 1024 # 最大消息大小(字节)
ack-timeout: 2000 # ACK 超时时间(毫秒
max-retransmit: 4 # 最大重传次数
# ====================================
# 针对引入的 WebSocket 组件的配置
# ====================================
websocket:
enabled: false # 是否启用 WebSocket 协议
port: 8094 # WebSocket 服务端口(默认 8094
path: /ws # WebSocket 路径(默认 /ws
max-message-size: 65536 # 最大消息大小(字节,默认 64KB
max-frame-size: 65536 # 最大帧大小(字节,默认 64KB
idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60
ssl-enabled: false # 是否启用 SSLwss://
sub-protocol: mqtt # WebSocket 子协议
--- #################### 日志相关配置 ####################
@@ -137,6 +154,8 @@ logging:
cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
# 根日志级别
root: INFO

View File

@@ -0,0 +1,227 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.config.UdpConfig;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* IoT 直连设备 CoAP 协议集成测试(手动测试)
*
* <p>测试场景直连设备IotProductDeviceTypeEnum 的 DIRECT 类型)通过 CoAP 协议直接连接平台
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务CoAP 端口 5683</li>
* <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
* <li>运行 {@link #testAuth()} 获取设备 token将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
* <li>{@link #testEventPost()} - 设备事件上报</li>
* </ul>
* </li>
* </ol>
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotDirectDeviceCoapProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 5683;
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
/**
* 直连设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk5MjgxOSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.UHLCXsoGNsKbtJcbTV3n1psp03G75hVcVpV4wwd39r4";
@BeforeAll
public static void initCaliforniumConfig() {
// 注册 Californium 配置定义
CoapConfig.register();
UdpConfig.register();
// 创建默认配置
Configuration.setStandard(Configuration.createStandardWithoutFile());
}
// ===================== 认证测试 =====================
/**
* 认证测试:获取设备 Token
*/
@Test
public void testAuth() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuth][请求 URI: {}]", uri);
log.info("[testAuth][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
// 2.2 输出结果
log.info("[testAuth][响应码: {}]", response.getCode());
log.info("[testAuth][响应体: {}]", response.getResponseText());
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
} finally {
client.shutdown();
}
}
// ===================== 直连设备属性上报测试 =====================
/**
* 属性上报测试
*/
@Test
@SuppressWarnings("deprecation")
public void testPropertyPost() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
.put("version", "1.0")
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("width", 1)
.put("height", "2")
.build())
)
.build());
// 1.2 输出请求
log.info("[testPropertyPost][请求 URI: {}]", uri);
log.info("[testPropertyPost][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testPropertyPost][响应码: {}]", response.getCode());
log.info("[testPropertyPost][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
// ===================== 直连设备事件上报测试 =====================
/**
* 事件上报测试
*/
@Test
@SuppressWarnings("deprecation")
public void testEventPost() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
.put("version", "1.0")
.put("params", IotDeviceEventPostReqDTO.of(
"eat",
MapUtil.<String, Object>builder().put("rice", 3).build(),
System.currentTimeMillis())
)
.build());
// 1.2 输出请求
log.info("[testEventPost][请求 URI: {}]", uri);
log.info("[testEventPost][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testEventPost][响应码: {}]", response.getCode());
log.info("[testEventPost][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
// ===================== 动态注册测试 =====================
/**
* 直连设备动态注册测试(一型一密)
* <p>
* 使用产品密钥productSecret验证身份成功后返回设备密钥deviceSecret
* <p>
* 注意:此接口不需要 Token 认证
*/
@Test
public void testDeviceRegister() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
// 1.2 构建请求参数
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
reqDTO.setProductKey(PRODUCT_KEY);
reqDTO.setDeviceName("test-" + System.currentTimeMillis());
reqDTO.setProductSecret("test-product-secret");
String payload = JsonUtils.toJsonString(reqDTO);
// 1.3 输出请求
log.info("[testDeviceRegister][请求 URI: {}]", uri);
log.info("[testDeviceRegister][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
// 2.2 输出结果
log.info("[testDeviceRegister][响应码: {}]", response.getCode());
log.info("[testDeviceRegister][响应体: {}]", response.getResponseText());
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
} finally {
client.shutdown();
}
}
}

View File

@@ -0,0 +1,376 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.config.UdpConfig;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* IoT 网关设备 CoAP 协议集成测试(手动测试)
*
* <p>测试场景网关设备IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 CoAP 协议管理子设备拓扑关系
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务CoAP 端口 5683</li>
* <li>运行 {@link #testAuth()} 获取网关设备 token将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
* </ul>
* </li>
* </ol>
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotGatewayDeviceCoapProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 5683;
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
/**
* 网关设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
@BeforeAll
public static void initCaliforniumConfig() {
// 注册 Californium 配置定义
CoapConfig.register();
UdpConfig.register();
// 创建默认配置
Configuration.setStandard(Configuration.createStandardWithoutFile());
}
// ===================== 认证测试 =====================
/**
* 网关设备认证测试:获取网关设备 Token
*/
@Test
public void testAuth() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuth][请求 URI: {}]", uri);
log.info("[testAuth][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
// 2.2 输出结果
log.info("[testAuth][响应码: {}]", response.getCode());
log.info("[testAuth][响应体: {}]", response.getResponseText());
log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
} finally {
client.shutdown();
}
}
// ===================== 拓扑管理测试 =====================
/**
* 添加子设备拓扑关系测试
* <p>
* 网关设备向平台上报需要绑定的子设备信息
*/
@Test
@SuppressWarnings("deprecation")
public void testTopoAdd() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/add",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建子设备认证信息
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
.setClientId(subAuthInfo.getClientId())
.setUsername(subAuthInfo.getUsername())
.setPassword(subAuthInfo.getPassword());
// 1.3 构建请求参数
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
params.setSubDevices(Collections.singletonList(subDeviceAuth));
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.4 输出请求
log.info("[testTopoAdd][请求 URI: {}]", uri);
log.info("[testTopoAdd][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testTopoAdd][响应码: {}]", response.getCode());
log.info("[testTopoAdd][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
/**
* 删除子设备拓扑关系测试
* <p>
* 网关设备向平台上报需要解绑的子设备信息
*/
@Test
@SuppressWarnings("deprecation")
public void testTopoDelete() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/delete",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
params.setSubDevices(Collections.singletonList(
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.3 输出请求
log.info("[testTopoDelete][请求 URI: {}]", uri);
log.info("[testTopoDelete][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testTopoDelete][响应码: {}]", response.getCode());
log.info("[testTopoDelete][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
/**
* 获取子设备拓扑关系测试
* <p>
* 网关设备向平台查询已绑定的子设备列表
*/
@Test
@SuppressWarnings("deprecation")
public void testTopoGet() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/get",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数(目前为空,预留扩展)
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.3 输出请求
log.info("[testTopoGet][请求 URI: {}]", uri);
log.info("[testTopoGet][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testTopoGet][响应码: {}]", response.getCode());
log.info("[testTopoGet][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
// ===================== 子设备注册测试 =====================
/**
* 子设备动态注册测试
* <p>
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
* <p>
* 注意:此接口需要网关 Token 认证
*/
@Test
@SuppressWarnings("deprecation")
public void testSubDeviceRegister() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/auth/register/sub-device/%s/%s",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
subDevice.setDeviceName("mougezishebei");
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
.put("version", "1.0")
.put("params", Collections.singletonList(subDevice))
.build());
// 1.3 输出请求
log.info("[testSubDeviceRegister][请求 URI: {}]", uri);
log.info("[testSubDeviceRegister][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testSubDeviceRegister][响应码: {}]", response.getCode());
log.info("[testSubDeviceRegister][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
// ===================== 批量上报测试 =====================
/**
* 批量上报属性测试(网关 + 子设备)
* <p>
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
*/
@Test
@SuppressWarnings("deprecation")
public void testPropertyPackPost() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建【网关设备】自身属性
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
.put("temperature", 25.5)
.build();
// 1.3 构建【网关设备】自身事件
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
gatewayEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("statusReport", gatewayEvent)
.build();
// 1.4 构建【网关子设备】属性
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
.put("power", 100)
.build();
// 1.5 构建【网关子设备】事件
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
subDeviceEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("healthCheck", subDeviceEvent)
.build();
// 1.6 构建子设备数据
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
subDeviceData.setProperties(subDeviceProperties);
subDeviceData.setEvents(subDeviceEvents);
// 1.7 构建请求参数
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
params.setProperties(gatewayProperties);
params.setEvents(gatewayEvents);
params.setSubDevices(List.of(subDeviceData));
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.8 输出请求
log.info("[testPropertyPackPost][请求 URI: {}]", uri);
log.info("[testPropertyPackPost][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testPropertyPackPost][响应码: {}]", response.getCode());
log.info("[testPropertyPackPost][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
}

View File

@@ -0,0 +1,199 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.config.UdpConfig;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* IoT 网关子设备 CoAP 协议集成测试(手动测试)
*
* <p>测试场景子设备IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
*
* <p><b>重要说明子设备无法直接连接平台所有请求均由网关设备Gateway代为转发。</b>
* <p>网关设备转发子设备请求时Token 使用子设备自己的信息。
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务CoAP 端口 5683</li>
* <li>确保子设备已通过 {@link IotGatewayDeviceCoapProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
* <li>运行 {@link #testAuth()} 获取子设备 token将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
* </ul>
* </li>
* </ol>
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotGatewaySubDeviceCoapProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 5683;
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
/**
* 网关子设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTk1NDY3OSwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.jfbUAoU0xkJl4UvO-NUvcJ6yITPRgUjQ4MKATPuwneg";
@BeforeAll
public static void initCaliforniumConfig() {
// 注册 Californium 配置定义
CoapConfig.register();
UdpConfig.register();
// 创建默认配置
Configuration.setStandard(Configuration.createStandardWithoutFile());
}
// ===================== 认证测试 =====================
/**
* 子设备认证测试:获取子设备 Token
*/
@Test
public void testAuth() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuth][请求 URI: {}]", uri);
log.info("[testAuth][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
// 2.2 输出结果
log.info("[testAuth][响应码: {}]", response.getCode());
log.info("[testAuth][响应体: {}]", response.getResponseText());
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
} finally {
client.shutdown();
}
}
// ===================== 子设备属性上报测试 =====================
/**
* 子设备属性上报测试
*/
@Test
@SuppressWarnings("deprecation")
public void testPropertyPost() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
.put("version", "1.0")
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("power", 100)
.put("status", "online")
.put("temperature", 36.5)
.build()))
.build());
// 1.2 输出请求
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
log.info("[testPropertyPost][请求 URI: {}]", uri);
log.info("[testPropertyPost][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testPropertyPost][响应码: {}]", response.getCode());
log.info("[testPropertyPost][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
// ===================== 子设备事件上报测试 =====================
/**
* 子设备事件上报测试
*/
@Test
@SuppressWarnings("deprecation")
public void testEventPost() throws Exception {
// 1.1 构建请求
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
.put("version", "1.0")
.put("params", IotDeviceEventPostReqDTO.of(
"alarm",
MapUtil.<String, Object>builder()
.put("level", "warning")
.put("message", "temperature too high")
.put("threshold", 40)
.put("current", 42)
.build(),
System.currentTimeMillis()))
.build());
// 1.2 输出请求
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
log.info("[testEventPost][请求 URI: {}]", uri);
log.info("[testEventPost][请求体: {}]", payload);
// 2.1 发送请求
CoapClient client = new CoapClient(uri);
try {
Request request = Request.newPost();
request.setURI(uri);
request.setPayload(payload);
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
CoapResponse response = client.advanced(request);
// 2.2 输出结果
log.info("[testEventPost][响应码: {}]", response.getCode());
log.info("[testEventPost][响应体: {}]", response.getResponseText());
} finally {
client.shutdown();
}
}
}

View File

@@ -0,0 +1,182 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* IoT 直连设备 HTTP 协议集成测试(手动测试)
*
* <p>测试场景直连设备IotProductDeviceTypeEnum 的 DIRECT 类型)通过 HTTP 协议直接连接平台
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务HTTP 端口 8092</li>
* <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
* <li>运行 {@link #testAuth()} 获取设备 token将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
* <li>{@link #testEventPost()} - 设备事件上报</li>
* </ul>
* </li>
* </ol>
*
* @author 芋道源码
*/
@Slf4j
@Disabled
@SuppressWarnings("HttpUrlsUsage")
public class IotDirectDeviceHttpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8092;
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
/**
* 直连设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
// ===================== 认证测试 =====================
/**
* 认证测试:获取设备 Token
*/
@Test
public void testAuth() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuth][请求 URL: {}]", url);
log.info("[testAuth][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testAuth][响应体: {}]", response);
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
}
// ===================== 直连设备属性上报测试 =====================
/**
* 属性上报测试
*/
@Test
public void testPropertyPost() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
.put("version", "1.0")
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("width", 1)
.put("height", "2")
.build())
)
.build());
// 1.2 输出请求
log.info("[testPropertyPost][请求 URL: {}]", url);
log.info("[testPropertyPost][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testPropertyPost][响应体: {}]", httpResponse.body());
}
}
// ===================== 直连设备事件上报测试 =====================
/**
* 事件上报测试
*/
@Test
public void testEventPost() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
.put("version", "1.0")
.put("params", IotDeviceEventPostReqDTO.of(
"eat",
MapUtil.<String, Object>builder().put("rice", 3).build(),
System.currentTimeMillis())
)
.build());
// 1.2 输出请求
log.info("[testEventPost][请求 URL: {}]", url);
log.info("[testEventPost][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testEventPost][响应体: {}]", httpResponse.body());
}
}
// ===================== 动态注册测试 =====================
/**
* 直连设备动态注册测试(一型一密)
* <p>
* 使用产品密钥productSecret验证身份成功后返回设备密钥deviceSecret
* <p>
* 注意:此接口不需要 Token 认证
*/
@Test
public void testDeviceRegister() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
// 1.2 构建请求参数
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
reqDTO.setProductKey(PRODUCT_KEY);
reqDTO.setDeviceName("test-" + System.currentTimeMillis());
reqDTO.setProductSecret("test-product-secret");
String payload = JsonUtils.toJsonString(reqDTO);
// 1.3 输出请求
log.info("[testDeviceRegister][请求 URL: {}]", url);
log.info("[testDeviceRegister][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testDeviceRegister][响应体: {}]", response);
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
}
}

View File

@@ -0,0 +1,312 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* IoT 网关设备 HTTP 协议集成测试(手动测试)
*
* <p>测试场景网关设备IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 HTTP 协议管理子设备拓扑关系
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务HTTP 端口 8092</li>
* <li>运行 {@link #testAuth()} 获取网关设备 token将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
* </ul>
* </li>
* </ol>
*
* @author 芋道源码
*/
@Slf4j
@Disabled
@SuppressWarnings("HttpUrlsUsage")
public class IotGatewayDeviceHttpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8092;
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
/**
* 网关设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// ===================== 认证测试 =====================
/**
* 网关设备认证测试:获取网关设备 Token
*/
@Test
public void testAuth() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuth][请求 URL: {}]", url);
log.info("[testAuth][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testAuth][响应体: {}]", response);
log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
}
// ===================== 拓扑管理测试 =====================
/**
* 添加子设备拓扑关系测试
* <p>
* 网关设备向平台上报需要绑定的子设备信息
*/
@Test
public void testTopoAdd() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建子设备认证信息
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
.setClientId(subAuthInfo.getClientId())
.setUsername(subAuthInfo.getUsername())
.setPassword(subAuthInfo.getPassword());
// 1.3 构建请求参数
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
params.setSubDevices(Collections.singletonList(subDeviceAuth));
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.4 输出请求
log.info("[testTopoAdd][请求 URL: {}]", url);
log.info("[testTopoAdd][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", GATEWAY_TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testTopoAdd][响应体: {}]", httpResponse.body());
}
}
/**
* 删除子设备拓扑关系测试
* <p>
* 网关设备向平台上报需要解绑的子设备信息
*/
@Test
public void testTopoDelete() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/delete",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
params.setSubDevices(Collections.singletonList(
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.3 输出请求
log.info("[testTopoDelete][请求 URL: {}]", url);
log.info("[testTopoDelete][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", GATEWAY_TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testTopoDelete][响应体: {}]", httpResponse.body());
}
}
/**
* 获取子设备拓扑关系测试
* <p>
* 网关设备向平台查询已绑定的子设备列表
*/
@Test
public void testTopoGet() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/get",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数(目前为空,预留扩展)
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.3 输出请求
log.info("[testTopoGet][请求 URL: {}]", url);
log.info("[testTopoGet][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", GATEWAY_TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testTopoGet][响应体: {}]", httpResponse.body());
}
}
// ===================== 子设备注册测试 =====================
// TODO @芋艿:待测试
/**
* 子设备动态注册测试
* <p>
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
* <p>
* 注意:此接口需要网关 Token 认证
*/
@Test
public void testSubDeviceRegister() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
subDevice.setDeviceName("mougezishebei");
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
.put("version", "1.0")
.put("params", Collections.singletonList(subDevice))
.build());
// 1.3 输出请求
log.info("[testSubDeviceRegister][请求 URL: {}]", url);
log.info("[testSubDeviceRegister][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", GATEWAY_TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body());
}
}
// ===================== 批量上报测试 =====================
/**
* 批量上报属性测试(网关 + 子设备)
* <p>
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
*/
@Test
public void testPropertyPackPost() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建【网关设备】自身属性
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
.put("temperature", 25.5)
.build();
// 1.3 构建【网关设备】自身事件
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
gatewayEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("statusReport", gatewayEvent)
.build();
// 1.4 构建【网关子设备】属性
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
.put("power", 100)
.build();
// 1.5 构建【网关子设备】事件
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
subDeviceEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("healthCheck", subDeviceEvent)
.build();
// 1.6 构建子设备数据
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
subDeviceData.setProperties(subDeviceProperties);
subDeviceData.setEvents(subDeviceEvents);
// 1.7 构建请求参数
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
params.setProperties(gatewayProperties);
params.setEvents(gatewayEvents);
params.setSubDevices(List.of(subDeviceData));
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())
.put("version", "1.0")
.put("params", params)
.build());
// 1.8 输出请求
log.info("[testPropertyPackPost][请求 URL: {}]", url);
log.info("[testPropertyPackPost][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", GATEWAY_TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testPropertyPackPost][响应体: {}]", httpResponse.body());
}
}
}

View File

@@ -0,0 +1,162 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* IoT 网关子设备 HTTP 协议集成测试(手动测试)
*
* <p>测试场景子设备IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
*
* <p><b>重要说明子设备无法直接连接平台所有请求均由网关设备Gateway代为转发。</b>
* <p>网关设备转发子设备请求时URL 和 Token 都使用子设备自己的信息。
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务HTTP 端口 8092</li>
* <li>确保子设备已通过 {@link IotGatewayDeviceHttpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
* <li>运行 {@link #testAuth()} 获取子设备 token将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
* </ul>
* </li>
* </ol>
*
* @author 芋道源码
*/
@Slf4j
@Disabled
@SuppressWarnings("HttpUrlsUsage")
public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8092;
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
/**
* 网关子设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTg3MTI3NCwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.99sAlRalzMU3CqRlGStDzCwWSBJq6u3PJw48JQ3NpzQ";
// ===================== 认证测试 =====================
/**
* 子设备认证测试:获取子设备 Token
*/
@Test
public void testAuth() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuth][请求 URL: {}]", url);
log.info("[testAuth][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testAuth][响应体: {}]", response);
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
}
// ===================== 子设备属性上报测试 =====================
/**
* 子设备属性上报测试
*/
@Test
public void testPropertyPost() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
.put("version", "1.0")
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("power", 100)
.put("status", "online")
.put("temperature", 36.5)
.build())
)
.build());
// 1.2 输出请求
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
log.info("[testPropertyPost][请求 URL: {}]", url);
log.info("[testPropertyPost][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testPropertyPost][响应体: {}]", httpResponse.body());
}
}
// ===================== 子设备事件上报测试 =====================
/**
* 子设备事件上报测试
*/
@Test
public void testEventPost() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post",
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
.put("version", "1.0")
.put("params", IotDeviceEventPostReqDTO.of(
"alarm",
MapUtil.<String, Object>builder()
.put("level", "warning")
.put("message", "temperature too high")
.put("threshold", 40)
.put("current", 42)
.build(),
System.currentTimeMillis())
)
.build());
// 1.2 输出请求
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
log.info("[testEventPost][请求 URL: {}]", url);
log.info("[testEventPost][请求体: {}]", payload);
// 2.1 发送请求
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testEventPost][响应体: {}]", httpResponse.body());
}
}
}

View File

@@ -0,0 +1,408 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* IoT 直连设备 MQTT 协议集成测试(手动测试)
*
* <p>测试场景直连设备IotProductDeviceTypeEnum 的 DIRECT 类型)通过 MQTT 协议直接连接平台
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务MQTT 端口 1883</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testAuth()} - 设备连接认证</li>
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
* <li>{@link #testEventPost()} - 设备事件上报</li>
* <li>{@link #testSubscribe()} - 订阅下行消息</li>
* </ul>
* </li>
* </ol>
*
* <p>注意MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
* 认证成功后同一连接上的后续请求无需再携带认证信息
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotDirectDeviceMqttProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 1883;
private static final int TIMEOUT_SECONDS = 10;
private static Vertx vertx;
// ===================== 编解码器MQTT 使用 Alink 协议) =====================
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();
}
@AfterAll
public static void tearDown() {
if (vertx != null) {
vertx.close();
}
}
// ===================== 连接认证测试 =====================
/**
* 认证测试:获取设备 Token
*/
@Test
public void testAuth() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
// 1. 构建认证信息
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
// 2. 创建客户端并连接
MqttClient client = connect(authInfo);
client.connect(SERVER_PORT, SERVER_HOST)
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
// 断开连接
client.disconnect()
.onComplete(disconnectAr -> {
if (disconnectAr.succeeded()) {
log.info("[testAuth][断开连接成功]");
} else {
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
}
latch.countDown();
});
} else {
log.error("[testAuth][连接失败]", ar.cause());
latch.countDown();
}
});
// 3. 等待测试完成
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!completed) {
log.warn("[testAuth][测试超时]");
}
}
// ===================== 直连设备属性上报测试 =====================
/**
* 属性上报测试
*/
@Test
public void testPropertyPost() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testPropertyPost][连接认证成功]");
// 2. 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
subscribeReply(client, replyTopic);
// 3. 构建属性上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("width", 1)
.put("height", "2")
.build()),
null, null, null);
// 4. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testPropertyPost][响应消息: {}]", response);
// 5. 断开连接
disconnect(client);
}
// ===================== 直连设备事件上报测试 =====================
/**
* 事件上报测试
*/
@Test
public void testEventPost() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testEventPost][连接认证成功]");
// 2. 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
subscribeReply(client, replyTopic);
// 3. 构建事件上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
IotDeviceEventPostReqDTO.of(
"eat",
MapUtil.<String, Object>builder().put("rice", 3).build(),
System.currentTimeMillis()),
null, null, null);
// 4. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testEventPost][响应消息: {}]", response);
// 5. 断开连接
disconnect(client);
}
// ===================== 设备动态注册测试(一型一密) =====================
/**
* 直连设备动态注册测试(一型一密)
* <p>
* 使用产品密钥productSecret验证身份成功后返回设备密钥deviceSecret
* <p>
* 注意:此接口不需要认证
*/
@Test
public void testDeviceRegister() throws Exception {
// 1. 连接并认证(使用已有设备连接)
MqttClient client = connectAndAuth();
log.info("[testDeviceRegister][连接认证成功]");
// 2.1 构建注册消息
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
registerReqDTO.setProductKey(PRODUCT_KEY);
registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis());
registerReqDTO.setProductSecret("test-product-secret");
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
// 2.2 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply",
registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
subscribeReply(client, replyTopic);
// 3. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/auth/register",
registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testDeviceRegister][响应消息: {}]", response);
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
// 4. 断开连接
disconnect(client);
}
// ===================== 订阅下行消息测试 =====================
/**
* 订阅下行消息测试:订阅服务端下发的消息
*/
@Test
public void testSubscribe() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testSubscribe][连接认证成功]");
// 2. 设置消息处理器
client.publishHandler(message -> {
log.info("[testSubscribe][收到消息: topic={}, payload={}]",
message.topicName(), message.payload().toString());
});
// 3. 订阅下行主题
String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
log.info("[testSubscribe][订阅主题: {}]", topic);
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
.onComplete(subscribeAr -> {
if (subscribeAr.succeeded()) {
log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
// 保持连接 30 秒等待消息
vertx.setTimer(30000, id -> {
client.disconnect()
.onComplete(disconnectAr -> {
log.info("[testSubscribe][断开连接]");
latch.countDown();
});
});
} else {
log.error("[testSubscribe][订阅失败]", subscribeAr.cause());
latch.countDown();
}
});
// 4. 等待测试完成
boolean completed = latch.await(60, TimeUnit.SECONDS);
if (!completed) {
log.warn("[testSubscribe][测试超时]");
}
}
// ===================== 辅助方法 =====================
/**
* 创建 MQTT 客户端
*
* @param authInfo 认证信息
* @return MQTT 客户端
*/
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
MqttClientOptions options = new MqttClientOptions()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword())
.setCleanSession(true)
.setKeepAliveInterval(60);
return MqttClient.create(vertx, options);
}
/**
* 连接并认证设备
*
* @return 已认证的 MQTT 客户端
*/
private MqttClient connectAndAuth() throws Exception {
// 1. 创建客户端并连接
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
MqttClient client = connect(authInfo);
// 2.1 连接
CompletableFuture<MqttClient> future = new CompletableFuture<>();
client.connect(SERVER_PORT, SERVER_HOST)
.onComplete(ar -> {
if (ar.succeeded()) {
future.complete(client);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2.2 等待连接结果
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* 订阅响应主题
*
* @param client MQTT 客户端
* @param replyTopic 响应主题
*/
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
// 1. 订阅响应主题
CompletableFuture<Void> future = new CompletableFuture<>();
client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
future.complete(null);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2. 等待订阅结果
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* 发布消息并等待响应
*
* @param client MQTT 客户端
* @param topic 发布主题
* @param request 请求消息
* @return 响应消息
*/
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
// 1. 设置消息处理器,接收响应
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
client.publishHandler(message -> {
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
message.topicName(), message.payload().toString());
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
future.complete(response);
});
// 2. 编码并发布消息
byte[] payload = CODEC.encode(request);
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
CODEC.type(), topic, new String(payload));
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[publishAndWaitReply][消息发布成功messageId={}]", ar.result());
} else {
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
future.completeExceptionally(ar.cause());
}
});
// 3. 等待响应(超时返回 null
try {
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[publishAndWaitReply][等待响应超时或失败]");
return null;
}
}
/**
* 断开连接
*
* @param client MQTT 客户端
*/
private void disconnect(MqttClient client) throws Exception {
// 1. 断开连接
CompletableFuture<Void> future = new CompletableFuture<>();
client.disconnect()
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[disconnect][断开连接成功]");
future.complete(null);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2. 等待断开结果
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,498 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关设备 MQTT 协议集成测试(手动测试)
*
* <p>测试场景网关设备IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 MQTT 协议管理子设备拓扑关系
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务MQTT 端口 1883</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testAuth()} - 网关设备连接认证</li>
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
* </ul>
* </li>
* </ol>
*
* <p>注意MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
* 认证成功后同一连接上的后续请求无需再携带认证信息
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotGatewayDeviceMqttProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 1883;
private static final int TIMEOUT_SECONDS = 10;
private static Vertx vertx;
// ===================== 编解码器MQTT 使用 Alink 协议) =====================
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();
}
@AfterAll
public static void tearDown() {
if (vertx != null) {
vertx.close();
}
}
// ===================== 连接认证测试 =====================
/**
* 网关设备认证测试:获取网关设备 Token
*/
@Test
public void testAuth() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
// 1. 构建认证信息
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
// 2. 创建客户端并连接
MqttClient client = connect(authInfo);
client.connect(SERVER_PORT, SERVER_HOST)
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
// 断开连接
client.disconnect()
.onComplete(disconnectAr -> {
if (disconnectAr.succeeded()) {
log.info("[testAuth][断开连接成功]");
} else {
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
}
latch.countDown();
});
} else {
log.error("[testAuth][连接失败]", ar.cause());
latch.countDown();
}
});
// 3. 等待测试完成
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!completed) {
log.warn("[testAuth][测试超时]");
}
}
// ===================== 拓扑管理测试 =====================
/**
* 添加子设备拓扑关系测试
* <p>
* 网关设备向平台上报需要绑定的子设备信息
*/
@Test
public void testTopoAdd() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testTopoAdd][连接认证成功]");
// 2.1 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
subscribeReply(client, replyTopic);
// 2.2 构建子设备认证信息
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
.setClientId(subAuthInfo.getClientId())
.setUsername(subAuthInfo.getUsername())
.setPassword(subAuthInfo.getPassword());
// 2.3 构建请求消息
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
params.setSubDevices(Collections.singletonList(subDeviceAuth));
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
params,
null, null, null);
// 3. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/topo/add",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testTopoAdd][响应消息: {}]", response);
// 4. 断开连接
disconnect(client);
}
/**
* 删除子设备拓扑关系测试
* <p>
* 网关设备向平台上报需要解绑的子设备信息
*/
@Test
public void testTopoDelete() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testTopoDelete][连接认证成功]");
// 2.1 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
subscribeReply(client, replyTopic);
// 2.2 构建请求消息
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
params.setSubDevices(Collections.singletonList(
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
params,
null, null, null);
// 3. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/topo/delete",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testTopoDelete][响应消息: {}]", response);
// 4. 断开连接
disconnect(client);
}
/**
* 获取子设备拓扑关系测试
* <p>
* 网关设备向平台查询已绑定的子设备列表
*/
@Test
public void testTopoGet() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testTopoGet][连接认证成功]");
// 2.1 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
subscribeReply(client, replyTopic);
// 2.2 构建请求消息
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
params,
null, null, null);
// 3. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/topo/get",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testTopoGet][响应消息: {}]", response);
// 4. 断开连接
disconnect(client);
}
// ===================== 子设备注册测试 =====================
/**
* 子设备动态注册测试
* <p>
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
* <p>
* 注意:此接口需要网关认证
*/
@Test
public void testSubDeviceRegister() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testSubDeviceRegister][连接认证成功]");
// 2.1 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
subscribeReply(client, replyTopic);
// 2.2 构建请求消息
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
subDevice.setDeviceName("mougezishebei-mqtt");
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
Collections.singletonList(subDevice),
null, null, null);
// 3. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testSubDeviceRegister][响应消息: {}]", response);
// 4. 断开连接
disconnect(client);
}
// ===================== 批量上报测试 =====================
/**
* 批量上报属性测试(网关 + 子设备)
* <p>
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
*/
@Test
public void testPropertyPackPost() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testPropertyPackPost][连接认证成功]");
// 2.1 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
subscribeReply(client, replyTopic);
// 2.2 构建【网关设备】自身属性
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
.put("temperature", 25.5)
.build();
// 2.3 构建【网关设备】自身事件
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
gatewayEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("statusReport", gatewayEvent)
.build();
// 2.4 构建【网关子设备】属性
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
.put("power", 100)
.build();
// 2.5 构建【网关子设备】事件
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
subDeviceEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("healthCheck", subDeviceEvent)
.build();
// 2.6 构建子设备数据
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
subDeviceData.setProperties(subDeviceProperties);
subDeviceData.setEvents(subDeviceEvents);
// 2.7 构建请求消息
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
params.setProperties(gatewayProperties);
params.setEvents(gatewayEvents);
params.setSubDevices(List.of(subDeviceData));
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
params,
null, null, null);
// 3. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testPropertyPackPost][响应消息: {}]", response);
// 4. 断开连接
disconnect(client);
}
// ===================== 辅助方法 =====================
/**
* 创建 MQTT 客户端
*
* @param authInfo 认证信息
* @return MQTT 客户端
*/
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
MqttClientOptions options = new MqttClientOptions()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword())
.setCleanSession(true)
.setKeepAliveInterval(60);
return MqttClient.create(vertx, options);
}
/**
* 连接并认证网关设备
*
* @return 已认证的 MQTT 客户端
*/
private MqttClient connectAndAuth() throws Exception {
// 1. 创建客户端并连接
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
MqttClient client = connect(authInfo);
// 2.1 连接
CompletableFuture<MqttClient> future = new CompletableFuture<>();
client.connect(SERVER_PORT, SERVER_HOST)
.onComplete(ar -> {
if (ar.succeeded()) {
future.complete(client);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2.2 等待连接结果
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* 订阅响应主题
*
* @param client MQTT 客户端
* @param replyTopic 响应主题
*/
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
// 1. 订阅响应主题
CompletableFuture<Void> future = new CompletableFuture<>();
client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
future.complete(null);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2. 等待订阅结果
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* 发布消息并等待响应
*
* @param client MQTT 客户端
* @param topic 发布主题
* @param request 请求消息
* @return 响应消息
*/
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
// 1. 设置消息处理器,接收响应
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
client.publishHandler(message -> {
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
message.topicName(), message.payload().toString());
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
future.complete(response);
});
// 2. 编码并发布消息
byte[] payload = CODEC.encode(request);
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
CODEC.type(), topic, new String(payload));
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[publishAndWaitReply][消息发布成功messageId={}]", ar.result());
} else {
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
future.completeExceptionally(ar.cause());
}
});
// 3. 等待响应(超时返回 null
try {
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[publishAndWaitReply][等待响应超时或失败]");
return null;
}
}
/**
* 断开连接
*
* @param client MQTT 客户端
*/
private void disconnect(MqttClient client) throws Exception {
// 1. 断开连接
CompletableFuture<Void> future = new CompletableFuture<>();
client.disconnect()
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[disconnect][断开连接成功]");
future.complete(null);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2. 等待断开结果
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,332 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关子设备 MQTT 协议集成测试(手动测试)
*
* <p>测试场景子设备IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
*
* <p><b>重要说明子设备无法直接连接平台所有请求均由网关设备Gateway代为转发。</b>
* <p>网关设备转发子设备请求时,使用子设备自己的认证信息连接。
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务MQTT 端口 1883</li>
* <li>确保子设备已通过 {@link IotGatewayDeviceMqttProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testAuth()} - 子设备连接认证</li>
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
* </ul>
* </li>
* </ol>
*
* <p>注意MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
* 认证成功后同一连接上的后续请求无需再携带认证信息
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 1883;
private static final int TIMEOUT_SECONDS = 10;
private static Vertx vertx;
// ===================== 编解码器MQTT 使用 Alink 协议) =====================
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();
}
@AfterAll
public static void tearDown() {
if (vertx != null) {
vertx.close();
}
}
// ===================== 连接认证测试 =====================
/**
* 子设备认证测试:获取子设备 Token
*/
@Test
public void testAuth() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
// 1. 构建认证信息
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
// 2. 创建客户端并连接
MqttClient client = connect(authInfo);
client.connect(SERVER_PORT, SERVER_HOST)
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
// 断开连接
client.disconnect()
.onComplete(disconnectAr -> {
if (disconnectAr.succeeded()) {
log.info("[testAuth][断开连接成功]");
} else {
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
}
latch.countDown();
});
} else {
log.error("[testAuth][连接失败]", ar.cause());
latch.countDown();
}
});
// 3. 等待测试完成
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!completed) {
log.warn("[testAuth][测试超时]");
}
}
// ===================== 子设备属性上报测试 =====================
/**
* 子设备属性上报测试
*/
@Test
public void testPropertyPost() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testPropertyPost][连接认证成功]");
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
// 2. 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
subscribeReply(client, replyTopic);
// 3. 构建属性上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("power", 100)
.put("status", "online")
.put("temperature", 36.5)
.build()),
null, null, null);
// 4. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testPropertyPost][响应消息: {}]", response);
// 5. 断开连接
disconnect(client);
}
// ===================== 子设备事件上报测试 =====================
/**
* 子设备事件上报测试
*/
@Test
public void testEventPost() throws Exception {
// 1. 连接并认证
MqttClient client = connectAndAuth();
log.info("[testEventPost][连接认证成功]");
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
// 2. 订阅 _reply 主题
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
subscribeReply(client, replyTopic);
// 3. 构建事件上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
IotDeviceEventPostReqDTO.of(
"alarm",
MapUtil.<String, Object>builder()
.put("level", "warning")
.put("message", "temperature too high")
.put("threshold", 40)
.put("current", 42)
.build(),
System.currentTimeMillis()),
null, null, null);
// 4. 发布消息并等待响应
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testEventPost][响应消息: {}]", response);
// 5. 断开连接
disconnect(client);
}
// ===================== 辅助方法 =====================
/**
* 创建 MQTT 客户端
*
* @param authInfo 认证信息
* @return MQTT 客户端
*/
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
MqttClientOptions options = new MqttClientOptions()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword())
.setCleanSession(true)
.setKeepAliveInterval(60);
return MqttClient.create(vertx, options);
}
/**
* 连接并认证子设备
*
* @return 已认证的 MQTT 客户端
*/
private MqttClient connectAndAuth() throws Exception {
// 1. 创建客户端并连接
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
MqttClient client = connect(authInfo);
// 2.1 连接
CompletableFuture<MqttClient> future = new CompletableFuture<>();
client.connect(SERVER_PORT, SERVER_HOST)
.onComplete(ar -> {
if (ar.succeeded()) {
future.complete(client);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2.2 等待连接结果
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* 订阅响应主题
*
* @param client MQTT 客户端
* @param replyTopic 响应主题
*/
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
// 1. 订阅响应主题
CompletableFuture<Void> future = new CompletableFuture<>();
client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
future.complete(null);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2. 等待订阅结果
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* 发布消息并等待响应
*
* @param client MQTT 客户端
* @param topic 发布主题
* @param request 请求消息
* @return 响应消息
*/
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
// 1. 设置消息处理器,接收响应
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
client.publishHandler(message -> {
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
message.topicName(), message.payload().toString());
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
future.complete(response);
});
// 2. 编码并发布消息
byte[] payload = CODEC.encode(request);
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
CODEC.type(), topic, new String(payload));
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[publishAndWaitReply][消息发布成功messageId={}]", ar.result());
} else {
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
future.completeExceptionally(ar.cause());
}
});
// 3. 等待响应(超时返回 null
try {
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[publishAndWaitReply][等待响应超时或失败]");
return null;
}
}
/**
* 断开连接
*
* @param client MQTT 客户端
*/
private void disconnect(MqttClient client) throws Exception {
// 1. 断开连接
CompletableFuture<Void> future = new CompletableFuture<>();
client.disconnect()
.onComplete(ar -> {
if (ar.succeeded()) {
log.info("[disconnect][断开连接成功]");
future.complete(null);
} else {
future.completeExceptionally(ar.cause());
}
});
// 2. 等待断开结果
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,278 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* IoT 直连设备 TCP 协议集成测试(手动测试)
*
* <p>测试场景直连设备IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台
*
* <p>支持两种编解码格式:
* <ul>
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
* </ul>
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务TCP 端口 8091</li>
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testAuth()} - 设备认证</li>
* <li>{@link #testDeviceRegister()} - 设备动态注册(一型一密)</li>
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
* <li>{@link #testEventPost()} - 设备事件上报</li>
* </ul>
* </li>
* </ol>
*
* <p>注意TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotDirectDeviceTcpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8091;
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
// ===================== 认证测试 =====================
/**
* 认证测试:获取设备 Token
*/
@Test
public void testAuth() throws Exception {
// 1.1 构建认证消息
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
// 1.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
// 2.1 发送请求
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
byte[] responseBytes = sendAndReceive(socket, payload);
// 2.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testAuth][响应消息: {}]", response);
} else {
log.warn("[testAuth][未收到响应]");
}
}
}
// ===================== 动态注册测试 =====================
/**
* 直连设备动态注册测试(一型一密)
* <p>
* 使用产品密钥productSecret验证身份成功后返回设备密钥deviceSecret
* <p>
* 注意:此接口不需要认证
*/
@Test
public void testDeviceRegister() throws Exception {
// 1.1 构建注册消息
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
registerReqDTO.setProductKey(PRODUCT_KEY);
registerReqDTO.setDeviceName("test-tcp-" + System.currentTimeMillis());
registerReqDTO.setProductSecret("test-product-secret");
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
// 1.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
// 2.1 发送请求
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
byte[] responseBytes = sendAndReceive(socket, payload);
// 2.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testDeviceRegister][响应消息: {}]", response);
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
} else {
log.warn("[testDeviceRegister][未收到响应]");
}
}
}
// ===================== 直连设备属性上报测试 =====================
/**
* 属性上报测试
*/
@Test
public void testPropertyPost() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testPropertyPost][认证响应: {}]", authResponse);
// 2.1 构建属性上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("width", 1)
.put("height", "2")
.build()),
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testPropertyPost][响应消息: {}]", response);
} else {
log.warn("[testPropertyPost][未收到响应]");
}
}
}
// ===================== 直连设备事件上报测试 =====================
/**
* 事件上报测试
*/
@Test
public void testEventPost() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testEventPost][认证响应: {}]", authResponse);
// 2.1 构建事件上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
IotDeviceEventPostReqDTO.of(
"eat",
MapUtil.<String, Object>builder().put("rice", 3).build(),
System.currentTimeMillis()),
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testEventPost][响应消息: {}]", response);
} else {
log.warn("[testEventPost][未收到响应]");
}
}
}
// ===================== 辅助方法 =====================
/**
* 执行设备认证
*
* @param socket TCP 连接
* @return 认证响应消息
*/
private IotDeviceMessage authenticate(Socket socket) throws Exception {
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
byte[] payload = CODEC.encode(request);
byte[] responseBytes = sendAndReceive(socket, payload);
if (responseBytes != null) {
log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]",
responseBytes.length,
String.format("%02X", responseBytes[0]),
HexUtil.encodeHexStr(responseBytes));
return CODEC.decode(responseBytes);
}
return null;
}
/**
* 发送 TCP 请求并接收响应
*
* @param socket TCP Socket
* @param payload 请求数据
* @return 响应数据
*/
private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
// 1. 发送请求
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
out.write(payload);
out.flush();
// 2.1 等待一小段时间让服务器处理
Thread.sleep(100);
// 2.2 接收响应
byte[] buffer = new byte[4096];
try {
int length = in.read(buffer);
if (length > 0) {
byte[] response = new byte[length];
System.arraycopy(buffer, 0, response, 0, length);
return response;
}
return null;
} catch (java.net.SocketTimeoutException e) {
log.warn("[sendAndReceive][接收响应超时]");
return null;
}
}
}

View File

@@ -0,0 +1,397 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* IoT 网关设备 TCP 协议集成测试(手动测试)
*
* <p>测试场景网关设备IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 TCP 协议管理子设备拓扑关系
*
* <p>支持两种编解码格式:
* <ul>
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
* </ul>
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务TCP 端口 8091</li>
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testAuth()} - 网关设备认证</li>
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
* </ul>
* </li>
* </ol>
*
* <p>注意TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotGatewayDeviceTcpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8091;
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// ===================== 认证测试 =====================
/**
* 网关设备认证测试:获取网关设备 Token
*/
@Test
public void testAuth() throws Exception {
// 1.1 构建认证消息
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
// 1.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
// 2.1 发送请求
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
byte[] responseBytes = sendAndReceive(socket, payload);
// 2.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testAuth][响应消息: {}]", response);
} else {
log.warn("[testAuth][未收到响应]");
}
}
}
// ===================== 拓扑管理测试 =====================
/**
* 添加子设备拓扑关系测试
*/
@Test
public void testTopoAdd() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testTopoAdd][认证响应: {}]", authResponse);
// 2.1 构建子设备认证信息
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
.setClientId(subAuthInfo.getClientId())
.setUsername(subAuthInfo.getUsername())
.setPassword(subAuthInfo.getPassword());
// 2.2 构建请求参数
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
params.setSubDevices(Collections.singletonList(subDeviceAuth));
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
params,
null, null, null);
// 2.3 编码
byte[] payload = CODEC.encode(request);
log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testTopoAdd][响应消息: {}]", response);
} else {
log.warn("[testTopoAdd][未收到响应]");
}
}
}
/**
* 删除子设备拓扑关系测试
*/
@Test
public void testTopoDelete() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testTopoDelete][认证响应: {}]", authResponse);
// 2.1 构建请求参数
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
params.setSubDevices(Collections.singletonList(
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
params,
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testTopoDelete][响应消息: {}]", response);
} else {
log.warn("[testTopoDelete][未收到响应]");
}
}
}
/**
* 获取子设备拓扑关系测试
*/
@Test
public void testTopoGet() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testTopoGet][认证响应: {}]", authResponse);
// 2.1 构建请求参数
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
params,
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testTopoGet][响应消息: {}]", response);
} else {
log.warn("[testTopoGet][未收到响应]");
}
}
}
// ===================== 子设备注册测试 =====================
/**
* 子设备动态注册测试
*/
@Test
public void testSubDeviceRegister() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testSubDeviceRegister][认证响应: {}]", authResponse);
// 2.1 构建请求参数
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
subDevice.setDeviceName("mougezishebei");
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
Collections.singletonList(subDevice),
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testSubDeviceRegister][响应消息: {}]", response);
} else {
log.warn("[testSubDeviceRegister][未收到响应]");
}
}
}
// ===================== 批量上报测试 =====================
/**
* 批量上报属性测试(网关 + 子设备)
*/
@Test
public void testPropertyPackPost() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testPropertyPackPost][认证响应: {}]", authResponse);
// 2.1 构建【网关设备】自身属性
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
.put("temperature", 25.5)
.build();
// 2.2 构建【网关设备】自身事件
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
gatewayEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("statusReport", gatewayEvent)
.build();
// 2.3 构建【网关子设备】属性
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
.put("power", 100)
.build();
// 2.4 构建【网关子设备】事件
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
subDeviceEvent.setTime(System.currentTimeMillis());
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
.put("healthCheck", subDeviceEvent)
.build();
// 2.5 构建子设备数据
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
subDeviceData.setProperties(subDeviceProperties);
subDeviceData.setEvents(subDeviceEvents);
// 2.6 构建请求参数
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
params.setProperties(gatewayProperties);
params.setEvents(gatewayEvents);
params.setSubDevices(List.of(subDeviceData));
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
params,
null, null, null);
// 2.7 编码
byte[] payload = CODEC.encode(request);
log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testPropertyPackPost][响应消息: {}]", response);
} else {
log.warn("[testPropertyPackPost][未收到响应]");
}
}
}
// ===================== 辅助方法 =====================
/**
* 执行网关设备认证
*
* @param socket TCP 连接
* @return 认证响应消息
*/
private IotDeviceMessage authenticate(Socket socket) throws Exception {
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
byte[] payload = CODEC.encode(request);
byte[] responseBytes = sendAndReceive(socket, payload);
if (responseBytes != null) {
return CODEC.decode(responseBytes);
}
return null;
}
/**
* 发送 TCP 请求并接收响应
*
* @param socket TCP Socket
* @param payload 请求数据
* @return 响应数据
*/
private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
// 1. 发送请求
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
out.write(payload);
out.flush();
// 2.1 等待一小段时间让服务器处理
Thread.sleep(100);
// 2.2 接收响应
byte[] buffer = new byte[4096];
try {
int length = in.read(buffer);
if (length > 0) {
byte[] response = new byte[length];
System.arraycopy(buffer, 0, response, 0, length);
return response;
}
return null;
} catch (java.net.SocketTimeoutException e) {
log.warn("[sendAndReceive][接收响应超时]");
return null;
}
}
}

View File

@@ -0,0 +1,245 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* IoT 网关子设备 TCP 协议集成测试(手动测试)
*
* <p>测试场景子设备IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
*
* <p><b>重要说明子设备无法直接连接平台所有请求均由网关设备Gateway代为转发。</b>
*
* <p>支持两种编解码格式:
* <ul>
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
* </ul>
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务TCP 端口 8091</li>
* <li>确保子设备已通过 {@link IotGatewayDeviceTcpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
* <li>运行以下测试方法:
* <ul>
* <li>{@link #testAuth()} - 子设备认证</li>
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
* </ul>
* </li>
* </ol>
*
* <p>注意TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
*
* @author 芋道源码
*/
@Slf4j
@Disabled
public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8091;
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// ===================== 认证测试 =====================
/**
* 子设备认证测试:获取子设备 Token
*/
@Test
public void testAuth() throws Exception {
// 1.1 构建认证消息
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
// 1.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
// 2.1 发送请求
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
byte[] responseBytes = sendAndReceive(socket, payload);
// 2.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testAuth][响应消息: {}]", response);
} else {
log.warn("[testAuth][未收到响应]");
}
}
}
// ===================== 子设备属性上报测试 =====================
/**
* 子设备属性上报测试
*/
@Test
public void testPropertyPost() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testPropertyPost][认证响应: {}]", authResponse);
// 2.1 构建属性上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
.put("power", 100)
.put("status", "online")
.put("temperature", 36.5)
.build()),
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testPropertyPost][响应消息: {}]", response);
} else {
log.warn("[testPropertyPost][未收到响应]");
}
}
}
// ===================== 子设备事件上报测试 =====================
/**
* 子设备事件上报测试
*/
@Test
public void testEventPost() throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
socket.setSoTimeout(TIMEOUT_MS);
// 1. 先进行认证
IotDeviceMessage authResponse = authenticate(socket);
log.info("[testEventPost][认证响应: {}]", authResponse);
// 2.1 构建事件上报消息
IotDeviceMessage request = IotDeviceMessage.of(
IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
IotDeviceEventPostReqDTO.of(
"alarm",
MapUtil.<String, Object>builder()
.put("level", "warning")
.put("message", "temperature too high")
.put("threshold", 40)
.put("current", 42)
.build(),
System.currentTimeMillis()),
null, null, null);
// 2.2 编码
byte[] payload = CODEC.encode(request);
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
// 3.1 发送请求
byte[] responseBytes = sendAndReceive(socket, payload);
// 3.2 解码响应
if (responseBytes != null) {
IotDeviceMessage response = CODEC.decode(responseBytes);
log.info("[testEventPost][响应消息: {}]", response);
} else {
log.warn("[testEventPost][未收到响应]");
}
}
}
// ===================== 辅助方法 =====================
/**
* 执行子设备认证
*
* @param socket TCP 连接
* @return 认证响应消息
*/
private IotDeviceMessage authenticate(Socket socket) throws Exception {
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
byte[] payload = CODEC.encode(request);
byte[] responseBytes = sendAndReceive(socket, payload);
if (responseBytes != null) {
return CODEC.decode(responseBytes);
}
return null;
}
/**
* 发送 TCP 请求并接收响应
*
* @param socket TCP Socket
* @param payload 请求数据
* @return 响应数据
*/
private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
// 1. 发送请求
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
out.write(payload);
out.flush();
// 2.1 等待一小段时间让服务器处理
Thread.sleep(100);
// 2.2 接收响应
byte[] buffer = new byte[4096];
try {
int length = in.read(buffer);
if (length > 0) {
byte[] response = new byte[length];
System.arraycopy(buffer, 0, response, 0, length);
return response;
}
return null;
} catch (java.net.SocketTimeoutException e) {
log.warn("[sendAndReceive][接收响应超时]");
return null;
}
}
}

View File

@@ -0,0 +1,193 @@
# TCP 二进制协议数据包格式说明
## 1. 协议概述
TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。
### 1.1 协议特点
- **高效传输**:完全二进制格式,减少数据传输量
- **版本控制**:内置协议版本号,支持协议升级
- **类型安全**:明确的消息类型标识
- **简洁设计**:去除冗余字段,协议更加精简
- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容
## 2. 协议格式
### 2.1 整体结构
```
+--------+--------+--------+---------------------------+--------+--------+
| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) |
+--------+--------+--------+---------------------------+--------+--------+
| 消息 ID 长度(2字节) | 消息 ID (变长字符串) |
+--------+--------+--------+--------+--------+--------+--------+--------+
| 方法名长度(2字节) | 方法名(变长字符串) |
+--------+--------+--------+--------+--------+--------+--------+--------+
| 消息体数据(变长) |
+--------+--------+--------+--------+--------+--------+--------+--------+
```
### 2.2 字段详细说明
| 字段 | 长度 | 类型 | 说明 |
|------|------|------|------|
| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 |
| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 |
| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 |
| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) |
| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 |
| 消息 ID | 变长 | string | 消息唯一标识符UTF-8编码 |
| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 |
| 方法名 | 变长 | string | 消息方法名UTF-8编码 |
| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 |
**⚠️ 重要说明**deviceId 不包含在协议中,由服务器根据连接上下文自动设置
### 2.3 协议常量定义
```java
// 协议标识
private static final byte MAGIC_NUMBER = (byte) 0x7E;
private static final byte PROTOCOL_VERSION = (byte) 0x01;
// 消息类型
private static final byte REQUEST = (byte) 0x01; // 请求消息
private static final byte RESPONSE = (byte) 0x02; // 响应消息
// 协议长度
private static final int HEADER_FIXED_LENGTH = 7; // 固定头部长度
private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度
```
## 3. 消息类型和格式
### 3.1 请求消息 (REQUEST - 0x01)
请求消息用于设备向服务器发送数据或请求。
#### 3.1.1 消息体格式
```
消息体 = params 数据(JSON格式)
```
#### 3.1.2 示例:设备认证请求
**消息内容:**
- 消息 ID: `auth_1704067200000_123`
- 方法名: `auth`
- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}`
**二进制数据包结构:**
```
7E // 魔术字 (0x7E)
01 // 版本号 (0x01)
01 // 消息类型 (REQUEST)
00 00 00 89 // 消息长度 (137字节)
00 19 // 消息 ID 长度 (25字节)
61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123"
36 37 32 30 30 30 30 30 5F 31
32 33
00 04 // 方法名长度 (4字节)
61 75 74 68 // 方法名: "auth"
7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据
22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001",
30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName",
6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"}
64 75 63 74 4B 65 79 5F 64 65
76 69 63 65 4E 61 6D 65 22 2C
22 70 61 73 73 77 6F 72 64 22
3A 22 64 65 76 69 63 65 5F 70
61 73 73 77 6F 72 64 22 7D
```
#### 3.1.3 示例:属性数据上报
**消息内容:**
- 消息 ID: `property_1704067200000_456`
- 方法名: `thing.property.post`
- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}`
### 3.2 响应消息 (RESPONSE - 0x02)
响应消息用于服务器向设备回复请求结果。
#### 3.2.1 消息体格式
```
消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON)
```
#### 3.2.2 字段说明
| 字段 | 长度 | 类型 | 说明 |
|------|------|------|------|
| 响应码 | 4字节 | int | HTTP状态码风格0=成功,其他=错误 |
| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 |
| 响应消息 | 变长 | string | 响应提示信息UTF-8编码 |
| 响应数据 | 变长 | binary | JSON格式的响应数据可选 |
#### 3.2.3 示例:认证成功响应
**消息内容:**
- 消息 ID: `auth_response_1704067200000_123`
- 方法名: `auth`
- 响应码: `0`
- 响应消息: `认证成功`
- 响应数据: `{"success":true,"message":"认证成功"}`
**二进制数据包结构:**
```
7E // 魔术字 (0x7E)
01 // 版本号 (0x01)
02 // 消息类型 (RESPONSE)
00 00 00 A4 // 消息长度 (164字节)
00 22 // 消息 ID 长度 (34字节)
61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123"
6E 73 65 5F 31 37 30 34 30 36
37 32 30 30 30 30 30 5F 31 32
33
00 04 // 方法名长度 (4字节)
61 75 74 68 // 方法名: "auth"
00 00 00 00 // 响应码 (0 = 成功)
00 0C // 响应消息长度 (12字节)
E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8)
8A 9F
7B 22 73 75 63 63 65 73 73 22 // JSON响应数据
3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"}
73 61 67 65 22 3A 22 E8 AE A4
E8 AF 81 E6 88 90 E5 8A 9F 22
7D
```
## 4. 编解码器标识
```java
public static final String TYPE = "TCP_BINARY";
```
## 5. 协议优势
- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量
- **解析高效**:直接二进制操作,减少字符串转换开销
- **类型安全**:明确的消息类型和字段定义
- **设计简洁**:去除冗余字段,协议更加精简高效
- **版本控制**:内置版本号支持协议升级
## 6. 与 JSON 协议对比
| 特性 | 二进制协议 | JSON协议 |
|------|-------------|--------|
| 数据大小 | 小节省30-50% | 大 |
| 解析性能 | 高 | 中等 |
| 网络开销 | 低 | 高 |
| 可读性 | 差 | 优秀 |
| 调试难度 | 高 | 低 |
| 扩展性 | 良好 | 优秀 |
**推荐场景**
-**高频数据传输**:传感器数据实时上报
-**带宽受限环境**:移动网络、卫星通信
-**性能要求高**:需要低延迟、高吞吐的场景
-**设备资源有限**:嵌入式设备、低功耗设备
-**开发调试阶段**:调试困难,建议使用 JSON 协议
-**快速原型开发**:开发效率低

Some files were not shown because too many files have changed in this diff Show More