【同步】BOOT 和 CLOUD 的功能(IoT)

This commit is contained in:
YunaiV
2026-02-14 16:35:48 +08:00
parent 2d4251eda7
commit 92eda45afd
245 changed files with 14927 additions and 7689 deletions

View File

@@ -1,33 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
/**
* {@link IotDeviceMessage} 的编解码器
*
* @author 芋道源码
*/
public interface IotDeviceMessageCodec {
/**
* 编码消息
*
* @param message 消息
* @return 编码后的消息内容
*/
byte[] encode(IotDeviceMessage message);
/**
* 解码消息
*
* @param bytes 消息内容
* @return 解码后的消息内容
*/
IotDeviceMessage decode(byte[] bytes);
/**
* @return 数据格式(编码器类型)
*/
String type();
}

View File

@@ -1,89 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec.alink;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 阿里云 Alink {@link IotDeviceMessage} 的编解码器
*
* @author 芋道源码
*/
@Component
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
public static final String TYPE = "Alink";
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class AlinkMessage {
public static final String VERSION_1 = "1.0";
/**
* 消息 ID且每个消息 ID 在当前设备具有唯一性
*/
private String id;
/**
* 版本号
*/
private String version;
/**
* 请求方法
*/
private String method;
/**
* 请求参数
*/
private Object params;
/**
* 响应结果
*/
private Object data;
/**
* 响应错误码
*/
private Integer code;
/**
* 响应提示
*
* 特殊:这里阿里云是 message为了保持和项目的 {@link CommonResult#getMsg()} 一致。
*/
private String msg;
}
@Override
public String type() {
return TYPE;
}
@Override
public byte[] encode(IotDeviceMessage message) {
AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg());
return JsonUtils.toJsonByte(alinkMessage);
}
@Override
@SuppressWarnings("DataFlowIssue")
public IotDeviceMessage decode(byte[] bytes) {
AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class);
Assert.notNull(alinkMessage, "消息不能为空");
Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0");
return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(),
alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
}
}

View File

@@ -1,4 +0,0 @@
/**
* 提供设备接入的各种数据(请求、响应)的编解码
*/
package cn.iocoder.yudao.module.iot.gateway.codec;

View File

@@ -1,4 +0,0 @@
/**
* TODO @芋艿:实现一个 alink 的 xml 版本
*/
package cn.iocoder.yudao.module.iot.gateway.codec.simple;

View File

@@ -1,110 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
*
* 采用纯 JSON 格式传输,格式如下:
* {
* "id": "消息 ID",
* "method": "消息方法",
* "params": {...}, // 请求参数
* "data": {...}, // 响应结果
* "code": 200, // 响应错误码
* "msg": "success", // 响应提示
* "timestamp": 时间戳
* }
*
* @author 芋道源码
*/
@Component
public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
public static final String TYPE = "TCP_JSON";
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class TcpJsonMessage {
/**
* 消息 ID且每个消息 ID 在当前设备具有唯一性
*/
private String id;
/**
* 请求方法
*/
private String method;
/**
* 请求参数
*/
private Object params;
/**
* 响应结果
*/
private Object data;
/**
* 响应错误码
*/
private Integer code;
/**
* 响应提示
*/
private String msg;
/**
* 时间戳
*/
private Long timestamp;
}
@Override
public String type() {
return TYPE;
}
@Override
public byte[] encode(IotDeviceMessage message) {
TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
message.getRequestId(),
message.getMethod(),
message.getParams(),
message.getData(),
message.getCode(),
message.getMsg(),
System.currentTimeMillis());
return JsonUtils.toJsonByte(tcpJsonMessage);
}
@Override
@SuppressWarnings("DataFlowIssue")
public IotDeviceMessage decode(byte[] bytes) {
String jsonStr = StrUtil.utf8Str(bytes).trim();
TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
Assert.notNull(tcpJsonMessage, "消息不能为空");
Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
return IotDeviceMessage.of(
tcpJsonMessage.getId(),
tcpJsonMessage.getMethod(),
tcpJsonMessage.getParams(),
tcpJsonMessage.getData(),
tcpJsonMessage.getCode(),
tcpJsonMessage.getMsg());
}
}

View File

@@ -1,254 +1,28 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* IoT 网关配置类
*
* @author 芋道源码
*/
@Configuration
@EnableConfigurationProperties(IotGatewayProperties.class)
@Slf4j
public class IotGatewayConfiguration {
/**
* IoT 网关 HTTP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true")
@Slf4j
public static class HttpProtocolConfiguration {
@Bean(name = "httpVertx", destroyMethod = "close")
public Vertx httpVertx() {
return Vertx.vertx();
}
@Bean
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("httpVertx") Vertx httpVertx) {
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
}
@Bean
public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol,
IotMessageBus messageBus) {
return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus);
}
@Bean
public IotMessageSerializerManager iotMessageSerializerManager() {
return new IotMessageSerializerManager();
}
/**
* IoT 网关 EMQX 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
@Slf4j
public static class EmqxProtocolConfiguration {
@Bean(name = "emqxVertx", destroyMethod = "close")
public Vertx emqxVertx() {
return Vertx.vertx();
}
@Bean
public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("emqxVertx") Vertx emqxVertx) {
return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
}
@Bean
public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("emqxVertx") Vertx emqxVertx) {
return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
}
@Bean
public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
IotMessageBus messageBus) {
return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
}
}
/**
* IoT 网关 TCP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true")
@Slf4j
public static class TcpProtocolConfiguration {
@Bean(name = "tcpVertx", destroyMethod = "close")
public Vertx tcpVertx() {
return Vertx.vertx();
}
@Bean
public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
@Qualifier("tcpVertx") Vertx tcpVertx) {
return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
deviceService, messageService, connectionManager, tcpVertx);
}
@Bean
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}
/**
* IoT 网关 MQTT 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
@Slf4j
public static class MqttProtocolConfiguration {
@Bean(name = "mqttVertx", destroyMethod = "close")
public Vertx mqttVertx() {
return Vertx.vertx();
}
@Bean
public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager,
@Qualifier("mqttVertx") Vertx mqttVertx) {
return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService,
connectionManager, mqttVertx);
}
@Bean
public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager) {
return new IotMqttDownstreamHandler(messageService, connectionManager);
}
@Bean
public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol,
IotMqttDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus);
}
}
/**
* IoT 网关 UDP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
@Slf4j
public static class UdpProtocolConfiguration {
@Bean(name = "udpVertx", destroyMethod = "close")
public Vertx udpVertx() {
return Vertx.vertx();
}
@Bean
public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
@Qualifier("udpVertx") Vertx udpVertx) {
return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
deviceService, messageService, sessionManager, udpVertx);
}
@Bean
public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
IotMessageBus messageBus) {
return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
}
}
/**
* IoT 网关 CoAP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
@Slf4j
public static class CoapProtocolConfiguration {
@Bean
public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
}
@Bean
public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
IotMessageBus messageBus) {
return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
}
}
/**
* IoT 网关 WebSocket 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
@Slf4j
public static class WebSocketProtocolConfiguration {
@Bean(name = "websocketVertx", destroyMethod = "close")
public Vertx websocketVertx() {
return Vertx.vertx();
}
@Bean
public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
@Qualifier("websocketVertx") Vertx websocketVertx) {
return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
deviceService, messageService, connectionManager, websocketVertx);
}
@Bean
public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
@Bean
public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) {
return new IotProtocolManager(gatewayProperties);
}
}

View File

@@ -1,5 +1,16 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -24,9 +35,9 @@ public class IotGatewayProperties {
private TokenProperties token;
/**
* 协议配置
* 协议实例列表
*/
private ProtocolProperties protocol;
private List<ProtocolProperties> protocols;
@Data
public static class RpcProperties {
@@ -65,582 +76,158 @@ public class IotGatewayProperties {
}
/**
* 协议实例配置
*/
@Data
public static class ProtocolProperties {
/**
* HTTP 组件配置
* 协议实例 ID如 "http-alink"、"tcp-binary"
*/
private HttpProperties http;
@NotEmpty(message = "协议实例 ID 不能为空")
private String id;
/**
* EMQX 组件配置
* 是否启用
*/
private EmqxProperties emqx;
@NotNull(message = "是否启用不能为空")
private Boolean enabled = true;
/**
* TCP 组件配置
* 协议类型
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
*/
private TcpProperties tcp;
/**
* MQTT 组件配置
*/
private MqttProperties mqtt;
/**
* MQTT WebSocket 组件配置
*/
private MqttWsProperties mqttWs;
/**
* UDP 组件配置
*/
private UdpProperties udp;
/**
* CoAP 组件配置
*/
private CoapProperties coap;
/**
* WebSocket 组件配置
*/
private WebSocketProperties websocket;
}
@Data
public static class HttpProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
@NotEmpty(message = "协议类型不能为空")
private String protocol;
/**
* 服务端口
*/
private Integer serverPort;
/**
* 是否开启 SSL
*/
@NotNull(message = "是否开启 SSL 不能为空")
private Boolean sslEnabled = false;
/**
* SSL 证书路径
*/
private String sslKeyPath;
/**
* SSL 证书路径
*/
private String sslCertPath;
}
@Data
public static class EmqxProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* HTTP 服务端口默认8090
*/
private Integer httpPort = 8090;
/**
* MQTT 服务器地址
*/
@NotEmpty(message = "MQTT 服务器地址不能为空")
private String mqttHost;
/**
* MQTT 服务器端口默认1883
*/
@NotNull(message = "MQTT 服务器端口不能为空")
private Integer mqttPort = 1883;
/**
* MQTT 用户名
*/
@NotEmpty(message = "MQTT 用户名不能为空")
private String mqttUsername;
/**
* MQTT 密码
*/
@NotEmpty(message = "MQTT 密码不能为空")
private String mqttPassword;
/**
* MQTT 客户端的 SSL 开关
*/
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
private Boolean mqttSsl = false;
/**
* MQTT 客户端 ID如果为空系统将自动生成
*/
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
private String mqttClientId;
/**
* MQTT 订阅的主题
*/
@NotEmpty(message = "MQTT 主题不能为空")
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
/**
* 默认 QoS 级别
* <p>
* 0 - 最多一次
* 1 - 至少一次
* 2 - 刚好一次
* 不同协议含义不同:
* 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP对应网关自身监听的服务端口
* 2. EMQX对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event
*/
private Integer mqttQos = 1;
@NotNull(message = "服务端口不能为空")
private Integer port;
/**
* 序列化类型(可选)
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
*
* 为什么是可选的呢?
* 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
* 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析
*/
private String serialize;
// ========== SSL 配置 ==========
/**
* 连接超时时间(秒
* SSL 配置(可选,配置文件中不配置则为 null
*/
private Integer connectTimeoutSeconds = 10;
@Valid
private SslConfig ssl;
// ========== 各协议配置 ==========
/**
* 重连延迟时间(毫秒)
* HTTP 协议配置
*/
private Long reconnectDelayMs = 5000L;
@Valid
private IotHttpConfig http;
/**
* WebSocket 协议配置
*/
@Valid
private IotWebSocketConfig websocket;
/**
* 是否启用 Clean Session (清理会话)
* true: 每次连接都是新会话Broker 不保留离线消息和订阅关系。
* 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
* TCP 协议配置
*/
private Boolean cleanSession = true;
@Valid
private IotTcpConfig tcp;
/**
* UDP 协议配置
*/
@Valid
private IotUdpConfig udp;
/**
* 心跳间隔(秒)
* 用于保持连接活性,及时发现网络中断。
* CoAP 协议配置
*/
private Integer keepAliveIntervalSeconds = 60;
@Valid
private IotCoapConfig coap;
/**
* 最大未确认消息队列大小
* 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
* MQTT 协议配置
*/
private Integer maxInflightQueue = 10000;
@Valid
private IotMqttConfig mqtt;
/**
* EMQX 协议配置
*/
@Valid
private IotEmqxConfig emqx;
/**
* 是否信任所有 SSL 证书
* 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
* 在生产环境中,应设置为 false并配置正确的信任库。
* Modbus TCP Client 协议配置
*/
private Boolean trustAll = false;
@Valid
private IotModbusTcpClientConfig modbusTcpClient;
/**
* 遗嘱消息配置 (用于网关异常下线时通知其他系统)
* Modbus TCP Server 协议配置
*/
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;
}
@Valid
private IotModbusTcpServerConfig modbusTcpServer;
}
/**
* SSL 配置
*/
@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;
public static class SslConfig {
/**
* 是否启用 SSL
*/
private Boolean sslEnabled = false;
/**
* SSL 配置
*/
private SslOptions sslOptions = new SslOptions();
/**
* SSL 配置选项
*/
@Data
public static class SslOptions {
/**
* 密钥证书选项
*/
private io.vertx.core.net.KeyCertOptions keyCertOptions;
/**
* 信任选项
*/
private io.vertx.core.net.TrustOptions trustOptions;
/**
* SSL 证书路径
*/
private String certPath;
/**
* SSL 私钥路径
*/
private String keyPath;
/**
* 信任存储路径
*/
private String trustStorePath;
/**
* 信任存储密码
*/
private String trustStorePassword;
}
}
@Data
public static class MqttWsProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* WebSocket 服务器端口默认8083
*/
private Integer port = 8083;
/**
* WebSocket 路径(默认:/mqtt
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/mqtt";
/**
* 最大消息大小(字节)
*/
private Integer maxMessageSize = 8192;
/**
* 连接超时时间(秒)
*/
private Integer connectTimeoutSeconds = 60;
/**
* 保持连接超时时间(秒)
*/
private Integer keepAliveTimeoutSeconds = 300;
/**
* 是否启用 SSLwss://
*/
private Boolean sslEnabled = false;
/**
* SSL 配置
*/
private SslOptions sslOptions = new SslOptions();
/**
* WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1"
*/
@NotEmpty(message = "WebSocket 子协议不能为空")
private String subProtocol = "mqtt";
/**
* 最大帧大小(字节)
*/
private Integer maxFrameSize = 65536;
/**
* SSL 配置选项
*/
@Data
public static class SslOptions {
/**
* 密钥证书选项
*/
private io.vertx.core.net.KeyCertOptions keyCertOptions;
/**
* 信任选项
*/
private io.vertx.core.net.TrustOptions trustOptions;
/**
* SSL 证书路径
*/
private String certPath;
/**
* SSL 私钥路径
*/
private String keyPath;
/**
* 信任存储路径
*/
private String trustStorePath;
/**
* 信任存储密码
*/
private String trustStorePassword;
}
}
@Data
public static class UdpProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务端口(默认 8093
*/
private Integer port = 8093;
/**
* 接收缓冲区大小(默认 64KB
*/
private Integer receiveBufferSize = 65536;
/**
* 发送缓冲区大小(默认 64KB
*/
private Integer sendBufferSize = 65536;
/**
* 会话超时时间(毫秒,默认 60 秒)
* <p>
* 用于清理不活跃的设备地址映射
*/
private Long sessionTimeoutMs = 60000L;
/**
* 会话清理间隔(毫秒,默认 30 秒)
*/
private Long sessionCleanIntervalMs = 30000L;
}
@Data
public static class CoapProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务端口CoAP 默认端口 5683
*/
@NotNull(message = "服务端口不能为空")
private Integer port = 5683;
/**
* 最大消息大小(字节)
*/
@NotNull(message = "最大消息大小不能为空")
private Integer maxMessageSize = 1024;
/**
* ACK 超时时间(毫秒)
*/
@NotNull(message = "ACK 超时时间不能为空")
private Integer ackTimeout = 2000;
/**
* 最大重传次数
*/
@NotNull(message = "最大重传次数不能为空")
private Integer maxRetransmit = 4;
}
@Data
public static class WebSocketProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务器端口默认8094
*/
private Integer port = 8094;
/**
* WebSocket 路径(默认:/ws
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/ws";
/**
* 最大消息大小(字节,默认 64KB
*/
private Integer maxMessageSize = 65536;
/**
* 最大帧大小(字节,默认 64KB
*/
private Integer maxFrameSize = 65536;
/**
* 空闲超时时间(秒,默认 60
*/
private Integer idleTimeoutSeconds = 60;
/**
* 是否启用 SSLwss://
*/
private Boolean sslEnabled = false;
@NotNull(message = "是否启用 SSL 不能为空")
private Boolean ssl = false;
/**
* SSL 证书路径
*/
@NotEmpty(message = "SSL 证书路径不能为空")
private String sslCertPath;
/**
* SSL 私钥路径
*/
@NotEmpty(message = "SSL 私钥路径不能为空")
private String sslKeyPath;
/**
* 密钥库KeyStore路径
* <p>
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)
*/
private String keyStorePath;
/**
* 密钥库密码
*/
private String keyStorePassword;
/**
* 信任库TrustStore路径
* <p>
* 包含服务端信任的 CA 证书,用于验证服务端的身份
*/
private String trustStorePath;
/**
* 信任库密码
*/
private String trustStorePassword;
}
}

View File

@@ -1,49 +1,53 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 EMQX 订阅者接收下行给设备的消息
* IoT 协议下行消息订阅者抽象类
*
* 负责接收来自消息总线的下行消息并委托给子类进行业务处理
*
* @author 芋道源码
*/
@AllArgsConstructor
@Slf4j
public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotEmqxDownstreamHandler downstreamHandler;
private final IotProtocol protocol;
private final IotMessageBus messageBus;
private final IotEmqxUpstreamProtocol protocol;
public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
this.protocol = protocol;
this.messageBus = messageBus;
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
}
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
/**
* 保证点对点消费需要保证独立的 Group所以使用 Topic 作为 Group
*/
@Override
public String getGroup() {
// 保证点对点消费需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void start() {
messageBus.register(this);
log.info("[start][{} 下行消息订阅成功Topic{}]", protocol.getType().name(), getTopic());
}
@Override
public void stop() {
messageBus.unregister(this);
log.info("[stop][{} 下行消息订阅已停止Topic{}]", protocol.getType().name(), getTopic());
}
@Override
public void onMessage(IotDeviceMessage message) {
log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
@@ -51,18 +55,25 @@ public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDevi
try {
// 1. 校验
String method = message.getMethod();
if (method == null) {
if (StrUtil.isBlank(method)) {
log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
message.getId(), message.getDeviceId());
return;
}
// 2. 处理下行消息
downstreamHandler.handle(message);
handleMessage(message);
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
message.getId(), message.getMethod(), message.getDeviceId(), e);
}
}
}
/**
* 处理下行消息
*
* @param message 下行消息
*/
protected abstract void handleMessage(IotDeviceMessage message);
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
/**
* IoT 协议接口
*
* 定义传输层协议的生命周期管理
*
* @author 芋道源码
*/
public interface IotProtocol {
/**
* 获取协议实例 ID
*
* @return 协议实例 ID如 "http-alink"、"tcp-binary"
*/
String getId();
/**
* 获取服务器 ID用于消息追踪全局唯一
*
* @return 服务器 ID
*/
String getServerId();
/**
* 获取协议类型
*
* @return 协议类型枚举
*/
IotProtocolTypeEnum getType();
/**
* 启动协议服务
*/
void start();
/**
* 停止协议服务
*/
void stop();
/**
* 检查协议服务是否正在运行
*
* @return 是否正在运行
*/
boolean isRunning();
}

View File

@@ -0,0 +1,217 @@
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import java.util.ArrayList;
import java.util.List;
/**
* IoT 协议管理器:负责根据配置创建和管理协议实例
*
* @author 芋道源码
*/
@Slf4j
public class IotProtocolManager implements SmartLifecycle {
private final IotGatewayProperties gatewayProperties;
/**
* 协议实例列表
*/
private final List<IotProtocol> protocols = new ArrayList<>();
@Getter
private volatile boolean running = false;
public IotProtocolManager(IotGatewayProperties gatewayProperties) {
this.gatewayProperties = gatewayProperties;
}
@Override
public void start() {
if (running) {
return;
}
List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
if (CollUtil.isEmpty(protocolConfigs)) {
log.info("[start][没有配置协议实例,跳过启动]");
return;
}
for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
if (BooleanUtil.isFalse(config.getEnabled())) {
log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
continue;
}
IotProtocol protocol = createProtocol(config);
if (protocol == null) {
continue;
}
protocol.start();
protocols.add(protocol);
}
running = true;
log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size());
}
@Override
public void stop() {
if (!running) {
return;
}
for (IotProtocol protocol : protocols) {
try {
protocol.stop();
} catch (Exception e) {
log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e);
}
}
protocols.clear();
running = false;
log.info("[stop][协议管理器已停止]");
}
/**
* 创建协议实例
*
* @param config 协议实例配置
* @return 协议实例
*/
@SuppressWarnings({"EnhancedSwitchMigration"})
private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) {
IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol());
if (protocolType == null) {
log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
return null;
}
switch (protocolType) {
case HTTP:
return createHttpProtocol(config);
case TCP:
return createTcpProtocol(config);
case UDP:
return createUdpProtocol(config);
case COAP:
return createCoapProtocol(config);
case WEBSOCKET:
return createWebSocketProtocol(config);
case MQTT:
return createMqttProtocol(config);
case EMQX:
return createEmqxProtocol(config);
case MODBUS_TCP_CLIENT:
return createModbusTcpClientProtocol(config);
case MODBUS_TCP_SERVER:
return createModbusTcpServerProtocol(config);
default:
throw new IllegalArgumentException(String.format(
"[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
}
}
/**
* 创建 HTTP 协议实例
*
* @param config 协议实例配置
* @return HTTP 协议实例
*/
private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotHttpProtocol(config);
}
/**
* 创建 TCP 协议实例
*
* @param config 协议实例配置
* @return TCP 协议实例
*/
private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotTcpProtocol(config);
}
/**
* 创建 UDP 协议实例
*
* @param config 协议实例配置
* @return UDP 协议实例
*/
private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotUdpProtocol(config);
}
/**
* 创建 CoAP 协议实例
*
* @param config 协议实例配置
* @return CoAP 协议实例
*/
private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotCoapProtocol(config);
}
/**
* 创建 WebSocket 协议实例
*
* @param config 协议实例配置
* @return WebSocket 协议实例
*/
private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotWebSocketProtocol(config);
}
/**
* 创建 MQTT 协议实例
*
* @param config 协议实例配置
* @return MQTT 协议实例
*/
private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotMqttProtocol(config);
}
/**
* 创建 EMQX 协议实例
*
* @param config 协议实例配置
* @return EMQX 协议实例
*/
private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotEmqxProtocol(config);
}
/**
* 创建 Modbus TCP Client 协议实例
*
* @param config 协议实例配置
* @return Modbus TCP Client 协议实例
*/
private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpClientProtocol(config);
}
/**
* 创建 Modbus TCP Server 协议实例
*
* @param config 协议实例配置
* @return Modbus TCP Server 协议实例
*/
private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpServerProtocol(config);
}
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT CoAP 协议配置
*
* @author 芋道源码
*/
@Data
public class IotCoapConfig {
/**
* 最大消息大小(字节)
*/
@NotNull(message = "最大消息大小不能为空")
@Min(value = 64, message = "最大消息大小必须大于 64 字节")
private Integer maxMessageSize = 1024;
/**
* ACK 超时时间(毫秒)
*/
@NotNull(message = "ACK 超时时间不能为空")
@Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒")
private Integer ackTimeoutMs = 2000;
/**
* 最大重传次数
*/
@NotNull(message = "最大重传次数不能为空")
@Min(value = 0, message = "最大重传次数必须大于等于 0")
private Integer maxRetransmit = 4;
}

View File

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

View File

@@ -0,0 +1,168 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import java.util.concurrent.TimeUnit;
/**
* IoT CoAP 协议实现
* <p>
* 基于 Eclipse Californium 实现,支持:
* 1. 认证POST /auth
* 2. 设备动态注册POST /auth/register/device
* 3. 子设备动态注册POST /auth/register/sub-device/{productKey}/{deviceName}
* 4. 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 5. 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* CoAP 服务器
*/
private CoapServer coapServer;
/**
* 下行消息订阅者
*/
private IotCoapDownstreamSubscriber downstreamSubscriber;
public IotCoapProtocol(ProtocolProperties properties) {
IotCoapConfig coapConfig = properties.getCoap();
Assert.notNull(coapConfig, "CoAP 协议配置coap不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.COAP;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1.1 创建 CoAP 配置
IotCoapConfig coapConfig = properties.getCoap();
Configuration config = Configuration.createStandardWithoutFile();
config.set(CoapConfig.COAP_PORT, properties.getPort());
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit());
// 1.2 创建 CoAP 服务器
coapServer = new CoapServer(config);
// 2.1 添加 /auth 认证资源
IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId);
IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler);
coapServer.add(authResource);
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
// 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源
IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler();
IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler);
authResource.add(new CoapResource("register") {{
add(registerResource);
add(registerSubResource);
}});
// 2.4 添加 /topic 根资源(用于上行消息)
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId);
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler);
coapServer.add(topicResource);
// 3. 启动服务器
coapServer.start();
running = true;
log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{}serverId{}]",
getId(), properties.getPort(), serverId);
// 4. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2. 关闭 CoAP 服务器
if (coapServer != null) {
try {
coapServer.stop();
coapServer.destroy();
coapServer = null;
log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT CoAP 协议 {} 已停止]", getId());
}
}

View File

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

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 CoAP 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
}
}

View File

