MQTT入门与进阶

538 阅读14分钟

前言

MQTT是什么

MQTT全称MQ Telemetry Transport,是一种基于发布/订阅模式的轻量级消息传输协议,专门针对低带宽和不稳定网络环境的物联网应用设计,可以用极少的代码和资源提供可靠的消息服务。

来源

MQTT协议最初于 1999 年发布,用于石油行业。工程师需要一种协议只占用最小的带宽和电池损耗,以通过卫星监控石油管道。现在MQTT协议广泛运用于物联网,移动物联网,智能硬件等等领域。

2010 年,IBM 发布了 MQTT 3.1 作为任何人都可以实施的免费开放协议,然后于 2013 年将其提交给结构化信息标准促进组织 (OASIS) 规范机构进行维护。2019 年,OASIS 发布了升级的 MQTT 版本 5。

本文基于MQTT 3.1

特点

MQTT 必须具备以下几点

  • 简单容易实现

  • 支持 QoS(设备网络环境复杂)

  • 轻量且省带宽(最初带宽很贵)

  • 数据无关(不关心 Payload 数据格式)

  • 有持续地会话感知能力(时刻知道设备是否在线)

发布订阅

发布订阅模式是一种消息传递的模式,消息发送者(发布者)和消息接收者(订阅者)互相不知道对方的存在,因此充分解耦。消息具有主题(Topic),双方可以通过指定主题来发送和获取特定的消息。

image.png

发布订阅模型具有以下几个部分

  • 发布者(Publisher)

    负责发送指定主题的消息,一次只能向一个主题发送数据。

  • 订阅者(Subscriber)

    订阅者通过订阅主题接收消息,一次可订阅多个主题。

  • 代理(Broker)

    负责接收发送者发来的消息,根据规则发送到指定订阅者。也负责处理客户端发起的连接、断开连接、订阅、取消订阅等请求

  • 主题(Topic)

    消息需要指定主题,双方通过主题来发送和获取特定的消息,它类似 URL 路径,使用斜杠 / 进行分层,比如 sensor/1/temperature

在发布/订阅模型中,发布者与订阅者只是根据行为定义的不同身份,实际上一个客户端既可以是发布者也可以是订阅者。

发布

消息

消息组成

  • 包ID(Packet ID)

    这个标识符唯一标识一个在客户端和服务端传递的消息,仅在Qos大于0时生效,由MQTT自动生成

  • 主题(Topic Name)

    发布的信息的主题名字,MQTT 协议基于主题进行消息路由,主题类似 URL 路径/sensor1/temparature

  • 消息服务质量等级(QoS)

    Qos指定了消息的质量等级,数值由0、1、2,它保证了在不同的网络环境下消息传递的可靠性。

  • 保留消息标志(Retain Flag )

    指定服务是否为该主题保留一条最新消息,若是,则当新订阅者订阅时,可以马上获取该消息

  • 消息负载(Payload)

    实际要发送的消息内容,不限制格式

  • 重复发送标志(DUP flag)

    消息,当重发时标记

创建连接

MQTT是基于TCP/IP的协议,端口通常为1883,还有TLS/SSL,端口一般为8883

MQTT连接由客户端向服务端发起,客户端先发送一个CONNECT数据包给服务器。服务器收到 CONNECT 包后会回复一个 CONNACK 给客户端,客户端收到 CONNACK 包后表示 MQTT 连接建立成功。如果客户端在超时时间内未收到服务器的 CONNACK 数据包,就会主动关闭连接。

以下是连接参数

  • 客户端ID(Client ID)

    MQTT服务器使用Client ID识别客户端,要求每个客户端的Client ID都必须唯一,通常是1-23个字节的UTF-8字符串,如果重复使用同一个Client ID连接,服务器会将之前的同Client ID的客户端踢下线。

  • 用户名与密码(Username & Password)

    MQTT协议可以通过用户名和密码进行认证和授权,最好使用mqtts以避免认证信息明文泄露

  • 保活周期(Keep Alive)

    保活周期是为了确保连接不被断开,在客户端无报文发送时,定时发送心跳报文的参数,以秒为单位的间隔。

    在连接建立成功后,如果服务器在Keep Alive的1.5倍时间内没有收到客户端的任何包,则认定连接出现了问题,会断开和客户端的连接。

  • 清除会话(Clean Session)

    该参数表示在客户端断开连接时,是否清除会话。为false时表示创建一个持久会话,客户端断开连接时,保持会话并保存离线消息。持久会话恢复的前提是客户端使用固定的Client ID再次连接。

  • 遗嘱消息(Last Will)

    遗嘱消息是 MQTT 为那些可能出现意外断线的设备提供的将遗嘱优雅地发送给其他客户端的能力。设置了遗嘱消息消息的 MQTT 客户端异常下线时,MQTT 服务器会发布该客户端设置的遗嘱消息。

