Commit 890d304340f1d20c52cf5288d47956eaed5814ee

Authored by 安浩浩
1 parent deab8c1c

【功能完善】IoT: 更新 Vert.x 版本至 4.5.1,新增 EMQX 插件及其相关配置,重构 MQTT 插件以支持 Vert.x MQTT 服务器,优化插件启动和停止逻辑,更新插件描述信息。

yudao-dependencies/pom.xml
... ... @@ -626,6 +626,12 @@
626 626 <artifactId>vertx-web</artifactId>
627 627 <version>${vertx.version}</version>
628 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 636 </dependencies>
631 637 </dependencyManagement>
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/plugin.properties 0 → 100644
  1 +plugin.id=emqx-plugin
  2 +plugin.class=cn.iocoder.yudao.module.iot.plugin.EmqxPlugin
  3 +plugin.version=0.0.1
  4 +plugin.provider=ahh
  5 +plugin.dependencies=
  6 +plugin.description=emqx-plugin-0.0.1
... ...
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 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 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 147 <version>${lombok.version}</version>
148 148 <scope>provided</scope>
149 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 151 <dependency>
157 152 <groupId>io.vertx</groupId>
158 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 155 </dependency>
165 156 </dependencies>
166 157 </project>
167 158 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/plugin.properties
1 1 plugin.id=mqtt-plugin
  2 +plugin.description=Vert.x MQTT plugin
2 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 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 145 <version>${lombok.version}</version>
146 146 <scope>provided</scope>
147 147 </dependency>
148   - <!-- MQTT -->
  148 + <!-- Vert.x MQTT -->
149 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 153 </dependency>
153 154 </dependencies>
154 155 </project>
155 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 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 3 import lombok.extern.slf4j.Slf4j;
6 4 import org.pf4j.Plugin;
7 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 7 @Slf4j
14 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 12 public MqttPlugin(PluginWrapper wrapper) {
21 13 super(wrapper);
22   - this.executorService = Executors.newSingleThreadExecutor();
23 14 }
24 15  
25 16 @Override
26 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 23 @Override
42 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 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 +}
... ...