@@ -0,0 +1,186 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等
*
* @author 芋道源码
*/
@Slf4j
public abstract class IotCoapAbstractHandler {
/**
* 自定义 CoAP Option 编号,用于携带 Token
* <p>
* CoAP Option 范围 2048-65535 属于实验/自定义范围
*/
public static final int OPTION_TOKEN = 2088;
private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
/**
* 处理 CoAP 请求(模板方法)
*
* @param exchange CoAP 交换对象
*/
public final void handle(CoapExchange exchange) {
try {
// 1. 前置处理
beforeHandle(exchange);
// 2. 执行业务逻辑
CommonResult<Object> result = handle0(exchange);
writeResponse(exchange, result);
} catch (ServiceException e) {
// 业务异常,返回对应的错误码和消息
writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage()));
} catch (IllegalArgumentException e) {
// 参数校验异常hutool Assert 抛出),返回 BAD_REQUEST
writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
} catch (Exception e) {
// 其他未知异常,返回 INTERNAL_SERVER_ERROR
log.error("[handle][CoAP 请求处理异常]", e);
writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR));
}
}
/**
* 处理 CoAP 请求(子类实现)
*
* @param exchange CoAP 交换对象
* @return 处理结果
*/
protected abstract CommonResult<Object> handle0(CoapExchange exchange);
/**
* 前置处理:认证等
*
* @param exchange CoAP 交换对象
*/
private void beforeHandle(CoapExchange exchange) {
// 1.1 如果不需要认证,则不走前置处理
if (!requiresAuthentication()) {
return;
}
// 1.2 从自定义 Option 获取 token
String token = getTokenFromOption(exchange);
if (StrUtil.isEmpty(token)) {
throw exception(UNAUTHORIZED);
}
// 1.3 校验 token
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
if (deviceInfo == null) {
throw exception(UNAUTHORIZED);
}
// 2.1 解析 productKey 和 deviceName
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) {
throw exception(BAD_REQUEST);
}
// 2.2 校验设备信息是否匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
throw exception(FORBIDDEN);
}
}
// ========== Token 相关方法 ==========
/**
* 是否需要认证(子类可覆盖)
* <p>
* 默认不需要认证
*
* @return 是否需要认证
*/
protected boolean requiresAuthentication() {
return false;
}
/**
* 从 URI 路径中获取 productKey子类实现
* <p>
* 默认抛出异常,需要认证的子类必须实现此方法
*
* @param uriPath URI 路径
* @return productKey
*/
protected String getProductKey(List<String> uriPath) {
throw new UnsupportedOperationException("子类需要实现 getProductKey 方法");
}
/**
* 从 URI 路径中获取 deviceName子类实现
* <p>
* 默认抛出异常,需要认证的子类必须实现此方法
*
* @param uriPath URI 路径
* @return deviceName
*/
protected String getDeviceName(List<String> uriPath) {
throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法");
}
/**
* 从自定义 CoAP Option 中获取 Token
*
* @param exchange CoAP 交换对象
* @return Token 值,如果不存在则返回 null
*/
protected String getTokenFromOption(CoapExchange exchange) {
Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
o -> o.getNumber() == OPTION_TOKEN);
return option != null ? new String(option.getValue()) : null;
}
// ========== 序列化相关方法 ==========
/**
* 解析请求体为指定类型
*
* @param exchange CoAP 交换对象
* @param clazz 目标类型
* @param <T> 目标类型泛型
* @return 解析后的对象,解析失败返回 null
*/
protected <T> T deserializeRequest(CoapExchange exchange, Class<T> clazz) {
byte[] payload = exchange.getRequestPayload();
if (ArrayUtil.isEmpty(payload)) {
return null;
}
return JsonUtils.parseObject(payload, clazz);
}
private static String serializeResponse(Object data) {
return JsonUtils.toJsonString(data);
}
protected void writeResponse(CoapExchange exchange, CommonResult<?> data) {
String json = serializeResponse(data);
exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
}
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
/**
* IoT 网关 CoAP 协议的【认证】处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapAuthHandler extends IotCoapAbstractHandler {
private final String serverId;
private final IotDeviceTokenService deviceTokenService;
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService deviceMessageService;
public IotCoapAuthHandler(String serverId) {
this.serverId = serverId;
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1. 解析参数
IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class);
Assert.notNull(request, "请求体不能为空");
Assert.notBlank(request.getClientId(), "clientId 不能为空");
Assert.notBlank(request.getUsername(), "username 不能为空");
Assert.notBlank(request.getPassword(), "password 不能为空");
// 2.1 执行认证
CommonResult<Boolean> result = deviceApi.authDevice(request);
result.checkError();
if (BooleanUtil.isFalse(result.getData())) {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空");
// 3. 执行上线
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(message,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
// 4. 构建响应数据
return CommonResult.success(MapUtil.of("token", token));
}
}

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
@@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource {
public static final String PATH = "auth";
private final IotCoapUpstreamProtocol protocol;
private final IotCoapAuthHandler authHandler;
public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
IotCoapAuthHandler authHandler) {
public IotCoapAuthResource(IotCoapAuthHandler authHandler) {
super(PATH);
this.protocol = protocol;
this.authHandler = authHandler;
log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
}
@@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource {
@Override
public void handlePOST(CoapExchange exchange) {
log.debug("[handlePOST][收到 /auth POST 请求]");
authHandler.handle(exchange, protocol);
authHandler.handle(exchange);
}
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
/**
* IoT 网关 CoAP 协议的【设备动态注册】处理器
* <p>
* 用于直连设备/网关的一型一密动态注册,不需要认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@Slf4j
public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
private final IotDeviceCommonApi deviceApi;
public IotCoapRegisterHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1. 解析参数
IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class);
Assert.notNull(request, "请求体不能为空");
Assert.notBlank(request.getProductKey(), "productKey 不能为空");
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(request.getSign(), "sign 不能为空");
// 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
result.checkError();
// 3. 构建响应数据
return CommonResult.success(result.getData());
}
}

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;

View File

@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* IoT 网关 CoAP 协议的【子设备动态注册】处理器
* <p>
* 用于子设备的动态注册,需要网关认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
@Slf4j
public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler {
private final IotDeviceCommonApi deviceApi;
public IotCoapRegisterSubHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1.1 解析通用参数(从 URI 路径获取网关设备信息)
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
// 1.2 解析子设备列表
SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notEmpty(request.getParams(), "params 不能为空");
// 2. 调用子设备动态注册
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
.setGatewayProductKey(productKey)
.setGatewayDeviceName(deviceName)
.setSubDevices(request.getParams());
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
result.checkError();
// 3. 返回结果
return success(result.getData());
}
@Override
protected boolean requiresAuthentication() {
return true;
}
@Override
protected String getProductKey(List<String> uriPath) {
// 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
return CollUtil.get(uriPath, 3);
}
@Override
protected String getDeviceName(List<String> uriPath) {
// 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
return CollUtil.get(uriPath, 4);
}
@Data
public static class SubDeviceRegisterRequest {
private List<IotSubDeviceRegisterReqDTO> params;
}
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
/**
* IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName}
* <p>
* 用于子设备的动态注册,需要网关认证
* <p>
* 支持动态路径匹配productKey 和 deviceName 是网关设备的标识
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapRegisterSubResource extends CoapResource {
public static final String PATH = "sub-device";
private final IotCoapRegisterSubHandler registerSubHandler;
/**
* 创建根资源(/auth/register/sub-device
*/
public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) {
this(PATH, registerSubHandler);
log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH);
}
/**
* 创建子资源(动态路径)
*/
private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) {
super(name);
this.registerSubHandler = registerSubHandler;
}
@Override
public Resource getChild(String name) {
// 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径
return new IotCoapRegisterSubResource(name, registerSubHandler);
}
@Override
public void handlePOST(CoapExchange exchange) {
log.debug("[handlePOST][收到子设备动态注册请求]");
registerSubHandler.handle(exchange);
}
}

View File

@@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
/**
* IoT 网关 CoAP 协议的【上行】处理器
*
* 处理设备通过 CoAP 协议发送的上行消息,包括:
* 1. 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 2. 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* Token 通过自定义 CoAP Option 2088 携带
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapUpstreamHandler extends IotCoapAbstractHandler {
private final String serverId;
private final IotDeviceMessageService deviceMessageService;
public IotCoapUpstreamHandler(String serverId) {
this.serverId = serverId;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1.1 解析通用参数
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
// 1.2 解析消息
IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class);
Assert.notNull(message, "请求参数不能为空");
Assert.equals(method, message.getMethod(), "method 不匹配");
// 2. 发送消息
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
// 3. 返回结果
return CommonResult.success(MapUtil.of("messageId", message.getId()));
}
@Override
protected boolean requiresAuthentication() {
return true;
}
@Override
protected String getProductKey(List<String> uriPath) {
// 路径格式:/topic/sys/{productKey}/{deviceName}/...
return CollUtil.get(uriPath, 2);
}
@Override
protected String getDeviceName(List<String> uriPath) {
// 路径格式:/topic/sys/{productKey}/{deviceName}/...
return CollUtil.get(uriPath, 3);
}
}

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
@@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
public static final String PATH = "topic";
private final IotCoapUpstreamProtocol protocol;
private final String serverId;
private final IotCoapUpstreamHandler upstreamHandler;
/**
* 创建根资源/topic
*/
public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
public IotCoapUpstreamTopicResource(String serverId,
IotCoapUpstreamHandler upstreamHandler) {
this(PATH, protocol, upstreamHandler);
this(PATH, serverId, upstreamHandler);
log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
}
@@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
* 创建子资源动态路径
*/
private IotCoapUpstreamTopicResource(String name,
IotCoapUpstreamProtocol protocol,
String serverId,
IotCoapUpstreamHandler upstreamHandler) {
super(name);
this.protocol = protocol;
this.serverId = serverId;
this.upstreamHandler = upstreamHandler;
}
@Override
public Resource getChild(String name) {
// 递归创建动态子资源支持任意深度路径
return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler);
}
@Override
public void handleGET(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
upstreamHandler.handle(exchange);
}
@Override
public void handlePOST(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
upstreamHandler.handle(exchange);
}
@Override
public void handlePUT(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
upstreamHandler.handle(exchange);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,104 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 EMQX 认证事件协议服务
* <p>
* 为 EMQX 提供 HTTP 接口服务,包括:
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
*
* @author 芋道源码
*/
@Slf4j
public class IotEmqxAuthEventProtocol {
private final IotGatewayProperties.EmqxProperties emqxProperties;
private final String serverId;
private final Vertx vertx;
private HttpServer httpServer;
public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
Vertx vertx) {
this.emqxProperties = emqxProperties;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
}
@PostConstruct
public void start() {
try {
startHttpServer();
log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
} catch (Exception e) {
log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
stopHttpServer();
log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
}
/**
* 启动 HTTP 服务器
*/
private void startHttpServer() {
int port = emqxProperties.getHttpPort();
// 1. 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 2. 创建处理器,传入 serverId
IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
// TODO @haohao/mqtt/acl 需要处理么?
// TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理
// 3. 启动 HTTP 服务器
try {
httpServer = vertx.createHttpServer()
.requestHandler(router)
.listen(port)
.result();
} catch (Exception e) {
log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e);
throw e;
}
}
/**
* 停止 HTTP 服务器
*/
private void stopHttpServer() {
if (httpServer == null) {
return;
}
try {
httpServer.close().result();
log.info("[stopHttpServer][HTTP 服务器已停止]");
} catch (Exception e) {
log.error("[stopHttpServer][HTTP 服务器停止失败]", e);
}
}
}

View File

@@ -0,0 +1,225 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* IoT EMQX 协议配置
*
* @author 芋道源码
*/
@Data
public class IotEmqxConfig {
// ========== MQTT Client 配置(连接 EMQX Broker ==========
/**
* MQTT 服务器地址
*/
@NotEmpty(message = "MQTT 服务器地址不能为空")
private String mqttHost;
/**
* MQTT 服务器端口默认1883
*/
@NotNull(message = "MQTT 服务器端口不能为空")
private Integer mqttPort = 1883;
/**
* MQTT 用户名
*/
@NotEmpty(message = "MQTT 用户名不能为空")
private String mqttUsername;
/**
* MQTT 密码
*/
@NotEmpty(message = "MQTT 密码不能为空")
private String mqttPassword;
/**
* MQTT 客户端的 SSL 开关
*/
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
private Boolean mqttSsl = false;
/**
* MQTT 客户端 ID
*/
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
private String mqttClientId;
/**
* MQTT 订阅的主题
*/
@NotEmpty(message = "MQTT 主题不能为空")
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
/**
* 默认 QoS 级别
* <p>
* 0 - 最多一次
* 1 - 至少一次
* 2 - 刚好一次
*/
@NotNull(message = "MQTT QoS 不能为空")
@Min(value = 0, message = "MQTT QoS 不能小于 0")
@Max(value = 2, message = "MQTT QoS 不能大于 2")
private Integer mqttQos = 1;
/**
* 连接超时时间(秒)
*/
@NotNull(message = "连接超时时间不能为空")
@Min(value = 1, message = "连接超时时间不能小于 1 秒")
private Integer connectTimeoutSeconds = 10;
/**
* 重连延迟时间(毫秒)
*/
@NotNull(message = "重连延迟时间不能为空")
@Min(value = 0, message = "重连延迟时间不能小于 0 毫秒")
private Long reconnectDelayMs = 5000L;
/**
* 是否启用 Clean Session (清理会话)
* true: 每次连接都是新会话Broker 不保留离线消息和订阅关系。
* 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
*/
@NotNull(message = "是否启用 Clean Session 不能为空")
private Boolean cleanSession = true;
/**
* 心跳间隔(秒)
* 用于保持连接活性,及时发现网络中断。
*/
@NotNull(message = "心跳间隔不能为空")
@Min(value = 1, message = "心跳间隔不能小于 1 秒")
private Integer keepAliveIntervalSeconds = 60;
/**
* 最大未确认消息队列大小
* 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
*/
@NotNull(message = "最大未确认消息队列大小不能为空")
@Min(value = 1, message = "最大未确认消息队列大小不能小于 1")
private Integer maxInflightQueue = 10000;
/**
* 是否信任所有 SSL 证书
* 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
* 在生产环境中,应设置为 false并配置正确的信任库。
*/
@NotNull(message = "是否信任所有 SSL 证书不能为空")
private Boolean trustAll = false;
// ========== MQTT Will / SSL 高级配置 ==========
/**
* 遗嘱消息配置 (用于网关异常下线时通知其他系统)
*/
@Valid
private Will will = new Will();
/**
* 高级 SSL/TLS 配置 (用于生产环境)
*/
@Valid
private Ssl sslOptions = new Ssl();
// ========== HTTP Hook 配置(网关提供给 EMQX 调用) ==========
/**
* HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event
*/
@Valid
private Http http = new Http();
/**
* 遗嘱消息 (Last Will and Testament)
*/
@Data
public static class Will {
/**
* 是否启用遗嘱消息
*/
private boolean enabled = false;
/**
* 遗嘱消息主题
*/
private String topic;
/**
* 遗嘱消息内容
*/
private String payload;
/**
* 遗嘱消息 QoS 等级
*/
@Min(value = 0, message = "遗嘱消息 QoS 不能小于 0")
@Max(value = 2, message = "遗嘱消息 QoS 不能大于 2")
private Integer qos = 1;
/**
* 遗嘱消息是否作为保留消息发布
*/
private boolean retain = true;
}
/**
* 高级 SSL/TLS 配置
*/
@Data
public static class Ssl {
/**
* 密钥库KeyStore路径例如classpath:certs/client.jks
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
*/
private String keyStorePath;
/**
* 密钥库密码
*/
private String keyStorePassword;
/**
* 信任库TrustStore路径例如classpath:certs/trust.jks
* 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
*/
private String trustStorePath;
/**
* 信任库密码
*/
private String trustStorePassword;
}
/**
* HTTP Hook 服务 SSL 配置
*/
@Data
public static class Http {
/**
* 是否启用 SSL
*/
private Boolean sslEnabled = false;
/**
* SSL 证书路径
*/
private String sslCertPath;
/**
* SSL 私钥路径
*/
private String sslKeyPath;
}
}

View File

@@ -0,0 +1,532 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* IoT 网关 EMQX 协议实现:
* <p>
* 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event给 EMQX 调用
* 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker
*
* @author 芋道源码
*/
@Slf4j
public class IotEmqxProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* EMQX 配置
*/
private final IotEmqxConfig emqxConfig;
/**
* 服务器 ID
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private Vertx vertx;
/**
* HTTP Hook 服务器
*/
private HttpServer httpServer;
/**
* MQTT Client
*/
private volatile MqttClient mqttClient;
/**
* MQTT 重连定时器 ID
*/
private volatile Long reconnectTimerId;
/**
* 上行消息处理器
*/
private final IotEmqxUpstreamHandler upstreamHandler;
/**
* 下行消息订阅者
*/
private IotEmqxDownstreamSubscriber downstreamSubscriber;
public IotEmqxProtocol(ProtocolProperties properties) {
Assert.notNull(properties, "协议实例配置不能为空");
Assert.notNull(properties.getEmqx(), "EMQX 协议配置emqx不能为空");
this.properties = properties;
this.emqxConfig = properties.getEmqx();
Assert.notNull(emqxConfig.getConnectTimeoutSeconds(),
"MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.EMQX;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId());
return;
}
// 1.1 创建 Vertx 实例 和 下行消息订阅者
this.vertx = Vertx.vertx();
try {
// 1.2 启动 HTTP Hook 服务
startHttpServer();
// 1.3 启动 MQTT Client
startMqttClient();
running = true;
log.info("[start][IoT EMQX 协议 {} 启动成功hookPort{}serverId{}]",
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
// 启动失败时,关闭资源
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 先置为 false避免 closeHandler 触发重连
running = false;
stopMqttClientReconnectChecker();
// 2.2 停止 MQTT Client
stopMqttClient();
// 2.3 停止 HTTP Hook 服务
stopHttpServer();
// 2.4 关闭 Vertx
if (vertx != null) {
try {
vertx.close().toCompletionStage().toCompletableFuture()
.get(10, TimeUnit.SECONDS);
log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e);
}
vertx = null;
}
log.info("[stop][IoT EMQX 协议 {} 已停止]", getId());
}
// ======================================= HTTP Hook Server =======================================
/**
* 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event
*/
private void startHttpServer() {
// 1. 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB防止大包攻击
// 2. 创建处理器
IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this);
router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl);
router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
// 3. 启动 HTTP Server支持 HTTPS
IotEmqxConfig.Http httpConfig = emqxConfig.getHttp();
HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空");
Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空");
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(httpConfig.getSslKeyPath())
.setCertPath(httpConfig.getSslCertPath());
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
try {
httpServer = vertx.createHttpServer(options)
.requestHandler(router)
.listen()
.toCompletionStage().toCompletableFuture()
.get(10, TimeUnit.SECONDS);
log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]",
getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled()));
} catch (Exception e) {
log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e);
throw new RuntimeException("HTTP Hook 服务启动失败", e);
}
}
private void stopHttpServer() {
if (httpServer == null) {
return;
}
try {
httpServer.close().toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId());
} catch (Exception e) {
log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e);
} finally {
httpServer = null;
}
}
// ======================================= MQTT Client ======================================
private void startMqttClient() {
// 1.1 创建 MQTT Client
MqttClient client = createMqttClient();
this.mqttClient = client;
// 1.2 连接 MQTT Broker
if (!connectMqttClient(client)) {
throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败");
}
// 2. 启动定时重连检查
startMqttClientReconnectChecker();
}
private void stopMqttClient() {
MqttClient client = this.mqttClient;
this.mqttClient = null; // 先清理引用
if (client == null) {
return;
}
// 1. 批量取消订阅(仅在连接时)
if (client.isConnected()) {
List<String> topicList = emqxConfig.getMqttTopics();
if (CollUtil.isNotEmpty(topicList)) {
try {
client.unsubscribe(topicList).toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e);
}
}
}
// 2. 断开 MQTT 连接
try {
client.disconnect().toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e);
}
}
// ======================================= MQTT 基础方法 ======================================
/**
* 创建 MQTT 客户端
*
* @return 新创建的 MqttClient
*/
private MqttClient createMqttClient() {
// 1.1 基础配置
MqttClientOptions options = new MqttClientOptions()
.setClientId(emqxConfig.getMqttClientId())
.setUsername(emqxConfig.getMqttUsername())
.setPassword(emqxConfig.getMqttPassword())
.setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl()))
.setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession()))
.setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds())
.setMaxInflightQueue(emqxConfig.getMaxInflightQueue());
options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒
options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll()));
// 1.2 配置遗嘱消息
IotEmqxConfig.Will will = emqxConfig.getWill();
if (will != null && will.isEnabled()) {
Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空");
Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空");
options.setWillFlag(true)
.setWillTopic(will.getTopic())
.setWillMessageBytes(Buffer.buffer(will.getPayload()))
.setWillQoS(will.getQos())
.setWillRetain(will.isRetain());
}
// 1.3 配置高级 SSL/TLS仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空)
IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions();
if (Boolean.TRUE.equals(emqxConfig.getMqttSsl())
&& Boolean.FALSE.equals(emqxConfig.getTrustAll())
&& sslOptions != null) {
if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
options.setTrustStoreOptions(new JksOptions()
.setPath(sslOptions.getTrustStorePath())
.setPassword(sslOptions.getTrustStorePassword()));
}
if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
options.setKeyStoreOptions(new JksOptions()
.setPath(sslOptions.getKeyStorePath())
.setPassword(sslOptions.getKeyStorePassword()));
}
}
// 2. 创建客户端
return MqttClient.create(vertx, options);
}
/**
* 连接 MQTT Broker同步等待
*
* @param client MQTT 客户端
* @return 连接成功返回 true失败返回 false
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private synchronized boolean connectMqttClient(MqttClient client) {
String host = emqxConfig.getMqttHost();
int port = emqxConfig.getMqttPort();
int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds();
try {
// 1. 连接 Broker
client.connect(port, host).toCompletionStage().toCompletableFuture()
.get(timeoutSeconds, TimeUnit.SECONDS);
log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]",
getId(), host, port);
// 2. 设置处理器
setupMqttClientHandlers(client);
subscribeMqttClientTopics(client);
return true;
} catch (Exception e) {
log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e);
return false;
}
}
/**
* 关闭 MQTT 客户端
*/
private void closeMqttClient() {
MqttClient oldClient = this.mqttClient;
this.mqttClient = null; // 先清理引用
if (oldClient == null) {
return;
}
// 尽力释放(无论是否连接都尝试 disconnect
try {
oldClient.disconnect().toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
}
}
// ======================================= MQTT 重连机制 ======================================
/**
* 启动 MQTT Client 周期性重连检查器
*/
private void startMqttClientReconnectChecker() {
long interval = emqxConfig.getReconnectDelayMs();
this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> {
if (!running) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId());
// 用 executeBlocking 避免阻塞 event-looptryReconnectMqttClient 内部有同步等待)
vertx.executeBlocking(() -> {
tryReconnectMqttClient();
return null;
});
});
}
/**
* 停止 MQTT Client 重连检查器
*/
private void stopMqttClientReconnectChecker() {
if (reconnectTimerId != null && vertx != null) {
try {
vertx.cancelTimer(reconnectTimerId);
} catch (Exception ignored) {
}
reconnectTimerId = null;
}
}
/**
* 尝试重连 MQTT Client
*/
private synchronized void tryReconnectMqttClient() {
// 1. 前置检查
if (!running) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId());
try {
// 2. 关闭旧客户端
closeMqttClient();
// 3.1 创建新客户端
MqttClient client = createMqttClient();
this.mqttClient = client;
// 3.2 连接(失败只打印日志,等下次定时)
if (!connectMqttClient(client)) {
log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId());
}
} catch (Exception e) {
log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e);
}
}
// ======================================= MQTT Handler ======================================
/**
* 设置 MQTT Client 事件处理器
*/
private void setupMqttClientHandlers(MqttClient client) {
// 1. 断开重连监听
client.closeHandler(closeEvent -> {
if (!running) {
return;
}
log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId());
// 用 executeBlocking 避免阻塞 event-looptryReconnectMqttClient 内部有同步等待)
vertx.executeBlocking(() -> {
tryReconnectMqttClient();
return null;
});
});
// 2. 异常处理
client.exceptionHandler(exception ->
log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception));
// 3. 上行消息处理
client.publishHandler(upstreamHandler::handle);
}
/**
* 订阅 MQTT Client 主题(同步等待)
*/
private void subscribeMqttClientTopics(MqttClient client) {
List<String> topicList = emqxConfig.getMqttTopics();
if (!client.isConnected()) {
log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId());
return;
}
if (CollUtil.isEmpty(topicList)) {
log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId());
return;
}
// 执行订阅
Map<String, Integer> topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic,
topic -> emqxConfig.getMqttQos());
try {
client.subscribe(topics).toCompletionStage().toCompletableFuture()
.get(10, TimeUnit.SECONDS);
log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size());
} catch (Exception e) {
log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e);
}
}
/**
* 发布消息到 MQTT Broker
*
* @param topic 主题
* @param payload 消息内容
*/
public void publishMessage(String topic, byte[] payload) {
if (mqttClient == null || !mqttClient.isConnected()) {
log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId());
return;
}
MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos());
mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false)
.onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e));
}
/**
* 延迟发布消息到 MQTT Broker
*
* @param topic 主题
* @param payload 消息内容
* @param delayMs 延迟时间(毫秒)
*/
public void publishDelayMessage(String topic, byte[] payload, long delayMs) {
vertx.setTimer(delayMs, id -> publishMessage(topic, payload));
}
}

View File

@@ -1,365 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.JksOptions;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* IoT 网关 EMQX 协议:接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotEmqxUpstreamProtocol {
private final IotGatewayProperties.EmqxProperties emqxProperties;
private volatile boolean isRunning = false;
private final Vertx vertx;
@Getter
private final String serverId;
private MqttClient mqttClient;
private IotEmqxUpstreamHandler upstreamHandler;
public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
Vertx vertx) {
this.emqxProperties = emqxProperties;
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
this.vertx = vertx;
}
@PostConstruct
public void start() {
if (isRunning) {
return;
}
try {
// 1. 启动 MQTT 客户端
startMqttClient();
// 2. 标记服务为运行状态
isRunning = true;
log.info("[start][IoT 网关 EMQX 协议启动成功]");
} catch (Exception e) {
log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
stop();
// 异步关闭应用
Thread shutdownThread = new Thread(() -> {
try {
// 确保日志输出完成,使用更优雅的方式
log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
// 等待日志输出完成
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.warn("[start][应用关闭被中断]");
}
System.exit(1);
});
shutdownThread.setDaemon(true);
shutdownThread.setName("emergency-shutdown");
shutdownThread.start();
throw e;
}
}
@PreDestroy
public void stop() {
if (!isRunning) {
return;
}
// 1. 停止 MQTT 客户端
stopMqttClient();
// 2. 标记服务为停止状态
isRunning = false;
log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
}
/**
* 启动 MQTT 客户端
*/
private void startMqttClient() {
try {
// 1. 初始化消息处理器
this.upstreamHandler = new IotEmqxUpstreamHandler(this);
// 2. 创建 MQTT 客户端
createMqttClient();
// 3. 同步连接 MQTT Broker
connectMqttSync();
} catch (Exception e) {
log.error("[startMqttClient][MQTT 客户端启动失败]", e);
throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e);
}
}
/**
* 同步连接 MQTT Broker
*/
private void connectMqttSync() {
String host = emqxProperties.getMqttHost();
int port = emqxProperties.getMqttPort();
// 1. 连接 MQTT Broker
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean success = new AtomicBoolean(false);
mqttClient.connect(port, host, connectResult -> {
if (connectResult.succeeded()) {
log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
setupMqttHandlers();
subscribeToTopics();
success.set(true);
} else {
log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
host, port, connectResult.cause());
}
latch.countDown();
});
// 2. 等待连接结果
try {
// 应用层超时控制防止启动过程无限阻塞与MQTT客户端的网络超时是不同层次的控制
boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
if (!awaitResult) {
log.error("[connectMqttSync][等待连接结果超时]");
throw new RuntimeException("连接 MQTT Broker 超时");
}
if (!success.get()) {
throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("[connectMqttSync][等待连接结果被中断]", e);
throw new RuntimeException("连接 MQTT Broker 被中断", e);
}
}
/**
* 异步连接 MQTT Broker
*/
private void connectMqttAsync() {
String host = emqxProperties.getMqttHost();
int port = emqxProperties.getMqttPort();
mqttClient.connect(port, host, connectResult -> {
if (connectResult.succeeded()) {
log.info("[connectMqttAsync][MQTT 客户端重连成功]");
setupMqttHandlers();
subscribeToTopics();
} else {
log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]",
host, port, connectResult.cause());
log.warn("[connectMqttAsync][重连失败,将再次尝试]");
reconnectWithDelay();
}
});
}
/**
* 延迟重连
*/
private void reconnectWithDelay() {
if (!isRunning) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
long delay = emqxProperties.getReconnectDelayMs();
log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay);
vertx.setTimer(delay, timerId -> {
if (!isRunning) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
log.info("[reconnectWithDelay][开始重连 MQTT Broker]");
try {
createMqttClient();
connectMqttAsync();
} catch (Exception e) {
log.error("[reconnectWithDelay][重连过程中发生异常]", e);
vertx.setTimer(delay, t -> reconnectWithDelay());
}
});
}
/**
* 停止 MQTT 客户端
*/
private void stopMqttClient() {
if (mqttClient == null) {
return;
}
try {
if (mqttClient.isConnected()) {
// 1. 取消订阅所有主题
List<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);
}
}

