MQTT协议解析

简介

  MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的 “轻量级”通讯协议,由IBM在1999年发布,它工作在TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议。

  MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

  目前应用比较广泛的是MQTT3.1.1,这个版本包括各种数据传输所需的功能和特征,而且对应生态非常成熟,因此以MQTT 3.1.1为例介绍一下MQTT的协议格式。

mqtt-fidge-2.svg

MQTT协议原理

MQTT协议实现方式

  实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker,服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。

MQTT传输的消息分为:主题(Topic)和负载(payload)两部分:

  • (1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload);
  • (2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。

网络传输与应用消息

MQTT会构建底层网络传输:它将建立客户端到服务器的连接,提供两者之间的一个有序的、无损的、基于字节流的双向传输。

当应用数据通过 MQTT 网络发送时,MQTT 会把与之相关的服务质量(QoS)和主题名(Topic)相关连。

MQTT 连接始终位于一个客户端和 Broker 之间。客户从不直接连接,要启动连接,客户端会向 Broker 发送连接消息。Broker 以 CONNACK 消息和状态代码进行响应。连接建立后,Broker 将其保持打开状态,直到客户发送断开命令或连接中断。MQTT Clients 与 Broker 之间保持TCP长连接。

通过NAT进行MQT连接

在许多常用案例中,MQTT 客户端位于使用网络地址翻译 (NAT) 从专用网络地址(如 192.168.x、10.0.x.x)翻译到面向公共地址的路由器后面。MQTT 客户端通过向 Broker 发送 CONNECT 消息来启动连接。由于 Broker 有一个公共地址,并保持连接开放,在初始连接后,允许双向发送和接收消息。

MQTT客户端

一个使用MQTT协议的应用程序或者设备,它总是建立到服务器的网络连接。客户端可以:

  • (1)发布其他客户端可能会订阅的信息;
  • (2)订阅其它客户端发布的消息;
  • (3)退订或删除应用程序的消息;
  • (4)断开与服务器连接。

MQTT服务器

MQTT服务器以称为”消息代理”(Broker),可以是一个应用程序或一台设备。它是位于消息发布者和订阅者之间,它可以:

(1)接受来自客户的网络连接;
(2)接受客户发布的应用信息;
(3)处理来自客户端的订阅和退订请求;
(4)向订阅的客户转发应用程序消息。

MQTT安全认证

MQTT是基于TCP的,默认情况通讯并不加密。

三种方法:

  • 网络层:通过拉专线或者使用VPN来连接设备与MQTT代理。
  • 传输层:传输层使用TLS加密,可以防止中间人攻击(Man-In-The-Middle Attack)。客户端证书不但可以作为设备的身份凭证,还可以用来验证设备。
  • 应用层:MQTT还提供客户标识(Client Identifier),用户名密码以及X509证书,在应用层验证设备。

MQTT协议中的订阅、主题、会话

一、订阅(Subscription)

订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。

二、会话(Session)

每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。

三、主题名(Topic Name)

连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。

四、主题筛选器(Topic Filter)

一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。

五、负载(Payload)

消息订阅者所具体接收的内容。

MQTT协议中的方法

MQTT协议中定义了一些方法(也被称为动作),来于表示对确定资源所进行操作。这个资源可以代表预先存在的数据或动态生成数据,这取决于服务器的实现。

通常来说,资源指服务器上的文件或输出。主要方法有:

  • (1)Connect。等待与服务器建立连接。
  • (2)Disconnect。等待MQTT客户端完成所做的工作,并与服务器断开TCP/IP会话。
  • (3)Subscribe。等待完成订阅。
  • (4)UnSubscribe。等待服务器取消客户端的一个或多个topics订阅。
  • (5)Publish。MQTT客户端发送消息请求,发送完成后返回应用程序线程。

MQTT协议数据包结构

在MQTT协议中,一个MQTT数据包由:固定头(Fixed header)、可变头(Variable header)、消息体(payload)三部分构成。

MQTT数据包结构如下:

  • (1)固定头(Fixed header)。存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识。
  • (2)可变头(Variable header)。存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容。
  • (3)消息体(Payload)。存在于部分MQTT数据包中,表示客户端收到的具体内容。

MQTT固定头

所有的MQTT报文都包含固定报头:可变报头与有效载荷是部分MQTT报文包含。

固定报头占据两字节的空间:

20210320224829539.png

报文类型

固定报头的第一个字节分为控制报文的类型(4bit),以及控制报文类型的标志位,控制类型共有14种,其中0与15被系统保留出来:

类型 报文流动方向 说明
Reserved 0 禁止 系统保留
CONNECT 1 客户端到服务端 客户端请求连接服务端
CONNACK 2 服务端到客户端 连接报文确认
PUBLISH 3 两个方向都允许 发布消息
PUBACK 4 两个方向都允许 消息发布收到确认(QoS 1)
PUBREC 5 两个方向都允许 发布收到(QoS2 第一阶段)
PUBREL 6 两个方向都允许 发布释放(QoS2第二阶段))
PUBCOMP 7 两个方向都允许 消息发布完成(QoS2第三阶段)
SUBSCRIBE 8 客户端到服务端 客户端订阅请求
SUBACK 9 服务端到客户端 订阅请求报文确认
UNSUBSCRIBE 10 客户端到服务端 客户端取消订阅请求
UNSUBACK 11 服务端到客户端 取消订阅报文确认
PINGREQ 12 客户端到服务端 心跳请求
PINGRESP 13 服务端到客户端 心跳响应
DISCONNECT 14 客户端到服务端 客户端断开连接
Reserved 15 禁止 系统保留

