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

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

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

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

    MQTT示意图二
    上图中的所有客户端都是围绕“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
    
    • 1

    查看状态

    [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
    
    • 1
    • 2

    查看部署日志

    [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 = "7305ee268494@172.17.0.2"
    node.process_limit = "2097152"
    rpc.port_discovery = "manual"
    Starting emqx on node 7305ee268494@172.17.0.2
    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!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    登录后的页面如下图:

    dashboard页面展示
    在页面上提供了监控、客户端信息、告警、统计等实用功能,同时设置中提供了主题和语言的切换。

    MQTT客户端安装

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

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

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

    安装完成后界面如下:

    mqttfx客户端页面一
    点击齿轮进入设置页面

    mqtt客户端页面二

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

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

    mqttfx客户端连接
    配置完成后点击界面上的Connect按钮,如果右侧出现绿色圆点,说明链接成功了。

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

    下图为简单示例:

    mqttfx客户端订阅
    mqttfx客户端发布
    mqttfx客户端消息查看
    先订阅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>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    2. 添加配置文件

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

      @Data
      @Configuration
      @ConfigurationProperties(prefix = "mqtt")
      public class MqttConfig {
      
          private String host;
      
          private String username;
      
          private String password;
      
          private String clientId;
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    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();
              }
          }
      
      
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
    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() + "发布消息成功!");
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
    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<>();
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
    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主题下是否收到该消息。

      mqttfx客户端消息接收验证

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

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

    dashboard查看客户端消息
    如果需要查看指定主题下的数据需要打开主题监控模块,

    打开主题监控模块
    启用后进入到统计分析-主题监控模块下新建监控的主题,输入test

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

    dashboard查看主题监控数据
    这里我发送了三条消息,有两个客户端订阅了该主题,所以流入3条,流出6条。均为正常数据。

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

    总结

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

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

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

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

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

    参考文章:

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

  • 相关阅读:
    关于大型语言模型的争论和局限
    React 中利用解构语法 ... 快速方便传递 props 参数
    Linux·知识点常见问题
    全自动采集软件-自动采集为原创发布工具
    代码随想录训练营 Day58打卡 图论part08 拓扑排序 dijkstra朴素版 + 堆优化版
    缓存一致性MESI与内存屏障
    【HMS Core】【SDK集成】如何解决集成华为分析SDK带来的隐私政策合规检测异常的问题
    C#复习:面向对象基本概念
    R语言读取(加载)txt格式数据为dataframe、可视化绘制温度直方图、为直方图添加标题
    WSL2编译ijkplayer
  • 原文地址:https://blog.csdn.net/u010361276/article/details/132841598