MQTT 的原理与 VertX MQTT 的简单使用

2,796 阅读5分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

简介

MQTT(MQ Telemetry Transport)协议是物联网应用最广泛的协议,MQTT是指IBM的MQseries产品,与“消息队列”无关。当时需要开发一种最小电池损耗,最小宽带的协议,起初主要应用于嵌入式现在转变为开放物联网(IOT)用例。

发布/订阅模式

发布订阅模式广泛的应用于生活中,例如你订阅了微信公众号,作者发布了的公众号文章会推送给每个订阅者,公众号就是Publish ,读者就是 Subscriber ,文章就是 Message。

这个场景的特色在于:

  1. 空间解耦:Publish 和 Subscriber不需要相互知道他们的存在
  2. 时间解耦:Publish 和 Subscriber不需要同时运行
  3. 同步解耦:Publish 和 Subscriber不需要交互,在发布和接收期间不会相互锁定

总之,发布/订阅模型消除了消息发布者和接收者/订阅者之间的直接通信。代理的过滤活动可以控制哪个客户端/订阅者接收哪个消息。解耦包括三个维度:空间、时间和同步。

MQTT的应用

MQTT体现了发布/订阅模式的所有方面

  • MQTT 在空间上分离了发布者和订阅者。要发布或接收消息,发布者和订阅者只需要知道代理的主机名/IP 和端口。
  • MQTT 按时间解耦。尽管大多数 MQTT 用例都近乎实时地传递消息,但如果需要,代理可以为不在线的客户端存储消息。(必须满足两个条件才能存储消息:客户端已连接到持久会话并订阅了QoS大于 0 的主题)。
  • 异步工作。大多数Client 基于异步或回调模型,因此等待消息和发布消息不会阻塞任务。当然某些用例是可以同步,所以某些实现会提供同步API。但流程是同步的

MQTT与消息队列的区别

  1. 消息队列的消息直到被消费都会被存储
  2. 消息队列一条消息只能被一个客户端消费,MQTT的订阅者都会受到消息
  3. 队列在使用之前要用命令显示创建队列,topic 可以实时创建。

实时创建topic

//java服务器  Vert.x MQTT 相关代码
endpoint.publishHandler(message -> {
​
                System.out.println("Just received message [" + message.payload().toString(Charset.defaultCharset()) + "] with QoS [" + message.qosLevel() + "]");
​
                if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
                    endpoint.publishAcknowledge(message.messageId());
                } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
                    endpoint.publishReceived(message.messageId());
                }
​
            }).publishReleaseHandler(messageId -> {
​
                endpoint.publishComplete(messageId);
            });

使用MQTTX发消息

Topic: test1QoS: 2
​
{
  "msg": "这是test1"
}
2021-08-04 15:37:17

程序展示结果

使用MQTTX发消息

Topic: tes2QoS: 2
​
{
  "msg": "这是test2"
}
2021-08-04 15:39:14

程序展示结果

Just received message [{  "msg": "这是test2"}] with QoS [EXACTLY_ONCE]

服务的建立与连接

MQTT 协议基于 TCP/IP。客户端和代理都需要有一个 TCP/IP 堆栈。

MQTT 连接始终位于一个客户端和代理之间。客户端从不直接相互连接。要发起连接,客户端向代理发送 CONNECT 消息。代理使用 CONNACK 消息和状态代码进行响应。建立连接后,代理将保持打开状态,直到客户端发送断开连接命令或连接中断。

MQTTClient

public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        MqttClientOptions options = new MqttClientOptions()
                .setUsername("Oneths").setPassword("123456")
                .setWillFlag(true).setWillRetain(true).setWillQoS(1).setWillTopic("oneths").setWillMessage("ONETHS_IOT");
        MqttClient client = MqttClient.create(vertx, options);
        client.connect(40000, "127.0.0.1", s -> {
            client.disconnect();
        });
    }

MQTTServer

public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        MqttServerOptions options = new MqttServerOptions()
                .setPort(40000);
        MqttServer mqttServer = MqttServer.create(vertx,options);
        //略
    }

发布订阅和取消订阅

发布(Publish)

MQTT 客户端可以在连接到代理后立即发布消息。MQTT 在代理上使用基于主题的消息过滤。每条消息都必须包含一个主题,代理可以使用该主题将消息转发给感兴趣的客户端。通常,每条消息都有一个负载,其中包含要以字节格式传输的数据

Topic 名称

主题名称是一个简单的字符串 ,它以正斜杠作为分隔符进行分层结构 ,例如 Germany/Munich/Octoberfest/people/myhome/livingroom/temperature

使用工具MTQQX后可以图形化操作

QoS(服务质量)

有三个级别:0、1 和 2。服务级别决定了消息到达预期接收者(客户端或代理)的保证类型

QoS 0 - 最多一次:最低等级,此服务最大努力交付,不能保证交货。接收方 不会确认收到消息,发送方 不会存储和重新传输。

QoS 1 - 至少一次 : 发送方 消息可以多次发送,保证消息至少传递一次给接收者,发送方会存贮消息,直到接收到 接收方发送的 PUBACK 数据包。接收方接收到之后,可以立即处理,如果是代理接收,代理会回复PUBACK 并且给所有订阅者发送消息,如果重复发送信息 会设置重复 (DUP) 标志。

Qos 2 - 恰好一次: MQTT 中最高级别的服务。此级别保证每条消息仅被预期收件人接收一次。QoS 2 是最安全、最慢的服务质量级别。

发送方发送 QoS 2 PUBLISH数据包 给接受方 ,接受方回复PUBREC 数据包 让发送方确认。如果发送方没有收到会重复发送 QoS 2 PUBLISH数据包 。直到收到PUBREC 数据包

一旦发送方收到来自接收方的 PUBREC 数据包,发送方就可以安全地丢弃初始的 PUBLISH 数据包。发送方存储来自接收方的 PUBREC 数据包,并以PUBREL数据包进行响应 。

package io.netty.handler.codec.mqtt;
​
public enum MqttQoS {
    AT_MOST_ONCE(0),
    AT_LEAST_ONCE(1),
    EXACTLY_ONCE(2),
    FAILURE(0x80);
    //略 
}

订阅(Subscribe)

如果没有人收到消息,发布消息就没有意义。换句话说,如果没有客户端订阅消息的主题。要接收有关感兴趣主题的消息,客户端向MQTT 代理发送SUBSCRIBE消息。这个订阅消息非常简单,它包含一个唯一的数据包标识符和一个订阅列表。

订阅部分代码

endpoint.subscribeHandler(subscribe -> {
​
                List<MqttQoS> grantedQosLevels = new ArrayList<>();
                for (MqttTopicSubscription s: subscribe.topicSubscriptions()) {
                    System.out.println("Subscription for " + s.topicName() + " with QoS " + s.qualityOfService());
                    grantedQosLevels.add(s.qualityOfService());
                }
                // ack the subscriptions request
                endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQosLevels);
​

程序反馈的消息

Subscription for test1 with QoS AT_MOST_ONCE
Subscription for test2 with QoS EXACTLY_ONCE
Subscription for test3 with QoS AT_LEAST_ONCE

推荐

本文的理论部分主要参考 HiveMq编写的 mqtt入门文档

www.hivemq.com/mqtt-essent…

本文的 代码实现部分主要参考

vertx.io/docs/vertx-…

工具

mqttx.app/