Springboot 集成MQTT

637 阅读7分钟

Springboot 集成MQTT

概念

MQTT

MQTT是一种基于发布/订阅模式的轻量级消息传输协议,专门用于低带宽和不稳定的网络环境的物联网应用而设计,可以用极少的代码为联网设备提供实时可靠的消息服务。

以下为MQTT其中两个特性:

QoS

MQTT 协议中规定了消息服务质量(Quality of Service),它保证了在不同的网络环境下消息传递的可靠性。

QoS 等级描述应用
QoS 0最多交付一次,可能会丢失消息传输一些高频且不那么重要的数据,比如传感器数据,周期性更新,即使遗漏几个周期的数据也可以接受
QoS 1至少交付一次,保证信息可达,但是消息可能会重复适合传输一些较为重要的数据,比如下达关键指令、更新重要的有实时性要求的状态等(注意重复消费)
QoS 2只交付一次,可以保证消息到达,也可以保证消息不会重复,但传输成本最高不愿意自行实现去重方案,并且能够接受 QoS 2 带来的额外开。金融、航空场景应用较多

Qos消息保证不是端到端的,而是客户端与服务之间的,订阅者接收到的消息等级取决于发布消息Qos与订阅主题的Qos。 举个例子: 有个订阅者订阅的消息主题是Qos 1,如果发布者对该主题发布一条消息Qos为2,那么订阅者接收到该消息的QoS为2,如果发布是一条QoS 0的消息,则订阅者接收的消息QoS等级也为Qos 0。

订阅者接收的消息QoS level <= 订阅消息主题的QoS level

消息主题QoS发布者QoS订阅者QoS
000
010
020
100
111
121
200
211
222

保留消息

MQTT客户端向服务器发布(publish)消息时,可以设置保留消息(Retained Messages)标志在当前设置一条持久消息,消息被保存在服务器上,新的订阅者订阅主题时将接受到该信息。 每个主题下只能存在一份保留信息,因此如果已经存在相同主题的保留信息,则该保留消息会被替换

保留消息虽然存储在服务端中,但它不属于会话的一部分。所以即使发布消息的会话结束,该保留消息也不会被删除,删除方式有以下几种:

  1. 客户端向有保留消息的主题发布一个空消息。
  2. 超过EMQX设置的最大保留信息数。
  3. 通过EMQX保留消息REST API删除。
  4. 设置了消息过期间隔,到期后保留消息将被删除(MQTT5.0)

EMQX

EMQX (opens new window)是一款大规模可弹性伸缩的云原生分布式物联网 MQTT (opens new window)消息服务器。

作为全球最具扩展性的 MQTT 消息服务器,EMQX 提供了高效可靠海量物联网设备连接,能够高性能实时移动与处理消息和事件流数据,快速构建关键业务的物联网平台与应用。

部署

docker run -itd \
    --restart always \
    --name emqx \
    -p 1883:1883 -p 8083:8083 \
    -p 8084:8084 -p 8883:8883 \
    -p 18083:18083 emqx/emqx:latest

windows:Windows | EMQX 5.0 文档

Springboot使用

pom.xml


<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.13</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-integration</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-mqtt</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.26</version>
    </dependency>

</dependencies>

spring-integration-mqtt集成了org.eclipse.paho.client.mqttv3 org.eclipse.paho.mqttv5.client版本都是1.2.5版本

application.yml

server:
  port: 8899
mqtt:
  broker: tcp://localhost:1883
  username: admin
  password: public
  client-id: blessing-client
  integration-client-id: gravel-client
  qos-list:
    - 2
    - 2
    - 2
  topics:
    - blessing
    - star
    - gravel

公共配置文件

package com.silvergravel.share;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/8
 */
@ConfigurationProperties(prefix = "mqtt")
@Component
@Data
public class MqttProperties {
    private String broker;
    private String username;
    private String password;
    private String clientId;
    private String integrationClientId;
    private String[] topics;
    private Integer[] qosList;
}

方式一:使用Spring Integration

项目结构

image.png

config

package com.silvergravel.intergration.config;

import com.silvergravel.share.MqttProperties;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
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.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

import javax.annotation.Resource;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/1
 */
@Configuration
@IntegrationComponentScan
public class IntegrationMqttConfiguration {

    @Resource
    private MqttProperties mqttProperties;

