【工作记录】MQTT介绍、安装部署及springboot集成@20230912

背景

近期公司可能会有物联网设备相关项目内容,提前对用到的mqtt协议做预研和初步使用。
最初接触到mqtt协议应该是早些年的即时通讯吧,现在已经是物联网设备最热门的协议了。
作为记录,也希望能帮助到需要的朋友。

MQTT介绍

《MQTT 协议规范中文版》一书中对 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)进行了描述:

MQTT 是一种基于客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、 简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT)。----MQTT 协议中文版

以上这段话很好的描述了 MQTT 的全部含义,它是一种轻巧、开放、简单、规范的网络通信协议。与 HTTP 协议一样,MQTT 协议也是应用层协议,工作在 TCP/IP 四层模型中的最上层(应用层),构建于 TCP/IP协议上。MQTT 最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

如今,MQTT 成为了最受欢迎的物联网协议,已广泛应用于车联网、智能家居、即时聊天应用和工业互联网等领域。目前通过 MQTT 协议连接的设备已经过亿,这些都得益于 MQTT 协议为设备提供了稳定、可靠、易用的通信基础。

MQTT 的主要特性

MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,它具 有以下主要的几项特性:

① 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。

② 基于 TCP/IP 提供网络连接。主流的 MQTT 是基于 TCP 连接进行数据推送的,但是同样也有基于 UDP 的版本,叫做 MQTT-SN。这两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。

③ 支持 QoS 服务质量等级。根据消息的重要性不同设置不同的服务质量等级。

④ 小型传输,开销很小,协议交换最小化,以降低网络流量。这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集",要知道嵌入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了,在手机移动应用方面,MQTT 是一种不错的 Android 消息推送方案。

⑤ 使用 will 遗嘱机制来通知客户端异常断线。

⑥ 基于主题发布/订阅消息,对负载内容屏蔽的消息传输。

⑦ 支持心跳机制。

MQTT 协议

MQTT 是一种基于客户端-服务端架构的消息传输协议,所以在 MQTT 协议通信中,有两个最为重要的角色,它们便是服务端和客户端。

服务端

MQTT 服务端通常是一台服务器(broker),它是 MQTT 信息传输的枢纽,负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端;MQTT 服务端还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。

客户端

MQTT 客户端可以向服务端发布信息,也可以从服务端收取信息;我们把客户端发送信息的行为称为 “发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作 很像我们在使用微信时“关注”了某个公众号,当公众号的作者发布新的文章时,微信官方会向关注了该公众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看。

MQTT 主题

上面我们讲到了,客户端想要从服务器获取信息,首先需要订阅信息,那客户端如何订阅信息呢?这里我们要引入“主题(Topic)”的概念,“主题”在 MQTT 通信中是一个非常重要的概念,客户端发布信息以及订阅信息都是围绕“主题”来进行的,并且 MQTT 服务端在管理 MQTT 信息时,也是使用“主题”来控制的。

客户端发布消息时需要为消息指定一个“主题”,表示将消息发布到该主题;而对于订阅消息的客户端 来说,可通过订阅“主题”来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)。

为了便于您更好理解服务端是如何通过“主题”来控制客户端之间的信息通讯,我们来看看下图实例:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第1张图片
在以上图示中一共有三个 MQTT 客户端,它们分别是开发板、手机和电脑。MQTT 服务端在管理 MQTT通信时使用了“主题”来对信息进行管理。比如上图所示,假设我们需要利用手机和电脑获取开发板在运行过程中 SoC 芯片的温度,那么首先电脑和手机这两个客户端需要向 MQTT 服务器订阅主题“芯片温度”;接下来,当开发板客户端向服务端的“芯片温度”主题发布信息(假设信息的内容就是当前的温度值)后,服务端就会首先检查都有哪些客户端订阅了“芯片温度”这一主题的信息,而当它发现订阅了该主题的客户端有一个手机和一个电脑,于是服务端就会将刚刚收到的“芯片温度”信息转发给订阅了该主题的手机和电脑客户端。

通过以上的这种实例,手机和电脑便可以获取到开发板运行时 SoC 芯片的温度值。

以上实例中,开发板是“芯片温度”主题的发布者,而手机和电脑则是该主题的订阅者。

值得注意的是,MQTT 客户端在通信时,角色往往不是单一的,一个客户端既可以作为信息发布者也 可以同时作为信息订阅者。如下图所示:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第2张图片
上图中的所有客户端都是围绕“LED 控制”这一主题进行通信。此时,对于“LED 控制”这一主题来 说,手机和电脑客户端成为了 MQTT 信息的发布者而开发板则成为了 MQTT 信息的订阅者(接收者)。

