开发者学堂课程【基于STM32的端到端物联网全栈开发:Paho MQTT 客户端接入阿里云物联网平台(2)】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/574/detail/7938
Paho MQTT 客户端接入阿里云物联网平台(2)
内容介绍:
一、MQTT 协议介绍
二、协议关键特性
三、MQTT 协议的通信模型
四、MQTT 协议的主题和消息
五、MQTT 协议的连接和会话
六、MQTT 报文格式
七、报文类型汇总
八、资料下载
九、阿里云 IoT 平台侧的 MQTT 实现
一、MQTT 协议介绍
MQTT (Message Queuing Telemetry Transport)全称是消息队列遥测传输,是用于对消息进行操作和传输的。
MQTT 名字里的第一个 T 是 telemetry,说明它是最适合用于遥测数据的传输,比如传感器数据上传,自动节点数据下发的,mqtt 协议开销很小,尤其适用于 mqm,也就是机器间通行,还有 wsn 无线传感器网络,It 互联网等场景传感器和自动节点,通过 mqtt 消息代理和应用程序进行通信。举例,光线传感器不断向 mqtt 消息代理发送光强信息,楼宇控制系统,接收到文字 mqtt 消息代理转发的光强信息,根据设定的阈值来决定是否打开百叶窗。
需要的时候楼宇控制系统通过向 mqtt 消息代理发送打开百叶窗的消息来给到自动节点去执行。
Mqtt 最初是由 IBM 和 eurotech 制定开发的,之前公布的版本是3.1,2014年 mqtt 被 oasis 采用并以3.1.1版本进行官方发布,之后都是由 oasis 对 mqtt 协议的演进,进行维护和开发,3.1.1版本对3.1版本相互兼容,主要区别在connect 这条消息的稍许变化。
二、协议关键特性
1、轻量级协议,开销小
消息的头最少只有两个字键,消息格式详细信息稍后会介绍。协议简单、通信、握手等交互过程简单明了,整个规范的文档不超过50页的样子,目前总共也就14条消息,就已经可以 cover 通信双方的通信需求。
2、异步通信模式,解耦通信双方
mqtt 是异步通信模式,通过消息、topic 机制背后的消息队列解耦通信双方,一方面是空间上的解耦,通信双方不需要知道对方的 IP 地址和端口号,另一方面是时间上的解耦,通信双方不需要同时在线,但是解耦的背后有一条暗线把通信双方连接起来,那就是消息的 topic,主题,会在协议通信模型中再细讲。
3、基于 TCP
第三个关键特点就是mqtt作为应用层协议是基于面向连接的 TCP 协议的。
三、MQTT 协议的通信模型
接下来具体看 mqtt 协议到底是如何工作的?协议规定的哪些角色?通信模型又是怎样的?
协议关键字
1、客户端(client)服务器端/代理(server/broker)
2、会话(session)
3、消息(message)
4、主题(topic)
5、订阅(subscribe)
6、发布(publish)
mqtt 常用于 M2M 节点设备间的通行,但由于协议的解耦特性,在一个节点看来,它是在和服务器通信,并不知道对方节点是否存在,是否上线以及IP地址和端口号,它看到的就是和服务器通信。
因此 mqtt 协议中规定了两种通信角色,客户端和服务器端,服务器端在 mqtt 协议中又叫代理,客户端通常是传感器节点或者制动器节点,在本课程中就是 stm32,客户端有自己的业务逻辑,因此它的主要工作就是向服务器发布消息,向服务器订阅消息,并接收之后到来的所订阅的消息。发布者和订阅者是客户端上的两个功能角色可以同时存在,也可以只有其中一个,完全取决于应用逻辑。
本课程中 stm32节点要定时发布传感器数值,要在温度超过预值后发布报警信息,因此会是发布者。
同时 stm32要接受来自云端,实际上是应用服务器通过 iot 平台 mqtt 服务端转发的温度阈值,因此 stm32要事先订阅相应的主题,并接收该主题下的消息,因此也是一个订阅者,服务器端或者是代理,本次培训中就统一叫服务器端。
一般还在公有的 iot 平台上,比如阿里云 iot 平台,它没有具体的业务逻辑,主要是作为主题的队列维护和消息的队列以及转发操作,它自己本身不会产生消息或者是主题,它做的就是接收客户端的主题订阅,并转发该主题下的消息到对应的客户端,讲完 mqtt 协议中的角色。
再了解以下 mqtt 协议中传输的消息和主题消息是数据交互的一个但愿,每条信息都有它的主题,比如家里客厅温度,这是一个主题,客厅温度传感器作为 mqtt 客户端连接到了阿里云 iot 平台,手机上也有一个 mqtt 客户端 app,如果想知道客厅温度,就需要有手机 app 的 mqtt 客户端去订阅家里客厅温度这个主题,而客厅的温度计需要定时往阿里云 IoT 平台向客厅温度这个主题发布消息,消息内容就是当前的温度值,在阿里云 IoT 平台测,它要维护很多不同的主题,并记录哪些客户端订阅了这个主题,以便在收到该主题的消息后可以转发给订阅这个主题的所有客户端。
四、MQTT 协议的主题和消息
主题具有层级属性,支持通配符。
技术实现上来讲,主题就是消息队列,一个主题下面由若干个带着该主题标签的信息,主题是用来订阅的,并在发布信息是作为该信息的技术实践上来讲,主题就是消息队列,一个主题下面有若干带着该主题标签的消息,主题是用来订阅的,并在发布消息时作为该消息的标签要一起给到对方,消息是用来发布的,无论是客户端自主产生的数据组成的消息来向服务器发布,还是服务器根据该消息所属的主题对应的那些订阅者进行转发,都叫做发布。
从图示可以看出,发布参数要带上主题和消息,订阅的参数就是主题,订阅这个动作本身是把客户端和主题联系起来,主题具有层级属性,每一级直接有斜线分开, 具有一定的语义,因此主题的表达支持单机通配符加号和多级通配符警号,具体可以看图中的例子。
在中间是 mqtt 的服务器,另外一共有三个 mqtt 的客户端,图中蓝色的粗线是 mqtt 客户端向服务器发布消息的途径,绿色的粗线条是 mqtt 服务器端向客户端发布消息的路径,另外还有一个蓝色的细线,这条路径是 mqtt 客户端向服务器订阅某个主题的路径,mqtt 客户端一即是消息的发布者,又是消息的订阅者,它向服务器发布主题一的消息,同时又将服务器订阅了第二个主题的消息,那 mqtt 客户端二它仅仅只是消息的发布者,它像 mqtt 服务器发布主题二的消息。
mqtt 客户端三它仅仅只是消息的订阅者,并不像服务器发布任何消息,而只接收服务器推送过来的,它订阅了的主题的消息,实际上 IoT 平台会根据用户建立的产品和设备默认产生系统自定义的一些主题,这些主题通常由$符号开头,当然用户也可以自己来定义相关的主题。
五、MQTT 协议的连接和会话
1、连接由客户端发起,会建立一个会话,把客户端附着到服务器上。
2、服务器根据连接参数(ClientID,用户名,密码)对客户端进行鉴权和授权。
3、连接参数(CleanSession)决定此次会话是否是持久会话(Persistent Session)。
刚才讲的消息和主题都是在实现业务逻辑,数据的交互是协议的核心内容,但在此之前要先由 mqtt 客户端向服务器发起连接的请求,并可在业务逻辑数据交互完毕后关闭连接,连接建立后,一个客户端就负责到了一个服务器上了,所谓建立了一个 Session,中文通常在通信标准领域把 session 翻译成一个绘画,客户端发起建立连接的请 求时必须携带若干参数,其中的 client ID 用户名、密码是用于给服务器对该客户端进行健全的,健全的内容包括第一该客户端是否是一个合法的设备。
第二根据 client ID 来对其授权不同的资源操作权限,对于客户端发起的连接请求,服务器一定会发送一个连接应答,在这个应答中会说明该连接是否成功以及被拒绝的原因。
发起建立廉政请求的时候,客户端还会携带一个参数饱和时间及心跳间隔,连接正确建立后,服务器根据该间隔来监听来自客户端的心跳消息或者是业务消息,超时没有收到任何消息,服务器可以主动断开此次链接,当然客户端也可以根据应用,在业务数据交互完毕后自己主动状态连接,如果客户端想保持长链接,在没有业务消息发送的时候,也需要定期发送心跳消息包,以维护此次的连接。
另外一个连接参数 cleansession,cleansession 这个参数是对应于一次连接的,表示该次连接建立成功之后,并在后续断开以后,是否清除此次连接建立的绘画状态。
这里的绘画状态包括已经发出但没有收到响应的 QS1和 QS2级别的消息,对于服务器侧绘画状态还额外包括此次连接保持期间,某客户端向服务器发出的所有订阅请求,它订阅了哪些主题服务器还未转发给客户端的消息,
例如如果建立连接的时候,客户端发过来的 cleansession 参数是0,那表示将建立一个持续绘画,在连接建立后,客户端所有的订阅主题信息都会被服务器记录并保留。
即使该客户端断开连接后下次再连上来,无需再订阅之前该客户端订阅了哪些主题服务器,还记着在客户端离线这段时间内有这些主题的消息到来,服务器都会在客户端再次上线之后主动的把消息推送到客户端。
职责 |
依据的参数 |
鉴权 |
用户名、密码 |
授权 |
客户端 ID |
监测心跳 |
心跳间隔 |
遗嘱事直 |
Will flag/主题/消息... |
会话保持 |
CleanSession |
六、MQTT 报文格式
接下来看协议定义的十四种报文,所有这些就可以完成刚刚提到的各种连接、心跳发布、消息、订阅主题等等交互操作,首先看一下报文格式。MQTT 的有报文三部分组成,固定报头,可变报头和有效负载。
1、固定报头,2~5字节,所有报文都包含。
2、可变报头,长度不固定,部分报文才包含。
3、有效负载,长度不固定,部分报文才包含。
固定报头就是图中的橙色部分的长度是2-5字节,其中第二到第五字节表示,可变报头和有效负载的总长度,根据具体的内容占用一到四个字节不等,可变报头和有效负载分别是图中的绿色和蓝色部分,都是长度不固定的,并且只有部分报文才会有这两部分内容,固定报头中的 message type,这个四位的域表示了该报文是14种报文中的哪一种,在mqttpacke 协议栈的 mqttpacket.h 这个文件中可以看到对14种不同报文类型定义的枚举值。
七、报文类型汇总
在这个表格中列出了所有14种报文的类型,它的 message 值的方向是客户端发出还是由服务器的发出,或者是双方都可以发出这样的报文,固定报头的 udp、qos 、level、retain 这几个标志,仅仅在 publish 报文中才会出现的,其它报文中这些都暂时不会用的,除了所有类型的报文,都必须包括第一部分,即橙色的固定报头之外,绿色的可变报头,蓝色的有效负载部分在哪些报文中才会有哪些,报文里面不包含都在这个表格中列出来了,可以看到商品pingresp 的这样子的报文只需要两字节固定包头就可以了,非常简洁,这里 publish 报文它的有效负载部分是可选的,具体什么意思,在后面将该报文时会说明。
14种报文类型如下:
Middlewares
Mbedtls
paho. mqtt. embedded-c
MQT TClient-C
MQT TClient.c
MQTTPacket
MQTTConnectClient.c
MQTTDeserializePublish.c
MQTTFormatc
MQT TPacketc
MQTTSerializePublish.c
MQTTSubscribeClient.c
MQT TUnsubscribeClient.c
1、连接报文: CONNEECT
MQTTConnect()
int MQTIConnect (MQITClient* C, MQTTPacket. connectData. optiong)
{
MQIIConnackData data;|
return MQTTConnectWithResulta(c, optiong, sdata);
typedef struct
{
/** The eyecatcher for this structure. must be MQTC.
char atruct_ 1d[4];
/** The version number of this structure. Must be 0 */
int 3truct. veraion;
/** Version of MOTT to be used. 3 = 3.1 4 = 3.1.1
*/
unsigmed char MQITVeraion;
MQTTString clientID;
unsigned short keepAliveInterval;
unsigmed char cleanse33ion;
unsigned char w111Flag;
MQIIPacket_ _wi110ptiong wi1l;
MQITString u9ername;
MQTTString pas3word;
} MQITPacket_ connectData;
接下来会讲几个重要的报文类型,以及在 mqtt 客户端协议栈中如何调用它,首先是 MQTT 连接,在固定报头的message type 里面取值为一,表示这是连接报文,绿色的可变长度报头指定了重要的连接参数的标志,比如在后续的可变长度有效负载中是否会包含 MQTT 连接的用户名密码是否有遗嘱相关信息,此次连接是持久会话,以及此次连接的心跳间隔等等。
如果之前的相应标志都是1,那么具体的内容也就是遗嘱主题。遗嘱的消息,mqtt 连接的用户名和密码就会在蓝色的可变长度的有效负载中出现。
协议栈中的调用
mqtt 客户端协议栈在 mqttclient.c 中封装出了 mqttclient 的 API,用户代码在负责阿里云 iot 平台的 mqtt 适配的ali_iotclient. C 中,通过 connect2Aliiothub 和 connect2MQT TServer()对其进行调用。
2、遗嘱(Will)消息
1、Will Flag
2、Will Retain
3、Will QoS
4、Will Topic
5、Will Message
遗嘱消息是客户端在连接服务器时告诉服务器的消息,所以刚才的连接报文里面有几个和遗嘱消息相关的参数,是对该消息端遗嘱相关消息的描述,如期收到客户端发过来的遗嘱消息之后,它会先把这条消息保存起来,服务器检测到客户端意外断开后就会把该消息转发给所有订阅了该客户端遗嘱消息对应主题的那些其它的客户端,尤其会一直保留记录该客户端遗嘱主体和遗嘱消息直到该客户端发送 disconnect 断开报文时,服务器才会删除相应的记录。
遗嘱消息不是必须的,取决于客户端连接报文时 will flag 是不是智慧,而遗嘱作为一套消息自然也是有是否 return 的属性,在后面的发布报文中也会对 return 的属性进行讲解,同样遗嘱消息的发布包括转发也是有自己的 QS 级别的。
3、订阅报文: SUBSCRIBE
MQTTSubscribe()
&Client, threshold_ topic,
QOS0,
Parameters message_ handler)
MQTTSubscribe
&Client,
clearAlarm_ _topic,
QOS0,
Service_ message_ handler)
下面来看订阅报文,订阅操作的对象是某个主题,可以是一个或者是多个主体,因此订阅报文重要的参数之一就是主题的名称,服务器记录了该订阅消息当有该主题的消息到来后,服务器会以正以设置好的 QS 级别来转发消息,所以这里的 QS 是用于服务器到客户端进行下行转发时的个人信息,是订阅报文的第二个重要参数。一次订阅操作可以同时订阅多个不同的主题,各自的主题地域的 QS 也可以不一样,因此 QS 这个域不是在订阅报文的固定报头,而是跟主题名在一起。
协议栈中的调用
同样它获得 mqtt 客户端协议栈在 mqttclient.c 中,封装出了 mqttclient API,用户代码在负责阿里云 IoT 平台的mqtt 连接适配的文件中,通过 device subscribe 对其进行调用。
4、发布报文: PUBLISH
MQTTPublish()
MQTTPublish(
&Client, temp_ hum_ topic,
&MQTT_ msg)
MQTTPublish(
&Client, tempAlarm_ topic,
&MQTT_ msg)
发布报文发布操作的对象是某个主题的消息,因此发布报文里重要的参数就是主题名称和消息负载和订阅报文里面的QS 级别是作用于服务器到客户端的消息下行过程相对应,发布报为的 QS 级别作用于客户端到服务器的消息上行的过程。
协议栈中的调用
它获得 mqtt 客户端协议栈在 mqttclient.c 中,封装出了 mqttpublish API,用户代码在负责阿里云 IoT 平台的 mqtt 连接适配的文件中,通过 ali iotclient. C 中通过 deviceStatusPub()和 deviceAlarmPub()调用。
5、QoS 级别
|
|
发送方报文 |
接收方报文 |
QoS0 |
最多收到一次 |
Publish |
|
QoS1 |
最少收到一次 |
Publish |
Puback |
QoS2 |
收且仅收到一 次 |
Publish Pubrel(ease) |
Pubrec(ord) Pubcomp(lete) |
刚才在发布和订阅报文的参数中都看到了 QS 级别这个参数,它分别负责上行消息段和下行消息段的报文的投送质量。Mqtt 协议定义了三种报文投送的质量,分别是 QoS0、QoS1和 QoS2。QoS 是 quality of service 的简称,QoS0级别就是发送方只发送一次,它不管对方是否收到这条消息,发送方用到的报文就是 publish,接收方也不用对这条报文进行任何的回应,注意这里的发送方既可以是客户端,也可以是服务器,同样接收方即可以是客户端,也可以是服务器,消息的投递质量完全是由 tcpip 网络决定,如果消息发送过程中TCP连接断掉,那么数据就会丢失。
QoS0级别的适用场景,通常是比如温度传感器数据的定时发送,个别数据的丢失,对于消耗该数据的应用来说并不是关键的问题,因为通常应用都会在一段时间内去整合数据再进行处理,比如取平均值。
对于 QoS1的级别,发送方要保证对方一定是收到了发出来的消息,因此接收方通过 pubrel(ease)报文来表示应答,如果虽然接受方收到了发布的消息,但是它回复的 pubcomp(lete)没有被发送方所收到,那么发送方会再一次的执行 publish,因此就会出现接触方收到两次重复消息的可能,所以这种 QS 级别也叫做最少可以收到一次这种级别的场景。
举个例子,比如门上的传感器要感知门的状态,状态的改变,比如关到开到关一定要无遗失的告诉订阅者,比如报警程序或者是主人的手机消息重复收到,可以通过 message ID 来进行甄别并丢弃,对 QoS2级别它则是通过更加复杂的一个报文交互来保证该消息可以被接收方收到,而且就收到一次不会重复,这种级别会带来最大的协议开销,并且在服务器上分布式实现 QoS2比较麻烦,目前主流的公有 ioT 平台都没有实现它,就像前面所说的用户可以自行在QoS1级别的基础上,通过 message ID 在应用层来对重复做到消息进行删除。
现在所看到的这个图中的示意图就描述了在发布和订阅的过程中不同的 QoS 级别的一个使用。
在示意图中可以看到二号客户端发布的消息 messagea 在上行端也就是从客户端二到服务器这个上行端是按照客户端二的 publish 是报文中的 QoS 的参数,也就是 QoS1来进行投递的,messagea 对应的主题是红星,可以看到图中这个主题是被客户端一和客户端三都分别订阅了,但是是以不同的 QoS 级别来订阅的,因此这个消息被服务器分别进行转发的时候,它的消息投递质量分别是 QoS2和 QoS0。
6、消息的 retain
客户端发布的消息报文里,如果 retain 标志置位,服务器需要保留该条消息以及它的 QoS 区别,这样当后面有一个新的订阅发生,并且和该消息的主题一致,服务器马上就会把这条 retain 的消息转发给这个订阅者,而不用等到这个主题的下一个新消息到来之后再转发这条最新的消息,服务器端对每个主题仅保留最近的一个带 retain 标准的消息,删除 return 消息是通过客户端发布一条 payload 为空的消息来实现的。
来看一下示意图,在这个示意图里面客户端一仅仅只发布消息,它是一个发布者,客户端二和客户端三是消息订阅者。
为了简便在这里并没有标出主题,那么默认所有的发布和订阅动作都是默认对一个相同的主题进行操作的,图中不同的颜色是代表了同一个主题的不同的消息,最开始客户端一发布了某个主题下的一条消息,retain 标志物为0,服务器收到这条消息,但是这个时候因为没有任何客户端订阅这个主题,所以这条消息不会被转发。
在客户端一发第二条消息之前,客户端二订阅了这个主题,但是这个时候它是收不到任何消息的,只有等到客户端一发布第二条消息的时候,服务器就会转发这条消息给到客户端二。
接下来客户端一又发布了第三条消息,也就是黄色这条消息,这个时候 retain 标志被制成一,于是服务器端就会保留这条消息作为 retain 消息。接下来在后面的某个时刻,客户端三订阅了同样的主题,这个时候服务器就立刻把它保留的这条 retain 的消息转发给客户端三,并不需要等到客户端一来发第四次消息的时候,客户端3才会收到新消息,然后又过了一段时间,发布者也就是客户端1来发布第四条消息,绿色这条消息,同样它的 retain 的标志也是一,这个时候服务器除了转发这条绿色的消息给到客户端二和客户端三以外,还会把刚才服务器保留的黄色的消息删掉,把最新的这条绿色的消息作为 retain 消息保留起来。如果接下来有新的订阅建立,而且主题符合,服务器就会把这条绿色的消息作为 retain 消息马上发给订阅方。
这个就是 retain 消息的一个工作的模式。
八、资料下载
在前面是了解了 mqtt 的一个大致的工作模式,还有它的一些主要的报文格式以及 QoS 级别,遗嘱消息和 retain 消息的一个工作的方式,在这里来看一下 mqtt 的一些相关的资源,mqtt 协议的资源很多,规范可以在 oasis 的网站上下载,目前官方正式发布的最新版本是3.1.1,mqtt 的客户端实现和服务器的实现,网上也可以找到很多开源的信息。
1、Paho.mqtt. embedded-c
(1)MQTTPacket
底层的 C 代码,提供基本的解析数据,以及将数据串行化的功能。是上层接口的基础,也可以单独使用。
(2)MQTTClient-C
提供 C 的上层接口,针对那些不支持 C++编程的平台。
本课程在 stm32节上使用的 paho 的 mqtt 客户端的实现,可以从 embedded-c 上下载到全部的代码实现,从前面的几个典型的 mqtt 报文的调用讲解可以看到它们的 mqtt 实现主要分两部分,mqttpacket 的文件夹里主要负责底层的功能,比如基本的数据信息数据的全新化等,而在 MQTTClient-C 中除了需要上层应用调用的各种 MQTT 报文的API,这个也会在后面介绍 demo 的过程中做更加详细的具体的介绍。
|
src |
MQTTClient-C |
[FreeRTOS] MQTTFreeROTS.c/.hMQTTClient.c/.h |
MQTTPacket |
MQTTClient.c/.h MQTTPacket MQTTConenct.h, MQTTConnectClient.c, MQTTConnectServer.c, MQTTPublish.h, MQTTDeserializePublish.c, MQTTSerializePublish.c, MQTTSubscribe. h, MQTTSubscribeClient.c, MQTTSubscribeServer.c, MQTTUnsubscribe.h, MQTTUnsubscribeClient,c, MQTTUnsubscribeServer.c, StackTrace.h, MQTTFormate c/.h, MQTTPacket.c/.h, |
九、阿里云IoT平台侧的MQTT实现
支持的 MQTT 协议版本 |
兼容3.1和3.1.1版本 |
与标准 MQTT 的区别 |
不支持遗嘱消息 不支持 retained message 不支持 QoS2 心跳间隔范围: 30~1200秒, 建议300秒以上 在原生 MQTT topic 上支持 RRPC 同步模式,服务器可以对设备进行同步访问(得到设备回执) |
安全等级 |
支持 TLSv1,v1.1, v1.2版本 支持非加密通道连接 |
MQTT 连接方式 |
MQTT 客户端域名直连(本课程使用此种连接方式) 使用 HTTPS 认证再连接模式(参见 S T资料 i-Cube-Aliyun ) |
阿里云 IoT 平台上实现的是 MQTT 服务器这一侧的功能,它支持最新的3.1.1版本,相后兼容3.1版本,但是它不支持遗嘱消息,不支持消息的 retained 的特性,也不支持 QoS 二级别的消息投送质量的保证。另外在 MQTT 的连接期间,节点端和服务器端的心跳间隔,阿里云 IoT 平台要求是30秒~1200秒建议是300秒以上。阿里云 IoT 平台可以通过两种方式进行连接,一种是基于 MQTT 直连,此种方式下 MQTT 的服务器的地址,根据不同用户的不同产品,地址的字符串取决于用户创建产品的 rrpc 和连接阿里云 IoT 平台所对应的 readID,在 MQTT 服务器地址确定后,进行 MQTT 参数的三大参数和 ID 用户名密码,这些都需要按照阿里云 IoT 平台的要求,使用所见产品名下某设备的三元组信息进过一定的规则计算得到,本课程中的两种连接事例都是使用的这种两种连接方式。
另外一种连接方式是基于 https 认证授权,首先设备以自己的 product team 和 device name 的信息作为参数,向固定的https 服务器地址发送 POS 请求,在返回的参数中得到 iotid 和 imtalking 分别作为下一步连接 MQTT 服务器时的用户名密码,ST 开发并挂在阿里网站上的 i-Cube-Aliyun 软件包采用的就是这种连接方式。