报文类型的标志位

固定报头( Byte 1 )的 bit0-bit3 为标志位,依照报文类型有不同的含义。

在不使用标识位的消息类型中,标识位被作为保留位。如果收到无效的标志时,接收端必须关闭网络连接:

(1)DUP:发布消息的副本。用来在保证消息的可靠传输,如果设置为1,则在下面的变长中增加 MessageId,并且需要回复确认,以保证消息传输完成,但不能用于检测消息重复发送。

(2)QoS:发布消息的服务质量(可靠性控制QoS),即:保证消息传递的次数

00:<=1,至多一次,只发一次,不确保到达。
01:>=1,至少一次,确保消息到达但可能有重复
10:=1,只有一次,确保消息到达且无重复
11:预留,客户端或服务器认为这是一条非法的消息,会关闭当前连接。

(3)RETAIN: 发布保留标识,表示服务器要保留这次推送的信息,如果有新的订阅者出现,就把这消息推送给它,如果设有那么推送至当前订阅者后释放。

  目前Bit[3-0]只在 PUBLISH 协议中使用有效,并且表中指明了是 MQTT 3.1.1 版本,其他报文的标志位均为系统保留。对于其它 MQTT 协议版本,内容可能不同。

  所有固定头标记为”保留”的协议类型,Bit[3-0] 必须保持与表中保持一致,如 SUBSCRIBE 协议,其 Bit 1 必须为 1。如果接收方接收到非法的消息,会强行关闭当前连接。

  PUBLISH 报文的第一字节 bit3 是控制报文的重复分发标志(DUP),bit1-bit2是服务质量等级,bit0PUBLISH 报文的保留标志,用于标识 PUBLISH 是否保留。

  当客户端发送一个PUBLISH 消息到服务器,如果保留标识位置1,那么服务器应该保留这条消息,当一个新的订阅者订阅这个主题的时候,最后保留的主题消息应被发送到新订阅的用户。如果 DUP 字段( bit 3 )设置为1,表明这是一条重复消息,否则是第一次发布消息。为了保证消息的可靠性传递,当 QoS 设置为1时,客户端或服务器发布消息时,需要得到对方的确认(PUBACK),如果一段时间后没收到PUBACK,那么会再次发送当前消息,并将DUP字段标记为1。

报文类型 固定头标记 Bit3 Bit2 Bit1 Bit0
CONNECT 保留 0 0 0 0
CONNACK 保留 0 0 0 0
PUBLISH Used in MQTT 3.1.1 DUP QoS QoS RETAIN
PUBACK 保留 0 0 0 0
PUBREC 保留 0 0 0 0
PUBREL 保留 0 0 1 0
PUBCOMP 保留 0 0 0 0
SUBSCRIBE 保留 0 0 1 0
SUBACK 保留 0 0 0 0
UNSUBACRIBE 保留 0 0 1 0
UNSUBACK 保留 0 0 0 0
PINGREQ 保留 0 0 0 0
PINGRESP 保留 0 0 0 0
DISCONNECT 保留 0 0 0 0

