MQTT协议理解---发布与订阅

本文深入解析MQTT协议中的订阅与发布模型,包括订阅与取消订阅的过程,PUBLISH数据包的结构,以及如何通过代码实现消息的发布和订阅。重点介绍了主题在MQTT中的作用和QoS级别,同时强调了持久化会话和离线消息的概念。
摘要由CSDN通过智能技术生成

4.1 订阅与发布模型

在第一课中,我们介绍了 MQTT 基于订阅与发布的消息模型,MQTT 协议的订阅与发布是基于主题的(Topic),一个典型的 MQTT 消息发送与接收的流程如下:

  1. ClientA 连接到 Broker;
  2. ClientB 连接到 Broker,并订阅主题 Topic1;
  3. ClientA 发送给 Broker 一条消息,主题为 Topic1;
  4. Broker 收到 ClientA 的消息,发现 ClientB 订阅了 Topic1,然后将消息转发到 ClientB;
  5. ClientB 从 Broker 接收到该消息。

和传统的队列有点不同,如果 ClientB 在 ClientA 发布消息之后再订阅 Topic1,ClientB 不会收到该条消息。

MQTT 通过订阅与发布模型对消息的发布者和订阅者进行解耦,发布者在发布消息时并不需要订阅方也连接到 Broker,只要订阅方之前订阅过相应主题,那么它在连接到 Broker 之后就可以收到发布方在它离线期间发布的消息。为了方便起见,在本课程中我们称这种消息为离线消息。

接收离线的消息需要 Client 使用持久化会话,且发布时消息的 QoS 大于 1。

在我们往下继续学习之前,很有必要搞清楚两组概念:发布者(Publisher)和订阅者(Subscriber),发送方(Sender)和接收方(Recevier)。弄清楚这两个概念才能很好理解订阅和发布的流程,以及之后 QoS 的概念。

4.1.1 Publisher 和 Subscriber

Publisher 和 Subscriber 是相对于 Topic 来说的身份,如果一个 Client 向某个 Topic 发布消息,那么它就是 Publisher;如果一个 Client 订阅了某个 Topic,那么它就是 Subscriber。在上面的例子中,ClientA 是 Publisher, ClientB 是 Subscriber。

4.1.2 Sender 和 Receiver

Sender 和 Receiver 是相对于消息传输方向的身份,仍然是上面的例子:

  • 当 ClientA 发布消息时,它发送给 Broker 一条消息,那么 ClientA 是 Sender,Broker 是 Receiver;
  • 当 Broker 转发消息给 ClientB 时,Broker 是 Sender,ClientB 是 Receiver。

Publisher/Subscriber、Sender/Receiver 这两组概念最大的区别就是,Publisher 和 Subscriber 只可能是 Client。而 Sender/Receiver 有可能是 Client 和 Broker。解释清楚这两个不同的概念之后,我们接下来看一下 PUBLISH 消息包。

4.2 PUBLISH

PUBLISH 数据包是用于在 Sender 和 Receiver 之间传输消息数据的,也就是说,当 Publisher 要向某个 Topic 发布一条消息的时候,Publisher 会向 Broker 发送一个 PUBLISH 数据包;当 Broker 要将一条消息转发给订阅了某条主题的 Subscriber 时,Broker 也会向 Subscriber 发送一条 PUBLISH 数据包。PUBLISH 数据包的内容如下。

4.2.1 固定头

消息重复标识(DUP flag):1bit,0 或者 1,当 DUP flag = 1 的时候,代表该消息是一条重发消息,因 Receiver 没有确认收到之前的消息而重新发送的。这个标识只在 QoS 大于 0 的消息中使用。

QoS:2bit,0、1 或者 2,代表 PUBLISH 消息的 QoS level,我们在 QoS 课程再详细讲解。

Retain 标识(Retain flag):1bit,0 或者 1,在从 Client 发送到 Broker 的 PUBLISH 消息中被设为 1 的时候,Broker 应该保存该条消息,当之后有任何新的 Subscriber 订阅 PUBLISH 消息中指定的主题时,都会先收到该条消息,这种消息也叫 Retained 消息;在从 Broker 发送到 Client 的 PUBLISH 消息中被设为 1 的时候,代表该条消息是一条 Retained 消息。