遗嘱消息(Will Message)

遗嘱消息可以看作是一个简化版的 MQTT 消息,它也包含 Topic、Payload、QoS、Retain 等信息。当设备意外断线时,遗嘱消息将被发送至遗嘱 Topic;

遗嘱消息的典型使用场景,通常结合Retain使用

  1. 客户端 A 遗嘱消息内容设定为 offline,该遗嘱主题与一个普通发送状态的主题设定成同一个 A/status
  2. 当客户端 A 连接时,向主题 A/status 发送内容为 online 的 Retained 消息,其它客户端订阅主题 A/status 的时候,将获取到 Retained 消息为 online
  3. 当客户端 A 异常断开时,系统自动向主题 A/status 发送内容为 offline 的消息,其它订阅了此主题的客户端会马上收到 offline 消息;如果遗嘱消息设置了 Will Retain,那么此时如果有新的订阅 A/status 主题的客户端上线,也将获取到内容为 offline 的遗嘱消息

消息主题(Topic)

MQTT 主题本质上是一个 UTF-8 编码的字符串,是 MQTT 协议进行消息路由的基础。MQTT 主题类似 URL 路径,使用斜杠 / 进行分层:

chat/room/1
sensor/10/temperature
sensor/+/temperature
sensor/#

MQTT主题不需要提前创建,发布和订阅时若不存在则自动创建。也不需要手动删除

通配符

订阅者订阅MQTT主题可以用单层通配符 +及多层通配符 #更灵活指定

注意:通配符只能用于订阅,不能用于发布。

单层通配符
+ 有效
sensor/+ 有效
sensor/+/temperature 有效
sensor+ 无效(没有占据整个层级)

如果客户端订阅了主题 sensor/+/temperature,将会收到以下主题的消息:

sensor/1/temperature
sensor/2/temperature
...
sensor/n/temperature

但是不会匹配以下主题:

sensor/temperature
sensor/bedroom/1/temperature
多层通配符

多层通配符表示它的父级和任意数量的子层级,在使用多层通配符时,它必须占据整个层级并且必须是主题的最后一个字符:

# 有效,匹配所有主题
sensor/# 有效
sensor/bedroom# 无效(没有占据整个层级)
sensor/#/temperature 无效(不是主题最后一个字符)

如果客户端订阅主题 senser/#,它将会收到以下主题的消息:

sensor
sensor/temperature
sensor/1/temperature

以 $ 开头的主题

服务器包含以 $SYS/ 开头的主题为系统主题,系统主题主要用于获取MQTT 服务器自身运行状态、消息统计、客户端上下线事件等数据。

主题说明
$SYS/brokersEMQX 集群节点列表
$SYS/brokers/emqx@127.0.0.1/versionEMQX 版本
$SYS/brokers/emqx@127.0.0.1/uptimeEMQX 运行时间
$SYS/brokers/emqx@127.0.0.1/datetimeEMQX 系统时间
$SYS/brokers/emqx@127.0.0.1/sysdescrEMQX 系统信息

QoS

而只依靠底层的 TCP 传输协议,并不能完全保证消息的可靠到达。因此,MQTT 提供了 QoS 机制,其核心是设计了多种消息交互机制来提供不同的服务质量,来满足用户在各种场景下对消息可靠性的要求。

MQTT 定义了三个 QoS 等级,分别为:

  • QoS 0,最多交付一次。(可能丢失)

  • QoS 1,至少交付一次。(可能重复)

  • QoS 2,只交付一次。(流程复杂,耗资源)

如果发送和订阅指定的QoS等级不同,则QoS可能会降级