View File

@@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
@@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class IotEmqxDownstreamHandler {
private final IotEmqxUpstreamProtocol protocol;
private final IotEmqxProtocol protocol;
private final IotDeviceService deviceService;
private final IotDeviceMessageService deviceMessageService;
public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) {
this.protocol = protocol;
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
@@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler {
return;
}
// 2.2 构建载荷
byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
deviceInfo.getDeviceName());
// 2.3 发布消息
// 3. 发布消息
protocol.publishMessage(topic, payload);
}
@@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler {
return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
}
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 EMQX 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotEmqxDownstreamHandler downstreamHandler;
public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@@ -1,25 +1,35 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;
import java.util.Locale;
/**
* IoT 网关 EMQX 认证事件处理器
* <p>
* EMQX 提供 HTTP 接口服务包括
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
* 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)}
* 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)}
* 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)}
* 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)}
*
* @author 芋道源码
*/
@@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler {
private static final String RESULT_IGNORE = "ignore";
/**
* EMQX 事件类型常量
* EMQX 事件类型常量 - 客户端连接
*/
private static final String EVENT_CLIENT_CONNECTED = "client.connected";
/**
* EMQX 事件类型常量 - 客户端断开连接
*/
private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
/**
* 认证类型标识 - 设备注册
*/
private static final String AUTH_TYPE_REGISTER = "|authType=register|";
private final String serverId;
private final IotDeviceMessageService deviceMessageService;
private final IotEmqxProtocol protocol;
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceCommonApi deviceApi;
public IotEmqxAuthEventHandler(String serverId) {
public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
this.serverId = serverId;
this.protocol = protocol;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
// ========== 认证处理 ==========
/**
* EMQX 认证接口
*/
public void handleAuth(RoutingContext context) {
JsonObject body = null;
try {
// 1. 参数校验
JsonObject body = parseRequestBody(context);
body = parseRequestBody(context);
if (body == null) {
return;
}
@@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler {
return;
}
// 2. 执行认证
// 2.1 情况一判断是否为注册请求
if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
handleDeviceRegister(context, username, password);
return;
}
// 2.2 情况二执行认证
boolean authResult = handleDeviceAuth(clientId, username, password);
log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
if (authResult) {
@@ -91,11 +120,179 @@ public class IotEmqxAuthEventHandler {
sendAuthResponse(context, RESULT_DENY);
}
} catch (Exception e) {
log.error("[handleAuth][设备认证异常]", e);
log.error("[handleAuth][设备认证异常][body={}]", body, e);
sendAuthResponse(context, RESULT_IGNORE);
}
}
/**
* 解析认证接口请求体
* <p>
* 认证接口解析失败时返回 JSON 格式响应包含 result 字段
*
* @param context 路由上下文
* @return 请求体JSON对象解析失败时返回null
*/
private JsonObject parseRequestBody(RoutingContext context) {
try {
JsonObject body = context.body().asJsonObject();
if (body == null) {
log.info("[parseRequestBody][请求体为空]");
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
return body;
} catch (Exception e) {
log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
}
/**
* 执行设备认证
*
* @param clientId 客户端ID
* @param username 用户名
* @param password 密码
* @return 认证是否成功
*/
private boolean handleDeviceAuth(String clientId, String username, String password) {
try {
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
result.checkError();
return BooleanUtil.isTrue(result.getData());
} catch (Exception e) {
log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
throw e;
}
}
/**
* 发送 EMQX 认证响应
* 根据 EMQX 官方文档要求必须返回 JSON 格式响应
*
* @param context 路由上下文
* @param result 认证结果allowdenyignore
*/
private void sendAuthResponse(RoutingContext context, String result) {
// 构建符合 EMQX 官方规范的响应
JsonObject response = new JsonObject()
.put("result", result)
.put("is_superuser", false);
// 可以根据业务需求添加客户端属性
// response.put("client_attrs", new JsonObject().put("role", "device"));
// 可以添加认证过期时间可选
// response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
// 回复响应
context.response()
.setStatusCode(SUCCESS_STATUS_CODE)
.putHeader("Content-Type", "application/json; charset=utf-8")
.end(response.encode());
}
// ========== ACL 处理 ==========
/**
* EMQX ACL 接口
* <p>
* 用于 EMQX HTTP ACL 插件校验设备的 publish/subscribe 权限
* 若请求参数无法识别则返回 ignore 交给 EMQX 自身 ACL 规则处理
*/
public void handleAcl(RoutingContext context) {
JsonObject body = null;
try {
// 1.1 解析请求体
body = parseRequestBody(context);
if (body == null) {
return;
}
String username = body.getString("username");
String topic = body.getString("topic");
if (StrUtil.hasBlank(username, topic)) {
log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic);
sendAuthResponse(context, RESULT_IGNORE);
return;
}
// 1.2 解析设备身份
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
sendAuthResponse(context, RESULT_IGNORE);
return;
}
// 1.3 解析 ACL 动作兼容多种 EMQX 版本/插件字段
Boolean subscribe = parseAclSubscribeFlag(body);
if (subscribe == null) {
sendAuthResponse(context, RESULT_IGNORE);
return;
}
// 2. 执行 ACL 校验
boolean allowed = subscribe
? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName())
: IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY);
} catch (Exception e) {
log.error("[handleAcl][ACL 处理失败][body={}]", body, e);
sendAuthResponse(context, RESULT_IGNORE);
}
}
/**
* 解析 ACL 动作类型订阅/发布
*
* @param body ACL 请求体
* @return true 订阅false 发布null 不识别
*/
private static Boolean parseAclSubscribeFlag(JsonObject body) {
// 1. action 字段常见为 publish/subscribe
String action = body.getString("action");
if (StrUtil.isNotBlank(action)) {
String lower = action.toLowerCase(Locale.ROOT);
if (lower.contains("sub")) {
return true;
}
if (lower.contains("pub")) {
return false;
}
}
// 2. access 字段可能是数字或字符串
Integer access = body.getInteger("access");
if (access != null) {
if (access == 1) {
return true;
}
if (access == 2) {
return false;
}
}
String accessText = body.getString("access");
if (StrUtil.isNotBlank(accessText)) {
String lower = accessText.toLowerCase(Locale.ROOT);
if (lower.contains("sub")) {
return true;
}
if (lower.contains("pub")) {
return false;
}
if (StrUtil.isNumeric(accessText)) {
int value = Integer.parseInt(accessText);
if (value == 1) {
return true;
}
if (value == 2) {
return false;
}
}
}
return null;
}
// ========== 事件处理 ==========
/**
* EMQX 统一事件处理接口根据 EMQX 官方 Webhook 设计统一处理所有客户端事件
* 支持的事件类型client.connectedclient.disconnected
@@ -124,58 +321,15 @@ public class IotEmqxAuthEventHandler {
break;
}
// EMQX Webhook 只需要 200 状态码无需响应体
// 3. EMQX Webhook 只需要 200 状态码无需响应体
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
} catch (Exception e) {
log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
// 即使处理失败也返回 200 避免EMQX重试
log.error("[handleEvent][事件处理失败][body={}]", body, e);
// 即使处理失败也返回 200 避免 EMQX 重试
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
}
}
/**
* 处理客户端连接事件
*/
private void handleClientConnected(JsonObject body) {
String username = body.getString("username");
log.info("[handleClientConnected][设备上线: {}]", username);
handleDeviceStateChange(username, true);
}
/**
* 处理客户端断开连接事件
*/
private void handleClientDisconnected(JsonObject body) {
String username = body.getString("username");
String reason = body.getString("reason");
log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
handleDeviceStateChange(username, false);
}
/**
* 解析认证接口请求体
* <p>
* 认证接口解析失败时返回 JSON 格式响应包含 result 字段
*
* @param context 路由上下文
* @return 请求体JSON对象解析失败时返回null
*/
private JsonObject parseRequestBody(RoutingContext context) {
try {
JsonObject body = context.body().asJsonObject();
if (body == null) {
log.info("[parseRequestBody][请求体为空]");
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
return body;
} catch (Exception e) {
log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
}
/**
* 解析事件接口请求体
* <p>
@@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler {
}
/**
* 执行设备认证
*
* @param clientId 客户端ID
* @param username 用户名
* @param password 密码
* @return 认证是否成功
* 处理客户端连接事件
*/
private boolean handleDeviceAuth(String clientId, String username, String password) {
try {
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
result.checkError();
return BooleanUtil.isTrue(result.getData());
} catch (Exception e) {
log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
throw e;
}
private void handleClientConnected(JsonObject body) {
String username = body.getString("username");
log.info("[handleClientConnected][设备上线: {}]", username);
handleDeviceStateChange(username, true);
}
/**
* 处理客户端断开连接事件
*/
private void handleClientDisconnected(JsonObject body) {
String username = body.getString("username");
String reason = body.getString("reason");
log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
handleDeviceStateChange(username, false);
}
/**
@@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler {
}
}
// ========= 注册处理 =========
/**
* 发送 EMQX 认证响应
* 根据 EMQX 官方文档要求必须返回 JSON 格式响应
* 处理设备注册请求一型一密
*
* @param context 路由上下文
* @param result 认证结果allowdenyignore
* @param context 路由上下文
* @param username 用户名
* @param password 密码签名
*/
private void sendAuthResponse(RoutingContext context, String result) {
// 构建符合 EMQX 官方规范的响应
JsonObject response = new JsonObject()
.put("result", result)
.put("is_superuser", false);
private void handleDeviceRegister(RoutingContext context, String username, String password) {
try {
// 1. 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username);
sendAuthResponse(context, RESULT_DENY);
return;
}
// 可以根据业务需求添加客户端属性
// response.put("client_attrs", new JsonObject().put("role", "device"));
// 2. 调用注册 API
IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
.setProductKey(deviceInfo.getProductKey())
.setDeviceName(deviceInfo.getDeviceName())
.setSign(password);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
result.checkError();
// 可以添加认证过期时间可选
// response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
// 3. 允许连接
log.info("[handleDeviceRegister][设备注册成功: {}]", username);
sendAuthResponse(context, RESULT_ALLOW);
context.response()
.setStatusCode(SUCCESS_STATUS_CODE)
.putHeader("Content-Type", "application/json; charset=utf-8")
.end(response.encode());
// 4. 延迟 5 秒发送注册结果等待设备连接成功并完成订阅
sendRegisterResultMessage(username, result.getData());
} catch (Exception e) {
log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage());
sendAuthResponse(context, RESULT_DENY);
}
}
}
/**
* 发送注册结果消息给设备
* <p>
* 注意延迟 5 秒发送等待设备连接成功并完成订阅
*
* @param username 用户名
* @param result 注册结果
*/
@SuppressWarnings("DataFlowIssue")
private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) {
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
Assert.notNull(deviceInfo, "设备信息不能为空");
try {
// 1.1 构建响应消息
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null);
// 1.2 序列化消息
byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage,
cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON);
// 1.3 构建响应主题
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true);
// 2. 构建响应主题并延迟发布等待设备连接成功并完成订阅
protocol.publishDelayMessage(replyTopic, encodedData, 5000);
log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic);
} catch (Exception e) {
log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e);
}
}
}

View File

@@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.mqtt.messages.MqttPublishMessage;
import lombok.extern.slf4j.Slf4j;
@@ -20,41 +21,42 @@ public class IotEmqxUpstreamHandler {
private final String serverId;
public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
public IotEmqxUpstreamHandler(String serverId) {
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.serverId = protocol.getServerId();
this.serverId = serverId;
}
/**
* 处理 MQTT 发布消息
*/
public void handle(MqttPublishMessage mqttMessage) {
log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
String topic = mqttMessage.topicName();
byte[] payload = mqttMessage.payload().getBytes();
try {
// 1. 解析主题一次性获取所有信息
String[] topicParts = topic.split("/");
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
String productKey = ArrayUtil.get(topicParts, 2);
String deviceName = ArrayUtil.get(topicParts, 3);
if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) {
log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
return;
}
String productKey = topicParts[2];
String deviceName = topicParts[3];
// 3. 解码消息
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
// 2.1 反序列化消息
IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
if (message == null) {
log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
return;
}
// 2.2 标准化回复消息的 methodMQTT 协议中设备回复消息的 method 会携带 _reply 后缀
IotMqttTopicUtils.normalizeReplyMethod(message);
// 4. 发送消息到队列
// 3. 发送消息到队列
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
} catch (Exception e) {
log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
}
}
}
}

View File

@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import lombok.Data;
/**
* IoT HTTP 协议配置
*
* @author 芋道源码
*/
@Data
public class IotHttpConfig {
}

View File

@@ -1,45 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 HTTP 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotHttpDownstreamSubscriber implements IotMessageSubscriber<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);
}
}

View File

@@ -0,0 +1,176 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT HTTP 协议实现
* <p>
* 基于 Vert.x 实现 HTTP 服务器,接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotHttpProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private Vertx vertx;
/**
* HTTP 服务器
*/
private HttpServer httpServer;
/**
* 下行消息订阅者
*/
private IotHttpDownstreamSubscriber downstreamSubscriber;
public IotHttpProtocol(ProtocolProperties properties) {
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.HTTP;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId());
return;
}
// 1.1 创建 Vertx 实例
this.vertx = Vertx.vertx();
// 1.2 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 1.3 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
// 1.4 启动 HTTP 服务器
HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(sslConfig.getSslKeyPath())
.setCertPath(sslConfig.getSslCertPath());
options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
try {
httpServer = vertx.createHttpServer(options)
.requestHandler(router)
.listen()
.result();
running = true;
log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{}serverId{}]",
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 关闭 HTTP 服务器
if (httpServer != null) {
try {
httpServer.close().result();
log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e);
}
httpServer = null;
}
// 2.2 关闭 Vertx 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e);
}
vertx = null;
}
running = false;
log.info("[stop][IoT HTTP 协议 {} 已停止]", getId());
}
}

View File

@@ -1,91 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 HTTP 协议:接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotHttpUpstreamProtocol {
private final IotGatewayProperties.HttpProperties httpProperties;
private final Vertx vertx;
private HttpServer httpServer;
@Getter
private final String serverId;
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) {
this.httpProperties = httpProperties;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
}
@PostConstruct
public void start() {
// 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
// 启动 HTTP 服务器
HttpServerOptions options = new HttpServerOptions()
.setPort(httpProperties.getServerPort());
if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath())
.setCertPath(httpProperties.getSslCertPath());
options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
try {
httpServer = vertx.createHttpServer(options)
.requestHandler(router)
.listen()
.result();
log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort());
} catch (Exception e) {
log.error("[start][IoT 网关 HTTP 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (httpServer != null) {
try {
httpServer.close().result();
log.info("[stop][IoT 网关 HTTP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 HTTP 协议停止失败]", e);
}
}
}
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 HTTP 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message);
}
}

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
@@ -13,12 +14,10 @@ import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
@@ -27,7 +26,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> {
@@ -43,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
CommonResult<Object> result = handle0(context);
writeResponse(context, result);
} catch (ServiceException e) {
// 已知异常返回对应的错误码和错误信息
writeResponse(context, CommonResult.error(e.getCode(), e.getMessage()));
} catch (IllegalArgumentException e) {
// 参数校验异常返回 400 错误
writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
} catch (Exception e) {
// 其他未知异常返回 500 错误
log.error("[handle][path({}) 处理异常]", context.request().path(), e);
writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR));
}
}
/**
* 处理 HTTP 请求子类实现
*
* @param context RoutingContext 对象
* @return 处理结果
*/
protected abstract CommonResult<Object> handle0(RoutingContext context);
/**
* 前置处理认证等
*
* @param context RoutingContext 对象
*/
private void beforeHandle(RoutingContext context) {
// 如果不需要认证则不走前置处理
String path = context.request().path();
@@ -83,12 +97,26 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
}
}
// ========== 序列化相关方法 ==========
protected static <T> T deserializeRequest(RoutingContext context, Class<T> clazz) {
byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null;
if (ArrayUtil.isEmpty(body)) {
throw invalidParamException("请求体不能为空");
}
return JsonUtils.parseObject(body, clazz);
}
private static String serializeResponse(Object data) {
return JsonUtils.toJsonString(data);
}
@SuppressWarnings("deprecation")
public static void writeResponse(RoutingContext context, Object data) {
public static void writeResponse(RoutingContext context, CommonResult<?> data) {
context.response()
.setStatusCode(200)
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
.end(JsonUtils.toJsonString(data));
.end(serializeResponse(data));
}
}

View File

@@ -1,23 +1,20 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
@@ -32,7 +29,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
public static final String PATH = "/auth";
private final IotHttpUpstreamProtocol protocol;
private final String serverId;
private final IotDeviceTokenService deviceTokenService;
@@ -40,42 +37,31 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
private final IotDeviceMessageService deviceMessageService;
public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) {
this.protocol = protocol;
public IotHttpAuthHandler(IotHttpProtocol protocol) {
this.serverId = protocol.getServerId();
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
String clientId = body.getString("clientId");
if (StrUtil.isEmpty(clientId)) {
throw invalidParamException("clientId 不能为空");
}
String username = body.getString("username");
if (StrUtil.isEmpty(username)) {
throw invalidParamException("username 不能为空");
}
String password = body.getString("password");
if (StrUtil.isEmpty(password)) {
throw invalidParamException("password 不能为空");
}
IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notBlank(request.getClientId(), "clientId 不能为空");
Assert.notBlank(request.getUsername(), "username 不能为空");
Assert.notBlank(request.getPassword(), "password 不能为空");
// 2.1 执行认证
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
CommonResult<Boolean> result = deviceApi.authDevice(request);
result.checkError();
if (!BooleanUtil.isTrue(result.getData())) {
if (BooleanUtil.isFalse(result.getData())) {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空位");
@@ -83,7 +69,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
// 3. 执行上线
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(message,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
// 构建响应数据
return success(MapUtil.of("token", token));

View File

@@ -1,15 +1,13 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@@ -33,27 +31,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
String productKey = body.getString("productKey");
if (StrUtil.isEmpty(productKey)) {
throw invalidParamException("productKey 不能为空");
}
String deviceName = body.getString("deviceName");
if (StrUtil.isEmpty(deviceName)) {
throw invalidParamException("deviceName 不能为空");
}
String productSecret = body.getString("productSecret");
if (StrUtil.isEmpty(productSecret)) {
throw invalidParamException("productSecret 不能为空");
}
IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notBlank(request.getProductKey(), "productKey 不能为空");
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(request.getSign(), "sign 不能为空");
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
result.checkError();
// 3. 返回结果

View File

@@ -1,17 +1,17 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.Data;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@@ -39,29 +39,31 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析通用参数
// 1.1 解析通用参数
String productKey = context.pathParam("productKey");
String deviceName = context.pathParam("deviceName");
// 1.2 解析子设备列表
SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notEmpty(request.getParams(), "params 不能为空");
// 2. 解析子设备列表
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
if (body.getJsonArray("params") == null) {
throw invalidParamException("params 不能为空");
}
List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
// 3. 调用子设备动态注册
// 2. 调用子设备动态注册
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
.setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
.setGatewayProductKey(productKey)
.setGatewayDeviceName(deviceName)
.setSubDevices(request.getParams());
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
result.checkError();
// 4. 返回结果
// 3. 返回结果
return success(result.getData());
}
@Data
public static class SubDeviceRegisterRequest {
private List<IotSubDeviceRegisterReqDTO> params;
}
}

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
@@ -6,55 +6,47 @@ import cn.hutool.core.text.StrPool;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* IoT 网关 HTTP 协议的上行处理器
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
public static final String PATH = "/topic/sys/:productKey/:deviceName/*";
private final IotHttpUpstreamProtocol protocol;
private final String serverId;
private final IotDeviceMessageService deviceMessageService;
public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) {
this.protocol = protocol;
public IotHttpUpstreamHandler(IotHttpProtocol protocol) {
this.serverId = protocol.getServerId();
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
protected CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析通用参数
// 1.1 解析通用参数
String productKey = context.pathParam("productKey");
String deviceName = context.pathParam("deviceName");
String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
// 2.1 解析消息
if (context.body().buffer() == null) {
throw invalidParamException("请求体不能为空");
}
byte[] bytes = context.body().buffer().getBytes();
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
productKey, deviceName);
// 1.2 根据 Content-Type 反序列化消息
IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class);
Assert.notNull(message, "请求参数不能为空");
Assert.equals(method, message.getMethod(), "method 不匹配");
// 2.2 发送消息
// 2. 发送消息
deviceMessageService.sendDeviceMessage(message,
productKey, deviceName, protocol.getServerId());
productKey, deviceName, serverId);
// 3. 返回结果
return CommonResult.success(MapUtil.of("messageId", message.getId()));
}
}
}

View File

@@ -0,0 +1,278 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import io.vertx.core.Vertx;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
* Modbus 轮询调度器基类
* <p>
* 封装通用的定时器管理、per-device 请求队列限速逻辑。
* 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。
* <p>
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractIotModbusPollScheduler {
protected final Vertx vertx;
/**
* 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
*/
private static final long MIN_REQUEST_INTERVAL = 1000;
/**
* 每个设备请求队列的最大长度,超出时丢弃最旧请求
*/
private static final int MAX_QUEUE_SIZE = 1000;
/**
* 设备点位的定时器映射deviceId -> (pointId -> PointTimerInfo)
*/
private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
/**
* per-device 请求队列deviceId -> 待执行请求队列
*/
private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>();
/**
* per-device 上次请求时间戳deviceId -> lastRequestTimeMs
*/
private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>();
/**
* per-device 延迟 timer 标记deviceId -> 是否有延迟 timer 在等待
*/
private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>();
protected AbstractIotModbusPollScheduler(Vertx vertx) {
this.vertx = vertx;
}
/**
* 点位定时器信息
*/
@Data
@AllArgsConstructor
private static class PointTimerInfo {
/**
* Vert.x 定时器 ID
*/
private Long timerId;
/**
* 轮询间隔(用于判断是否需要更新定时器)
*/
private Integer pollInterval;
}
// ========== 轮询管理 ==========
/**
* 更新轮询任务(增量更新)
*
* 1. 【删除】点位:停止对应的轮询定时器
* 2. 【新增】点位:创建对应的轮询定时器
* 3. 【修改】点位pollInterval 变化,重建对应的轮询定时器
* 【修改】其他属性变化不需要重建定时器pollPoint 运行时从 configCache 取最新 point
*/
public void updatePolling(IotModbusDeviceConfigRespDTO config) {
Long deviceId = config.getDeviceId();
List<IotModbusPointRespDTO> newPoints = config.getPoints();
Map<Long, PointTimerInfo> currentTimers = devicePointTimers
.computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
// 1.1 计算新配置中的点位 ID 集合
Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
// 1.2 计算删除的点位 ID 集合
Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
removedPointIds.removeAll(newPointIds);
// 2. 处理删除的点位:停止不再存在的定时器
for (Long pointId : removedPointIds) {
PointTimerInfo timerInfo = currentTimers.remove(pointId);
if (timerInfo != null) {
vertx.cancelTimer(timerInfo.getTimerId());
log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
}
}
// 3. 处理新增和修改的点位
if (CollUtil.isEmpty(newPoints)) {
return;
}
for (IotModbusPointRespDTO point : newPoints) {
Long pointId = point.getId();
Integer newPollInterval = point.getPollInterval();
PointTimerInfo existingTimer = currentTimers.get(pointId);
// 3.1 新增点位:创建定时器
if (existingTimer == null) {
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
deviceId, pointId, newPollInterval);
}
} else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
// 3.2 pollInterval 变化:重建定时器
vertx.cancelTimer(existingTimer.getTimerId());
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
} else {
currentTimers.remove(pointId);
}
}
// 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point自动使用新配置
}
}
/**
* 创建轮询定时器
*/
private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
if (pollInterval == null || pollInterval <= 0) {
return null;
}
return vertx.setPeriodic(pollInterval, timerId -> {
try {
submitPollRequest(deviceId, pointId);
} catch (Exception e) {
log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
}
});
}
// ========== 请求队列per-device 限速) ==========
/**
* 提交轮询请求到设备请求队列(保证同设备请求间隔)
*/
private void submitPollRequest(Long deviceId, Long pointId) {
// 1. 【重要】将请求添加到设备的请求队列
Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
while (queue.size() >= MAX_QUEUE_SIZE) {
// 超出上限时,丢弃最旧的请求
queue.poll();
log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
}
queue.offer(() -> pollPoint(deviceId, pointId));
// 2. 处理设备请求队列(如果没有延迟 timer 在等待)
processDeviceQueue(deviceId);
}
/**
* 处理设备请求队列
*/
private void processDeviceQueue(Long deviceId) {
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return;
}
// 检查是否已有延迟 timer 在等待
if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) {
return;
}
// 不满足间隔要求,延迟执行
long now = System.currentTimeMillis();
long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L);
long elapsed = now - lastTime;
if (elapsed < MIN_REQUEST_INTERVAL) {
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed);
return;
}
// 满足间隔要求,立即执行
Runnable task = queue.poll();
if (task == null) {
return;
}
deviceLastRequestTime.put(deviceId, now);
task.run();
// 继续处理队列中的下一个(如果有的话,需要延迟)
if (CollUtil.isNotEmpty(queue)) {
scheduleNextRequest(deviceId);
}
}
private void scheduleNextRequest(Long deviceId) {
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL);
}
private void scheduleNextRequest(Long deviceId, long delayMs) {
deviceDelayTimerActive.put(deviceId, true);
vertx.setTimer(delayMs, id -> {
deviceDelayTimerActive.put(deviceId, false);
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return;
}
// 满足间隔要求,立即执行
Runnable task = queue.poll();
if (task == null) {
return;
}
deviceLastRequestTime.put(deviceId, System.currentTimeMillis());
task.run();
// 继续处理队列中的下一个(如果有的话,需要延迟)
if (CollUtil.isNotEmpty(queue)) {
scheduleNextRequest(deviceId);
}
});
}
// ========== 轮询执行 ==========
/**
* 轮询单个点位(子类实现具体的读取逻辑)
*
* @param deviceId 设备 ID
* @param pointId 点位 ID
*/
protected abstract void pollPoint(Long deviceId, Long pointId);
// ========== 停止 ==========
/**
* 停止设备的轮询
*/
public void stopPolling(Long deviceId) {
Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
if (CollUtil.isEmpty(timers)) {
return;
}
for (PointTimerInfo timerInfo : timers.values()) {
vertx.cancelTimer(timerInfo.getTimerId());
}
// 清理请求队列
deviceRequestQueues.remove(deviceId);
deviceLastRequestTime.remove(deviceId);
deviceDelayTimerActive.remove(deviceId);
log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
}
/**
* 停止所有轮询
*/
public void stopAll() {
for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
stopPolling(deviceId);
}
}
}

