SpringBoot 结合官网对MQTT消息队列整合记录

SpringBoot 结合官网对MQTT消息队列整合

官方参考资料如下

具体实现如下:

首先是 maven Pom 的引入 Mqtt Client

        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
            <version>1.2.2</version>
        </dependency>

其次编写一个Mqtt回调方法类 去实现 MqttCallback 接口 用于消息的接收


import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import java.util.Base64;

/**
 * @author caicai
 *
 *
 * 消息回调显示
 */
@Slf4j
public class PushCallback implements MqttCallback {

    /**
     * 连接丢失
     * @param throwable
     */
    @Override
    public void connectionLost(Throwable throwable) {

    }
    /**
     * 消息到达
     * @param s
     * @param mqttMessage
     * @throws Exception
     */

    @Override
    public void messageArrived(java.lang.String s, MqttMessage mqttMessage) throws Exception {
        log.info("com.kxcd.testkfka.mqtt.tools.PushCallback.messageArrived===>接收到topic {} ",s);
        byte[] payload = mqttMessage.getPayload();
        String s1 = Base64.getEncoder().encodeToString(payload);
        log.info("PushCallback.messageArrived===>接收到消息内容 {} ",mqttMessage);
    }



    /**
     * 消息发送完成
     * @param iMqttDeliveryToken
     */
    @Override
    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {

    }
}

为了代码的美观,这里写死配置,都定义的常量,如果真正使用的话,建议这里修改成 读取配置方式 例如 nacos

/**
 * @author caicai
 */
public class MqConfigConstant {

    public class MqClientConfig {
        // 用户名称
        public static final String MQ_USER_NAME = "test";
        // 用户密码
        public static final String MQ_PASS_WORD = "test";
        // 默认主题
        public static final String MQ_DEFAULT_TOPIC = "test";
        // 客户端ID
        public static final String MQ_CLIENT_ID = "TEST_MQ";
        // 连接地址
        public static final String MQ_CONNECT_URL = "tcp://47.102.199.23:1883";

    }
    public class ConstantNumber {
        // 默认消息质量 QoS 0,最多交付一次。
        // QoS 1,至少交付一次。
        //QoS 2,只交付一次。
        public static final int MQ_QOS1 = 1;
    }
}

编写一个初始化配置类

这里本来是打算使用单例的模式进行配置的,写了一个单例的工具类,后面觉得这样方式 很麻烦 ,于是决定 修改成一次性初始化的

package com.xx.xx.mqtt.config;

import com.xx.xx.constant.MqConfigConstant;
import com.xxx.xx.mqtt.xx.PushCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @author caicai
 */

@Component
public class MqClientConfig {

    private  MqttClient mqttClientInit;
    @Bean
    public MqttClient getMqttClient() {
        // 首先判断一下是否已经初始化过,如果已经初始化过,则直接返回
        if(mqttClientInit == null){
            synchronized (MqClientConfig.class){
                if(mqttClientInit == null){
                    try {
                        MemoryPersistence persistence = new MemoryPersistence();
                        MqttClient client = new MqttClient(MqConfigConstant.MqClientConfig.MQ_CONNECT_URL, MqConfigConstant.MqClientConfig.MQ_CLIENT_ID, persistence);
                        // 设置连接时协带参数
                        MqttConnectOptions connOpts = new MqttConnectOptions();
                        //  设置连接时候需要用的用户名和密码
                        connOpts.setUserName(MqConfigConstant.MqClientConfig.MQ_USER_NAME);
                        connOpts.setPassword(MqConfigConstant.MqClientConfig.MQ_PASS_WORD.toCharArray());
                        // 连接
                        client.connect(connOpts);
                        // 调用之前的回调方法类
                        client.setCallback(new PushCallback());
                        // 设置默认订阅 -这里我为了方便测试,加的。可以去掉
                        client.subscribe(MqConfigConstant.MqClientConfig.MQ_DEFAULT_TOPIC);
                        mqttClientInit = client;
                    }catch (Exception e){
                        System.out.println("初始化MqttClient失败");
                    }
                }
            }
        }
        return mqttClientInit;
    }
}

写提供实现服务,来方便后续的调用 例如跨服务调用

@Service
public class MqttService {
    @Resource
   private MqttClient mqttClient;
    public void setSubscribe(String subTopic) throws MqttException {
        mqttClient.subscribe(subTopic);
    }
    public void  setPublish(String pubTopic,String msg) throws MqttException {
        MqttMessage message = new MqttMessage(msg.getBytes());
        mqttClient.publish(pubTopic,message);
    }
    public void setPublish(String pubTopic,String msg,int qos) throws MqttException {
        MqttMessage message = new MqttMessage(msg.getBytes());
        message.setQos(qos);
        mqttClient.publish(pubTopic,message);
    }

}