例如,订阅者在订阅时要求 Broker 可以向其转发的消息的最大 QoS 等级为 QoS 1,那么后续所有 QoS 2 消息都会降级至 QoS 1 转发给此订阅者,而所有 QoS 0 和 QoS 1 消息则会保持原始的 QoS 等级转发。

QoS 0 - 最多交付一次

QoS 0 消息即发即弃,不需要等待确认,不需要存储和重传,因此对于接收方来说,永远都不需要担心收到重复的消息。

此处发送端与接收端指客户端与服务端之间的交互,并不指发布者与订阅者之间的完整流程。

QoS 1 - 至少交付一次

QoS 0只发送一次就再不管不问,因此为了保证消息到达,QoS 1加入了应答与重传机制,发送方只有在收到接收方的 PUBACK 报文以后,才能认为消息投递成功,在此之前,发送方需要存储该 PUBLISH 报文以便下次重传。

QoS 1 需要在 PUBLISH 报文中设置 Packet ID,而作为响应的 PUBACK 报文,则会使用与 PUBLISH 报文相同的 Packet ID,以便发送方收到后删除正确的 PUBLISH 报文缓存。

QoS1

QoS 1为了保证到达,有可能重发导致重复。

发送方没收到PUBACK存在以下情况

  1. PUBLISH 未到达接收方

    该情况下虽然发送方重传了报文,但是接收方只收到一次

  2. PUBLISH 已经到达接收方,接收方的 PUBACK 报文还未到达发送方

    该情况下,发送方重传时,接收方已经受到过PUBLISH报文里,因此重复。

第二种情况下接收者收到两次消息,虽然第二次消息DUP被标识为1,表明这是重发的消息,且Packet ID一致,但是并不能假定第二次收到的和第一次收到的是同一个消息来解决重复问题,它都要将其视为全新的消息,交给业务端处理。

因为对接收方来说存在以下两种情况,它并不能甄别是其中哪一项

MQTT QoS 1 重复消息

  • 第一种情况,发送方由于没有收到 PUBACK 报文而重传了 PUBLISH 报文。此时,接收方收到的前后两个 PUBLISH 报文使用了相同的 Packet ID,并且第二个 PUBLISH 报文的 DUP 标志为 1,此时它确实是一个重复的消息。

  • 第二种情况,第一个 PUBLISH 报文已经完成了投递,1024 这个 Packet ID 重新变为可用状态。发送方使用这个 Packet ID 发送了一个全新的 PUBLISH 报文,但这一次报文未能到达对端,所以发送方后续重传了这个 PUBLISH 报文。这就使得虽然接收方收到的第二个 PUBLISH 报文同样是相同的 Packet ID,并且 DUP 为 1,但确实是一个全新的消息。

    由于我们无法区分这两种情况,所以只能让接收方将这些 PUBLISH 报文都当作全新的消息来处理,都交给业务端。因此当我们使用 QoS 1 时,消息的重复在协议层面上是无法避免的,只能从业务方面去重或幂等,如Payload种带上一个时间戳,来区分不通的消息

QoS 2 - 只交付一次

QoS 2为了解决消息可能的丢失或重复问题, 每一次消息投递,都要求发送方与接收方进行至少两次请求/响应流程。

  1. 首先,发送方存储并发送 QoS 为 2 的 PUBLISH 报文以启动一次 QoS 2 消息的传输,然后等待接收方回复 PUBREC 报文。这一部分与 QoS 1 基本一致,只是响应报文从 PUBACK 变成了 PUBREC。

  2. 当发送方收到 PUBREC 报文,即可确认对端已经收到了 PUBLISH 报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。所以此时发送方可以删除本地存储的 PUBLISH 报文,然后发送一个 PUBREL 报文,通知对端自己准备将本次使用的 Packet ID 标记为可用了。与 PUBLISH 报文一样,我们需要确保 PUBREL 报文到达对端,所以也需要一个响应报文,并且这个 PUBREL 报文需要被存储下来以便后续重传。

  3. 当接收方收到 PUBREL 报文,也可以确认在这一次的传输流程中不会再有重传的 PUBLISH 报文到达,因此回复 PUBCOMP 报文表示自己也准备好将当前的 Packet ID 用于新的消息了。

  4. 当发送方收到 PUBCOMP 报文,这一次的 QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的 PUBLISH 报文时,也会将它视为一个全新的消息。

  5. 此时才会将消息正式交给业务端处理。

