MQTT、SpringBoot

SpringBoot 整合 Mqtt (发布、订阅)

文中部分内容借鉴了其他作者,在此感谢提供方法的各位作者。

1、MQTT 协议介绍(简单介绍)

1.1、Mqtt协议中的三个角色:

实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。

1.2、mqtt传输的消息内容

Topic(主题),可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload)

payload,可以理解为消息的内容,是指订阅者具体要使用的内容。

1.3、mqtt客户端

一个使用MQTT协议的应用程序或者设备,它总是建立到服务器的网络连接。客户端可以:

A、发布其他客户端可能会订阅的信息;

B、订阅其它客户端发布的消息;

C、退订或删除应用程序的消息;

D、断开与服务器连接

1.4、mqtt服务端器

MQTT服务器以称为"消息代理"(Broker),可以是一个应用程序或一台设备。它是位于消息发布者和订阅者之间,它可以:

A、接受来自客户的网络连接;

B、接受客户发布的应用信息;

C、处理来自客户端的订阅和退订请求;

D、向订阅的客户转发应用程序消息。

1.5、mqtt的订阅、主题、会话

1.5.1、订阅(Subscription)

订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。

1.5.2、会话(Session)

每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。

1.5.3、主题名(Topic Name)

连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。

1.5.4、主题筛选器(Topic Filter)

一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。

1.5.5、负载(Payload)

消息订阅者所具体接收的内容。

2、工具介绍(介绍本文中使用的)

2.1、代理服务器:Emqx

EMQ X 是基于 Erlang/OTP 语言平台开发,支持大规模连接和分布式集群,发布订阅模式的开源 MQTT 消息服务器。

Linux docker 安装启动命令:

docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx

安转后访问http://ip:18083/

默认用户名/密码: admin/public

访问如下表示安装成功:

2.2、MQTT.fx

MQTT.fx 是目前主流的mqtt客户端,可以快速验证是否可以与IoT Hub 服务交流发布或订阅消息。设备将当前所处的状态作为MQTT主题发送给IoT Hub,每个MQTT主题topic具有不同等级的名称,” MQTT代理服务器将接收到的主题topic发送给给所有订阅的客户端。
下载连接:http://www.jensd.de/apps/mqttfx/1.7.1/

参考:https://blog.csdn.net/tiantang_1986/article/details/85101366

3、环境简介:

  3.1、SpringBoot-version:2.1.2
  3.2、Maven-version:3.6.1
  3.3、开发工具: IDEA 2019.3
  3.4、JDK-version:1.8

4、MQTT 发布

4.1、新建工程:MQTT-SpringBoot

4.2、pom

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>

    <!--MQTT-->
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-stream</artifactId>
            <version>4.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-mqtt</artifactId>
            <version>5.0.6.RELEASE</version>
        </dependency>
    </dependencies>

4.3、配置文件:

server:
  port: 8090

spring:
  application:
    name: MQTT-SpringBoot

mqtt:
  username: admin
  password: public
# 推送信息的连接地址,如果有多个,用逗号隔开,如:tcp://ip:1883,tcp://ip:1883
  url: tcp://ip:1883,tcp://ip:1883
  sender:
#    默认发送的主题
    defaultTopic: goods
#    clientid
    clientId: mqtttest

4.4、代码

4.4.1、modal

package com.riest.modal;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * ClassName:Send
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:06
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Send {
    private String topic;
    private String key;
    private String value;
}

4.4.2、config

package com.riest.config;

import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.StringUtils;

/**
 * ClassName:MqttConfig
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:08
 */
@Configuration
public class MqttConfig {

    /**
     * 发布的bean名称
     */
    public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";

    /**
     * 客户端与服务器之间的连接意外中断,服务器将发布客户端的"遗嘱"消息
     */
    private static final byte[] WILL_DATA;
    static {
        WILL_DATA = "offline".getBytes();
    }

    @Value("${mqtt.username}")
    private String username;

    @Value("${mqtt.password}")
    private String password;

    @Value("${mqtt.url}")
    private String url;

    @Value("${mqtt.sender.clientId}")
    private String clientId;

    @Value("${mqtt.sender.defaultTopic}")
    private String defaultTopic;

    /**
     * MQTT连接器选项
     */
    @Bean
    public MqttConnectOptions getSenderMqttConnectOptions(){
        MqttConnectOptions options=new MqttConnectOptions();
        // 设置连接的用户名
        if(!username.trim().equals("")){
            options.setUserName(username);
        }
        // 设置连接的密码
        options.setPassword(password.toCharArray());
        // 设置连接的地址
        options.setServerURIs(StringUtils.split(url, ","));
        // 设置超时时间 单位为秒
        options.setConnectionTimeout(10);
        // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线
        // 但这个方法并没有重连的机制
        options.setKeepAliveInterval(20);
        // 设置 "遗嘱" 消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的"遗嘱"消息。
        options.setWill("willTopic", WILL_DATA, 2, false);
        return options;
    }