4.2.2 可变头

数据包标识( Packet Identifier):2bit,用来标识一个唯一数据包,数据包标识只需要保证在从 Sender 到 Receiver 的一次消息交互(比如发送、应答为一次交互)中保持唯一。只在 QoS 大于 1 的消息中使用,因为只有 QoS 大于 1 的消息有应答流程,我们会在《第06课:QoS0 和 QoS1》详细讲解。

主题名称(Topic Name):主题名称是一个 UTF-8 编码的字符串,用来命名该消息发布到哪一个主题,Topic Name 可以是长度大于等于 1 任何一个字符串(可包含空格),但是在实际项目中,我们最好还是遵循以下一些最优方法。

  • 主题名称应该包含层级,不同的层级用 / 划分,比如,2 楼 201 房间的温度感应器可以用这个主题:“home/2ndfloor/201/temperature”。
  • 主题名称开头不要使用 /,例如:“/home/2ndfloor/201/temperature”。
  • 不要在主题中使用空格。
  • 只使用 ASCII 字符。
  • 主题名称在可读的前提下尽量短。
  • 主题是大小写敏感的,“Home” 和 “home” 是两个不同的主题。
  • 可以将设备的唯一标识加到主题中,比如:“warehouse/shelf/shelf1_ID/status”。
  • 主题尽量精确,不要使用泛用的主题,例如在 201 房间有三个传感器,温度、亮度和湿度,那么你应该使用三个主题名称:“home/2ndfloor/201/temperature”、“home/2ndfloor/201/brightness”和“home/2ndfloor/201/humidity”,而不是让三个传感器都使用“home/2ndfloor/201”。
  • 以 $ 开头的主题属于 Broker 预留的系统主题,通常用于发布 Broker 的内部统计信息,比如 $SYS/broker/clients/connected,应用程序不要使用 $ 开头的主题收发数据。

4.2.3 消息体(Payload)

PUBLISH 消息的消息体中包含的是该消息要发送的具体数据,数据可以是任何格式的,二进制数据、文本、JSON 等,由应用程序来定义。在实际生产中,我们可以使用 JSON、Protocol Buffer 等对数据进行编码。

当 Receiver 收到来自 Sender 的 PUBLISH 消息时,根据 QoS 的不同,还有后续的应答流程。我们在 QoS 课程再详细讲解。

当 PUBLISH 消息的 QoS=0 时, Receiver 不做任何应答。

4.3 代码实践:发布消息

接下来我们写一小段代码,向一个主题发布一条 QoS 为 1 的使用 JSON 编码的数据,然后退出:

//publisher.js

javascript

var mqtt = require(‘mqtt’)

var client = mqtt.connect(‘mqtt://iot.eclipse.org’, {

​ clientId: “mqtt_sample_publisher_1”,

​ clean: false

})

client.on(‘connect’, function (connack) {

if(connack.returnCode == 0){

​ client.publish(“home/2ndfloor/201/temperature”, JSON.stringify({current: 25}), {qos: 1}, function (err) {

if(err == undefined) {

console.log(“Publish finished”)

​ client.end()

​ }else{

console.log(“Publish failed”)

​ }

​ })

​ }else{

console.log(Connection failed: ${connack.returnCode})

​ }

})

运行 node publisher.js,会得到以下输出:

Publish finished

5.1 订阅

订阅主题的流程如下:

img

  1. Client 向 Broker 发送一个 SUBSCRIBE 数据包,其中包含了 Client 想要订阅的主题以及其他一些参数;
  2. Broker 收到 SUBSCRIBE 数据包后,向 Client 发送一个 SUBACK 数据包作为应答。

接下来我们看数据包的具体内容。

5.1.1 SUBSCRIBE
5.1.1.1 可变头(Variable header)

数据包标识(Packet Identifier):两个字节,用来唯一标识一个数据包,数据包标识只需要保证在从 Sender 到 Receiver 的一次消息交互中保持唯一。