View File

@@ -0,0 +1,557 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* IoT Modbus 协议工具类
* <p>
* 提供 Modbus 协议全链路能力:
* <ul>
* <li>协议常量功能码FC01~FC16、异常掩码等</li>
* <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li>
* <li>CRC-16/MODBUS 计算和校验</li>
* <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues}</li>
* <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues}</li>
* <li>点位查找({@link #findPoint}</li>
* </ul>
*
* @author 芋道源码
*/
@UtilityClass
@Slf4j
public class IotModbusCommonUtils {
/** FC01: 读线圈 */
public static final int FC_READ_COILS = 1;
/** FC02: 读离散输入 */
public static final int FC_READ_DISCRETE_INPUTS = 2;
/** FC03: 读保持寄存器 */
public static final int FC_READ_HOLDING_REGISTERS = 3;
/** FC04: 读输入寄存器 */
public static final int FC_READ_INPUT_REGISTERS = 4;
/** FC05: 写单个线圈 */
public static final int FC_WRITE_SINGLE_COIL = 5;
/** FC06: 写单个寄存器 */
public static final int FC_WRITE_SINGLE_REGISTER = 6;
/** FC15: 写多个线圈 */
public static final int FC_WRITE_MULTIPLE_COILS = 15;
/** FC16: 写多个寄存器 */
public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
/**
* 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应
* 例如:请求 FC=0x03异常响应 FC=0x830x03 | 0x80
*/
public static final int FC_EXCEPTION_MASK = 0x80;
/**
* 功能码掩码:用于从异常响应中提取原始功能码
* 例如:异常 FC=0x83原始 FC = 0x83 & 0x7F = 0x03
*/
public static final int FC_MASK = 0x7F;
// ==================== 功能码分类判断 ====================
/**
* 判断是否为读响应FC01-04
*/
public static boolean isReadResponse(int functionCode) {
return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
}
/**
* 判断是否为写响应FC05/06/15/16
*/
public static boolean isWriteResponse(int functionCode) {
return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
|| functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
}
/**
* 判断是否为异常响应
*/
public static boolean isExceptionResponse(int functionCode) {
return (functionCode & FC_EXCEPTION_MASK) != 0;
}
/**
* 从异常响应中提取原始功能码
*/
public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
return exceptionFunctionCode & FC_MASK;
}
/**
* 判断读功能码是否支持写操作
* <p>
* FC01读线圈和 FC03读保持寄存器支持写操作
* FC02读离散输入和 FC04读输入寄存器为只读。
*
* @param readFunctionCode 读功能码FC01-04
* @return 是否支持写操作
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isWritable(int readFunctionCode) {
return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS;
}
/**
* 获取单写功能码
* <p>
* FC01读线圈→ FC05写单个线圈
* FC03读保持寄存器→ FC06写单个寄存器
* 其他返回 null不支持写
*
* @param readFunctionCode 读功能码
* @return 单写功能码,不支持写时返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static Integer getWriteSingleFunctionCode(int readFunctionCode) {
switch (readFunctionCode) {
case FC_READ_COILS:
return FC_WRITE_SINGLE_COIL;
case FC_READ_HOLDING_REGISTERS:
return FC_WRITE_SINGLE_REGISTER;
default:
return null;
}
}
/**
* 获取多写功能码
* <p>
* FC01读线圈→ FC15写多个线圈
* FC03读保持寄存器→ FC16写多个寄存器
* 其他返回 null不支持写
*
* @param readFunctionCode 读功能码
* @return 多写功能码,不支持写时返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static Integer getWriteMultipleFunctionCode(int readFunctionCode) {
switch (readFunctionCode) {
case FC_READ_COILS:
return FC_WRITE_MULTIPLE_COILS;
case FC_READ_HOLDING_REGISTERS:
return FC_WRITE_MULTIPLE_REGISTERS;
default:
return null;
}
}
// ==================== CRC16 工具 ====================
/**
* 计算 CRC-16/MODBUS
*
* @param data 数据
* @param length 计算长度
* @return CRC16 值
*/
public static int calculateCrc16(byte[] data, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= (data[i] & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
/**
* 校验 CRC16
*
* @param data 包含 CRC 的完整数据
* @return 校验是否通过
*/
public static boolean verifyCrc16(byte[] data) {
if (data.length < 3) {
return false;
}
int computed = calculateCrc16(data, data.length - 2);
int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
return computed == received;
}
// ==================== 数据转换 ====================
/**
* 将原始值转换为物模型属性值
*
* @param rawValues 原始值数组(寄存器值或线圈值)
* @param point 点位配置
* @return 转换后的属性值
*/
public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) {
if (ArrayUtil.isEmpty(rawValues)) {
return null;
}
String rawDataType = point.getRawDataType();
String byteOrder = point.getByteOrder();
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
// 1. 根据原始数据类型解析原始数值
Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder);
if (rawNumber == null) {
return null;
}
// 2. 应用缩放因子:实际值 = 原始值 × scale
BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale);
// 3. 根据数据类型返回合适的 Java 类型
return formatValue(actualValue, rawDataType);
}
/**
* 将物模型属性值转换为原始寄存器值
*
* @param propertyValue 属性值
* @param point 点位配置
* @return 原始值数组
*/
public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) {
if (propertyValue == null) {
return new int[0];
}
String rawDataType = point.getRawDataType();
String byteOrder = point.getByteOrder();
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1);
// 1. 转换为 BigDecimal
BigDecimal actualValue = new BigDecimal(propertyValue.toString());
// 2. 应用缩放因子:原始值 = 实际值 ÷ scale
BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP);
// 3. 根据原始数据类型编码为寄存器值
return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount);
}
@SuppressWarnings("EnhancedSwitchMigration")
private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
return rawValues[0];
}
switch (dataTypeEnum) {
case BOOLEAN:
return rawValues[0] != 0 ? 1 : 0;
case INT16:
return (short) rawValues[0];
case UINT16:
return rawValues[0] & 0xFFFF;
case INT32:
return parseInt32(rawValues, byteOrder);
case UINT32:
return parseUint32(rawValues, byteOrder);
case FLOAT:
return parseFloat(rawValues, byteOrder);
case DOUBLE:
return parseDouble(rawValues, byteOrder);
default:
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
return rawValues[0];
}
}
private static int parseInt32(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
}
private static long parseUint32(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return rawValues[0] & 0xFFFFFFFFL;
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL;
}
private static float parseFloat(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return (float) rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
}
private static double parseDouble(int[] rawValues, String byteOrder) {
if (rawValues.length < 4) {
return rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble();
}
private static byte[] registersToBytes(int[] registers, int count) {
byte[] bytes = new byte[count * 2];
for (int i = 0; i < Math.min(registers.length, count); i++) {
bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF);
bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF);
}
return bytes;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static byte[] reorderBytes(byte[] bytes, String byteOrder) {
IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder);
// null 或者大端序,不需要调整
if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) {
return bytes;
}
// 其他字节序调整
byte[] result = new byte[bytes.length];
switch (byteOrderEnum) {
case BA: // 小端序:按每 2 字节一组交换16 位场景 [1,0]32 位场景 [1,0,3,2]
for (int i = 0; i + 1 < bytes.length; i += 2) {
result[i] = bytes[i + 1];
result[i + 1] = bytes[i];
}
break;
case CDAB: // 大端字交换32 位)
if (bytes.length >= 4) {
result[0] = bytes[2];
result[1] = bytes[3];
result[2] = bytes[0];
result[3] = bytes[1];
}
break;
case DCBA: // 小端序32 位)
if (bytes.length >= 4) {
result[0] = bytes[3];
result[1] = bytes[2];
result[2] = bytes[1];
result[3] = bytes[0];
}
break;
case BADC: // 小端字交换32 位)
if (bytes.length >= 4) {
result[0] = bytes[1];
result[1] = bytes[0];
result[2] = bytes[3];
result[3] = bytes[2];
}
break;
default:
return bytes;
}
return result;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
return new int[]{rawValue.intValue()};
}
switch (dataTypeEnum) {
case BOOLEAN:
return new int[]{rawValue.intValue() != 0 ? 1 : 0};
case INT16:
case UINT16:
return new int[]{rawValue.intValue() & 0xFFFF};
case INT32:
return encodeInt32(rawValue.intValue(), byteOrder);
case UINT32:
// 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出,
// 强转 int 保留低 32 位 bit pattern写入寄存器的字节是正确的无符号值
return encodeInt32((int) rawValue.longValue(), byteOrder);
case FLOAT:
return encodeFloat(rawValue.floatValue(), byteOrder);
case DOUBLE:
return encodeDouble(rawValue.doubleValue(), byteOrder);
default:
return new int[]{rawValue.intValue()};
}
}
private static int[] encodeInt32(int value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] encodeFloat(float value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] encodeDouble(double value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] bytesToRegisters(byte[] bytes) {
int[] registers = new int[bytes.length / 2];
for (int i = 0; i < registers.length; i++) {
registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF);
}
return registers;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static Object formatValue(BigDecimal value, String rawDataType) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
return value;
}
switch (dataTypeEnum) {
case BOOLEAN:
return value.intValue() != 0;
case INT16:
case INT32:
return value.intValue();
case UINT16:
case UINT32:
return value.longValue();
case FLOAT:
return value.floatValue();
case DOUBLE:
return value.doubleValue();
default:
return value;
}
}
// ==================== 帧值提取 ====================
/**
* 从帧中提取寄存器值FC01-04 读响应)
*
* @param frame 解码后的 Modbus 帧
* @return 寄存器值数组int[]),失败返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static int[] extractValues(IotModbusFrame frame) {
if (frame == null || frame.isException()) {
return null;
}
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return null;
}
int functionCode = frame.getFunctionCode();
switch (functionCode) {
case FC_READ_COILS:
case FC_READ_DISCRETE_INPUTS:
return extractCoilValues(pdu);
case FC_READ_HOLDING_REGISTERS:
case FC_READ_INPUT_REGISTERS:
return extractRegisterValues(pdu);
default:
log.warn("[extractValues][不支持的功能码: {}]", functionCode);
return null;
}
}
private static int[] extractCoilValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int bitCount = byteCount * 8;
int[] values = new int[bitCount];
for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
}
return values;
}
private static int[] extractRegisterValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int registerCount = byteCount / 2;
int[] values = new int[registerCount];
for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
}
return values;
}
/**
* 从响应帧中提取 registerCount通过 PDU 的 byteCount 推断)
*
* @param frame 解码后的 Modbus 响应帧
* @return registerCount无法提取时返回 -1匹配时跳过校验
*/
public static int extractRegisterCountFromResponse(IotModbusFrame frame) {
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return -1;
}
int byteCount = pdu[0] & 0xFF;
int fc = frame.getFunctionCode();
// FC03/04 寄存器读响应registerCount = byteCount / 2
if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
return byteCount / 2;
}
// FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
return -1;
}
// ==================== 点位查找 ====================
/**
* 查找点位配置
*
* @param config 设备 Modbus 配置
* @param identifier 点位标识符
* @return 匹配的点位配置,未找到返回 null
*/
public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
if (config == null || StrUtil.isBlank(identifier)) {
return null;
}
return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
}
/**
* 根据点位 ID 查找点位配置
*
* @param config 设备 Modbus 配置
* @param pointId 点位 ID
* @return 匹配的点位配置,未找到返回 null
*/
public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
if (config == null || pointId == null) {
return null;
}
return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
}
}

View File

@@ -0,0 +1,195 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
import com.ghgande.j2mod.modbus.msg.*;
import com.ghgande.j2mod.modbus.procimg.InputRegister;
import com.ghgande.j2mod.modbus.procimg.Register;
import com.ghgande.j2mod.modbus.procimg.SimpleRegister;
import com.ghgande.j2mod.modbus.util.BitVector;
import io.vertx.core.Future;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*;
/**
* IoT Modbus TCP 客户端工具类
* <p>
* 封装基于 j2mod 的 Modbus TCP 读写操作:
* 1. 根据功能码创建对应的 Modbus 读/写请求
* 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务
* 3. 从响应中提取原始值
*
* @author 芋道源码
*/
@UtilityClass
@Slf4j
public class IotModbusTcpClientUtils {
/**
* 读取 Modbus 数据
*
* @param connection Modbus 连接
* @param slaveId 从站地址
* @param point 点位配置
* @return 原始值int 数组)
*/
public static Future<int[]> read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
ModbusRequest request = createReadRequest(point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount());
request.setUnitID(slaveId);
// 2. 执行事务(请求)
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
transaction.setRequest(request);
transaction.execute();
// 3. 解析响应
ModbusResponse response = transaction.getResponse();
return extractValues(response, point.getFunctionCode());
} catch (Exception e) {
throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]",
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
}
});
}
/**
* 写入 Modbus 数据
*
* @param connection Modbus 连接
* @param slaveId 从站地址
* @param point 点位配置
* @param values 要写入的值
* @return 是否成功
*/
public static Future<Boolean> write(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point,
int[] values) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
ModbusRequest request = createWriteRequest(point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(), values);
if (request == null) {
throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作");
}
request.setUnitID(slaveId);
// 2. 执行事务(请求)
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
transaction.setRequest(request);
transaction.execute();
return true;
} catch (Exception e) {
throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]",
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
}
});
}
/**
* 创建读取请求
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) {
switch (functionCode) {
case FC_READ_COILS:
return new ReadCoilsRequest(address, count);
case FC_READ_DISCRETE_INPUTS:
return new ReadInputDiscretesRequest(address, count);
case FC_READ_HOLDING_REGISTERS:
return new ReadMultipleRegistersRequest(address, count);
case FC_READ_INPUT_REGISTERS:
return new ReadInputRegistersRequest(address, count);
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
/**
* 创建写入请求
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) {
switch (functionCode) {
case FC_READ_COILS: // 写线圈(使用功能码 5 或 15
if (count == 1) {
return new WriteCoilRequest(address, values[0] != 0);
} else {
BitVector bv = new BitVector(count);
for (int i = 0; i < Math.min(values.length, count); i++) {
bv.setBit(i, values[i] != 0);
}
return new WriteMultipleCoilsRequest(address, bv);
}
case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16
if (count == 1) {
return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0]));
} else {
Register[] registers = new SimpleRegister[count];
for (int i = 0; i < count; i++) {
registers[i] = new SimpleRegister(i < values.length ? values[i] : 0);
}
return new WriteMultipleRegistersRequest(address, registers);
}
case FC_READ_DISCRETE_INPUTS: // 只读
case FC_READ_INPUT_REGISTERS: // 只读
return null;
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
/**
* 从响应中提取值
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static int[] extractValues(ModbusResponse response, Integer functionCode) {
switch (functionCode) {
case FC_READ_COILS:
ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response;
int bitCount = coilsResponse.getBitCount();
int[] coilValues = new int[bitCount];
for (int i = 0; i < bitCount; i++) {
coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0;
}
return coilValues;
case FC_READ_DISCRETE_INPUTS:
ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response;
int discreteCount = discretesResponse.getBitCount();
int[] discreteValues = new int[discreteCount];
for (int i = 0; i < discreteCount; i++) {
discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0;
}
return discreteValues;
case FC_READ_HOLDING_REGISTERS:
ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response;
InputRegister[] holdingRegisters = holdingResponse.getRegisters();
int[] holdingValues = new int[holdingRegisters.length];
for (int i = 0; i < holdingRegisters.length; i++) {
holdingValues[i] = holdingRegisters[i].getValue();
}
return holdingValues;
case FC_READ_INPUT_REGISTERS:
ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response;
InputRegister[] inputRegisters = inputResponse.getRegisters();
int[] inputValues = new int[inputRegisters.length];
for (int i = 0; i < inputRegisters.length; i++) {
inputValues[i] = inputRegisters[i].getValue();
}
return inputValues;
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP Client 协议配置
*
* @author 芋道源码
*/
@Data
public class IotModbusTcpClientConfig {
/**
* 配置刷新间隔(秒)
*/
@NotNull(message = "配置刷新间隔不能为空")
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
private Integer configRefreshInterval = 30;
}

View File

@@ -0,0 +1,218 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private final Vertx vertx;
/**
* 配置刷新定时器 ID
*/
private Long configRefreshTimerId;
/**
* 连接管理器
*/
private final IotModbusTcpClientConnectionManager connectionManager;
/**
* 下行消息订阅者
*/
private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber;
private final IotModbusTcpClientConfigCacheService configCacheService;
private final IotModbusTcpClientPollScheduler pollScheduler;
public IotModbusTcpClientProtocol(ProtocolProperties properties) {
IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient();
Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置modbusTcpClient不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化 Vertx
this.vertx = Vertx.vertx();
// 初始化 Manager
RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi);
this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx,
messageService, configCacheService, serverId);
// 初始化 Handler
IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId);
// 初始化轮询调度器
this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP_CLIENT;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1.1 首次加载配置
refreshConfig();
// 1.2 启动配置刷新定时器
int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(refreshInterval),
id -> refreshConfig()
);
running = true;
log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功serverId={}]", getId(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager,
configCacheService);
this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 取消配置刷新定时器
if (configRefreshTimerId != null) {
vertx.cancelTimer(configRefreshTimerId);
configRefreshTimerId = null;
}
// 2.2 停止轮询调度器
pollScheduler.stopAll();
// 2.3 关闭所有连接
connectionManager.closeAll();
// 3. 关闭 Vert.x 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId());
}
/**
* 刷新配置
*/
private synchronized void refreshConfig() {
try {
// 1. 从 biz 拉取最新配置API 失败时返回 null
List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
if (configs == null) {
log.warn("[refreshConfig][API 失败,跳过本轮刷新]");
return;
}
log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
// 2. 更新连接和轮询任务
for (IotModbusDeviceConfigRespDTO config : configs) {
try {
// 2.1 确保连接存在
connectionManager.ensureConnection(config);
// 2.2 更新轮询任务
pollScheduler.updatePolling(config);
} catch (Exception e) {
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
}
}
// 3. 清理已删除设备的资源
Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs);
for (Long deviceId : removedDeviceIds) {
pollScheduler.stopPolling(deviceId);
connectionManager.removeDevice(deviceId);
}
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
}
}
}

View File

@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* IoT Modbus TCP Client 下行消息处理器
* <p>
* 负责:
* 1. 处理下行消息(如属性设置 thing.service.property.set
* 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpClientDownstreamHandler {
private final IotModbusTcpClientConnectionManager connectionManager;
private final IotModbusTcpClientConfigCacheService configCacheService;
/**
* 处理下行消息
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
public void handle(IotDeviceMessage message) {
// 1.1 检查是否是属性设置消息
if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
return;
}
if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
return;
}
// 1.2 获取设备配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
if (config == null) {
log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
return;
}
// 2. 解析属性值并写入
Object params = message.getParams();
if (!(params instanceof Map)) {
log.warn("[handle][params 不是 Map 类型: {}]", params);
return;
}
Map<String, Object> propertyMap = (Map<String, Object>) params;
for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
String identifier = entry.getKey();
Object value = entry.getValue();
// 2.1 查找对应的点位配置
IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
if (point == null) {
log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
continue;
}
// 2.2 检查是否支持写操作
if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
continue;
}
// 2.3 执行写入
writeProperty(config, point, value);
}
}
/**
* 写入属性值
*/
private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) {
// 1.1 获取连接
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
if (connection == null) {
log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId());
return;
}
// 1.2 获取 slave ID
Integer slaveId = connectionManager.getSlaveId(config.getDeviceId());
if (slaveId == null) {
log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId());
return;
}
// 2.1 转换属性值为原始值
int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
// 2.2 执行 Modbus 写入
IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
.onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
config.getDeviceId(), point.getIdentifier(), value))
.onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e));
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotModbusTcpClientDownstreamHandler downstreamHandler;
public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol,
IotModbusTcpClientDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = downstreamHandler;
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientUpstreamHandler {
private final IotDeviceMessageService messageService;
private final String serverId;
public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService,
String serverId) {
this.messageService = messageService;
this.serverId = serverId;
}
/**
* 处理 Modbus 读取结果
*
* @param config 设备配置
* @param point 点位配置
* @param rawValue 原始值int 数组)
*/
public void handleReadResult(IotModbusDeviceConfigRespDTO config,
IotModbusPointRespDTO point,
int[] rawValue) {
try {
// 1.1 转换原始值为物模型属性值(点位翻译)
Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
// 1.2 构造属性上报消息
Map<String, Object> params = MapUtil.of(point.getIdentifier(), convertedValue);
IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
// 2. 发送到消息总线
messageService.sendDeviceMessage(message, config.getProductKey(),
config.getDeviceName(), serverId);
} catch (Exception e) {
log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e);
}
}
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.IotModbusDeviceConfigListReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
* IoT Modbus TCP Client 配置缓存服务
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpClientConfigCacheService {
private final IotDeviceCommonApi deviceApi;
/**
* 配置缓存deviceId -> 配置
*/
private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
/**
* 已知的设备 ID 集合(作用:用于检测已删除的设备)
*
* @see #cleanupRemovedDevices(List)
*/
private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
/**
* 刷新配置
*
* @return 最新的配置列表API 失败时返回 null调用方应跳过 cleanup
*/
public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
try {
// 1. 从远程获取配置
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
.setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType()));
result.checkError();
List<IotModbusDeviceConfigRespDTO> configs = result.getData();
// 2. 更新缓存(注意:不在这里更新 knownDeviceIds由 cleanupRemovedDevices 统一管理)
for (IotModbusDeviceConfigRespDTO config : configs) {
configCache.put(config.getDeviceId(), config);
}
return configs;
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
return null;
}
}
/**
* 获取设备配置
*
* @param deviceId 设备 ID
* @return 配置
*/
public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
return configCache.get(deviceId);
}
/**
* 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合
*
* @param currentConfigs 当前有效的配置列表
* @return 已删除的设备 ID 集合
*/
public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) {
// 1.1 获取当前有效的设备 ID
Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
// 1.2 找出已删除的设备(基于旧的 knownDeviceIds
Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
removedDeviceIds.removeAll(currentDeviceIds);
// 2. 清理已删除设备的缓存
for (Long deviceId : removedDeviceIds) {
log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
configCache.remove(deviceId);
}
// 3. 更新已知设备 ID 集合为当前有效的设备 ID
knownDeviceIds.clear();
knownDeviceIds.addAll(currentDeviceIds);
return removedDeviceIds;
}
}

View File

@@ -0,0 +1,317 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT Modbus TCP 连接管理器
* <p>
* 统一管理 Modbus TCP 连接:
* 1. 管理 TCP 连接(相同 ip:port 共用连接)
* 2. 分布式锁管理(连接级别),避免多节点重复创建连接
* 3. 连接重试和故障恢复
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientConnectionManager {
private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:";
private final RedissonClient redissonClient;
private final Vertx vertx;
private final IotDeviceMessageService messageService;
private final IotModbusTcpClientConfigCacheService configCacheService;
private final String serverId;
/**
* 连接池key = ip:port
*/
private final Map<String, ModbusConnection> connectionPool = new ConcurrentHashMap<>();
/**
* 设备 ID 到连接 key 的映射
*/
private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>();
public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx,
IotDeviceMessageService messageService,
IotModbusTcpClientConfigCacheService configCacheService,
String serverId) {
this.redissonClient = redissonClient;
this.vertx = vertx;
this.messageService = messageService;
this.configCacheService = configCacheService;
this.serverId = serverId;
}
/**
* 确保连接存在
* <p>
* 首次建连成功时,直接发送设备上线消息
*
* @param config 设备配置
*/
public void ensureConnection(IotModbusDeviceConfigRespDTO config) {
// 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接
String connectionKey = buildConnectionKey(config.getIp(), config.getPort());
String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId());
if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) {
log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]",
config.getDeviceId(), oldConnectionKey, connectionKey);
removeDevice(config.getDeviceId());
}
// 1.2 记录设备与连接的映射
deviceConnectionMap.put(config.getDeviceId(), connectionKey);
// 2. 情况一:连接已存在,注册设备并发送上线消息
ModbusConnection connection = connectionPool.get(connectionKey);
if (connection != null) {
addDeviceAndOnline(connection, config);
return;
}
// 3. 情况二:连接不存在,加分布式锁创建新连接
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey);
if (!lock.tryLock()) {
log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey);
return;
}
try {
// 3.1 double-check拿到锁后再次检查避免并发创建重复连接
connection = connectionPool.get(connectionKey);
if (connection != null) {
addDeviceAndOnline(connection, config);
lock.unlock();
return;
}
// 3.2 创建新连接
connection = createConnection(config);
connection.setLock(lock);
connectionPool.put(connectionKey, connection);
log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
// 3.3 注册设备并发送上线消息
addDeviceAndOnline(connection, config);
} catch (Exception e) {
log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e);
// 建连失败,释放锁让其他节点可重试
lock.unlock();
}
}
/**
* 创建 Modbus TCP 连接
*/
private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception {
// 1. 创建 TCP 连接
TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp()));
tcpConnection.setPort(config.getPort());
tcpConnection.setTimeout(config.getTimeout());
tcpConnection.connect();
// 2. 创建 Modbus 连接对象
return new ModbusConnection()
.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort()))
.setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext())
.setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval());
}
/**
* 获取连接
*/
public ModbusConnection getConnection(Long deviceId) {
String connectionKey = deviceConnectionMap.get(deviceId);
if (connectionKey == null) {
return null;
}
return connectionPool.get(connectionKey);
}
/**
* 获取设备的 slave ID
*/
public Integer getSlaveId(Long deviceId) {
ModbusConnection connection = getConnection(deviceId);
if (connection == null) {
return null;
}
return connection.getSlaveId(deviceId);
}
/**
* 移除设备
* <p>
* 移除时直接发送设备下线消息
*/
public void removeDevice(Long deviceId) {
// 1.1 移除设备时,发送下线消息
sendOfflineMessage(deviceId);
// 1.2 移除设备引用
String connectionKey = deviceConnectionMap.remove(deviceId);
if (connectionKey == null) {
return;
}
// 2.1 移除连接中的设备引用
ModbusConnection connection = connectionPool.get(connectionKey);
if (connection == null) {
return;
}
connection.removeDevice(deviceId);
// 2.2 如果没有设备引用了,关闭连接
if (connection.getDeviceCount() == 0) {
closeConnection(connectionKey);
}
}
// ==================== 设备连接 & 上下线消息 ====================
/**
* 注册设备到连接,并发送上线消息
*/
private void addDeviceAndOnline(ModbusConnection connection,
IotModbusDeviceConfigRespDTO config) {
Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId());
// 首次注册,发送上线消息
if (previous == null) {
sendOnlineMessage(config);
}
}
/**
* 发送设备上线消息
*/
private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) {
try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
messageService.sendDeviceMessage(onlineMessage,
config.getProductKey(), config.getDeviceName(), serverId);
} catch (Exception ex) {
log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex);
}
}
/**
* 发送设备下线消息
*/
private void sendOfflineMessage(Long deviceId) {
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
if (config == null) {
return;
}
try {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
messageService.sendDeviceMessage(offlineMessage,
config.getProductKey(), config.getDeviceName(), serverId);
} catch (Exception ex) {
log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex);
}
}
/**
* 关闭指定连接
*/
private void closeConnection(String connectionKey) {
ModbusConnection connection = connectionPool.remove(connectionKey);
if (connection == null) {
return;
}
try {
if (connection.getTcpConnection() != null) {
connection.getTcpConnection().close();
}
// 释放分布式锁,让其他节点可接管
RLock lock = connection.getLock();
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
} catch (Exception e) {
log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e);
}
}
/**
* 关闭所有连接
*/
public void closeAll() {
// 先复制再遍历,避免 closeConnection 中 remove 导致并发修改
List<String> connectionKeys = new ArrayList<>(connectionPool.keySet());
for (String connectionKey : connectionKeys) {
closeConnection(connectionKey);
}
deviceConnectionMap.clear();
}
private String buildConnectionKey(String ip, Integer port) {
return ip + ":" + port;
}
/**
* Modbus 连接信息
*/
@Data
public static class ModbusConnection {
private String connectionKey;
private TCPMasterConnection tcpConnection;
private Integer timeout;
private Integer retryInterval;
/**
* 设备 ID 到 slave ID 的映射
*/
private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>();
/**
* 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站
*/
private RLock lock;
/**
* Vert.x Context用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行
*/
private Context context;
public Integer addDevice(Long deviceId, Integer slaveId) {
return deviceSlaveMap.putIfAbsent(deviceId, slaveId);
}
public void removeDevice(Long deviceId) {
deviceSlaveMap.remove(deviceId);
}
public int getDeviceCount() {
return deviceSlaveMap.size();
}
public Integer getSlaveId(Long deviceId) {
return deviceSlaveMap.get(deviceId);
}
/**
* 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行)
*/
public <T> Future<T> executeBlocking(java.util.function.Function<TCPMasterConnection, T> operation) {
// ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行
return context.executeBlocking(() -> operation.apply(tcpConnection), true);
}
}
}

