从零搭建MQTT通信:EMQX安装 + Spring Boot完整实战教程

前言

在物联网开发中,MQTT 已经成为事实上的通信标准。很多朋友刚开始接触时,总觉得 MQTT 很复杂------Broker 是什么?QoS 怎么选?Spring Boot 怎么接入?

这篇文章不讲虚的,从 EMQX 服务端安装Spring Boot 完整代码实现,一步不落,带你跑通全流程。

一、MQTT 是什么?

一句话概括:MQTT 是一种基于"发布/订阅"模式的轻量级消息传输协议

你可以把它理解成一个"中间人":

  • 发布者:发送消息到某个主题(Topic)

  • 订阅者:订阅感兴趣的主题,接收消息

  • Broker(代理) :负责转发消息

不像 HTTP 那样"你请求我响应",MQTT 更像是"谁关心这个消息,就订阅它"。这种方式让发布者和订阅者完全解耦,非常适合物联网设备通信。

MQTT 核心概念速览

概念 说明
Topic(主题) 消息的"地址",用 / 分层,如 device/123/status
QoS(服务质量) 0=最多一次(可能丢),1=至少一次(可能重复),2=只有一次(最可靠)
通配符 + 匹配单层,# 匹配多层(仅用于订阅)

二、环境准备:安装 EMQX(Windows)

2.1 下载

访问 EMQX 官网下载页面,选择 Windows 版本的 ZIP 安装包。

官网下载地址

⚠️ 注意 :下载 emqx-5.x.x-windows-amd64.zip,区分开源版和企业版,我们选开源版即可。

系统要求:

  • Windows 10/11 64位系统

  • 至少 2GB 可用内存

  • 磁盘空间 ≥ 200MB

2.2 解压

推荐 :解压到 D:\MQTT\emqxC:\emqx 这样的纯英文路径

解压后的目录结构:

text

复制代码
emqx/
├── bin/          # 核心执行文件
├── etc/          # 配置文件
├── data/         # 运行数据
├── log/          # 日志文件
└── releases/     # 版本信息

2.3 启动 EMQX

打开 管理员权限 的 PowerShell 或 CMD,进入 emqx/bin 目录:

复制代码
先安装emqx install ,后启动emqx console

2.4 验证启动

启动成功后,打开浏览器访问 http://localhost:18083,进入 EMQX Dashboard 管理控制台。

默认登录账号密码:

  • 用户名:admin

  • 密码:public

登录后建议立即修改密码。

2.5 端口说明

EMQX 默认使用以下端口:

端口 用途
1883 MQTT TCP 协议端口
8883 MQTT SSL/TLS 端口
8083 MQTT WebSocket 端口
18083 Dashboard 管理控制台

三、Spring Boot 集成 MQTT

3.1 项目依赖

pom.xml 中添加 Spring Integration MQTT 依赖:

xml

XML 复制代码
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mqtt</artifactId>
</dependency>
<dependency>
    <groupId>org.eclipse.paho</groupId>
    <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
    <version>1.2.5</version>
</dependency>

3.2 配置文件(application.yml)

yaml

XML 复制代码
spring:
  application:
    name: mqtt-demo

mqtt:
  brokerUrl: tcp://localhost:1883
  user: root
  password: 123456
  clientId: bitstorm-server
  topics:
    - /x/+/notice
    - /x/+/device
  persistence: /var/mqtt/persistence
  completionTimeout: 5000
  keepAlive: 60
  connectionTimeout: 30
  defaultQos: 1
  autoReconnect: true
  cleanSession: true
  maxInflight: 10000

3.3 配置类:MqttProperties

java

java 复制代码
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "mqtt")
@Data
@Component
public class MqttProperties {
    private String brokerUrl;
    private String user;
    private String password;
    private String clientId;
    private String[] topics;
    private String persistence;
    private int completionTimeout;
    private int keepAlive;
    private int connectionTimeout;
    private int defaultQos;
    private boolean autoReconnect;
    private boolean cleanSession;
    private int maxInflight;
}