QoS 2与QoS 1的核心区别在于QoS 2增加的PUBREL流程,帮助双方正确释放Packet ID。

QoS 规定只有在发送方收到对端恢复的PUBCOMP信号,才可以释放Packet ID,重新使用该ID发送新消息。

因此我们能够在协议层面去重,业务端收到的消息必定是新消息。

适用情况

  • QoS 0:投递效率高,但可能丢失,所以通常选择使用 QoS 0 传输一些高频且不那么重要的数据,比如传感器数据,周期性更新,即使遗漏几个周期的数据也可以接受。

  • QoS 1:保证到达,但可能重复。效率略高于前者。适合下达关键指令,更新等操作。但需注意开关等指令,可能导致错乱需要去重处理。

  • QoS 2: 既保证到达,也不会重复,传输成本最高。效率大致为前两者一半。

客户端使用

Java方面可以使用Eclipse的Paho客户端

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

发布 MQTT 消息

EMQX提供了免费的公共测试服务器,我们可以直接使用,也可以下载docker镜像本地安装。

创建一个发布客户端类 PublishSample,该类将发布一条 Hello MQTT 消息至主题 mqtt/test

package io.emqx.mqtt;

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.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class PublishSample {

   public static void main(String[] args) {

       String broker = "tcp://broker.emqx.io:1883";
       String topic = "mqtt/test";
       String username = "emqx";
       String password = "public";
       String clientid = "publish_client";
       String content = "Hello MQTT";
       int qos = 0;

       try {
           MqttClient client = new MqttClient(broker, clientid, new MemoryPersistence());
           // 连接参数
           MqttConnectOptions options = new MqttConnectOptions();
           // 设置用户名和密码
           options.setUserName(username);
           options.setPassword(password.toCharArray());
           options.setConnectionTimeout(60);
           options.setKeepAliveInterval(60);
           // 连接
           client.connect(options);
           // 创建消息并设置 QoS
           MqttMessage message = new MqttMessage(content.getBytes());
           message.setQos(qos);
           // 发布消息
           client.publish(topic, message);
           System.out.println("Message published");
           System.out.println("topic: " + topic);
           System.out.println("message content: " + content);
           // 关闭连接
           client.disconnect();
           // 关闭客户端
           client.close();
      } catch (MqttException e) {
           throw new RuntimeException(e);
      }
  }
}

订阅 MQTT 主题

创建一个订阅客户端类 SubscribeSample,该类将订阅主题 mqtt/test

package io.emqx.mqtt;

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class SubscribeSample {
   public static void main(String[] args) {
       String broker = "tcp://broker.emqx.io:1883";
       String topic = "mqtt/test";
       String username = "emqx";
       String password = "public";
       String clientid = "subscribe_client";
       int qos = 0;

       try {
           MqttClient client = new MqttClient(broker, clientid, new MemoryPersistence());
           // 连接参数
           MqttConnectOptions options = new MqttConnectOptions();
           options.setUserName(username);
           options.setPassword(password.toCharArray());
           options.setConnectionTimeout(60);
           options.setKeepAliveInterval(60);
           // 设置回调
           client.setCallback(new MqttCallback() {

               public void connectionLost(Throwable cause) {
                   System.out.println("connectionLost: " + cause.getMessage());
              }

               public void messageArrived(String topic, MqttMessage message) {
                   System.out.println("topic: " + topic);
                   System.out.println("Qos: " + message.getQos());
                   System.out.println("message content: " + new String(message.getPayload()));

              }

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

          });
           client.connect(options);
           client.subscribe(topic, qos);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
}

MqttCallback 说明:

  • connectionLost(Throwable cause): 连接丢失时被调用
  • messageArrived(String topic, MqttMessage message): 接收到消息时被调用
  • deliveryComplete(IMqttDeliveryToken token): 消息发送完成时被调用

以上就是全部内容啦。

资料引用自以下链接

EMQX - MQTT 协议入门与进阶

HiveMQ - MQTT概要