mirror of
https://github.com/YunaiV/ruoyi-vue-pro.git
synced 2026-04-19 09:28:37 +00:00
Merge branch 'feature/iot-protocol' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/iot-modbus
# Conflicts: # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java # yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
This commit is contained in:
@@ -75,7 +75,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
|
||||
IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
|
||||
if (product != null) {
|
||||
deviceDTO.setCodecType(product.getCodecType());
|
||||
deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -67,10 +67,15 @@ public class IotProductRespVO {
|
||||
@DictFormat(DictTypeConstants.NET_TYPE)
|
||||
private Integer netType;
|
||||
|
||||
@Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@ExcelProperty(value = "数据格式", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.CODEC_TYPE)
|
||||
private String codecType;
|
||||
@Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
|
||||
@ExcelProperty(value = "协议类型", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.PROTOCOL_TYPE)
|
||||
private String protocolType;
|
||||
|
||||
@Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
|
||||
@ExcelProperty(value = "序列化类型", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.SERIALIZE_TYPE)
|
||||
private String serializeType;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("创建时间")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -44,9 +46,15 @@ public class IotProductSaveReqVO {
|
||||
@InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
|
||||
private Integer netType;
|
||||
|
||||
@Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@NotEmpty(message = "数据格式不能为空")
|
||||
private String codecType;
|
||||
@Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
|
||||
@InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}")
|
||||
@NotEmpty(message = "协议类型不能为空")
|
||||
private String protocolType;
|
||||
|
||||
@Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
|
||||
@InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}")
|
||||
@NotEmpty(message = "序列化类型不能为空")
|
||||
private String serializeType;
|
||||
|
||||
@Schema(description = "是否开启动态注册", example = "false")
|
||||
@NotNull(message = "是否开启动态注册不能为空")
|
||||
|
||||
@@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO {
|
||||
*/
|
||||
private Integer netType;
|
||||
/**
|
||||
* 数据格式(编解码器类型)
|
||||
* 协议类型
|
||||
* <p>
|
||||
* 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE}
|
||||
*
|
||||
* 目的:用于 gateway-server 解析消息格式
|
||||
* 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum}
|
||||
*/
|
||||
private String codecType;
|
||||
private String protocolType;
|
||||
/**
|
||||
* 序列化类型
|
||||
* <p>
|
||||
* 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum}
|
||||
*/
|
||||
private String serializeType;
|
||||
|
||||
}
|
||||
@@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums;
|
||||
public class DictTypeConstants {
|
||||
|
||||
public static final String NET_TYPE = "iot_net_type";
|
||||
public static final String LOCATION_TYPE = "iot_location_type";
|
||||
public static final String CODEC_TYPE = "iot_codec_type";
|
||||
public static final String PROTOCOL_TYPE = "iot_protocol_type";
|
||||
public static final String SERIALIZE_TYPE = "iot_serialize_type";
|
||||
|
||||
public static final String PRODUCT_STATUS = "iot_product_status";
|
||||
public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type";
|
||||
|
||||
@@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
@@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
|
||||
throw exception(DEVICE_REGISTER_DISABLED);
|
||||
}
|
||||
// 1.3 验证 productSecret
|
||||
if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
|
||||
// 1.3 【重要!!!】验证签名
|
||||
if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(),
|
||||
product.getProductSecret(), reqDTO.getSign())) {
|
||||
throw exception(DEVICE_REGISTER_SECRET_INVALID);
|
||||
}
|
||||
return TenantUtils.execute(product.getTenantId(), () -> {
|
||||
|
||||
@@ -34,8 +34,12 @@ public class IotDeviceRespDTO {
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 编解码器类型
|
||||
* 协议类型
|
||||
*/
|
||||
private String codecType;
|
||||
private String protocolType;
|
||||
/**
|
||||
* 序列化类型
|
||||
*/
|
||||
private String serializeType;
|
||||
|
||||
}
|
||||
@@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO {
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 产品密钥
|
||||
* 注册签名
|
||||
*
|
||||
* @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
|
||||
*/
|
||||
@NotEmpty(message = "产品密钥不能为空")
|
||||
private String productSecret;
|
||||
@NotEmpty(message = "签名不能为空")
|
||||
private String sign;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.module.iot.core.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.crypto.digest.HmacAlgorithm;
|
||||
|
||||
/**
|
||||
* IoT 产品【动态注册】认证工具类
|
||||
* <p>
|
||||
* 用于一型一密场景,使用 productSecret 生成签名
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class IotProductAuthUtils {
|
||||
|
||||
/**
|
||||
* 生成设备动态注册签名
|
||||
*
|
||||
* @param productKey 产品标识
|
||||
* @param deviceName 设备名称
|
||||
* @param productSecret 产品密钥
|
||||
* @return 签名
|
||||
*/
|
||||
public static String buildSign(String productKey, String deviceName, String productSecret) {
|
||||
String content = buildContent(productKey, deviceName);
|
||||
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
|
||||
.digestHex(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证设备动态注册签名
|
||||
*
|
||||
* @param productKey 产品标识
|
||||
* @param deviceName 设备名称
|
||||
* @param productSecret 产品密钥
|
||||
* @param sign 待验证的签名
|
||||
* @return 是否验证通过
|
||||
*/
|
||||
public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
|
||||
String expectedSign = buildSign(productKey, deviceName, productSecret);
|
||||
return expectedSign.equals(sign);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建签名内容
|
||||
*
|
||||
* @param productKey 产品标识
|
||||
* @param deviceName 设备名称
|
||||
* @return 签名内容
|
||||
*/
|
||||
private static String buildContent(String productKey, String deviceName) {
|
||||
return "deviceName" + deviceName + "productKey" + productKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
|
||||
/**
|
||||
* {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IotDeviceMessageCodec {
|
||||
|
||||
/**
|
||||
* 编码消息
|
||||
*
|
||||
* @param message 消息
|
||||
* @return 编码后的消息内容
|
||||
*/
|
||||
byte[] encode(IotDeviceMessage message);
|
||||
|
||||
/**
|
||||
* 解码消息
|
||||
*
|
||||
* @param bytes 消息内容
|
||||
* @return 解码后的消息内容
|
||||
*/
|
||||
IotDeviceMessage decode(byte[] bytes);
|
||||
|
||||
/**
|
||||
* @return 数据格式(编码器类型)
|
||||
*/
|
||||
String type();
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec.alink;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 阿里云 Alink {@link IotDeviceMessage} 的编解码器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
public static final String TYPE = "Alink";
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class AlinkMessage {
|
||||
|
||||
public static final String VERSION_1 = "1.0";
|
||||
|
||||
/**
|
||||
* 消息 ID,且每个消息 ID 在当前设备具有唯一性
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
private String method;
|
||||
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
private Object params;
|
||||
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private Object data;
|
||||
/**
|
||||
* 响应错误码
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 响应提示
|
||||
*
|
||||
* 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode(IotDeviceMessage message) {
|
||||
AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
|
||||
message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg());
|
||||
return JsonUtils.toJsonByte(alinkMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public IotDeviceMessage decode(byte[] bytes) {
|
||||
AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class);
|
||||
Assert.notNull(alinkMessage, "消息不能为空");
|
||||
Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0");
|
||||
return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(),
|
||||
alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 提供设备接入的各种数据(请求、响应)的编解码
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec;
|
||||
@@ -1,286 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器
|
||||
* <p>
|
||||
* 二进制协议格式(所有数值使用大端序):
|
||||
*
|
||||
* <pre>
|
||||
* +--------+--------+--------+---------------------------+--------+--------+
|
||||
* | 魔术字 | 版本号 | 消息类型| 消息长度(4 字节) |
|
||||
* +--------+--------+--------+---------------------------+--------+--------+
|
||||
* | 消息 ID 长度(2 字节) | 消息 ID (变长字符串) |
|
||||
* +--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
* | 方法名长度(2 字节) | 方法名(变长字符串) |
|
||||
* +--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
* | 消息体数据(变长) |
|
||||
* +--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
* </pre>
|
||||
* <p>
|
||||
* 消息体格式:
|
||||
* - 请求消息:params 数据(JSON)
|
||||
* - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON)
|
||||
* <p>
|
||||
* 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
public static final String TYPE = "TcpBinary";
|
||||
|
||||
/**
|
||||
* 协议魔术字,用于协议识别
|
||||
*/
|
||||
private static final byte MAGIC_NUMBER = (byte) 0x7E;
|
||||
|
||||
/**
|
||||
* 协议版本号
|
||||
*/
|
||||
private static final byte PROTOCOL_VERSION = (byte) 0x01;
|
||||
|
||||
/**
|
||||
* 请求消息类型
|
||||
*/
|
||||
private static final byte REQUEST = (byte) 0x01;
|
||||
|
||||
/**
|
||||
* 响应消息类型
|
||||
*/
|
||||
private static final byte RESPONSE = (byte) 0x02;
|
||||
|
||||
/**
|
||||
* 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度)
|
||||
*/
|
||||
private static final int HEADER_FIXED_LENGTH = 7;
|
||||
|
||||
/**
|
||||
* 最小消息长度(头部 + 消息ID长度 + 方法名长度)
|
||||
*/
|
||||
private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4;
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode(IotDeviceMessage message) {
|
||||
Assert.notNull(message, "消息不能为空");
|
||||
Assert.notBlank(message.getMethod(), "消息方法不能为空");
|
||||
try {
|
||||
// 1. 确定消息类型
|
||||
byte messageType = determineMessageType(message);
|
||||
// 2. 构建消息体
|
||||
byte[] bodyData = buildMessageBody(message, messageType);
|
||||
// 3. 构建完整消息
|
||||
return buildCompleteMessage(message, messageType, bodyData);
|
||||
} catch (Exception e) {
|
||||
log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e);
|
||||
throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceMessage decode(byte[] bytes) {
|
||||
Assert.notNull(bytes, "待解码数据不能为空");
|
||||
Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足");
|
||||
try {
|
||||
Buffer buffer = Buffer.buffer(bytes);
|
||||
// 解析协议头部和消息内容
|
||||
int index = 0;
|
||||
// 1. 验证魔术字
|
||||
byte magic = buffer.getByte(index++);
|
||||
Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic);
|
||||
|
||||
// 2. 验证版本号
|
||||
byte version = buffer.getByte(index++);
|
||||
Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version);
|
||||
|
||||
// 3. 读取消息类型
|
||||
byte messageType = buffer.getByte(index++);
|
||||
// 直接验证消息类型,无需抽取方法
|
||||
Assert.isTrue(messageType == REQUEST || messageType == RESPONSE,
|
||||
"无效的消息类型: " + messageType);
|
||||
|
||||
// 4. 读取消息长度
|
||||
int messageLength = buffer.getInt(index);
|
||||
index += 4;
|
||||
Assert.isTrue(messageLength == buffer.length(),
|
||||
"消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length());
|
||||
|
||||
// 5. 读取消息 ID
|
||||
short messageIdLength = buffer.getShort(index);
|
||||
index += 2;
|
||||
String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name());
|
||||
index += messageIdLength;
|
||||
|
||||
// 6. 读取方法名
|
||||
short methodLength = buffer.getShort(index);
|
||||
index += 2;
|
||||
String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name());
|
||||
index += methodLength;
|
||||
|
||||
// 7. 解析消息体
|
||||
return parseMessageBody(buffer, index, messageType, messageId, method);
|
||||
} catch (Exception e) {
|
||||
log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e);
|
||||
throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定消息类型
|
||||
* 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息
|
||||
*/
|
||||
private byte determineMessageType(IotDeviceMessage message) {
|
||||
// 判断是否为响应消息:有响应码或响应消息时为响应
|
||||
if (message.getCode() != null) {
|
||||
return RESPONSE;
|
||||
}
|
||||
// 默认为请求消息
|
||||
return REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建消息体
|
||||
*/
|
||||
private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) {
|
||||
Buffer bodyBuffer = Buffer.buffer();
|
||||
if (messageType == RESPONSE) {
|
||||
// code
|
||||
bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0);
|
||||
// msg
|
||||
String msg = message.getMsg() != null ? message.getMsg() : "";
|
||||
byte[] msgBytes = StrUtil.utf8Bytes(msg);
|
||||
bodyBuffer.appendShort((short) msgBytes.length);
|
||||
bodyBuffer.appendBytes(msgBytes);
|
||||
// data
|
||||
if (message.getData() != null) {
|
||||
bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData()));
|
||||
}
|
||||
} else {
|
||||
// 请求消息只处理 params 参数
|
||||
// TODO @haohao:如果为空,是不是得写个长度 0 哈?
|
||||
if (message.getParams() != null) {
|
||||
bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams()));
|
||||
}
|
||||
}
|
||||
return bodyBuffer.getBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整消息
|
||||
*/
|
||||
private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) {
|
||||
Buffer buffer = Buffer.buffer();
|
||||
// 1. 写入协议头部
|
||||
buffer.appendByte(MAGIC_NUMBER);
|
||||
buffer.appendByte(PROTOCOL_VERSION);
|
||||
buffer.appendByte(messageType);
|
||||
// 2. 预留消息长度位置(在 5. 更新消息长度)
|
||||
int lengthPosition = buffer.length();
|
||||
buffer.appendInt(0);
|
||||
// 3. 写入消息 ID
|
||||
String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId()
|
||||
: IotDeviceMessageUtils.generateMessageId();
|
||||
byte[] messageIdBytes = StrUtil.utf8Bytes(messageId);
|
||||
buffer.appendShort((short) messageIdBytes.length);
|
||||
buffer.appendBytes(messageIdBytes);
|
||||
// 4. 写入方法名
|
||||
byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod());
|
||||
buffer.appendShort((short) methodBytes.length);
|
||||
buffer.appendBytes(methodBytes);
|
||||
// 5. 写入消息体
|
||||
buffer.appendBytes(bodyData);
|
||||
// 6. 更新消息长度
|
||||
buffer.setInt(lengthPosition, buffer.length());
|
||||
return buffer.getBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析消息体
|
||||
*/
|
||||
private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType,
|
||||
String messageId, String method) {
|
||||
if (startIndex >= buffer.length()) {
|
||||
// 空消息体
|
||||
return IotDeviceMessage.of(messageId, method, null, null, null, null);
|
||||
}
|
||||
|
||||
if (messageType == RESPONSE) {
|
||||
// 响应消息:解析 code + msg + data
|
||||
return parseResponseMessage(buffer, startIndex, messageId, method);
|
||||
} else {
|
||||
// 请求消息:解析 payload
|
||||
Object payload = parseJsonData(buffer, startIndex, buffer.length());
|
||||
return IotDeviceMessage.of(messageId, method, payload, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应消息
|
||||
*/
|
||||
private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) {
|
||||
int index = startIndex;
|
||||
|
||||
// 1. 读取响应码
|
||||
Integer code = buffer.getInt(index);
|
||||
index += 4;
|
||||
|
||||
// 2. 读取响应消息
|
||||
short msgLength = buffer.getShort(index);
|
||||
index += 2;
|
||||
String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null;
|
||||
index += msgLength;
|
||||
|
||||
// 3. 读取响应数据
|
||||
Object data = null;
|
||||
if (index < buffer.length()) {
|
||||
data = parseJsonData(buffer, index, buffer.length());
|
||||
}
|
||||
|
||||
return IotDeviceMessage.of(messageId, method, null, data, code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 数据
|
||||
*/
|
||||
private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) {
|
||||
if (startIndex >= endIndex) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
|
||||
return JsonUtils.parseObject(jsonStr, Object.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e);
|
||||
return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速检测是否为二进制格式
|
||||
*
|
||||
* @param data 数据
|
||||
* @return 是否为二进制格式
|
||||
*/
|
||||
public static boolean isBinaryFormatQuick(byte[] data) {
|
||||
return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
|
||||
*
|
||||
* 采用纯 JSON 格式传输,格式如下:
|
||||
* {
|
||||
* "id": "消息 ID",
|
||||
* "method": "消息方法",
|
||||
* "params": {...}, // 请求参数
|
||||
* "data": {...}, // 响应结果
|
||||
* "code": 200, // 响应错误码
|
||||
* "msg": "success", // 响应提示
|
||||
* "timestamp": 时间戳
|
||||
* }
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
public static final String TYPE = "TcpJson";
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class TcpJsonMessage {
|
||||
|
||||
/**
|
||||
* 消息 ID,且每个消息 ID 在当前设备具有唯一性
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
private String method;
|
||||
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
private Object params;
|
||||
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 响应错误码
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应提示
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode(IotDeviceMessage message) {
|
||||
TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
|
||||
message.getRequestId(),
|
||||
message.getMethod(),
|
||||
message.getParams(),
|
||||
message.getData(),
|
||||
message.getCode(),
|
||||
message.getMsg(),
|
||||
System.currentTimeMillis());
|
||||
return JsonUtils.toJsonByte(tcpJsonMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public IotDeviceMessage decode(byte[] bytes) {
|
||||
String jsonStr = StrUtil.utf8Str(bytes).trim();
|
||||
TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
|
||||
Assert.notNull(tcpJsonMessage, "消息不能为空");
|
||||
Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
|
||||
return IotDeviceMessage.of(
|
||||
tcpJsonMessage.getId(),
|
||||
tcpJsonMessage.getMethod(),
|
||||
tcpJsonMessage.getParams(),
|
||||
tcpJsonMessage.getData(),
|
||||
tcpJsonMessage.getCode(),
|
||||
tcpJsonMessage.getMsg());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
@@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(IotGatewayProperties.class)
|
||||
@Slf4j
|
||||
public class IotGatewayConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -44,75 +57,6 @@ public class IotGatewayConfiguration {
|
||||
return new IotProtocolManager(gatewayProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class EmqxProtocolConfiguration {
|
||||
|
||||
@Bean(name = "emqxVertx", destroyMethod = "close")
|
||||
public Vertx emqxVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
|
||||
@Qualifier("emqxVertx") Vertx emqxVertx) {
|
||||
return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
@Qualifier("emqxVertx") Vertx emqxVertx) {
|
||||
return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class MqttProtocolConfiguration {
|
||||
|
||||
@Bean(name = "mqttVertx", destroyMethod = "close")
|
||||
public Vertx mqttVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttConnectionManager connectionManager,
|
||||
@Qualifier("mqttVertx") Vertx mqttVertx) {
|
||||
return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService,
|
||||
connectionManager, mqttVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService,
|
||||
IotMqttConnectionManager connectionManager) {
|
||||
return new IotMqttDownstreamHandler(messageService, connectionManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol,
|
||||
IotMqttDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 Modbus TCP 协议配置类
|
||||
*/
|
||||
@@ -188,8 +132,8 @@ public class IotGatewayConfiguration {
|
||||
|
||||
@Bean
|
||||
public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol,
|
||||
IotModbusTcpDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
IotModbusTcpDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.iot.gateway.config;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
|
||||
import io.vertx.core.net.KeyCertOptions;
|
||||
import io.vertx.core.net.TrustOptions;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@@ -32,15 +32,10 @@ public class IotGatewayProperties {
|
||||
*/
|
||||
private TokenProperties token;
|
||||
|
||||
/**
|
||||
* 协议配置(旧版,保持兼容)
|
||||
*/
|
||||
private ProtocolProperties protocol;
|
||||
|
||||
/**
|
||||
* 协议实例列表
|
||||
*/
|
||||
private List<ProtocolInstanceProperties> protocols;
|
||||
private List<ProtocolProperties> protocols;
|
||||
|
||||
@Data
|
||||
public static class RpcProperties {
|
||||
@@ -79,304 +74,11 @@ public class IotGatewayProperties {
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProtocolProperties {
|
||||
|
||||
/**
|
||||
* EMQX 组件配置
|
||||
*/
|
||||
private EmqxProperties emqx;
|
||||
|
||||
/**
|
||||
* MQTT 组件配置
|
||||
*/
|
||||
private MqttProperties mqtt;
|
||||
|
||||
/**
|
||||
* Modbus TCP 组件配置
|
||||
*/
|
||||
private ModbusTcpProperties modbusTcp;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class HttpProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 服务端口
|
||||
*/
|
||||
private Integer serverPort;
|
||||
|
||||
/**
|
||||
* 是否开启 SSL
|
||||
*/
|
||||
@NotNull(message = "是否开启 SSL 不能为空")
|
||||
private Boolean sslEnabled = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class EmqxProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* HTTP 服务端口(默认:8090)
|
||||
*/
|
||||
private Integer httpPort = 8090;
|
||||
|
||||
/**
|
||||
* MQTT 服务器地址
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 服务器地址不能为空")
|
||||
private String mqttHost;
|
||||
|
||||
/**
|
||||
* MQTT 服务器端口(默认:1883)
|
||||
*/
|
||||
@NotNull(message = "MQTT 服务器端口不能为空")
|
||||
private Integer mqttPort = 1883;
|
||||
|
||||
/**
|
||||
* MQTT 用户名
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 用户名不能为空")
|
||||
private String mqttUsername;
|
||||
|
||||
/**
|
||||
* MQTT 密码
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 密码不能为空")
|
||||
private String mqttPassword;
|
||||
|
||||
/**
|
||||
* MQTT 客户端的 SSL 开关
|
||||
*/
|
||||
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
|
||||
private Boolean mqttSsl = false;
|
||||
|
||||
/**
|
||||
* MQTT 客户端 ID(如果为空,系统将自动生成)
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
|
||||
private String mqttClientId;
|
||||
|
||||
/**
|
||||
* MQTT 订阅的主题
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 主题不能为空")
|
||||
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
|
||||
|
||||
/**
|
||||
* 默认 QoS 级别
|
||||
* <p>
|
||||
* 0 - 最多一次
|
||||
* 1 - 至少一次
|
||||
* 2 - 刚好一次
|
||||
*/
|
||||
private Integer mqttQos = 1;
|
||||
|
||||
/**
|
||||
* 连接超时时间(秒)
|
||||
*/
|
||||
private Integer connectTimeoutSeconds = 10;
|
||||
|
||||
/**
|
||||
* 重连延迟时间(毫秒)
|
||||
*/
|
||||
private Long reconnectDelayMs = 5000L;
|
||||
|
||||
/**
|
||||
* 是否启用 Clean Session (清理会话)
|
||||
* true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。
|
||||
* 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
|
||||
*/
|
||||
private Boolean cleanSession = true;
|
||||
|
||||
/**
|
||||
* 心跳间隔(秒)
|
||||
* 用于保持连接活性,及时发现网络中断。
|
||||
*/
|
||||
private Integer keepAliveIntervalSeconds = 60;
|
||||
|
||||
/**
|
||||
* 最大未确认消息队列大小
|
||||
* 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
|
||||
*/
|
||||
private Integer maxInflightQueue = 10000;
|
||||
|
||||
/**
|
||||
* 是否信任所有 SSL 证书
|
||||
* 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
|
||||
* 在生产环境中,应设置为 false,并配置正确的信任库。
|
||||
*/
|
||||
private Boolean trustAll = false;
|
||||
|
||||
/**
|
||||
* 遗嘱消息配置 (用于网关异常下线时通知其他系统)
|
||||
*/
|
||||
private final Will will = new Will();
|
||||
|
||||
/**
|
||||
* 高级 SSL/TLS 配置 (用于生产环境)
|
||||
*/
|
||||
private final Ssl sslOptions = new Ssl();
|
||||
|
||||
/**
|
||||
* 遗嘱消息 (Last Will and Testament)
|
||||
*/
|
||||
@Data
|
||||
public static class Will {
|
||||
|
||||
/**
|
||||
* 是否启用遗嘱消息
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
/**
|
||||
* 遗嘱消息主题
|
||||
*/
|
||||
private String topic;
|
||||
/**
|
||||
* 遗嘱消息内容
|
||||
*/
|
||||
private String payload;
|
||||
/**
|
||||
* 遗嘱消息 QoS 等级
|
||||
*/
|
||||
private Integer qos = 1;
|
||||
/**
|
||||
* 遗嘱消息是否作为保留消息发布
|
||||
*/
|
||||
private boolean retain = true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级 SSL/TLS 配置
|
||||
*/
|
||||
@Data
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* 密钥库(KeyStore)路径,例如:classpath:certs/client.jks
|
||||
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
|
||||
*/
|
||||
private String keyStorePath;
|
||||
/**
|
||||
* 密钥库密码
|
||||
*/
|
||||
private String keyStorePassword;
|
||||
/**
|
||||
* 信任库(TrustStore)路径,例如:classpath:certs/trust.jks
|
||||
* 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
|
||||
*/
|
||||
private String trustStorePath;
|
||||
/**
|
||||
* 信任库密码
|
||||
*/
|
||||
private String trustStorePassword;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MqttProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 服务器端口
|
||||
*/
|
||||
private Integer port = 1883;
|
||||
|
||||
/**
|
||||
* 最大消息大小(字节)
|
||||
*/
|
||||
private Integer maxMessageSize = 8192;
|
||||
|
||||
/**
|
||||
* 连接超时时间(秒)
|
||||
*/
|
||||
private Integer connectTimeoutSeconds = 60;
|
||||
/**
|
||||
* 保持连接超时时间(秒)
|
||||
*/
|
||||
private Integer keepAliveTimeoutSeconds = 300;
|
||||
|
||||
// NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低)
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
private Boolean sslEnabled = false;
|
||||
/**
|
||||
* SSL 配置
|
||||
*/
|
||||
private SslOptions sslOptions = new SslOptions();
|
||||
|
||||
/**
|
||||
* SSL 配置选项
|
||||
*/
|
||||
@Data
|
||||
public static class SslOptions {
|
||||
|
||||
/**
|
||||
* 密钥证书选项
|
||||
*/
|
||||
private KeyCertOptions keyCertOptions;
|
||||
/**
|
||||
* 信任选项
|
||||
*/
|
||||
private TrustOptions trustOptions;
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String certPath;
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String keyPath;
|
||||
/**
|
||||
* 信任存储路径
|
||||
*/
|
||||
private String trustStorePath;
|
||||
/**
|
||||
* 信任存储密码
|
||||
*/
|
||||
private String trustStorePassword;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整
|
||||
/**
|
||||
* 协议实例配置
|
||||
*/
|
||||
@Data
|
||||
public static class ProtocolInstanceProperties {
|
||||
public static class ProtocolProperties {
|
||||
|
||||
/**
|
||||
* 协议实例 ID,如 "http-alink"、"tcp-binary"
|
||||
@@ -394,9 +96,13 @@ public class IotGatewayProperties {
|
||||
* @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
|
||||
*/
|
||||
@NotEmpty(message = "协议类型不能为空")
|
||||
private String type;
|
||||
private String protocol;
|
||||
/**
|
||||
* 服务端口
|
||||
* <p>
|
||||
* 不同协议含义不同:
|
||||
* 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口
|
||||
* 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event)
|
||||
*/
|
||||
@NotNull(message = "服务端口不能为空")
|
||||
private Integer port;
|
||||
@@ -406,23 +112,37 @@ public class IotGatewayProperties {
|
||||
* @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
|
||||
*
|
||||
* 为什么是可选的呢?
|
||||
* 1. {@link IotProtocolTypeEnum#HTTP}、${@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
|
||||
* 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
|
||||
* 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析
|
||||
*/
|
||||
private String serialize;
|
||||
|
||||
// ========== SSL 配置 ==========
|
||||
|
||||
/**
|
||||
* SSL 配置(可选,配置文件中不配置则为 null)
|
||||
*/
|
||||
@Valid
|
||||
private SslConfig ssl;
|
||||
|
||||
// ========== 各协议配置 ==========
|
||||
|
||||
/**
|
||||
* HTTP 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotHttpConfig http;
|
||||
/**
|
||||
* WebSocket 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotWebSocketConfig websocket;
|
||||
|
||||
/**
|
||||
* TCP 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotTcpConfig tcp;
|
||||
|
||||
/**
|
||||
* UDP 协议配置
|
||||
*/
|
||||
@@ -436,10 +156,73 @@ public class IotGatewayProperties {
|
||||
private IotCoapConfig coap;
|
||||
|
||||
/**
|
||||
* WebSocket 协议配置
|
||||
* MQTT 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotWebSocketConfig websocket;
|
||||
private IotMqttConfig mqtt;
|
||||
/**
|
||||
* EMQX 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotEmqxConfig emqx;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL 配置
|
||||
*/
|
||||
@Data
|
||||
public static class SslConfig {
|
||||
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
@NotNull(message = "是否启用 SSL 不能为空")
|
||||
private Boolean ssl = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
@NotEmpty(message = "SSL 证书路径不能为空")
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
@NotEmpty(message = "SSL 私钥路径不能为空")
|
||||
private String sslKeyPath;
|
||||
|
||||
/**
|
||||
* 密钥库(KeyStore)路径
|
||||
* <p>
|
||||
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)
|
||||
*/
|
||||
private String keyStorePath;
|
||||
/**
|
||||
* 密钥库密码
|
||||
*/
|
||||
private String keyStorePassword;
|
||||
|
||||
/**
|
||||
* 信任库(TrustStore)路径
|
||||
* <p>
|
||||
* 包含服务端信任的 CA 证书,用于验证服务端的身份
|
||||
*/
|
||||
private String trustStorePath;
|
||||
/**
|
||||
* 信任库密码
|
||||
*/
|
||||
private String trustStorePassword;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProtocolProperties {
|
||||
|
||||
/**
|
||||
* Modbus TCP 组件配置
|
||||
*/
|
||||
private ModbusTcpProperties modbusTcp;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
|
||||
@@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
List<IotGatewayProperties.ProtocolInstanceProperties> protocolConfigs = gatewayProperties.getProtocols();
|
||||
List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
|
||||
if (CollUtil.isEmpty(protocolConfigs)) {
|
||||
log.info("[start][没有配置协议实例,跳过启动]");
|
||||
return;
|
||||
}
|
||||
|
||||
for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) {
|
||||
for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
|
||||
if (BooleanUtil.isFalse(config.getEnabled())) {
|
||||
log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
|
||||
continue;
|
||||
@@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
* @return 协议实例
|
||||
*/
|
||||
@SuppressWarnings({"EnhancedSwitchMigration"})
|
||||
private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
|
||||
IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType());
|
||||
private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol());
|
||||
if (protocolType == null) {
|
||||
log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType());
|
||||
log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
|
||||
return null;
|
||||
}
|
||||
switch (protocolType) {
|
||||
@@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
return createCoapProtocol(config);
|
||||
case WEBSOCKET:
|
||||
return createWebSocketProtocol(config);
|
||||
case MQTT:
|
||||
return createMqttProtocol(config);
|
||||
case EMQX:
|
||||
return createEmqxProtocol(config);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
|
||||
@@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
* @param config 协议实例配置
|
||||
* @return HTTP 协议实例
|
||||
*/
|
||||
private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
|
||||
private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotHttpProtocol(config);
|
||||
}
|
||||
|
||||
@@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
* @param config 协议实例配置
|
||||
* @return TCP 协议实例
|
||||
*/
|
||||
private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
|
||||
private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotTcpProtocol(config);
|
||||
}
|
||||
|
||||
@@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
* @param config 协议实例配置
|
||||
* @return UDP 协议实例
|
||||
*/
|
||||
private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
|
||||
private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotUdpProtocol(config);
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
* @param config 协议实例配置
|
||||
* @return CoAP 协议实例
|
||||
*/
|
||||
private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
|
||||
private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotCoapProtocol(config);
|
||||
}
|
||||
|
||||
@@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
* @param config 协议实例配置
|
||||
* @return WebSocket 协议实例
|
||||
*/
|
||||
private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
|
||||
private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotWebSocketProtocol(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MQTT 协议实例
|
||||
*
|
||||
* @param config 协议实例配置
|
||||
* @return MQTT 协议实例
|
||||
*/
|
||||
private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotMqttProtocol(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 EMQX 协议实例
|
||||
*
|
||||
* @param config 协议实例配置
|
||||
* @return EMQX 协议实例
|
||||
*/
|
||||
private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotEmqxProtocol(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler;
|
||||
@@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.CoapServer;
|
||||
import org.eclipse.californium.core.config.CoapConfig;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol {
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolInstanceProperties properties;
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol {
|
||||
*/
|
||||
private final IotCoapDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
public IotCoapProtocol(ProtocolInstanceProperties properties) {
|
||||
public IotCoapProtocol(ProtocolProperties properties) {
|
||||
IotCoapConfig coapConfig = properties.getCoap();
|
||||
Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
|
||||
this.properties = properties;
|
||||
|
||||
@@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
|
||||
Assert.notNull(request, "请求体不能为空");
|
||||
Assert.notBlank(request.getProductKey(), "productKey 不能为空");
|
||||
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
|
||||
Assert.notBlank(request.getSign(), "sign 不能为空");
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 认证事件协议服务
|
||||
* <p>
|
||||
* 为 EMQX 提供 HTTP 接口服务,包括:
|
||||
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
|
||||
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxAuthEventProtocol {
|
||||
|
||||
private final IotGatewayProperties.EmqxProperties emqxProperties;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
private HttpServer httpServer;
|
||||
|
||||
public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
|
||||
Vertx vertx) {
|
||||
this.emqxProperties = emqxProperties;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
try {
|
||||
startHttpServer();
|
||||
log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
stopHttpServer();
|
||||
log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 HTTP 服务器
|
||||
*/
|
||||
private void startHttpServer() {
|
||||
int port = emqxProperties.getHttpPort();
|
||||
|
||||
// 1. 创建路由
|
||||
Router router = Router.router(vertx);
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
// 2. 创建处理器,传入 serverId
|
||||
IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
|
||||
router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
|
||||
router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
|
||||
// TODO @haohao:/mqtt/acl 需要处理么?
|
||||
// TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理
|
||||
|
||||
// 3. 启动 HTTP 服务器
|
||||
try {
|
||||
httpServer = vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(port)
|
||||
.result();
|
||||
} catch (Exception e) {
|
||||
log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 HTTP 服务器
|
||||
*/
|
||||
private void stopHttpServer() {
|
||||
if (httpServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
httpServer.close().result();
|
||||
log.info("[stopHttpServer][HTTP 服务器已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stopHttpServer][HTTP 服务器停止失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT EMQX 协议配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotEmqxConfig {
|
||||
|
||||
// ========== MQTT Client 配置(连接 EMQX Broker) ==========
|
||||
|
||||
/**
|
||||
* MQTT 服务器地址
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 服务器地址不能为空")
|
||||
private String mqttHost;
|
||||
|
||||
/**
|
||||
* MQTT 服务器端口(默认:1883)
|
||||
*/
|
||||
@NotNull(message = "MQTT 服务器端口不能为空")
|
||||
private Integer mqttPort = 1883;
|
||||
|
||||
/**
|
||||
* MQTT 用户名
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 用户名不能为空")
|
||||
private String mqttUsername;
|
||||
|
||||
/**
|
||||
* MQTT 密码
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 密码不能为空")
|
||||
private String mqttPassword;
|
||||
|
||||
/**
|
||||
* MQTT 客户端的 SSL 开关
|
||||
*/
|
||||
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
|
||||
private Boolean mqttSsl = false;
|
||||
|
||||
/**
|
||||
* MQTT 客户端 ID
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
|
||||
private String mqttClientId;
|
||||
|
||||
/**
|
||||
* MQTT 订阅的主题
|
||||
*/
|
||||
@NotEmpty(message = "MQTT 主题不能为空")
|
||||
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
|
||||
|
||||
/**
|
||||
* 默认 QoS 级别
|
||||
* <p>
|
||||
* 0 - 最多一次
|
||||
* 1 - 至少一次
|
||||
* 2 - 刚好一次
|
||||
*/
|
||||
@NotNull(message = "MQTT QoS 不能为空")
|
||||
@Min(value = 0, message = "MQTT QoS 不能小于 0")
|
||||
@Max(value = 2, message = "MQTT QoS 不能大于 2")
|
||||
private Integer mqttQos = 1;
|
||||
|
||||
/**
|
||||
* 连接超时时间(秒)
|
||||
*/
|
||||
@NotNull(message = "连接超时时间不能为空")
|
||||
@Min(value = 1, message = "连接超时时间不能小于 1 秒")
|
||||
private Integer connectTimeoutSeconds = 10;
|
||||
|
||||
/**
|
||||
* 重连延迟时间(毫秒)
|
||||
*/
|
||||
@NotNull(message = "重连延迟时间不能为空")
|
||||
@Min(value = 0, message = "重连延迟时间不能小于 0 毫秒")
|
||||
private Long reconnectDelayMs = 5000L;
|
||||
|
||||
/**
|
||||
* 是否启用 Clean Session (清理会话)
|
||||
* true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。
|
||||
* 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
|
||||
*/
|
||||
@NotNull(message = "是否启用 Clean Session 不能为空")
|
||||
private Boolean cleanSession = true;
|
||||
|
||||
/**
|
||||
* 心跳间隔(秒)
|
||||
* 用于保持连接活性,及时发现网络中断。
|
||||
*/
|
||||
@NotNull(message = "心跳间隔不能为空")
|
||||
@Min(value = 1, message = "心跳间隔不能小于 1 秒")
|
||||
private Integer keepAliveIntervalSeconds = 60;
|
||||
|
||||
/**
|
||||
* 最大未确认消息队列大小
|
||||
* 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
|
||||
*/
|
||||
@NotNull(message = "最大未确认消息队列大小不能为空")
|
||||
@Min(value = 1, message = "最大未确认消息队列大小不能小于 1")
|
||||
private Integer maxInflightQueue = 10000;
|
||||
|
||||
/**
|
||||
* 是否信任所有 SSL 证书
|
||||
* 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
|
||||
* 在生产环境中,应设置为 false,并配置正确的信任库。
|
||||
*/
|
||||
@NotNull(message = "是否信任所有 SSL 证书不能为空")
|
||||
private Boolean trustAll = false;
|
||||
|
||||
// ========== MQTT Will / SSL 高级配置 ==========
|
||||
|
||||
/**
|
||||
* 遗嘱消息配置 (用于网关异常下线时通知其他系统)
|
||||
*/
|
||||
@Valid
|
||||
private Will will = new Will();
|
||||
|
||||
/**
|
||||
* 高级 SSL/TLS 配置 (用于生产环境)
|
||||
*/
|
||||
@Valid
|
||||
private Ssl sslOptions = new Ssl();
|
||||
|
||||
// ========== HTTP Hook 配置(网关提供给 EMQX 调用) ==========
|
||||
|
||||
/**
|
||||
* HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event)
|
||||
*/
|
||||
@Valid
|
||||
private Http http = new Http();
|
||||
|
||||
/**
|
||||
* 遗嘱消息 (Last Will and Testament)
|
||||
*/
|
||||
@Data
|
||||
public static class Will {
|
||||
|
||||
/**
|
||||
* 是否启用遗嘱消息
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
/**
|
||||
* 遗嘱消息主题
|
||||
*/
|
||||
private String topic;
|
||||
/**
|
||||
* 遗嘱消息内容
|
||||
*/
|
||||
private String payload;
|
||||
/**
|
||||
* 遗嘱消息 QoS 等级
|
||||
*/
|
||||
@Min(value = 0, message = "遗嘱消息 QoS 不能小于 0")
|
||||
@Max(value = 2, message = "遗嘱消息 QoS 不能大于 2")
|
||||
private Integer qos = 1;
|
||||
/**
|
||||
* 遗嘱消息是否作为保留消息发布
|
||||
*/
|
||||
private boolean retain = true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级 SSL/TLS 配置
|
||||
*/
|
||||
@Data
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* 密钥库(KeyStore)路径,例如:classpath:certs/client.jks
|
||||
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
|
||||
*/
|
||||
private String keyStorePath;
|
||||
/**
|
||||
* 密钥库密码
|
||||
*/
|
||||
private String keyStorePassword;
|
||||
/**
|
||||
* 信任库(TrustStore)路径,例如:classpath:certs/trust.jks
|
||||
* 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
|
||||
*/
|
||||
private String trustStorePath;
|
||||
/**
|
||||
* 信任库密码
|
||||
*/
|
||||
private String trustStorePassword;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Hook 服务 SSL 配置
|
||||
*/
|
||||
@Data
|
||||
public static class Http {
|
||||
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
private Boolean sslEnabled = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 协议实现:
|
||||
* <p>
|
||||
* 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用
|
||||
* 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxProtocol implements IotProtocol {
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* EMQX 配置
|
||||
*/
|
||||
private final IotEmqxConfig emqxConfig;
|
||||
/**
|
||||
* 服务器 ID
|
||||
*/
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
/**
|
||||
* 运行状态
|
||||
*/
|
||||
@Getter
|
||||
private volatile boolean running = false;
|
||||
|
||||
/**
|
||||
* Vert.x 实例
|
||||
*/
|
||||
private Vertx vertx;
|
||||
/**
|
||||
* HTTP Hook 服务器
|
||||
*/
|
||||
private HttpServer httpServer;
|
||||
|
||||
/**
|
||||
* MQTT Client
|
||||
*/
|
||||
private volatile MqttClient mqttClient;
|
||||
/**
|
||||
* MQTT 重连定时器 ID
|
||||
*/
|
||||
private volatile Long reconnectTimerId;
|
||||
|
||||
/**
|
||||
* 上行消息处理器
|
||||
*/
|
||||
private final IotEmqxUpstreamHandler upstreamHandler;
|
||||
|
||||
/**
|
||||
* 下行消息订阅者
|
||||
*/
|
||||
private final IotEmqxDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
public IotEmqxProtocol(ProtocolProperties properties) {
|
||||
Assert.notNull(properties, "协议实例配置不能为空");
|
||||
Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空");
|
||||
this.properties = properties;
|
||||
this.emqxConfig = properties.getEmqx();
|
||||
Assert.notNull(emqxConfig.getConnectTimeoutSeconds(),
|
||||
"MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
|
||||
|
||||
// 初始化下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return properties.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotProtocolTypeEnum getType() {
|
||||
return IotProtocolTypeEnum.EMQX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (running) {
|
||||
log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 创建 Vertx 实例
|
||||
this.vertx = Vertx.vertx();
|
||||
|
||||
try {
|
||||
// 1.2 启动 HTTP Hook 服务
|
||||
startHttpServer();
|
||||
|
||||
// 1.3 启动 MQTT Client
|
||||
startMqttClient();
|
||||
running = true;
|
||||
log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]",
|
||||
getId(), properties.getPort(), serverId);
|
||||
|
||||
// 2. 启动下行消息订阅者
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
|
||||
// 启动失败时,关闭资源
|
||||
stop0();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
stop0();
|
||||
}
|
||||
|
||||
private void stop0() {
|
||||
// 1. 停止下行消息订阅者
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
}
|
||||
|
||||
// 2.1 先置为 false:避免 closeHandler 触发重连
|
||||
running = false;
|
||||
stopMqttClientReconnectChecker();
|
||||
// 2.2 停止 MQTT Client
|
||||
stopMqttClient();
|
||||
|
||||
// 2.3 停止 HTTP Hook 服务
|
||||
stopHttpServer();
|
||||
|
||||
// 2.4 关闭 Vertx
|
||||
if (vertx != null) {
|
||||
try {
|
||||
vertx.close().toCompletionStage().toCompletableFuture()
|
||||
.get(10, TimeUnit.SECONDS);
|
||||
log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e);
|
||||
}
|
||||
vertx = null;
|
||||
}
|
||||
|
||||
log.info("[stop][IoT EMQX 协议 {} 已停止]", getId());
|
||||
}
|
||||
|
||||
// ======================================= HTTP Hook Server =======================================
|
||||
|
||||
/**
|
||||
* 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)
|
||||
*/
|
||||
private void startHttpServer() {
|
||||
// 1. 创建路由
|
||||
Router router = Router.router(vertx);
|
||||
router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击
|
||||
|
||||
// 2. 创建处理器
|
||||
IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this);
|
||||
router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
|
||||
router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl);
|
||||
router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
|
||||
|
||||
// 3. 启动 HTTP Server(支持 HTTPS)
|
||||
IotEmqxConfig.Http httpConfig = emqxConfig.getHttp();
|
||||
HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
|
||||
if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
|
||||
Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空");
|
||||
Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空");
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(httpConfig.getSslKeyPath())
|
||||
.setCertPath(httpConfig.getSslCertPath());
|
||||
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
try {
|
||||
httpServer = vertx.createHttpServer(options)
|
||||
.requestHandler(router)
|
||||
.listen()
|
||||
.toCompletionStage().toCompletableFuture()
|
||||
.get(10, TimeUnit.SECONDS);
|
||||
log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]",
|
||||
getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled()));
|
||||
} catch (Exception e) {
|
||||
log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e);
|
||||
throw new RuntimeException("HTTP Hook 服务启动失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopHttpServer() {
|
||||
if (httpServer == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
httpServer.close().toCompletionStage().toCompletableFuture()
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e);
|
||||
} finally {
|
||||
httpServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================= MQTT Client ======================================
|
||||
|
||||
private void startMqttClient() {
|
||||
// 1.1 创建 MQTT Client
|
||||
MqttClient client = createMqttClient();
|
||||
this.mqttClient = client;
|
||||
// 1.2 连接 MQTT Broker
|
||||
if (!connectMqttClient(client)) {
|
||||
throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败");
|
||||
}
|
||||
|
||||
// 2. 启动定时重连检查
|
||||
startMqttClientReconnectChecker();
|
||||
}
|
||||
|
||||
private void stopMqttClient() {
|
||||
MqttClient client = this.mqttClient;
|
||||
this.mqttClient = null; // 先清理引用
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 批量取消订阅(仅在连接时)
|
||||
if (client.isConnected()) {
|
||||
List<String> topicList = emqxConfig.getMqttTopics();
|
||||
if (CollUtil.isNotEmpty(topicList)) {
|
||||
try {
|
||||
client.unsubscribe(topicList).toCompletionStage().toCompletableFuture()
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 断开 MQTT 连接
|
||||
try {
|
||||
client.disconnect().toCompletionStage().toCompletableFuture()
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================= MQTT 基础方法 ======================================
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端
|
||||
*
|
||||
* @return 新创建的 MqttClient
|
||||
*/
|
||||
private MqttClient createMqttClient() {
|
||||
// 1.1 基础配置
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(emqxConfig.getMqttClientId())
|
||||
.setUsername(emqxConfig.getMqttUsername())
|
||||
.setPassword(emqxConfig.getMqttPassword())
|
||||
.setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl()))
|
||||
.setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession()))
|
||||
.setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds())
|
||||
.setMaxInflightQueue(emqxConfig.getMaxInflightQueue());
|
||||
options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒
|
||||
options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll()));
|
||||
// 1.2 配置遗嘱消息
|
||||
IotEmqxConfig.Will will = emqxConfig.getWill();
|
||||
if (will != null && will.isEnabled()) {
|
||||
Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空");
|
||||
Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空");
|
||||
options.setWillFlag(true)
|
||||
.setWillTopic(will.getTopic())
|
||||
.setWillMessageBytes(Buffer.buffer(will.getPayload()))
|
||||
.setWillQoS(will.getQos())
|
||||
.setWillRetain(will.isRetain());
|
||||
}
|
||||
// 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空)
|
||||
IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions();
|
||||
if (Boolean.TRUE.equals(emqxConfig.getMqttSsl())
|
||||
&& Boolean.FALSE.equals(emqxConfig.getTrustAll())
|
||||
&& sslOptions != null) {
|
||||
if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
|
||||
options.setTrustStoreOptions(new JksOptions()
|
||||
.setPath(sslOptions.getTrustStorePath())
|
||||
.setPassword(sslOptions.getTrustStorePassword()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
|
||||
options.setKeyStoreOptions(new JksOptions()
|
||||
.setPath(sslOptions.getKeyStorePath())
|
||||
.setPassword(sslOptions.getKeyStorePassword()));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建客户端
|
||||
return MqttClient.create(vertx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 MQTT Broker(同步等待)
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @return 连接成功返回 true,失败返回 false
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private synchronized boolean connectMqttClient(MqttClient client) {
|
||||
String host = emqxConfig.getMqttHost();
|
||||
int port = emqxConfig.getMqttPort();
|
||||
int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds();
|
||||
try {
|
||||
// 1. 连接 Broker
|
||||
client.connect(port, host).toCompletionStage().toCompletableFuture()
|
||||
.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]",
|
||||
getId(), host, port);
|
||||
|
||||
// 2. 设置处理器
|
||||
setupMqttClientHandlers(client);
|
||||
subscribeMqttClientTopics(client);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 MQTT 客户端
|
||||
*/
|
||||
private void closeMqttClient() {
|
||||
MqttClient oldClient = this.mqttClient;
|
||||
this.mqttClient = null; // 先清理引用
|
||||
if (oldClient == null) {
|
||||
return;
|
||||
}
|
||||
// 尽力释放(无论是否连接都尝试 disconnect)
|
||||
try {
|
||||
oldClient.disconnect().toCompletionStage().toCompletableFuture()
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================= MQTT 重连机制 ======================================
|
||||
|
||||
/**
|
||||
* 启动 MQTT Client 周期性重连检查器
|
||||
*/
|
||||
private void startMqttClientReconnectChecker() {
|
||||
long interval = emqxConfig.getReconnectDelayMs();
|
||||
this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId());
|
||||
// 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待)
|
||||
vertx.executeBlocking(() -> {
|
||||
tryReconnectMqttClient();
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 MQTT Client 重连检查器
|
||||
*/
|
||||
private void stopMqttClientReconnectChecker() {
|
||||
if (reconnectTimerId != null && vertx != null) {
|
||||
try {
|
||||
vertx.cancelTimer(reconnectTimerId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
reconnectTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试重连 MQTT Client
|
||||
*/
|
||||
private synchronized void tryReconnectMqttClient() {
|
||||
// 1. 前置检查
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId());
|
||||
try {
|
||||
// 2. 关闭旧客户端
|
||||
closeMqttClient();
|
||||
|
||||
// 3.1 创建新客户端
|
||||
MqttClient client = createMqttClient();
|
||||
this.mqttClient = client;
|
||||
// 3.2 连接(失败只打印日志,等下次定时)
|
||||
if (!connectMqttClient(client)) {
|
||||
log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================= MQTT Handler ======================================
|
||||
|
||||
/**
|
||||
* 设置 MQTT Client 事件处理器
|
||||
*/
|
||||
private void setupMqttClientHandlers(MqttClient client) {
|
||||
// 1. 断开重连监听
|
||||
client.closeHandler(closeEvent -> {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId());
|
||||
// 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待)
|
||||
vertx.executeBlocking(() -> {
|
||||
tryReconnectMqttClient();
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 异常处理
|
||||
client.exceptionHandler(exception ->
|
||||
log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception));
|
||||
|
||||
// 3. 上行消息处理
|
||||
client.publishHandler(upstreamHandler::handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅 MQTT Client 主题(同步等待)
|
||||
*/
|
||||
private void subscribeMqttClientTopics(MqttClient client) {
|
||||
List<String> topicList = emqxConfig.getMqttTopics();
|
||||
if (!client.isConnected()) {
|
||||
log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId());
|
||||
return;
|
||||
}
|
||||
if (CollUtil.isEmpty(topicList)) {
|
||||
log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId());
|
||||
return;
|
||||
}
|
||||
// 执行订阅
|
||||
Map<String, Integer> topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic,
|
||||
topic -> emqxConfig.getMqttQos());
|
||||
try {
|
||||
client.subscribe(topics).toCompletionStage().toCompletableFuture()
|
||||
.get(10, TimeUnit.SECONDS);
|
||||
log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size());
|
||||
} catch (Exception e) {
|
||||
log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息到 MQTT Broker
|
||||
*
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void publishMessage(String topic, byte[] payload) {
|
||||
if (mqttClient == null || !mqttClient.isConnected()) {
|
||||
log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId());
|
||||
return;
|
||||
}
|
||||
MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos());
|
||||
mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false)
|
||||
.onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e));
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟发布消息到 MQTT Broker
|
||||
*
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
* @param delayMs 延迟时间(毫秒)
|
||||
*/
|
||||
public void publishDelayMessage(String topic, byte[] payload, long delayMs) {
|
||||
vertx.setTimer(delayMs, id -> publishMessage(topic, payload));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxUpstreamProtocol implements IotProtocol {
|
||||
|
||||
private static final String ID = "emqx";
|
||||
|
||||
private final IotGatewayProperties.EmqxProperties emqxProperties;
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private MqttClient mqttClient;
|
||||
|
||||
private IotEmqxUpstreamHandler upstreamHandler;
|
||||
|
||||
public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
|
||||
Vertx vertx) {
|
||||
this.emqxProperties = emqxProperties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
|
||||
this.vertx = vertx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotProtocolTypeEnum getType() {
|
||||
return IotProtocolTypeEnum.EMQX;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 启动 MQTT 客户端
|
||||
startMqttClient();
|
||||
|
||||
// 2. 标记服务为运行状态
|
||||
running = true;
|
||||
log.info("[start][IoT 网关 EMQX 协议启动成功]");
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
|
||||
stop();
|
||||
|
||||
// 异步关闭应用
|
||||
Thread shutdownThread = new Thread(() -> {
|
||||
try {
|
||||
// 确保日志输出完成,使用更优雅的方式
|
||||
log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
|
||||
// 等待日志输出完成
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("[start][应用关闭被中断]");
|
||||
}
|
||||
System.exit(1);
|
||||
});
|
||||
shutdownThread.setDaemon(true);
|
||||
shutdownThread.setName("emergency-shutdown");
|
||||
shutdownThread.start();
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 停止 MQTT 客户端
|
||||
stopMqttClient();
|
||||
|
||||
// 2. 标记服务为停止状态
|
||||
running = false;
|
||||
log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 MQTT 客户端
|
||||
*/
|
||||
private void startMqttClient() {
|
||||
try {
|
||||
// 1. 初始化消息处理器
|
||||
this.upstreamHandler = new IotEmqxUpstreamHandler(this);
|
||||
|
||||
// 2. 创建 MQTT 客户端
|
||||
createMqttClient();
|
||||
|
||||
// 3. 同步连接 MQTT Broker
|
||||
connectMqttSync();
|
||||
} catch (Exception e) {
|
||||
log.error("[startMqttClient][MQTT 客户端启动失败]", e);
|
||||
throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步连接 MQTT Broker
|
||||
*/
|
||||
private void connectMqttSync() {
|
||||
String host = emqxProperties.getMqttHost();
|
||||
int port = emqxProperties.getMqttPort();
|
||||
// 1. 连接 MQTT Broker
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicBoolean success = new AtomicBoolean(false);
|
||||
mqttClient.connect(port, host, connectResult -> {
|
||||
if (connectResult.succeeded()) {
|
||||
log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
|
||||
setupMqttHandlers();
|
||||
subscribeToTopics();
|
||||
success.set(true);
|
||||
} else {
|
||||
log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
|
||||
host, port, connectResult.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// 2. 等待连接结果
|
||||
try {
|
||||
// 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制
|
||||
boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (!awaitResult) {
|
||||
log.error("[connectMqttSync][等待连接结果超时]");
|
||||
throw new RuntimeException("连接 MQTT Broker 超时");
|
||||
}
|
||||
if (!success.get()) {
|
||||
throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("[connectMqttSync][等待连接结果被中断]", e);
|
||||
throw new RuntimeException("连接 MQTT Broker 被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步连接 MQTT Broker
|
||||
*/
|
||||
private void connectMqttAsync() {
|
||||
String host = emqxProperties.getMqttHost();
|
||||
int port = emqxProperties.getMqttPort();
|
||||
mqttClient.connect(port, host, connectResult -> {
|
||||
if (connectResult.succeeded()) {
|
||||
log.info("[connectMqttAsync][MQTT 客户端重连成功]");
|
||||
setupMqttHandlers();
|
||||
subscribeToTopics();
|
||||
} else {
|
||||
log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]",
|
||||
host, port, connectResult.cause());
|
||||
log.warn("[connectMqttAsync][重连失败,将再次尝试]");
|
||||
reconnectWithDelay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟重连
|
||||
*/
|
||||
private void reconnectWithDelay() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long delay = emqxProperties.getReconnectDelayMs();
|
||||
log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay);
|
||||
vertx.setTimer(delay, timerId -> {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[reconnectWithDelay][开始重连 MQTT Broker]");
|
||||
try {
|
||||
createMqttClient();
|
||||
connectMqttAsync();
|
||||
} catch (Exception e) {
|
||||
log.error("[reconnectWithDelay][重连过程中发生异常]", e);
|
||||
vertx.setTimer(delay, t -> reconnectWithDelay());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 MQTT 客户端
|
||||
*/
|
||||
private void stopMqttClient() {
|
||||
if (mqttClient == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (mqttClient.isConnected()) {
|
||||
// 1. 取消订阅所有主题
|
||||
List<String> topicList = emqxProperties.getMqttTopics();
|
||||
for (String topic : topicList) {
|
||||
try {
|
||||
mqttClient.unsubscribe(topic);
|
||||
} catch (Exception e) {
|
||||
log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 断开 MQTT 客户端连接
|
||||
try {
|
||||
CountDownLatch disconnectLatch = new CountDownLatch(1);
|
||||
mqttClient.disconnect(ar -> disconnectLatch.countDown());
|
||||
if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||
log.warn("[stopMqttClient][断开 MQTT 连接超时]");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e);
|
||||
} finally {
|
||||
mqttClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端
|
||||
*/
|
||||
private void createMqttClient() {
|
||||
// 1.1 创建基础配置
|
||||
MqttClientOptions options = (MqttClientOptions) new MqttClientOptions()
|
||||
.setClientId(emqxProperties.getMqttClientId())
|
||||
.setUsername(emqxProperties.getMqttUsername())
|
||||
.setPassword(emqxProperties.getMqttPassword())
|
||||
.setSsl(emqxProperties.getMqttSsl())
|
||||
.setCleanSession(emqxProperties.getCleanSession())
|
||||
.setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds())
|
||||
.setMaxInflightQueue(emqxProperties.getMaxInflightQueue())
|
||||
.setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒
|
||||
.setTrustAll(emqxProperties.getTrustAll());
|
||||
// 1.2 配置遗嘱消息
|
||||
IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill();
|
||||
if (will.isEnabled()) {
|
||||
Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空");
|
||||
Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空");
|
||||
options.setWillFlag(true)
|
||||
.setWillTopic(will.getTopic())
|
||||
.setWillMessageBytes(Buffer.buffer(will.getPayload()))
|
||||
.setWillQoS(will.getQos())
|
||||
.setWillRetain(will.isRetain());
|
||||
}
|
||||
// 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效)
|
||||
if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
|
||||
IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions();
|
||||
if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
|
||||
options.setTrustStoreOptions(new JksOptions()
|
||||
.setPath(sslOptions.getTrustStorePath())
|
||||
.setPassword(sslOptions.getTrustStorePassword()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
|
||||
options.setKeyStoreOptions(new JksOptions()
|
||||
.setPath(sslOptions.getKeyStorePath())
|
||||
.setPassword(sslOptions.getKeyStorePassword()));
|
||||
}
|
||||
}
|
||||
// 1.4 安全警告日志
|
||||
if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
|
||||
log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]");
|
||||
}
|
||||
|
||||
// 2. 创建客户端实例
|
||||
this.mqttClient = MqttClient.create(vertx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MQTT 处理器
|
||||
*/
|
||||
private void setupMqttHandlers() {
|
||||
// 1. 设置断开重连监听器
|
||||
mqttClient.closeHandler(closeEvent -> {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
|
||||
reconnectWithDelay();
|
||||
});
|
||||
|
||||
// 2. 设置异常处理器
|
||||
mqttClient.exceptionHandler(exception ->
|
||||
log.error("[exceptionHandler][MQTT 客户端异常]", exception));
|
||||
|
||||
// 3. 设置消息处理器
|
||||
mqttClient.publishHandler(upstreamHandler::handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅设备上行消息主题
|
||||
*/
|
||||
private void subscribeToTopics() {
|
||||
// 1. 校验 MQTT 客户端是否连接
|
||||
List<String> topicList = emqxProperties.getMqttTopics();
|
||||
if (mqttClient == null || !mqttClient.isConnected()) {
|
||||
log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 批量订阅所有主题
|
||||
Map<String, Integer> topics = new HashMap<>();
|
||||
int qos = emqxProperties.getMqttQos();
|
||||
for (String topic : topicList) {
|
||||
topics.put(topic, qos);
|
||||
}
|
||||
mqttClient.subscribe(topics, subscribeResult -> {
|
||||
if (subscribeResult.succeeded()) {
|
||||
log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size());
|
||||
} else {
|
||||
log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]",
|
||||
topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息到 MQTT Broker
|
||||
*
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void publishMessage(String topic, byte[] payload) {
|
||||
if (mqttClient == null || !mqttClient.isConnected()) {
|
||||
log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]");
|
||||
return;
|
||||
}
|
||||
MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos());
|
||||
mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
@@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class IotEmqxDownstreamHandler {
|
||||
|
||||
private final IotEmqxUpstreamProtocol protocol;
|
||||
private final IotEmqxProtocol protocol;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
|
||||
public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) {
|
||||
this.protocol = protocol;
|
||||
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
@@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler {
|
||||
return;
|
||||
}
|
||||
// 2.2 构建载荷
|
||||
byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
|
||||
byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
// 2.3 发布消息
|
||||
|
||||
// 3. 发布消息
|
||||
protocol.publishMessage(topic, payload);
|
||||
}
|
||||
|
||||
@@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler {
|
||||
return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber
|
||||
|
||||
private final IotEmqxDownstreamHandler downstreamHandler;
|
||||
|
||||
public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
|
||||
public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) {
|
||||
super(protocol, messageBus);
|
||||
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
|
||||
}
|
||||
@@ -1,25 +1,35 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 认证事件处理器
|
||||
* <p>
|
||||
* 为 EMQX 提供 HTTP 接口服务,包括:
|
||||
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
|
||||
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
|
||||
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)}
|
||||
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)}
|
||||
* 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)}
|
||||
* 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)}
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler {
|
||||
private static final String RESULT_IGNORE = "ignore";
|
||||
|
||||
/**
|
||||
* EMQX 事件类型常量
|
||||
* EMQX 事件类型常量 - 客户端连接
|
||||
*/
|
||||
private static final String EVENT_CLIENT_CONNECTED = "client.connected";
|
||||
/**
|
||||
* EMQX 事件类型常量 - 客户端断开连接
|
||||
*/
|
||||
private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
|
||||
|
||||
/**
|
||||
* 认证类型标识 - 设备注册
|
||||
*/
|
||||
private static final String AUTH_TYPE_REGISTER = "|authType=register|";
|
||||
|
||||
private final String serverId;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
private final IotEmqxProtocol protocol;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
public IotEmqxAuthEventHandler(String serverId) {
|
||||
public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
|
||||
this.serverId = serverId;
|
||||
this.protocol = protocol;
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
}
|
||||
|
||||
// ========== 认证处理 ==========
|
||||
|
||||
/**
|
||||
* EMQX 认证接口
|
||||
*/
|
||||
public void handleAuth(RoutingContext context) {
|
||||
JsonObject body = null;
|
||||
try {
|
||||
// 1. 参数校验
|
||||
JsonObject body = parseRequestBody(context);
|
||||
body = parseRequestBody(context);
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 执行认证
|
||||
// 2.1 情况一:判断是否为注册请求
|
||||
if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
|
||||
handleDeviceRegister(context, username, password);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.2 情况二:执行认证
|
||||
boolean authResult = handleDeviceAuth(clientId, username, password);
|
||||
log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
|
||||
if (authResult) {
|
||||
@@ -91,11 +120,179 @@ public class IotEmqxAuthEventHandler {
|
||||
sendAuthResponse(context, RESULT_DENY);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[handleAuth][设备认证异常]", e);
|
||||
log.error("[handleAuth][设备认证异常][body={}]", body, e);
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析认证接口请求体
|
||||
* <p>
|
||||
* 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
|
||||
*
|
||||
* @param context 路由上下文
|
||||
* @return 请求体JSON对象,解析失败时返回null
|
||||
*/
|
||||
private JsonObject parseRequestBody(RoutingContext context) {
|
||||
try {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
if (body == null) {
|
||||
log.info("[parseRequestBody][请求体为空]");
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return null;
|
||||
}
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行设备认证
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 认证是否成功
|
||||
*/
|
||||
private boolean handleDeviceAuth(String clientId, String username, String password) {
|
||||
try {
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId).setUsername(username).setPassword(password));
|
||||
result.checkError();
|
||||
return BooleanUtil.isTrue(result.getData());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 EMQX 认证响应
|
||||
* 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
|
||||
*
|
||||
* @param context 路由上下文
|
||||
* @param result 认证结果:allow、deny、ignore
|
||||
*/
|
||||
private void sendAuthResponse(RoutingContext context, String result) {
|
||||
// 构建符合 EMQX 官方规范的响应
|
||||
JsonObject response = new JsonObject()
|
||||
.put("result", result)
|
||||
.put("is_superuser", false);
|
||||
// 可以根据业务需求添加客户端属性
|
||||
// response.put("client_attrs", new JsonObject().put("role", "device"));
|
||||
// 可以添加认证过期时间(可选)
|
||||
// response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
|
||||
|
||||
// 回复响应
|
||||
context.response()
|
||||
.setStatusCode(SUCCESS_STATUS_CODE)
|
||||
.putHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.end(response.encode());
|
||||
}
|
||||
|
||||
// ========== ACL 处理 ==========
|
||||
|
||||
/**
|
||||
* EMQX ACL 接口
|
||||
* <p>
|
||||
* 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。
|
||||
* 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。
|
||||
*/
|
||||
public void handleAcl(RoutingContext context) {
|
||||
JsonObject body = null;
|
||||
try {
|
||||
// 1.1 解析请求体
|
||||
body = parseRequestBody(context);
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
String username = body.getString("username");
|
||||
String topic = body.getString("topic");
|
||||
if (StrUtil.hasBlank(username, topic)) {
|
||||
log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic);
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return;
|
||||
}
|
||||
// 1.2 解析设备身份
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return;
|
||||
}
|
||||
// 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段)
|
||||
Boolean subscribe = parseAclSubscribeFlag(body);
|
||||
if (subscribe == null) {
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 执行 ACL 校验
|
||||
boolean allowed = subscribe
|
||||
? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName())
|
||||
: IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleAcl][ACL 处理失败][body={}]", body, e);
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 ACL 动作类型:订阅/发布
|
||||
*
|
||||
* @param body ACL 请求体
|
||||
* @return true 订阅;false 发布;null 不识别
|
||||
*/
|
||||
private static Boolean parseAclSubscribeFlag(JsonObject body) {
|
||||
// 1. action 字段(常见为 publish/subscribe)
|
||||
String action = body.getString("action");
|
||||
if (StrUtil.isNotBlank(action)) {
|
||||
String lower = action.toLowerCase(Locale.ROOT);
|
||||
if (lower.contains("sub")) {
|
||||
return true;
|
||||
}
|
||||
if (lower.contains("pub")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. access 字段:可能是数字或字符串
|
||||
Integer access = body.getInteger("access");
|
||||
if (access != null) {
|
||||
if (access == 1) {
|
||||
return true;
|
||||
}
|
||||
if (access == 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
String accessText = body.getString("access");
|
||||
if (StrUtil.isNotBlank(accessText)) {
|
||||
String lower = accessText.toLowerCase(Locale.ROOT);
|
||||
if (lower.contains("sub")) {
|
||||
return true;
|
||||
}
|
||||
if (lower.contains("pub")) {
|
||||
return false;
|
||||
}
|
||||
if (StrUtil.isNumeric(accessText)) {
|
||||
int value = Integer.parseInt(accessText);
|
||||
if (value == 1) {
|
||||
return true;
|
||||
}
|
||||
if (value == 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== 事件处理 ==========
|
||||
|
||||
/**
|
||||
* EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
|
||||
* 支持的事件类型:client.connected、client.disconnected 等
|
||||
@@ -124,58 +321,15 @@ public class IotEmqxAuthEventHandler {
|
||||
break;
|
||||
}
|
||||
|
||||
// EMQX Webhook 只需要 200 状态码,无需响应体
|
||||
// 3. EMQX Webhook 只需要 200 状态码,无需响应体
|
||||
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
|
||||
} catch (Exception e) {
|
||||
log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
|
||||
// 即使处理失败,也返回 200 避免EMQX重试
|
||||
log.error("[handleEvent][事件处理失败][body={}]", body, e);
|
||||
// 即使处理失败,也返回 200 避免 EMQX 重试
|
||||
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端连接事件
|
||||
*/
|
||||
private void handleClientConnected(JsonObject body) {
|
||||
String username = body.getString("username");
|
||||
log.info("[handleClientConnected][设备上线: {}]", username);
|
||||
handleDeviceStateChange(username, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接事件
|
||||
*/
|
||||
private void handleClientDisconnected(JsonObject body) {
|
||||
String username = body.getString("username");
|
||||
String reason = body.getString("reason");
|
||||
log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
|
||||
handleDeviceStateChange(username, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析认证接口请求体
|
||||
* <p>
|
||||
* 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
|
||||
*
|
||||
* @param context 路由上下文
|
||||
* @return 请求体JSON对象,解析失败时返回null
|
||||
*/
|
||||
private JsonObject parseRequestBody(RoutingContext context) {
|
||||
try {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
if (body == null) {
|
||||
log.info("[parseRequestBody][请求体为空]");
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return null;
|
||||
}
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析事件接口请求体
|
||||
* <p>
|
||||
@@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行设备认证
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 认证是否成功
|
||||
* 处理客户端连接事件
|
||||
*/
|
||||
private boolean handleDeviceAuth(String clientId, String username, String password) {
|
||||
try {
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId).setUsername(username).setPassword(password));
|
||||
result.checkError();
|
||||
return BooleanUtil.isTrue(result.getData());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
|
||||
throw e;
|
||||
}
|
||||
private void handleClientConnected(JsonObject body) {
|
||||
String username = body.getString("username");
|
||||
log.info("[handleClientConnected][设备上线: {}]", username);
|
||||
handleDeviceStateChange(username, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接事件
|
||||
*/
|
||||
private void handleClientDisconnected(JsonObject body) {
|
||||
String username = body.getString("username");
|
||||
String reason = body.getString("reason");
|
||||
log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
|
||||
handleDeviceStateChange(username, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ========= 注册处理 =========
|
||||
|
||||
/**
|
||||
* 发送 EMQX 认证响应
|
||||
* 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
|
||||
* 处理设备注册请求(一型一密)
|
||||
*
|
||||
* @param context 路由上下文
|
||||
* @param result 认证结果:allow、deny、ignore
|
||||
* @param context 路由上下文
|
||||
* @param username 用户名
|
||||
* @param password 密码(签名)
|
||||
*/
|
||||
private void sendAuthResponse(RoutingContext context, String result) {
|
||||
// 构建符合 EMQX 官方规范的响应
|
||||
JsonObject response = new JsonObject()
|
||||
.put("result", result)
|
||||
.put("is_superuser", false);
|
||||
private void handleDeviceRegister(RoutingContext context, String username, String password) {
|
||||
try {
|
||||
// 1. 解析设备信息
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username);
|
||||
sendAuthResponse(context, RESULT_DENY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 可以根据业务需求添加客户端属性
|
||||
// response.put("client_attrs", new JsonObject().put("role", "device"));
|
||||
// 2. 调用注册 API
|
||||
IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(deviceInfo.getProductKey())
|
||||
.setDeviceName(deviceInfo.getDeviceName())
|
||||
.setSign(password);
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
result.checkError();
|
||||
|
||||
// 可以添加认证过期时间(可选)
|
||||
// response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
|
||||
// 3. 允许连接
|
||||
log.info("[handleDeviceRegister][设备注册成功: {}]", username);
|
||||
sendAuthResponse(context, RESULT_ALLOW);
|
||||
|
||||
context.response()
|
||||
.setStatusCode(SUCCESS_STATUS_CODE)
|
||||
.putHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.end(response.encode());
|
||||
// 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅)
|
||||
sendRegisterResultMessage(username, result.getData());
|
||||
} catch (Exception e) {
|
||||
log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage());
|
||||
sendAuthResponse(context, RESULT_DENY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 发送注册结果消息给设备
|
||||
* <p>
|
||||
* 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param result 注册结果
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) {
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
Assert.notNull(deviceInfo, "设备信息不能为空");
|
||||
try {
|
||||
// 1.1 构建响应消息
|
||||
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null);
|
||||
// 1.2 序列化消息
|
||||
byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage,
|
||||
cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON);
|
||||
// 1.3 构建响应主题
|
||||
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method,
|
||||
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true);
|
||||
|
||||
// 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅)
|
||||
protocol.publishDelayMessage(replyTopic, encodedData, 5000);
|
||||
log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.mqtt.messages.MqttPublishMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler {
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
|
||||
public IotEmqxUpstreamHandler(String serverId) {
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
this.serverId = protocol.getServerId();
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MQTT 发布消息
|
||||
*/
|
||||
public void handle(MqttPublishMessage mqttMessage) {
|
||||
log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
|
||||
log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
|
||||
String topic = mqttMessage.topicName();
|
||||
byte[] payload = mqttMessage.payload().getBytes();
|
||||
try {
|
||||
// 1. 解析主题,一次性获取所有信息
|
||||
String[] topicParts = topic.split("/");
|
||||
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
|
||||
String productKey = ArrayUtil.get(topicParts, 2);
|
||||
String deviceName = ArrayUtil.get(topicParts, 3);
|
||||
if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) {
|
||||
log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
// 3. 解码消息
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
|
||||
// 2. 反序列化消息
|
||||
IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 发送消息到队列
|
||||
// 3. 发送消息到队列
|
||||
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,4 @@ import lombok.Data;
|
||||
@Data
|
||||
public class IotHttpConfig {
|
||||
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
private Boolean sslEnabled = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
|
||||
@@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolInstanceProperties properties;
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
*/
|
||||
private IotHttpDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
public IotHttpProtocol(ProtocolInstanceProperties properties) {
|
||||
public IotHttpProtocol(ProtocolProperties properties) {
|
||||
this.properties = properties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
|
||||
@@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
|
||||
|
||||
// 1.4 启动 HTTP 服务器
|
||||
IotHttpConfig httpConfig = properties.getHttp();
|
||||
HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
|
||||
if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
|
||||
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
|
||||
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(httpConfig.getSslKeyPath())
|
||||
.setCertPath(httpConfig.getSslCertPath());
|
||||
.setKeyPath(sslConfig.getSslKeyPath())
|
||||
.setCertPath(sslConfig.getSslCertPath());
|
||||
options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
try {
|
||||
@@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
|
||||
// 启动失败时关闭 Vertx
|
||||
// 启动失败时关闭资源
|
||||
if (httpServer != null) {
|
||||
httpServer.close();
|
||||
httpServer = null;
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
vertx = null;
|
||||
|
||||
@@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
|
||||
Assert.notNull(request, "请求参数不能为空");
|
||||
Assert.notBlank(request.getProductKey(), "productKey 不能为空");
|
||||
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
|
||||
Assert.notBlank(request.getSign(), "sign 不能为空");
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotMqttConfig {
|
||||
|
||||
/**
|
||||
* 最大消息大小(字节)
|
||||
*/
|
||||
@NotNull(message = "最大消息大小不能为空")
|
||||
@Min(value = 1024, message = "最大消息大小不能小于 1024 字节")
|
||||
private Integer maxMessageSize = 8192;
|
||||
|
||||
/**
|
||||
* 连接超时时间(秒)
|
||||
*/
|
||||
@NotNull(message = "连接超时时间不能为空")
|
||||
@Min(value = 1, message = "连接超时时间不能小于 1 秒")
|
||||
private Integer connectTimeoutSeconds = 60;
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:下行消息订阅器
|
||||
* <p>
|
||||
* 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
|
||||
|
||||
private final IotMqttDownstreamHandler downstreamHandler;
|
||||
|
||||
public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol,
|
||||
IotMqttDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
super(protocol, messageBus);
|
||||
this.downstreamHandler = downstreamHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMessage(IotDeviceMessage message) {
|
||||
boolean success = downstreamHandler.handleDownstreamMessage(message);
|
||||
if (success) {
|
||||
log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId());
|
||||
} else {
|
||||
log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import io.vertx.mqtt.MqttServer;
|
||||
import io.vertx.mqtt.MqttServerOptions;
|
||||
import io.vertx.mqtt.MqttTopicSubscription;
|
||||
import io.vertx.mqtt.messages.MqttPublishMessage;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttProtocol implements IotProtocol {
|
||||
|
||||
/**
|
||||
* 注册连接的 clientId 标识
|
||||
*
|
||||
* @see #handleEndpoint(MqttEndpoint)
|
||||
*/
|
||||
private static final String AUTH_TYPE_REGISTER = "|authType=register|";
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
/**
|
||||
* 运行状态
|
||||
*/
|
||||
@Getter
|
||||
private volatile boolean running = false;
|
||||
|
||||
/**
|
||||
* Vert.x 实例
|
||||
*/
|
||||
private Vertx vertx;
|
||||
/**
|
||||
* MQTT 服务器
|
||||
*/
|
||||
private MqttServer mqttServer;
|
||||
|
||||
/**
|
||||
* 连接管理器
|
||||
*/
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
/**
|
||||
* 下行消息订阅者
|
||||
*/
|
||||
private final IotMqttDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotMqttAuthHandler authHandler;
|
||||
private final IotMqttRegisterHandler registerHandler;
|
||||
private final IotMqttUpstreamHandler upstreamHandler;
|
||||
|
||||
public IotMqttProtocol(ProtocolProperties properties) {
|
||||
IotMqttConfig mqttConfig = properties.getMqtt();
|
||||
Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空");
|
||||
this.properties = properties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
|
||||
// 初始化连接管理器
|
||||
this.connectionManager = new IotMqttConnectionManager();
|
||||
|
||||
// 初始化 Handler
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
|
||||
this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
|
||||
this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
|
||||
|
||||
// 初始化下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
|
||||
this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return properties.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotProtocolTypeEnum getType() {
|
||||
return IotProtocolTypeEnum.MQTT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (running) {
|
||||
log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 创建 Vertx 实例
|
||||
this.vertx = Vertx.vertx();
|
||||
|
||||
// 1.2 创建服务器选项
|
||||
IotMqttConfig mqttConfig = properties.getMqtt();
|
||||
MqttServerOptions options = new MqttServerOptions()
|
||||
.setPort(properties.getPort())
|
||||
.setMaxMessageSize(mqttConfig.getMaxMessageSize())
|
||||
.setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds());
|
||||
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
|
||||
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(sslConfig.getSslKeyPath())
|
||||
.setCertPath(sslConfig.getSslCertPath());
|
||||
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
|
||||
// 1.3 创建服务器并设置连接处理器
|
||||
mqttServer = MqttServer.create(vertx, options);
|
||||
mqttServer.endpointHandler(this::handleEndpoint);
|
||||
|
||||
// 1.4 启动 MQTT 服务器
|
||||
try {
|
||||
mqttServer.listen().result();
|
||||
running = true;
|
||||
log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]",
|
||||
getId(), properties.getPort(), serverId);
|
||||
|
||||
// 2. 启动下行消息订阅者
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e);
|
||||
// 启动失败时关闭资源
|
||||
if (mqttServer != null) {
|
||||
mqttServer.close();
|
||||
mqttServer = null;
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
vertx = null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
// 1. 停止下行消息订阅者
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
}
|
||||
|
||||
// 2.1 关闭 MQTT 服务器
|
||||
if (mqttServer != null) {
|
||||
try {
|
||||
mqttServer.close().result();
|
||||
log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e);
|
||||
}
|
||||
mqttServer = null;
|
||||
}
|
||||
// 2.2 关闭 Vertx 实例
|
||||
if (vertx != null) {
|
||||
try {
|
||||
vertx.close().result();
|
||||
log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e);
|
||||
}
|
||||
vertx = null;
|
||||
}
|
||||
running = false;
|
||||
log.info("[stop][IoT MQTT 协议 {} 已停止]", getId());
|
||||
}
|
||||
|
||||
// ======================================= MQTT 连接处理 ======================================
|
||||
|
||||
/**
|
||||
* 处理 MQTT 连接端点
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
*/
|
||||
private void handleEndpoint(MqttEndpoint endpoint) {
|
||||
// 1. 如果是注册请求,注册待认证连接;否则走正常认证流程
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
|
||||
// 情况一:设备注册请求
|
||||
registerHandler.handleRegister(endpoint);
|
||||
return;
|
||||
} else {
|
||||
// 情况二:普通认证请求
|
||||
if (!authHandler.handleAuthenticationRequest(endpoint)) {
|
||||
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 设置异常和关闭处理器
|
||||
endpoint.exceptionHandler(ex -> {
|
||||
log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]",
|
||||
clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
|
||||
endpoint.close();
|
||||
});
|
||||
endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等)
|
||||
endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文
|
||||
log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
|
||||
cleanupConnection(endpoint);
|
||||
});
|
||||
// 2.2 设置心跳处理器
|
||||
endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId));
|
||||
|
||||
// 3.1 设置消息处理器
|
||||
endpoint.publishHandler(message -> processMessage(endpoint, message));
|
||||
// 3.2 设置 QoS 2 消息的 PUBREL 处理器
|
||||
endpoint.publishReleaseHandler(endpoint::publishComplete);
|
||||
|
||||
// 4.1 设置订阅处理器(带 ACL 校验)
|
||||
endpoint.subscribeHandler(subscribe -> {
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
|
||||
List<MqttQoS> grantedQoSLevels = new ArrayList<>();
|
||||
for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) {
|
||||
String topicName = sub.topicName();
|
||||
// 校验主题是否属于当前设备
|
||||
if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed(
|
||||
topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) {
|
||||
grantedQoSLevels.add(sub.qualityOfService());
|
||||
log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName);
|
||||
} else {
|
||||
log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName);
|
||||
grantedQoSLevels.add(MqttQoS.FAILURE);
|
||||
}
|
||||
}
|
||||
endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
|
||||
});
|
||||
// 4.2 设置取消订阅处理器
|
||||
endpoint.unsubscribeHandler(unsubscribe -> {
|
||||
log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
|
||||
endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
|
||||
});
|
||||
|
||||
// 5. 接受连接
|
||||
endpoint.accept(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息(发布)
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param message 发布消息
|
||||
*/
|
||||
private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
try {
|
||||
// 1. 处理业务消息
|
||||
String topic = message.topicName();
|
||||
byte[] payload = message.payload().getBytes();
|
||||
upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
|
||||
|
||||
// 2. 根据 QoS 级别发送相应的确认消息
|
||||
handleQoSAck(endpoint, message);
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
|
||||
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
|
||||
endpoint.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 QoS 确认
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param message 发布消息
|
||||
*/
|
||||
private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) {
|
||||
if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
|
||||
// QoS 1: 发送 PUBACK 确认
|
||||
endpoint.publishAcknowledge(message.messageId());
|
||||
} else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
|
||||
// QoS 2: 发送 PUBREC 确认
|
||||
endpoint.publishReceived(message.messageId());
|
||||
}
|
||||
// QoS 0 无需确认
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
*/
|
||||
private void cleanupConnection(MqttEndpoint endpoint) {
|
||||
try {
|
||||
// 1. 发送设备离线消息
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
|
||||
if (connectionInfo != null) {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
}
|
||||
|
||||
// 2. 注销连接
|
||||
connectionManager.unregisterConnection(endpoint);
|
||||
} catch (Exception e) {
|
||||
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
|
||||
endpoint.clientIdentifier(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.mqtt.MqttServer;
|
||||
import io.vertx.mqtt.MqttServerOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttUpstreamProtocol implements IotProtocol {
|
||||
|
||||
private static final String ID = "mqtt";
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final IotGatewayProperties.MqttProperties mqttProperties;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private MqttServer mqttServer;
|
||||
|
||||
public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttConnectionManager connectionManager,
|
||||
Vertx vertx) {
|
||||
this.mqttProperties = mqttProperties;
|
||||
this.messageService = messageService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotProtocolTypeEnum getType() {
|
||||
return IotProtocolTypeEnum.MQTT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
// TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈;
|
||||
@Override
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 创建服务器选项
|
||||
MqttServerOptions options = new MqttServerOptions()
|
||||
.setPort(mqttProperties.getPort())
|
||||
.setMaxMessageSize(mqttProperties.getMaxMessageSize())
|
||||
.setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds());
|
||||
|
||||
// 配置 SSL(如果启用)
|
||||
if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) {
|
||||
options.setSsl(true)
|
||||
.setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions())
|
||||
.setTrustOptions(mqttProperties.getSslOptions().getTrustOptions());
|
||||
}
|
||||
|
||||
// 创建服务器并设置连接处理器
|
||||
mqttServer = MqttServer.create(vertx, options);
|
||||
mqttServer.endpointHandler(endpoint -> {
|
||||
IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager);
|
||||
handler.handle(endpoint);
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
try {
|
||||
mqttServer.listen().result();
|
||||
running = true;
|
||||
log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 MQTT 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (mqttServer != null) {
|
||||
try {
|
||||
mqttServer.close().result();
|
||||
running = false;
|
||||
log.info("[stop][IoT 网关 MQTT 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 MQTT 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:下行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class IotMqttDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*
|
||||
* @param message 设备消息
|
||||
*/
|
||||
public void handle(IotDeviceMessage message) {
|
||||
try {
|
||||
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
|
||||
// 1. 检查设备连接
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
|
||||
message.getDeviceId());
|
||||
if (connectionInfo == null) {
|
||||
log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 序列化消息
|
||||
byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName());
|
||||
Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空");
|
||||
// 2.2 构建主题
|
||||
Assert.notBlank(message.getMethod(), "消息方法不能为空");
|
||||
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
|
||||
String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), isReply);
|
||||
Assert.notBlank(topic, "主题不能为空");
|
||||
|
||||
// 3. 发送到设备
|
||||
boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload,
|
||||
MqttQoS.AT_LEAST_ONCE.value(), false);
|
||||
if (!success) {
|
||||
throw new RuntimeException("下行消息发送失败");
|
||||
}
|
||||
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
|
||||
|
||||
private final IotMqttDownstreamHandler downstreamHandler;
|
||||
|
||||
public IotMqttDownstreamSubscriber(IotMqttProtocol protocol,
|
||||
IotMqttDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
super(protocol, messageBus);
|
||||
this.downstreamHandler = downstreamHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMessage(IotDeviceMessage message) {
|
||||
downstreamHandler.handle(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议的处理器抽象基类
|
||||
* <p>
|
||||
* 提供通用的连接校验、响应发送等功能
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public abstract class IotMqttAbstractHandler {
|
||||
|
||||
protected final IotMqttConnectionManager connectionManager;
|
||||
protected final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
/**
|
||||
* 发送成功响应到设备
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param requestId 请求 ID
|
||||
* @param method 方法名
|
||||
* @param data 响应数据
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
|
||||
String requestId, String method, Object data) {
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null);
|
||||
writeResponse(endpoint, productKey, deviceName, method, responseMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误响应到设备
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param requestId 请求 ID
|
||||
* @param method 方法名
|
||||
* @param errorCode 错误码
|
||||
* @param errorMessage 错误消息
|
||||
*/
|
||||
protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
|
||||
String requestId, String method, Integer errorCode, String errorMessage) {
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage);
|
||||
writeResponse(endpoint, productKey, deviceName, method, responseMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入响应消息到设备
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param method 方法名
|
||||
* @param responseMessage 响应消息
|
||||
*/
|
||||
private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName,
|
||||
String method, IotDeviceMessage responseMessage) {
|
||||
try {
|
||||
// 1.1 序列化消息(根据设备配置的序列化类型)
|
||||
byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName);
|
||||
// 1.2 构建响应主题
|
||||
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true);
|
||||
|
||||
// 2. 发送响应消息
|
||||
endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode());
|
||||
} catch (Exception e) {
|
||||
log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
|
||||
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 认证处理器
|
||||
* <p>
|
||||
* 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttAuthHandler extends IotMqttAbstractHandler {
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
private final IotDeviceService deviceService;
|
||||
private final String serverId;
|
||||
|
||||
public IotMqttAuthHandler(IotMqttConnectionManager connectionManager,
|
||||
IotDeviceMessageService deviceMessageService,
|
||||
IotDeviceCommonApi deviceApi,
|
||||
String serverId) {
|
||||
super(connectionManager, deviceMessageService);
|
||||
this.deviceApi = deviceApi;
|
||||
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MQTT 连接(认证)请求
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @return 认证是否成功
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
|
||||
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
|
||||
log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
|
||||
clientId, username, connectionManager.getEndpointAddress(endpoint));
|
||||
|
||||
try {
|
||||
// 1.1 解析认证参数
|
||||
Assert.notBlank(clientId, "clientId 不能为空");
|
||||
Assert.notBlank(username, "username 不能为空");
|
||||
Assert.notBlank(password, "password 不能为空");
|
||||
// 1.2 构建认证参数
|
||||
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId)
|
||||
.setUsername(username)
|
||||
.setPassword(password);
|
||||
|
||||
// 2.1 执行认证
|
||||
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
|
||||
authResult.checkError();
|
||||
if (BooleanUtil.isFalse(authResult.getData())) {
|
||||
throw exception(DEVICE_AUTH_FAIL);
|
||||
}
|
||||
// 2.2 解析设备信息
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
Assert.notNull(deviceInfo, "解析设备信息失败");
|
||||
// 2.3 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
Assert.notNull(device, "设备不存在");
|
||||
|
||||
// 3.1 注册连接
|
||||
registerConnection(endpoint, device, clientId);
|
||||
// 3.2 发送设备上线消息
|
||||
sendOnlineMessage(device);
|
||||
log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]",
|
||||
clientId, username, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册连接
|
||||
*/
|
||||
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
|
||||
.setDeviceId(device.getId())
|
||||
.setProductKey(device.getProductKey())
|
||||
.setDeviceName(device.getDeviceName())
|
||||
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
|
||||
connectionManager.registerConnection(endpoint, connectionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*/
|
||||
private void sendOnlineMessage(IotDeviceRespDTO device) {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
|
||||
IotDeviceMessageService deviceMessageService) {
|
||||
super(connectionManager, deviceMessageService);
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理注册连接
|
||||
* <p>
|
||||
* 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public void handleRegister(MqttEndpoint endpoint) {
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
|
||||
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
|
||||
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
|
||||
String productKey = null;
|
||||
String deviceName = null;
|
||||
|
||||
try {
|
||||
// 1.1 校验参数
|
||||
Assert.notBlank(clientId, "clientId 不能为空");
|
||||
Assert.notBlank(username, "username 不能为空");
|
||||
Assert.notBlank(password, "password 不能为空");
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
Assert.notNull(deviceInfo, "解析设备信息失败");
|
||||
productKey = deviceInfo.getProductKey();
|
||||
deviceName = deviceInfo.getDeviceName();
|
||||
log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]",
|
||||
clientId, productKey, deviceName);
|
||||
// 1.2 构建注册参数
|
||||
IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(productKey)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(password);
|
||||
|
||||
// 2. 调用动态注册 API
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
result.checkError();
|
||||
|
||||
// 3. 接受连接,并发送成功响应
|
||||
endpoint.accept(false);
|
||||
sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData());
|
||||
log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId);
|
||||
} catch (Exception e) {
|
||||
log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
|
||||
// 接受连接,并发送错误响应
|
||||
endpoint.accept(false);
|
||||
sendErrorResponse(endpoint, productKey, deviceName, null, method,
|
||||
INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
|
||||
} finally {
|
||||
// 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接)
|
||||
endpoint.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
|
||||
IotDeviceMessageService deviceMessageService,
|
||||
String serverId) {
|
||||
super(connectionManager, deviceMessageService);
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务消息
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) {
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
IotDeviceMessage message = null;
|
||||
String productKey = null;
|
||||
String deviceName = null;
|
||||
|
||||
try {
|
||||
// 1.1 基础检查
|
||||
if (ArrayUtil.isEmpty(payload)) {
|
||||
return;
|
||||
}
|
||||
// 1.2 解析主题,获取 productKey 和 deviceName
|
||||
String[] topicParts = topic.split("/");
|
||||
productKey = ArrayUtil.get(topicParts, 2);
|
||||
deviceName = ArrayUtil.get(topicParts, 3);
|
||||
Assert.notBlank(productKey, "产品 Key 不能为空");
|
||||
Assert.notBlank(deviceName, "设备名称不能为空");
|
||||
// 1.3 校验设备信息,防止伪造设备消息
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
|
||||
Assert.notNull(connectionInfo, "无法获取连接信息");
|
||||
Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
|
||||
Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
|
||||
|
||||
// 2. 反序列化消息
|
||||
message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
|
||||
sendErrorResponse(endpoint, productKey, deviceName, null, null,
|
||||
BAD_REQUEST.getCode(), "消息解码失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理业务消息
|
||||
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
|
||||
log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic);
|
||||
} catch (ServiceException e) {
|
||||
log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage());
|
||||
String requestId = message != null ? message.getRequestId() : null;
|
||||
String method = message != null ? message.getMethod() : null;
|
||||
sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage());
|
||||
String requestId = message != null ? message.getRequestId() : null;
|
||||
String method = message != null ? message.getMethod() : null;
|
||||
sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
|
||||
BAD_REQUEST.getCode(), e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage(), e);
|
||||
String requestId = message != null ? message.getRequestId() : null;
|
||||
String method = message != null ? message.getMethod() : null;
|
||||
sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
|
||||
INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,7 +66,6 @@ public class IotMqttConnectionManager {
|
||||
} catch (Exception ignored) {
|
||||
// 连接已关闭,忽略异常
|
||||
}
|
||||
|
||||
return realTimeAddress;
|
||||
}
|
||||
|
||||
@@ -74,24 +73,24 @@ public class IotMqttConnectionManager {
|
||||
* 注册设备连接(包含认证信息)
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param deviceId 设备 ID
|
||||
* @param connectionInfo 连接信息
|
||||
*/
|
||||
public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
|
||||
public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) {
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
// 如果设备已有其他连接,先清理旧连接
|
||||
MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
|
||||
if (oldEndpoint != null && oldEndpoint != endpoint) {
|
||||
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
|
||||
deviceId, getEndpointAddress(oldEndpoint));
|
||||
oldEndpoint.close();
|
||||
// 清理旧连接的映射
|
||||
// 先清理映射,再关闭连接(避免旧连接处理器干扰)
|
||||
connectionMap.remove(oldEndpoint);
|
||||
oldEndpoint.close();
|
||||
}
|
||||
|
||||
// 注册新连接
|
||||
connectionMap.put(endpoint, connectionInfo);
|
||||
deviceEndpointMap.put(deviceId, endpoint);
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]",
|
||||
deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
|
||||
}
|
||||
|
||||
@@ -129,25 +128,10 @@ public class IotMqttConnectionManager {
|
||||
if (endpoint == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取连接信息
|
||||
return getConnectionInfo(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否在线
|
||||
*/
|
||||
public boolean isDeviceOnline(Long deviceId) {
|
||||
return deviceEndpointMap.containsKey(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否离线
|
||||
*/
|
||||
public boolean isDeviceOffline(Long deviceId) {
|
||||
return !isDeviceOnline(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备
|
||||
*
|
||||
@@ -192,27 +176,15 @@ public class IotMqttConnectionManager {
|
||||
* 设备 ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 产品 Key
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 客户端 ID
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
private boolean authenticated;
|
||||
|
||||
/**
|
||||
* 连接地址
|
||||
*/
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:下行消息处理器
|
||||
* <p>
|
||||
* 专门处理下行消息的业务逻辑,包括:
|
||||
* 1. 消息编码
|
||||
* 2. 主题构建
|
||||
* 3. 消息发送
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
|
||||
public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService,
|
||||
IotMqttConnectionManager connectionManager) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @return 是否处理成功
|
||||
*/
|
||||
public boolean handleDownstreamMessage(IotDeviceMessage message) {
|
||||
try {
|
||||
// 1. 基础校验
|
||||
if (message == null || message.getDeviceId() == null) {
|
||||
log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查设备是否在线
|
||||
if (connectionManager.isDeviceOffline(message.getDeviceId())) {
|
||||
log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 获取连接信息
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
|
||||
if (connectionInfo == null) {
|
||||
log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 编码消息
|
||||
byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName());
|
||||
if (payload == null || payload.length == 0) {
|
||||
log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 发送消息到设备
|
||||
return sendMessageToDevice(message, connectionInfo, payload);
|
||||
} catch (Exception e) {
|
||||
if (message != null) {
|
||||
log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",
|
||||
message.getDeviceId(), e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @param connectionInfo 连接信息
|
||||
* @param payload 消息负载
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
private boolean sendMessageToDevice(IotDeviceMessage message,
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo,
|
||||
byte[] payload) {
|
||||
// 1. 构建主题
|
||||
String topic = buildDownstreamTopic(message, connectionInfo);
|
||||
if (StrUtil.isBlank(topic)) {
|
||||
log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]",
|
||||
message.getDeviceId(), message.getMethod());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 发送消息
|
||||
boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false);
|
||||
if (success) {
|
||||
log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]",
|
||||
message.getDeviceId(), topic, message.getMethod());
|
||||
} else {
|
||||
log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]",
|
||||
message.getDeviceId(), topic, message.getMethod());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建下行消息主题
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @param connectionInfo 连接信息
|
||||
* @return 主题
|
||||
*/
|
||||
private String buildDownstreamTopic(IotDeviceMessage message,
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo) {
|
||||
String method = message.getMethod();
|
||||
if (StrUtil.isBlank(method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用工具类构建主题,支持回复消息处理
|
||||
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
|
||||
return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), isReply);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import io.vertx.mqtt.MqttTopicSubscription;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MQTT 上行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttUpstreamHandler {
|
||||
|
||||
/**
|
||||
* 默认编解码类型(MQTT 使用 Alink 协议)
|
||||
*/
|
||||
private static final String DEFAULT_CODEC_TYPE = "Alink";
|
||||
|
||||
/**
|
||||
* register 请求的 topic 后缀
|
||||
*/
|
||||
private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol,
|
||||
IotDeviceMessageService deviceMessageService,
|
||||
IotMqttConnectionManager connectionManager) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.connectionManager = connectionManager;
|
||||
this.serverId = protocol.getServerId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MQTT 连接
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
*/
|
||||
public void handle(MqttEndpoint endpoint) {
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
|
||||
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
|
||||
|
||||
log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
|
||||
clientId, username, connectionManager.getEndpointAddress(endpoint));
|
||||
|
||||
// 1. 先进行认证
|
||||
if (!authenticateDevice(clientId, username, password, endpoint)) {
|
||||
log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username);
|
||||
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
|
||||
|
||||
// 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
|
||||
endpoint.pingHandler(v -> {
|
||||
log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
|
||||
// Vert.x 会自动发送 PINGRESP 响应,无需手动处理
|
||||
});
|
||||
|
||||
// 3. 设置异常和关闭处理器
|
||||
endpoint.exceptionHandler(ex -> {
|
||||
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
|
||||
cleanupConnection(endpoint);
|
||||
});
|
||||
endpoint.closeHandler(v -> {
|
||||
cleanupConnection(endpoint);
|
||||
});
|
||||
|
||||
// 4. 设置消息处理器
|
||||
endpoint.publishHandler(mqttMessage -> {
|
||||
try {
|
||||
// 4.1 根据 topic 判断是否为 register 请求
|
||||
String topic = mqttMessage.topicName();
|
||||
byte[] payload = mqttMessage.payload().getBytes();
|
||||
if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
|
||||
// register 请求:使用默认编解码器处理(设备可能未注册)
|
||||
processRegisterMessage(clientId, topic, payload, endpoint);
|
||||
} else {
|
||||
// 业务请求:正常处理
|
||||
processMessage(clientId, topic, payload);
|
||||
}
|
||||
|
||||
// 4.2 根据 QoS 级别发送相应的确认消息
|
||||
if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
|
||||
// QoS 1: 发送 PUBACK 确认
|
||||
endpoint.publishAcknowledge(mqttMessage.messageId());
|
||||
} else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
|
||||
// QoS 2: 发送 PUBREC 确认
|
||||
endpoint.publishReceived(mqttMessage.messageId());
|
||||
}
|
||||
// QoS 0 无需确认
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
|
||||
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
|
||||
cleanupConnection(endpoint);
|
||||
endpoint.close();
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 设置订阅处理器
|
||||
endpoint.subscribeHandler(subscribe -> {
|
||||
// 提取主题名称列表用于日志显示
|
||||
List<String> topicNames = subscribe.topicSubscriptions().stream()
|
||||
.map(MqttTopicSubscription::topicName)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
|
||||
|
||||
// 提取 QoS 列表
|
||||
List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream()
|
||||
.map(MqttTopicSubscription::qualityOfService)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
|
||||
});
|
||||
|
||||
// 6. 设置取消订阅处理器
|
||||
endpoint.unsubscribeHandler(unsubscribe -> {
|
||||
log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
|
||||
endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
|
||||
});
|
||||
|
||||
// 7. 设置 QoS 2消息的 PUBREL 处理器
|
||||
endpoint.publishReleaseHandler(endpoint::publishComplete);
|
||||
|
||||
// 8. 设置断开连接处理器
|
||||
endpoint.disconnectHandler(v -> {
|
||||
log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
|
||||
cleanupConnection(endpoint);
|
||||
});
|
||||
|
||||
// 9. 接受连接
|
||||
endpoint.accept(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
private void processMessage(String clientId, String topic, byte[] payload) {
|
||||
// 1. 基础检查
|
||||
if (payload == null || payload.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 解析主题,获取 productKey 和 deviceName
|
||||
String[] topicParts = topic.split("/");
|
||||
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
|
||||
log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
try {
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 处理业务消息(认证已在连接时完成)
|
||||
log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
|
||||
productKey, deviceName, message.getMethod());
|
||||
handleBusinessRequest(message, productKey, deviceName);
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 MQTT 连接时进行设备认证
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @return 认证是否成功
|
||||
*/
|
||||
private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) {
|
||||
try {
|
||||
// 1. 参数校验
|
||||
if (StrUtil.hasEmpty(clientId, username, password)) {
|
||||
log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 构建认证参数
|
||||
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId)
|
||||
.setUsername(username)
|
||||
.setPassword(password);
|
||||
|
||||
// 3. 调用设备认证 API
|
||||
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
|
||||
if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
|
||||
log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]",
|
||||
clientId, username, authResult.getMsg());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 获取设备信息
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
|
||||
return false;
|
||||
}
|
||||
|
||||
IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO()
|
||||
.setProductKey(deviceInfo.getProductKey())
|
||||
.setDeviceName(deviceInfo.getDeviceName());
|
||||
|
||||
CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO);
|
||||
if (!deviceResult.isSuccess() || deviceResult.getData() == null) {
|
||||
log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]",
|
||||
clientId, username, deviceResult.getMsg());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 注册连接
|
||||
IotDeviceRespDTO device = deviceResult.getData();
|
||||
registerConnection(endpoint, device, clientId);
|
||||
|
||||
// 6. 发送设备上线消息
|
||||
sendOnlineMessage(device);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 register 消息(设备动态注册,使用默认编解码器)
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
* @param endpoint MQTT 连接端点
|
||||
*/
|
||||
private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
|
||||
// 1.1 基础检查
|
||||
if (ArrayUtil.isEmpty(payload)) {
|
||||
return;
|
||||
}
|
||||
// 1.2 解析主题,获取 productKey 和 deviceName
|
||||
String[] topicParts = topic.split("/");
|
||||
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
|
||||
log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
|
||||
return;
|
||||
}
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
// 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
|
||||
IotDeviceMessage message;
|
||||
try {
|
||||
message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
|
||||
if (message == null) {
|
||||
log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理设备动态注册请求
|
||||
log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
|
||||
productKey, deviceName, message.getMethod());
|
||||
try {
|
||||
handleRegisterRequest(message, productKey, deviceName, endpoint);
|
||||
} catch (Exception e) {
|
||||
log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备动态注册请求(一型一密,不需要 deviceSecret)
|
||||
*
|
||||
* @param message 消息信息
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @see <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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
*/
|
||||
private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) {
|
||||
// 发送消息到消息总线
|
||||
message.setServerId(serverId);
|
||||
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册连接
|
||||
*/
|
||||
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
|
||||
.setDeviceId(device.getId())
|
||||
.setProductKey(device.getProductKey())
|
||||
.setDeviceName(device.getDeviceName())
|
||||
.setClientId(clientId)
|
||||
.setAuthenticated(true)
|
||||
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
|
||||
connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*/
|
||||
private void sendOnlineMessage(IotDeviceRespDTO device) {
|
||||
try {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接
|
||||
*/
|
||||
private void cleanupConnection(MqttEndpoint endpoint) {
|
||||
try {
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
|
||||
if (connectionInfo != null) {
|
||||
// 发送设备离线消息
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
|
||||
}
|
||||
|
||||
// 注销连接
|
||||
connectionManager.unregisterConnection(endpoint);
|
||||
} catch (Exception e) {
|
||||
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
* 提供设备接入的各种协议的实现
|
||||
* 设备接入协议:MQTT、EMQX、HTTP、TCP 等协议的实现
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol;
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol;
|
||||
|
||||
@@ -27,20 +27,6 @@ public class IotTcpConfig {
|
||||
@Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒")
|
||||
private Long keepAliveTimeoutMs = 30000L;
|
||||
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
@NotNull(message = "是否启用 SSL 不能为空")
|
||||
private Boolean sslEnabled = false;
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
|
||||
/**
|
||||
* 拆包配置
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
|
||||
@@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* IoT TCP 协议实现
|
||||
@@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol {
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolInstanceProperties properties;
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol {
|
||||
*/
|
||||
private final IotTcpFrameCodec frameCodec;
|
||||
|
||||
public IotTcpProtocol(ProtocolInstanceProperties properties) {
|
||||
public IotTcpProtocol(ProtocolProperties properties) {
|
||||
IotTcpConfig tcpConfig = properties.getTcp();
|
||||
Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空");
|
||||
Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空");
|
||||
@@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol {
|
||||
.setTcpNoDelay(true)
|
||||
.setReuseAddress(true)
|
||||
.setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时
|
||||
if (Boolean.TRUE.equals(tcpConfig.getSslEnabled())) {
|
||||
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
|
||||
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(tcpConfig.getSslKeyPath())
|
||||
.setCertPath(tcpConfig.getSslCertPath());
|
||||
.setKeyPath(sslConfig.getSslKeyPath())
|
||||
.setCertPath(sslConfig.getSslCertPath());
|
||||
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import io.vertx.core.Handler;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* IoT TCP 分隔符帧编解码器
|
||||
@@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
|
||||
private final byte[] delimiterBytes;
|
||||
|
||||
public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
|
||||
Assert.hasText(config.getDelimiter(), "delimiter 不能为空");
|
||||
Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
|
||||
this.delimiterBytes = parseDelimiter(config.getDelimiter());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.vertx.core.Handler;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* IoT TCP 定长帧编解码器
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.vertx.core.Handler;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ public class IotTcpDownstreamHandler {
|
||||
}
|
||||
|
||||
// 2. 序列化 + 帧编码
|
||||
byte[] serializedData = serializer.serialize(message);
|
||||
Buffer frameData = codec.encode(serializedData);
|
||||
byte[] payload = serializer.serialize(message);
|
||||
Buffer frameData = codec.encode(payload);
|
||||
|
||||
// 3. 发送到设备
|
||||
boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes());
|
||||
|
||||
@@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
@@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
// 1. 解析认证参数
|
||||
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
|
||||
Assert.notNull(authParams, "认证参数不能为空");
|
||||
Assert.hasText(authParams.getUsername(), "username 不能为空");
|
||||
Assert.hasText(authParams.getPassword(), "password 不能为空");
|
||||
Assert.notBlank(authParams.getUsername(), "username 不能为空");
|
||||
Assert.notBlank(authParams.getPassword(), "password 不能为空");
|
||||
|
||||
// 2.1 执行认证
|
||||
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
|
||||
@@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
|
||||
Assert.notNull(params, "注册参数不能为空");
|
||||
Assert.hasText(params.getProductKey(), "productKey 不能为空");
|
||||
Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(params.getProductKey(), "productKey 不能为空");
|
||||
Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(params.getSign(), "sign 不能为空");
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
|
||||
@@ -47,8 +47,8 @@ public class IotTcpConnectionManager {
|
||||
* @param deviceId 设备 ID
|
||||
* @param connectionInfo 连接信息
|
||||
*/
|
||||
public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
|
||||
// 检查连接数是否已达上限
|
||||
public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
|
||||
// 检查连接数是否已达上限(同步方法确保检查和注册的原子性)
|
||||
if (connectionMap.size() >= maxConnections) {
|
||||
throw new IllegalStateException("连接数已达上限: " + maxConnections);
|
||||
}
|
||||
@@ -57,9 +57,9 @@ public class IotTcpConnectionManager {
|
||||
if (oldSocket != null && oldSocket != socket) {
|
||||
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
|
||||
deviceId, oldSocket.remoteAddress());
|
||||
oldSocket.close();
|
||||
// 清理旧连接的映射
|
||||
// 先清理映射,再关闭连接
|
||||
connectionMap.remove(oldSocket);
|
||||
oldSocket.close();
|
||||
}
|
||||
|
||||
// 注册新连接
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber;
|
||||
@@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket;
|
||||
import io.vertx.core.datagram.DatagramSocketOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* IoT UDP 协议实现
|
||||
@@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol {
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolInstanceProperties properties;
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol {
|
||||
*/
|
||||
private final IotMessageSerializer serializer;
|
||||
|
||||
public IotUdpProtocol(ProtocolInstanceProperties properties) {
|
||||
public IotUdpProtocol(ProtocolProperties properties) {
|
||||
IotUdpConfig udpConfig = properties.getUdp();
|
||||
Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空");
|
||||
this.properties = properties;
|
||||
|
||||
@@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.datagram.DatagramPacket;
|
||||
import io.vertx.core.datagram.DatagramSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
@@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler {
|
||||
// 1. 解析认证参数
|
||||
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
|
||||
Assert.notNull(authParams, "认证参数不能为空");
|
||||
Assert.hasText(authParams.getUsername(), "username 不能为空");
|
||||
Assert.hasText(authParams.getPassword(), "password 不能为空");
|
||||
Assert.notBlank(authParams.getUsername(), "username 不能为空");
|
||||
Assert.notBlank(authParams.getPassword(), "password 不能为空");
|
||||
|
||||
// 2.1 执行认证
|
||||
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
|
||||
@@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
|
||||
Assert.notNull(params, "注册参数不能为空");
|
||||
Assert.hasText(params.getProductKey(), "productKey 不能为空");
|
||||
Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(params.getProductKey(), "productKey 不能为空");
|
||||
Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(params.getSign(), "sign 不能为空");
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
|
||||
@@ -9,6 +9,7 @@ import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -47,8 +48,8 @@ public class IotUdpSessionManager {
|
||||
* @param deviceId 设备 ID
|
||||
* @param sessionInfo 会话信息
|
||||
*/
|
||||
public void registerSession(Long deviceId, SessionInfo sessionInfo) {
|
||||
// 检查是否为新设备,且会话数已达上限
|
||||
public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) {
|
||||
// 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性)
|
||||
if (deviceSessionCache.getIfPresent(deviceId) == null
|
||||
&& deviceSessionCache.size() >= maxSessions) {
|
||||
throw new IllegalStateException("会话数已达上限: " + maxSessions);
|
||||
@@ -113,16 +114,21 @@ public class IotUdpSessionManager {
|
||||
}
|
||||
InetSocketAddress address = sessionInfo.getAddress();
|
||||
try {
|
||||
// 使用 CompletableFuture 同步等待发送结果
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
|
||||
if (result.succeeded()) {
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
|
||||
deviceId, buildAddressKey(address), data.length);
|
||||
return;
|
||||
future.complete(true);
|
||||
} else {
|
||||
log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
|
||||
deviceId, buildAddressKey(address), result.cause());
|
||||
future.complete(false);
|
||||
}
|
||||
log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
|
||||
deviceId, buildAddressKey(address), result.cause());
|
||||
});
|
||||
return true;
|
||||
// 同步等待结果,超时 5 秒
|
||||
return future.get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
|
||||
return false;
|
||||
|
||||
@@ -35,20 +35,4 @@ public class IotWebSocketConfig {
|
||||
@NotNull(message = "空闲超时时间不能为空")
|
||||
private Integer idleTimeoutSeconds = 60;
|
||||
|
||||
/**
|
||||
* 是否启用 SSL(wss://)
|
||||
*/
|
||||
@NotNull(message = "是否启用 SSL 不能为空")
|
||||
private Boolean sslEnabled = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler;
|
||||
@@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* IoT WebSocket 协议实现
|
||||
@@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol {
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolInstanceProperties properties;
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol {
|
||||
*/
|
||||
private final IotMessageSerializer serializer;
|
||||
|
||||
public IotWebSocketProtocol(ProtocolInstanceProperties properties) {
|
||||
public IotWebSocketProtocol(ProtocolProperties properties) {
|
||||
Assert.notNull(properties, "协议实例配置不能为空");
|
||||
Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空");
|
||||
this.properties = properties;
|
||||
@@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol {
|
||||
.setIdleTimeout(wsConfig.getIdleTimeoutSeconds())
|
||||
.setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize())
|
||||
.setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize());
|
||||
if (Boolean.TRUE.equals(wsConfig.getSslEnabled())) {
|
||||
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
|
||||
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(wsConfig.getSslKeyPath())
|
||||
.setCertPath(wsConfig.getSslCertPath());
|
||||
.setKeyPath(sslConfig.getSslKeyPath())
|
||||
.setCertPath(sslConfig.getSslCertPath());
|
||||
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
@@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
|
||||
// 1.2 解码消息
|
||||
message = serializer.deserialize(payload);
|
||||
Assert.notNull(message, "消息反序列化失败");
|
||||
Assert.hasText(message.getMethod(), "method 不能为空");
|
||||
Assert.notBlank(message.getMethod(), "method 不能为空");
|
||||
|
||||
// 2. 根据消息类型路由处理
|
||||
if (AUTH_METHOD.equals(message.getMethod())) {
|
||||
@@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
|
||||
// 1. 解析认证参数
|
||||
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
|
||||
Assert.notNull(authParams, "认证参数不能为空");
|
||||
Assert.hasText(authParams.getUsername(), "username 不能为空");
|
||||
Assert.hasText(authParams.getPassword(), "password 不能为空");
|
||||
Assert.notBlank(authParams.getUsername(), "username 不能为空");
|
||||
Assert.notBlank(authParams.getPassword(), "password 不能为空");
|
||||
|
||||
// 2.1 执行认证
|
||||
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
|
||||
@@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
|
||||
Assert.notNull(params, "注册参数不能为空");
|
||||
Assert.hasText(params.getProductKey(), "productKey 不能为空");
|
||||
Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(params.getProductKey(), "productKey 不能为空");
|
||||
Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
|
||||
Assert.notBlank(params.getSign(), "sign 不能为空");
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 消息序列化:将设备消息转换为字节数组(JSON、二进制等格式)
|
||||
*
|
||||
* @see cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.serialize;
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device.message;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
|
||||
/**
|
||||
@@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
public interface IotDeviceMessageService {
|
||||
|
||||
/**
|
||||
* 编码消息
|
||||
* 序列化消息
|
||||
*
|
||||
* @param message 消息
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 编码后的消息内容
|
||||
* @return 序列化后的消息内容
|
||||
*/
|
||||
byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName);
|
||||
byte[] serializeDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 编码消息
|
||||
* 序列化消息
|
||||
*
|
||||
* @param message 消息
|
||||
* @param codecType 编解码器类型
|
||||
* @return 编码后的消息内容
|
||||
* @param message 消息
|
||||
* @param serializeType 序列化类型
|
||||
* @return 序列化后的消息内容
|
||||
*/
|
||||
byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String codecType);
|
||||
byte[] serializeDeviceMessage(IotDeviceMessage message,
|
||||
IotSerializeTypeEnum serializeType);
|
||||
|
||||
/**
|
||||
* 解码消息
|
||||
* 反序列化消息
|
||||
*
|
||||
* @param bytes 消息内容
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 解码后的消息内容
|
||||
* @return 反序列化后的消息内容
|
||||
*/
|
||||
IotDeviceMessage decodeDeviceMessage(byte[] bytes,
|
||||
String productKey, String deviceName);
|
||||
IotDeviceMessage deserializeDeviceMessage(byte[] bytes,
|
||||
String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 解码消息
|
||||
* 反序列化消息
|
||||
*
|
||||
* @param bytes 消息内容
|
||||
* @param codecType 编解码器类型
|
||||
* @return 解码后的消息内容
|
||||
* @param bytes 消息内容
|
||||
* @param serializeType 序列化类型
|
||||
* @return 反序列化后的消息内容
|
||||
*/
|
||||
IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType);
|
||||
IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType);
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device.message;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
|
||||
@@ -28,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC
|
||||
@Slf4j
|
||||
public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
|
||||
|
||||
/**
|
||||
* 编解码器
|
||||
*/
|
||||
private final Map<String, IotDeviceMessageCodec> codes;
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private IotDeviceMessageProducer deviceMessageProducer;
|
||||
|
||||
public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) {
|
||||
this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type);
|
||||
}
|
||||
@Resource
|
||||
private IotMessageSerializerManager messageSerializerManager;
|
||||
|
||||
@Override
|
||||
public byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName) {
|
||||
public byte[] serializeDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName) {
|
||||
// 1.1 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
|
||||
if (device == null) {
|
||||
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
|
||||
}
|
||||
// 1.2 获取编解码器
|
||||
IotDeviceMessageCodec codec = codes.get(device.getCodecType());
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
|
||||
}
|
||||
// 1.2 获取序列化器
|
||||
IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
|
||||
Assert.notNull(serializeType, "设备序列化类型不能为空");
|
||||
|
||||
// 2. 编码消息
|
||||
return codec.encode(message);
|
||||
// 2. 序列化消息
|
||||
return serializeDeviceMessage(message, serializeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String codecType) {
|
||||
// 1. 获取编解码器
|
||||
IotDeviceMessageCodec codec = codes.get(codecType);
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
|
||||
public byte[] serializeDeviceMessage(IotDeviceMessage message,
|
||||
IotSerializeTypeEnum serializeType) {
|
||||
// 1. 获取序列化器
|
||||
IotMessageSerializer serializer = messageSerializerManager.get(serializeType);
|
||||
if (serializer == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType));
|
||||
}
|
||||
|
||||
// 2. 编码消息
|
||||
return codec.encode(message);
|
||||
// 2. 序列化消息
|
||||
return serializer.serialize(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceMessage decodeDeviceMessage(byte[] bytes,
|
||||
String productKey, String deviceName) {
|
||||
public IotDeviceMessage deserializeDeviceMessage(byte[] bytes,
|
||||
String productKey, String deviceName) {
|
||||
// 1.1 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
|
||||
if (device == null) {
|
||||
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
|
||||
}
|
||||
// 1.2 获取编解码器
|
||||
IotDeviceMessageCodec codec = codes.get(device.getCodecType());
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
|
||||
}
|
||||
// 1.2 获取序列化器
|
||||
IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
|
||||
Assert.notNull(serializeType, "设备序列化类型不能为空");
|
||||
|
||||
// 2. 解码消息
|
||||
return codec.decode(bytes);
|
||||
// 2. 反序列化消息
|
||||
return deserializeDeviceMessage(bytes, serializeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) {
|
||||
// 1. 获取编解码器
|
||||
IotDeviceMessageCodec codec = codes.get(codecType);
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
|
||||
public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType) {
|
||||
// 1. 获取序列化器
|
||||
IotMessageSerializer serializer = messageSerializerManager.get(serializeType);
|
||||
if (serializer == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType));
|
||||
}
|
||||
|
||||
// 2. 解码消息
|
||||
return codec.decode(bytes);
|
||||
// 2. 反序列化消息
|
||||
return serializer.deserialize(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -38,6 +38,12 @@ public final class IotMqttTopicUtils {
|
||||
*/
|
||||
public static final String MQTT_EVENT_PATH = "/mqtt/event";
|
||||
|
||||
/**
|
||||
* MQTT ACL 接口路径
|
||||
* 对应 EMQX HTTP ACL 插件的 ACL 请求接口
|
||||
*/
|
||||
public static final String MQTT_ACL_PATH = "/mqtt/acl";
|
||||
|
||||
// ========== 工具方法 ==========
|
||||
|
||||
/**
|
||||
@@ -63,4 +69,50 @@ public final class IotMqttTopicUtils {
|
||||
return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 校验主题是否允许订阅
|
||||
* <p>
|
||||
* 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,
|
||||
* 或者是通配符形式 /sys/{productKey}/{deviceName}/#
|
||||
*
|
||||
* @param topic 订阅的主题
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 是否允许订阅
|
||||
*/
|
||||
public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) {
|
||||
if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) {
|
||||
return false;
|
||||
}
|
||||
// 构建设备主题前缀
|
||||
String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/";
|
||||
// 主题必须以设备前缀开头,或者是设备前缀的通配符形式
|
||||
return topic.startsWith(deviceTopicPrefix)
|
||||
|| topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#");
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验主题是否允许发布
|
||||
* <p>
|
||||
* 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,且不允许包含通配符(+/#)。
|
||||
*
|
||||
* @param topic 发布的主题
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 是否允许发布
|
||||
*/
|
||||
// TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用;
|
||||
// TODO @AI:那你改下 mqtt,也调用!!!
|
||||
public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) {
|
||||
if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) {
|
||||
return false;
|
||||
}
|
||||
// MQTT publish topic 不允许包含通配符,但这里做一次兜底校验
|
||||
if (topic.contains("#") || topic.contains("+")) {
|
||||
return false;
|
||||
}
|
||||
String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/";
|
||||
return topic.startsWith(deviceTopicPrefix);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ yudao:
|
||||
# 针对引入的 HTTP 组件的配置
|
||||
# ====================================
|
||||
- id: http-json
|
||||
type: http
|
||||
protocol: http
|
||||
port: 8092
|
||||
enabled: false
|
||||
http:
|
||||
@@ -57,9 +57,9 @@ yudao:
|
||||
# 针对引入的 TCP 组件的配置
|
||||
# ====================================
|
||||
- id: tcp-json
|
||||
type: tcp
|
||||
port: 8091
|
||||
enabled: false
|
||||
protocol: tcp
|
||||
port: 8091
|
||||
serialize: json
|
||||
tcp:
|
||||
max-connections: 1000
|
||||
@@ -79,9 +79,9 @@ yudao:
|
||||
# 针对引入的 UDP 组件的配置
|
||||
# ====================================
|
||||
- id: udp-json
|
||||
type: udp
|
||||
port: 8093
|
||||
enabled: false
|
||||
protocol: udp
|
||||
port: 8093
|
||||
serialize: json
|
||||
udp:
|
||||
max-sessions: 1000 # 最大会话数
|
||||
@@ -92,9 +92,9 @@ yudao:
|
||||
# 针对引入的 WebSocket 组件的配置
|
||||
# ====================================
|
||||
- id: websocket-json
|
||||
type: websocket
|
||||
enabled: false
|
||||
protocol: websocket
|
||||
port: 8094
|
||||
enabled: true
|
||||
serialize: json
|
||||
websocket:
|
||||
path: /ws
|
||||
@@ -106,21 +106,68 @@ yudao:
|
||||
# 针对引入的 CoAP 组件的配置
|
||||
# ====================================
|
||||
- id: coap-json
|
||||
type: coap
|
||||
enabled: false
|
||||
protocol: coap
|
||||
port: 5683
|
||||
enabled: true
|
||||
coap:
|
||||
max-message-size: 1024 # 最大消息大小(字节)
|
||||
ack-timeout-ms: 2000 # ACK 超时时间(毫秒)
|
||||
max-retransmit: 4 # 最大重传次数
|
||||
|
||||
# 协议配置(旧版,保持兼容)
|
||||
protocol:
|
||||
# ====================================
|
||||
# 针对引入的 MQTT 组件的配置
|
||||
# ====================================
|
||||
- id: mqtt-json
|
||||
enabled: true
|
||||
protocol: mqtt
|
||||
port: 1883
|
||||
serialize: json
|
||||
mqtt:
|
||||
max-message-size: 8192 # 最大消息大小(字节)
|
||||
connect-timeout-seconds: 60 # 连接超时时间(秒)
|
||||
ssl-enabled: false # 是否启用 SSL
|
||||
# ====================================
|
||||
# 针对引入的 EMQX 组件的配置
|
||||
# ====================================
|
||||
emqx:
|
||||
- id: emqx-1
|
||||
enabled: false
|
||||
protocol: emqx
|
||||
port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event)
|
||||
emqx:
|
||||
mqtt-host: 127.0.0.1 # MQTT Broker 地址
|
||||
mqtt-port: 1883 # MQTT Broker 端口
|
||||
mqtt-username: admin # MQTT 用户名
|
||||
mqtt-password: public # MQTT 密码
|
||||
mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID
|
||||
mqtt-ssl: false # 是否开启 SSL
|
||||
mqtt-topics:
|
||||
- "/sys/#" # 系统主题
|
||||
mqtt-qos: 1 # 默认 QoS
|
||||
clean-session: true # 是否启用 Clean Session (默认: true)
|
||||
keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60)
|
||||
max-inflight-queue: 10000 # 最大飞行消息队列,单位:条
|
||||
connect-timeout-seconds: 10 # 连接超时,单位:秒
|
||||
reconnect-delay-ms: 5000 # 重连延迟,单位:毫秒
|
||||
# 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false!
|
||||
# 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true
|
||||
trust-all: true # 在 dev 环境可以设为 true
|
||||
# EMQX HTTP Hook 回调网关的 HTTPS 配置(可选)
|
||||
http:
|
||||
ssl-enabled: false
|
||||
# ssl-cert-path: "path/to/server.crt"
|
||||
# ssl-key-path: "path/to/server.key"
|
||||
# 遗嘱消息配置 (用于网关异常下线时通知其他系统)
|
||||
will:
|
||||
enabled: true # 生产环境强烈建议开启
|
||||
topic: "gateway/status/iot-gateway-mqtt" # 遗嘱消息主题
|
||||
payload: "offline" # 遗嘱消息负载
|
||||
qos: 1 # 遗嘱消息 QoS
|
||||
retain: true # 遗嘱消息是否保留
|
||||
# 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效)
|
||||
ssl-options:
|
||||
key-store-path: "classpath:certs/client.jks" # 客户端证书库路径
|
||||
key-store-password: "your-keystore-password" # 客户端证书库密码
|
||||
trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
|
||||
trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
|
||||
http-port: 8090 # MQTT HTTP 服务端口
|
||||
mqtt-host: 127.0.0.1 # MQTT Broker 地址
|
||||
mqtt-port: 1883 # MQTT Broker 端口
|
||||
@@ -153,25 +200,8 @@ yudao:
|
||||
# ====================================
|
||||
# 针对引入的 MQTT 组件的配置
|
||||
# ====================================
|
||||
mqtt:
|
||||
enabled: false
|
||||
port: 1883
|
||||
max-message-size: 8192
|
||||
connect-timeout-seconds: 60
|
||||
ssl-enabled: false
|
||||
# ====================================
|
||||
# 针对引入的 MQTT WebSocket 组件的配置
|
||||
# ====================================
|
||||
mqtt-ws:
|
||||
enabled: true # 是否启用 MQTT WebSocket
|
||||
port: 8083 # WebSocket 服务端口
|
||||
path: /mqtt # WebSocket 路径
|
||||
max-message-size: 8192 # 最大消息大小(字节)
|
||||
max-frame-size: 65536 # 最大帧大小(字节)
|
||||
connect-timeout-seconds: 60 # 连接超时时间(秒)
|
||||
keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒)
|
||||
ssl-enabled: false # 是否启用 SSL(wss://)
|
||||
sub-protocol: mqtt # WebSocket 子协议
|
||||
# 协议配置(旧版,保持兼容)
|
||||
protocol:
|
||||
modbus-tcp:
|
||||
enabled: true
|
||||
config-refresh-interval: 30 # 配置刷新间隔(秒)
|
||||
@@ -194,7 +224,6 @@ logging:
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
|
||||
# 根日志级别
|
||||
|
||||
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
@@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
|
||||
// 1.2 构建请求参数
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
|
||||
reqDTO.setProductKey(PRODUCT_KEY);
|
||||
reqDTO.setDeviceName("test-" + System.currentTimeMillis());
|
||||
reqDTO.setProductSecret("test-product-secret");
|
||||
String deviceName = "test-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
String payload = JsonUtils.toJsonString(reqDTO);
|
||||
// 1.3 输出请求
|
||||
log.info("[testDeviceRegister][请求 URI: {}]", uri);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* IoT 网关 EMQX 协议集成测试包
|
||||
*
|
||||
* <p>
|
||||
* 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。
|
||||
*
|
||||
* @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt
|
||||
*
|
||||
* <h2>架构</h2>
|
||||
* <pre>
|
||||
* +--------+ MQTT +-------------+ HTTP Hook +---------+
|
||||
* | 设备 | --------------> | EMQX Broker | ----------------> | 网关 |
|
||||
* +--------+ +-------------+ +---------+
|
||||
* </pre>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
|
||||
// 1.2 构建请求参数
|
||||
String deviceName = "test-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName("test-" + System.currentTimeMillis())
|
||||
.setProductSecret("test-product-secret");
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
String payload = JsonUtils.toJsonString(reqDTO);
|
||||
// 1.3 输出请求
|
||||
log.info("[testDeviceRegister][请求 URL: {}]", url);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
@@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
@@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 构建认证信息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
|
||||
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
MqttClient client = connect(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
// 断开连接
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
if (disconnectAr.succeeded()) {
|
||||
log.info("[testAuth][断开连接成功]");
|
||||
} else {
|
||||
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
} else {
|
||||
log.error("[testAuth][连接失败]", ar.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待测试完成
|
||||
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testAuth][测试超时]");
|
||||
MqttClient client = createClient(authInfo);
|
||||
try {
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testPropertyPost][连接认证成功]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 2.2 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
@@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testEventPost][连接认证成功]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 设备动态注册测试(一型一密) =====================
|
||||
@@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
/**
|
||||
* 直连设备动态注册测试(一型一密)
|
||||
* <p>
|
||||
* 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
|
||||
* 认证方式:
|
||||
* - clientId: 任意值 + "|authType=register|" 后缀
|
||||
* - username: {deviceName}&{productKey}(与普通认证相同)
|
||||
* - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256)
|
||||
* <p>
|
||||
* 注意:此接口不需要认证
|
||||
* 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1. 连接并认证(使用已有设备连接)
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testDeviceRegister][连接认证成功]");
|
||||
// 1.1 构建注册参数
|
||||
String deviceName = "test-mqtt-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
// 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀)
|
||||
String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|";
|
||||
String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName);
|
||||
log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]",
|
||||
clientId, username, sign);
|
||||
// 1.3 创建客户端并连接(连接时服务端自动处理注册)
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(clientId)
|
||||
.setUsername(username)
|
||||
.setPassword(sign)
|
||||
.setCleanSession(true)
|
||||
.setKeepAliveInterval(60);
|
||||
MqttClient client = MqttClient.create(vertx, options);
|
||||
|
||||
// 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);
|
||||
try {
|
||||
// 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testDeviceRegister][连接成功,等待注册响应...]");
|
||||
|
||||
// 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 进行一机一密认证]");
|
||||
// 3.1 设置消息处理器,接收注册响应
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[testDeviceRegister][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
// 3.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 4. 等待注册响应
|
||||
IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testDeviceRegister][注册响应: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 订阅下行消息测试 =====================
|
||||
@@ -237,44 +234,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testSubscribe() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testSubscribe][连接认证成功]");
|
||||
|
||||
// 2. 设置消息处理器
|
||||
client.publishHandler(message -> {
|
||||
log.info("[testSubscribe][收到消息: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
});
|
||||
try {
|
||||
// 2. 设置消息处理器
|
||||
client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString()));
|
||||
|
||||
// 3. 订阅下行主题
|
||||
String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
|
||||
log.info("[testSubscribe][订阅主题: {}]", topic);
|
||||
// 3. 订阅下行主题
|
||||
String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
|
||||
log.info("[testSubscribe][订阅主题: {}]", topic);
|
||||
subscribe(client, topic);
|
||||
log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
|
||||
|
||||
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][测试超时]");
|
||||
// 4. 保持连接 30 秒等待消息
|
||||
Thread.sleep(30000);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +264,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
@@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @return 已认证的 MQTT 客户端
|
||||
*/
|
||||
private MqttClient connectAndAuth() throws Exception {
|
||||
// 1. 创建客户端并连接
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
MqttClient client = connect(authInfo);
|
||||
|
||||
// 2.1 连接
|
||||
CompletableFuture<MqttClient> future = new CompletableFuture<>();
|
||||
MqttClient client = createClient(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
future.complete(client);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2.2 等待连接结果
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
* 订阅主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 主题
|
||||
*/
|
||||
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
|
||||
// 1. 订阅响应主题
|
||||
CompletableFuture<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);
|
||||
private void subscribe(MqttClient client, String topic) throws Exception {
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[subscribe][订阅主题成功: {}]", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,34 +307,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
|
||||
throws Exception {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
|
||||
future.complete(response);
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
// 2. 序列化并发布消息
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
|
||||
SERIALIZER.getType(), topic, new String(payload));
|
||||
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
|
||||
} else {
|
||||
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[publishAndWaitReply][消息发布成功]");
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
// 3. 等待响应
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
@@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @param client MQTT 客户端
|
||||
*/
|
||||
private void disconnect(MqttClient client) throws Exception {
|
||||
// 1. 断开连接
|
||||
CompletableFuture<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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
@@ -13,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
@@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
@@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 构建认证信息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
@@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
MqttClient client = connect(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
// 断开连接
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
if (disconnectAr.succeeded()) {
|
||||
log.info("[testAuth][断开连接成功]");
|
||||
} else {
|
||||
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
} else {
|
||||
log.error("[testAuth][连接失败]", ar.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待测试完成
|
||||
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testAuth][测试超时]");
|
||||
MqttClient client = createClient(authInfo);
|
||||
try {
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testTopoAdd][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建子设备认证信息
|
||||
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
|
||||
.setClientId(subAuthInfo.getClientId())
|
||||
.setUsername(subAuthInfo.getUsername())
|
||||
.setPassword(subAuthInfo.getPassword());
|
||||
|
||||
// 2.2 构建子设备认证信息
|
||||
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
|
||||
.setClientId(subAuthInfo.getClientId())
|
||||
.setUsername(subAuthInfo.getUsername())
|
||||
.setPassword(subAuthInfo.getPassword());
|
||||
// 2.2 构建请求消息
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO()
|
||||
.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
params);
|
||||
|
||||
// 2.3 构建请求消息
|
||||
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 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/add",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/add",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testTopoDelete][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建请求消息
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO()
|
||||
.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
|
||||
params);
|
||||
|
||||
// 2.2 构建请求消息
|
||||
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 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/delete",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/delete",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testTopoGet][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建请求消息
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params);
|
||||
|
||||
// 2.2 构建请求消息
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/get",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/get",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
@@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testSubDeviceRegister][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建请求消息
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO()
|
||||
.setProductKey(SUB_DEVICE_PRODUCT_KEY)
|
||||
.setDeviceName("mougezishebei-mqtt");
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
|
||||
Collections.singletonList(subDevice));
|
||||
|
||||
// 2.2 构建请求消息
|
||||
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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
@@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testPropertyPackPost][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
|
||||
// 2.2 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
// 2.2 构建【网关设备】自身事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("message", "gateway started").build())
|
||||
.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
|
||||
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.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.3 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
|
||||
// 2.4 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 2.4 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("errorCode", 0).build())
|
||||
.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
|
||||
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("healthCheck", subDeviceEvent)
|
||||
.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.5 构建子设备数据
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData()
|
||||
.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))
|
||||
.setProperties(subDeviceProperties)
|
||||
.setEvents(subDeviceEvents);
|
||||
|
||||
// 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.6 构建请求消息
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO()
|
||||
.setProperties(gatewayProperties)
|
||||
.setEvents(gatewayEvents)
|
||||
.setSubDevices(ListUtil.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params);
|
||||
|
||||
// 2.7 构建请求消息
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(ListUtil.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.7 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
@@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
@@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @return 已认证的 MQTT 客户端
|
||||
*/
|
||||
private MqttClient connectAndAuth() throws Exception {
|
||||
// 1. 创建客户端并连接
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
MqttClient client = connect(authInfo);
|
||||
|
||||
// 2.1 连接
|
||||
CompletableFuture<MqttClient> future = new CompletableFuture<>();
|
||||
MqttClient client = createClient(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
future.complete(client);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2.2 等待连接结果
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
* 订阅主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 主题
|
||||
*/
|
||||
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
|
||||
// 1. 订阅响应主题
|
||||
CompletableFuture<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);
|
||||
private void subscribe(MqttClient client, String topic) throws Exception {
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[subscribe][订阅主题成功: {}]", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
|
||||
throws Exception {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
|
||||
future.complete(response);
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
// 2. 序列化并发布消息
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
|
||||
SERIALIZER.getType(), topic, new String(payload));
|
||||
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
|
||||
} else {
|
||||
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[publishAndWaitReply][消息发布成功]");
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
// 3. 等待响应
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
@@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @param client MQTT 客户端
|
||||
*/
|
||||
private void disconnect(MqttClient client) throws Exception {
|
||||
// 1. 断开连接
|
||||
CompletableFuture<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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
@@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
@@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 构建认证信息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
|
||||
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
MqttClient client = connect(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
// 断开连接
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
if (disconnectAr.succeeded()) {
|
||||
log.info("[testAuth][断开连接成功]");
|
||||
} else {
|
||||
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
} else {
|
||||
log.error("[testAuth][连接失败]", ar.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待测试完成
|
||||
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testAuth][测试超时]");
|
||||
MqttClient client = createClient(authInfo);
|
||||
try {
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
log.info("[testPropertyPost][连接认证成功]");
|
||||
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
@@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
log.info("[testEventPost][连接认证成功]");
|
||||
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
try {
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
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()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
@@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
@@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @return 已认证的 MQTT 客户端
|
||||
*/
|
||||
private MqttClient connectAndAuth() throws Exception {
|
||||
// 1. 创建客户端并连接
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
MqttClient client = connect(authInfo);
|
||||
|
||||
// 2.1 连接
|
||||
CompletableFuture<MqttClient> future = new CompletableFuture<>();
|
||||
MqttClient client = createClient(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
future.complete(client);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2.2 等待连接结果
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
* 订阅主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 主题
|
||||
*/
|
||||
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
|
||||
// 1. 订阅响应主题
|
||||
CompletableFuture<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);
|
||||
private void subscribe(MqttClient client, String topic) throws Exception {
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[subscribe][订阅主题成功: {}]", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
|
||||
throws Exception {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
|
||||
future.complete(response);
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
// 2. 序列化并发布消息
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
|
||||
SERIALIZER.getType(), topic, new String(payload));
|
||||
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
|
||||
} else {
|
||||
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[publishAndWaitReply][消息发布成功]");
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
// 3. 等待响应
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
@@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @param client MQTT 客户端
|
||||
*/
|
||||
private void disconnect(MqttClient client) throws Exception {
|
||||
// 1. 断开连接
|
||||
CompletableFuture<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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
|
||||
@@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1. 构建注册消息
|
||||
String deviceName = "test-tcp-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName("test-tcp-" + System.currentTimeMillis())
|
||||
.setProductSecret("test-product-secret");
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1. 构建注册消息
|
||||
String deviceName = "test-udp-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName("test-udp-" + System.currentTimeMillis())
|
||||
.setProductSecret("test-product-secret");
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
@@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1.1 构建注册消息
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
|
||||
registerReqDTO.setProductKey(PRODUCT_KEY);
|
||||
registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis());
|
||||
registerReqDTO.setProductSecret("test-product-secret");
|
||||
String deviceName = "test-ws-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
|
||||
// 1.2 序列化
|
||||
|
||||
Reference in New Issue
Block a user