3.4 核心配置类:MqttIntegrationConfig

这是整个集成的核心,负责创建 MQTT 客户端工厂、消息入站适配器(接收消息)和出站适配器(发送消息)。

java 复制代码
import jakarta.annotation.Resource;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.ExecutorChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;

import java.util.UUID;
import java.util.concurrent.*;

@Configuration
@IntegrationComponentScan(basePackages = "com.bitstorm.mqtt")
@EnableIntegration
public class MqttIntegrationConfig {

    private static final Logger log = LoggerFactory.getLogger(MqttIntegrationConfig.class);

    @Resource
    private MqttProperties mqttProps;

    /**
     * MQTT 客户端工厂
     */
    @Bean
    public DefaultMqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        MqttConnectOptions options = getMqttConnectOptions();
        factory.setPersistence(new MqttDefaultFilePersistence(mqttProps.getPersistence()));
        factory.setConnectionOptions(options);
        return factory;
    }

    private MqttConnectOptions getMqttConnectOptions() {
        MqttConnectOptions options = new MqttConnectOptions();
        options.setServerURIs(new String[]{mqttProps.getBrokerUrl()});
        options.setUserName(mqttProps.getUser());
        options.setPassword(mqttProps.getPassword().toCharArray());
        options.setKeepAliveInterval(mqttProps.getKeepAlive());
        options.setConnectionTimeout(mqttProps.getConnectionTimeout());
        options.setAutomaticReconnect(mqttProps.isAutoReconnect());
        options.setCleanSession(mqttProps.isCleanSession());
        options.setMaxInflight(mqttProps.getMaxInflight());
        return options;
    }

    /**
     * 消息输入通道(带线程池,用于异步处理)
     */
    @Bean
    public MessageChannel mqttInputChannel() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10, 20, 60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(mqttProps.getMaxInflight()),
                r -> {
                    Thread t = new Thread(r, "mqtt-inbound-" + Thread.currentThread().getName());
                    t.setDaemon(true);
                    return t;
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        return new ExecutorChannel(executor);
    }

    /**
     * 消息入站适配器(订阅消息)
     */
    @Bean
    public MqttPahoMessageDrivenChannelAdapter inboundAdapter(
            DefaultMqttPahoClientFactory clientFactory,
            MessageChannel mqttInputChannel,
            MessageChannel mqttErrorChannel) {

        String clientId = mqttProps.getClientId() + "-consume-" + UUID.randomUUID();
        MqttPahoMessageDrivenChannelAdapter adapter =
                new MqttPahoMessageDrivenChannelAdapter(clientId, clientFactory, mqttProps.getTopics());
        adapter.setCompletionTimeout(mqttProps.getCompletionTimeout());
        adapter.setQos(mqttProps.getDefaultQos());
        adapter.setOutputChannel(mqttInputChannel);
        adapter.setErrorChannel(mqttErrorChannel);
        adapter.setManualAcks(true);  // 开启手动确认
        return adapter;
    }

    /**
     * 消息输出通道
     */
    @Bean
    public MessageChannel mqttOutputChannel() {
        return new DirectChannel();
    }

    /**
     * 消息出站处理器(发布消息)
     */
    @Bean
    @ServiceActivator(inputChannel = "mqttOutputChannel")
    public MessageHandler mqttOutboundHandler(DefaultMqttPahoClientFactory clientFactory) {
        String clientId = mqttProps.getClientId() + "-production-" + UUID.randomUUID();
        MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId, clientFactory);
        handler.setAsync(true);
        handler.setDefaultRetained(false);
        handler.setDefaultQos(mqttProps.getDefaultQos());
        return handler;
    }

    /**
     * 全局错误通道
     */
    @Bean
    public MessageChannel mqttErrorChannel() {
        return new DirectChannel();
    }

    /**
     * 错误通道处理器
     */
    @Bean
    @ServiceActivator(inputChannel = "mqttErrorChannel")
    public MessageHandler mqttErrorHandler() {
        return message -> {
            Throwable cause = message.getPayload() instanceof Throwable ?
                    (Throwable) message.getPayload() :
                    new MessagingException("Unknown error", (Throwable) message.getPayload());
            log.error("MQTT 错误发生,消息头:{}", message.getHeaders(), cause);
        };
    }
}