5.1.1.2 消息体(Payload)

订阅列表(List of Subscriptions):SUBSCRIBE 的消息体中包含 Client 想要订阅的主题列表,列表中的每一项由订阅主题名和对应的 QoS 组成。主题名中可以包含通配符,单层通配符“+”和多层通配符“#”。使用包含通配符的主题名可以订阅满足匹配条件的所有主题。为了和 PUBLISH 中的主题区分,我们叫 SUBSCRIBE 中的主题名为主题过滤器(Topic Filter)。

单层通配符“+”:就如之前我们讲的,MQTT 的主题是具有层级概念的,不同的层级之间用“/”分割,“+”可以用来指代任意一个层级。

例如“home/2ndfloor/+/temperature”,可匹配:

  • home/2ndfloor/201/temperature
  • home/2ndfloor/202/temperature

不可匹配:

  • home/2ndfloor/201/livingroom/temperature
  • home/3ndfloor/301/temperature

多层通配符“#”:“#”和“+”的区别在于,“#”可以用来指定任意多个层级,但是“#”必须是 Topic Filter 的最后一个字符,同时它必须跟在“/”后面,除非 Topic Filter 只包含“#”这一个字符。

例如“home/2ndfloor/#”,可匹配:

  • home/2ndfloor
  • home/2ndfloor/201
  • home/2ndfloor/201/temperature
  • home/2ndfloor/202/temperature
  • home/2ndfloor/201/livingroom/temperature

不可匹配:

  • home/3ndfloor/301/temperature

注意:“#”是一个合法的 Topic Filter,代表所有的主题;而“home#”不是一个合法的 Topic Filter,因为“#”号需要跟在“/”后面。

SUBSCRIBE 数据包中 QoS 代表针对某一个或者一组主题,Client 希望 Broker 在发送来自这些主题的消息给它时,消息使用的 QoS 级别,我们在《第06课:QoS0 和 QoS1》里面再详细讨论。

5.1.2 SUBACK

为了确认每一次的订阅,Broker 收到 SUBSCRIBE 之后会回复一个 SUBACK 数据包作为应答。

5.1.2.1 可变头(Variable header)

数据包标识(Packet Identifier):两个字节,用来唯一标识一个数据包,数据包标识只需要保证在从 Sender 到 Receiver 的一次消息交互中保持唯一。

5.1.2.2 消息体(Payload)

返回码(return codes):SUBBACK 数据包包含了一组返回码,返回码的数量和顺序和 SUBSCRIBE 数据包的订阅列表对应,用于标识订阅类别中的每一个订阅项的订阅结果。

返回码含义
0订阅成功, 最大可用QoS为0
1订阅成功,最大可用QoS为1
2订阅成功, 最大可用QoS为2
128订阅失败

返回码 0~2 代表订阅成功,同时 Broker 授予 Subscriber 不同的 QoS 等级,这个等级可能会和 Subscriber 在 SUBSCRIBE 数据包中要求的不一样。

返回码 128 代表订阅失败,比如 Client 没有权限订阅某个主题,或者要求订阅的主题格式不正确等。

5.1.3 代码实践:订阅一个主题

接下来我们来写订阅并处理消息的代码,我们订阅在上一课中的 publisher.js 中的主题,并通过捕获“message”事件获取接收的消息并打印出来。

通常我们在建立和 Broker 的连接之后就可以开始订阅了,但是这里有一个小小的优化,如果你建立的是持久会话的连接,那么有可能 Broker 已经保存你在之前的连接时订阅的主题,你就没有必要再发起 SUBSCRIBE 请求了,这个小优化在网络带宽或者设备处理能力较差的情况尤为重要。

完整的代码 subscriber.js 如下:

var mqtt = require('mqtt')
var client = mqtt.connect('mqtt://iot.eclipse.org', {
    clientId: "mqtt_sample_subscriber_id_1",
    clean: false
})
 
