引用
MQTT (Message Queue Telemetry Transport),翻译成中文就是,遥测传输协议,其主要提供了订阅/发布两种消息模式,更为简约、轻量,易于使用,特别适合于受限环境(带宽低、网络延迟高、网络通信不稳定)的消息分发,属于物联网(Internet of Thing)的一个标准传输协议。
MQTT协议的设计思想是开放、简单、轻量、易于实现。现在国内很多企业都已经广泛使用 MQTT 作为手机客户端与服务器端推送消息的协议,除此之外,由于其协议的特别针对受限环境(带宽低、网络延迟高、网络通信不稳定),在物联网(Internet of Thing)应用中已经大展拳脚,因为 很多物联网的设备都是一些计算和储存能力受限的嵌入式设备。
那么应该如何学习这个协议呢? 笔者提供了一种深入浅出的方法,通过使用 WireShark 进行网络抓包,结合真实存在网络字节,深入其中,来一场栩栩如生的学习体验。另作者为了照顾没有基础背景的读者,将会安装下面的顺序循序渐进的进行说明和分享。
一、为什么使用MQTT 协议?
MQTT(英语全称,Message Queue Telemetry Transport),中文翻译过来就是遥测传输协议:其主要提供订阅/发布模式,更为简约、轻量,易于使用,针对受限环境(带宽低、网络延迟高、网络通信不稳定),属于物联网(Internet of Thing)的一个传输协议。设计思想是开放、简单、轻量、易于实现。这些特点使它适用于受限环境。例如,但不仅限于此:
- 特别适合于网络代价昂贵,带宽低、不可靠的环境。
- 能在处理器和内存资源有限的嵌入式设备中运行。
- 使用发布/订阅消息模式,提供一对多的消息发布,从而解除应用程序耦合。
- 使用 TCP/IP 提供网络连接。
- 提供Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。
对消息中间件,估计大家不得不关心的就是消息的可靠性,也就是消息的发布服务质量,可喜的是,MQTT支持三种消息发布服务质量(QoS):
- “至多一次”(QoS==0),消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。
- “至少一次”(QoS==1),确保消息到达,但消息重复可能会发生。
- “只有一次”(QoS==2),确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量。
二、支持MQTT协议消息中间件产品
目前有很多的MQTT消息中间件服务器,如下,都是MQTT协议的服务器端的实现。
- IBM Websphere
- MQ Telemetry
- IBM MessageSight
- Mosquitto
- Eclipse Paho
- emqttd Xively
- m2m.io
- webMethods
- Nirvana Messaging
- RabbitMQ
- Apache ActiveMQ
- Apache Apollo
- Moquette
- HiveMQ
- Mosca
- Litmus Automation Loop
- JoramMQ
- ThingMQ
- VerneMQ
因为Mosquitto 是当前用户最多的一款产品,所以本文将选择以Mosquitto为MQTT的服务器来和大家分享MQTT协议。大家可以到此网站地址下载。 其支持几乎市面上所有的操作系统,而且更为神奇的是,其还提供了javascript版的Mosquitto服务器,简直要逆天的节奏,笔者是在Windows机器上安装Mosquitto的,所以下载一个Windows的二进制的安装包安装即可,很简单,作者就不再啰嗦了。
三、 什么是MQTT 协议?
我们知道MQTT是一个物联网协议的一个规范,MQTT的协议最新的三个版本是:3.1.1,3.1.0和5.0的协议。
其中5.0还只是一个提案(2017年7月13日发布的一个草稿版的提案),所以本篇文章暂时不涉及MQTT 5.0的协议,考虑到目前主流和成熟的MQTT协议的应用都是MQTT 3.1.1的协议,所以笔者还是以MQTT 3.1.1的协议为基准,给大家分享,只要这个会了,其他的自然不在话下。需要注意的是,MQTT 3.1.0和3.1.1 规范,还是有小部分的区别的。比如可变头部中,在3.1.0的规范中,其关键字是,“MQISdP”,
表 1 MQTT V3.1规范可变消息头的前8个字节
而在MQTT V3.1.1的规范中,其可变消息的头的协议名称的关键字是“MQTT”.如下图所示意。
表2 MQTT V3.1.1可变消息头的前6个字节
MQTT的协议说明文档主要描述了MQTT消息的通用格式,消息的命令,消息流动等等的规范和原则。
在前面的3个章节,我们对MQTT已经有了一个大概的感性认识,在接下来的系列文章,我将带领大家从MQTT的14种消息类型中,挑出2种比较经典的消息格式,不管是发布还是订阅都要用到,那就是MQTT连接的请求和相应的消息类型。 并通过抓包软件(WireShark)结合MQTT 3.1.1 定义的具体规范和大家走一遍。
注意,因为我用的Eclipse Paho API的Java库,支持MQTT 3.1.1 的版本,且mosquitto也支持3.1.1的版本,所以本系列所有的抓包都会是基于MQTT 3.1.1的规范,而不是MQTT 3.1的规范,这点敬请大家注意。
下面的表格列出了MQTT的14中不同的消息类型,其中0,15都属于保留字段,不算消息类型,所以总共有16-2=14 种消息类型。
表3 MQTT的14种消息类型(Command message)
四、如何抓取MQTT 协议包?
在进行协议分析之前,我们先考虑一下下面2个问题:
- 如何产生MQTT的数据包?
- 如何抓取MQTT的包?
首先看如何产生MQTT的数据包,因为MQTT协议被几乎编程界所有的主流语言支持,为了照顾所有的读者,笔者就不选任何一门语言来做示范,而是用一款工具软件来进行MQTT数据包的发送和订阅。服务器端选择当前用户最多的服务器,Mosquitto; 客户端选择Eclipse Paho 提供的MQTT的UI客户端,这个客户端工具是一个基于Java的Eclipse桌面客户端程序,其底层的和MQTT服务器进行的交互的java类库就是Eclipse Paho java库。假设我们在本机(127.0.0.1)已经启动了一个mosquitto MQTT服务器,其端口为1883。如何使用 Eclipse Paho MQTT工具?
1、下载Eclipse Paho MQTT 工具
图1:Eclipse Paho下载地址
2、解压缩后,双击paho.exe,打开后的对界面如下。
图2:Eclipse Paho UI开始界面
3、点击上图中的 十字图标,就能新建一个MQTT的客户端的连接,输入正确的MQTT服务端的连接地址,比如,本例中的连接地址是tcp://localhost:1883,然后点击“Connect”按钮,这个时候,如果MQTT服务端没有设置密码(默认情况是没有密码的)的话,这个时候,我们就能看到连接得到状态(status)是“Connected”。
图3 Eclipse Paho UI开始界面
4、这个时候我们就能订阅消息了。选择“Subscription”下方的绿色十字图标,就可以输入订阅的主题(topic)的名字,比如我们设置主题名称为“test”,并点击 “Subscribe”按钮。
图4 Eclipse Paho UI订阅界面
5、往MQTT服务发送一条消息主题为“test”,内容为“大家好,这是我一条消息。”的MQTT消息。然后点击“Publish”按钮,这个时候,我们就能看到消息已经发送成功,且在步骤(4)订阅的同一主题也收到了消息。
图5 Eclipse Paho UI消息显示界面
神奇吧,恭喜你,已经基本学会了如何用Eclipse Paho MQTT工具作为客户端来发送和接收消息了。
MQTT消息的模拟,已经用一种最简单的方法来模拟了,那么应该用什么工具来抓包呢? 当然是WireShark了,在Wireshark中,有capture filter和Display Filer,我们只需要在WireShark软件中的capture filter 输入下面的过滤条件,则与MQTT服务交互的相关TCP的数据包就能顺利抓取到。如下图所示意,注意,下面的1883端口是MQTT服务器的默认推荐端口,如果读者改变这个默认端口,请把1883端口改成相应的端口。
图6 WireShark转包配置界面
五、如何分析MQTT协议包?
万事具备了,环境准备好了,抓包工具也准备好了,是时候通过WireShark抓取到的TCP包来分析MQTT协议了,考虑到MQTT协议有多个版本,本文采用的MQTT的协议是MQTT 3.1.1的版本,如果不做特别的说明,服务器的地址应该是192.168.80.196,用的是Mosquitto MQTT服务器。
另外,因为牵涉到最基础的计算机知识,二进制,16进制以及10进制的转换,笔者想只要是参加Gitchat的读者朋友都是对技术有追求的朋友,笔者在这就不在赘述,默认大家都会啊,如果有不会的同学,请单独与我联系啊。
5.1 CONNECT 消息类型分析
假设有这么一个场景,MQTT的客户端需要连接MQTT的服务器端,这个时候,TCP的三次握手协议完成后,MQTT的客户端就会马上发送一条MQTT CONNECT消息帧。
咱们还是以Phao UI工具为例子,当我们点击“Connect”按钮的时候,究竟在TCP协议层发生了什么?
其实MQTT的CONNECT消息主要用来在客户端和服务端建立一个TCP的通信连接,这个CONNECT的发起者肯定是客户端。因为要建立一个连接,所以客户端在这个消息里面需要提供Client ID, 如果服务端设置了用户名和密码认证,在这个消息里面还必须要包括用户名和密码的相关信息,且如果设置了连接断开的最后遗言(Last Will and Testament),则在这个消息里面还必须包含和最后遗言相关的信息。下面是实验的具体步骤:
首先,在点击Eclipse Phao UI客户端工具上的“Connect”按钮之前,先在客户端把WireShark软件打开,并按照上一个章节(第4章节)的最后一部分关于WireShark的Capture Filter的设置把协议和端口设置好(TCP and port 1883)。
然后,打开Eclipse Paho的UI客户端,在本实例中,用于连接MQTT服务器的相关配置信息如下图7所示意(为了显示方便,笔者把Eclipse Paho对MQTT 连接的配置信息的不同配置页面整合到了一个图里面了,就是图7),请大家记住下面用于建立MQTT连接的相关信息,然后点击“Connect ”按钮。
图7 Eclipse Paho UI的连接信息配置页面
这个时候WireShark就能抓取到类似于下面的TCP数据包。具体的包如下:
图8 Eclipse Paho UI的WireShark抓包数据
从上面的抓包可知,TCP的三次握手连接之后,MQTT的客户端会发送第一条MQTT的CONNECT消息格式的数据给MQTT服务器。 其具体数据,见上图左下角,10 4e 00 4d 51 54 54 04 ee 00 3c 00 1e 31 31 31 ..........
根据MQTT v3.1.1的协议,首先咱们来看头部信息。
5.1.1 CONNECT 固定头部
WireShark抓包抓取的固定头部(Fix head)的16进制的数据为:10 4e, 那10 4e这两个字节代表什么意思呢?
首先来看一下,MQTT 3.1.1的协议对CONNECT 消息固定头部的2个字节的规范和定义。
表4 CONNECT 报文的固定头部(Fixed Header)格式
从这张图片可以知道,MQTT的Connect消息格式中,有一个固定的头部,其是由2个二进制的8位(bit)字节来表示的。
10---表示发送的Connect的请求(也就是二进制的0001 0000, 详细信息,请参考 表3 MQTT的14种消息类型(Command message) )** ,前4个的二进制位是0001,我们可以知道其标识的是一个MQTT CONNECT 消息类型,后面的4个bit是保留位,默认为4个二进制位的0000),MQTT CONNECT的消息格式的第一个字节分析完成。
4e---表示后面将跟着4*16+14=78个字节,也就这个MQTT的消息帧,后面部分还有78个字节,包括10个字节的可变头部(规定是10个字节)和 68个字节的负载(payload)。具体什么是可变头部和什么是消息负载,请继续看下面的分析。
5.1.2 CONNECT 可变头部(10个字节)
接下来的10个字节是:00 04 4d 51 54 54 04 ee 00 3e, 这10个字节,根据MQTT的3.1.1的规范,是10个字节的可变头部,其MQTT的CONNECT的可变头部主要由四大部分按顺序组成:
- 协议名称(Protocol Name,)
- 协议级别(Protocol Level)
- 连接标志(Connect Flags)
- 保持心跳(Keep Alive)
下面笔者就把上面通过WireShark抓包抓到的10个字节填充到MQTT的3.1.1x协议可变头部的模板表格中,可变头部的规范马上就一目了然了。
表5 固定头部格式重放
下面对这4部分做进一步的详细分析,首先看协议名称。
5.1.2.1 协议名称
协议名称由6个字节组成,16进制数据为:00 04 4d 51 54 54,其代表的是MQTT协议。对于一些第三方的网络监测软件,可以通过分析TCP的包,从而知道这是MQTT协议。
接下来看协议级别。
5.1.2.2 协议级别
协议级别只有1个字节,我们这个实验中抓取到的16进制数据为:04,其实代表的就是协议的修订级别(revision level),说白了,就是修订版本,04 代表的是MQTT 3.1.1的修订协议;而03 代表的是MQTT 3.1.0的修订协议,下面来看连接标志。
5.1.2.3 连接标志
对于连接标志这一部分,还是挺有意思的,其对应了一个字节。并用字节中不同位(bit)是true(二进制 1)或者false(二进制 0)来表示不同的业务意义, 其本质就是定义了7种类型的连接标志位(包括最后一位的保留标志位)。
那为什么是7种呢?8个位应该是8种啊?原来其中一种定义的是QoS(质量服务标准)占据了2个位(bit),其有三种情况(具体参考第一章节的MQTT支持三种消息发布服务质量(QoS))
表6 连接标志位数据重放
上面这张表也许看的还不是特别清楚,如果把上面的表转个维度和方向查看,其对应的意义就一目了然了。
表7 连接标志位数据格式换角度重放
其中我们可以看到,因为在Eclipse Paho UI 工具中,我们选择了认证且输入了用户名和密码,所以User Name和Password的标志位都置成了1。
下面来看最后的遗愿(Last Will and Testament),在Eclipse Paho的连接配置上,对应有2个设置(QoS,Retained),如下图9。
图9 Eclipse Paho UI的连接配置设置中的最后遗愿(Will)配置
Will Retained 因为在Eclipse Paho UI工具上勾选了,所以其连接的标准为为1。
Will QoS的值为01,刚好和Eclipse Phao的UI工具上的设置(1-At Least Once)对应了起来。
对于Clean Session的设置,请见上图7 Eclipse Paho UI的连接信息配置页面Clean Session的单选框被勾上了,所以Clean Session对应的标志位(bit)为1。
最后剩下的保留位(bit),保留位(bit)默认值为0。
5.1.2.4 保持心跳间隔
根据MQTT协议,每隔固定的时间,如果没有业务相关的消息发送的话,MQTT的客户端必须在1.5倍的Keep Alive的时间内,向MQTT的服务器发送一个PINGREQ的消息帧,以表示我们的客户端还是在线的,还活着。否则服务器则任务网络发生了故障,将会强制断掉于客户端的连接。
表8 保持在线配置协议格式定义
其中:
00 Keep alive的时间的高位(MSB):0。
3c 代表Keep alive时间的低位(LSB): 3*16+12=60秒,刚好和图7 Eclipse Paho UI的连接信息配置页面 中Keep Alive的数字输入框对应起来。10个字节的可变消息头分析完了,下面来看消息负载(Payload),但是奇怪的是消息负载没有和可变消息头直接连起来,而是中间隔了2个字节,00 1e,这就是传说中的分隔标志字节,请看下面的分析。
5.1.3 分隔标志字节
实际上,在MQTT的协议里面也没有找到对其的描述,其实就是一个分割标志字节,在可变消息头和消息负载(payload)之间,用00 1e进行分割;后面还能遇到00 3c; 00 05 都是分隔标志符号,用于分隔不同的数据类型;好了,下面去看望一下消息负载吧(Payload)。
5.1.4 消息负载(Payload)
前面的固定消息头,可变消息头,以及分隔标志字节已经分析完了,下面正式进入消息的负载(Payload)的字节分析。
本实验中的消息负载包括了MQTT的客户端的ID,遗愿的主题(Will Topic),遗愿的消息内容(Will Topic)以及为了通过MQTT服务器的Basic认证而要提供的用户名和密码。下面逐一进行分析。
5.1.4.1 客户端ID
首先看客户端的ID,16进制为: 31 31 31 31 31 31 31 31 31 31 31 31 31 31 3131 31 31 31 31 31 31 31 31 31 31 31 31 31 31,如果翻译成ASCII的表示,其就是(111111111111111111111111111111),其对应的就是,图7 中的Client ID的输入,不好意思,有点调皮了,输入了这么多个1 作为MQTT的Client ID,其实笔者就是想看看Eclipse Paho这个Client ID字段最多能让我输入多个字符,好奇害死猫啊。
接下来,看看最后的遗愿(Last Will and Testament)在消息负载中放入什么东东。
5.1.4.2 遗愿(Will)主题
最后的遗愿(Last Will and Testament)的主题中放入的还是图7中输入Topic。
Will Topic: 16进制数据为:6c 77 74, 翻译成人能懂的ASCII码就是: lwt。
后面紧跟的00 0c,是遗愿主题和遗愿消息之间的分隔标志符,具体说明请参看5.1.3.
5.1.4.3 遗愿(Will)消息体
后面的16进制数: 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 代表的就是人能懂得ASCII码Hello World!。 这个就是最后的遗愿(Last Will and Testament)的消息体(Message)
后面紧跟的00 05是遗愿(Topic,Message) 和认证的用户名密码之间的分隔标志符号。
5.1.4.4 连接MQTT服务端的用户名和密码
下面看用户名和密码:
16进制: 61 64 6d 69 6e 代表的ASCII码就是用户名:admin
16进制: 70 6173 73 77 6f 72 64 代表的ASCII码就是用户名 :password
中间用 00 08标志分隔符分开了。
恭喜你,整个的Connect的消息格式就分析完毕了。安奈不住激动心情的你,是不是想跃跃欲试了! 通过对抓取的包和MQTT 3.1.1协议标准对比,我们非常容易掌握了其MQTT CONNECT消息类型背后的规范和定义,这个感觉是不是非常的好啊。
5.2 CONNACK 消息类型分析
在上节中我们分析了MQTT CONNECT消息类型的格式,我们知道CONNECT消息是客户端发送出去的,作为对客户端的连接请求,服务器端同样会有一个消息的返回,这个消息就是MQTT CONNACK的消息类型。我们在发出去CONNECT消息后,如果WireShark抓包工具依然开启的话,将会抓到类似于下面的TCP消息,其16进制为:20 02 00 00,那么其代表什么意思呢?如下图10所示意。
图10 服务器连接应答抓包
5.2.1 连接应答固定消息头
在分析之前,我们先回到MQTT3.1.1的协议中对客户端连接应答(CONNACK)消息帧的定义和描述。
表9 连接应答的固定消息头
其和表4的连接消息帧(CONNECT)固定消息头定义是一样的,只不过,对于第1个字节,10表示的是Connect的消息帧类型;而20表示的是连接应答(CONNACK)的消息帧类型。对于第2个字节,02 表示后面将跟着2个字节. 其意义和前一节的连接消息帧(CONNECT)定义的规范是类似的,表示后面还跟有多少个字节(剩下多少个字节,既包括消息头也包括消息的负载)。
02 表示后面将跟着2个字节。
5.2.2 连接应答的可变消息头
5.2.1章节提到了,连接应答的固定消息头的第2个字节是02,表示后面还跟着2个字节,这两个字节就是连接应答的可变消息头。
表10 连接应答的可变消息头
面我们来分析其可变头的2个字节: 00 00
- 其中第一个00,目前没有什么特别的含义,是MQTT协议的保留字段,也许在将来的协议版本中会用到。
- 第二个字节的00 表示连接成功。MQTT协议对于返回的结果总共定义了6种类型(请见下面的表3): 连接接受,连接拒绝(不可接受的版本),连接拒绝(Client ID服务器不允许),连接拒绝(服务器不可达),连接拒绝(错误的用户名和密码),连接拒绝(客户端没有通过授权认证)。具体含义请见下表11。
表11 连接应答返回值状态速查表
这个消息格式简单吧,嘿嘿,恭喜读者在学习MQTT消息格式的旅途中又往前快速前进了一步。
剩下的12种MQTT消息类型(请见,表3 MQTT的14种消息类型(Command message)),分析过程其实也是类似的,笔者就不在啰嗦赘述了,大家只要把抓到的协议包然后对照MQTT 3.1.1协议规范做一个对比,就能非常容易的把MQTT的底层协议分析出来。
六、总结
在这篇Chat中,笔者首先分享了为什么使用MQTT协议,然后列出了当前那些开源产品和框架支持MQTT协议,紧接着为了照顾一些没有接触过MQTT的读者,笔者对MQTT协议做了一个扫盲;工欲善其事必先利其器,在给大家做了理论铺垫之后,笔者又给大家分享了如何不用写代码,也能模拟MQTT消息的发送,且如何通过WireShark来抓取MQTT的TCP的协议帧;接下的章节,笔者详细的给大家分享了MQTT连接请求和连接应答包两种MQTT消息类型的格式,具体是如何定义的,也分析了每个字节对应的含义;希望能够给读者抛砖引玉石,举一反三,这样大家就会在以后的工作和学习过程中,对MQTT协议的本质有一个非常深入的理解。
最后,提一下,如果读者对MQTT的协议的代码的实现感兴趣的话,可以参考Eclipse Paho MQTT jar包实现。代码写的非常的好,如果读者看过了这篇文章的话,我相信,读者肯定就会非常容易看懂其代码的实现原理,对于研究开源软件的实现也是一个好的例子,而且其代码的实现,写的非常的简洁,代码量也不太,特别适合初学者和进阶者学习和研究。
谢谢大家把文章读完,如果大家感觉需要进一步补充什么,请进一步与笔者联系,希望这篇文章能给大家带来一点启发和收获。