3.5 MQTT 网关接口(发送消息)

java

java 复制代码
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;

@MessagingGateway(defaultRequestChannel = "mqttOutputChannel")
public interface MqttGateway {

    /**
     * 发送消息到指定主题(使用默认 QoS)
     */
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);

    /**
     * 发送消息到指定主题,并指定 QoS
     */
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
                    @Header(MqttHeaders.QOS) int qos,
                    String payload);
}

3.6 消息接收处理器(订阅消息)

java

java 复制代码
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.acks.AcknowledgmentCallback;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@Slf4j
public class MqttMessageHandler {

    @Resource
    private MqttGateway mqttGateway;

    /**
     * 定时发送测试消息
     */
    @Scheduled(fixedDelay = 3000)
    public void sendTestMessage() {
        String payload = String.format("设备信息: %d, 时间: %s",
                System.currentTimeMillis(),
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        mqttGateway.sendToMqtt("/x/identify/device", payload);
        log.info("测试消息已发送: {}", payload);
    }

    /**
     * 消息处理器 - 手动 ACK
     */
    @ServiceActivator(inputChannel = "mqttInputChannel")
    public void handleMessage(Message<?> message) {
        // 获取 ACK 回调
        Object ackObj = message.getHeaders().get(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK);
        AcknowledgmentCallback ack = null;
        if (ackObj instanceof AcknowledgmentCallback) {
            ack = (AcknowledgmentCallback) ackObj;
        }

        try {
            String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC, String.class);
            String payload = message.getPayload().toString();
            log.info("收到消息,主题:{},内容:{}", topic, payload);
            
            // TODO: 在这里编写你的业务逻辑
            
            if (ack != null) {
                ack.acknowledge();  // 手动确认消息
                log.debug("消息已确认");
            }
        } catch (Exception e) {
            log.error("处理MQTT消息异常", e);
            // 根据业务决定是否确认(不确认会触发重发)
        }
    }
}

四、测试验证

4.1 启动 Spring Boot 应用

启动后,控制台会输出定时发送的测试消息日志。

五、常见问题与踩坑指南

5.1 EMQX 启动失败

原因:端口 1883 被占用

解决

bash

复制代码
netstat -ano | findstr :1883

找到占用进程并关闭,或修改 EMQX 配置文件中的端口。

5.2 连接断开

MQTT 是长连接,网络波动可能导致断开。配置中已开启 autoReconnect: true,会自动重连。

5.3 Topic 设计不合理

不要随意命名 Topic,建议采用层级结构:

text

复制代码
✅ device/{deviceId}/status
✅ device/{deviceId}/command
❌ /a/b/c/d/e/f/g

5.4 QoS 怎么选?

QoS 适用场景
0 传感器高频上报的非关键数据(丢一条无所谓)
1 控制指令(开关灯、设备控制)
2 金融、航空等关键数据

大多数场景 QoS 1 就够用了。

5.5 消息重复消费

使用 QoS 1 时可能收到重复消息。如果需要幂等处理,可以在业务层根据消息 ID 去重。


六、总结

本文从零开始,完成了:

  1. ✅ Windows 上安装 EMQX 并启动

  2. ✅ Spring Boot 集成 Spring Integration MQTT

  3. ✅ 消息发布(定时发送测试消息)

  4. ✅ 消息订阅(接收并手动 ACK)

  5. ✅ 使用 MQTTX 进行端到端测试

整套代码可以直接复制到项目中运行。如果遇到问题,欢迎在评论区留言交流!


📌 源码地址https://gitee.com/byte1026/mqtt-case.git

文中的所有代码均可直接使用,记得根据实际环境修改 application.yml 中的连接配置。