    /**
     * 设置默认连接参数
     */
    @Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        MqttConnectOptions options = new MqttConnectOptions();
        options.setServerURIs(new String[] { "tcp://localhost:1883"});
        // 删除会话,默认true
        options.setCleanSession(false);
        options.setUserName(mqttProperties.getUsername());
        options.setPassword(mqttProperties.getPassword().toCharArray());
        factory.setConnectionOptions(options);
        return factory;
    }

    @Bean
    @ServiceActivator(inputChannel = "mqttOutboundChannel")
    public MessageHandler mqttOutbound() {
        MqttPahoMessageHandler messageHandler =
                new MqttPahoMessageHandler("silver-gravel", mqttClientFactory());
        messageHandler.setAsync(true);
        // 设置推送消息的默认主题,topic不能为null
        messageHandler.setDefaultTopic(mqttProperties.getTopics()[0]);
        // 设置推送QoS默认等级
        messageHandler.setDefaultQos(1);
        return messageHandler;
    }

    @Bean
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }

    /**
     * 多个MessageProducer实例可以共用一个管道
     * 也可以使用
     * @return 一个消息管道
     */
    @Bean
    public MessageChannel mqttInboundChannel() {
        return new DirectChannel();
    }

    @Bean
    public MessageChannel mqttInboundChannel1() {
        return new DirectChannel();
    }

    @Bean
    public MessageProducer client1() {
        MqttPahoMessageDrivenChannelAdapter adapter =
                new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getBroker(),
                        mqttProperties.getIntegrationClientId(),
                        mqttProperties.getTopics());
        adapter.setCompletionTimeout(5000);
        adapter.setConverter(new DefaultPahoMessageConverter());
        // 订阅主题Qos为1
        adapter.setQos(1);
        adapter.setOutputChannel(mqttInboundChannel());
        return adapter;
    }

    @Bean
    public MessageProducer client2() {
        MqttPahoMessageDrivenChannelAdapter adapter =
                new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getBroker(),
                        mqttProperties.getIntegrationClientId()+1,
                        mqttProperties.getTopics()[0]);
        adapter.setCompletionTimeout(5000);
        adapter.setConverter(new DefaultPahoMessageConverter());
        // 订阅主题为0
        adapter.setQos(0);
        adapter.setOutputChannel(mqttInboundChannel());
        return adapter;
    }


    @Bean
    @ServiceActivator(inputChannel = "mqttInboundChannel")
    public MessageHandler handler() {
        // 处理topic 以及相应payload内容
        return (message -> {
            String prefix = "**************";
            String content = "\n集成Client:" +
                    "\ntopic:"+message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC)
                    +"\nqos:"+message.getHeaders().get(MqttHeaders.RECEIVED_QOS)
                    +"\npayload:"+message.getPayload()
                    +"\nretained:"+message.getHeaders().get(MqttHeaders.RECEIVED_RETAINED)
                    +"\n";
            System.out.println(prefix+content+prefix);
        });
    }


//    @Bean
//    @ServiceActivator(inputChannel = "mqttInboundChannel1")
//    public MessageHandler handler1() {
//        return message -> System.out.println("handler1:"+message.getHeaders()+":"+message.getPayload());
//    }

}

controller

package com.silvergravel.intergration.controller;

import com.silvergravel.intergration.service.MqttGateway;
import com.silvergravel.share.MqttProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/1
 */
@RestController
public class MessageController {
    @Resource
    private MqttGateway mqttGateway;

    @Resource
    private MqttProperties mqttProperties;

    @GetMapping("/integration/qos0")
    public String qos0() {
        String content = "这是integration接口的一条 QoS 0消息";
        mqttGateway.sendData(content,mqttProperties.getTopics()[0],0);
        return content;
    }

    @GetMapping("/integration/qos1")
    public String qos1() {
        String content = "这是integration接口的一条 QoS 1消息";
        mqttGateway.sendData(content,mqttProperties.getTopics()[0],1);
        return content;
    }

    @GetMapping("/integration/qos2")
    public String qos2() {
        String content = "这是integration接口的一条 QoS 2消息";
        mqttGateway.sendData(content,mqttProperties.getTopics()[0],2);
        return content;
    }

    @GetMapping("/integration/qos2/retained")
    public String retained() {
        String content = "这是integration接口的一条 QoS 2消息并且开启保留策略";
        mqttGateway.sendData(content,mqttProperties.getTopics()[0],2,true);
        return content;
    }

    @GetMapping("/integration/empty")
    public String empty() {
        mqttGateway.sendData("",mqttProperties.getTopics()[0],1,true);
        return "发送一条空消息删除保留信息,空信息是带有保留策略的空信息,无关QoS等级";
    }
}

生产者

package com.silvergravel.intergration.service;

import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/1
 */
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
@Service
public interface MqttGateway {
    /**
     * 发送数据,没有指定Topic 默认是MqttPahoMessageHandler默认值
     * 不设置默认Qos 0
     *
     * @param data 数据
     * @see MqttPahoMessageHandler#setDefaultTopic(String)
     */
    void sendData(String data);

    /**
     * 发送数据
     *
     * @param topic 指定topic
     * @param data  数据
     */
    void sendData(String data, @Header(MqttHeaders.TOPIC) String topic);


    /**
     * 发送数据
     *
     * @param topic 指定topic
     * @param qos   消息质量等级
     * @param data  数据
     */
    void sendData(String data, @Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos);


    /**
     * 发送数据
     *
     * @param data     数据
     * @param topic    指定主题
     * @param qos      消息质量等级
     * @param retained 消息是否保留 (如果这条消息没有被消费,
     *                 那么就会保留在代理服务器上)
     */
    void sendData(String data, @Header(MqttHeaders.TOPIC) String topic
            , @Header(MqttHeaders.QOS) int qos,
                  @Header(MqttHeaders.RETAINED) boolean retained);
}

方式二:使用普通方式