View File

@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
import io.vertx.core.Vertx;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler {
private final IotModbusTcpClientConnectionManager connectionManager;
private final IotModbusTcpClientUpstreamHandler upstreamHandler;
private final IotModbusTcpClientConfigCacheService configCacheService;
public IotModbusTcpClientPollScheduler(Vertx vertx,
IotModbusTcpClientConnectionManager connectionManager,
IotModbusTcpClientUpstreamHandler upstreamHandler,
IotModbusTcpClientConfigCacheService configCacheService) {
super(vertx);
this.connectionManager = connectionManager;
this.upstreamHandler = upstreamHandler;
this.configCacheService = configCacheService;
}
// ========== 轮询执行 ==========
/**
* 轮询单个点位
*/
@Override
protected void pollPoint(Long deviceId, Long pointId) {
// 1.1 从 configCache 获取最新配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
if (config == null || CollUtil.isEmpty(config.getPoints())) {
log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
return;
}
// 1.2 查找点位
IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
if (point == null) {
log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
return;
}
// 2.1 获取连接
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
if (connection == null) {
log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
return;
}
// 2.2 获取 slave ID
Integer slaveId = connectionManager.getSlaveId(deviceId);
Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
// 3. 执行 Modbus 读取
IotModbusTcpClientUtils.read(connection, slaveId, point)
.onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
.onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]",
deviceId, point.getIdentifier(), e));
}
}

View File

@@ -0,0 +1,6 @@
/**
* Modbus TCP Client主站协议网关主动连接并轮询 Modbus 从站设备
* <p>
* 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP Server 协议配置
*
* @author 芋道源码
*/
@Data
public class IotModbusTcpServerConfig {
/**
* 配置刷新间隔(秒)
*/
@NotNull(message = "配置刷新间隔不能为空")
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
private Integer configRefreshInterval = 30;
/**
* 自定义功能码(用于认证等扩展交互)
* Modbus 协议保留 65-72 给用户自定义,默认 65
*/
@NotNull(message = "自定义功能码不能为空")
@Min(value = 65, message = "自定义功能码不能小于 65")
@Max(value = 72, message = "自定义功能码不能大于 72")
private Integer customFunctionCode = 65;
/**
* Pending Request 超时时间(毫秒)
*/
@NotNull(message = "请求超时时间不能为空")
private Integer requestTimeout = 5000;
/**
* Pending Request 清理间隔(毫秒)
*/
@NotNull(message = "请求清理间隔不能为空")
private Integer requestCleanupInterval = 10000;
}

View File

@@ -0,0 +1,334 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler;
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.NetSocket;
import io.vertx.core.parsetools.RecordParser;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IoT 网关 Modbus TCP Server 协议
* <p>
* 作为 TCP Server 接收设备主动连接:
* 1. 设备通过自定义功能码FC 65发送认证请求
* 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式)
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private final Vertx vertx;
/**
* TCP Server
*/
private NetServer netServer;
/**
* 配置刷新定时器 ID
*/
private Long configRefreshTimerId;
/**
* Pending Request 清理定时器 ID
*/
private Long requestCleanupTimerId;
/**
* 连接管理器
*/
private final IotModbusTcpServerConnectionManager connectionManager;
/**
* 下行消息订阅者
*/
private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber;
private final IotModbusFrameDecoder frameDecoder;
@SuppressWarnings("FieldCanBeLocal")
private final IotModbusFrameEncoder frameEncoder;
private final IotModbusTcpServerConfigCacheService configCacheService;
private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
private final IotModbusTcpServerUpstreamHandler upstreamHandler;
private final IotModbusTcpServerPollScheduler pollScheduler;
private final IotDeviceMessageService messageService;
public IotModbusTcpServerProtocol(ProtocolProperties properties) {
IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置modbusTcpServer不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化 Vertx
this.vertx = Vertx.vertx();
// 初始化 Manager
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.connectionManager = new IotModbusTcpServerConnectionManager();
this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi);
this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager();
// 初始化帧编解码器
this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode());
this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode());
// 初始化共享事务 ID 自增器PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突)
AtomicInteger transactionIdCounter = new AtomicInteger(0);
// 初始化轮询调度器
this.pollScheduler = new IotModbusTcpServerPollScheduler(
vertx, connectionManager, frameEncoder, pendingRequestManager,
slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService);
// 初始化 Handler
this.messageService = SpringUtil.getBean(IotDeviceMessageService.class);
IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
this.upstreamHandler = new IotModbusTcpServerUpstreamHandler(
deviceApi, this.messageService, frameEncoder,
connectionManager, configCacheService, pendingRequestManager,
pollScheduler, deviceService, serverId);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP_SERVER;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1. 启动配置刷新定时器
IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()),
id -> refreshConfig());
// 2.1 启动 TCP Server
startTcpServer();
// 2.2 启动 PendingRequest 清理定时器
requestCleanupTimerId = vertx.setPeriodic(
slaveConfig.getRequestCleanupInterval(),
id -> pendingRequestManager.cleanupExpired());
running = true;
log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]",
getId(), serverId, properties.getPort());
// 3. 启动下行消息订阅
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler(
connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter());
this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber(
this, downstreamHandler, messageBus);
downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
} catch (Exception e) {
log.error("[stop][下行消息订阅器停止失败]", e);
}
downstreamSubscriber = null;
}
// 2.1 取消定时器
if (configRefreshTimerId != null) {
vertx.cancelTimer(configRefreshTimerId);
configRefreshTimerId = null;
}
if (requestCleanupTimerId != null) {
vertx.cancelTimer(requestCleanupTimerId);
requestCleanupTimerId = null;
}
// 2.2 停止轮询
pollScheduler.stopAll();
// 2.3 清理 PendingRequest
pendingRequestManager.clear();
// 2.4 关闭所有连接
connectionManager.closeAll();
// 2.5 关闭 TCP Server
if (netServer != null) {
try {
netServer.close().result();
log.info("[stop][TCP Server 已关闭]");
} catch (Exception e) {
log.error("[stop][TCP Server 关闭失败]", e);
}
netServer = null;
}
// 3. 关闭 Vertx
if (vertx != null) {
try {
vertx.close().result();
} catch (Exception e) {
log.error("[stop][Vertx 关闭失败]", e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId());
}
/**
* 启动 TCP Server
*/
private void startTcpServer() {
// 1. 创建 TCP Server
NetServerOptions options = new NetServerOptions()
.setPort(properties.getPort());
netServer = vertx.createNetServer(options);
// 2. 设置连接处理器
netServer.connectHandler(this::handleConnection);
try {
netServer.listen().toCompletionStage().toCompletableFuture().get();
log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort());
} catch (Exception e) {
throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e);
}
}
/**
* 处理新连接
*/
private void handleConnection(NetSocket socket) {
log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());
// 1. 创建 RecordParser 并设置为数据处理器
RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> {
// 【重要】帧处理分发,即消息处理
upstreamHandler.handleFrame(socket, frame, frameFormat);
});
socket.handler(recordParser);
// 2.1 连接关闭处理
socket.closeHandler(v -> {
ConnectionInfo info = connectionManager.removeConnection(socket);
if (info == null || info.getDeviceId() == null) {
log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress());
return;
}
pollScheduler.stopPolling(info.getDeviceId());
pendingRequestManager.removeDevice(info.getDeviceId());
configCacheService.removeConfig(info.getDeviceId());
// 发送设备下线消息
try {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId);
} catch (Exception ex) {
log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex);
}
log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
info.getDeviceId(), socket.remoteAddress());
});
// 2.2 异常处理
socket.exceptionHandler(e -> {
log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e);
socket.close();
});
}
/**
* 刷新已连接设备的配置(定时调用)
*/
private synchronized void refreshConfig() {
try {
// 1. 只刷新已连接设备的配置
Set<Long> connectedDeviceIds = connectionManager.getConnectedDeviceIds();
if (CollUtil.isEmpty(connectedDeviceIds)) {
return;
}
List<IotModbusDeviceConfigRespDTO> configs =
configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds);
if (configs == null) {
log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]");
return;
}
log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size());
// 2. 更新已连接设备的轮询任务
for (IotModbusDeviceConfigRespDTO config : configs) {
try {
pollScheduler.updatePolling(config);
} catch (Exception e) {
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
}
}
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
}
}
}

View File

@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* IoT Modbus 统一帧数据模型TCP/RTU 公用)
*
* @author 芋道源码
*/
@Data
@Accessors(chain = true)
public class IotModbusFrame {
/**
* 从站地址
*/
private int slaveId;
/**
* 功能码
*/
private int functionCode;
/**
* PDU 数据(不含 slaveId
*/
private byte[] pdu;
/**
* 事务标识符
* <p>
* 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值
*/
private Integer transactionId;
/**
* 异常码
* <p>
* 当功能码最高位为 1 时(异常响应),此字段存储异常码。
*
* @see IotModbusCommonUtils#FC_EXCEPTION_MASK
*/
private Integer exceptionCode;
/**
* 自定义功能码时的 JSON 字符串(用于 auth 认证等等)
*/
private String customData;
/**
* 是否异常响应(基于 exceptionCode 是否有值判断)
*/
public boolean isException() {
return exceptionCode != null;
}
}

View File

@@ -0,0 +1,477 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.function.BiConsumer;
/**
* IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。
* <p>
* 流程:
* 1. 首帧检测:读前 6 字节,判断 MODBUS_TCPProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
* 2. 检测后切换到对应的拆包 Handler并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理
* 3. 拆包完成后解码为 IotModbusFrame通过回调返回
* - MODBUS_TCP两阶段 RecordParserMBAP length 字段驱动)
* - MODBUS_RTU功能码驱动的状态机
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusFrameDecoder {
private static final Boolean REQUEST_MODE_DEFAULT = false;
/**
* 自定义功能码
*/
private final int customFunctionCode;
/**
* 创建带自动帧格式检测的 RecordParser默认响应模式
*
* @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
* @return RecordParser 实例
*/
public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler) {
return createRecordParser(frameHandler, REQUEST_MODE_DEFAULT);
}
/**
* 创建带自动帧格式检测的 RecordParser
*
* @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
* @param requestMode 是否为请求模式true接收方收到的是 Modbus 请求帧FC01-04 按固定 8 字节解析;
* false接收方收到的是 Modbus 响应帧FC01-04 按 byteCount 变长解析)
* @return RecordParser 实例
*/
public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler,
boolean requestMode) {
// 先创建一个 RecordParser使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测
RecordParser parser = RecordParser.newFixed(6);
parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, requestMode));
return parser;
}
// ==================== 帧解码 ====================
/**
* 解码响应帧(拆包后的完整帧 byte[]
*
* @param data 完整帧字节数组
* @param format 帧格式
* @return 解码后的 IotModbusFrame
*/
private IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) {
if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
return decodeTcpResponse(data);
} else {
return decodeRtuResponse(data);
}
}
/**
* 解码 MODBUS_TCP 响应
* 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...]
*/
private IotModbusFrame decodeTcpResponse(byte[] data) {
if (data.length < 8) {
log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length);
return null;
}
ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
int transactionId = buf.getShort() & 0xFFFF;
buf.getShort(); // protocolId固定 0x0000Modbus 协议标识
buf.getShort(); // length后续字节数UnitId + PDU拆包阶段已使用
int slaveId = buf.get() & 0xFF;
int functionCode = buf.get() & 0xFF;
// 提取 PDU 数据(从 functionCode 之后到末尾)
byte[] pdu = new byte[data.length - 8];
System.arraycopy(data, 8, pdu, 0, pdu.length);
// 构建 IotModbusFrame
return buildFrame(slaveId, functionCode, pdu, transactionId);
}
/**
* 解码 MODBUS_RTU 响应
* 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)]
*/
private IotModbusFrame decodeRtuResponse(byte[] data) {
if (data.length < 4) {
log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length);
return null;
}
// 校验 CRC
if (!IotModbusCommonUtils.verifyCrc16(data)) {
log.warn("[decodeRtuResponse][CRC 校验失败]");
return null;
}
int slaveId = data[0] & 0xFF;
int functionCode = data[1] & 0xFF;
// PDU 数据(不含 slaveId、functionCode、CRC
byte[] pdu = new byte[data.length - 4];
System.arraycopy(data, 2, pdu, 0, pdu.length);
// 构建 IotModbusFrame
return buildFrame(slaveId, functionCode, pdu, null);
}
/**
* 构建 IotModbusFrame
*/
private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) {
IotModbusFrame frame = new IotModbusFrame()
.setSlaveId(slaveId)
.setFunctionCode(functionCode)
.setPdu(pdu)
.setTransactionId(transactionId);
// 异常响应
if (IotModbusCommonUtils.isExceptionResponse(functionCode)) {
frame.setFunctionCode(IotModbusCommonUtils.extractOriginalFunctionCode(functionCode));
if (pdu.length >= 1) {
frame.setExceptionCode(pdu[0] & 0xFF);
}
return frame;
}
// 自定义功能码
if (functionCode == customFunctionCode) {
// data 区格式:[byteCount(1)] [JSON data(N)]
if (pdu.length >= 1) {
int byteCount = pdu[0] & 0xFF;
if (pdu.length >= 1 + byteCount) {
frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8));
}
}
}
return frame;
}
// ==================== 拆包 Handler ====================
/**
* 帧格式检测阶段 Handler仅处理首包探测后切换到对应的拆包 Handler
*/
@RequiredArgsConstructor
private class DetectPhaseHandler implements Handler<Buffer> {
private final RecordParser parser;
private final int customFunctionCode;
private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
private final boolean requestMode;
@Override
public void handle(Buffer buffer) {
// 检测帧格式protocolId==0x0000 且 length 合法 → MODBUS_TCP否则 → MODBUS_RTU
byte[] bytes = buffer.getBytes();
int protocolId = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF);
int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF);
// 分别处理 MODBUS_TCP、MODBUS_RTU 两种情况
if (protocolId == 0x0000 && length >= 1 && length <= 253) {
// MODBUS_TCP切换到 TCP 拆包 Handler
log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]");
TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler);
parser.handler(tcpHandler);
// 当前 bytes 就是 MBAP 的前 6 字节,直接交给 tcpHandler 处理
tcpHandler.handleFirstBytes(bytes);
} else {
// MODBUS_RTU切换到 RTU 拆包 Handler
log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]");
RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode, requestMode);
parser.handler(rtuHandler);
// 当前 bytes 包含前 6 字节slaveId + FC + 部分数据),交给 rtuHandler 处理
rtuHandler.handleFirstBytes(bytes);
}
}
}
/**
* MODBUS_TCP 拆包 Handler两阶段 RecordParser
* <p>
* Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length
* Phase 2: fixedSizeMode(length) → 读 unitId + PDU
*/
@RequiredArgsConstructor
private class TcpFrameHandler implements Handler<Buffer> {
private final RecordParser parser;
private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
private byte[] mbapHeader;
private boolean waitingForBody = false;
/**
* 处理探测阶段传来的首帧 6 字节(即 MBAP 头)
*
* @param bytes 探测阶段消费的 6 字节
*/
void handleFirstBytes(byte[] bytes) {
int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF);
this.mbapHeader = bytes;
this.waitingForBody = true;
parser.fixedSizeMode(length);
}
@Override
public void handle(Buffer buffer) {
if (waitingForBody) {
// Phase 2: 收到 bodyunitId + PDU
byte[] body = buffer.getBytes();
// 拼接完整帧MBAP(6) + body
byte[] fullFrame = new byte[mbapHeader.length + body.length];
System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length);
System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length);
// 解码并回调
IotModbusFrame frame = decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP);
if (frame != null) {
frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_TCP);
}
// 切回 Phase 1
waitingForBody = false;
mbapHeader = null;
parser.fixedSizeMode(6);
} else {
// Phase 1: 收到 MBAP 头 6 字节
byte[] header = buffer.getBytes();
int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF);
if (length < 1 || length > 253) {
log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length);
parser.fixedSizeMode(6);
return;
}
this.mbapHeader = header;
this.waitingForBody = true;
parser.fixedSizeMode(length);
}
}
}
/**
* MODBUS_RTU 拆包 Handler功能码驱动的状态机
* <p>
* 状态机流程:
* Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
* Phase 2: 根据 functionCode 确定剩余长度:
* - 异常响应 (FC & EXCEPTION_MASK)fixedSizeMode(3) → exceptionCode(1) + CRC(2)
* - 自定义 FC / FC01-04 响应fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
* - FC05/06 响应fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
* - FC15/16 响应fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
* <p>
* 请求模式requestMode=trueFC01-04 按固定 8 字节解析(与写响应相同路径),
* 因为读请求格式为 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)]
*/
@RequiredArgsConstructor
private class RtuFrameHandler implements Handler<Buffer> {
private static final int STATE_HEADER = 0;
private static final int STATE_EXCEPTION_BODY = 1;
private static final int STATE_READ_BYTE_COUNT = 2;
private static final int STATE_READ_DATA = 3;
private static final int STATE_WRITE_BODY = 4;
private final RecordParser parser;
private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
private final int customFunctionCode;
/**
* 请求模式:
* - true 表示接收方收到的是 Modbus 请求帧如设备端收到网关下发的读请求FC01-04 按固定 8 字节帧解析
* - false 表示接收方收到的是 Modbus 响应帧FC01-04 按 byteCount 变长解析
*/
private final boolean requestMode;
private int state = STATE_HEADER;
private byte slaveId;
private byte functionCode;
private byte byteCount;
private Buffer pendingData;
private int expectedDataLen;
/**
* 处理探测阶段传来的首帧 6 字节
* <p>
* 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据
*
* @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...]
*/
void handleFirstBytes(byte[] bytes) {
this.slaveId = bytes[0];
this.functionCode = bytes[1];
int fc = functionCode & 0xFF;
if (IotModbusCommonUtils.isExceptionResponse(fc)) {
// 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节
// 已有 6 字节(多 1 字节),取前 5 字节组装
Buffer frame = Buffer.buffer(5);
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC
emitFrame(frame);
resetToHeader();
} else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) {
// 请求模式下的读请求:固定 8 字节 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)]
// 已有 6 字节,还需 2 字节CRC
state = STATE_WRITE_BODY;
this.pendingData = Buffer.buffer();
this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节StartAddr + Quantity
parser.fixedSizeMode(2); // 还需 2 字节CRC
} else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
// 读响应或自定义 FCbytes[2] = byteCount
this.byteCount = bytes[2];
int bc = byteCount & 0xFF;
// 已有数据bytes[3..5] = 3 字节
// 还需byteCount + CRC(2) - 3 字节已有
int remaining = bc + 2 - 3;
if (remaining <= 0) {
// 数据已足够,组装完整帧
int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC
Buffer frame = Buffer.buffer(totalLen);
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendByte(byteCount);
frame.appendBytes(bytes, 3, bc + 2); // data + CRC
emitFrame(frame);
resetToHeader();
} else {
// 需要继续读
state = STATE_READ_DATA;
this.pendingData = Buffer.buffer();
this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节
this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
parser.fixedSizeMode(remaining);
}
} else if (IotModbusCommonUtils.isWriteResponse(fc)) {
// 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节
// 已有 6 字节,还需 2 字节
state = STATE_WRITE_BODY;
this.pendingData = Buffer.buffer();
this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节
parser.fixedSizeMode(2); // 还需 2 字节CRC
} else {
log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
resetToHeader();
}
}
@Override
public void handle(Buffer buffer) {
switch (state) {
case STATE_HEADER:
handleHeader(buffer);
break;
case STATE_EXCEPTION_BODY:
handleExceptionBody(buffer);
break;
case STATE_READ_BYTE_COUNT:
handleReadByteCount(buffer);
break;
case STATE_READ_DATA:
handleReadData(buffer);
break;
case STATE_WRITE_BODY:
handleWriteBody(buffer);
break;
default:
resetToHeader();
}
}
private void handleHeader(Buffer buffer) {
byte[] header = buffer.getBytes();
this.slaveId = header[0];
this.functionCode = header[1];
int fc = functionCode & 0xFF;
if (IotModbusCommonUtils.isExceptionResponse(fc)) {
// 异常响应
state = STATE_EXCEPTION_BODY;
parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
} else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) {
// 请求模式下的读请求:固定 8 字节,已读 2 字节slaveId + FC还需 6 字节
state = STATE_WRITE_BODY;
pendingData = Buffer.buffer();
parser.fixedSizeMode(6); // StartAddr(2) + Quantity(2) + CRC(2)
} else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
// 读响应或自定义 FC
state = STATE_READ_BYTE_COUNT;
parser.fixedSizeMode(1); // byteCount
} else if (IotModbusCommonUtils.isWriteResponse(fc)) {
// 写响应
state = STATE_WRITE_BODY;
pendingData = Buffer.buffer();
parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2)
} else {
log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
resetToHeader();
}
}
private void handleExceptionBody(Buffer buffer) {
// buffer = exceptionCode(1) + CRC(2)
Buffer frame = Buffer.buffer();
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendBuffer(buffer);
emitFrame(frame);
resetToHeader();
}
private void handleReadByteCount(Buffer buffer) {
this.byteCount = buffer.getByte(0);
int bc = byteCount & 0xFF;
state = STATE_READ_DATA;
pendingData = Buffer.buffer();
expectedDataLen = bc + 2; // data(bc) + CRC(2)
parser.fixedSizeMode(expectedDataLen);
}
private void handleReadData(Buffer buffer) {
pendingData.appendBuffer(buffer);
if (pendingData.length() >= expectedDataLen) {
// 组装完整帧
Buffer frame = Buffer.buffer();
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendByte(byteCount);
frame.appendBuffer(pendingData);
emitFrame(frame);
resetToHeader();
}
// 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode
}
private void handleWriteBody(Buffer buffer) {
pendingData.appendBuffer(buffer);
// 完整帧
Buffer frame = Buffer.buffer();
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendBuffer(pendingData);
emitFrame(frame);
resetToHeader();
}
/**
* 发射完整帧:解码并回调
*/
private void emitFrame(Buffer frameBuffer) {
IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
if (frame != null) {
frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU);
}
}
private void resetToHeader() {
state = STATE_HEADER;
pendingData = null;
parser.fixedSizeMode(2); // slaveId + FC
}
}
}

View File

@@ -0,0 +1,210 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/**
* IoT Modbus 帧编码器:负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCPMBAP和 MODBUS_RTUCRC16两种帧格式。
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusFrameEncoder {
private final int customFunctionCode;
// ==================== 编码 ====================
/**
* 编码读请求
*
* @param slaveId 从站地址
* @param functionCode 功能码
* @param startAddress 起始寄存器地址
* @param quantity 寄存器数量
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用RTU 模式传 null
* @return 编码后的字节数组
*/
public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
IotModbusFrameFormatEnum format, Integer transactionId) {
// PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
byte[] pdu = new byte[5];
pdu[0] = (byte) functionCode;
pdu[1] = (byte) ((startAddress >> 8) & 0xFF);
pdu[2] = (byte) (startAddress & 0xFF);
pdu[3] = (byte) ((quantity >> 8) & 0xFF);
pdu[4] = (byte) (quantity & 0xFF);
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码写请求(单个寄存器 FC06 / 单个线圈 FC05
*
* @param slaveId 从站地址
* @param functionCode 功能码
* @param address 寄存器地址
* @param value 值
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用RTU 模式传 null
* @return 编码后的字节数组
*/
public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
IotModbusFrameFormatEnum format, Integer transactionId) {
// FC05 单写线圈Modbus 标准要求 value 为 0xFF00ON或 0x0000OFF
if (functionCode == IotModbusCommonUtils.FC_WRITE_SINGLE_COIL) {
value = (value != 0) ? 0xFF00 : 0x0000;
}
// PDU: [FC(1)] [Address(2)] [Value(2)]
byte[] pdu = new byte[5];
pdu[0] = (byte) functionCode;
pdu[1] = (byte) ((address >> 8) & 0xFF);
pdu[2] = (byte) (address & 0xFF);
pdu[3] = (byte) ((value >> 8) & 0xFF);
pdu[4] = (byte) (value & 0xFF);
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码写多个寄存器请求FC16
*
* @param slaveId 从站地址
* @param address 起始地址
* @param values 值数组
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用RTU 模式传 null
* @return 编码后的字节数组
*/
public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
IotModbusFrameFormatEnum format, Integer transactionId) {
// PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
int quantity = values.length;
int byteCount = quantity * 2;
byte[] pdu = new byte[6 + byteCount];
pdu[0] = (byte) 16; // FC16
pdu[1] = (byte) ((address >> 8) & 0xFF);
pdu[2] = (byte) (address & 0xFF);
pdu[3] = (byte) ((quantity >> 8) & 0xFF);
pdu[4] = (byte) (quantity & 0xFF);
pdu[5] = (byte) byteCount;
for (int i = 0; i < quantity; i++) {
pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF);
}
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码写多个线圈请求FC15
* <p>
* 按 Modbus FC15 标准,线圈值按 bit 打包(每个 byte 包含 8 个线圈状态)。
*
* @param slaveId 从站地址
* @param address 起始地址
* @param values 线圈值数组int[]非0 表示 ON0 表示 OFF
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用RTU 模式传 null
* @return 编码后的字节数组
*/
public byte[] encodeWriteMultipleCoilsRequest(int slaveId, int address, int[] values,
IotModbusFrameFormatEnum format, Integer transactionId) {
// PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [CoilValues(N)]
int quantity = values.length;
int byteCount = (quantity + 7) / 8; // 向上取整
byte[] pdu = new byte[6 + byteCount];
pdu[0] = (byte) IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS; // FC15
pdu[1] = (byte) ((address >> 8) & 0xFF);
pdu[2] = (byte) (address & 0xFF);
pdu[3] = (byte) ((quantity >> 8) & 0xFF);
pdu[4] = (byte) (quantity & 0xFF);
pdu[5] = (byte) byteCount;
// 按 bit 打包:每个 byte 的 bit0 对应最低地址的线圈
for (int i = 0; i < quantity; i++) {
if (values[i] != 0) {
pdu[6 + i / 8] |= (byte) (1 << (i % 8));
}
}
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码自定义功能码帧(认证响应等)
*
* @param slaveId 从站地址
* @param jsonData JSON 数据
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用RTU 模式传 null
* @return 编码后的字节数组
*/
public byte[] encodeCustomFrame(int slaveId, String jsonData,
IotModbusFrameFormatEnum format, Integer transactionId) {
byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
// PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
byte[] pdu = new byte[2 + jsonBytes.length];
pdu[0] = (byte) customFunctionCode;
pdu[1] = (byte) jsonBytes.length;
System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length);
return wrapFrame(slaveId, pdu, format, transactionId);
}
// ==================== 帧封装 ====================
/**
* 将 PDU 封装为完整帧
*
* @param slaveId 从站地址
* @param pdu PDU 数据(含 functionCode
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用RTU 模式可为 null
* @return 完整帧字节数组
*/
private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, Integer transactionId) {
if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0);
} else {
return wrapRtuFrame(slaveId, pdu);
}
}
/**
* 封装 MODBUS_TCP 帧
* [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...]
*/
private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) {
int length = 1 + pdu.length; // UnitId + PDU
byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU
// MBAP Header
frame[0] = (byte) ((transactionId >> 8) & 0xFF);
frame[1] = (byte) (transactionId & 0xFF);
frame[2] = 0; // Protocol ID high
frame[3] = 0; // Protocol ID low
frame[4] = (byte) ((length >> 8) & 0xFF);
frame[5] = (byte) (length & 0xFF);
// Unit ID
frame[6] = (byte) slaveId;
// PDU
System.arraycopy(pdu, 0, frame, 7, pdu.length);
return frame;
}
/**
* 封装 MODBUS_RTU 帧
* [SlaveId(1)] [PDU...] [CRC(2)]
*/
private byte[] wrapRtuFrame(int slaveId, byte[] pdu) {
byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC
frame[0] = (byte) slaveId;
System.arraycopy(pdu, 0, frame, 1, pdu.length);
// 计算并追加 CRC16
int crc = IotModbusCommonUtils.calculateCrc16(frame, frame.length - 2);
frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
return frame;
}
}

