第一次提交
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
yudao-ui-admin CI / build (14.x) (push) Has been cancelled
yudao-ui-admin CI / build (16.x) (push) Has been cancelled
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
yudao-ui-admin CI / build (14.x) (push) Has been cancelled
yudao-ui-admin CI / build (16.x) (push) Has been cancelled
This commit is contained in:
78
yudao-module-iot/yudao-module-iot-gateway/pom.xml
Normal file
78
yudao-module-iot/yudao-module-iot-gateway/pom.xml
Normal file
@@ -0,0 +1,78 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>yudao-module-iot</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<artifactId>yudao-module-iot-gateway</artifactId>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
iot 模块下,设备网关:
|
||||
① 功能一:接收来自设备的消息,并进行解码(decode)后,发送到消息网关,提供给 iot-biz 进行处理
|
||||
② 功能二:接收来自消息网关的消息(由 iot-biz 发送),并进行编码(encode)后,发送给设备
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-iot-core</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq -->
|
||||
<!-- <optional>true</optional> -->
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT 相关 -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<!-- 设置构建的 jar 包名 -->
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<!-- 打包 -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class IotGatewayServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(IotGatewayServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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 {
|
||||
|
||||
private 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 提供设备接入的各种数据(请求、响应)的编解码
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* TODO @芋艿:实现一个 alink 的 xml 版本
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.codec.simple;
|
||||
@@ -0,0 +1,286 @@
|
||||
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 二进制格式 {@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 = "TCP_BINARY";
|
||||
|
||||
/**
|
||||
* 协议魔术字,用于协议识别
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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 JSON 格式 {@link IotDeviceMessage} 编解码器
|
||||
*
|
||||
* 采用纯 JSON 格式传输,格式如下:
|
||||
* {
|
||||
* "id": "消息 ID",
|
||||
* "method": "消息方法",
|
||||
* "params": {...}, // 请求参数
|
||||
* "data": {...}, // 响应结果
|
||||
* "code": 200, // 响应错误码
|
||||
* "msg": "success", // 响应提示
|
||||
* "timestamp": 时间戳
|
||||
* }
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
public static final String TYPE = "TCP_JSON";
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class TcpJsonMessage {
|
||||
|
||||
/**
|
||||
* 消息 ID,且每个消息 ID 在当前设备具有唯一性
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
private String method;
|
||||
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
private Object params;
|
||||
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 响应错误码
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应提示
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode(IotDeviceMessage message) {
|
||||
TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
|
||||
message.getRequestId(),
|
||||
message.getMethod(),
|
||||
message.getParams(),
|
||||
message.getData(),
|
||||
message.getCode(),
|
||||
message.getMsg(),
|
||||
System.currentTimeMillis());
|
||||
return JsonUtils.toJsonByte(tcpJsonMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public IotDeviceMessage decode(byte[] bytes) {
|
||||
String jsonStr = StrUtil.utf8Str(bytes).trim();
|
||||
TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
|
||||
Assert.notNull(tcpJsonMessage, "消息不能为空");
|
||||
Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
|
||||
return IotDeviceMessage.of(
|
||||
tcpJsonMessage.getId(),
|
||||
tcpJsonMessage.getMethod(),
|
||||
tcpJsonMessage.getParams(),
|
||||
tcpJsonMessage.getData(),
|
||||
tcpJsonMessage.getCode(),
|
||||
tcpJsonMessage.getMsg());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.config;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.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 {
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class HttpProtocolConfiguration {
|
||||
|
||||
@Bean
|
||||
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) {
|
||||
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class EmqxProtocolConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public Vertx emqxVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
|
||||
Vertx emqxVertx) {
|
||||
return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
Vertx emqxVertx) {
|
||||
return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 TCP 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class TcpProtocolConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public Vertx tcpVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
IotDeviceService deviceService,
|
||||
IotDeviceMessageService messageService,
|
||||
IotTcpConnectionManager connectionManager,
|
||||
Vertx tcpVertx) {
|
||||
return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
|
||||
deviceService, messageService, connectionManager, tcpVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
|
||||
IotDeviceMessageService messageService,
|
||||
IotDeviceService deviceService,
|
||||
IotTcpConnectionManager connectionManager,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,
|
||||
messageBus);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class MqttProtocolConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public Vertx mqttVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttConnectionManager connectionManager,
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
@ConfigurationProperties(prefix = "yudao.iot.gateway")
|
||||
@Validated
|
||||
@Data
|
||||
public class IotGatewayProperties {
|
||||
|
||||
/**
|
||||
* 设备 RPC 服务配置
|
||||
*/
|
||||
private RpcProperties rpc;
|
||||
/**
|
||||
* Token 配置
|
||||
*/
|
||||
private TokenProperties token;
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private ProtocolProperties protocol;
|
||||
|
||||
@Data
|
||||
public static class RpcProperties {
|
||||
|
||||
/**
|
||||
* 主程序 API 地址
|
||||
*/
|
||||
@NotEmpty(message = "主程序 API 地址不能为空")
|
||||
private String url;
|
||||
/**
|
||||
* 连接超时时间
|
||||
*/
|
||||
@NotNull(message = "连接超时时间不能为空")
|
||||
private Duration connectTimeout;
|
||||
/**
|
||||
* 读取超时时间
|
||||
*/
|
||||
@NotNull(message = "读取超时时间不能为空")
|
||||
private Duration readTimeout;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TokenProperties {
|
||||
|
||||
/**
|
||||
* 密钥
|
||||
*/
|
||||
@NotEmpty(message = "密钥不能为空")
|
||||
private String secret;
|
||||
/**
|
||||
* 令牌有效期
|
||||
*/
|
||||
@NotNull(message = "令牌有效期不能为空")
|
||||
private Duration expiration;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProtocolProperties {
|
||||
|
||||
/**
|
||||
* HTTP 组件配置
|
||||
*/
|
||||
private HttpProperties http;
|
||||
|
||||
/**
|
||||
* EMQX 组件配置
|
||||
*/
|
||||
private EmqxProperties emqx;
|
||||
|
||||
/**
|
||||
* TCP 组件配置
|
||||
*/
|
||||
private TcpProperties tcp;
|
||||
|
||||
/**
|
||||
* MQTT 组件配置
|
||||
*/
|
||||
private MqttProperties mqtt;
|
||||
|
||||
}
|
||||
|
||||
@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 TcpProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 服务器端口
|
||||
*/
|
||||
private Integer port = 8091;
|
||||
|
||||
/**
|
||||
* 心跳超时时间(毫秒)
|
||||
*/
|
||||
private Long keepAliveTimeoutMs = 30000L;
|
||||
|
||||
/**
|
||||
* 最大连接数
|
||||
*/
|
||||
private Integer maxConnections = 1000;
|
||||
|
||||
/**
|
||||
* 是否启用SSL
|
||||
*/
|
||||
private Boolean sslEnabled = false;
|
||||
|
||||
/**
|
||||
* SSL证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* SSL私钥路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
private Boolean sslEnabled = false;
|
||||
/**
|
||||
* SSL 配置
|
||||
*/
|
||||
private SslOptions sslOptions = new SslOptions();
|
||||
|
||||
/**
|
||||
* SSL 配置选项
|
||||
*/
|
||||
@Data
|
||||
public static class SslOptions {
|
||||
|
||||
/**
|
||||
* 密钥证书选项
|
||||
*/
|
||||
private io.vertx.core.net.KeyCertOptions keyCertOptions;
|
||||
/**
|
||||
* 信任选项
|
||||
*/
|
||||
private io.vertx.core.net.TrustOptions trustOptions;
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String certPath;
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String keyPath;
|
||||
/**
|
||||
* 信任存储路径
|
||||
*/
|
||||
private String trustStorePath;
|
||||
/**
|
||||
* 信任存储密码
|
||||
*/
|
||||
private String trustStorePassword;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.enums;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* iot gateway 错误码枚举类
|
||||
* <p>
|
||||
* iot 系统,使用 1-051-000-000 段
|
||||
*/
|
||||
public interface ErrorCodeConstants {
|
||||
|
||||
// ========== 设备认证 1-050-001-000 ============
|
||||
ErrorCode DEVICE_AUTH_FAIL = new ErrorCode(1_051_001_000, "设备鉴权失败"); // 对应阿里云 20000
|
||||
ErrorCode DEVICE_TOKEN_EXPIRED = new ErrorCode(1_051_001_002, "token 失效。需重新调用 auth 进行鉴权,获取token"); // 对应阿里云 20001
|
||||
|
||||
// ========== 设备信息 1-050-002-000 ============
|
||||
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_051_002_001, "设备({}/{}) 不存在");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* 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,69 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 订阅者:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotEmqxDownstreamHandler downstreamHandler;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
private final IotEmqxUpstreamProtocol protocol;
|
||||
|
||||
public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
|
||||
this.protocol = protocol;
|
||||
this.messageBus = messageBus;
|
||||
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
messageBus.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId());
|
||||
try {
|
||||
// 1. 校验
|
||||
String method = message.getMethod();
|
||||
if (method == null) {
|
||||
log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
|
||||
message.getId(), message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 处理下行消息
|
||||
downstreamHandler.handle(message);
|
||||
} catch (Exception e) {
|
||||
log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.EmqxProperties emqxProperties;
|
||||
|
||||
private volatile boolean isRunning = false;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private MqttClient mqttClient;
|
||||
|
||||
private IotEmqxUpstreamHandler upstreamHandler;
|
||||
|
||||
public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
|
||||
Vertx vertx) {
|
||||
this.emqxProperties = emqxProperties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
|
||||
this.vertx = vertx;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
if (isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 启动 MQTT 客户端
|
||||
startMqttClient();
|
||||
|
||||
// 2. 标记服务为运行状态
|
||||
isRunning = true;
|
||||
log.info("[start][IoT 网关 EMQX 协议启动成功]");
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
|
||||
stop();
|
||||
|
||||
// 异步关闭应用
|
||||
Thread shutdownThread = new Thread(() -> {
|
||||
try {
|
||||
// 确保日志输出完成,使用更优雅的方式
|
||||
log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
|
||||
// 等待日志输出完成
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("[start][应用关闭被中断]");
|
||||
}
|
||||
System.exit(1);
|
||||
});
|
||||
shutdownThread.setDaemon(true);
|
||||
shutdownThread.setName("emergency-shutdown");
|
||||
shutdownThread.start();
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 停止 MQTT 客户端
|
||||
stopMqttClient();
|
||||
|
||||
// 2. 标记服务为停止状态
|
||||
isRunning = false;
|
||||
log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 MQTT 客户端
|
||||
*/
|
||||
private void startMqttClient() {
|
||||
try {
|
||||
// 1. 初始化消息处理器
|
||||
this.upstreamHandler = new IotEmqxUpstreamHandler(this);
|
||||
|
||||
// 2. 创建 MQTT 客户端
|
||||
createMqttClient();
|
||||
|
||||
// 3. 同步连接 MQTT Broker
|
||||
connectMqttSync();
|
||||
} catch (Exception e) {
|
||||
log.error("[startMqttClient][MQTT 客户端启动失败]", e);
|
||||
throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步连接 MQTT Broker
|
||||
*/
|
||||
private void connectMqttSync() {
|
||||
String host = emqxProperties.getMqttHost();
|
||||
int port = emqxProperties.getMqttPort();
|
||||
// 1. 连接 MQTT Broker
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicBoolean success = new AtomicBoolean(false);
|
||||
mqttClient.connect(port, host, connectResult -> {
|
||||
if (connectResult.succeeded()) {
|
||||
log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
|
||||
setupMqttHandlers();
|
||||
subscribeToTopics();
|
||||
success.set(true);
|
||||
} else {
|
||||
log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
|
||||
host, port, connectResult.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// 2. 等待连接结果
|
||||
try {
|
||||
// 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制
|
||||
boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (!awaitResult) {
|
||||
log.error("[connectMqttSync][等待连接结果超时]");
|
||||
throw new RuntimeException("连接 MQTT Broker 超时");
|
||||
}
|
||||
if (!success.get()) {
|
||||
throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("[connectMqttSync][等待连接结果被中断]", e);
|
||||
throw new RuntimeException("连接 MQTT Broker 被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步连接 MQTT Broker
|
||||
*/
|
||||
private void connectMqttAsync() {
|
||||
String host = emqxProperties.getMqttHost();
|
||||
int port = emqxProperties.getMqttPort();
|
||||
mqttClient.connect(port, host, connectResult -> {
|
||||
if (connectResult.succeeded()) {
|
||||
log.info("[connectMqttAsync][MQTT 客户端重连成功]");
|
||||
setupMqttHandlers();
|
||||
subscribeToTopics();
|
||||
} else {
|
||||
log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]",
|
||||
host, port, connectResult.cause());
|
||||
log.warn("[connectMqttAsync][重连失败,将再次尝试]");
|
||||
reconnectWithDelay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟重连
|
||||
*/
|
||||
private void reconnectWithDelay() {
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long delay = emqxProperties.getReconnectDelayMs();
|
||||
log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay);
|
||||
vertx.setTimer(delay, timerId -> {
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[reconnectWithDelay][开始重连 MQTT Broker]");
|
||||
try {
|
||||
createMqttClient();
|
||||
connectMqttAsync();
|
||||
} catch (Exception e) {
|
||||
log.error("[reconnectWithDelay][重连过程中发生异常]", e);
|
||||
vertx.setTimer(delay, t -> reconnectWithDelay());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 MQTT 客户端
|
||||
*/
|
||||
private void stopMqttClient() {
|
||||
if (mqttClient == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (mqttClient.isConnected()) {
|
||||
// 1. 取消订阅所有主题
|
||||
List<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 (!isRunning) {
|
||||
return;
|
||||
}
|
||||
log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
|
||||
reconnectWithDelay();
|
||||
});
|
||||
|
||||
// 2. 设置异常处理器
|
||||
mqttClient.exceptionHandler(exception ->
|
||||
log.error("[exceptionHandler][MQTT 客户端异常]", exception));
|
||||
|
||||
// 3. 设置消息处理器
|
||||
mqttClient.publishHandler(upstreamHandler::handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅设备上行消息主题
|
||||
*/
|
||||
private void subscribeToTopics() {
|
||||
// 1. 校验 MQTT 客户端是否连接
|
||||
List<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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
|
||||
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 认证事件处理器
|
||||
* <p>
|
||||
* 为 EMQX 提供 HTTP 接口服务,包括:
|
||||
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
|
||||
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxAuthEventHandler {
|
||||
|
||||
/**
|
||||
* HTTP 成功状态码(EMQX 要求固定使用 200)
|
||||
*/
|
||||
private static final int SUCCESS_STATUS_CODE = 200;
|
||||
|
||||
/**
|
||||
* 认证允许结果
|
||||
*/
|
||||
private static final String RESULT_ALLOW = "allow";
|
||||
/**
|
||||
* 认证拒绝结果
|
||||
*/
|
||||
private static final String RESULT_DENY = "deny";
|
||||
/**
|
||||
* 认证忽略结果
|
||||
*/
|
||||
private static final String RESULT_IGNORE = "ignore";
|
||||
|
||||
/**
|
||||
* EMQX 事件类型常量
|
||||
*/
|
||||
private static final String EVENT_CLIENT_CONNECTED = "client.connected";
|
||||
private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
|
||||
|
||||
private final String serverId;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
public IotEmqxAuthEventHandler(String serverId) {
|
||||
this.serverId = serverId;
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* EMQX 认证接口
|
||||
*/
|
||||
public void handleAuth(RoutingContext context) {
|
||||
try {
|
||||
// 1. 参数校验
|
||||
JsonObject body = parseRequestBody(context);
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
String clientId = body.getString("clientid");
|
||||
String username = body.getString("username");
|
||||
String password = body.getString("password");
|
||||
log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username);
|
||||
if (StrUtil.hasEmpty(clientId, username, password)) {
|
||||
log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username);
|
||||
sendAuthResponse(context, RESULT_DENY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 执行认证
|
||||
boolean authResult = handleDeviceAuth(clientId, username, password);
|
||||
log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
|
||||
if (authResult) {
|
||||
sendAuthResponse(context, RESULT_ALLOW);
|
||||
} else {
|
||||
sendAuthResponse(context, RESULT_DENY);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[handleAuth][设备认证异常]", e);
|
||||
sendAuthResponse(context, RESULT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
|
||||
* 支持的事件类型:client.connected、client.disconnected 等
|
||||
*/
|
||||
public void handleEvent(RoutingContext context) {
|
||||
JsonObject body = null;
|
||||
try {
|
||||
// 1. 解析请求体
|
||||
body = parseRequestBody(context);
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
String event = body.getString("event");
|
||||
String username = body.getString("username");
|
||||
log.debug("[handleEvent][收到事件: {} - {}]", event, username);
|
||||
|
||||
// 2. 根据事件类型进行分发处理
|
||||
switch (event) {
|
||||
case EVENT_CLIENT_CONNECTED:
|
||||
handleClientConnected(body);
|
||||
break;
|
||||
case EVENT_CLIENT_DISCONNECTED:
|
||||
handleClientDisconnected(body);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// 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重试
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析请求体
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备状态变化
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param online 是否在线 true 在线 false 离线
|
||||
*/
|
||||
private void handleDeviceStateChange(String username, boolean online) {
|
||||
// 1. 解析设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 构建设备状态消息
|
||||
IotDeviceMessage message = online ? IotDeviceMessage.buildStateUpdateOnline()
|
||||
: IotDeviceMessage.buildStateOffline();
|
||||
|
||||
// 3. 发送设备状态消息
|
||||
deviceMessageService.sendDeviceMessage(message,
|
||||
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
|
||||
|
||||
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.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 下行消息处理器
|
||||
* <p>
|
||||
* 从消息总线接收到下行消息,然后发布到 MQTT Broker,从而被设备所接收
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxDownstreamHandler {
|
||||
|
||||
private final IotEmqxUpstreamProtocol protocol;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
|
||||
this.protocol = protocol;
|
||||
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*
|
||||
* @param message 设备消息
|
||||
*/
|
||||
public void handle(IotDeviceMessage message) {
|
||||
// 1. 获取设备信息
|
||||
IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
if (deviceInfo == null) {
|
||||
log.error("[handle][设备信息({})不存在]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 根据方法构建主题
|
||||
String topic = buildTopicByMethod(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
if (StrUtil.isBlank(topic)) {
|
||||
log.warn("[handle][未知的消息方法: {}]", message.getMethod());
|
||||
return;
|
||||
}
|
||||
// 2.2 构建载荷
|
||||
byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
// 2.3 发布消息
|
||||
protocol.publishMessage(topic, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息方法和回复状态构建主题
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @param productKey 产品标识
|
||||
* @param deviceName 设备名称
|
||||
* @return 构建的主题,如果方法不支持返回 null
|
||||
*/
|
||||
private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) {
|
||||
// 1. 判断是否为回复消息
|
||||
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
|
||||
// 2. 根据消息方法类型构建对应的主题
|
||||
return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* IoT 网关 EMQX 上行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxUpstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
this.serverId = protocol.getServerId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MQTT 发布消息
|
||||
*/
|
||||
public void handle(MqttPublishMessage mqttMessage) {
|
||||
log.info("[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])) {
|
||||
log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
// 3. 解码消息
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 发送消息到队列
|
||||
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 订阅者:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class IotHttpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotHttpUpstreamProtocol protocol;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
messageBus.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotHttpUpstreamProtocol extends AbstractVerticle {
|
||||
|
||||
private final IotGatewayProperties.HttpProperties httpProperties;
|
||||
|
||||
private HttpServer httpServer;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties) {
|
||||
this.httpProperties = httpProperties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 创建路由
|
||||
Vertx vertx = Vertx.vertx();
|
||||
Router router = Router.router(vertx);
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
// 创建处理器,添加路由处理器
|
||||
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
|
||||
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
|
||||
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
|
||||
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
|
||||
|
||||
// 启动 HTTP 服务器
|
||||
HttpServerOptions options = new HttpServerOptions()
|
||||
.setPort(httpProperties.getServerPort());
|
||||
if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath())
|
||||
.setCertPath(httpProperties.getSslCertPath());
|
||||
options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
try {
|
||||
httpServer = vertx.createHttpServer(options)
|
||||
.requestHandler(router)
|
||||
.listen()
|
||||
.result();
|
||||
log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 HTTP 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (httpServer != null) {
|
||||
try {
|
||||
httpServer.close().result();
|
||||
log.info("[stop][IoT 网关 HTTP 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 HTTP 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.http.HttpHeaders;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议的处理器抽象基类:提供通用的前置处理(认证)、全局的异常捕获等
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> {
|
||||
|
||||
private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
|
||||
|
||||
@Override
|
||||
public final void handle(RoutingContext context) {
|
||||
try {
|
||||
// 1. 前置处理
|
||||
beforeHandle(context);
|
||||
|
||||
// 2. 执行逻辑
|
||||
CommonResult<Object> result = handle0(context);
|
||||
writeResponse(context, result);
|
||||
} catch (ServiceException e) {
|
||||
writeResponse(context, CommonResult.error(e.getCode(), e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][path({}) 处理异常]", context.request().path(), e);
|
||||
writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract CommonResult<Object> handle0(RoutingContext context);
|
||||
|
||||
private void beforeHandle(RoutingContext context) {
|
||||
// 如果不需要认证,则不走前置处理
|
||||
String path = context.request().path();
|
||||
if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
String token = context.request().getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isEmpty(token)) {
|
||||
throw invalidParamException("token 不能为空");
|
||||
}
|
||||
String productKey = context.pathParam("productKey");
|
||||
if (StrUtil.isEmpty(productKey)) {
|
||||
throw invalidParamException("productKey 不能为空");
|
||||
}
|
||||
String deviceName = context.pathParam("deviceName");
|
||||
if (StrUtil.isEmpty(deviceName)) {
|
||||
throw invalidParamException("deviceName 不能为空");
|
||||
}
|
||||
|
||||
// 校验 token
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token);
|
||||
Assert.notNull(deviceInfo, "设备信息不能为空");
|
||||
// 校验设备信息是否匹配
|
||||
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|
||||
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
|
||||
throw exception(FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void writeResponse(RoutingContext context, Object data) {
|
||||
context.response()
|
||||
.setStatusCode(200)
|
||||
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
.end(JsonUtils.toJsonString(data));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议的【认证】处理器
|
||||
*
|
||||
* 参考 <a href="阿里云 IoT —— HTTPS 连接通信">https://help.aliyun.com/zh/iot/user-guide/establish-connections-over-https</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class IotHttpAuthHandler extends IotHttpAbstractHandler {
|
||||
|
||||
public static final String PATH = "/auth";
|
||||
|
||||
private final IotHttpUpstreamProtocol protocol;
|
||||
|
||||
private final IotDeviceTokenService deviceTokenService;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) {
|
||||
this.protocol = protocol;
|
||||
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Object> handle0(RoutingContext context) {
|
||||
// 1. 解析参数
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
String clientId = body.getString("clientId");
|
||||
if (StrUtil.isEmpty(clientId)) {
|
||||
throw invalidParamException("clientId 不能为空");
|
||||
}
|
||||
String username = body.getString("username");
|
||||
if (StrUtil.isEmpty(username)) {
|
||||
throw invalidParamException("username 不能为空");
|
||||
}
|
||||
String password = body.getString("password");
|
||||
if (StrUtil.isEmpty(password)) {
|
||||
throw invalidParamException("password 不能为空");
|
||||
}
|
||||
|
||||
// 2.1 执行认证
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId).setUsername(username).setPassword(password));
|
||||
result.checkError();
|
||||
if (!BooleanUtil.isTrue(result.getData())) {
|
||||
throw exception(DEVICE_AUTH_FAIL);
|
||||
}
|
||||
// 2.2 生成 Token
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username);
|
||||
Assert.notNull(deviceInfo, "设备信息不能为空");
|
||||
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
Assert.notBlank(token, "生成 token 不能为空位");
|
||||
|
||||
// 3. 执行上线
|
||||
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(message,
|
||||
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
|
||||
|
||||
// 构建响应数据
|
||||
return success(MapUtil.of("token", token));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.text.StrPool;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议的【上行】处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
|
||||
|
||||
public static final String PATH = "/topic/sys/:productKey/:deviceName/*";
|
||||
|
||||
private final IotHttpUpstreamProtocol protocol;
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) {
|
||||
this.protocol = protocol;
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommonResult<Object> handle0(RoutingContext context) {
|
||||
// 1. 解析通用参数
|
||||
String productKey = context.pathParam("productKey");
|
||||
String deviceName = context.pathParam("deviceName");
|
||||
String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
|
||||
|
||||
// 2.1 解析消息
|
||||
byte[] bytes = context.body().buffer().getBytes();
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
|
||||
productKey, deviceName);
|
||||
Assert.equals(method, message.getMethod(), "method 不匹配");
|
||||
// 2.2 发送消息
|
||||
deviceMessageService.sendDeviceMessage(message,
|
||||
productKey, deviceName, protocol.getServerId());
|
||||
|
||||
// 3. 返回结果
|
||||
return CommonResult.success(MapUtil.of("messageId", message.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:下行消息订阅器
|
||||
* <p>
|
||||
* 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotMqttUpstreamProtocol upstreamProtocol;
|
||||
|
||||
private final IotMqttDownstreamHandler downstreamHandler;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol,
|
||||
IotMqttDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
this.upstreamProtocol = upstreamProtocol;
|
||||
this.downstreamHandler = downstreamHandler;
|
||||
this.messageBus = messageBus;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void subscribe() {
|
||||
messageBus.register(this);
|
||||
log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId());
|
||||
try {
|
||||
// 1. 校验
|
||||
String method = message.getMethod();
|
||||
if (method == null) {
|
||||
log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
|
||||
message.getId(), message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 委托给下行处理器处理业务逻辑
|
||||
boolean success = downstreamHandler.handleDownstreamMessage(message);
|
||||
if (success) {
|
||||
log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId());
|
||||
} else {
|
||||
log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
|
||||
message.getId(), message.getMethod(), message.getDeviceId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.mqtt.MqttServer;
|
||||
import io.vertx.mqtt.MqttServerOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.MqttProperties mqttProperties;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private MqttServer mqttServer;
|
||||
|
||||
public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttConnectionManager connectionManager,
|
||||
Vertx vertx) {
|
||||
this.mqttProperties = mqttProperties;
|
||||
this.messageService = messageService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort());
|
||||
}
|
||||
|
||||
// TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈;
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 创建服务器选项
|
||||
MqttServerOptions options = new MqttServerOptions()
|
||||
.setPort(mqttProperties.getPort())
|
||||
.setMaxMessageSize(mqttProperties.getMaxMessageSize())
|
||||
.setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds());
|
||||
|
||||
// 配置 SSL(如果启用)
|
||||
if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) {
|
||||
options.setSsl(true)
|
||||
.setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions())
|
||||
.setTrustOptions(mqttProperties.getSslOptions().getTrustOptions());
|
||||
}
|
||||
|
||||
// 创建服务器并设置连接处理器
|
||||
mqttServer = MqttServer.create(vertx, options);
|
||||
mqttServer.endpointHandler(endpoint -> {
|
||||
IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager);
|
||||
handler.handle(endpoint);
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
try {
|
||||
mqttServer.listen().result();
|
||||
log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 MQTT 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (mqttServer != null) {
|
||||
try {
|
||||
mqttServer.close().result();
|
||||
log.info("[stop][IoT 网关 MQTT 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 MQTT 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 连接管理器
|
||||
* <p>
|
||||
* 统一管理 MQTT 连接的认证状态、设备会话和消息发送功能:
|
||||
* 1. 管理 MQTT 连接的认证状态
|
||||
* 2. 管理设备会话和在线状态
|
||||
* 3. 管理消息发送到设备
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IotMqttConnectionManager {
|
||||
|
||||
/**
|
||||
* 未知地址常量(当获取端点地址失败时使用)
|
||||
*/
|
||||
private static final String UNKNOWN_ADDRESS = "unknown";
|
||||
|
||||
/**
|
||||
* 连接信息映射:MqttEndpoint -> 连接信息
|
||||
*/
|
||||
private final Map<MqttEndpoint, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备 ID -> MqttEndpoint 的映射
|
||||
*/
|
||||
private final Map<Long, MqttEndpoint> deviceEndpointMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 安全获取 endpoint 地址
|
||||
* <p>
|
||||
* 优先从缓存获取地址,缓存为空时再尝试实时获取
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @return 地址字符串,获取失败时返回 "unknown"
|
||||
*/
|
||||
public String getEndpointAddress(MqttEndpoint endpoint) {
|
||||
String realTimeAddress = UNKNOWN_ADDRESS;
|
||||
if (endpoint == null) {
|
||||
return realTimeAddress;
|
||||
}
|
||||
|
||||
// 1. 优先从缓存获取(避免连接关闭时的异常)
|
||||
ConnectionInfo connectionInfo = connectionMap.get(endpoint);
|
||||
if (connectionInfo != null && StrUtil.isNotBlank(connectionInfo.getRemoteAddress())) {
|
||||
return connectionInfo.getRemoteAddress();
|
||||
}
|
||||
|
||||
// 2. 缓存为空时尝试实时获取
|
||||
try {
|
||||
realTimeAddress = endpoint.remoteAddress().toString();
|
||||
} catch (Exception ignored) {
|
||||
// 连接已关闭,忽略异常
|
||||
}
|
||||
|
||||
return realTimeAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册设备连接(包含认证信息)
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param deviceId 设备 ID
|
||||
* @param connectionInfo 连接信息
|
||||
*/
|
||||
public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
|
||||
// 如果设备已有其他连接,先清理旧连接
|
||||
MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
|
||||
if (oldEndpoint != null && oldEndpoint != endpoint) {
|
||||
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
|
||||
deviceId, getEndpointAddress(oldEndpoint));
|
||||
oldEndpoint.close();
|
||||
// 清理旧连接的映射
|
||||
connectionMap.remove(oldEndpoint);
|
||||
}
|
||||
|
||||
connectionMap.put(endpoint, connectionInfo);
|
||||
deviceEndpointMap.put(deviceId, endpoint);
|
||||
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
|
||||
deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销设备连接
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
*/
|
||||
public void unregisterConnection(MqttEndpoint endpoint) {
|
||||
ConnectionInfo connectionInfo = connectionMap.remove(endpoint);
|
||||
if (connectionInfo != null) {
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceEndpointMap.remove(deviceId);
|
||||
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId,
|
||||
getEndpointAddress(endpoint));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接信息
|
||||
*/
|
||||
public ConnectionInfo getConnectionInfo(MqttEndpoint endpoint) {
|
||||
return connectionMap.get(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备 ID 获取连接信息
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @return 连接信息
|
||||
*/
|
||||
public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
|
||||
// 通过设备 ID 获取连接端点
|
||||
MqttEndpoint endpoint = getDeviceEndpoint(deviceId);
|
||||
if (endpoint == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取连接信息
|
||||
return getConnectionInfo(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否在线
|
||||
*/
|
||||
public boolean isDeviceOnline(Long deviceId) {
|
||||
return deviceEndpointMap.containsKey(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否离线
|
||||
*/
|
||||
public boolean isDeviceOffline(Long deviceId) {
|
||||
return !isDeviceOnline(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
* @param qos 服务质量
|
||||
* @param retain 是否保留消息
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendToDevice(Long deviceId, String topic, byte[] payload, int qos, boolean retain) {
|
||||
MqttEndpoint endpoint = deviceEndpointMap.get(deviceId);
|
||||
if (endpoint == null) {
|
||||
log.warn("[sendToDevice][设备离线,无法发送消息,设备 ID: {},主题: {}]", deviceId, topic);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
endpoint.publish(topic, io.vertx.core.buffer.Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[sendToDevice][发送消息失败,设备 ID: {},主题: {},错误: {}]", deviceId, topic, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备连接端点
|
||||
*/
|
||||
public MqttEndpoint getDeviceEndpoint(Long deviceId) {
|
||||
return deviceEndpointMap.get(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接信息
|
||||
*/
|
||||
@Data
|
||||
public static class ConnectionInfo {
|
||||
|
||||
/**
|
||||
* 设备 ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 产品 Key
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 客户端 ID
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
private boolean authenticated;
|
||||
|
||||
/**
|
||||
* 连接地址
|
||||
*/
|
||||
private String remoteAddress;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* MQTT 协议实现包
|
||||
* <p>
|
||||
* 提供基于 Vert.x MQTT Server 的 IoT 设备连接和消息处理功能
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
@@ -0,0 +1,132 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
|
||||
|
||||
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.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.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 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;
|
||||
|
||||
/**
|
||||
* MQTT 上行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttUpstreamHandler {
|
||||
|
||||
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(message -> {
|
||||
try {
|
||||
processMessage(clientId, message.topicName(), message.payload().getBytes());
|
||||
|
||||
// 根据 QoS 级别发送相应的确认消息
|
||||
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 无需确认
|
||||
|
||||
} 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;
|
||||
}
|
||||
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
|
||||
try {
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
|
||||
productKey, deviceName, message.getMethod());
|
||||
|
||||
// 4. 处理业务消息(认证已在连接时完成)
|
||||
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. 获取设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 提供设备接入的各种协议的实现
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol;
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* IoT 网关 TCP 下游订阅者:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotTcpUpstreamProtocol protocol;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotTcpConnectionManager connectionManager;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
private IotTcpDownstreamHandler downstreamHandler;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化下游处理器
|
||||
this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager);
|
||||
|
||||
messageBus.register(this);
|
||||
log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
|
||||
protocol.getServerId(), getTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
try {
|
||||
downstreamHandler.handle(message);
|
||||
} catch (Exception e) {
|
||||
log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.net.NetServer;
|
||||
import io.vertx.core.net.NetServerOptions;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* IoT 网关 TCP 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotTcpUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.TcpProperties tcpProperties;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotTcpConnectionManager connectionManager;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private NetServer tcpServer;
|
||||
|
||||
public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
|
||||
IotDeviceService deviceService,
|
||||
IotDeviceMessageService messageService,
|
||||
IotTcpConnectionManager connectionManager,
|
||||
Vertx vertx) {
|
||||
this.tcpProperties = tcpProperties;
|
||||
this.deviceService = deviceService;
|
||||
this.messageService = messageService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 创建服务器选项
|
||||
NetServerOptions options = new NetServerOptions()
|
||||
.setPort(tcpProperties.getPort())
|
||||
.setTcpKeepAlive(true)
|
||||
.setTcpNoDelay(true)
|
||||
.setReuseAddress(true);
|
||||
// 配置 SSL(如果启用)
|
||||
if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(tcpProperties.getSslKeyPath())
|
||||
.setCertPath(tcpProperties.getSslCertPath());
|
||||
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
|
||||
// 创建服务器并设置连接处理器
|
||||
tcpServer = vertx.createNetServer(options);
|
||||
tcpServer.connectHandler(socket -> {
|
||||
IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService,
|
||||
connectionManager);
|
||||
handler.handle(socket);
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
try {
|
||||
tcpServer.listen().result();
|
||||
log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 TCP 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (tcpServer != null) {
|
||||
try {
|
||||
tcpServer.close().result();
|
||||
log.info("[stop][IoT 网关 TCP 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 TCP 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;
|
||||
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* IoT 网关 TCP 连接管理器
|
||||
* <p>
|
||||
* 统一管理 TCP 连接的认证状态、设备会话和消息发送功能:
|
||||
* 1. 管理 TCP 连接的认证状态
|
||||
* 2. 管理设备会话和在线状态
|
||||
* 3. 管理消息发送到设备
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IotTcpConnectionManager {
|
||||
|
||||
/**
|
||||
* 连接信息映射:NetSocket -> 连接信息
|
||||
*/
|
||||
private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备 ID -> NetSocket 的映射
|
||||
*/
|
||||
private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 注册设备连接(包含认证信息)
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @param deviceId 设备 ID
|
||||
* @param connectionInfo 连接信息
|
||||
*/
|
||||
public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
|
||||
// 如果设备已有其他连接,先清理旧连接
|
||||
NetSocket oldSocket = deviceSocketMap.get(deviceId);
|
||||
if (oldSocket != null && oldSocket != socket) {
|
||||
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
|
||||
deviceId, oldSocket.remoteAddress());
|
||||
oldSocket.close();
|
||||
// 清理旧连接的映射
|
||||
connectionMap.remove(oldSocket);
|
||||
}
|
||||
|
||||
connectionMap.put(socket, connectionInfo);
|
||||
deviceSocketMap.put(deviceId, socket);
|
||||
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
|
||||
deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销设备连接
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
*/
|
||||
public void unregisterConnection(NetSocket socket) {
|
||||
ConnectionInfo connectionInfo = connectionMap.remove(socket);
|
||||
if (connectionInfo != null) {
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceSocketMap.remove(deviceId);
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
|
||||
deviceId, socket.remoteAddress());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接是否已认证
|
||||
*/
|
||||
public boolean isAuthenticated(NetSocket socket) {
|
||||
ConnectionInfo info = connectionMap.get(socket);
|
||||
return info != null && info.isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接是否未认证
|
||||
*/
|
||||
public boolean isNotAuthenticated(NetSocket socket) {
|
||||
return !isAuthenticated(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接信息
|
||||
*/
|
||||
public ConnectionInfo getConnectionInfo(NetSocket socket) {
|
||||
return connectionMap.get(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否在线
|
||||
*/
|
||||
public boolean isDeviceOnline(Long deviceId) {
|
||||
return deviceSocketMap.containsKey(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否离线
|
||||
*/
|
||||
public boolean isDeviceOffline(Long deviceId) {
|
||||
return !isDeviceOnline(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备
|
||||
*/
|
||||
public boolean sendToDevice(Long deviceId, byte[] data) {
|
||||
NetSocket socket = deviceSocketMap.get(deviceId);
|
||||
if (socket == null) {
|
||||
log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.write(io.vertx.core.buffer.Buffer.buffer(data));
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e);
|
||||
// 发送失败时清理连接
|
||||
unregisterConnection(socket);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接信息(包含认证信息)
|
||||
*/
|
||||
@Data
|
||||
public static class ConnectionInfo {
|
||||
|
||||
/**
|
||||
* 设备 ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 产品 Key
|
||||
*/
|
||||
private String productKey;
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 客户端 ID
|
||||
*/
|
||||
private String clientId;
|
||||
/**
|
||||
* 消息编解码类型(认证后确定)
|
||||
*/
|
||||
private String codecType;
|
||||
// TODO @haohao:有没可能不要 authenticated 字段,通过 deviceId 或者其他的?进一步简化,想的是哈。
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
private boolean authenticated;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 TCP 下行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class IotTcpDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotTcpConnectionManager connectionManager;
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*/
|
||||
public void handle(IotDeviceMessage message) {
|
||||
try {
|
||||
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
|
||||
// 1.1 获取设备信息
|
||||
IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
if (deviceInfo == null) {
|
||||
log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
// 1.2 检查设备是否在线
|
||||
if (connectionManager.isDeviceOffline(message.getDeviceId())) {
|
||||
log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 根据产品 Key 和设备名称编码消息并发送到设备
|
||||
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
|
||||
if (success) {
|
||||
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
|
||||
} else {
|
||||
log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* TCP 上行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
|
||||
private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
|
||||
private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
|
||||
|
||||
private static final String AUTH_METHOD = "auth";
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotTcpConnectionManager connectionManager;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol,
|
||||
IotDeviceMessageService deviceMessageService,
|
||||
IotDeviceService deviceService,
|
||||
IotTcpConnectionManager connectionManager) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.deviceService = deviceService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.serverId = protocol.getServerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(NetSocket socket) {
|
||||
String clientId = IdUtil.simpleUUID();
|
||||
log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
|
||||
|
||||
// 设置异常和关闭处理器
|
||||
socket.exceptionHandler(ex -> {
|
||||
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
|
||||
cleanupConnection(socket);
|
||||
});
|
||||
socket.closeHandler(v -> {
|
||||
log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
|
||||
cleanupConnection(socket);
|
||||
});
|
||||
|
||||
// 设置消息处理器
|
||||
socket.handler(buffer -> {
|
||||
try {
|
||||
processMessage(clientId, buffer, socket);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
|
||||
clientId, socket.remoteAddress(), e.getMessage());
|
||||
cleanupConnection(socket);
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param buffer 消息
|
||||
* @param socket 网络连接
|
||||
* @throws Exception 消息解码失败时抛出异常
|
||||
*/
|
||||
private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception {
|
||||
// 1. 基础检查
|
||||
if (buffer == null || buffer.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取消息格式类型
|
||||
String codecType = getMessageCodecType(buffer, socket);
|
||||
|
||||
// 3. 解码消息
|
||||
IotDeviceMessage message;
|
||||
try {
|
||||
message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
|
||||
if (message == null) {
|
||||
throw new Exception("解码后消息为空");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 消息格式错误时抛出异常,由上层处理连接断开
|
||||
throw new Exception("消息解码失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 4. 根据消息类型路由处理
|
||||
try {
|
||||
if (AUTH_METHOD.equals(message.getMethod())) {
|
||||
// 认证请求
|
||||
handleAuthenticationRequest(clientId, message, codecType, socket);
|
||||
} else {
|
||||
// 业务消息
|
||||
handleBusinessRequest(clientId, message, codecType, socket);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]",
|
||||
clientId, message.getMethod(), e);
|
||||
// 发送错误响应,避免客户端一直等待
|
||||
try {
|
||||
sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType);
|
||||
} catch (Exception responseEx) {
|
||||
log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证请求
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息信息
|
||||
* @param codecType 消息编解码类型
|
||||
* @param socket 网络连接
|
||||
*/
|
||||
private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType,
|
||||
NetSocket socket) {
|
||||
try {
|
||||
// 1.1 解析认证参数
|
||||
IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
|
||||
if (authParams == null) {
|
||||
log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId);
|
||||
sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType);
|
||||
return;
|
||||
}
|
||||
// 1.2 执行认证
|
||||
if (!validateDeviceAuth(authParams)) {
|
||||
log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
|
||||
clientId, authParams.getUsername());
|
||||
sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
|
||||
if (deviceInfo == null) {
|
||||
sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
|
||||
return;
|
||||
}
|
||||
// 2.2 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
if (device == null) {
|
||||
sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3.1 注册连接
|
||||
registerConnection(socket, device, clientId, codecType);
|
||||
// 3.2 发送上线消息
|
||||
sendOnlineMessage(device);
|
||||
// 3.3 发送成功响应
|
||||
sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType);
|
||||
log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
|
||||
device.getId(), device.getDeviceName());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
|
||||
sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息信息
|
||||
* @param codecType 消息编解码类型
|
||||
* @param socket 网络连接
|
||||
*/
|
||||
private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) {
|
||||
try {
|
||||
// 1. 检查认证状态
|
||||
if (connectionManager.isNotAuthenticated(socket)) {
|
||||
log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId);
|
||||
sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取认证信息并处理业务消息
|
||||
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
|
||||
// 3. 发送消息到消息总线
|
||||
deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}",
|
||||
clientId, message.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息编解码类型
|
||||
*
|
||||
* @param buffer 消息
|
||||
* @param socket 网络连接
|
||||
* @return 消息编解码类型
|
||||
*/
|
||||
private String getMessageCodecType(Buffer buffer, NetSocket socket) {
|
||||
// 1. 如果已认证,优先使用缓存的编解码类型
|
||||
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
if (connectionInfo != null && connectionInfo.isAuthenticated() &&
|
||||
StrUtil.isNotBlank(connectionInfo.getCodecType())) {
|
||||
return connectionInfo.getCodecType();
|
||||
}
|
||||
|
||||
// 2. 未认证时检测消息格式类型
|
||||
return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY
|
||||
: CODEC_TYPE_JSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册连接信息
|
||||
*
|
||||
* @param socket 网络连接
|
||||
* @param device 设备
|
||||
* @param clientId 客户端 ID
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void registerConnection(NetSocket socket, IotDeviceRespDTO device,
|
||||
String clientId, String codecType) {
|
||||
IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo()
|
||||
.setDeviceId(device.getId())
|
||||
.setProductKey(device.getProductKey())
|
||||
.setDeviceName(device.getDeviceName())
|
||||
.setClientId(clientId)
|
||||
.setCodecType(codecType)
|
||||
.setAuthenticated(true);
|
||||
// 注册连接
|
||||
connectionManager.registerConnection(socket, device.getId(), connectionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*
|
||||
* @param device 设备信息
|
||||
*/
|
||||
private void sendOnlineMessage(IotDeviceRespDTO device) {
|
||||
try {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接
|
||||
*
|
||||
* @param socket 网络连接
|
||||
*/
|
||||
private void cleanupConnection(NetSocket socket) {
|
||||
try {
|
||||
// 1. 发送离线消息(如果已认证)
|
||||
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
if (connectionInfo != null) {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
}
|
||||
|
||||
// 2. 注销连接
|
||||
connectionManager.unregisterConnection(socket);
|
||||
} catch (Exception e) {
|
||||
log.error("[cleanupConnection][清理连接失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送响应消息
|
||||
*
|
||||
* @param socket 网络连接
|
||||
* @param success 是否成功
|
||||
* @param message 消息
|
||||
* @param requestId 请求 ID
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) {
|
||||
try {
|
||||
Object responseData = MapUtil.builder()
|
||||
.put("success", success)
|
||||
.put("message", message)
|
||||
.build();
|
||||
|
||||
int code = success ? 0 : 401;
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData,
|
||||
code, message);
|
||||
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
|
||||
socket.write(Buffer.buffer(encodedData));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证设备认证信息
|
||||
*
|
||||
* @param authParams 认证参数
|
||||
* @return 是否认证成功
|
||||
*/
|
||||
private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
|
||||
try {
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
|
||||
.setPassword(authParams.getPassword()));
|
||||
result.checkError();
|
||||
return BooleanUtil.isTrue(result.getData());
|
||||
} catch (Exception e) {
|
||||
log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误响应
|
||||
*
|
||||
* @param socket 网络连接
|
||||
* @param requestId 请求 ID
|
||||
* @param errorMessage 错误消息
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) {
|
||||
sendResponse(socket, false, errorMessage, requestId, codecType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送成功响应
|
||||
*
|
||||
* @param socket 网络连接
|
||||
* @param requestId 请求 ID
|
||||
* @param message 消息
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) {
|
||||
sendResponse(socket, true, message, requestId, codecType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析认证参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 认证参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof java.util.Map) {
|
||||
java.util.Map<String, Object> paramMap = (java.util.Map<String, Object>) params;
|
||||
return new IotDeviceAuthReqDTO()
|
||||
.setClientId(MapUtil.getStr(paramMap, "clientId"))
|
||||
.setUsername(MapUtil.getStr(paramMap, "username"))
|
||||
.setPassword(MapUtil.getStr(paramMap, "password"));
|
||||
}
|
||||
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceAuthReqDTO) {
|
||||
return (IotDeviceAuthReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
String jsonStr = JsonUtils.toJsonString(params);
|
||||
return JsonUtils.parseObject(jsonStr, IotDeviceAuthReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.auth;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
|
||||
/**
|
||||
* IoT 设备 Token Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IotDeviceTokenService {
|
||||
|
||||
/**
|
||||
* 创建设备 Token
|
||||
*
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 设备 Token
|
||||
*/
|
||||
String createToken(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 验证设备 Token
|
||||
*
|
||||
* @param token 设备 Token
|
||||
* @return 设备信息
|
||||
*/
|
||||
IotDeviceAuthUtils.DeviceInfo verifyToken(String token);
|
||||
|
||||
/**
|
||||
* 解析用户名
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 设备信息
|
||||
*/
|
||||
IotDeviceAuthUtils.DeviceInfo parseUsername(String username);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.auth;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
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_TOKEN_EXPIRED;
|
||||
|
||||
/**
|
||||
* IoT 设备 Token Service 实现类:调用远程的 device http 接口,进行设备 Token 生成、解析等逻辑
|
||||
*
|
||||
* 注意:目前仅 HTTP 协议使用
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
|
||||
|
||||
@Resource
|
||||
private IotGatewayProperties gatewayProperties;
|
||||
|
||||
@Override
|
||||
public String createToken(String productKey, String deviceName) {
|
||||
Assert.notBlank(productKey, "productKey 不能为空");
|
||||
Assert.notBlank(deviceName, "deviceName 不能为空");
|
||||
// 构建 JWT payload
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("productKey", productKey);
|
||||
payload.put("deviceName", deviceName);
|
||||
LocalDateTime expireTime = LocalDateTimeUtils.addTime(gatewayProperties.getToken().getExpiration());
|
||||
payload.put("exp", LocalDateTimeUtils.toEpochSecond(expireTime)); // 过期时间(exp 是 JWT 规范推荐)
|
||||
|
||||
// 生成 JWT Token
|
||||
return JWTUtil.createToken(payload, gatewayProperties.getToken().getSecret().getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) {
|
||||
Assert.notBlank(token, "token 不能为空");
|
||||
// 校验 JWT Token
|
||||
boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes());
|
||||
if (!verify) {
|
||||
throw exception(DEVICE_TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
// 解析 Token
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
JSONObject payload = jwt.getPayloads();
|
||||
// 检查过期时间
|
||||
Long exp = payload.getLong("exp");
|
||||
if (exp == null || exp < System.currentTimeMillis() / 1000) {
|
||||
throw exception(DEVICE_TOKEN_EXPIRED);
|
||||
}
|
||||
String productKey = payload.getStr("productKey");
|
||||
String deviceName = payload.getStr("deviceName");
|
||||
Assert.notBlank(productKey, "productKey 不能为空");
|
||||
Assert.notBlank(deviceName, "deviceName 不能为空");
|
||||
return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) {
|
||||
return IotDeviceAuthUtils.parseUsername(username);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
|
||||
/**
|
||||
* IoT 设备信息 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IotDeviceService {
|
||||
|
||||
/**
|
||||
* 根据 productKey 和 deviceName 获取设备信息
|
||||
*
|
||||
* @param productKey 产品标识
|
||||
* @param deviceName 设备名称
|
||||
* @return 设备信息
|
||||
*/
|
||||
IotDeviceRespDTO getDeviceFromCache(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 根据 id 获取设备信息
|
||||
*
|
||||
* @param id 设备编号
|
||||
* @return 设备信息
|
||||
*/
|
||||
IotDeviceRespDTO getDeviceFromCache(Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
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.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.Duration;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
||||
|
||||
/**
|
||||
* IoT 设备信息 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
|
||||
private static final Duration CACHE_EXPIRE = Duration.ofMinutes(1);
|
||||
|
||||
/**
|
||||
* 通过 id 查询设备的缓存
|
||||
*/
|
||||
private final LoadingCache<Long, IotDeviceRespDTO> deviceCaches = buildAsyncReloadingCache(
|
||||
CACHE_EXPIRE,
|
||||
new CacheLoader<Long, IotDeviceRespDTO>() {
|
||||
|
||||
@Override
|
||||
public IotDeviceRespDTO load(Long id) {
|
||||
CommonResult<IotDeviceRespDTO> result = deviceApi.getDevice(new IotDeviceGetReqDTO().setId(id));
|
||||
IotDeviceRespDTO device = result.getCheckedData();
|
||||
Assert.notNull(device, "设备({}) 不能为空", id);
|
||||
// 相互缓存
|
||||
deviceCaches2.put(new KeyValue<>(device.getProductKey(), device.getDeviceName()), device);
|
||||
return device;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* 通过 productKey + deviceName 查询设备的缓存
|
||||
*/
|
||||
private final LoadingCache<KeyValue<String, String>, IotDeviceRespDTO> deviceCaches2 = buildAsyncReloadingCache(
|
||||
CACHE_EXPIRE,
|
||||
new CacheLoader<KeyValue<String, String>, IotDeviceRespDTO>() {
|
||||
|
||||
@Override
|
||||
public IotDeviceRespDTO load(KeyValue<String, String> kv) {
|
||||
CommonResult<IotDeviceRespDTO> result = deviceApi.getDevice(new IotDeviceGetReqDTO()
|
||||
.setProductKey(kv.getKey()).setDeviceName(kv.getValue()));
|
||||
IotDeviceRespDTO device = result.getCheckedData();
|
||||
Assert.notNull(device, "设备({}/{}) 不能为空", kv.getKey(), kv.getValue());
|
||||
// 相互缓存
|
||||
deviceCaches.put(device.getId(), device);
|
||||
return device;
|
||||
}
|
||||
});
|
||||
|
||||
@Resource
|
||||
private IotDeviceCommonApi deviceApi;
|
||||
|
||||
@Override
|
||||
public IotDeviceRespDTO getDeviceFromCache(String productKey, String deviceName) {
|
||||
return deviceCaches2.getUnchecked(new KeyValue<>(productKey, deviceName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceRespDTO getDeviceFromCache(Long id) {
|
||||
return deviceCaches.getUnchecked(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device.message;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
|
||||
/**
|
||||
* IoT 设备消息 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IotDeviceMessageService {
|
||||
|
||||
/**
|
||||
* 编码消息
|
||||
*
|
||||
* @param message 消息
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 编码后的消息内容
|
||||
*/
|
||||
byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 编码消息
|
||||
*
|
||||
* @param message 消息
|
||||
* @param codecType 编解码器类型
|
||||
* @return 编码后的消息内容
|
||||
*/
|
||||
byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String codecType);
|
||||
|
||||
/**
|
||||
* 解码消息
|
||||
*
|
||||
* @param bytes 消息内容
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 解码后的消息内容
|
||||
*/
|
||||
IotDeviceMessage decodeDeviceMessage(byte[] bytes,
|
||||
String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 解码消息
|
||||
*
|
||||
* @param bytes 消息内容
|
||||
* @param codecType 编解码器类型
|
||||
* @return 解码后的消息内容
|
||||
*/
|
||||
IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType);
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param message 消息
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param serverId 设备连接的 serverId
|
||||
*/
|
||||
void sendDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName, String serverId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device.message;
|
||||
|
||||
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.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.service.device.IotDeviceService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
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;
|
||||
|
||||
/**
|
||||
* IoT 设备消息 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeDeviceMessage(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()));
|
||||
}
|
||||
|
||||
// 2. 编码消息
|
||||
return codec.encode(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeDeviceMessage(IotDeviceMessage message,
|
||||
String codecType) {
|
||||
// 1. 获取编解码器
|
||||
IotDeviceMessageCodec codec = codes.get(codecType);
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
|
||||
}
|
||||
|
||||
// 2. 编码消息
|
||||
return codec.encode(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceMessage decodeDeviceMessage(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()));
|
||||
}
|
||||
|
||||
// 2. 解码消息
|
||||
return codec.decode(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) {
|
||||
// 1. 获取编解码器
|
||||
IotDeviceMessageCodec codec = codes.get(codecType);
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
|
||||
}
|
||||
|
||||
// 2. 解码消息
|
||||
return codec.decode(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendDeviceMessage(IotDeviceMessage message,
|
||||
String productKey, String deviceName, String serverId) {
|
||||
// 1. 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
|
||||
if (device == null) {
|
||||
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
|
||||
}
|
||||
|
||||
// 2. 发送消息
|
||||
appendDeviceMessage(message, device, serverId);
|
||||
deviceMessageProducer.sendDeviceMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 补充消息的后端字段
|
||||
*
|
||||
* @param message 消息
|
||||
* @param device 设备信息
|
||||
* @param serverId 设备连接的 serverId
|
||||
*/
|
||||
private void appendDeviceMessage(IotDeviceMessage message,
|
||||
IotDeviceRespDTO device, String serverId) {
|
||||
message.setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now())
|
||||
.setDeviceId(device.getId()).setTenantId(device.getTenantId()).setServerId(serverId);
|
||||
// 特殊:如果设备没有指定 requestId,则使用 messageId
|
||||
if (StrUtil.isEmpty(message.getRequestId())) {
|
||||
message.setRequestId(message.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.device.remote;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
* Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class IotDeviceApiImpl implements IotDeviceCommonApi {
|
||||
|
||||
@Resource
|
||||
private IotGatewayProperties gatewayProperties;
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc();
|
||||
restTemplate = new RestTemplateBuilder()
|
||||
.rootUri(rpc.getUrl() + "/rpc-api/iot/device")
|
||||
.setConnectTimeout(rpc.getReadTimeout())
|
||||
.setReadTimeout(rpc.getConnectTimeout())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) {
|
||||
return doPost("/auth", authReqDTO, new ParameterizedTypeReference<CommonResult<Boolean>>() { });
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO getReqDTO) {
|
||||
return doPost("/get", getReqDTO, new ParameterizedTypeReference<CommonResult<IotDeviceRespDTO>>() { });
|
||||
}
|
||||
|
||||
private <T, R> CommonResult<R> doPost(String url, T body,
|
||||
ParameterizedTypeReference<CommonResult<R>> responseType) {
|
||||
try {
|
||||
// 请求
|
||||
HttpEntity<T> requestEntity = new HttpEntity<>(body);
|
||||
ResponseEntity<CommonResult<R>> response = restTemplate.exchange(
|
||||
url, HttpMethod.POST, requestEntity, responseType);
|
||||
// 响应
|
||||
CommonResult<R> result = response.getBody();
|
||||
Assert.notNull(result, "请求结果不能为空");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("[doPost][url({}) body({}) 发生异常]", url, body, e);
|
||||
return CommonResult.error(INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT 主题工具类
|
||||
* <p>
|
||||
* 用于统一管理 MQTT 协议中的主题常量,基于 Alink 协议规范
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public final class IotMqttTopicUtils {
|
||||
|
||||
// ========== 静态常量 ==========
|
||||
|
||||
/**
|
||||
* 系统主题前缀
|
||||
*/
|
||||
private static final String SYS_TOPIC_PREFIX = "/sys/";
|
||||
|
||||
/**
|
||||
* 回复主题后缀
|
||||
*/
|
||||
private static final String REPLY_TOPIC_SUFFIX = "_reply";
|
||||
|
||||
// ========== MQTT HTTP 接口路径常量 ==========
|
||||
|
||||
/**
|
||||
* MQTT 认证接口路径
|
||||
* 对应 EMQX HTTP 认证插件的认证请求接口
|
||||
*/
|
||||
public static final String MQTT_AUTH_PATH = "/mqtt/auth";
|
||||
|
||||
/**
|
||||
* MQTT 统一事件处理接口路径
|
||||
* 对应 EMQX Webhook 的统一事件处理接口,支持所有客户端事件
|
||||
* 包括:client.connected、client.disconnected、message.publish 等
|
||||
*/
|
||||
public static final String MQTT_EVENT_PATH = "/mqtt/event";
|
||||
|
||||
// ========== 工具方法 ==========
|
||||
|
||||
/**
|
||||
* 根据消息方法构建对应的主题
|
||||
*
|
||||
* @param method 消息方法,例如 thing.property.post
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param isReply 是否为回复消息
|
||||
* @return 完整的主题路径
|
||||
*/
|
||||
public static String buildTopicByMethod(String method, String productKey, String deviceName, boolean isReply) {
|
||||
if (StrUtil.isBlank(method)) {
|
||||
return null;
|
||||
}
|
||||
// 1. 将点分隔符转换为斜杠
|
||||
String topicSuffix = method.replace('.', '/');
|
||||
// 2. 对于回复消息,添加 _reply 后缀
|
||||
if (isReply) {
|
||||
topicSuffix += REPLY_TOPIC_SUFFIX;
|
||||
}
|
||||
// 3. 构建完整主题
|
||||
return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
spring:
|
||||
application:
|
||||
name: iot-gateway-server
|
||||
profiles:
|
||||
active: local # 默认激活本地开发环境
|
||||
|
||||
# Redis 配置
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1 # Redis 服务器地址
|
||||
port: 6379 # Redis 服务器端口
|
||||
database: 0 # Redis 数据库索引
|
||||
# password: # Redis 密码,如果有的话
|
||||
timeout: 30000ms # 连接超时时间
|
||||
|
||||
--- #################### 消息队列相关 ####################
|
||||
|
||||
# rocketmq 配置项,对应 RocketMQProperties 配置类
|
||||
rocketmq:
|
||||
name-server: 127.0.0.1:9876 # RocketMQ Namesrv
|
||||
# Producer 配置项
|
||||
producer:
|
||||
group: ${spring.application.name}_PRODUCER # 生产者分组
|
||||
|
||||
--- #################### IoT 网关相关配置 ####################
|
||||
|
||||
yudao:
|
||||
iot:
|
||||
# 消息总线配置
|
||||
message-bus:
|
||||
type: redis # 消息总线的类型
|
||||
|
||||
# 网关配置
|
||||
gateway:
|
||||
# 设备 RPC 配置
|
||||
rpc:
|
||||
url: http://127.0.0.1:48080 # 主程序 API 地址
|
||||
connect-timeout: 30s
|
||||
read-timeout: 30s
|
||||
# 设备 Token 配置
|
||||
token:
|
||||
secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位
|
||||
expiration: 7d
|
||||
|
||||
# 协议配置
|
||||
protocol:
|
||||
# ====================================
|
||||
# 针对引入的 HTTP 组件的配置
|
||||
# ====================================
|
||||
http:
|
||||
enabled: true
|
||||
server-port: 8092
|
||||
# ====================================
|
||||
# 针对引入的 EMQX 组件的配置
|
||||
# ====================================
|
||||
emqx:
|
||||
enabled: false
|
||||
http-port: 8090 # MQTT HTTP 服务端口
|
||||
mqtt-host: 127.0.0.1 # MQTT Broker 地址
|
||||
mqtt-port: 1883 # MQTT Broker 端口
|
||||
mqtt-username: admin # MQTT 用户名
|
||||
mqtt-password: public # MQTT 密码
|
||||
mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID
|
||||
mqtt-ssl: false # 是否开启 SSL
|
||||
mqtt-topics:
|
||||
- "/sys/#" # 系统主题
|
||||
clean-session: true # 是否启用 Clean Session (默认: true)
|
||||
keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60)
|
||||
max-inflight-queue: 10000 # 最大飞行消息队列,单位:条
|
||||
connect-timeout-seconds: 10 # 连接超时,单位:秒
|
||||
# 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false!
|
||||
# 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true
|
||||
trust-all: true # 在 dev 环境可以设为 true
|
||||
# 遗嘱消息配置 (用于网关异常下线时通知其他系统)
|
||||
will:
|
||||
enabled: true # 生产环境强烈建议开启
|
||||
topic: "gateway/status/${yudao.iot.gateway.emqx.mqtt-client-id}" # 遗嘱消息主题
|
||||
payload: "offline" # 遗嘱消息负载
|
||||
qos: 1 # 遗嘱消息 QoS
|
||||
retain: true # 遗嘱消息是否保留
|
||||
# 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效)
|
||||
ssl-options:
|
||||
key-store-path: "classpath:certs/client.jks" # 客户端证书库路径
|
||||
key-store-password: "your-keystore-password" # 客户端证书库密码
|
||||
trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
|
||||
trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
|
||||
# ====================================
|
||||
# 针对引入的 TCP 组件的配置
|
||||
# ====================================
|
||||
tcp:
|
||||
enabled: false
|
||||
port: 8091
|
||||
keep-alive-timeout-ms: 30000
|
||||
max-connections: 1000
|
||||
ssl-enabled: false
|
||||
ssl-cert-path: "classpath:certs/client.jks"
|
||||
ssl-key-path: "classpath:certs/client.jks"
|
||||
# ====================================
|
||||
# 针对引入的 MQTT 组件的配置
|
||||
# ====================================
|
||||
mqtt:
|
||||
enabled: true
|
||||
port: 1883
|
||||
max-message-size: 8192
|
||||
connect-timeout-seconds: 60
|
||||
ssl-enabled: false
|
||||
|
||||
--- #################### 日志相关配置 ####################
|
||||
|
||||
# 基础日志配置
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
level:
|
||||
# 应用基础日志级别
|
||||
cn.iocoder.yudao.module.iot.gateway: INFO
|
||||
org.springframework.boot: INFO
|
||||
# RocketMQ 日志
|
||||
org.apache.rocketmq: WARN
|
||||
# MQTT 客户端日志
|
||||
# io.vertx.mqtt: DEBUG
|
||||
# 开发环境详细日志
|
||||
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
|
||||
# 根日志级别
|
||||
root: INFO
|
||||
|
||||
debug: false
|
||||
@@ -0,0 +1,193 @@
|
||||
# TCP 二进制协议数据包格式说明
|
||||
|
||||
## 1. 协议概述
|
||||
|
||||
TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。
|
||||
|
||||
### 1.1 协议特点
|
||||
|
||||
- **高效传输**:完全二进制格式,减少数据传输量
|
||||
- **版本控制**:内置协议版本号,支持协议升级
|
||||
- **类型安全**:明确的消息类型标识
|
||||
- **简洁设计**:去除冗余字段,协议更加精简
|
||||
- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容
|
||||
|
||||
## 2. 协议格式
|
||||
|
||||
### 2.1 整体结构
|
||||
|
||||
```
|
||||
+--------+--------+--------+---------------------------+--------+--------+
|
||||
| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) |
|
||||
+--------+--------+--------+---------------------------+--------+--------+
|
||||
| 消息 ID 长度(2字节) | 消息 ID (变长字符串) |
|
||||
+--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
| 方法名长度(2字节) | 方法名(变长字符串) |
|
||||
+--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
| 消息体数据(变长) |
|
||||
+--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
```
|
||||
|
||||
### 2.2 字段详细说明
|
||||
|
||||
| 字段 | 长度 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 |
|
||||
| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 |
|
||||
| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 |
|
||||
| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) |
|
||||
| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 |
|
||||
| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) |
|
||||
| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 |
|
||||
| 方法名 | 变长 | string | 消息方法名(UTF-8编码) |
|
||||
| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 |
|
||||
|
||||
**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
|
||||
|
||||
### 2.3 协议常量定义
|
||||
|
||||
```java
|
||||
// 协议标识
|
||||
private static final byte MAGIC_NUMBER = (byte) 0x7E;
|
||||
private static final byte PROTOCOL_VERSION = (byte) 0x01;
|
||||
|
||||
// 消息类型
|
||||
private static final byte REQUEST = (byte) 0x01; // 请求消息
|
||||
private static final byte RESPONSE = (byte) 0x02; // 响应消息
|
||||
|
||||
// 协议长度
|
||||
private static final int HEADER_FIXED_LENGTH = 7; // 固定头部长度
|
||||
private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度
|
||||
```
|
||||
|
||||
## 3. 消息类型和格式
|
||||
|
||||
### 3.1 请求消息 (REQUEST - 0x01)
|
||||
|
||||
请求消息用于设备向服务器发送数据或请求。
|
||||
|
||||
#### 3.1.1 消息体格式
|
||||
```
|
||||
消息体 = params 数据(JSON格式)
|
||||
```
|
||||
|
||||
#### 3.1.2 示例:设备认证请求
|
||||
|
||||
**消息内容:**
|
||||
- 消息 ID: `auth_1704067200000_123`
|
||||
- 方法名: `auth`
|
||||
- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}`
|
||||
|
||||
**二进制数据包结构:**
|
||||
```
|
||||
7E // 魔术字 (0x7E)
|
||||
01 // 版本号 (0x01)
|
||||
01 // 消息类型 (REQUEST)
|
||||
00 00 00 89 // 消息长度 (137字节)
|
||||
00 19 // 消息 ID 长度 (25字节)
|
||||
61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123"
|
||||
36 37 32 30 30 30 30 30 5F 31
|
||||
32 33
|
||||
00 04 // 方法名长度 (4字节)
|
||||
61 75 74 68 // 方法名: "auth"
|
||||
7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据
|
||||
22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001",
|
||||
30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName",
|
||||
6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"}
|
||||
64 75 63 74 4B 65 79 5F 64 65
|
||||
76 69 63 65 4E 61 6D 65 22 2C
|
||||
22 70 61 73 73 77 6F 72 64 22
|
||||
3A 22 64 65 76 69 63 65 5F 70
|
||||
61 73 73 77 6F 72 64 22 7D
|
||||
```
|
||||
|
||||
#### 3.1.3 示例:属性数据上报
|
||||
|
||||
**消息内容:**
|
||||
- 消息 ID: `property_1704067200000_456`
|
||||
- 方法名: `thing.property.post`
|
||||
- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}`
|
||||
|
||||
### 3.2 响应消息 (RESPONSE - 0x02)
|
||||
|
||||
响应消息用于服务器向设备回复请求结果。
|
||||
|
||||
#### 3.2.1 消息体格式
|
||||
```
|
||||
消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON)
|
||||
```
|
||||
|
||||
#### 3.2.2 字段说明
|
||||
|
||||
| 字段 | 长度 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 |
|
||||
| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 |
|
||||
| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) |
|
||||
| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) |
|
||||
|
||||
#### 3.2.3 示例:认证成功响应
|
||||
|
||||
**消息内容:**
|
||||
- 消息 ID: `auth_response_1704067200000_123`
|
||||
- 方法名: `auth`
|
||||
- 响应码: `0`
|
||||
- 响应消息: `认证成功`
|
||||
- 响应数据: `{"success":true,"message":"认证成功"}`
|
||||
|
||||
**二进制数据包结构:**
|
||||
```
|
||||
7E // 魔术字 (0x7E)
|
||||
01 // 版本号 (0x01)
|
||||
02 // 消息类型 (RESPONSE)
|
||||
00 00 00 A4 // 消息长度 (164字节)
|
||||
00 22 // 消息 ID 长度 (34字节)
|
||||
61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123"
|
||||
6E 73 65 5F 31 37 30 34 30 36
|
||||
37 32 30 30 30 30 30 5F 31 32
|
||||
33
|
||||
00 04 // 方法名长度 (4字节)
|
||||
61 75 74 68 // 方法名: "auth"
|
||||
00 00 00 00 // 响应码 (0 = 成功)
|
||||
00 0C // 响应消息长度 (12字节)
|
||||
E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8)
|
||||
8A 9F
|
||||
7B 22 73 75 63 63 65 73 73 22 // JSON响应数据
|
||||
3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"}
|
||||
73 61 67 65 22 3A 22 E8 AE A4
|
||||
E8 AF 81 E6 88 90 E5 8A 9F 22
|
||||
7D
|
||||
```
|
||||
|
||||
## 4. 编解码器标识
|
||||
|
||||
```java
|
||||
public static final String TYPE = "TCP_BINARY";
|
||||
```
|
||||
|
||||
## 5. 协议优势
|
||||
|
||||
- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量
|
||||
- **解析高效**:直接二进制操作,减少字符串转换开销
|
||||
- **类型安全**:明确的消息类型和字段定义
|
||||
- **设计简洁**:去除冗余字段,协议更加精简高效
|
||||
- **版本控制**:内置版本号支持协议升级
|
||||
|
||||
## 6. 与 JSON 协议对比
|
||||
|
||||
| 特性 | 二进制协议 | JSON协议 |
|
||||
|------|-------------|--------|
|
||||
| 数据大小 | 小(节省30-50%) | 大 |
|
||||
| 解析性能 | 高 | 中等 |
|
||||
| 网络开销 | 低 | 高 |
|
||||
| 可读性 | 差 | 优秀 |
|
||||
| 调试难度 | 高 | 低 |
|
||||
| 扩展性 | 良好 | 优秀 |
|
||||
|
||||
**推荐场景**:
|
||||
- ✅ **高频数据传输**:传感器数据实时上报
|
||||
- ✅ **带宽受限环境**:移动网络、卫星通信
|
||||
- ✅ **性能要求高**:需要低延迟、高吞吐的场景
|
||||
- ✅ **设备资源有限**:嵌入式设备、低功耗设备
|
||||
- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议
|
||||
- ❌ **快速原型开发**:开发效率低
|
||||
@@ -0,0 +1,191 @@
|
||||
# TCP JSON 格式协议说明
|
||||
|
||||
## 1. 协议概述
|
||||
|
||||
TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点:
|
||||
|
||||
- **标准化**:使用标准 JSON 格式,易于解析和处理
|
||||
- **可读性**:人类可读,便于调试和维护
|
||||
- **扩展性**:可以轻松添加新字段,向后兼容
|
||||
- **跨平台**:JSON 格式支持所有主流编程语言
|
||||
- **安全优化**:移除冗余的 deviceId 字段,提高安全性
|
||||
|
||||
## 2. 消息格式
|
||||
|
||||
### 2.1 基础消息结构
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "消息唯一标识",
|
||||
"method": "消息方法",
|
||||
"params": {
|
||||
// 请求参数
|
||||
},
|
||||
"data": {
|
||||
// 响应数据
|
||||
},
|
||||
"code": 响应码,
|
||||
"msg": "响应消息",
|
||||
"timestamp": 时间戳
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ 重要说明**:
|
||||
- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID
|
||||
- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息
|
||||
|
||||
### 2.2 字段详细说明
|
||||
|
||||
| 字段名 | 类型 | 必填 | 用途 | 说明 |
|
||||
|--------|------|------|------|------|
|
||||
| id | String | 是 | 所有消息 | 消息唯一标识 |
|
||||
| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` |
|
||||
| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 |
|
||||
| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 |
|
||||
| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 |
|
||||
| msg | String | 否 | 响应消息 | 响应提示信息 |
|
||||
| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 |
|
||||
|
||||
### 2.3 消息分类
|
||||
|
||||
#### 2.3.1 请求消息(上行)
|
||||
- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段
|
||||
- **方向**:设备 → 服务器
|
||||
- **用途**:设备认证、数据上报、状态更新等
|
||||
|
||||
#### 2.3.2 响应消息(下行)
|
||||
- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段
|
||||
- **方向**:服务器 → 设备
|
||||
- **用途**:认证结果、指令响应、错误提示等
|
||||
|
||||
## 3. 消息示例
|
||||
|
||||
### 3.1 设备认证 (auth)
|
||||
|
||||
#### 认证请求格式
|
||||
**消息方向**:设备 → 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "auth_1704067200000_123",
|
||||
"method": "auth",
|
||||
"params": {
|
||||
"clientId": "device_001",
|
||||
"username": "productKey_deviceName",
|
||||
"password": "设备密码"
|
||||
},
|
||||
"timestamp": 1704067200000
|
||||
}
|
||||
```
|
||||
|
||||
**认证参数说明:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| clientId | String | 是 | 客户端唯一标识,用于连接管理 |
|
||||
| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` |
|
||||
| password | String | 是 | 设备密码,在设备管理平台配置 |
|
||||
|
||||
#### 认证响应格式
|
||||
**消息方向**:服务器 → 设备
|
||||
|
||||
**认证成功响应:**
|
||||
```json
|
||||
{
|
||||
"id": "response_auth_1704067200000_123",
|
||||
"method": "auth",
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "认证成功"
|
||||
},
|
||||
"code": 0,
|
||||
"msg": "认证成功",
|
||||
"timestamp": 1704067200001
|
||||
}
|
||||
```
|
||||
|
||||
**认证失败响应:**
|
||||
```json
|
||||
{
|
||||
"id": "response_auth_1704067200000_123",
|
||||
"method": "auth",
|
||||
"data": {
|
||||
"success": false,
|
||||
"message": "认证失败:用户名或密码错误"
|
||||
},
|
||||
"code": 401,
|
||||
"msg": "认证失败",
|
||||
"timestamp": 1704067200001
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 属性数据上报 (thing.property.post)
|
||||
|
||||
**消息方向**:设备 → 服务器
|
||||
|
||||
**示例:温度传感器数据上报**
|
||||
```json
|
||||
{
|
||||
"id": "property_1704067200000_456",
|
||||
"method": "thing.property.post",
|
||||
"params": {
|
||||
"temperature": 25.5,
|
||||
"humidity": 60.2,
|
||||
"pressure": 1013.25,
|
||||
"battery": 85,
|
||||
"signal_strength": -65
|
||||
},
|
||||
"timestamp": 1704067200000
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 设备状态更新 (thing.state.update)
|
||||
|
||||
**消息方向**:设备 → 服务器
|
||||
|
||||
**示例:心跳请求**
|
||||
```json
|
||||
{
|
||||
"id": "heartbeat_1704067200000_321",
|
||||
"method": "thing.state.update",
|
||||
"params": {
|
||||
"state": "online",
|
||||
"uptime": 86400,
|
||||
"memory_usage": 65.2,
|
||||
"cpu_usage": 12.8
|
||||
},
|
||||
"timestamp": 1704067200000
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 编解码器标识
|
||||
|
||||
```java
|
||||
public static final String TYPE = "TCP_JSON";
|
||||
```
|
||||
|
||||
## 5. 协议优势
|
||||
|
||||
- **开发效率高**:JSON 格式,开发和调试简单
|
||||
- **跨语言支持**:所有主流语言都支持 JSON
|
||||
- **可读性优秀**:可以直接查看消息内容
|
||||
- **扩展性强**:可以轻松添加新字段
|
||||
- **安全性高**:移除 deviceId 字段,防止伪造攻击
|
||||
|
||||
## 6. 与二进制协议对比
|
||||
|
||||
| 特性 | JSON协议 | 二进制协议 |
|
||||
|------|----------|------------|
|
||||
| 开发难度 | 低 | 高 |
|
||||
| 调试难度 | 低 | 高 |
|
||||
| 可读性 | 优秀 | 差 |
|
||||
| 数据大小 | 中等 | 小(节省30-50%) |
|
||||
| 解析性能 | 中等 | 高 |
|
||||
| 学习成本 | 低 | 高 |
|
||||
|
||||
**推荐场景**:
|
||||
- ✅ **开发调试阶段**:调试友好,开发效率高
|
||||
- ✅ **快速原型开发**:实现简单,快速迭代
|
||||
- ✅ **多语言集成**:广泛的语言支持
|
||||
- ❌ **高频数据传输**:建议使用二进制协议
|
||||
- ❌ **带宽受限环境**:建议使用二进制协议
|
||||
Reference in New Issue
Block a user