目录
第一节 本文探讨的内容
第二节 环境搭建
第三节 MQTT控制报文格式
第四节 CONNEC报文
第五节 订阅和取消订阅
第六节 接收消息和发布消息
第七节 网络调试助手直连阿里云极速体验
第一节 本文探讨的内容
本文讨论MQTT协议底层数据收发的过程,直接通过网络调试助手与阿里云连接进行实验验证。本文不讲述阿里云的基础知识与创建阿里云设备,建议先初步学习MQTT协议API函数的使用和阿里云的简单操作再看本文。入门教程可参考正点原子推出的“STM32F1 lwIP开发手册.docx”的第12章。在本文第七节,我书写了一个网络调试助手直连阿里云的极速体验,想快速体验的小伙伴,可以先看一下第七节哇!
准备阿里云已注册账号及设备,得到如图2.1所示的设备证书第二节 环境搭建
图2.1 设备证书
Assist网络调试助手 V5.0,下载地址如下,软件启动界面如图2.2所示。
下载地址:/resource/102.html
图2.2 网络调试助手启动界面
3.我们现有以下信息(获取方式参考“STM32F1 lwIP开发手册.docx”的第12章):
服务器地址:a1Dm4VEK8BR.iot-as--
端口:1883
客户端ID:aliyun_dev|securemode=3,signmethod=hmacsha1|
用户名:aliyun_dev&a1Dm4VEK8BR
计算登录密码:clientIdaliyun_devdeviceNamealiyun_devproductKeya1Dm4VEK8BR
Password::8bdc84f9963105d65bdc79b6cb3ac0cec9ce1aa9
("Password "为hmacsha1加密后的密码,加密网站为: 获取方式如图2.3所示。)
注意!由于你注册的阿里云和我的设备证书是不一样的,故得到的信息也不一样!
图2.3 网站上使用hmacsha1加密得到Password
第三节 MQTT控制报文格式
为了进一步理解MQTT底层的原理,我们要先了解MTQQ控制报文的格式。
MQTT控制报文由固定报头(Fixed header)、可变报头(Variable header)、有效载荷(Payload)三部分组成。
首先讲解MQTT固定报文,该报文是所有控制报文都包含的,固定报文至少2字节(byte1一定要有,剩余长度至少要一个字节表示),最多为5字节(因为剩余长度最多4字节),其格式如表3.1所示,MQTT控制报文类型如图3.2所示,控制报文标志如图3.3所示。根据下面的描述,我们可以得知不同类型报文的固定报头内容。
表3.1 固定报文格式
图3.2 MQTT控制报文类型
图3.3 MQTT控制报文标志
报文的第二部分是可变报头,MQTT 控制报文包含一个可变报头部分。它在固定报头和可变负载之间。可变报头的内容根据报文类型的不同而不同,此处我们在具体的报文中作讲解。
报文的最后一个部分是可变负载,通俗来讲,这部分就是应用消息,因此也是具体的报文再作讲解。
下面简介MQTT底层中需要了解的知识:
1.UTF-8编码字符串
MQTT的报文的格式是UTF-8编码的,因此需要了解该编码的格式才能进行MQTT通讯。
UTF-8编码格式的要求为:每一个字符串都有一个两字节的长度字段作为前缀,它给出这个字符串 UTF-8 编码的字节数。因此,可以传送的 UTF-8 编码的字符串大小有一个限制,不能超过65535 字节。
例如字符串“123456”的UTF-8编码,可以将“123456”转换成ASCII码表示:31 32 33 34 35 36(十六进制),字节数大小为:6,因此需要添加前缀 0x00,0x06,综合原字符串ASCII码可得到该字符串最终的UTF-8编码为00 06 31 32 33 34 35 36(十六进制)。
2.剩余长度
剩余长度(Remaining Length)表示当前报文剩余部分的字节数,包括可变报头和负载的数据。剩余长度不包括用于编码剩余长度字段本身的字节数。
剩余长度字段使用一个变长度编码方案,对小于 128 的值它使用单字节编码。更大的值按下面的方式处理。低 7 位有效位用于编码数据,最高有效位用于指示是否有更多的字节。因此每个字节可以编码 128 个数值和一个 延续位( continuation bit ) 。剩余长度字段最大 4 个字节。MQTT中剩余长度为低字节在前,高字节在后。例如(x为剩余长度,所有数都为十六进制) x 01 02 03 04 05 06,则剩余长度为后面的字节数,由于x<128,则x=06,再例如 x 01 02 03 …(等等) c8,则剩余长度为后面字节数,但c8换算成十进制为200>128,则x=01 c8,但注意,在MQTT中剩余长度为低字节在前,高字节在后,x应该为 c8 01。
3.服务质量QOS
MQTT 按照这里定义的服务质量 (QoS) 等级分发应用消息。QOS等级分为三个等级。
QoS 0: 最多分发一次,消息的分发依赖于底层网络的能力。接收者不会发送响应,发送者也不会重试。
QoS 1: 至少分发一次,服务质量确保消息至少送达一次。
QoS 2: 仅仅分发一次,这是最高等级的服务质量,消息丢失和重复都是不可接受的。使用这个服务质量等级会有额外的开销。
本文是为了理解MQTT低层的工作原理,规定本文的所有QOS等级都为0。
4.JSON创建对象格式
JSON 对象使用在大括号中书写,对象可以包含多个 key/value(键/值)对两个"键:值"对以逗号分隔,例如,定义名为people对象:
"people":{"ID": 1001,"name": "张三","age": 24}
或
var people={"ID": 1001,"name": "张三","age": 24}
key必须是字符串,value可以是合法的JSON数据类型(字符串,数字,对象,数组,布尔值或者null);key和value中适用冒号(:)分割;每个键值对适用逗号(,)分割。
JSON 是一种纯数据格式,它只包含属性,没有方法。
6.字符串与ASCII码之间的转换和获取字符长度的简易方法
字符和ASCII的关系如图3.4所示,例如要知道字符’1’的ASCII码值可查找字元‘1’,然后对应行的左边的值就是ASCII码值,查找图2.4可知:字符‘1‘的ASCII码值为0x31或49(十进制)。字符串的ASCII码值是所有字符的ASCII码值组合,例如字符串“123456”的ASCII值为0x31 0x32 0x33 0x34 0x35 0x36。相反操作可以获取对应ASCII码值对应的字符,例如ASCII码值为0x31,表示的字符为‘1’。
但是遇到字符串比较长的情况,查表找字符串的值较为繁琐。可通过网络调试助手进行转换,如图3.5所示,我们使用了127回播地址进行数值的返回,接收区设置为HEX(意为16进制显示),发送区选择ASCII发送,我们发送字符串后,将会返回对应的ASCII码值。如图3.6所示将发送区设置为HEX(意为16进制显示)发送,接收区设置为ASCII,便可将ASCII码值转换为字符串。值得注意的是网络调试助手的右下角还会显示发送(TX)与接收(RX)的字节数,右下角的复位计数可以重新计算发送、接收区的字符总数。
图3.4 字符和ASCII码值的关系
图3.5 利用网络调试助手获取字符串的ASCII码值
图3.5 利用网络调试助手获取ASCII码值对应的字符串
第四节 CONNEC报文
客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是 CONNECT报文。服务端收到CONNECT报文后,将发送 CONNACK 报文响应从客户端收到的 CONNECT 报文。服务端发送给客户端的第一个报文也必须是CONNACK报文。下面讲解CONNECT报文的内容。
CONNEC固定报头可由第三节得出,为0x10+(剩余长度)。
CONNEC可变报头由协议名、协议级别、连接标志、保持连接四部分组成。协议名为字符串“MQTT”,由于规定为6字节,故其UTF-8编码格式的表示为:00 04 4D 51 54 54(十六进制)。协议级别为4,即0x04。连接标志由1个字节组成,其含义表4.1所示,我们需要验证User Name、Password,并且Clean Session,这样服务器每次session 都要重新建立,这也是大多数的场景使用情况,因此连接标志配置为0xC2。保持连接由两个字节组成,表示服务器和客户端保持连接的时间,此处设置为100s,也就是0x0064。综上所述,我们配置的CONNEC可变报文为00 04 4D 51 54 54 04 C2 00 64(十六进制)。
CONNEC报文的可变负载由三个字符串组成,分别为客户端ID、用户名、Password组成。此处注意字符串需要使用UTF-8格式的。
综合上面CONNEC报文的三大部分,我们计算出剩余长度为0x7A,可以最终得到CONNEC报文发送的内容为:10 7A 00 04 4D 51 54 54 04 C2 00 64 00 2c 63 6F 61 70 5F 74 65 78 74 31 7C 73 65 63 75 72 65 6D 6F 64 65 3D 33 2C 73 69 67 6E 6D 65 74 68 6F 64 3D 68 6D 61 63 73 68 61 31 7C 00 16 63 6F 61 70 5F 74 65 78 74 31 26 61 31 31 36 6E 37 4D 77 53 78 67 00 28 61 32 38 31 62 38 38 38 63 66 66 64 30 37 63 35 34 30 32 31 36 31 37 64 30 39 63 37 63 65 37 61 35 34 37 33 65 39 38 64(十六进制,不同设备的CONNEC报文负载是不同的)。
我们实验验证上诉理论的可行性,如图4.2所示,接收和发送设置都设置为HEX,将服务器地址和端口输入进去。然后在发送区发送CONNEC报文,此时阿里云端显示已经连接。并且服务区返回数据:20 02 00 00(十六进制)。这串数据为CONNACK – 确认连接请求报文,其固定报头为0x20、0x02,其中0x02表示剩余长度。后面两个数据0x00、0x00为可变报文,CONNACK报文无可变负载报文。第三个数据0x00为确定连接标志,这个在MQTT中固定为0x00,第四个数据含义如表4.3所示,我们接收到的第四个数据为0x00就表示连接已接受了。
表4.1 连接标志含义
图4.2 实验验证
表4.3 连接返回码的值
第五节 订阅和取消订阅
首先讲解一下如何订阅主题,客户端向服务端发送 SUBSCRIBE 报文用于创建订阅,每个订阅注册客户端关心的主题。然后服务器将会返回一个SUBACK 报文,用于确认它已收到并且正在处理 SUBSCRIBE 报文。
SUBSCRIBE固定报头可由第三节得出,为0x82+(剩余长度)。
SUBSCRIBE可变报头是客户端标识符,此处我们使用的标识符号为0x00 0x0a。
SUBSCRIBE报文的可变负载由主题过滤器(UTF-8字符串)和服务质量等级组成,我们的服务质量等级为0。主题过滤器其实就是Toic列表中的主题,我选取我阿里云上的主题“/a1Dm4VEK8BR/aliyun_dev/user/LED”。综合可得SUBSCRIBE报文的可变负载的可变负载内容为00 20 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 75 73 65 72 2F 4C 45 44 00(十六进制)
综上,我们的SUBSCRIBE 报文的内容为82 25 00 0a 00 20 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 75 73 65 72 2F 4C 45 44 00(十六进制)。如图4.1,我们连接阿里云服务器后,再发送SUBSCRIBE 报文的内容。
发送上面的消息后,我们将会收到订阅确认报文—SUBACK报文。该报文的固定报文为0x90 +可变长度(大小一般固定为03)。可变报文为客户端发送的标识符。有效负载为一个字节,内容和含义为:0x00 - 最大 QoS 0;0x01 - 成功 – 最大 QoS 1;0x02 - 成功 – 最大 QoS 2;0x80 - Failure 失败。如图4.1,我们的返回值为90 03 00 0A 01,订阅成功!这里值得注意的是,我们虽然设置了QoS为0,但是SUBACK报文的有效负载为01,这并不是服务器出问题,而是我们成功后,等级应该会默认设置为最大QoS 1。
以上是订阅主题的操作,那么如果我们要取消订阅,就要使用UNSUBSCRIBE 报文,其格式和SUBSCRIBE 报文类似,当我们发送了UNSUBSCRIBE 报文后,服务器也会返回UNSUBACK 报文给客户端用于确认收到 UNSUBSCRIBE 报文。
下面讲解一下UNSUBSCRIBE 报文,该报文的固定报头由第三节得出,为0x a2+(剩余长度)。可变报头为SUBSCRIBE可变报头的客户端标识符,我的UNSUBSCRIBE可变报头为0x00 0x0a。UNSUBSCRIBE 报文的有效载荷由要取消订阅的主题和QOS等级组成,当QOS等级=0时,可以省略QOS等级,我们要取消订阅的主题为“/a1Dm4VEK8BR/aliyun_dev/user/LED”。综合本段的内容,我们可以得出完整的UNSUBSCRIBE 报文为:a2 24 00 0a 00 20 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 75 73 65 72 2F 4C 45 44(十六进制)
我们连接阿里云服务器,发送上述的UNSUBSCRIBE 报文,如图4.1,服务端返回了 UNSUBACK 报文给客户端用于确认收到 UNSUBSCRIBE 报文,从图中可见到返回值为0xB0,0x02,0x00,0x0a。如果掌握了前述报文的内容,这个报文并不难分析。数据0xB0,0x02为固定报头,数据0x00,0x0a为可变报头,内容与SUBSCRIBE可变报头的客户端标识符一样,UNSUBACK 报文无有效负载部分。如图5.1所示,我们成功取消了订阅。
图5.1 订阅和取消订阅
第六节 接收消息和发布消息
经过前面几节的学习,我们已经初步了解MQTT底层的工作原理。本节先讲解设备如何接收阿里云的消息再讲解如何发布消息。我们在网络调试助手连接阿里云如图6.1所示,然后按照图示的六个步骤发送消息给网络助手。我们在网络调试助手的返回信息分析,可知固定报头为30 ,剩余长度为0x9b,此处不做累述,固定报头的格式可参考第一节。可变报头中,将00 36 2F 73 79 73 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 74 68 69 6E 67 2F 73 65 72 76 69 63 65 2F 70 72 6F 70 65 72 74 79 2F 73 65 74(十六进制,UTF-8编码)翻译后的意思为“/sys/a1Dm4VEK8BR/aliyun_dev/thing/service/property/set”,可以判断可变报头的内容为Topic名称。剩下的数据即为有效载荷:7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 2E 73 65 72 76 69 63 65 2E 70 72 6F 70 65 72 74 79 2E 73 65 74 22 2C 22 69 64 22 3A 22 37 35 31 34 31 33 35 37 30 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 4C 45 44 53 77 69 74 63 68 22 3A 30 7D 2C 22 76 65 72 73 69 6F 6E 22 3A 22 31 2E 30 2E 30 22 7D(十六进制,ASCII编码)。翻译后的意思如下:
“{"method":"thing.service.property.set","id":"751413570","params":{"LEDSwitch":0},"version":"1.0.0"}”这个明显为JSON表达式,JSON格式参考第三节“MQTT底层中需要了解的知识”。
综上所述,我们可以MQTT中发布消息的格式为:
①固定报头:0x30+剩余长度
②可变报头:Topic名称(UTF-8编码)
③报文负载:应用消息(JSON格式,ASCII编码)
下面我们按照上面的格式也尝试发消息给阿里云,我们先创建好了阿里云的设备LEDSwitch,并且打开自定义功能并发布上线(此处可参考正点原子推出的“STM32F1 lwIP开发手册.docx”的第十二章),我们发布消息给物理模型,需要订阅主题“/sys/(ProductKey)/ (DeviceName)/thing/event/property/set”(”ProductKey”和“DeviceName”根据自己设备填写,我最终订阅的主题内容为“/sys/a1Dm4VEK8BR/aliyun_dev/thing/event/property/set”,订阅方法参考本文第五节)。然后在发送消息中,我们写入如下内容:
①固定报头0x30+剩余长度
②可变报头:“/sys/a1Dm4VEK8BR/aliyun_dev/thing/event/property/post” (UTF-8编码)
③报文负载如下:
{"method":"thing.event.property.post","id":"00000001","params":{"LEDSwitch":0},"version":"1.0.0"}。注意此处method中的内容不同于阿里云服务器发送消息的method内容,ID我们填写00000001,内容是使LED的状态为0(也可以设置为1),版本号填写“1.0.0”便可。
综上3点我们最终的报文为:30 98 01 00 35 2F 73 79 73 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 74 68 69 6E 67 2F 65 76 65 6E 74 2F 70 72 6F 70 65 72 74 79 2F 70 6F 73 74 7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 2E 65 76 65 6E 74 2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C 22 69 64 22 3A 22 30 30 30 30 30 30 30 31 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 4C 45 44 53 77 69 74 63 68 22 3A 30 7D 2C 22 76 65 72 73 69 6F 6E 22 3A 22 31 2E 30 2E 30 22 7D
连接阿里云后,我们发送上述最终报文,结果如图6.2所示,我们成功使LEDSwitch的状态变为0。
图6.1 网络调试助手接收阿里云的消息
图6.2 网络调试助手向阿里云发送消息
第七节 网络调试助手直连阿里云极速体验
第一步,如图7.1打开网络调试助手 V5.0,选择协议类型为“TCP Client”,接收区设置为HEX,发送区设置为HEX。将远程主机地址填写为“a1Dm4VEK8BR.iot-as--”,端口填写“1883”。在点击连接前先将数据“10 7A 00 04 4D 51 54 54 04 C2 00 64 00 2c 61 6C 69 79 75 6E 5F 64 65 76 7C 73 65 63 75 72 65 6D 6F 64 65 3D 33 2C 73 69 67 6E 6D 65 74 68 6F 64 3D 68 6D 61 63 73 68 61 31 7C 00 16 61 6C 69 79 75 6E 5F 64 65 76 26 61 31 44 6D 34 56 45 4B 38 42 52 00 28 38 62 64 63 38 34 66 39 39 36 33 31 30 35 64 36 35 62 64 63 37 39 62 36 63 62 33 61 63 30 63 65 63 39 63 65 31 61 61 39”复制入数据发送框!然后点击连接,再点击发送。云端返回“20 02 00 00”!表示阿里云连接成功了!注意!100秒内将第二步的数据发给阿里云,否则阿里云将会自动断开连接的。
第二步,我们尝试在阿里云订阅,发送和接收设置一定要为HEX,发送的数据为“82 25 00 0a 00 20 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 75 73 65 72 2F 4C 45 44 00”,如图7.2所示我们点击发送数据。云端返回“90 03 00 0A 01”!表示订阅成功了!注意!100秒内将第三步的数据发给阿里云,否则阿里云将会自动断开连接的。
第三步,我们尝试取消第二步的订阅,发送和接收设置一定要为HEX,发送的数据为“a2 24 00 0a 00 20 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 75 73 65 72 2F 4C 45 44”,如图7.3所示我们点击发送数据。云端返回“B0 02 00 0A”,表示我们取消了第二步的订阅!注意!100秒内将第四步的数据发给阿里云,否则阿里云将会自动断开连接的。
第四步,我们尝试发送消息给阿里云,发送和接收设置一定要为HEX,发送的数据为“30 98 01 00 35 2F 73 79 73 2F 61 31 44 6D 34 56 45 4B 38 42 52 2F 61 6C 69 79 75 6E 5F 64 65 76 2F 74 68 69 6E 67 2F 65 76 65 6E 74 2F 70 72 6F 70 65 72 74 79 2F 70 6F 73 74 7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 2E 65 76 65 6E 74 2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C 22 69 64 22 3A 22 30 30 30 30 30 30 30 31 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 4C 45 44 53 77 69 74 63 68 22 3A 31 7D 2C 22 76 65 72 73 69 6F 6E 22 3A 22 31 2E 30 2E 30 22 7D”如图7.4所示发送成功!(阿里云不会返回数据的)
至此,极速体验完成。如果想知道上述发送数据的含义和返回数据的含义,或者连接自己的阿里云,请看本文其它章节,上述操作的阿里云是作者本人的阿里云,将会长期有效,但并不一定永久有效的!
图7.1 连接阿里云
图7.2 阿里云订阅
图7.3 取消阿里云订阅
图7.4 发送消息给阿里云