View File

@@ -0,0 +1,152 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IoT Modbus TCP Server 下行消息处理器
* <p>
* 负责:
* 1. 处理下行消息(如属性设置 thing.service.property.set
* 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerDownstreamHandler {
private final IotModbusTcpServerConnectionManager connectionManager;
private final IotModbusTcpServerConfigCacheService configCacheService;
private final IotModbusFrameEncoder frameEncoder;
/**
* TCP 事务 ID 自增器(与 PollScheduler 共享)
*/
private final AtomicInteger transactionIdCounter;
public IotModbusTcpServerDownstreamHandler(IotModbusTcpServerConnectionManager connectionManager,
IotModbusTcpServerConfigCacheService configCacheService,
IotModbusFrameEncoder frameEncoder,
AtomicInteger transactionIdCounter) {
this.connectionManager = connectionManager;
this.configCacheService = configCacheService;
this.frameEncoder = frameEncoder;
this.transactionIdCounter = transactionIdCounter;
}
/**
* 处理下行消息
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
public void handle(IotDeviceMessage message) {
// 1.1 检查是否是属性设置消息
if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
return;
}
if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod());
return;
}
// 1.2 获取设备配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
if (config == null) {
log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
return;
}
// 1.3 获取连接信息
ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
if (connInfo == null) {
log.warn("[handle][设备 {} 没有连接]", message.getDeviceId());
return;
}
// 2. 解析属性值并写入
Object params = message.getParams();
if (!(params instanceof Map)) {
log.warn("[handle][params 不是 Map 类型: {}]", params);
return;
}
Map<String, Object> propertyMap = (Map<String, Object>) params;
for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
String identifier = entry.getKey();
Object value = entry.getValue();
// 2.1 查找对应的点位配置
IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
if (point == null) {
log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
continue;
}
// 2.2 检查是否支持写操作
if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
continue;
}
// 2.3 执行写入
writeProperty(config.getDeviceId(), connInfo, point, value);
}
}
/**
* 写入属性值
*/
private void writeProperty(Long deviceId, ConnectionInfo connInfo,
IotModbusPointRespDTO point, Object value) {
// 1.1 转换属性值为原始值
int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
// 1.2 确定帧格式和事务 ID
IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
Assert.notNull(frameFormat, "连接帧格式不能为空");
Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
? (transactionIdCounter.incrementAndGet() & 0xFFFF)
: null;
int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
// 1.3 编码写请求
byte[] data;
int readFunctionCode = point.getFunctionCode();
Integer writeSingleCode = IotModbusCommonUtils.getWriteSingleFunctionCode(readFunctionCode);
Integer writeMultipleCode = IotModbusCommonUtils.getWriteMultipleFunctionCode(readFunctionCode);
if (rawValues.length == 1 && writeSingleCode != null) {
// 单个值使用单写功能码FC05/FC06
data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode,
point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
} else if (writeMultipleCode != null) {
// 多个值使用多写功能码FC15/FC16
if (writeMultipleCode == IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS) {
data = frameEncoder.encodeWriteMultipleCoilsRequest(slaveId,
point.getRegisterAddress(), rawValues, frameFormat, transactionId);
} else {
data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId,
point.getRegisterAddress(), rawValues, frameFormat, transactionId);
}
} else {
log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode);
return;
}
// 2. 发送消息
connectionManager.sendToDevice(deviceId, data).onSuccess(v ->
log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
deviceId, point.getIdentifier(), value)
).onFailure(e ->
log.error("[writeProperty][写入失败, deviceId={}, identifier={}, value={}]",
deviceId, point.getIdentifier(), value, e)
);
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus TCP Server 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotModbusTcpServerDownstreamHandler downstreamHandler;
public IotModbusTcpServerDownstreamSubscriber(IotModbusTcpServerProtocol protocol,
IotModbusTcpServerDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = downstreamHandler;
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@@ -0,0 +1,280 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
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.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler;
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.net.NetSocket;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* IoT Modbus TCP Server 上行数据处理器
* <p>
* 处理:
* 1. 自定义 FC 认证
* 2. 轮询响应 → 点位翻译 → thing.property.post
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerUpstreamHandler {
private static final String METHOD_AUTH = "auth";
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService messageService;
private final IotModbusFrameEncoder frameEncoder;
private final IotModbusTcpServerConnectionManager connectionManager;
private final IotModbusTcpServerConfigCacheService configCacheService;
private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
private final IotModbusTcpServerPollScheduler pollScheduler;
private final IotDeviceService deviceService;
private final String serverId;
public IotModbusTcpServerUpstreamHandler(IotDeviceCommonApi deviceApi,
IotDeviceMessageService messageService,
IotModbusFrameEncoder frameEncoder,
IotModbusTcpServerConnectionManager connectionManager,
IotModbusTcpServerConfigCacheService configCacheService,
IotModbusTcpServerPendingRequestManager pendingRequestManager,
IotModbusTcpServerPollScheduler pollScheduler,
IotDeviceService deviceService,
String serverId) {
this.deviceApi = deviceApi;
this.messageService = messageService;
this.frameEncoder = frameEncoder;
this.connectionManager = connectionManager;
this.configCacheService = configCacheService;
this.pendingRequestManager = pendingRequestManager;
this.pollScheduler = pollScheduler;
this.deviceService = deviceService;
this.serverId = serverId;
}
// ========== 帧处理入口 ==========
/**
* 处理帧
*/
public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
if (frame == null) {
return;
}
// 1. 异常响应
if (frame.isException()) {
log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]",
frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode());
return;
}
// 2. 情况一:自定义功能码(认证等扩展)
if (StrUtil.isNotEmpty(frame.getCustomData())) {
handleCustomFrame(socket, frame, frameFormat);
return;
}
// 3. 情况二:标准 Modbus 响应 → 轮询响应处理
handlePollingResponse(socket, frame, frameFormat);
}
// ========== 自定义 FC 处理(认证等) ==========
/**
* 处理自定义功能码帧
* <p>
* 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler}
*/
private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
String method = null;
try {
IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class);
if (message == null) {
throw invalidParamException("自定义 FC 数据解析失败");
}
method = message.getMethod();
if (METHOD_AUTH.equals(method)) {
handleAuth(socket, frame, frameFormat, message.getParams());
return;
}
log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]",
method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData());
} catch (ServiceException e) {
// 已知业务异常,返回对应的错误码和错误信息
sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage());
} catch (IllegalArgumentException e) {
// 参数校验异常,返回 400 错误
sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage());
} catch (Exception e) {
// 其他未知异常,返回 500 错误
log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]",
frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e);
sendCustomResponse(socket, frame, frameFormat, method,
INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
}
/**
* 处理认证请求
*/
@SuppressWarnings("DataFlowIssue")
private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) {
// 1. 解析认证参数
IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
Assert.notNull(request, "认证参数不能为空");
Assert.notBlank(request.getUsername(), "username 不能为空");
Assert.notBlank(request.getPassword(), "password 不能为空");
// 特殊:考虑到 modbus 消息体积较小,默认 clientId 传递空串
if (StrUtil.isBlank(request.getClientId())) {
request.setClientId(IotDeviceAuthUtils.buildClientIdFromUsername(request.getUsername()));
}
Assert.notBlank(request.getClientId(), "clientId 不能为空");
// 2.1 调用认证 API
CommonResult<Boolean> result = deviceApi.authDevice(request);
result.checkError();
if (BooleanUtil.isFalse(result.getData())) {
log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername());
sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "认证失败");
return;
}
// 2.2 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername());
Assert.notNull(deviceInfo, "解析设备信息失败");
// 2.3 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notNull(device, "设备不存在");
// 2.4 加载设备 Modbus 配置,无配置则阻断认证
IotModbusDeviceConfigRespDTO modbusConfig = configCacheService.loadDeviceConfig(device.getId());
if (modbusConfig == null) {
log.warn("[handleAuth][设备 {} 没有 Modbus 点位配置, 拒绝认证]", device.getId());
sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "设备无 Modbus 配置");
return;
}
// 2.5 协议不一致,阻断认证
if (ObjUtil.notEqual(frameFormat.getFormat(), modbusConfig.getFrameFormat())) {
log.warn("[handleAuth][设备 {} frameFormat 不一致, 连接协议={}, 设备配置={},拒绝认证]",
device.getId(), frameFormat.getFormat(), modbusConfig.getFrameFormat());
sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(),
"frameFormat 协议不一致");
return;
}
// 3.1 注册连接
ConnectionInfo connectionInfo = new ConnectionInfo()
.setDeviceId(device.getId())
.setProductKey(deviceInfo.getProductKey())
.setDeviceName(deviceInfo.getDeviceName())
.setSlaveId(frame.getSlaveId())
.setFrameFormat(frameFormat);
connectionManager.registerConnection(socket, connectionInfo);
// 3.2 发送上线消息
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
// 3.3 发送成功响应
sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH,
GlobalErrorCodeConstants.SUCCESS.getCode(), "success");
log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId());
// 4. 启动轮询
pollScheduler.updatePolling(modbusConfig);
}
/**
* 发送自定义功能码响应
*/
private void sendCustomResponse(NetSocket socket, IotModbusFrame frame,
IotModbusFrameFormatEnum frameFormat,
String method, int code, String message) {
Map<String, Object> response = MapUtil.<String, Object>builder()
.put("method", method)
.put("code", code)
.put("message", message)
.build();
byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response),
frameFormat, frame.getTransactionId());
connectionManager.sendToSocket(socket, data);
}
// ========== 轮询响应处理 ==========
/**
* 处理轮询响应(云端轮询模式)
*/
private void handlePollingResponse(NetSocket socket, IotModbusFrame frame,
IotModbusFrameFormatEnum frameFormat) {
// 1. 获取连接信息(未认证连接丢弃)
ConnectionInfo info = connectionManager.getConnectionInfo(socket);
if (info == null) {
log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress());
return;
}
// 2.1 匹配 PendingRequest
PendingRequest request = pendingRequestManager.matchResponse(
info.getDeviceId(), frame, frameFormat);
if (request == null) {
log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]",
info.getDeviceId(), frame.getFunctionCode());
return;
}
// 2.2 提取寄存器值
int[] rawValues = IotModbusCommonUtils.extractValues(frame);
if (rawValues == null) {
log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]",
info.getDeviceId(), request.getIdentifier());
return;
}
// 2.3 查找点位配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId());
IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId());
if (point == null) {
return;
}
// 3.1 转换原始值为物模型属性值(点位翻译)
Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point);
// 3.2 构造属性上报消息
Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
IotDeviceMessage message = IotDeviceMessage.requestOf(
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
// 4. 发送到消息总线
messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]",
info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue);
}
}

View File

@@ -0,0 +1,118 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.IotModbusDeviceConfigListReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT Modbus TCP Server 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpServerConfigCacheService {
private final IotDeviceCommonApi deviceApi;
/**
* 配置缓存deviceId -> 配置
*/
private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
/**
* 加载单个设备的配置(认证成功后调用)
*
* @param deviceId 设备 ID
* @return 设备配置
*/
public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) {
try {
// 1. 从远程 API 获取配置
IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO()
.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setMode(IotModbusModeEnum.POLLING.getMode())
.setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType())
.setDeviceIds(Collections.singleton(deviceId));
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(reqDTO);
result.checkError();
IotModbusDeviceConfigRespDTO modbusConfig = CollUtil.getFirst(result.getData());
if (modbusConfig == null) {
log.warn("[loadDeviceConfig][远程获取配置失败,未找到数据, deviceId={}]", deviceId);
return null;
}
// 2. 更新缓存并返回
configCache.put(modbusConfig.getDeviceId(), modbusConfig);
return modbusConfig;
} catch (Exception e) {
log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e);
return null;
}
}
/**
* 刷新已连接设备的配置缓存
* <p>
* 定时调用,从远程 API 拉取最新配置,只更新已连接设备的缓存。
*
* @param connectedDeviceIds 当前已连接的设备 ID 集合
* @return 已连接设备的最新配置列表
*/
public List<IotModbusDeviceConfigRespDTO> refreshConnectedDeviceConfigList(Set<Long> connectedDeviceIds) {
if (CollUtil.isEmpty(connectedDeviceIds)) {
return Collections.emptyList();
}
try {
// 1. 从远程获取已连接设备的配置
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
.setMode(IotModbusModeEnum.POLLING.getMode())
.setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType())
.setDeviceIds(connectedDeviceIds));
List<IotModbusDeviceConfigRespDTO> modbusConfigs = result.getCheckedData();
// 2. 更新缓存并返回
for (IotModbusDeviceConfigRespDTO config : modbusConfigs) {
configCache.put(config.getDeviceId(), config);
}
return modbusConfigs;
} catch (Exception e) {
log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e);
return null;
}
}
/**
* 获取设备配置
*/
public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
IotModbusDeviceConfigRespDTO config = configCache.get(deviceId);
if (config != null) {
return config;
}
// 缓存未命中,从远程 API 获取
return loadDeviceConfig(deviceId);
}
/**
* 移除设备配置缓存(设备断连时调用)
*/
public void removeConfig(Long deviceId) {
configCache.remove(deviceId);
}
}

View File

@@ -0,0 +1,174 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT Modbus TCP Server 连接管理器
* <p>
* 管理设备 TCP 连接socket ↔ 设备双向映射
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerConnectionManager {
/**
* socket → 连接信息
*/
private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
/**
* deviceId → socket
*/
private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
/**
* 连接信息
*/
@Data
@Accessors(chain = true)
public static class ConnectionInfo {
/**
* 设备编号
*/
private Long deviceId;
/**
* 产品标识
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 从站地址
*/
private Integer slaveId;
/**
* 帧格式(首帧自动检测得到)
*/
private IotModbusFrameFormatEnum frameFormat;
}
/**
* 注册已认证的连接
*/
public void registerConnection(NetSocket socket, ConnectionInfo info) {
// 先检查该设备是否有旧连接,若有且不是同一个 socket关闭旧 socket
NetSocket oldSocket = deviceSocketMap.get(info.getDeviceId());
if (oldSocket != null && oldSocket != socket) {
log.info("[registerConnection][设备 {} 存在旧连接, 关闭旧 socket, oldRemote={}, newRemote={}]",
info.getDeviceId(), oldSocket.remoteAddress(), socket.remoteAddress());
connectionMap.remove(oldSocket);
try {
oldSocket.close();
} catch (Exception e) {
log.warn("[registerConnection][关闭旧 socket 失败, deviceId={}, oldRemote={}]",
info.getDeviceId(), oldSocket.remoteAddress(), e);
}
}
// 注册新连接
connectionMap.put(socket, info);
deviceSocketMap.put(info.getDeviceId(), socket);
log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]",
info.getDeviceId(), socket.remoteAddress());
}
/**
* 获取连接信息
*/
public ConnectionInfo getConnectionInfo(NetSocket socket) {
return connectionMap.get(socket);
}
/**
* 根据设备 ID 获取连接信息
*/
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
NetSocket socket = deviceSocketMap.get(deviceId);
return socket != null ? connectionMap.get(socket) : null;
}
/**
* 获取所有已连接设备的 ID 集合
*/
public Set<Long> getConnectedDeviceIds() {
return deviceSocketMap.keySet();
}
/**
* 移除连接
*/
public ConnectionInfo removeConnection(NetSocket socket) {
ConnectionInfo info = connectionMap.remove(socket);
if (info != null && info.getDeviceId() != null) {
// 使用两参数 remove只有当 deviceSocketMap 中对应的 socket 就是当前 socket 时才删除,
// 避免新 socket 已注册后旧 socket 关闭时误删新映射
boolean removed = deviceSocketMap.remove(info.getDeviceId(), socket);
if (removed) {
log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId());
} else {
log.info("[removeConnection][设备 {} 旧连接关闭, 新连接仍在线, 跳过清理]", info.getDeviceId());
}
}
return info;
}
/**
* 发送数据到设备
*
* @return 发送结果 Future
*/
public Future<Void> sendToDevice(Long deviceId, byte[] data) {
NetSocket socket = deviceSocketMap.get(deviceId);
if (socket == null) {
log.warn("[sendToDevice][设备 {} 没有连接]", deviceId);
return Future.failedFuture("设备 " + deviceId + " 没有连接");
}
return sendToSocket(socket, data);
}
/**
* 发送数据到指定 socket
*
* @return 发送结果 Future
*/
public Future<Void> sendToSocket(NetSocket socket, byte[] data) {
return socket.write(Buffer.buffer(data));
}
/**
* 关闭所有连接
*/
public void closeAll() {
// 1. 先复制再清空,避免 closeHandler 回调时并发修改
List<NetSocket> sockets = new ArrayList<>(connectionMap.keySet());
connectionMap.clear();
deviceSocketMap.clear();
// 2. 关闭所有 socketcloseHandler 中 removeConnection 发现 map 为空会安全跳过)
for (NetSocket socket : sockets) {
try {
socket.close();
} catch (Exception e) {
log.error("[closeAll][关闭连接失败]", e);
}
}
}
}

View File

@@ -0,0 +1,154 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Deque;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
/**
* IoT Modbus TCP Server 待响应请求管理器
* <p>
* 管理轮询下发的请求,用于匹配设备响应:
* - TCP 模式:按 transactionId 精确匹配
* - RTU 模式:按 slaveId + functionCode FIFO 匹配
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerPendingRequestManager {
/**
* deviceId → 有序队列
*/
private final Map<Long, Deque<PendingRequest>> pendingRequests = new ConcurrentHashMap<>();
/**
* 待响应请求信息
*/
@Data
@AllArgsConstructor
public static class PendingRequest {
private Long deviceId;
private Long pointId;
private String identifier;
private int slaveId;
private int functionCode;
private int registerAddress;
private int registerCount;
private Integer transactionId;
private long expireAt;
}
/**
* 添加待响应请求
*/
public void addRequest(PendingRequest request) {
pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>())
.addLast(request);
}
/**
* 匹配响应TCP 模式按 transactionIdRTU 模式按 FIFO
*
* @param deviceId 设备 ID
* @param frame 收到的响应帧
* @param frameFormat 帧格式
* @return 匹配到的 PendingRequest没有匹配返回 null
*/
public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame,
IotModbusFrameFormatEnum frameFormat) {
Deque<PendingRequest> queue = pendingRequests.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return null;
}
// TCP 模式:按 transactionId 精确匹配
if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) {
return matchByTransactionId(queue, frame.getTransactionId());
}
// RTU 模式FIFO匹配 slaveId + functionCode + registerCount
int responseRegisterCount = IotModbusCommonUtils.extractRegisterCountFromResponse(frame);
return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount);
}
/**
* 按 transactionId 匹配
*/
private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) {
Iterator<PendingRequest> it = queue.iterator();
while (it.hasNext()) {
PendingRequest req = it.next();
if (req.getTransactionId() != null && req.getTransactionId() == transactionId) {
it.remove();
return req;
}
}
return null;
}
/**
* 按 FIFO 匹配slaveId + functionCode + registerCount
*/
private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode,
int responseRegisterCount) {
Iterator<PendingRequest> it = queue.iterator();
while (it.hasNext()) {
PendingRequest req = it.next();
if (req.getSlaveId() == slaveId
&& req.getFunctionCode() == functionCode
&& (responseRegisterCount <= 0 || req.getRegisterCount() == responseRegisterCount)) {
it.remove();
return req;
}
}
return null;
}
/**
* 清理过期请求
*/
public void cleanupExpired() {
long now = System.currentTimeMillis();
for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) {
Deque<PendingRequest> queue = entry.getValue();
int removed = 0;
Iterator<PendingRequest> it = queue.iterator();
while (it.hasNext()) {
PendingRequest req = it.next();
if (req.getExpireAt() < now) {
it.remove();
removed++;
}
}
if (removed > 0) {
log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed);
}
}
}
/**
* 清理指定设备的所有待响应请求
*/
public void removeDevice(Long deviceId) {
pendingRequests.remove(deviceId);
}
/**
* 清理所有待响应请求
*/
public void clear() {
pendingRequests.clear();
}
}

View File

@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest;
import io.vertx.core.Vertx;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IoT Modbus TCP Server 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerPollScheduler extends AbstractIotModbusPollScheduler {
private final IotModbusTcpServerConnectionManager connectionManager;
private final IotModbusFrameEncoder frameEncoder;
private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
private final IotModbusTcpServerConfigCacheService configCacheService;
private final int requestTimeout;
/**
* TCP 事务 ID 自增器(与 DownstreamHandler 共享)
*/
@Getter
private final AtomicInteger transactionIdCounter;
public IotModbusTcpServerPollScheduler(Vertx vertx,
IotModbusTcpServerConnectionManager connectionManager,
IotModbusFrameEncoder frameEncoder,
IotModbusTcpServerPendingRequestManager pendingRequestManager,
int requestTimeout,
AtomicInteger transactionIdCounter,
IotModbusTcpServerConfigCacheService configCacheService) {
super(vertx);
this.connectionManager = connectionManager;
this.frameEncoder = frameEncoder;
this.pendingRequestManager = pendingRequestManager;
this.requestTimeout = requestTimeout;
this.transactionIdCounter = transactionIdCounter;
this.configCacheService = configCacheService;
}
// ========== 轮询执行 ==========
/**
* 轮询单个点位
*/
@Override
@SuppressWarnings("DuplicatedCode")
protected void pollPoint(Long deviceId, Long pointId) {
// 1.1 从 configCache 获取最新配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
if (config == null || CollUtil.isEmpty(config.getPoints())) {
log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
return;
}
// 1.2 查找点位
IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
if (point == null) {
log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
return;
}
// 2.1 获取连接
ConnectionInfo connection = connectionManager.getConnectionInfoByDeviceId(deviceId);
if (connection == null) {
log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId);
return;
}
// 2.2 获取 slave ID
IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat();
Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId);
Integer slaveId = connection.getSlaveId();
Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId);
// 3.1 编码读请求
Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
? (transactionIdCounter.incrementAndGet() & 0xFFFF)
: null;
byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId);
// 3.2 注册 PendingRequest
PendingRequest pendingRequest = new PendingRequest(
deviceId, point.getId(), point.getIdentifier(),
slaveId, point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(),
transactionId,
System.currentTimeMillis() + requestTimeout);
pendingRequestManager.addRequest(pendingRequest);
// 3.3 发送读请求
connectionManager.sendToDevice(deviceId, data).onSuccess(v ->
log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
deviceId, point.getIdentifier(), point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount())
).onFailure(e ->
log.warn("[pollPoint][发送失败, 设备={}, 点位={}]", deviceId, point.getIdentifier(), e)
);
}
}

View File

@@ -0,0 +1,6 @@
/**
* Modbus TCP Server从站协议设备主动连接网关自定义 FC65 认证后由网关云端轮询
* <p>
* TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT 网关 MQTT 协议配置
*
* @author 芋道源码
*/
@Data
public class IotMqttConfig {
/**
* 最大消息大小(字节)
*/
@NotNull(message = "最大消息大小不能为空")
@Min(value = 1024, message = "最大消息大小不能小于 1024 字节")
private Integer maxMessageSize = 8192;
/**
* 连接超时时间(秒)
*/
@NotNull(message = "连接超时时间不能为空")
@Min(value = 1, message = "连接超时时间不能小于 1 秒")
private Integer connectTimeoutSeconds = 60;
}

View File

@@ -1,79 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 协议:下行消息订阅器
* <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);
}
}
}

View File

