Commit 890d304340f1d20c52cf5288d47956eaed5814ee
1 parent
deab8c1c
【功能完善】IoT: 更新 Vert.x 版本至 4.5.1,新增 EMQX 插件及其相关配置,重构 MQTT 插件以支持 Vert.x MQTT 服务器,优化插件启动和停止逻辑,更新插件描述信息。
Showing
10 changed files
with
508 additions
and
41 deletions
yudao-dependencies/pom.xml
| @@ -626,6 +626,12 @@ | @@ -626,6 +626,12 @@ | ||
| 626 | <artifactId>vertx-web</artifactId> | 626 | <artifactId>vertx-web</artifactId> |
| 627 | <version>${vertx.version}</version> | 627 | <version>${vertx.version}</version> |
| 628 | </dependency> | 628 | </dependency> |
| 629 | + <!-- Vert.x MQTT 模块 --> | ||
| 630 | + <dependency> | ||
| 631 | + <groupId>io.vertx</groupId> | ||
| 632 | + <artifactId>vertx-mqtt</artifactId> | ||
| 633 | + <version>${vertx.version}</version> | ||
| 634 | + </dependency> | ||
| 629 | 635 | ||
| 630 | </dependencies> | 636 | </dependencies> |
| 631 | </dependencyManagement> | 637 | </dependencyManagement> |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/plugin.properties
0 → 100644
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/pom.xml
0 → 100644
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 3 | + xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation=" | ||
| 4 | + http://maven.apache.org/POM/4.0.0 | ||
| 5 | + http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| 6 | + <parent> | ||
| 7 | + <artifactId>yudao-module-iot-plugin</artifactId> | ||
| 8 | + <groupId>cn.iocoder.boot</groupId> | ||
| 9 | + <version>${revision}</version> | ||
| 10 | + </parent> | ||
| 11 | + <modelVersion>4.0.0</modelVersion> | ||
| 12 | + <packaging>jar</packaging> | ||
| 13 | + | ||
| 14 | + <artifactId>yudao-module-iot-emqx-plugin</artifactId> | ||
| 15 | + | ||
| 16 | + <name>${project.artifactId}</name> | ||
| 17 | + <description> | ||
| 18 | + 物联网 插件模块 - emqx 插件 | ||
| 19 | + </description> | ||
| 20 | + | ||
| 21 | + <properties> | ||
| 22 | + <!-- 插件相关 --> | ||
| 23 | + <plugin.id>emqx-plugin</plugin.id> | ||
| 24 | + <plugin.class>cn.iocoder.yudao.module.iot.plugin.EmqxPlugin</plugin.class> | ||
| 25 | + <plugin.version>0.0.1</plugin.version> | ||
| 26 | + <plugin.provider>ahh</plugin.provider> | ||
| 27 | + <plugin.description>emqx-plugin-0.0.1</plugin.description> | ||
| 28 | + <plugin.dependencies/> | ||
| 29 | + </properties> | ||
| 30 | + | ||
| 31 | + <build> | ||
| 32 | + <plugins> | ||
| 33 | + <!-- DOESN'T WORK WITH MAVEN 3 (I defined the plugin metadata in properties section) | ||
| 34 | + <plugin> | ||
| 35 | + <groupId>org.codehaus.mojo</groupId> | ||
| 36 | + <artifactId>properties-maven-plugin</artifactId> | ||
| 37 | + <version>1.0-alpha-2</version> | ||
| 38 | + <executions> | ||
| 39 | + <execution> | ||
| 40 | + <phase>initialize</phase> | ||
| 41 | + <goals> | ||
| 42 | + <goal>read-project-properties</goal> | ||
| 43 | + </goals> | ||
| 44 | + <configuration> | ||
| 45 | + <files> | ||
| 46 | + <file>plugin.properties</file> | ||
| 47 | + </files> | ||
| 48 | + </configuration> | ||
| 49 | + </execution> | ||
| 50 | + </executions> | ||
| 51 | + </plugin> | ||
| 52 | + --> | ||
| 53 | + | ||
| 54 | + <plugin> | ||
| 55 | + <groupId>org.apache.maven.plugins</groupId> | ||
| 56 | + <artifactId>maven-antrun-plugin</artifactId> | ||
| 57 | + <version>1.6</version> | ||
| 58 | + <executions> | ||
| 59 | + <execution> | ||
| 60 | + <id>unzip jar file</id> | ||
| 61 | + <phase>package</phase> | ||
| 62 | + <configuration> | ||
| 63 | + <target> | ||
| 64 | + <unzip src="target/${project.artifactId}-${project.version}.${project.packaging}" | ||
| 65 | + dest="target/plugin-classes"/> | ||
| 66 | + </target> | ||
| 67 | + </configuration> | ||
| 68 | + <goals> | ||
| 69 | + <goal>run</goal> | ||
| 70 | + </goals> | ||
| 71 | + </execution> | ||
| 72 | + </executions> | ||
| 73 | + </plugin> | ||
| 74 | + | ||
| 75 | + <plugin> | ||
| 76 | + <artifactId>maven-assembly-plugin</artifactId> | ||
| 77 | + <version>2.3</version> | ||
| 78 | + <configuration> | ||
| 79 | + <descriptors> | ||
| 80 | + <descriptor> | ||
| 81 | + src/main/assembly/assembly.xml | ||
| 82 | + </descriptor> | ||
| 83 | + </descriptors> | ||
| 84 | + <appendAssemblyId>false</appendAssemblyId> | ||
| 85 | + </configuration> | ||
| 86 | + <executions> | ||
| 87 | + <execution> | ||
| 88 | + <id>make-assembly</id> | ||
| 89 | + <phase>package</phase> | ||
| 90 | + <goals> | ||
| 91 | + <goal>attached</goal> | ||
| 92 | + </goals> | ||
| 93 | + </execution> | ||
| 94 | + </executions> | ||
| 95 | + </plugin> | ||
| 96 | + | ||
| 97 | + <plugin> | ||
| 98 | + <groupId>org.apache.maven.plugins</groupId> | ||
| 99 | + <artifactId>maven-jar-plugin</artifactId> | ||
| 100 | + <version>2.4</version> | ||
| 101 | + <configuration> | ||
| 102 | + <archive> | ||
| 103 | + <manifestEntries> | ||
| 104 | + <Plugin-Id>${plugin.id}</Plugin-Id> | ||
| 105 | + <Plugin-Class>${plugin.class}</Plugin-Class> | ||
| 106 | + <Plugin-Version>${plugin.version}</Plugin-Version> | ||
| 107 | + <Plugin-Provider>${plugin.provider}</Plugin-Provider> | ||
| 108 | + <Plugin-Description>${plugin.description}</Plugin-Description> | ||
| 109 | + <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies> | ||
| 110 | + </manifestEntries> | ||
| 111 | + </archive> | ||
| 112 | + </configuration> | ||
| 113 | + </plugin> | ||
| 114 | + | ||
| 115 | + <plugin> | ||
| 116 | + <artifactId>maven-deploy-plugin</artifactId> | ||
| 117 | + <configuration> | ||
| 118 | + <skip>true</skip> | ||
| 119 | + </configuration> | ||
| 120 | + </plugin> | ||
| 121 | + </plugins> | ||
| 122 | + </build> | ||
| 123 | + | ||
| 124 | + <dependencies> | ||
| 125 | + <!-- 其他依赖项 --> | ||
| 126 | + <dependency> | ||
| 127 | + <groupId>org.springframework.boot</groupId> | ||
| 128 | + <artifactId>spring-boot-starter-web</artifactId> | ||
| 129 | + </dependency> | ||
| 130 | + <!-- PF4J Spring 集成 --> | ||
| 131 | + <dependency> | ||
| 132 | + <groupId>org.pf4j</groupId> | ||
| 133 | + <artifactId>pf4j-spring</artifactId> | ||
| 134 | + <scope>provided</scope> | ||
| 135 | + </dependency> | ||
| 136 | + <!-- 项目依赖 --> | ||
| 137 | + <dependency> | ||
| 138 | + <groupId>cn.iocoder.boot</groupId> | ||
| 139 | + <artifactId>yudao-module-iot-api</artifactId> | ||
| 140 | + <version>${revision}</version> | ||
| 141 | + </dependency> | ||
| 142 | + <dependency> | ||
| 143 | + <groupId>org.projectlombok</groupId> | ||
| 144 | + <artifactId>lombok</artifactId> | ||
| 145 | + <version>${lombok.version}</version> | ||
| 146 | + <scope>provided</scope> | ||
| 147 | + </dependency> | ||
| 148 | + <!-- Vert.x 核心依赖 --> | ||
| 149 | + <dependency> | ||
| 150 | + <groupId>io.vertx</groupId> | ||
| 151 | + <artifactId>vertx-core</artifactId> | ||
| 152 | + </dependency> | ||
| 153 | + <!-- Vert.x Web 模块 --> | ||
| 154 | + <dependency> | ||
| 155 | + <groupId>io.vertx</groupId> | ||
| 156 | + <artifactId>vertx-web</artifactId> | ||
| 157 | + </dependency> | ||
| 158 | + <!-- MQTT --> | ||
| 159 | + <dependency> | ||
| 160 | + <groupId>org.eclipse.paho</groupId> | ||
| 161 | + <artifactId>org.eclipse.paho.client.mqttv3</artifactId> | ||
| 162 | + </dependency> | ||
| 163 | + </dependencies> | ||
| 164 | +</project> | ||
| 0 | \ No newline at end of file | 165 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/src/main/assembly/assembly.xml
0 → 100644
| 1 | +<assembly> | ||
| 2 | + <id>plugin</id> | ||
| 3 | + <formats> | ||
| 4 | + <format>zip</format> | ||
| 5 | + </formats> | ||
| 6 | + <includeBaseDirectory>false</includeBaseDirectory> | ||
| 7 | + <dependencySets> | ||
| 8 | + <dependencySet> | ||
| 9 | + <useProjectArtifact>false</useProjectArtifact> | ||
| 10 | + <scope>runtime</scope> | ||
| 11 | + <outputDirectory>lib</outputDirectory> | ||
| 12 | + <includes> | ||
| 13 | + <include>*:jar:*</include> | ||
| 14 | + </includes> | ||
| 15 | + </dependencySet> | ||
| 16 | + </dependencySets> | ||
| 17 | + <!-- | ||
| 18 | + <fileSets> | ||
| 19 | + <fileSet> | ||
| 20 | + <directory>target/classes</directory> | ||
| 21 | + <outputDirectory>classes</outputDirectory> | ||
| 22 | + </fileSet> | ||
| 23 | + </fileSets> | ||
| 24 | + --> | ||
| 25 | + <fileSets> | ||
| 26 | + <fileSet> | ||
| 27 | + <directory>target/plugin-classes</directory> | ||
| 28 | + <outputDirectory>classes</outputDirectory> | ||
| 29 | + </fileSet> | ||
| 30 | + </fileSets> | ||
| 31 | +</assembly> |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/EmqxPlugin.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.plugin; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.api.ServiceRegistry; | ||
| 4 | +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | +import org.pf4j.Plugin; | ||
| 7 | +import org.pf4j.PluginWrapper; | ||
| 8 | + | ||
| 9 | +import javax.annotation.Resource; | ||
| 10 | +import java.util.concurrent.ExecutorService; | ||
| 11 | +import java.util.concurrent.Executors; | ||
| 12 | + | ||
| 13 | +@Slf4j | ||
| 14 | +public class EmqxPlugin extends Plugin { | ||
| 15 | + | ||
| 16 | + private ExecutorService executorService; | ||
| 17 | + @Resource | ||
| 18 | + private DeviceDataApi deviceDataApi; | ||
| 19 | + | ||
| 20 | + public EmqxPlugin(PluginWrapper wrapper) { | ||
| 21 | + super(wrapper); | ||
| 22 | + this.executorService = Executors.newSingleThreadExecutor(); | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + @Override | ||
| 26 | + public void start() { | ||
| 27 | + log.info("EmqxPlugin.start()"); | ||
| 28 | + | ||
| 29 | + if (executorService.isShutdown() || executorService.isTerminated()) { | ||
| 30 | + executorService = Executors.newSingleThreadExecutor(); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class); | ||
| 34 | + if (deviceDataApi == null) { | ||
| 35 | + log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!"); | ||
| 36 | + return; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public void stop() { | ||
| 43 | + log.info("EmqxPlugin.stop()"); | ||
| 44 | + } | ||
| 45 | +} | ||
| 0 | \ No newline at end of file | 46 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml
| @@ -147,20 +147,11 @@ | @@ -147,20 +147,11 @@ | ||
| 147 | <version>${lombok.version}</version> | 147 | <version>${lombok.version}</version> |
| 148 | <scope>provided</scope> | 148 | <scope>provided</scope> |
| 149 | </dependency> | 149 | </dependency> |
| 150 | - <!-- Vert.x 核心依赖 --> | ||
| 151 | - <dependency> | ||
| 152 | - <groupId>io.vertx</groupId> | ||
| 153 | - <artifactId>vertx-core</artifactId> | ||
| 154 | - </dependency> | ||
| 155 | - <!-- Vert.x Web 模块 --> | 150 | + <!-- Vert.x Web --> |
| 156 | <dependency> | 151 | <dependency> |
| 157 | <groupId>io.vertx</groupId> | 152 | <groupId>io.vertx</groupId> |
| 158 | <artifactId>vertx-web</artifactId> | 153 | <artifactId>vertx-web</artifactId> |
| 159 | - </dependency> | ||
| 160 | - <!-- MQTT --> | ||
| 161 | - <dependency> | ||
| 162 | - <groupId>org.eclipse.paho</groupId> | ||
| 163 | - <artifactId>org.eclipse.paho.client.mqttv3</artifactId> | 154 | + <version>4.5.11</version> |
| 164 | </dependency> | 155 | </dependency> |
| 165 | </dependencies> | 156 | </dependencies> |
| 166 | </project> | 157 | </project> |
| 167 | \ No newline at end of file | 158 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/plugin.properties
| 1 | plugin.id=mqtt-plugin | 1 | plugin.id=mqtt-plugin |
| 2 | +plugin.description=Vert.x MQTT plugin | ||
| 2 | plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin | 3 | plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin |
| 3 | -plugin.version=0.0.1 | 4 | +plugin.version=1.0.0 |
| 5 | +plugin.requires= | ||
| 4 | plugin.provider=ahh | 6 | plugin.provider=ahh |
| 5 | -plugin.dependencies= | ||
| 6 | -plugin.description=mqtt-plugin-0.0.1 | 7 | +plugin.license=Apache-2.0 |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/pom.xml
| @@ -145,10 +145,11 @@ | @@ -145,10 +145,11 @@ | ||
| 145 | <version>${lombok.version}</version> | 145 | <version>${lombok.version}</version> |
| 146 | <scope>provided</scope> | 146 | <scope>provided</scope> |
| 147 | </dependency> | 147 | </dependency> |
| 148 | - <!-- MQTT --> | 148 | + <!-- Vert.x MQTT --> |
| 149 | <dependency> | 149 | <dependency> |
| 150 | - <groupId>org.eclipse.paho</groupId> | ||
| 151 | - <artifactId>org.eclipse.paho.client.mqttv3</artifactId> | 150 | + <groupId>io.vertx</groupId> |
| 151 | + <artifactId>vertx-mqtt</artifactId> | ||
| 152 | + <version>4.5.11</version> | ||
| 152 | </dependency> | 153 | </dependency> |
| 153 | </dependencies> | 154 | </dependencies> |
| 154 | </project> | 155 | </project> |
| 155 | \ No newline at end of file | 156 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java
| 1 | package cn.iocoder.yudao.module.iot.plugin; | 1 | package cn.iocoder.yudao.module.iot.plugin; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.module.iot.api.ServiceRegistry; | ||
| 4 | -import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi; | ||
| 5 | import lombok.extern.slf4j.Slf4j; | 3 | import lombok.extern.slf4j.Slf4j; |
| 6 | import org.pf4j.Plugin; | 4 | import org.pf4j.Plugin; |
| 7 | import org.pf4j.PluginWrapper; | 5 | import org.pf4j.PluginWrapper; |
| 8 | 6 | ||
| 9 | -import javax.annotation.Resource; | ||
| 10 | -import java.util.concurrent.ExecutorService; | ||
| 11 | -import java.util.concurrent.Executors; | ||
| 12 | - | ||
| 13 | @Slf4j | 7 | @Slf4j |
| 14 | public class MqttPlugin extends Plugin { | 8 | public class MqttPlugin extends Plugin { |
| 15 | 9 | ||
| 16 | - private ExecutorService executorService; | ||
| 17 | - @Resource | ||
| 18 | - private DeviceDataApi deviceDataApi; | 10 | + private MqttServerExtension mqttServerExtension; |
| 19 | 11 | ||
| 20 | public MqttPlugin(PluginWrapper wrapper) { | 12 | public MqttPlugin(PluginWrapper wrapper) { |
| 21 | super(wrapper); | 13 | super(wrapper); |
| 22 | - this.executorService = Executors.newSingleThreadExecutor(); | ||
| 23 | } | 14 | } |
| 24 | 15 | ||
| 25 | @Override | 16 | @Override |
| 26 | public void start() { | 17 | public void start() { |
| 27 | - log.info("MqttPlugin.start()"); | ||
| 28 | - | ||
| 29 | - if (executorService.isShutdown() || executorService.isTerminated()) { | ||
| 30 | - executorService = Executors.newSingleThreadExecutor(); | ||
| 31 | - } | ||
| 32 | - | ||
| 33 | - deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class); | ||
| 34 | - if (deviceDataApi == null) { | ||
| 35 | - log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!"); | ||
| 36 | - return; | ||
| 37 | - } | ||
| 38 | - | 18 | + log.info("MQTT Plugin started."); |
| 19 | + mqttServerExtension = new MqttServerExtension(); | ||
| 20 | + mqttServerExtension.startMqttServer(); | ||
| 39 | } | 21 | } |
| 40 | 22 | ||
| 41 | @Override | 23 | @Override |
| 42 | public void stop() { | 24 | public void stop() { |
| 43 | - log.info("MqttPlugin.stop()"); | 25 | + log.info("MQTT Plugin stopped."); |
| 26 | + if (mqttServerExtension != null) { | ||
| 27 | + mqttServerExtension.stopMqttServer().onComplete(ar -> { | ||
| 28 | + if (ar.succeeded()) { | ||
| 29 | + log.info("Stopped MQTT Server successfully"); | ||
| 30 | + } else { | ||
| 31 | + log.error("Failed to stop MQTT Server: {}", ar.cause().getMessage()); | ||
| 32 | + } | ||
| 33 | + }); | ||
| 34 | + } | ||
| 44 | } | 35 | } |
| 45 | -} | ||
| 46 | \ No newline at end of file | 36 | \ No newline at end of file |
| 37 | +} |
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.plugin; | ||
| 2 | + | ||
| 3 | +import io.netty.handler.codec.mqtt.MqttProperties; | ||
| 4 | +import io.netty.handler.codec.mqtt.MqttQoS; | ||
| 5 | +import io.vertx.core.Future; | ||
| 6 | +import io.vertx.core.Vertx; | ||
| 7 | +import io.vertx.core.buffer.Buffer; | ||
| 8 | +import io.vertx.mqtt.MqttEndpoint; | ||
| 9 | +import io.vertx.mqtt.MqttServer; | ||
| 10 | +import io.vertx.mqtt.MqttServerOptions; | ||
| 11 | +import io.vertx.mqtt.MqttTopicSubscription; | ||
| 12 | +import io.vertx.mqtt.messages.MqttDisconnectMessage; | ||
| 13 | +import io.vertx.mqtt.messages.MqttPublishMessage; | ||
| 14 | +import io.vertx.mqtt.messages.MqttSubscribeMessage; | ||
| 15 | +import io.vertx.mqtt.messages.MqttUnsubscribeMessage; | ||
| 16 | +import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode; | ||
| 17 | +import lombok.extern.slf4j.Slf4j; | ||
| 18 | +import org.pf4j.Extension; | ||
| 19 | + | ||
| 20 | +import java.nio.charset.Charset; | ||
| 21 | +import java.util.ArrayList; | ||
| 22 | +import java.util.List; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 根据官方示例,整合常见 MQTT 功能到 PF4J 的 Extension 类中 | ||
| 26 | + */ | ||
| 27 | +@Slf4j | ||
| 28 | +@Extension | ||
| 29 | +public class MqttServerExtension { | ||
| 30 | + | ||
| 31 | + private Vertx vertx; | ||
| 32 | + private MqttServer mqttServer; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 启动 MQTT 服务端 | ||
| 36 | + * 可根据需要决定是否启用 SSL/TLS、WebSocket、多实例部署等 | ||
| 37 | + */ | ||
| 38 | + public void startMqttServer() { | ||
| 39 | + // 初始化 Vert.x | ||
| 40 | + vertx = Vertx.vertx(); | ||
| 41 | + | ||
| 42 | + // ========== 如果需要 SSL/TLS,请参考下面注释,启用注释并替换端口、证书路径等 ========== | ||
| 43 | + // MqttServerOptions options = new MqttServerOptions() | ||
| 44 | + // .setPort(8883) | ||
| 45 | + // .setKeyCertOptions(new PemKeyCertOptions() | ||
| 46 | + // .setKeyPath("./src/test/resources/tls/server-key.pem") | ||
| 47 | + // .setCertPath("./src/test/resources/tls/server-cert.pem")) | ||
| 48 | + // .setSsl(true); | ||
| 49 | + | ||
| 50 | + // ========== 如果需要 WebSocket,请设置 setUseWebSocket(true) ========== | ||
| 51 | + // options.setUseWebSocket(true); | ||
| 52 | + | ||
| 53 | + // ========== 默认不启用 SSL 的示例 ========== | ||
| 54 | + MqttServerOptions options = new MqttServerOptions() | ||
| 55 | + .setPort(1883) | ||
| 56 | + .setHost("0.0.0.0") | ||
| 57 | + .setUseWebSocket(false); // 如果需要 WebSocket,请改为 true | ||
| 58 | + | ||
| 59 | + mqttServer = MqttServer.create(vertx, options); | ||
| 60 | + | ||
| 61 | + // 指定 endpointHandler,处理客户端连接等 | ||
| 62 | + mqttServer.endpointHandler(endpoint -> { | ||
| 63 | + handleClientConnect(endpoint); | ||
| 64 | + handleDisconnect(endpoint); | ||
| 65 | + handleSubscribe(endpoint); | ||
| 66 | + handleUnsubscribe(endpoint); | ||
| 67 | + handlePublish(endpoint); | ||
| 68 | + handlePing(endpoint); | ||
| 69 | + }); | ||
| 70 | + | ||
| 71 | + // 启动监听 | ||
| 72 | + mqttServer.listen(ar -> { | ||
| 73 | + if (ar.succeeded()) { | ||
| 74 | + log.info("MQTT server is listening on port {}", mqttServer.actualPort()); | ||
| 75 | + } else { | ||
| 76 | + log.error("Error on starting the server", ar.cause()); | ||
| 77 | + } | ||
| 78 | + }); | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * 优雅关闭 MQTT 服务端 | ||
| 83 | + */ | ||
| 84 | + public Future<Void> stopMqttServer() { | ||
| 85 | + if (mqttServer != null) { | ||
| 86 | + return mqttServer.close().onComplete(ar -> { | ||
| 87 | + if (ar.succeeded()) { | ||
| 88 | + log.info("MQTT server closed."); | ||
| 89 | + if (vertx != null) { | ||
| 90 | + vertx.close(); | ||
| 91 | + log.info("Vert.x instance closed."); | ||
| 92 | + } | ||
| 93 | + } else { | ||
| 94 | + log.error("Failed to close MQTT server: {}", ar.cause().getMessage()); | ||
| 95 | + } | ||
| 96 | + }); | ||
| 97 | + } | ||
| 98 | + return Future.succeededFuture(); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + // ==================== 以下为官方示例中常见事件的处理封装 ==================== | ||
| 102 | + | ||
| 103 | + /** | ||
| 104 | + * 处理客户端连接 (CONNECT) | ||
| 105 | + */ | ||
| 106 | + private void handleClientConnect(MqttEndpoint endpoint) { | ||
| 107 | + // 打印 CONNECT 的主要信息 | ||
| 108 | + log.info("MQTT client [{}] request to connect, clean session = {}", | ||
| 109 | + endpoint.clientIdentifier(), endpoint.isCleanSession()); | ||
| 110 | + | ||
| 111 | + if (endpoint.auth() != null) { | ||
| 112 | + log.info("[username = {}, password = {}]", endpoint.auth().getUsername(), endpoint.auth().getPassword()); | ||
| 113 | + } | ||
| 114 | + log.info("[properties = {}]", endpoint.connectProperties()); | ||
| 115 | + | ||
| 116 | + if (endpoint.will() != null) { | ||
| 117 | + log.info("[will topic = {}, msg = {}, QoS = {}, isRetain = {}]", | ||
| 118 | + endpoint.will().getWillTopic(), | ||
| 119 | + new String(endpoint.will().getWillMessageBytes()), | ||
| 120 | + endpoint.will().getWillQos(), | ||
| 121 | + endpoint.will().isWillRetain()); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + log.info("[keep alive timeout = {}]", endpoint.keepAliveTimeSeconds()); | ||
| 125 | + | ||
| 126 | + // 接受远程客户端的连接 | ||
| 127 | + endpoint.accept(false); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + /** | ||
| 131 | + * 处理客户端主动断开 (DISCONNECT) | ||
| 132 | + */ | ||
| 133 | + private void handleDisconnect(MqttEndpoint endpoint) { | ||
| 134 | + endpoint.disconnectMessageHandler((MqttDisconnectMessage disconnectMessage) -> { | ||
| 135 | + log.info("Received disconnect from client [{}], reason code = {}", | ||
| 136 | + endpoint.clientIdentifier(), disconnectMessage.code()); | ||
| 137 | + }); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + /** | ||
| 141 | + * 处理客户端订阅 (SUBSCRIBE) | ||
| 142 | + */ | ||
| 143 | + private void handleSubscribe(MqttEndpoint endpoint) { | ||
| 144 | + endpoint.subscribeHandler((MqttSubscribeMessage subscribe) -> { | ||
| 145 | + List<MqttSubAckReasonCode> reasonCodes = new ArrayList<>(); | ||
| 146 | + for (MqttTopicSubscription s : subscribe.topicSubscriptions()) { | ||
| 147 | + log.info("Subscription for {} with QoS {}", s.topicName(), s.qualityOfService()); | ||
| 148 | + // 将客户端请求的 QoS 转换为返回给客户端的 reason code(可能是错误码或实际 granted QoS) | ||
| 149 | + reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService())); | ||
| 150 | + } | ||
| 151 | + // 回复 SUBACK,MQTT 5.0 时可指定 reasonCodes、properties | ||
| 152 | + endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES); | ||
| 153 | + }); | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + /** | ||
| 157 | + * 处理客户端取消订阅 (UNSUBSCRIBE) | ||
| 158 | + */ | ||
| 159 | + private void handleUnsubscribe(MqttEndpoint endpoint) { | ||
| 160 | + endpoint.unsubscribeHandler((MqttUnsubscribeMessage unsubscribe) -> { | ||
| 161 | + for (String topic : unsubscribe.topics()) { | ||
| 162 | + log.info("Unsubscription for {}", topic); | ||
| 163 | + } | ||
| 164 | + // 回复 UNSUBACK,MQTT 5.0 时可指定 reasonCodes、properties | ||
| 165 | + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); | ||
| 166 | + }); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + /** | ||
| 170 | + * 处理客户端发布的消息 (PUBLISH) | ||
| 171 | + */ | ||
| 172 | + private void handlePublish(MqttEndpoint endpoint) { | ||
| 173 | + // 接收 PUBLISH 消息 | ||
| 174 | + endpoint.publishHandler((MqttPublishMessage message) -> { | ||
| 175 | + String payload = message.payload().toString(Charset.defaultCharset()); | ||
| 176 | + log.info("Received message [{}] on topic [{}] with QoS [{}]", | ||
| 177 | + payload, message.topicName(), message.qosLevel()); | ||
| 178 | + | ||
| 179 | + // 根据不同 QoS,回复客户端 | ||
| 180 | + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | ||
| 181 | + endpoint.publishAcknowledge(message.messageId()); | ||
| 182 | + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { | ||
| 183 | + endpoint.publishReceived(message.messageId()); | ||
| 184 | + } | ||
| 185 | + }); | ||
| 186 | + | ||
| 187 | + // 如果 QoS = 2,需要处理 PUBREL | ||
| 188 | + endpoint.publishReleaseHandler(messageId -> { | ||
| 189 | + endpoint.publishComplete(messageId); | ||
| 190 | + }); | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + /** | ||
| 194 | + * 处理客户端 PINGREQ | ||
| 195 | + */ | ||
| 196 | + private void handlePing(MqttEndpoint endpoint) { | ||
| 197 | + endpoint.pingHandler(v -> { | ||
| 198 | + // 这里仅做日志, PINGRESP 已自动发送 | ||
| 199 | + log.info("Ping received from client [{}]", endpoint.clientIdentifier()); | ||
| 200 | + }); | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + // ==================== 如果需要服务端向客户端发布消息,可用以下示例 ==================== | ||
| 204 | + | ||
| 205 | + /** | ||
| 206 | + * 服务端主动向已连接的某个 endpoint 发布消息的示例 | ||
| 207 | + * 如果使用 MQTT 5.0,可以传递更多消息属性 | ||
| 208 | + */ | ||
| 209 | + public void publishToClient(MqttEndpoint endpoint, String topic, String content) { | ||
| 210 | + endpoint.publish(topic, | ||
| 211 | + Buffer.buffer(content), | ||
| 212 | + MqttQoS.AT_LEAST_ONCE, // QoS 自行选择 | ||
| 213 | + false, | ||
| 214 | + false); | ||
| 215 | + | ||
| 216 | + // 处理 QoS 1 和 QoS 2 的 ACK | ||
| 217 | + endpoint.publishAcknowledgeHandler(messageId -> { | ||
| 218 | + log.info("Received PUBACK from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); | ||
| 219 | + }).publishReceivedHandler(messageId -> { | ||
| 220 | + endpoint.publishRelease(messageId); | ||
| 221 | + }).publishCompletionHandler(messageId -> { | ||
| 222 | + log.info("Received PUBCOMP from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); | ||
| 223 | + }); | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + // ==================== 如果需要多实例部署,用于多核扩展,可参考以下思路 ==================== | ||
| 227 | + // 例如,在宿主应用或插件中循环启动多个 MqttServerExtension 实例,或使用 Vert.x 的 deployVerticle: | ||
| 228 | + // DeploymentOptions options = new DeploymentOptions().setInstances(10); | ||
| 229 | + // vertx.deployVerticle(() -> new MyMqttVerticle(), options); | ||
| 230 | + | ||
| 231 | +} |