所以由此可知,针对不同的主题,MQTT 客户端可以切换自己的角色,它们可能对主题 A 来说是信息发布者,但是对于主题 B 就成了信息订阅者,所以一个 MQTT 客户端它的角色并不是固定的,所以大家一定要理解“主题”这个概念。

MQTT 发布/订阅特性

从以上实例我们可以看到,MQTT 通信的核心枢纽是 MQTT 服务端,它负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端,还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。

正是因为有了服务端对 MQTT 信息的接收、储存、处理和发送,客户端在发布和订阅信息时,可以相 互独立、且在空间上可以分离、时间上可以异步,这就是 MQTT 发布/订阅的特性:客户端相互独立、空间上可分离、时间上可异步,具体介绍如下:

⚫ 客户端相互独立:MQTT 客户端是一个个独立的个体,它们无需了解彼此的存在,依然可以实现 信息交流。譬如在上面的实例中,开发板客户端在发布“芯片温度”信息时,开发板客户端本身完全不知道有多少个 MQTT 客户端订阅了“芯片温度”这一主题;而订阅了“芯片温度”主题的手机和电脑客户端也完全不知道彼此的存在,大家只要订阅了“芯片温度”这一主题,MQTT 服务端就会在每次收到新信息时,将信息发送给订阅了“芯片温度”主题的客户端。

⚫ 空间上分离:空间上分离相对容易理解,MQTT 客户端以及 MQTT 服务端它们在通信时是处于同一个通信网络中的,这个网络可以是互联网或者局域网;只要客户端联网,无论他们远在天边还是近在眼前,都可以实现彼此间的通讯交流;其实网络通信本就是如此,所以并不是 MQTT 通信所特有的。

⚫ 时间上可异步:MQTT 客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要,前面我们也介绍了,MQTT 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的,高延迟和不可靠网络必然就会导致时间上的异步;物联网设备在运行过程中发生意外掉线是非常正常的情况,我们使用上面的实例二的场景来作说明,当开发板在运行过程中,可能会由于突然断电(假设开发板是通过电源适配器供电的)导致掉线,这时开发板会断开与 MQTT 服务端的连接。假设此时我们的手机客户端向开发板客户端所订阅的“LED 控制”主题发布了信息,而开发板恰恰不在线,这时,MQTT 服务端可以将“LED 控制”主题的新信息保存,待开发板客户端再次上线后,服务端再将“LED 控制”信息推送给开发板。所以这就必然导致了,手机发送信息与开发板接收信息在时间上是异步的。

MQTT服务端部署

推荐使用docker部署,一行命令搞定。

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

查看状态

[root@hqd235 ~]# docker ps|grep emqx
7305ee268494        emqx/emqx                   "/usr/bin/docker-ent…"   27 hours ago        Up 27 hours         4369-4370/tcp, 5369/tcp, 0.0.0.0:1883->1883/tcp, 0.0.0.0:8083-8084->8083-8084/tcp, 6369-6370/tcp, 0.0.0.0:8883->8883/tcp, 0.0.0.0:18083->18083/tcp, 11883/tcp, 0.0.0.0:18081->8081/tcp   emqx

查看部署日志

[root@hqd235 ~]# docker logs -f emqx --tail 200
listener.ssl.external.acceptors = "32"
listener.ssl.external.max_connections = "102400"
listener.tcp.external.acceptors = "64"
listener.tcp.external.max_connections = "1024000"
listener.ws.external.acceptors = "16"
listener.ws.external.max_connections = "102400"
listener.wss.external.acceptors = "16"
listener.wss.external.max_connections = "102400"
log.to = "console"
node.max_ets_tables = "2097152"
node.max_ports = "1048576"
node.name = "[email protected]"
node.process_limit = "2097152"
rpc.port_discovery = "manual"
Starting emqx on node [email protected]
Start mqtt:tcp:internal listener on 127.0.0.1:11883 successfully.
Start mqtt:tcp:external listener on 0.0.0.0:1883 successfully.
Start mqtt:ws:external listener on 0.0.0.0:8083 successfully.
Start mqtt:ssl:external listener on 0.0.0.0:8883 successfully.
Start mqtt:wss:external listener on 0.0.0.0:8084 successfully.
Start http:management listener on 8081 successfully.
Start http:dashboard listener on 18083 successfully.
EMQ X Broker 4.3.11 is running now!