剩余长度(Remaining Length)

剩余长度(Remaining Length)= Variable Header + Payload的长度(如果存在)

剩余长度不包括用于编码剩余长度字段本身的字节数。

剩余长度从 Byte 2 开始,最长可达 4Byte。所以剩余长度范围是 Byte[2-5]

  确定其长度到底是1Byte 还是 4Byte ,这取决于字节的最高位 Bit 7(默认都是高字节在前),如果这个值是1,那么就继续计算字节长度,如果是0,那么就不再计算字节长度。

  剩余长度字段使用一个变长度编码方案,对小于 128(2^7) 的值它使用单字节编码,而对于更大的数值则按下面的方式处理:每个字节的低7位用于编码数据长度,最高位(bit7)用于标识剩余长度字段是否有更多的字节,且按照大端模式进行编码,因此每个字节可以编码128个数值和一个延续位,剩余长度字段最大可拥有4个字节,最大可以表示 128*128*128*128Byte/1024/1024=256MB

注:

  • 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
  • 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

字节对应取值:

  • 当剩余长度使用1个字节存储时,其取值范围为 0(0x00)~127(0x7f)
  • 2个字节,其取值范围为 128(0x80,0x01)~16383(0Xff,0x7f)
  • 3个字节,其取值范围为 16384(0x80,0x80,0x01)~2097151(0xFF,0xFF,0x7F)
  • 4个字节,取值范围为 2097152(0x80,0x80,0x80,0x01)~268435455(0xFF,0xFF,0xFF,0x7F)

总结:MQTT 报文理论上可以发送最大 256M 的报文,但这种情况非常少。

例子

1,消息假设长度是 [0X60],其二进制是 01100000 ,字节最高位 Bit7 (从左边起第0位)是0,所以不需要继续往后计算。那么消息长度就是0X60,十进制数是96。

2,如果消息长度是[0XC1, 0XC2, 0X33],那么他们的二进制分别如下,

1
2
3
0xC1=1100 0001
0xC2=1100 0010
0x33=0011 0011

第一字节最高位是1,那么需要继续向后计算,去掉标记位(0xC1%128),得到100 0001=41
第二字节最高位是1,那么需要继续向后计算,去掉标记位(0xC2%128),得到100 0010=42
第三字节最高位是0,不需要向后计算,其结果就是0x33=51

因为低位在前,高位在后,那么长度计算为 Length=41 + 42*128 + 51*128*128 = 841001B = 821KB

  需要注意的是,消息长度=可变头部长度+消息内容长度。不包括首字节和消息长度本身,如果消息长度为5(占用1字节长度),那么说明这条消息后边还有5字节,整条消息长度为7(首字节+1位长度字节+5)。

  另外如果消息长度为4字节,最后一位不能超过 0X7F=127,因为如果超出这个值,其最高位 Bit7 是1,还需要往后计算,这与消息最大长度为4字节矛盾。所以如果出现 [0XFF, 0XFF, 0XFF, 0XFF] 这样的消息长度,那么接收方认为这是一条非法的消息。

MQTT可变头(Variable header)

  MQTT数据包中包含一个可变头,它驻位于固定的头和负载之间。可变头的内容因数据包类型而不同,较常的应用是作为包的标识。

微信截图_20210401134259.png

  只有某些报文才拥有可变报头,它在固定报头和有效负载之间,可变报头的内容会根据报文类型的不同而有所不同,但可变报头的报文标识符(Packet Identifier)字段存在于在多个类型的报文里,而有一些报文又没有报文标识符字段,具体见表格,报文标识符结构具体见图:

微信截图_20210401134438.png

Payload消息体

Payload消息体位MQTT数据包的第三部分,包含 CONNECTSUBSCRIBESUBACKUNSUBSCRIBEPUBLISH(类型可选)五种类型的消息,除了上面列出的报文类型,其它的报文类型都没有 Payload

  • (1)CONNECT,消息体内容主要是:客户端的 ClientID、订阅的Topic、Message以及用户名和密码。
  • (2)SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS。
  • (3)SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。
  • (4)UNSUBSCRIBE,消息体内容是要订阅的主题。

参考:

MQTT–入门

MQTT数据包格式

MQTT 协议 3.1.1 中文版