补充知识 Qos消息质量

详细参考 : https://www.emqx.com/zh/blog/introduction-to-mqtt-qos

QoS 0 - 最多交付一次

QoS 0 是最低的 QoS 等级。QoS 0 消息即发即弃,不需要等待确认,不需要存储和重传,因此对于接收方来说,永远都不需要担心收到重复的消息。
为什么 QoS 0 消息会丢失?
当我们使用 QoS 0 传递消息时,消息的可靠性完全依赖于底层的 TCP 协议。

而 TCP 只能保证在连接稳定不关闭的情况下消息的可靠到达,一旦出现连接关闭、重置,仍有可能丢失当前处于网络链路或操作系统底层缓冲区中的消息。这也是 QoS 0 消息最主要的丢失场景。

QoS 1 - 至少交付一次

为了保证消息到达,QoS 1 加入了应答与重传机制,发送方只有在收到接收方的 PUBACK 报文以后,才能认为消息投递成功,在此之前,发送方需要存储该 PUBLISH 报文以便下次重传。
QoS 1 需要在 PUBLISH 报文中设置 Packet ID,而作为响应的 PUBACK 报文,则会使用与 PUBLISH 报文相同的 Packet ID,以便发送方收到后删除正确的 PUBLISH 报文缓存。

为什么 QoS 1 消息会重复?
对于发送方来说,没收到 PUBACK 报文分为以下两种情况:

PUBLISH 未到达接收方
PUBLISH 已经到达接收方,接收方的 PUBACK 报文还未到达发送方
在第一种情况下,发送方虽然重传了 PUBLISH 报文,但是对于接收方来说,实际上仍然仅收到了一次消息。

但是在第二种情况下,在发送方重传时,接收方已经收到过了这个 PUBLISH 报文,这就导致接收方将收到重复的消息。

QoS 2 - 只交付一次

QoS 2 解决了 QoS 0、1 消息可能丢失或者重复的问题,但相应地,它也带来了最复杂的交互流程和最高的开销。每一次的 QoS 2 消息投递,都要求发送方与接收方进行至少两次请求/响应流程。

首先,发送方存储并发送 QoS 为 2 的 PUBLISH 报文以启动一次 QoS 2 消息的传输,然后等待接收方回复 PUBREC 报文。这一部分与 QoS 1 基本一致,只是响应报文从 PUBACK 变成了 PUBREC。
当发送方收到 PUBREC 报文,即可确认对端已经收到了 PUBLISH 报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。所以此时发送方可以删除本地存储的 PUBLISH 报文,然后发送一个 PUBREL 报文,通知对端自己准备将本次使用的 Packet ID 标记为可用了。与 PUBLISH 报文一样,我们需要确保 PUBREL 报文到达对端,所以也需要一个响应报文,并且这个 PUBREL 报文需要被存储下来以便后续重传。
当接收方收到 PUBREL 报文,也可以确认在这一次的传输流程中不会再有重传的 PUBLISH 报文到达,因此回复 PUBCOMP 报文表示自己也准备好将当前的 Packet ID 用于新的消息了。
当发送方收到 PUBCOMP 报文,这一次的 QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的 PUBLISH 报文时,也会将它视为一个全新的消息

为什么 QoS 2 消息不会重复?
QoS 2 消息保证不会丢失的逻辑与 QoS 1 相同,所以这里我们就不再重复了。

与 QoS 1 相比,QoS 2 新增了 PUBREL 报文和 PUBCOMP 报文的流程,也正是这个新增的流程带来了消息不会重复的保证。

在我们更进一步之前,我们先快速回顾一下 QoS 1 消息无法避免重复的原因。

当我们使用 QoS 1 消息时,对接收方来说,回复完 PUBACK 这个响应报文以后 Packet ID 就重新可用了,也不管响应是否确实已经到达了发送方。所以就无法得知之后到达的,携带了相同 Packet ID 的 PUBLISH 报文,到底是发送方因为没有收到响应而重传的,还是发送方因为收到了响应所以重新使用了这个 Packet ID 发送了一个全新的消息。

总结 :一个主题可以多个客户端订阅,如果推送消息给这个主题里,两个客户端都会收到相关消息。其次客户端可以订阅,也可发送。

posted @ 2024-07-25 17:01  菜菜920  阅读(17)  评论(0编辑  收藏  举报