访问web端页面,地址为http://host:port/, 上述示例访问地址为http://172.16.10.235:18083, 默认用户名密码为admin/public

登录后的页面如下图:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第3张图片
在页面上提供了监控、客户端信息、告警、统计等实用功能,同时设置中提供了主题和语言的切换。

MQTT客户端安装

客户端推荐mqttfx,界面简洁好用,测试完全够用。

下载链接:https://pan.baidu.com/s/1kRWp78GpQSTxVqatLJf3yg?pwd=wmmi 提取码:wmmi

下载完成后一路next即可,如果遇到需要输入license key的情况,那一定是下载错版本了,应该下载的是1.7.1的版本。

安装完成后界面如下:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第4张图片
点击齿轮进入设置页面

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第5张图片

新增配置文件,broker地址即上面服务端的地址,端口默认是1883,在UserCredentials中配置用户名密码,如果使用默认的话也就是admin/public

配置完成后点击Apply和ok保存即可。

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第6张图片
配置完成后点击界面上的Connect按钮,如果右侧出现绿色圆点,说明链接成功了。

publish下可以输入要发送的目标topic和内容,在subscribe中可以配置订阅的主题及收到的主题下的消息内容。

下图为简单示例:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第7张图片
【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第8张图片
【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第9张图片
先订阅test主题,然后给test主题发布消息,再去Subscribe模块下查看,可以看到能正常收到消息。

Springboot集成MQTT