@@ -0,0 +1,344 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.mqtt.MqttEndpoint;
import io.vertx.mqtt.MqttServer;
import io.vertx.mqtt.MqttServerOptions;
import io.vertx.mqtt.MqttTopicSubscription;
import io.vertx.mqtt.messages.MqttPublishMessage;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* IoT 网关 MQTT 协议:接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttProtocol implements IotProtocol {
/**
* 注册连接的 clientId 标识
*
* @see #handleEndpoint(MqttEndpoint)
*/
private static final String AUTH_TYPE_REGISTER = "|authType=register|";
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private Vertx vertx;
/**
* MQTT 服务器
*/
private MqttServer mqttServer;
/**
* 连接管理器
*/
private final IotMqttConnectionManager connectionManager;
/**
* 下行消息订阅者
*/
private IotMqttDownstreamSubscriber downstreamSubscriber;
private final IotDeviceMessageService deviceMessageService;
private final IotMqttAuthHandler authHandler;
private final IotMqttRegisterHandler registerHandler;
private final IotMqttUpstreamHandler upstreamHandler;
public IotMqttProtocol(ProtocolProperties properties) {
IotMqttConfig mqttConfig = properties.getMqtt();
Assert.notNull(mqttConfig, "MQTT 协议配置mqtt不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化连接管理器
this.connectionManager = new IotMqttConnectionManager();
// 初始化 Handler
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MQTT;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId());
return;
}
// 1.1 创建 Vertx 实例
this.vertx = Vertx.vertx();
// 1.2 创建服务器选项
IotMqttConfig mqttConfig = properties.getMqtt();
MqttServerOptions options = new MqttServerOptions()
.setPort(properties.getPort())
.setMaxMessageSize(mqttConfig.getMaxMessageSize())
.setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds());
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(sslConfig.getSslKeyPath())
.setCertPath(sslConfig.getSslCertPath());
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
// 1.3 创建服务器并设置连接处理器
mqttServer = MqttServer.create(vertx, options);
mqttServer.endpointHandler(this::handleEndpoint);
// 1.4 启动 MQTT 服务器
try {
mqttServer.listen().result();
running = true;
log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{}serverId{}]",
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 关闭所有连接
connectionManager.closeAll();
// 2.2 关闭 MQTT 服务器
if (mqttServer != null) {
try {
mqttServer.close().result();
log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e);
}
mqttServer = null;
}
// 2.3 关闭 Vertx 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e);
}
vertx = null;
}
running = false;
log.info("[stop][IoT MQTT 协议 {} 已停止]", getId());
}
// ======================================= MQTT 连接处理 ======================================
/**
* 处理 MQTT 连接端点
*
* @param endpoint MQTT 连接端点
*/
private void handleEndpoint(MqttEndpoint endpoint) {
// 1. 如果是注册请求,注册待认证连接;否则走正常认证流程
String clientId = endpoint.clientIdentifier();
if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
// 情况一:设备注册请求
registerHandler.handleRegister(endpoint);
return;
} else {
// 情况二:普通认证请求
if (!authHandler.handleAuthenticationRequest(endpoint)) {
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
return;
}
}
// 2.1 设置异常和关闭处理器
endpoint.exceptionHandler(ex -> {
log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]",
clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
endpoint.close();
});
endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等)
endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文
log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
cleanupConnection(endpoint);
});
// 2.2 设置心跳处理器
endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId));
// 3.1 设置消息处理器
endpoint.publishHandler(message -> processMessage(endpoint, message));
// 3.2 设置 QoS 2 消息的 PUBREL 处理器
endpoint.publishReleaseHandler(endpoint::publishComplete);
// 4.1 设置订阅处理器(带 ACL 校验)
endpoint.subscribeHandler(subscribe -> {
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
List<MqttQoS> grantedQoSLevels = new ArrayList<>();
for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) {
String topicName = sub.topicName();
// 校验主题是否属于当前设备
if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed(
topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) {
grantedQoSLevels.add(sub.qualityOfService());
log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName);
} else {
log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName);
grantedQoSLevels.add(MqttQoS.FAILURE);
}
}
endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
});
// 4.2 设置取消订阅处理器
endpoint.unsubscribeHandler(unsubscribe -> {
log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
});
// 5. 接受连接
endpoint.accept(false);
}
/**
* 处理消息(发布)
*
* @param endpoint MQTT 连接端点
* @param message 发布消息
*/
private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
String clientId = endpoint.clientIdentifier();
try {
// 1. 处理业务消息
String topic = message.topicName();
byte[] payload = message.payload().getBytes();
upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
// 2. 根据 QoS 级别发送相应的确认消息
handleQoSAck(endpoint, message);
} catch (Exception e) {
log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
endpoint.close();
}
}
/**
* 处理 QoS 确认
*
* @param endpoint MQTT 连接端点
* @param message 发布消息
*/
private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) {
if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
// QoS 1: 发送 PUBACK 确认
endpoint.publishAcknowledge(message.messageId());
} else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
// QoS 2: 发送 PUBREC 确认
endpoint.publishReceived(message.messageId());
}
// QoS 0 无需确认
}
/**
* 清理连接
*
* @param endpoint MQTT 连接端点
*/
private void cleanupConnection(MqttEndpoint endpoint) {
try {
// 1. 发送设备离线消息
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
if (connectionInfo != null) {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
}
// 2. 注销连接
connectionManager.unregisterConnection(endpoint);
} catch (Exception e) {
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
endpoint.clientIdentifier(), e.getMessage());
}
}
}

View File

@@ -1,92 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.mqtt.MqttServer;
import io.vertx.mqtt.MqttServerOptions;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 协议:接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttUpstreamProtocol {
private final IotGatewayProperties.MqttProperties mqttProperties;
private final IotDeviceMessageService messageService;
private final IotMqttConnectionManager connectionManager;
private final Vertx vertx;
@Getter
private final String serverId;
private MqttServer mqttServer;
public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties,
IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager,
Vertx vertx) {
this.mqttProperties = mqttProperties;
this.messageService = messageService;
this.connectionManager = connectionManager;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort());
}
// TODO @haohao这里的编写是不是和 tcp 对应的,风格保持一致哈;
@PostConstruct
public void start() {
// 创建服务器选项
MqttServerOptions options = new MqttServerOptions()
.setPort(mqttProperties.getPort())
.setMaxMessageSize(mqttProperties.getMaxMessageSize())
.setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds());
// 配置 SSL如果启用
if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) {
options.setSsl(true)
.setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions())
.setTrustOptions(mqttProperties.getSslOptions().getTrustOptions());
}
// 创建服务器并设置连接处理器
mqttServer = MqttServer.create(vertx, options);
mqttServer.endpointHandler(endpoint -> {
IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager);
handler.handle(endpoint);
});
// 启动服务器
try {
mqttServer.listen().result();
log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort());
} catch (Exception e) {
log.error("[start][IoT 网关 MQTT 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (mqttServer != null) {
try {
mqttServer.close().result();
log.info("[stop][IoT 网关 MQTT 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 MQTT 协议停止失败]", e);
}
}
}
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 协议:下行消息处理器
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class IotMqttDownstreamHandler {
private final IotDeviceMessageService deviceMessageService;
private final IotMqttConnectionManager connectionManager;
/**
* 处理下行消息
*
* @param message 设备消息
*/
public void handle(IotDeviceMessage message) {
try {
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
// 1. 检查设备连接
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
message.getDeviceId());
if (connectionInfo == null) {
log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
return;
}
// 2.1 序列化消息
byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(),
connectionInfo.getDeviceName());
Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空");
// 2.2 构建主题
Assert.notBlank(message.getMethod(), "消息方法不能为空");
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), isReply);
Assert.notBlank(topic, "主题不能为空");
// 3. 发送到设备
boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload,
MqttQoS.AT_LEAST_ONCE.value(), false);
if (!success) {
throw new RuntimeException("下行消息发送失败");
}
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]",
message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length);
} catch (Exception e) {
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
message.getDeviceId(), message.getMethod(), message, e);
}
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 协议:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotMqttDownstreamHandler downstreamHandler;
public IotMqttDownstreamSubscriber(IotMqttProtocol protocol,
IotMqttDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = downstreamHandler;
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttEndpoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 协议的处理器抽象基类
* <p>
* 提供通用的连接校验、响应发送等功能
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public abstract class IotMqttAbstractHandler {
protected final IotMqttConnectionManager connectionManager;
protected final IotDeviceMessageService deviceMessageService;
/**
* 发送成功响应到设备
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param requestId 请求 ID
* @param method 方法名
* @param data 响应数据
*/
@SuppressWarnings("SameParameterValue")
protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String requestId, String method, Object data) {
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null);
writeResponse(endpoint, productKey, deviceName, method, responseMessage);
}
/**
* 发送错误响应到设备
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param requestId 请求 ID
* @param method 方法名
* @param errorCode 错误码
* @param errorMessage 错误消息
*/
protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String requestId, String method, Integer errorCode, String errorMessage) {
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage);
writeResponse(endpoint, productKey, deviceName, method, responseMessage);
}
/**
* 写入响应消息到设备
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param method 方法名
* @param responseMessage 响应消息
*/
private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String method, IotDeviceMessage responseMessage) {
try {
// 1.1 序列化消息(根据设备配置的序列化类型)
byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName);
// 1.2 构建响应主题
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true);
// 2. 发送响应消息
endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false);
log.debug("[writeResponse][发送响应,主题: {}code: {}]", replyTopic, responseMessage.getCode());
} catch (Exception e) {
log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e);
}
}
}

View File

@@ -0,0 +1,119 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.mqtt.MqttEndpoint;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
/**
* IoT 网关 MQTT 认证处理器
* <p>
* 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttAuthHandler extends IotMqttAbstractHandler {
private final IotDeviceCommonApi deviceApi;
private final IotDeviceService deviceService;
private final String serverId;
public IotMqttAuthHandler(IotMqttConnectionManager connectionManager,
IotDeviceMessageService deviceMessageService,
IotDeviceCommonApi deviceApi,
String serverId) {
super(connectionManager, deviceMessageService);
this.deviceApi = deviceApi;
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
this.serverId = serverId;
}
/**
* 处理 MQTT 连接(认证)请求
*
* @param endpoint MQTT 连接端点
* @return 认证是否成功
*/
@SuppressWarnings("DataFlowIssue")
public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier();
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
clientId, username, connectionManager.getEndpointAddress(endpoint));
try {
// 1.1 解析认证参数
Assert.notBlank(clientId, "clientId 不能为空");
Assert.notBlank(username, "username 不能为空");
Assert.notBlank(password, "password 不能为空");
// 1.2 构建认证参数
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
.setClientId(clientId)
.setUsername(username)
.setPassword(password);
// 2.1 执行认证
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
authResult.checkError();
if (BooleanUtil.isFalse(authResult.getData())) {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
Assert.notNull(deviceInfo, "解析设备信息失败");
// 2.3 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notNull(device, "设备不存在");
// 3.1 注册连接
registerConnection(endpoint, device, clientId);
// 3.2 发送设备上线消息
sendOnlineMessage(device);
log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
return true;
} catch (Exception e) {
log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]",
clientId, username, e.getMessage());
return false;
}
}
/**
* 注册连接
*/
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
.setDeviceId(device.getId())
.setProductKey(device.getProductKey())
.setDeviceName(device.getDeviceName())
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
connectionManager.registerConnection(endpoint, connectionInfo);
}
/**
* 发送设备上线消息
*/
private void sendOnlineMessage(IotDeviceRespDTO device) {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
device.getDeviceName(), serverId);
log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
}
}

View File

@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.mqtt.MqttEndpoint;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
/**
* IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
private final IotDeviceCommonApi deviceApi;
public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
IotDeviceMessageService deviceMessageService) {
super(connectionManager, deviceMessageService);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
/**
* 处理注册连接
* <p>
* 通过 MQTT 连接的 username 解析设备信息password 作为签名,直接处理设备注册
*
* @param endpoint MQTT 连接端点
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@SuppressWarnings("DataFlowIssue")
public void handleRegister(MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier();
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
String productKey = null;
String deviceName = null;
try {
// 1.1 校验参数
Assert.notBlank(clientId, "clientId 不能为空");
Assert.notBlank(username, "username 不能为空");
Assert.notBlank(password, "password 不能为空");
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
Assert.notNull(deviceInfo, "解析设备信息失败");
productKey = deviceInfo.getProductKey();
deviceName = deviceInfo.getDeviceName();
log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]",
clientId, productKey, deviceName);
// 1.2 构建注册参数
IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
.setProductKey(productKey)
.setDeviceName(deviceName)
.setSign(password);
// 2. 调用动态注册 API
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
result.checkError();
// 3. 接受连接,并发送成功响应
endpoint.accept(false);
sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData());
log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId);
} catch (Exception e) {
log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
// 接受连接,并发送错误响应
endpoint.accept(false);
sendErrorResponse(endpoint, productKey, deviceName, null, method,
INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
} finally {
// 注册完成后关闭连接(一型一密只用于获取 deviceSecret不保持连接
endpoint.close();
}
}
}

View File

@@ -0,0 +1,79 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.mqtt.MqttEndpoint;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
private final String serverId;
public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
IotDeviceMessageService deviceMessageService,
String serverId) {
super(connectionManager, deviceMessageService);
this.serverId = serverId;
}
/**
* 处理业务消息
*
* @param endpoint MQTT 连接端点
* @param topic 主题
* @param payload 消息内容
*/
public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) {
String clientId = endpoint.clientIdentifier();
try {
// 1.1 基础检查
if (ArrayUtil.isEmpty(payload)) {
return;
}
// 1.2 解析主题,获取 productKey 和 deviceName
String[] topicParts = topic.split("/");
String productKey = ArrayUtil.get(topicParts, 2);
String deviceName = ArrayUtil.get(topicParts, 3);
Assert.notBlank(productKey, "产品 Key 不能为空");
Assert.notBlank(deviceName, "设备名称不能为空");
// 1.3 校验设备信息,防止伪造设备消息
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
Assert.notNull(connectionInfo, "无法获取连接信息");
Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
// 1.4 校验 topic 是否允许发布
if (!IotMqttTopicUtils.isTopicPublishAllowed(topic, productKey, deviceName)) {
log.warn("[handleBusinessRequest][topic 不允许发布,客户端 ID: {},主题: {}]", clientId, topic);
return;
}
// 2.1 反序列化消息
IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
if (message == null) {
log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
return;
}
// 2.2 标准化回复消息的 methodMQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
IotMqttTopicUtils.normalizeReplyMethod(message);
// 3. 处理业务消息
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic);
} catch (Exception e) {
log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage(), e);
}
}
}

View File

@@ -8,6 +8,8 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -66,7 +68,6 @@ public class IotMqttConnectionManager {
} catch (Exception ignored) {
// 连接已关闭,忽略异常
}
return realTimeAddress;
}
@@ -74,24 +75,24 @@ public class IotMqttConnectionManager {
* 注册设备连接(包含认证信息)
*
* @param endpoint MQTT 连接端点
* @param deviceId 设备 ID
* @param connectionInfo 连接信息
*/
public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) {
Long deviceId = connectionInfo.getDeviceId();
// 如果设备已有其他连接,先清理旧连接
MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
if (oldEndpoint != null && oldEndpoint != endpoint) {
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
deviceId, getEndpointAddress(oldEndpoint));
oldEndpoint.close();
// 清理旧连接的映射
// 先清理映射,再关闭连接(避免旧连接处理器干扰)
connectionMap.remove(oldEndpoint);
oldEndpoint.close();
}
// 注册新连接
connectionMap.put(endpoint, connectionInfo);
deviceEndpointMap.put(deviceId, endpoint);
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {}product key: {}device name: {}]",
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {}productKey: {}deviceName: {}]",
deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
}
@@ -129,25 +130,10 @@ public class IotMqttConnectionManager {
if (endpoint == null) {
return null;
}
// 获取连接信息
return getConnectionInfo(endpoint);
}
/**
* 检查设备是否在线
*/
public boolean isDeviceOnline(Long deviceId) {
return deviceEndpointMap.containsKey(deviceId);
}
/**
* 检查设备是否离线
*/
public boolean isDeviceOffline(Long deviceId) {
return !isDeviceOnline(deviceId);
}
/**
* 发送消息到设备
*
@@ -182,6 +168,24 @@ public class IotMqttConnectionManager {
return deviceEndpointMap.get(deviceId);
}
/**
* 关闭所有连接
*/
public void closeAll() {
// 1. 先复制再清空,避免 closeHandler 回调时并发修改
List<MqttEndpoint> endpoints = new ArrayList<>(connectionMap.keySet());
connectionMap.clear();
deviceEndpointMap.clear();
// 2. 关闭所有连接closeHandler 中 unregisterConnection 发现 map 为空会安全跳过)
for (MqttEndpoint endpoint : endpoints) {
try {
endpoint.close();
} catch (Exception ignored) {
// 连接可能已关闭,忽略异常
}
}
}
/**
* 连接信息
*/
@@ -192,27 +196,15 @@ public class IotMqttConnectionManager {
* 设备 ID
*/
private Long deviceId;
/**
* 产品 Key
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 客户端 ID
*/
private String clientId;
/**
* 是否已认证
*/
private boolean authenticated;
/**
* 连接地址
*/

View File

@@ -1,132 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 MQTT 协议:下行消息处理器
* <p>
* 专门处理下行消息的业务逻辑,包括:
* 1. 消息编码
* 2. 主题构建
* 3. 消息发送
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttDownstreamHandler {
private final IotDeviceMessageService deviceMessageService;
private final IotMqttConnectionManager connectionManager;
public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService,
IotMqttConnectionManager connectionManager) {
this.deviceMessageService = deviceMessageService;
this.connectionManager = connectionManager;
}
/**
* 处理下行消息
*
* @param message 设备消息
* @return 是否处理成功
*/
public boolean handleDownstreamMessage(IotDeviceMessage message) {
try {
// 1. 基础校验
if (message == null || message.getDeviceId() == null) {
log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
return false;
}
// 2. 检查设备是否在线
if (connectionManager.isDeviceOffline(message.getDeviceId())) {
log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID{}]", message.getDeviceId());
return false;
}
// 3. 获取连接信息
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
if (connectionInfo == null) {
log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID{}]", message.getDeviceId());
return false;
}
// 4. 编码消息
byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(),
connectionInfo.getDeviceName());
if (payload == null || payload.length == 0) {
log.warn("[handleDownstreamMessage][消息编码失败,设备 ID{}]", message.getDeviceId());
return false;
}
// 5. 发送消息到设备
return sendMessageToDevice(message, connectionInfo, payload);
} catch (Exception e) {
if (message != null) {
log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID{},错误:{}]",
message.getDeviceId(), e.getMessage(), e);
}
return false;
}
}
/**
* 发送消息到设备
*
* @param message 设备消息
* @param connectionInfo 连接信息
* @param payload 消息负载
* @return 是否发送成功
*/
private boolean sendMessageToDevice(IotDeviceMessage message,
IotMqttConnectionManager.ConnectionInfo connectionInfo,
byte[] payload) {
// 1. 构建主题
String topic = buildDownstreamTopic(message, connectionInfo);
if (StrUtil.isBlank(topic)) {
log.warn("[sendMessageToDevice][主题构建失败,设备 ID{},方法:{}]",
message.getDeviceId(), message.getMethod());
return false;
}
// 2. 发送消息
boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false);
if (success) {
log.debug("[sendMessageToDevice][消息发送成功,设备 ID{},主题:{},方法:{}]",
message.getDeviceId(), topic, message.getMethod());
} else {
log.warn("[sendMessageToDevice][消息发送失败,设备 ID{},主题:{},方法:{}]",
message.getDeviceId(), topic, message.getMethod());
}
return success;
}
/**
* 构建下行消息主题
*
* @param message 设备消息
* @param connectionInfo 连接信息
* @return 主题
*/
private String buildDownstreamTopic(IotDeviceMessage message,
IotMqttConnectionManager.ConnectionInfo connectionInfo) {
String method = message.getMethod();
if (StrUtil.isBlank(method)) {
return null;
}
// 使用工具类构建主题,支持回复消息处理
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), isReply);
}
}

View File

@@ -1,511 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.mqtt.MqttEndpoint;
import io.vertx.mqtt.MqttTopicSubscription;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* MQTT 上行消息处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotMqttUpstreamHandler {
/**
* 默认编解码类型MQTT 使用 Alink 协议)
*/
private static final String DEFAULT_CODEC_TYPE = "Alink";
/**
* register 请求的 topic 后缀
*/
private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
private final IotDeviceMessageService deviceMessageService;
private final IotMqttConnectionManager connectionManager;
private final IotDeviceCommonApi deviceApi;
private final String serverId;
public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol,
IotDeviceMessageService deviceMessageService,
IotMqttConnectionManager connectionManager) {
this.deviceMessageService = deviceMessageService;
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.connectionManager = connectionManager;
this.serverId = protocol.getServerId();
}
/**
* 处理 MQTT 连接
*
* @param endpoint MQTT 连接端点
*/
public void handle(MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier();
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
clientId, username, connectionManager.getEndpointAddress(endpoint));
// 1. 先进行认证
if (!authenticateDevice(clientId, username, password, endpoint)) {
log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username);
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
return;
}
log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
// 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
endpoint.pingHandler(v -> {
log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
// Vert.x 会自动发送 PINGRESP 响应,无需手动处理
});
// 3. 设置异常和关闭处理器
endpoint.exceptionHandler(ex -> {
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
cleanupConnection(endpoint);
});
endpoint.closeHandler(v -> {
cleanupConnection(endpoint);
});
// 4. 设置消息处理器
endpoint.publishHandler(mqttMessage -> {
try {
// 4.1 根据 topic 判断是否为 register 请求
String topic = mqttMessage.topicName();
byte[] payload = mqttMessage.payload().getBytes();
if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
// register 请求:使用默认编解码器处理(设备可能未注册)
processRegisterMessage(clientId, topic, payload, endpoint);
} else {
// 业务请求:正常处理
processMessage(clientId, topic, payload);
}
// 4.2 根据 QoS 级别发送相应的确认消息
if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
// QoS 1: 发送 PUBACK 确认
endpoint.publishAcknowledge(mqttMessage.messageId());
} else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
// QoS 2: 发送 PUBREC 确认
endpoint.publishReceived(mqttMessage.messageId());
}
// QoS 0 无需确认
} catch (Exception e) {
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
cleanupConnection(endpoint);
endpoint.close();
}
});
// 5. 设置订阅处理器
endpoint.subscribeHandler(subscribe -> {
// 提取主题名称列表用于日志显示
List<String> topicNames = subscribe.topicSubscriptions().stream()
.map(MqttTopicSubscription::topicName)
.collect(java.util.stream.Collectors.toList());
log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
// 提取 QoS 列表
List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream()
.map(MqttTopicSubscription::qualityOfService)
.collect(java.util.stream.Collectors.toList());
endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
});
// 6. 设置取消订阅处理器
endpoint.unsubscribeHandler(unsubscribe -> {
log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
});
// 7. 设置 QoS 2消息的 PUBREL 处理器
endpoint.publishReleaseHandler(endpoint::publishComplete);
// 8. 设置断开连接处理器
endpoint.disconnectHandler(v -> {
log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
cleanupConnection(endpoint);
});
// 9. 接受连接
endpoint.accept(false);
}
/**
* 处理消息
*
* @param clientId 客户端 ID
* @param topic 主题
* @param payload 消息内容
*/
private void processMessage(String clientId, String topic, byte[] payload) {
// 1. 基础检查
if (payload == null || payload.length == 0) {
return;
}
// 2. 解析主题,获取 productKey 和 deviceName
String[] topicParts = topic.split("/");
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
return;
}
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName
String productKey = topicParts[2];
String deviceName = topicParts[3];
try {
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
if (message == null) {
log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
return;
}
// 4. 处理业务消息(认证已在连接时完成)
log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
productKey, deviceName, message.getMethod());
handleBusinessRequest(message, productKey, deviceName);
} catch (Exception e) {
log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage(), e);
}
}
/**
* 在 MQTT 连接时进行设备认证
*
* @param clientId 客户端 ID
* @param username 用户名
* @param password 密码
* @param endpoint MQTT 连接端点
* @return 认证是否成功
*/
private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) {
try {
// 1. 参数校验
if (StrUtil.hasEmpty(clientId, username, password)) {
log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username);
return false;
}
// 2. 构建认证参数
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
.setClientId(clientId)
.setUsername(username)
.setPassword(password);
// 3. 调用设备认证 API
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]",
clientId, username, authResult.getMsg());
return false;
}
// 4. 获取设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
return false;
}
IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO()
.setProductKey(deviceInfo.getProductKey())
.setDeviceName(deviceInfo.getDeviceName());
CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO);
if (!deviceResult.isSuccess() || deviceResult.getData() == null) {
log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]",
clientId, username, deviceResult.getMsg());
return false;
}
// 5. 注册连接
IotDeviceRespDTO device = deviceResult.getData();
registerConnection(endpoint, device, clientId);
// 6. 发送设备上线消息
sendOnlineMessage(device);
return true;
} catch (Exception e) {
log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e);
return false;
}
}
/**
* 处理 register 消息(设备动态注册,使用默认编解码器)
*
* @param clientId 客户端 ID
* @param topic 主题
* @param payload 消息内容
* @param endpoint MQTT 连接端点
*/
private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
// 1.1 基础检查
if (ArrayUtil.isEmpty(payload)) {
return;
}
// 1.2 解析主题,获取 productKey 和 deviceName
String[] topicParts = topic.split("/");
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
return;
}
String productKey = topicParts[2];
String deviceName = topicParts[3];
// 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType
IotDeviceMessage message;
try {
message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
if (message == null) {
log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
return;
}
} catch (Exception e) {
log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage(), e);
return;
}
// 3. 处理设备动态注册请求
log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
productKey, deviceName, message.getMethod());
try {
handleRegisterRequest(message, productKey, deviceName, endpoint);
} catch (Exception e) {
log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage(), e);
}
}
/**
* 处理设备动态注册请求(一型一密,不需要 deviceSecret
*
* @param message 消息信息
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param endpoint MQTT 连接端点
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier();
try {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
if (params == null) {
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整");
return;
}
// 2. 调用动态注册 API
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
if (result.isError()) {
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg());
return;
}
// 3. 发送成功响应(包含 deviceSecret
sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData());
log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
params.getDeviceName(), clientId);
} catch (Exception e) {
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常");
}
}
/**
* 解析注册参数
*
* @param params 参数对象(通常为 Map 类型)
* @return 注册参数 DTO解析失败时返回 null
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
if (params == null) {
return null;
}
try {
// 参数默认为 Map 类型,直接转换
if (params instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) params;
return new IotDeviceRegisterReqDTO()
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
}
// 如果已经是目标类型,直接返回
if (params instanceof IotDeviceRegisterReqDTO) {
return (IotDeviceRegisterReqDTO) params;
}
// 其他情况尝试 JSON 转换
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
} catch (Exception e) {
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
return null;
}
}
/**
* 发送注册成功响应(包含 deviceSecret
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param requestId 请求 ID
* @param registerResp 注册响应
*/
private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String requestId, IotDeviceRegisterRespDTO registerResp) {
try {
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
// 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
// 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
MqttQoS.AT_LEAST_ONCE, false, false);
log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic);
} catch (Exception e) {
log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]",
endpoint.clientIdentifier(), e);
}
}
/**
* 发送注册错误响应
*
* @param endpoint MQTT 连接端点
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param requestId 请求 ID
* @param errorMessage 错误消息
*/
private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
String requestId, String errorMessage) {
try {
// 1. 构建响应消息
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage);
// 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
// 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
MqttQoS.AT_LEAST_ONCE, false, false);
log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic);
} catch (Exception e) {
log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]",
endpoint.clientIdentifier(), e);
}
}
/**
* 处理业务请求
*/
private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) {
// 发送消息到消息总线
message.setServerId(serverId);
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
}
/**
* 注册连接
*/
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
.setDeviceId(device.getId())
.setProductKey(device.getProductKey())
.setDeviceName(device.getDeviceName())
.setClientId(clientId)
.setAuthenticated(true)
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
}
/**
* 发送设备上线消息
*/
private void sendOnlineMessage(IotDeviceRespDTO device) {
try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
device.getDeviceName(), serverId);
log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
} catch (Exception e) {
log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage());
}
}
/**
* 清理连接
*/
private void cleanupConnection(MqttEndpoint endpoint) {
try {
IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
if (connectionInfo != null) {
// 发送设备离线消息
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
}
// 注销连接
connectionManager.unregisterConnection(endpoint);
} catch (Exception e) {
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
}
}
}