    /**
     * MQTT客户端
     */
    @Bean
    public MqttPahoClientFactory senderMqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        factory.setConnectionOptions(getSenderMqttConnectOptions());
        return factory;
    }

    /**
     * MQTT信息通道(生产者)
     */
    @Bean(name = CHANNEL_NAME_OUT)
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }

    /**
     * MQTT消息处理器(生产者)
     */
    @Bean
    @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
    public MessageHandler mqttOutbound() {
        MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(clientId, senderMqttClientFactory());
        messageHandler.setAsync(true);
        messageHandler.setDefaultTopic(defaultTopic);
        return messageHandler;
    }
}

4.4.3、service

package com.riest.service;

import com.riest.config.MqttConfig;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

/**
 * ClassName:SendInterface
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:08
 */
@Component
@MessagingGateway(defaultRequestChannel = MqttConfig.CHANNEL_NAME_OUT)
public interface ISend {
    /**
     * 发送信息到MQTT服务器
     *
     * @param data 发送的文本
     */
    void sendToMqtt(String data);

    /**
     * 发送信息到MQTT服务器
     *
     * @param topic 主题
     * @param payload 消息主体
     */
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
                    String payload);

    /**
     * 发送信息到MQTT服务器
     *
     * @param topic 主题
     * @param qos 对消息处理的几种机制。
     * 0 表示的是订阅者没收到消息不会再次发送,消息会丢失。
     * 1 表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息。
     * 2 多了一次去重的动作,确保订阅者收到的消息有一次。
     * @param payload 消息主体
     */
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
                    @Header(MqttHeaders.QOS) int qos,
                    String payload);
}

4.4.4、controller

package com.riest.controller;

import com.alibaba.fastjson.JSONObject;
import com.riest.modal.Send;
import com.riest.service.ISend;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * ClassName:SendController
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:11
 */
@RestController
@Slf4j
public class SendController {

    @Autowired
    private ISend iMqttSender;

    /**
     *  发送自定义消息内容(使用默认主题)
     * @param data
     */
    @GetMapping(value = "/send")
    public void test1(Send data) {
        JSONObject json = (JSONObject) JSONObject.toJSON(data);
        log.error("----->{}","mqtt 消息发布,使用默认主题发布:"+json.toJSONString());
        iMqttSender.sendToMqtt(json.toJSONString());
    }

    /**
     *  发送自定义消息内容,且指定主题
     * @param data
     */
    @RequestMapping("/send/topic")
    public void test2(Send data) {
        JSONObject json = (JSONObject) JSONObject.toJSON(data);
        log.error("----->{}","mqtt 消息发布,指定主题:"+json.toJSONString());
        iMqttSender.sendToMqtt(data.getTopic(), json.toJSONString());

    }
}

4.4.5、主启动

package com.riest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * ClassName:MqttApplication
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:05
 */
@SpringBootApplication
public class MqttApplication {
    public static void main(String[] args) {
        SpringApplication.run(MqttApplication.class,args);
    }
}

4.5、工具设置

4.5.1、mqtt.fx


4.5.2、EMQX

4.6、测试

4.6.1 、调用(该请求时使用默认主题) http://localhost:8090/send?key=send&value=1234567

4.6.2、mqtt.fx收到订阅的消息

4.6.3 、emqx查看

4.6.4、调用(指定主题发布)http://localhost:8090/send/topic?key=send&value=1234567&topic=dgj-test

4.6.5、mqtt.fx收到订阅的消息

4.6.6 、emqx查看

5、MQTT 订阅

5.1 、config

package com.riest.config;

import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;

/**
 * ClassName:MQTTConnect
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:46
 */
public class MQTTConnect {

    @Value("${mqtt.username}")
    private String username;

    @Value("${mqtt.password}")
    private String password;

    /**
     *  把配置里的 cleanSession 设为false,客户端掉线后 服务器端不会清除session,
     *  当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息,
     *  如果短线需要删除之前的消息则可以设置为true
     *
     * @return
     */
    public MqttConnectOptions getOptions() {
        MqttConnectOptions options = new MqttConnectOptions();
        options.setCleanSession(false);
        options.setUserName("admin");
        options.setPassword("public".toCharArray());
        options.setConnectionTimeout(10);
        //设置心跳
        options.setKeepAliveInterval(20);
        return options;
    }

    public MqttConnectOptions getOptions(MqttConnectOptions options) {

        options.setCleanSession(false);
        options.setUserName(username);
        options.setPassword(password.toCharArray());
        options.setConnectionTimeout(10);
        options.setKeepAliveInterval(20);
        return options;
    }
}

5.2、sub端

package com.riest.service.sub;