springboot中集成Mqtt相对来说流程也比较简单,下面我们做一个简单的例子,仅为了演示流程。

  1. 新建springboot + maven项目

    pom中引入如下依赖:

    
    <dependency>
        <groupId>org.springframework.integrationgroupId>
        <artifactId>spring-integration-streamartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-integrationartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.integrationgroupId>
        <artifactId>spring-integration-mqttartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starterartifactId>
    dependency>
    
  2. 添加配置文件

    mqtt:
      #MQTT服务地址,端口号默认11883,如果有多个,用逗号隔开
      host: tcp://172.16.10.235:1883
      #用户名
      username: admin
      #密码
      password: public
      #客户端id(不能重复)
      clientId: from-springboot-apps
    
  3. 添加配置文件对应的类

    @Data
    @Configuration
    @ConfigurationProperties(prefix = "mqtt")
    public class MqttConfig {
    
        private String host;
    
        private String username;
    
        private String password;
    
        private String clientId;
    
    }
    
  4. 添加mqtt配置bean

    package com.zjtx.tech.message.config;
    
    import org.eclipse.paho.client.mqttv3.*;
    import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class MqttClientConfig {
    
        @Autowired
        private MqttConfig config;
    
        private MqttClient client;
    
        public static final Map<String, MqttClient> clientMap = new HashMap<>();
    
        @PostConstruct
        public void init() throws Exception {
            this.connect();
        }
    
        /**
         * 客户端连接服务端
         */
        public void connect() throws Exception {
            //创建MQTT客户端对象
            client = new MqttClient(config.getHost(), config.getClientId(), new MemoryPersistence());
            //连接设置
            MqttConnectOptions options = new MqttConnectOptions();
            //是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息
            //设置为true表示每次连接服务器都是以新的身份
            options.setCleanSession(true);
            //设置连接用户名
            options.setUserName(config.getUsername());
            //设置连接密码
            options.setPassword(config.getPassword().toCharArray());
            //设置超时时间,单位为秒
            options.setConnectionTimeout(100);
            //设置心跳时间 单位为秒,表示服务器每隔 1.5*20秒的时间向客户端发送心跳判断客户端是否在线
            options.setKeepAliveInterval(20);
            //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息
            options.setWill("willTopic", (config.getClientId() + "与服务器断开连接").getBytes(), 0, false);
            //设置回调
            client.setCallback(new MqttProviderCallBack(config.getClientId()));
            client.connect(options);
        }
    
        /**
         * 发布消息
         */
        public void publish(String topic,String message, int qos,boolean retained){
            MqttMessage mqttMessage = new MqttMessage();
            mqttMessage.setQos(qos);
            mqttMessage.setRetained(retained);
            mqttMessage.setPayload(message.getBytes());
            //主题的目的地,用于发布/订阅信息
            MqttTopic mqttTopic = client.getTopic(topic);
            //提供一种机制来跟踪消息的传递进度
            //用于在以非阻塞方式(在后台运行)执行发布是跟踪消息的传递进度
            MqttDeliveryToken token;
            try {
                //将指定消息发布到主题,但不等待消息传递完成,返回的token可用于跟踪消息的传递状态
                //一旦此方法干净地返回,消息就已被客户端接受发布,当连接可用,将在后台完成消息传递。
                token = mqttTopic.publish(mqttMessage);
                token.waitForCompletion();
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 断开连接
         */
        public void disConnect(){
            try {
                client.disconnect();
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 订阅主题
         */
        public void subscribe(String topic,int qos){
            try {
                client.subscribe(topic,qos);
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    
    
    }
    
    
  5. 添加回调类

    package com.zjtx.tech.message.config;
    
    import lombok.extern.slf4j.Slf4j;
    import org.eclipse.paho.client.mqttv3.IMqttAsyncClient;
    import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
    import org.eclipse.paho.client.mqttv3.MqttCallback;
    import org.eclipse.paho.client.mqttv3.MqttMessage;
    
    @Slf4j
    public class MqttProviderCallBack implements MqttCallback {
    
        public String clientId;
    
        public MqttProviderCallBack(String clientId) {
            this.clientId = clientId;
        }
    
        @Override
        public void connectionLost(Throwable throwable) {
            MqttClientConfig.clientMap.remove(clientId);
            log.info("{}与服务器断开链接", clientId);
        }
    
        @Override
        public void messageArrived(String topic, MqttMessage message) {
            log.info("接收消息主题 : {}", topic);
            log.info("接收消息Qos : {}",message.getQos());
            log.info("接收消息内容 : {}",new String(message.getPayload()));
        }
    
        @Override
        public void deliveryComplete(IMqttDeliveryToken token) {
            IMqttAsyncClient client = token.getClient();
            log.info(client.getClientId() + "发布消息成功!");
        }
    
    }
    
  6. 添加测试用的controller

    package com.zjtx.tech.message.controller;
    
    import com.cnhqd.common.core.web.domain.ResultBean;
    import com.cnhqd.message.config.MqttClientConfig;
    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;
    
    @RestController
    @RequestMapping("mqtt")
    public class MqttController {
    
        @Autowired
        private MqttClientConfig clientConfig;
    
        @GetMapping("publish")
        public ResultBean<Void> publish(String topic, String message){
            clientConfig.publish(topic, message, 2, true);
            return new ResultBean<>();
        }
    
        @GetMapping("subscribe")
        public ResultBean<Void> subscribe(String topic) {
            clientConfig.subscribe(topic, 2);
            return new ResultBean<>();
        }
    
    }
    
  7. 测试

    通过页面访问,先调用/mqtt/subscribe?topic=xxx, 再调用/mqtt/publish?topic=xxx&&message=xxxxxx,观察控制台输出。

    如我们执行http://localhost:9207/mqtt/subscribe?topic=test,订阅了test主题。

    再执行http://localhost:9207/mqtt/publish?topic=test&&message=FromSpringBootApplication,在test主题下发布了一条消息。

    查看控制台输出:

    控制台输出验证

    可以看到在应用中消息的发布和接收都是成功的。

    继续打开mqttfx客户端,查看test主题下是否收到该消息。

    【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第10张图片

mqttfx客户端也可以正常接收到消息。

我们再打开服务端的dashboard,查看下数据,如下所示:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第11张图片
如果需要查看指定主题下的数据需要打开主题监控模块,

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第12张图片
启用后进入到统计分析-主题监控模块下新建监控的主题,输入test

再次在网页上请求发布消息的接口,然后观察数据变化,演示如下:

【工作记录】MQTT介绍、安装部署及springboot集成@20230912_第13张图片
这里我发送了三条消息,有两个客户端订阅了该主题,所以流入3条,流出6条。均为正常数据。

至此,springboot中集成mqtt的整个过程就结束了。

总结

本文介绍了mqtt协议的相关特性,并总结了在springboot应用中集成mqtt的流程并验证。

mqtt作为目前物联网中高效的通讯协议,还是很值得研究的。

作为记录的同时也希望能帮助到需要的朋友们。

针对以上内容有任何问题欢迎留言评论~~~~

创作不易,欢迎一键三连~~~~

参考文章:

一文带你搞懂 MQTT - 知乎 (zhihu.com)

你可能感兴趣的:(工作记录,spring,boot,后端,java,mqtt,物联网)