项目结构

image.png

config

package com.silvergravel.base.config;

import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.util.Arrays;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/7
 */
public class MqttCallbackConfig implements MqttCallbackExtended {

    /**
     * @param reconnect If true, the connection was the result of automatic reconnect.
     * @param serverUri The server URI that the connection was made to.
     */
    @Override
    public void connectComplete(boolean reconnect, String serverUri) {
        System.out.println(serverUri);
        // reconnect进行自动重连
    }

    /**
     * 导致失去连接的原因
     *
     * @param cause the reason behind the loss of connection.
     */
    @Override
    public void connectionLost(Throwable cause) {
        cause.printStackTrace();
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        // 这里处理接收的消息
        String prefix = "**************";
        String content = "\n普通Client:\n" + "topic: " + topic
                + "\nqos:" + message.getQos()
                + "\npayload:" + new String(message.getPayload())
                + "\nretained:"+message.isRetained()+"\n";
        System.out.println(prefix + content + prefix);
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        System.out.println(Arrays.toString(token.getTopics()));
    }
}
package com.silvergravel.base.config;

import com.silvergravel.share.MqttProperties;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.function.IntFunction;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/7
 */
@Configuration
public class MqttConnectConfig {

    @Resource
    private MqttProperties mqttProperties;


    @Bean
    public MqttCallbackConfig mqttCallbackConfig() {
        return new MqttCallbackConfig();
    }

    @Bean
    public MqttClient mqttClient() throws MqttException {
        MemoryPersistence persistence = new MemoryPersistence();
        MqttClient mqttClient = new MqttClient(mqttProperties.getBroker(),
                mqttProperties.getClientId(), persistence);
        // topics.length == qos.length 两者长度一致
        String[] topics = mqttProperties.getTopics();
        mqttClient.setCallback(mqttCallbackConfig());
        mqttClient.connect(createMqttConnectOptions());
        Integer[] qosList = mqttProperties.getQosList();
        int[] qosArray = Arrays.stream(qosList).mapToInt(Integer::intValue).toArray();
        mqttClient.subscribe(topics, qosArray);
        return mqttClient;
    }

    @Bean
    public MqttClient mqttClient1() throws MqttException {
        MemoryPersistence persistence = new MemoryPersistence();
        MqttClient mqttClient = new MqttClient(mqttProperties.getBroker(),
                mqttProperties.getClientId() + 1, persistence);
        mqttClient.setCallback(mqttCallbackConfig());
        mqttClient.connect(createMqttConnectOptions());
        String topic = mqttProperties.getTopics()[0];
        int qos = mqttProperties.getQosList()[0];
        mqttClient.subscribe(topic, 1);
        return mqttClient;
    }


    private MqttConnectOptions createMqttConnectOptions() {
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setUserName(mqttProperties.getUsername());
        connOpts.setPassword(mqttProperties.getPassword().toCharArray());
        connOpts.setCleanSession(false);
        return connOpts;
    }

}

controller

package com.silvergravel.base.controller;

import com.silvergravel.share.MqttProperties;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/7/8
 */
@Configuration
@RestController
public class BaseController {
    @Resource
    private MqttProperties mqttProperties;

    @Resource
    private MqttClient mqttClient;

    @GetMapping("/base/qos0")
    public String qos0() throws MqttException {
        mqttClient.publish(mqttProperties.getTopics()[0], "这是一条QoS 0的消息".getBytes(), 0, false);
        return "这是一条QoS 0的消息";
    }

    @GetMapping("/base/qos1")
    public String qos1() throws MqttException {
        mqttClient.publish(mqttProperties.getTopics()[0], "这是一条QoS 1的消息".getBytes(), 1, false);
        return "这是一条QoS 1的消息";
    }


    @GetMapping("/base/qos2")
    public String qos2() throws MqttException {
        mqttClient.publish(mqttProperties.getTopics()[0], "这是一条QoS 2的消息".getBytes(), 2, false);
        return "这是一条QoS 2的消息";
    }
    @GetMapping("/base/qos2/retained")
    public String retained() throws MqttException {
        byte[] message = "这是一条QoS 2的消息 并且开启保留策略,接收之后重启系统查看retained的输出".getBytes();
        mqttClient.publish(mqttProperties.getTopics()[0],message , 2, true);
        return "这是一条QoS 2的消息 并且开启保留策略,接收之后重启系统查看retained的输出";
    }

    @GetMapping("/base/empty")
    public String empty() throws MqttException {
        mqttClient.publish(mqttProperties.getTopics()[0], "".getBytes(), 1,true);
        return "发送一条空消息删除保留信息,空信息是带有保留策略的空信息,无关QoS等级";
    }


}

启动类

@SpringBootApplication
public class MqttApplication {
    public static void main(String[] args) {
        SpringApplication.run(MqttApplication.class, args);
    }
}

github链接

拂晓银砾的 mqtt-spring-boot-study(github.com)

参考文档

Spring Integration MQTT

快速开始 | EMQX 5.0 文档

MQTT从入门到放弃_mqttcallbackextended_大银_strawberry的博客-CSDN博客