import com.riest.config.MQTTConnect;
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.stereotype.Component;

/**
 * ClassName:MqttSub
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:45
 */
@Component
public class MqttSub {
    public static final String HOST = "tcp://122.51.240.115:1883";
    private static final String clientid = "testrece";
    private String topic = "goods";
    public MqttClient client;
    private MQTTConnect mqttConnect = new MQTTConnect();	

    /**true为非持久订阅
     *
     *  方法实现说明 断线重连方法,如果是持久订阅,重连是不需要再次订阅,如果是非持久订阅,重连是需要重新订阅主题 取决于options.setCleanSession(true);
     *
     *  就是这里的clientId,服务器用来区分用户的,不能重复,clientId不能和发布的clientId一样,否则会出现频繁断开连接和重连的问题
     *  不仅不能和发布的clientId一样,而且也不能和其他订阅的clientId一样,如果想要接收之前的离线数据,这就需要将client的 setCleanSession
     *  设置为false,这样服务器才能保留它的session,再次建立连接的时候,它就会继续使用这个session了。 这时此连接clientId 是不能更改的。
     *  但是其实还有一个问题,就是使用热部署的时候还是会出现频繁断开连接和重连的问题,可能是因为刚启动时的连接没断开,然后热部署的时候又进行了重连,重启一	  *   下就可以了
     *  System.currentTimeMillis()
     * @throws MqttException
     */
    public void connect() throws MqttException {
        //防止重复创建MQTTClient实例
        if (client==null) {
            // MemoryPersistence设置clientid的保存形式,默认为以内存保存
            client = new MqttClient(HOST, clientid, new MemoryPersistence());
            //如果是订阅者则添加回调类,发布不需要
            client.setCallback(new PubCallBack(MqttSub.this));
        }
        MqttConnectOptions options = mqttConnect.getOptions();
        //判断拦截状态,这里注意一下,如果没有这个判断,是非常坑的
        if (!client.isConnected()) {
            client.connect(options);
            System.out.println("连接成功");
        }else {
            client.disconnect();
            client.connect(mqttConnect.getOptions(options));
            System.out.println("连接成功");
        }
    }

    public void init() {
        try {
            connect();
            subscribe(topic);
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

    /**
     * 订阅某个主题,qos默认为0
     *
     * @param topic .
     */
    public void subscribe(String topic) {
        subscribe(topic,0);
    }

    /**
     * 订阅某个主题
     *
     * @param topic .
     * @param qos .
     */
    public void subscribe(String topic, int qos) {

        try {
            client.subscribe(topic,0);
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }
}

package com.riest.service.sub;

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.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Value;

/**
 * ClassName:PubCallBack
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:48
 */
@Slf4j
public class PubCallBack implements MqttCallback {
    @Value("${mqtt.username}")
    private  String username;

    @Value("${mqtt.password}")
    private  String password;

    @Value("${mqtt.url}")
    private  String url;

    @Value("${mqtt.receiver.clientId}")
    private  String clientId;

    @Value("${mqtt.receiver.defaultTopic}")
    private  String defaultTopic;


    private MqttSub mqttSub;


    public  PubCallBack(MqttSub subsribe) throws MqttException {
        this.mqttSub = subsribe;
    }


    @Override
    public void connectionLost(Throwable cause) {
        // 连接丢失后,一般在这里面进行重连
        log.error("---------------------连接断开,可以做重连");

        while (true){
            try {
                //如果没有发生异常说明连接成功,如果发生异常,则死循环
                Thread.sleep(1000);
                mqttSub.init();
                break;
            }catch (Exception e){
//                e.printStackTrace();
                continue;
            }
        }

    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        System.out.println("deliveryComplete---------" + token.isComplete());
    }



    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        // subscribe后得到的消息会执行到这里面
        String result = new String(message.getPayload(),"UTF-8");
        System.out.println("接收消息主题 : " + topic);
        System.out.println("接收消息Qos : " + message.getQos());
        System.out.println("接收消息内容 : " + result);
        //这里可以针对收到的消息做处理
    }
}

5.3、主启动

package com.riest;

import com.riest.service.sub.MqttSub;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.annotation.PostConstruct;

/**
 * ClassName:MqttApplication
 * Describe:
 * Author:DGJ
 * Data:2020/10/29 10:05
 */
@SpringBootApplication
public class MqttApplication {
    public static void main(String[] args) {
        SpringApplication.run(MqttApplication.class,args);
    }

    @Autowired
    private MqttSub mqttSub;

    @PostConstruct
    public void consumeMqttClient() throws MqttException {
        mqttSub.init();           // 订阅 消息
    }
}

6、测试订阅

6.1、mqtt.fx

6.2、emqx

6.3、程序日志

7、最终项目结构

posted @ 2020-10-29 11:31  一个努力的人QAQ  阅读(3709)  评论(2编辑  收藏  举报