client.on('connect', function (connack) {
    if(connack.returnCode == 0) {
        if (connack.sessionPresent == false) {
            console.log("subscribing")
            client.subscribe("home/2ndfloor/201/temperature", {
                qos: 1
            }, function (err, granted) {
                if (err != undefined) {
                    console.log("subscribe failed")
                } else {
                    console.log(`subscribe succeeded with ${granted[0].topic}, qos: ${granted[0].qos}`)
                }
            })
        }
    }else {
        console.log(`Connection failed: ${connack.returnCode}`)
    }
})
 
client.on("message", function (_, message, _) {
    var jsonPayload = JSON.parse(message.toString())
    console.log(`current temperature is ${jsonPayload.current}`)
})

在终端上运行 node subscriber.js 我们会得到以下输出:

subscribing
subscribe succeeded with home/2ndfloor/201/temperature, qos: 1

第一次运行这个代码的时候,Broker 上面没有保存这个 Client 的会话,所以需要进行订阅,现在 CTRL+C 终止这段代码的运行,然后重新运行,因为 Broker 上面已经保存了这个 Client 的会话,所以就不需要再订阅了,你就不会看到订阅相关的输出了。

在上一课中,我们运行过 publisher.js,向“home/2ndfloor/201/temperature”这个主题发布过一个消息,但是这发生在 subscriber.js 订阅该主题之前,所以现在 Subscriber 不会收到任何消息,我们需要再运行一次 publish.js,然后在运行 subscriber.js 的终端上会输出:

current temperature is 25

好了,我们终于通过 MQTT 协议完成了一次点到点的消息传递,同时我们也验证了,建立持久性会话连接之后,Broker 会保存 Client 的订阅信息。

5.2 取消订阅

Subcriber 也可以取消对某些主题的订阅,取消订阅的流程如下:

  1. Client 向 Broker 发送一个 UNSUBSCRIBE 数据包,其中包含了 Client 想要取消订阅的主题;
  2. Broker 收到 UNSUBSCRIBE 数据包后,向 Client 发送一个 UNSUBACK 数据包作为应答。

接下来我们看数据包的具体内容。

5.2.1 UNSUBSCRIBE
5.2.1.1 可变头(Variable header)

数据包标识(Packet Identifier):两个字节,用来唯一标识一个数据包,数据包标识只需要保证在从 Sender 到 Receiver 的一次消息交互中保持唯一。

5.2.1.2 消息体(Payload)

主题列表(List of Topics):UNSUBSCRIBE 的消息体中包含 Client 想要取消订阅的主题过滤器列表,这些主题过滤器和 SUBSCRIBE 数据包中一样,可以包含通配符。UNSUBSCRIBE 消息体里面不再包含主题过滤器对应的 QoS 了。

5.2.2 UNSUBACK

Broker 收到 UNSUBSCRIBE 之后会回复一个 UNSUBACK 数据包作为应答:

5.2.2.1 可变头(Variable header)

数据包标识(Packet Identifier):两个字节,用来唯一标识一个数据包,数据包标识只需要保证在从 Sender 到 Receiver 的一次消息交互中保持唯一。

5.2.2.2 消息体(Payload)

UNSUBACK 数据包没有消息体。

5.3 代码实践:取消订阅

我们要完成的代码很简单,在建立连接之后取消对之前订阅的主题。

完整的代码 unsubscribe.js 如下:

var mqtt = require('mqtt')
var client = mqtt.connect('mqtt://iot.eclipse.org', {
    clientId: "mqtt_sample_subscriber_id_1",
    clean: false
})
client.on('connect', function (connack) {
    if (connack.returnCode == 0) {
        console.log("unsubscribing")
        client.unsubscribe("home/2ndfloor/201/temperature", function (err) {
            if (err != undefined) {
                console.log("unsubscribe failed")
            } else {
                console.log("unsubscribe succeeded")
            }
            client.end()
        })
    } else {
        console.log(`Connection failed: ${connack.returnCode}`)
    }
})

在终端上运行 node unsubscribe.js,会得到以下输出:

unsubscribing
unsubscribe succeeded

在这里取消了对“home/2ndfloor/201/temperature”的订阅,所以再运行 subscriber.js 和 publisher.js,再运行 subscribe.js 的终端不会再有消息的打印信息了。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值