View File

@@ -1,4 +1,4 @@
/**
* 提供设备接入的各种协议的实现
* 设备接入协议MQTT、EMQX、HTTP、TCP 等协议的实现
*/
package cn.iocoder.yudao.module.iot.gateway.protocol;
package cn.iocoder.yudao.module.iot.gateway.protocol;

View File

@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT TCP 协议配置
*
* @author 芋道源码
*/
@Data
public class IotTcpConfig {
/**
* 最大连接数
*/
@NotNull(message = "最大连接数不能为空")
@Min(value = 1, message = "最大连接数必须大于 0")
private Integer maxConnections = 1000;
/**
* 心跳超时时间(毫秒)
*/
@NotNull(message = "心跳超时时间不能为空")
@Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒")
private Long keepAliveTimeoutMs = 30000L;
/**
* 拆包配置
*/
@Valid
private CodecConfig codec;
/**
* TCP 拆包配置
*/
@Data
public static class CodecConfig {
/**
* 拆包类型
*
* @see IotTcpCodecTypeEnum
*/
@NotNull(message = "拆包类型不能为空")
private String type;
/**
* LENGTH_FIELD: 长度字段偏移量
* <p>
* 表示长度字段在消息中的起始位置(从 0 开始)
*/
private Integer lengthFieldOffset;
/**
* LENGTH_FIELD: 长度字段长度(字节数)
* <p>
* 常见值1最大 255、2最大 65535、4最大 2GB
*/
private Integer lengthFieldLength;
/**
* LENGTH_FIELD: 长度调整值
* <p>
* 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度
*/
private Integer lengthAdjustment = 0;
/**
* LENGTH_FIELD: 跳过的初始字节数
* <p>
* 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength
*/
private Integer initialBytesToStrip = 0;
/**
* DELIMITER: 分隔符
* <p>
* 支持转义字符:\n换行、\r回车、\r\n回车换行
*/
private String delimiter;
/**
* FIXED_LENGTH: 固定消息长度(字节)
* <p>
* 每条消息的固定长度
*/
private Integer fixedLength;
}
}

View File

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

View File

@@ -0,0 +1,207 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
import io.vertx.core.Vertx;
import io.vertx.core.net.NetServer;
import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT TCP 协议实现
* <p>
* 基于 Vert.x 实现 TCP 服务器,接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotTcpProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private Vertx vertx;
/**
* TCP 服务器
*/
private NetServer tcpServer;
/**
* TCP 连接管理器
*/
private final IotTcpConnectionManager connectionManager;
/**
* 下行消息订阅者
*/
private IotTcpDownstreamSubscriber downstreamSubscriber;
/**
* 消息序列化器
*/
private final IotMessageSerializer serializer;
/**
* TCP 帧编解码器
*/
private final IotTcpFrameCodec frameCodec;
public IotTcpProtocol(ProtocolProperties properties) {
IotTcpConfig tcpConfig = properties.getTcp();
Assert.notNull(tcpConfig, "TCP 协议配置tcp不能为空");
Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置tcp.codec不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化序列化器
IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize());
Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize());
IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class);
this.serializer = serializerManager.get(serializeType);
// 初始化帧编解码器
this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec());
// 初始化连接管理器
this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections());
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.TCP;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId());
return;
}
// 1.1 创建 Vertx 实例
this.vertx = Vertx.vertx();
// 1.2 创建服务器选项
IotTcpConfig tcpConfig = properties.getTcp();
NetServerOptions options = new NetServerOptions()
.setPort(properties.getPort())
.setTcpKeepAlive(true)
.setTcpNoDelay(true)
.setReuseAddress(true)
.setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(sslConfig.getSslKeyPath())
.setCertPath(sslConfig.getSslCertPath());
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
// 1.3 创建服务器并设置连接处理器
tcpServer = vertx.createNetServer(options);
tcpServer.connectHandler(socket -> {
IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(serverId, frameCodec, serializer, connectionManager);
handler.handle(socket);
});
// 1.4 启动 TCP 服务器
try {
tcpServer.listen().result();
running = true;
log.info("[start][IoT TCP 协议 {} 启动成功,端口:{}serverId{}]",
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer);
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 关闭所有连接
connectionManager.closeAll();
// 2.2 关闭 TCP 服务器
if (tcpServer != null) {
try {
tcpServer.close().result();
log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e);
}
tcpServer = null;
}
// 2.3 关闭 Vertx 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e);
}
vertx = null;
}
running = false;
log.info("[stop][IoT TCP 协议 {} 已停止]", getId());
}
}

View File

@@ -1,99 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.core.net.NetServer;
import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 TCP 协议:接收设备上行消息
*
* @author 芋道源码
*/
@Slf4j
public class IotTcpUpstreamProtocol {
private final IotGatewayProperties.TcpProperties tcpProperties;
private final IotDeviceService deviceService;
private final IotDeviceMessageService messageService;
private final IotTcpConnectionManager connectionManager;
private final Vertx vertx;
@Getter
private final String serverId;
private NetServer tcpServer;
public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
Vertx vertx) {
this.tcpProperties = tcpProperties;
this.deviceService = deviceService;
this.messageService = messageService;
this.connectionManager = connectionManager;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
}
@PostConstruct
public void start() {
// 创建服务器选项
NetServerOptions options = new NetServerOptions()
.setPort(tcpProperties.getPort())
.setTcpKeepAlive(true)
.setTcpNoDelay(true)
.setReuseAddress(true);
// 配置 SSL如果启用
if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(tcpProperties.getSslKeyPath())
.setCertPath(tcpProperties.getSslCertPath());
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
// 创建服务器并设置连接处理器
tcpServer = vertx.createNetServer(options);
tcpServer.connectHandler(socket -> {
IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService,
connectionManager);
handler.handle(socket);
});
// 启动服务器
try {
tcpServer.listen().result();
log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort());
} catch (Exception e) {
log.error("[start][IoT 网关 TCP 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (tcpServer != null) {
try {
tcpServer.close().result();
log.info("[stop][IoT 网关 TCP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 TCP 协议停止失败]", e);
}
}
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IoT TCP 拆包类型枚举
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public enum IotTcpCodecTypeEnum {
/**
* 基于固定长度的拆包
*/
FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec.class),
/**
* 基于分隔符的拆包
*/
DELIMITER("delimiter", IotTcpDelimiterFrameCodec.class),
/**
* 基于长度字段的拆包
*/
LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec.class),
;
/**
* 类型标识
*/
private final String type;
/**
* 编解码器类
*/
private final Class<? extends IotTcpFrameCodec> codecClass;
/**
* 根据类型获取枚举
*
* @param type 类型标识
* @return 枚举值
*/
public static IotTcpCodecTypeEnum of(String type) {
return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values());
}
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;
/**
* IoT TCP 帧编解码器接口
* <p>
* 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力
*
* @author 芋道源码
*/
public interface IotTcpFrameCodec {
/**
* 获取编解码器类型
*
* @return 编解码器类型
*/
IotTcpCodecTypeEnum getType();
/**
* 创建解码器RecordParser
* <p>
* 每个连接调用一次,返回的 parser 需绑定到 socket.handler()
*
* @param handler 消息处理器,当收到完整的消息帧后回调
* @return RecordParser 实例
*/
RecordParser createDecodeParser(Handler<Buffer> handler);
/**
* 编码消息(加帧)
* <p>
* 根据不同的编解码类型添加帧头/分隔符
*
* @param data 原始数据
* @return 编码后的数据(带帧头/分隔符)
*/
Buffer encode(byte[] data);
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
/**
* IoT TCP 帧编解码器工厂
*
* @author 芋道源码
*/
public class IotTcpFrameCodecFactory {
/**
* 根据配置创建编解码器
*
* @param config 拆包配置
* @return 编解码器实例,如果配置为空则返回 null
*/
public static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) {
Assert.notNull(config, "CodecConfig 不能为空");
IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType());
Assert.notNull(type, "不支持的 CodecType 类型:" + config.getType());
return ReflectUtil.newInstance(type.getCodecClass(), config);
}
}

View File

@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j;
/**
* IoT TCP 分隔符帧编解码器
* <p>
* 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符
* <p>
* 支持的分隔符:
* <ul>
* <li>\n - 换行符</li>
* <li>\r - 回车符</li>
* <li>\r\n - 回车换行</li>
* <li>自定义字符串</li>
* </ul>
*
* @author 芋道源码
*/
@Slf4j
public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
/**
* 最大记录大小64KB防止 DoS 攻击
*/
private static final int MAX_RECORD_SIZE = 65536;
/**
* 解析后的分隔符字节数组
*/
private final byte[] delimiterBytes;
public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
this.delimiterBytes = parseDelimiter(config.getDelimiter());
}
@Override
public IotTcpCodecTypeEnum getType() {
return IotTcpCodecTypeEnum.DELIMITER;
}
@Override
public RecordParser createDecodeParser(Handler<Buffer> handler) {
RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes));
parser.maxRecordSize(MAX_RECORD_SIZE); // 设置最大记录大小,防止 DoS 攻击
// 处理完整消息(不包含分隔符)
parser.handler(handler);
parser.exceptionHandler(ex -> {
throw new RuntimeException("[createDecodeParser][解析异常]", ex);
});
return parser;
}
@Override
public Buffer encode(byte[] data) {
Buffer buffer = Buffer.buffer();
buffer.appendBytes(data);
buffer.appendBytes(delimiterBytes);
return buffer;
}
/**
* 解析分隔符字符串为字节数组
* <p>
* 支持转义字符:\n、\r、\r\n、\t
*
* @param delimiter 分隔符字符串
* @return 分隔符字节数组
*/
private byte[] parseDelimiter(String delimiter) {
// 处理转义字符
String parsed = delimiter
.replace("\\r\\n", "\r\n")
.replace("\\r", "\r")
.replace("\\n", "\n")
.replace("\\t", "\t");
return StrUtil.utf8Bytes(parsed);
}
}

View File

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

View File

@@ -0,0 +1,181 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
/**
* IoT TCP 长度字段帧编解码器
* <p>
* 基于长度字段的拆包策略,消息格式:[长度字段][消息体]
* <p>
* 参数说明:
* <ul>
* <li>lengthFieldOffset: 长度字段在消息中的偏移量</li>
* <li>lengthFieldLength: 长度字段的字节数1/2/4</li>
* <li>lengthAdjustment: 长度调整值,用于调整长度字段的实际含义</li>
* <li>initialBytesToStrip: 解码后跳过的字节数</li>
* </ul>
*
* @author 芋道源码
*/
@Slf4j
public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec {
/**
* 最大帧长度64KB防止 DoS 攻击
*/
private static final int MAX_FRAME_LENGTH = 65536;
private final int lengthFieldOffset;
private final int lengthFieldLength;
private final int lengthAdjustment;
private final int initialBytesToStrip;
/**
* 头部长度 = 长度字段偏移量 + 长度字段长度
*/
private final int headerLength;
public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) {
Assert.notNull(config.getLengthFieldOffset(), "lengthFieldOffset 不能为空");
Assert.notNull(config.getLengthFieldLength(), "lengthFieldLength 不能为空");
Assert.notNull(config.getLengthAdjustment(), "lengthAdjustment 不能为空");
Assert.notNull(config.getInitialBytesToStrip(), "initialBytesToStrip 不能为空");
this.lengthFieldOffset = config.getLengthFieldOffset();
this.lengthFieldLength = config.getLengthFieldLength();
this.lengthAdjustment = config.getLengthAdjustment();
this.initialBytesToStrip = config.getInitialBytesToStrip();
this.headerLength = lengthFieldOffset + lengthFieldLength;
}
@Override
public IotTcpCodecTypeEnum getType() {
return IotTcpCodecTypeEnum.LENGTH_FIELD;
}
@Override
public RecordParser createDecodeParser(Handler<Buffer> handler) {
// 创建状态机:先读取头部,再读取消息体
RecordParser parser = RecordParser.newFixed(headerLength);
parser.maxRecordSize(MAX_FRAME_LENGTH); // 设置最大记录大小,防止 DoS 攻击
final AtomicReference<Integer> bodyLength = new AtomicReference<>(null); // 消息体长度null 表示读取头部阶段
final AtomicReference<Buffer> headerBuffer = new AtomicReference<>(null); // 头部消息
// 处理读取到的数据
parser.handler(buffer -> {
if (bodyLength.get() == null) {
// 阶段 1: 读取头部,解析长度字段
headerBuffer.set(buffer.copy());
int length = readLength(buffer, lengthFieldOffset, lengthFieldLength);
int frameBodyLength = length + lengthAdjustment;
// 检查帧长度是否合法
if (frameBodyLength < 0) {
throw new IllegalStateException(String.format(
"[createDecodeParser][帧长度异常length: %d, frameBodyLength: %d]",
length, frameBodyLength));
}
// 消息体为空,抛出异常
if (frameBodyLength == 0) {
throw new IllegalStateException("[createDecodeParser][消息体不能为空]");
}
// 【重要】切换到读取消息体模式
bodyLength.set(frameBodyLength);
parser.fixedSizeMode(frameBodyLength);
} else {
// 阶段 2: 读取消息体,组装完整帧
Buffer frame = processFrame(headerBuffer.get(), buffer);
// 重置状态,准备读取下一帧
bodyLength.set(null);
headerBuffer.set(null);
parser.fixedSizeMode(headerLength);
// 【重要】处理完整消息
handler.handle(frame);
}
});
parser.exceptionHandler(ex -> {
throw new RuntimeException("[createDecodeParser][解析异常]", ex);
});
return parser;
}
@Override
public Buffer encode(byte[] data) {
Buffer buffer = Buffer.buffer();
// 计算要写入的长度值
int lengthValue = data.length - lengthAdjustment;
// 写入偏移量前的填充字节(如果有)
for (int i = 0; i < lengthFieldOffset; i++) {
buffer.appendByte((byte) 0);
}
// 写入长度字段
writeLength(buffer, lengthValue, lengthFieldLength);
// 写入消息体
buffer.appendBytes(data);
return buffer;
}
/**
* 从 Buffer 中读取长度字段
*/
@SuppressWarnings("EnhancedSwitchMigration")
private int readLength(Buffer buffer, int offset, int length) {
switch (length) {
case 1:
return buffer.getUnsignedByte(offset);
case 2:
return buffer.getUnsignedShort(offset);
case 4:
return buffer.getInt(offset);
default:
throw new IllegalArgumentException("不支持的长度字段长度: " + length);
}
}
/**
* 向 Buffer 中写入长度字段
*/
private void writeLength(Buffer buffer, int length, int fieldLength) {
switch (fieldLength) {
case 1:
buffer.appendByte((byte) length);
break;
case 2:
buffer.appendShort((short) length);
break;
case 4:
buffer.appendInt(length);
break;
default:
throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength);
}
}
/**
* 处理帧数据(根据 initialBytesToStrip 跳过指定字节)
*/
private Buffer processFrame(Buffer header, Buffer body) {
Buffer fullFrame = Buffer.buffer();
if (header != null) {
fullFrame.appendBuffer(header);
}
if (body != null) {
fullFrame.appendBuffer(body);
}
// 根据 initialBytesToStrip 跳过指定字节
if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) {
return fullFrame.slice(initialBytesToStrip, fullFrame.length());
}
return fullFrame;
}
}

View File

@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
import io.vertx.core.buffer.Buffer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 TCP 下行消息处理器
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class IotTcpDownstreamHandler {
private final IotTcpConnectionManager connectionManager;
/**
* TCP 帧编解码器(处理粘包/拆包)
*/
private final IotTcpFrameCodec codec;
/**
* 消息序列化器(处理业务消息序列化/反序列化)
*/
private final IotMessageSerializer serializer;
/**
* 处理下行消息
*/
public void handle(IotDeviceMessage message) {
try {
// 1.1 检查是否是属性设置消息
if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
return;
}
if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
return;
}
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
// 1.2 检查设备连接
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
message.getDeviceId());
if (connectionInfo == null) {
log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
return;
}
// 2. 序列化 + 帧编码
byte[] payload = serializer.serialize(message);
Buffer frameData = codec.encode(payload);
// 3. 发送到设备
boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes());
if (!success) {
throw new RuntimeException("下行消息发送失败");
}
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
message.getDeviceId(), message.getMethod(), message.getId(), frameData.length());
} catch (Exception e) {
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
message.getDeviceId(), message.getMethod(), message, e);
}
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 TCP 下游订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
public class IotTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotTcpDownstreamHandler downstreamHandler;
public IotTcpDownstreamSubscriber(IotProtocol protocol,
IotTcpDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = downstreamHandler;
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@@ -0,0 +1,328 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
/**
* TCP 上行消息处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotTcpUpstreamHandler implements Handler<NetSocket> {
private static final String AUTH_METHOD = "auth";
private final String serverId;
/**
* TCP 帧编解码器(处理粘包/拆包)
*/
private final IotTcpFrameCodec codec;
/**
* 消息序列化器(处理业务消息序列化/反序列化)
*/
private final IotMessageSerializer serializer;
/**
* TCP 连接管理器
*/
private final IotTcpConnectionManager connectionManager;
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceService deviceService;
private final IotDeviceCommonApi deviceApi;
public IotTcpUpstreamHandler(String serverId,
IotTcpFrameCodec codec,
IotMessageSerializer serializer,
IotTcpConnectionManager connectionManager) {
Assert.notNull(codec, "TCP FrameCodec 必须配置");
Assert.notNull(serializer, "消息序列化器必须配置");
Assert.notNull(connectionManager, "连接管理器不能为空");
this.serverId = serverId;
this.codec = codec;
this.serializer = serializer;
this.connectionManager = connectionManager;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
public void handle(NetSocket socket) {
String remoteAddress = String.valueOf(socket.remoteAddress());
log.debug("[handle][设备连接,地址: {}]", remoteAddress);
// 1. 设置异常和关闭处理器
socket.exceptionHandler(ex -> {
log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex);
socket.close();
});
socket.closeHandler(v -> {
log.debug("[handle][连接关闭,地址: {}]", remoteAddress);
cleanupConnection(socket);
});
// 2.1 设置消息处理器
Handler<Buffer> messageHandler = buffer -> {
try {
processMessage(buffer, socket);
} catch (Exception e) {
log.error("[handle][消息处理失败,地址: {}]", remoteAddress, e);
socket.close();
}
};
// 2.2 使用拆包器处理粘包/拆包
RecordParser parser = codec.createDecodeParser(messageHandler);
socket.handler(parser);
log.debug("[handle][启用 {} 拆包器,地址: {}]", codec.getType(), remoteAddress);
}
/**
* 处理消息
*
* @param buffer 消息
* @param socket 网络连接
*/
private void processMessage(Buffer buffer, NetSocket socket) {
IotDeviceMessage message = null;
try {
// 1. 反序列化消息
message = serializer.deserialize(buffer.getBytes());
if (message == null) {
sendErrorResponse(socket, null, null, BAD_REQUEST.getCode(), "消息反序列化失败");
return;
}
// 2. 根据消息类型路由处理
if (AUTH_METHOD.equals(message.getMethod())) {
// 认证请求
handleAuthenticationRequest(message, socket);
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
// 设备动态注册请求
handleRegisterRequest(message, socket);
} else {
// 业务消息
handleBusinessRequest(message, socket);
}
} catch (ServiceException e) {
// 业务异常,返回对应的错误码和错误信息
log.warn("[processMessage][业务异常,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage());
String requestId = message != null ? message.getRequestId() : null;
String method = message != null ? message.getMethod() : null;
sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage());
} catch (IllegalArgumentException e) {
// 参数校验失败,返回 400
log.warn("[processMessage][参数校验失败,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage());
String requestId = message != null ? message.getRequestId() : null;
String method = message != null ? message.getMethod() : null;
sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage());
} catch (Exception e) {
// 其他异常,返回 500并重新抛出让上层关闭连接
log.error("[processMessage][处理消息失败,地址: {}]", socket.remoteAddress(), e);
String requestId = message != null ? message.getRequestId() : null;
String method = message != null ? message.getMethod() : null;
sendErrorResponse(socket, requestId, method,
INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
throw e;
}
}
/**
* 处理认证请求
*
* @param message 消息信息
* @param socket 网络连接
*/
@SuppressWarnings("DuplicatedCode")
private void handleAuthenticationRequest(IotDeviceMessage message, NetSocket socket) {
// 1. 解析认证参数
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
Assert.notNull(authParams, "认证参数不能为空");
Assert.notBlank(authParams.getUsername(), "username 不能为空");
Assert.notBlank(authParams.getPassword(), "password 不能为空");
// 2.1 执行认证
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
authResult.checkError();
if (BooleanUtil.isFalse(authResult.getData())) {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
Assert.notNull(deviceInfo, "解析设备信息失败");
// 2.3 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notNull(device, "设备不存在");
// 3.1 注册连接
registerConnection(socket, device);
// 3.2 发送上线消息
sendOnlineMessage(device);
// 3.3 发送成功响应
sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功");
log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName());
}
/**
* 处理设备动态注册请求(一型一密,不需要认证)
*
* @param message 消息信息
* @param socket 网络连接
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@SuppressWarnings("DuplicatedCode")
private void handleRegisterRequest(IotDeviceMessage message, NetSocket socket) {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
Assert.notNull(params, "注册参数不能为空");
Assert.notBlank(params.getProductKey(), "productKey 不能为空");
Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(params.getSign(), "sign 不能为空");
// 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
result.checkError();
// 3. 发送成功响应
sendSuccessResponse(socket, message.getRequestId(),
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData());
log.info("[handleRegisterRequest][注册成功,地址: {},设备名: {}]",
socket.remoteAddress(), params.getDeviceName());
}
/**
* 处理业务请求
*
* @param message 消息信息
* @param socket 网络连接
*/
private void handleBusinessRequest(IotDeviceMessage message, NetSocket socket) {
// 1. 获取认证信息并处理业务消息
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
if (connectionInfo == null) {
log.error("[handleBusinessRequest][无法获取连接信息,地址: {}]", socket.remoteAddress());
sendErrorResponse(socket, message.getRequestId(), message.getMethod(),
UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息");
return;
}
// 2. 发送消息到消息总线
deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
log.info("[handleBusinessRequest][发送消息到消息总线,地址: {},消息: {}]", socket.remoteAddress(), message);
}
/**
* 注册连接信息
*
* @param socket 网络连接
* @param device 设备
*/
private void registerConnection(NetSocket socket, IotDeviceRespDTO device) {
IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo()
.setDeviceId(device.getId())
.setProductKey(device.getProductKey())
.setDeviceName(device.getDeviceName());
connectionManager.registerConnection(socket, device.getId(), connectionInfo);
}
/**
* 发送设备上线消息
*
* @param device 设备信息
*/
private void sendOnlineMessage(IotDeviceRespDTO device) {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
device.getDeviceName(), serverId);
}
/**
* 清理连接
*
* @param socket 网络连接
*/
private void cleanupConnection(NetSocket socket) {
// 1. 发送离线消息
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
if (connectionInfo != null) {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
connectionInfo.getDeviceName(), serverId);
}
// 2. 注销连接
connectionManager.unregisterConnection(socket);
}
// ===================== 发送响应消息 =====================
/**
* 发送成功响应
*
* @param socket 网络连接
* @param requestId 请求 ID
* @param method 方法名
* @param data 响应数据
*/
private void sendSuccessResponse(NetSocket socket, String requestId, String method, Object data) {
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null);
writeResponse(socket, responseMessage);
}
/**
* 发送错误响应
*
* @param socket 网络连接
* @param requestId 请求 ID
* @param method 方法名
* @param code 错误码
* @param msg 错误消息
*/
private void sendErrorResponse(NetSocket socket, String requestId, String method, Integer code, String msg) {
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg);
writeResponse(socket, responseMessage);
}
/**
* 写入响应到 Socket
*
* @param socket 网络连接
* @param responseMessage 响应消息
*/
private void writeResponse(NetSocket socket, IotDeviceMessage responseMessage) {
byte[] serializedData = serializer.serialize(responseMessage);
Buffer frameData = codec.encode(serializedData);
socket.write(frameData);
}
}

View File

@@ -4,8 +4,9 @@ import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -20,9 +21,13 @@ import java.util.concurrent.ConcurrentHashMap;
* @author 芋道源码
*/
@Slf4j
@Component
public class IotTcpConnectionManager {
/**
* 最大连接数
*/
private final int maxConnections;
/**
* 连接信息映射NetSocket -> 连接信息
*/
@@ -33,6 +38,10 @@ public class IotTcpConnectionManager {
*/
private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
public IotTcpConnectionManager(int maxConnections) {
this.maxConnections = maxConnections;
}
/**
* 注册设备连接(包含认证信息)
*
@@ -40,15 +49,19 @@ public class IotTcpConnectionManager {
* @param deviceId 设备 ID
* @param connectionInfo 连接信息
*/
public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
// 检查连接数是否已达上限(同步方法确保检查和注册的原子性)
if (connectionMap.size() >= maxConnections) {
throw new IllegalStateException("连接数已达上限: " + maxConnections);
}
// 如果设备已有其他连接,先清理旧连接
NetSocket oldSocket = deviceSocketMap.get(deviceId);
if (oldSocket != null && oldSocket != socket) {
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
deviceId, oldSocket.remoteAddress());
oldSocket.close();
// 清理旧连接的映射
// 先清理映射,再关闭连接
connectionMap.remove(oldSocket);
oldSocket.close();
}
// 注册新连接
@@ -69,25 +82,11 @@ public class IotTcpConnectionManager {
return;
}
Long deviceId = connectionInfo.getDeviceId();
deviceSocketMap.remove(deviceId);
// 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接
deviceSocketMap.remove(deviceId, socket);
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress());
}
/**
* 检查连接是否已认证
*/
public boolean isAuthenticated(NetSocket socket) {
ConnectionInfo info = connectionMap.get(socket);
return info != null;
}
/**
* 检查连接是否未认证
*/
public boolean isNotAuthenticated(NetSocket socket) {
return !isAuthenticated(socket);
}
/**
* 获取连接信息
*/
@@ -125,6 +124,24 @@ public class IotTcpConnectionManager {
}
}
/**
* 关闭所有连接
*/
public void closeAll() {
// 1. 先复制再清空,避免 closeHandler 回调时并发修改
List<NetSocket> sockets = new ArrayList<>(connectionMap.keySet());
connectionMap.clear();
deviceSocketMap.clear();
// 2. 关闭所有连接closeHandler 中 unregisterConnection 发现 map 为空会安全跳过)
for (NetSocket socket : sockets) {
try {
socket.close();
} catch (Exception ignored) {
// 连接可能已关闭,忽略异常
}
}
}
/**
* 连接信息(包含认证信息)
*/
@@ -144,15 +161,6 @@ public class IotTcpConnectionManager {
*/
private String deviceName;
/**
* 客户端 ID
*/
private String clientId;
/**
* 消息编解码类型(认证后确定)
*/
private String codecType;
}
}

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