6 MQTT协议

1、简述

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。

2、设计规范

由于物联网的环境是非常特别的,所以MQTT遵循以下设计原则:

(1)精简,不添加可有可无的功能;

(2)发布/订阅(Pub/Sub)模式,方便消息在传感器之间传递;

(3)允许用户动态创建主题,零运维成本;

(4)把传输量降到最低以提高传输效率;

(5)把低带宽、高延迟、不稳定的网络等因素考虑在内;

(6)支持连续的会话控制;

(7)理解客户端计算能力可能很低;

(8)提供服务质量管理;

(9)假设数据不可知,不强求传输数据的类型与格式,保持灵活性。

3、主要特性

MQTT协议工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,它具有以下主要的几项特性:

(1)使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。

这一点很类似于XMPP,但是MQTT的信息冗余远小于XMPP,,因为XMPP使用XML格式文本来传递数据。

(2)对负载内容屏蔽的消息传输。

(3)使用TCP/IP提供网络连接。

主流的MQTT是基于TCP连接进行数据推送的,但是同样有基于UDP的版本,叫做MQTT-SN。这两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。

(4)有三种消息发布服务质量:

“至多一次”,消息发布完全依赖底层TCP/IP网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。这一种方式主要普通APP的推送,倘若你的智能设备在消息推送时未联网,推送过去没收到,再次联网也就收不到了。

“至少一次”,确保消息到达,但消息重复可能会发生。

“只有一次”,确保消息到达一次。在一些要求比较严格的计费系统中,可以使用此级别。在计费系统中,消息重复或丢失会导致不正确的结果。这种最高质量的消息发布服务还可以用于即时通讯类的APP的推送,确保用户收到且只会收到一次。

(5)小型传输,开销很小(固定长度的头部是2字节),协议交换最小化,以降低网络流量。

这就是为什么在介绍里说它非常适合“在物联网领域,传感器与服务器的通信,信息的收集”,要知道嵌入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了。

(6)使用Last Will和Testament特性通知有关各方客户端异常中断的机制。

Last Will:即遗言机制,用于通知同一主题下的其他设备发送遗言的设备已经断开了连接。

Testament:遗嘱机制,功能类似于Last Will。

4、MQTT协议原理

4.1 MQTT协议实现方式

IOT传感器的 MQTT发布和订阅模型

image-20210318170107091

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

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

(1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload);

(2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。

4.2 网络传输与应用消息

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

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

网络连接(Network Connection):MQTT使用的底层传输协议基础设施。

  • 客户端使用它连接服务端。
  • 它提供有序的、可靠的、双向字节流传输。

应用消息(Application Message): MQTT协议通过网络传输应用数据。应用消息通过MQTT传输时,它们有关联的服务质量(QoS)和主题(Topic)。

4.3 MQTT客户端(Client)

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

  • 发布应用消息给其它相关的客户端。.
  • 订阅以请求接受相关的应用消息
  • 取消订阅以移除接受应用消息的请求。
  • 从服务端断开连接。
4.4 MQTT服务器(Server)

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

  • 接受来自客户端的网络连接
  • 接受客户端发布的应用消息
  • 处理客户端的订阅和取消订阅请求。
  • 转发应用消息给符合条件的客户端订阅。
4.5 MQTT协议中的术语
4.5.1 订阅(Subscription)

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

4.5.2 会话(Session)

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

4.5.3 主题名(Topic Name)

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

4.5.4 主题筛选器(Topic Filter)

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

4.5.5 负载(Payload)

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

4.5.6 控制报文(MQTT Control Packet)

​ 通过网络连接发送的信息数据包。MQTT规范定义了十四种不同类型的控制报文,其中一个(PUBLISH报文)用于传输应用消息。

4.6 MQTT协议中的方法

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

(1)Connect。等待与服务器建立连接。

(2)Disconnect。等待MQTT客户端完成所做的工作,并与服务器断开TCP/IP会话。

(3)Subscribe。等待完成订阅。

(4)UnSubscribe。等待服务器取消客户端的一个或多个topics订阅。

(5)Publish。MQTT客户端发送消息请求,发送完成后返回应用程序线程。

5、MQTT协议报文结构

在MQTT协议中,一个MQTT数据包由:固定报头(Fixed header)、可变报头(Variable header)、有效载荷(payload)三部分构成。MQTT数据包结构如下:

(1)固定报头(Fixed header)。存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识。

(2)可变报头(Variable header)。存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容。

(3)有效载荷(Payload)。存在于部分MQTT数据包中,表示客户端收到的具体内容。

5.1 MQTT固定报头

固定报头存在于所有MQTT数据包中,其结构如下:

image-20210318190200279
5.1.1 MQTT控制报文的类型

位置:固定报头第1个字节,二进制位7-4。

相于一个4位的无符号值,类型、取值及描述如下:

名字流动方向描述
Reserved0禁止保留
CONNECT1客户端到服务端客户端到服务端的连接请求
CONNACK2服务端到客户端服务端对连接请求的响应
PUBLISH3两个方向都允许发布消息(QoS0)
PUBACK4两个方向都允许对QoS1发布消息的回应
PUBREC5两个方向都允许收到发布消息(QoS2保证传输第一步)
PUBREL6两个方向都允许释放发布消息(QoS2保证传输第二步)
PUBCOMP7两个方向都允许完成发布消息(QoS2保证传输第三步)
SUBSCRIBE8客户端到服务端客户端订阅请求
SUBACK9服务端到客户端订阅请求的确认
UNSUBSCRIBE10客户端到服务端客户端取消订阅请求
UNSUBACK11服务端到客户端取消订阅请求确认
PINGREQ12客户端到服务端Ping(心跳)请求(保持连接)
PINGRESP13服务端到客户端Ping(心跳)响应
DISCONNECT14客户端到服务端客户端断开连接
Reserved15禁止保留
5.1.2 标识位(Flag Bits)

位置:固定报头第1个字节,二进制位3-0。

在不使用标识位的消息类型中,标识位被作为保留位。表格中任何标记为“保留”的标志位,都是保留给以后使用的,必须设置为表格中列出的值。如果收到无效的标志时,接收端必须关闭网络连接。

控制报文固定报头标志Bit 3Bit 2Bit 1Bit 0
CONNECTReserved0000
CONNACKReserved0000
PUBLISHUsed in MQTT 3.1.1DUP1QoS2QoS2RETAIN3
PUBACKReserved0000
PUBRECReserved0000
PUBRELReserved0010
PUBCOMPReserved0000
SUBSCRIBEReserved0010
SUBACKReserved0000
UNSUBSCRIBEReserved0010
UNSUBACKReserved0000
PINGREQReserved0000
PINGRESPReserved0000
DISCONNECTReserved0000

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

(2)QoS:发布(PUBLISH)消息的服务质量等级,即:保证消息传递的次数

Ø00:最多一次,即:<=1
Ø01:至少一次,即:>=1
Ø10:一次,即:=1
Ø11:预留

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

5.1.3 剩余长度(Remaining Length)

**位置:**固定报头中,从第2个字节开始。

剩余长度(Remaining Length)表示当前报文剩余部分的字节数,包括可变报头和负载的数据。剩余长度不包括用于编码剩余长度字段本身的字节数。即剩余长度等于可变报头的长度(10字节)加上有效载荷的长度。

​ 剩余长度字段使用一个变长度编码方案,对小于128的值它使用单字节编码。更大的值按下面的方式处理。低7位有效位用于编码数据,最高有效位用于指示是否有更多的字节。因此每个字节可以编码128个数值和一个延续位(continuation bit)

​ 剩余长度字段的字节长度:最少1个字节,最大4个字节。

image-20210318193202955

分别表示(每个字节的低7位用于编码数据,最高位是标志位):

  • 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)

之所以1个字节不能表示 2^8 - 1 = 255长度,是因为:每个字节的最高位 Bit7,并不表示数据,是进位标志位

示例1

假设本帧剩余字节为 200,计算剩余长度字段。

  1. 使用电脑计算器,将 200 转换为二进制 1100 1000(MSB高位在前)
  2. 从右侧低位每7Bit进行一次拆分,依次拆分出:
  3. 第1个字节为 100 1000,有进位,高位加上进位1为 1100 1000 = 0xC8 (16进制)。
  4. 第2个字节为 1,无进位,为 1 = 0x01 (16进制)。

那么对应的 字节长度的帧格式如下表:

image-20210318203745348

示例2

假设本帧剩余字节为 1000,计算剩余长度字段。

  1. 使用电脑计算器,将 1000 转换为二进制 11 1110 1000(MSB高位在前)
  2. 从右侧低位每7Bit进行一次拆分,依次拆分出:
  3. 第1个字节为 110 1000,有进位,高位加上进位1为 1110 1000 = 0xE8 (16进制)。
  4. 第2个字节为 11 1,无进位,为 11 1 = 0x07 (16进制)。

那么对应的 字节长度的帧格式如下表:

image-20210318202154482

示例3

假设本帧剩余字节为 100,000,000,计算剩余长度字段。

  1. 使用电脑计算器,将 100,000,000 转换为二进制 101 1111 0101 1110 0001 0000 0000(MSB高位在前)
  2. 从右侧低位每7Bit进行一次拆分,依次拆分出:
  3. 第1个字节为 000 0000,有进位,高位加上进位1为 1000 0000 = 0x80 (16进制)。
  4. 第2个字节为 10 0001 0,有进位,高位加上进位1为 1100 0010 = 0xC2 (16进制)。
  5. 第3个字节为 1 0101 11,有进位,高位加上进位1为 1101 0111 = 0xD7 (16进制)。
  6. 第4个字节为 101 111 = 00,无进位,为 10 1111 = 0x2F (16进制)。

那么对应的 字节长度的帧格式如下表:

image-20210318204044807

5.2 MQTT可变报头(Variable header)

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

很多类型数据包中都包括一个2字节的数据包标识符(Packet Identifier)字段,这些类型的包有:PUBLISH (QoS > 0)、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK。

例如:

figure-3.6

5.2.1 报文标识符(Packet Identifier)
Bit7 - 0
byte 1报文标识符 MSB
byte 2报文标识符 LSB

​ SUBSCRIBE,UNSUBSCRIBE和PUBLISH(QoS大于0)控制报文必须包含一个非零的16位报文标识符(Packet Identifier)。客户端每次发送一个新的这些类型的报文时都必须分配一个当前未使用的报文标识符。如果一个客户端要重发这个特殊的控制报文,在随后重发那个报文时,它必须使用相同的标识符。当客户端处理完这个报文对应的确认后,这个报文标识符就释放可重用。

​ QoS 1的PUBLISH对应的是PUBACK,QoS 2的PUBLISH对应的是PUBCOMP,与SUBSCRIBE或UNSUBSCRIBE对应的分别是SUBACK或UNSUBACK 。发送一个QoS 0的PUBLISH报文时,相同的条件也适用于服务端。

​ QoS等于0的PUBLISH报文不能包含报文标识符 。

​ PUBACK, PUBREC, PUBREL报文必须包含与最初发送的PUBLISH报文相同的报文标识符 。类似地,SUBACK和UNSUBACK必须包含在对应的SUBSCRIBE和UNSUBSCRIBE报文中使用的报文标识符

5.3 有效载荷(Payload)

有效载荷(Payload)存在于MQTT数据包的第三部分,包含CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四种类型的消息(PUBLISH为可选):

(1)CONNECT,消息体内容主要是:客户端的ClientID、订阅的Topic、Message以及用户名和密码。

(2)SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS。

(3)SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。

(4)UNSUBSCRIBE,消息体内容是要订阅的主题。

6 MQTT控制报文

6.1 CONNECT – 连接服务端

​ 客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是CONNECT报文。

​ 在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接。有关错误处理的信息请查看操作行为章节。

​ 有效载荷包含一个或多个编码的字段。包括客户端的唯一标识符,Will主题,Will消息,用户名和密码。除了客户端标识之外,其它的字段都是可选的,基于标志位来决定可变报头中是否需要包含这些字段。

6.1.1 固定报头 Fixed header
image-20210319110755479

剩余长度字段

剩余长度等于可变报头的长度(10个字节)加上有效载荷的长度。

6.1.2 可变报头 Variable header

CONNECT报文的可变报头按下列次序包含四个字段:

  • 协议名(Protocol Name)
  • 协议级别(Protocol Level)
  • 连接标志(Connect Flags)
  • 保持连接(Keep Alive)

image-20210319172936613

00 04 4D 51 54 54 04 C2 00 64  //共10个字节
协议名 Protocol Name

协议名是表示协议名 MQTT 的UTF-8编码的字符串。MQTT规范的后续版本不会改变这个字符串的偏移和长度。

如果协议名不正确服务端可以断开客户端的连接,也可以按照某些其它规范继续处理CONNECT报文。对于后一种情况,按照本规范,服务端不能继续处理CONNECT报文。

非规范评注

数据包检测工具,例如防火墙,可以使用协议名来识别MQTT流量。

image-20210319111221146
00 04 4D 51 54 54
协议级别 Protocol Level

​ 客户端用8位的无符号值表示协议的修订版本。对于3.1.1版协议,协议级别字段的值是4(0x04)。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x01(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接。

Protocol Level byte协议级别字节构成

image-20210319111740345
04
连接标志 Connect Flags

连接标志字节包含一些用于指定MQTT连接行为的参数。它还指出有效载荷中的字段是否存在。

连接标志位

image-20210319112641076
如果为 1100 0010 则十六进制为 C2

保留标志位 Reserved

​ 位置:连接标志字节的第0位

​ 服务端必须验证CONNECT控制报文的保留标志位(第0位)是否为0,如果不为0必须断开客户端连接。

清理会话 Clean Session

​ 位置:连接标志字节的第1位

​ 这个二进制位指定了会话状态的处理方式。

​ 客户端和服务端可以保存会话状态,以支持跨网络连接的可靠消息传输。这个标志位用于控制会话状态的生存时间。

​ 如果清理会话(CleanSession)标志被设置为0,服务端必须基于当前会话(使用客户端标识符识别)的状态恢复与客户端的通信。如果没有与这个客户端标识符关联的会话,服务端必须创建一个新的会话。在连接断开之后,当连接断开后,客户端和服务端必须保存会话信息。当清理会话标志为0的会话连接断开之后,服务端必须将之后的QoS 1和QoS 2级别的消息保存为会话状态的一部分,如果这些消息匹配断开连接时客户端的任何订阅。服务端也可以保存满足相同条件的QoS 0级别的消息。

​ 如果清理会话(CleanSession)标志被设置为1,客户端和服务端必须丢弃之前的任何会话并开始一个新的会话。会话仅持续和网络连接同样长的时间。与这个会话关联的状态数据不能被任何之后的会话重用。

客户端的会话状态包括:

  • 已经发送给服务端,但是还没有完成确认的QoS 1和QoS 2级别的消息
  • 已从服务端接收,但是还没有完成确认的QoS 2级别的消息。

服务端的会话状态包括:

  • 会话是否存在,即使会话状态的其它部分都是空。
  • 客户端的订阅信息。
  • 已经发送给客户端,但是还没有完成确认的QoS 1和QoS 2级别的消息。
  • 即将传输给客户端的QoS 1和QoS 2级别的消息。
  • 已从客户端接收,但是还没有完成确认的QoS 2级别的消息。
  • 可选,准备发送给客户端的QoS 0级别的消息。

保留消息不是服务端会话状态的一部分,会话终止时不能删除保留消息。

有关状态存储的限制和细节见操作行为章节。

当清理会话标志被设置为1时,客户端和服务端的状态删除不需要是原子操作。

非规范评注

为了确保在发生故障时状态的一致性,客户端应该使用会话状态标志1重复请求连接,直到连接成功。

非规范评注

一般来说,客户端连接时总是将清理会话标志设置为0或1,并且不交替使用两种值。这个选择取决于具体的应用。清理会话标志设置为1的客户端不会收到旧的应用消息,而且在每次连接成功后都需要重新订阅任何相关的主题。清理会话标志设置为0的客户端会收到所有在它连接断开期间发布的QoS 1和QoS 2级别的消息。因此,要确保不丢失连接断开期间的消息,需要使用QoS 1或 QoS 2级别,同时将清理会话标志设置为0。

非规范评注

清理会话标志0的客户端连接时,它请求服务端在连接断开后保留它的MQTT会话状态。如果打算在之后的某个时间点重连到这个服务端,客户端连接应该只使用清理会话标志0。当客户端决定之后不再使用这个会话时,应该将清理会话标志设置为1最后再连接一次,然后断开连接。

遗嘱标志 Will Flag

​ 位置:连接标志的第2位。

​ 遗嘱标志(Will Flag)被设置为1,表示如果连接请求被接受了,遗嘱(Will Message)消息必须被存储在服务端并且与这个网络连接关联。之后网络连接关闭时,服务端必须发布这个遗嘱消息,除非服务端收到DISCONNECT报文时删除了这个遗嘱消息。

遗嘱消息发布的条件,包括但不限于:

  • 服务端检测到了一个I/O错误或者网络故障。
  • 客户端在保持连接(Keep Alive)的时间内未能通讯。
  • 客户端没有先发送DISCONNECT报文直接关闭了网络连接。
  • 由于协议错误服务端关闭了网络连接。

如果遗嘱标志被设置为1,连接标志中的Will QoS和Will Retain字段会被服务端用到,同时有效载荷中必须包含Will Topic和Will Message字段。

一旦被发布或者服务端收到了客户端发送的DISCONNECT报文,遗嘱消息就必须从存储的会话状态中移除。

如果遗嘱标志被设置为0,连接标志中的Will QoS和Will Retain字段必须设置为0,并且有效载荷中不能包含Will Topic和Will Message字段。

如果遗嘱标志被设置为0,网络连接断开时,不能发送遗嘱消息。

服务端应该迅速发布遗嘱消息。在关机或故障的情况下,服务端可以推迟遗嘱消息的发布直到之后的重启。如果发生了这种情况,在服务器故障和遗嘱消息被发布之间可能会有一个延迟。

遗嘱QoS Will QoS

​ **位置:**连接标志的第4和第3位。

​ 这两位用于指定发布遗嘱消息时使用的服务质量等级。

​ 如果遗嘱标志被设置为0,遗嘱QoS也必须设置为0(0x00) 。

​ 如果遗嘱标志被设置为1,遗嘱QoS的值可以等于0(0x00),1(0x01),2(0x02)。它的值不能等于3。

遗嘱保留 Will Retain

​ **位置:**连接标志的第5位。

​ 如果遗嘱消息被发布时需要保留,需要指定这一位的值。

​ 如果遗嘱标志被设置为0,遗嘱保留(Will Retain)标志也必须设置为0。

如果遗嘱标志被设置为1:

  • 如果遗嘱保留被设置为0,服务端必须将遗嘱消息当作非保留消息发布。
  • 如果遗嘱保留被设置为1,服务端必须将遗嘱消息当作保留消息发布。

密码标志 Password Flag

​ **位置:**连接标志的第6位。

​ 如果密码(Password)标志被设置为0,有效载荷中不能包含密码字段。

​ 如果密码(Password)标志被设置为1,有效载荷中必须包含密码字段。

​ 如果用户名标志被设置为0,密码标志也必须设置为0。

用户名标志 User Name Flag

​ **位置:**连接标志的第7位。

​ 如果用户名(User Name)标志被设置为0,有效载荷中不能包含用户名字段。

​ 如果用户名(User Name)标志被设置为1,有效载荷中必须包含用户名字段。

保持连接 Keep Alive

保持连接字节

image-20210319133837608
00 64

​ 保持连接(Keep Alive)是一个以秒为单位的时间间隔,表示为一个16位的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其它的控制报文可以发送,客户端必须发送一个PINGREQ报文。

​ 不管保持连接的值是多少,客户端任何时候都可以发送PINGREQ报文,并且使用PINGRESP报文判断网络和服务端的活动状态。

​ 如果保持连接的值非零,并且服务端在一点五倍的保持连接时间内没有收到客户端的控制报文,它必须断开客户端的网络连接,认为网络连接已断开。

​ 客户端发送了PINGREQ报文之后,如果在合理的时间内仍没有收到PINGRESP报文,它应该关闭到服务端的网络连接。

​ 保持连接的值为零表示关闭保持连接功能。这意味着,服务端不需要因为客户端不活跃而断开连接。注意:不管保持连接的值是多少,任何时候,只要服务端认为客户端是不活跃或无响应的,可以断开客户端的连接。

非规范评注

保持连接的实际值是由应用指定的,一般是几分钟。允许的最大值是18小时12分15秒。

6.1.3 有效载荷 Payload

CONNECT报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。

img
客户端标识符 Client Identifier

​ 服务端使用客户端标识符 (ClientId) 识别客户端。连接服务端的每个客户端都有唯一的客户端标识符(ClientId)。客户端和服务端都必须使用ClientId识别两者之间的MQTT会话相关的状态。

​ 客户端标识符 (ClientId) 必须存在而且必须是CONNECT报文有效载荷的第一个字段。

​ 客户端标识符必须是1.5.3节定义的UTF-8编码字符串。

​ 服务端必须允许1到23个字节长的UTF-8编码的客户端标识符,客户端标识符只能包含这些字符:“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”(大写字母,小写字母和数字)。

​ 服务端可以允许编码后超过23个字节的客户端标识符 (ClientId)。服务端可以允许包含不是上面列表字符的客户端标识符 (ClientId)。

​ 服务端可以允许客户端提供一个零字节的客户端标识符 (ClientId) ,如果这样做了,服务端必须将这看作特殊情况并分配唯一的客户端标识符给那个客户端。然后它必须假设客户端提供了那个唯一的客户端标识符,正常处理这个CONNECT报文。

​ 如果客户端提供了一个零字节的客户端标识符,它必须同时将清理会话标志设置为1。

​ 如果客户端提供的ClientId为零字节且清理会话标志为0,服务端必须发送返回码为0x02(表示标识符不合格)的CONNACK报文响应客户端的CONNECT报文,然后关闭网络连接。也就是说如果不指定 clientId ,必须清除连接(即将 clean session 设置为 true)

​ 如果服务端拒绝了这个ClientId,它必须发送返回码为0x02(表示标识符不合格)的CONNACK报文响应客户端的CONNECT报文,然后关闭网络连接。

非规范评注

客户端实现可以提供一个方便的方法用于生成随机的ClientId。当清理会话标志被设置为0时应该主动放弃使用这种方法。

遗嘱主题 Will Topic

​ 如果可变报头中的遗嘱标志位被设置为1,有效载荷的下一个字段是遗嘱主题(Will Topic)。遗嘱主题必须是 1.5.3节定义的UTF-8编码字符串。如果遗嘱标志位被设置为0,有效载荷中就不能存在遗嘱主题。

遗嘱消息 Will Message

​ 如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱消息。遗嘱消息定义了将被发布到遗嘱主题的应用消息,见3.1.2.5节的描述。这个字段由一个两字节的长度和遗嘱消息的有效载荷组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。

​ 遗嘱消息被发布到遗嘱主题时,它的有效载荷只包含这个字段的数据部分,不包含开头的两个长度字节。

用户名 User Name

​ 如果用户名(User Name)标志被设置为1,有效载荷的下一个字段就是它。用户名必须是 1.5.3节定义的UTF-8编码字符串。服务端可以将它用于身份验证和授权。

密码 Password

​ 如果密码(Password)标志被设置为1,有效载荷的下一个字段就是它。密码字段包含一个两字节的长度字段,长度表示二进制数据的字节数(不包含长度字段本身占用的两个字节),后面跟着0到65535字节的二进制数据。

1、消息模型:
 MQTT是一种基于代理的发布/订阅的消息协议。提供一对多的消息分发,解除应用程序耦合。一个发布者可以对应多个订阅者,当发布者发生变化的时候,他可以将消息一一通知给所有的订阅者。这种模式提供了更大的网络扩展性和更动态的网络拓扑。

image-20210318160544377

2、消息质量
  MQTT提供三种质量的服务:
  1)至多一次,可能会出现丢包的现象。使用在对实时性要求不高的情况。这一级别可应用于如下情景,如环境传感器数据,丢失一次读记录无所谓,因为很快下一次读记录就会产生。

2)至少一次,保证包会到达目的地,但是可能出现重包。

3)正好一次,保证包会到达目的地,且不会出现重包的现象。这一级别可用于如计费系统等场景,在计费系统中,消息丢失或重复可能会导致生成错误的费用。

3、主题名称
  主题名称(Topic name)用来标识已发布消息的信息的渠道。订阅者用它来确定接收到所关心的信息。它是一个分层的结构,用斜线“/”作为分隔符。有两种通配符可以在主题发布、订阅时使用:“#”和“+”。前者可以通配多层结构,而后者只能通配一层结构。例如一个topic : “a/b/c”,则“a/+/c”和“a/#”都可以和它相等。发布不支持模糊匹配,必须是确定的主题。

4、遗属

当一个客户端断开连接的时候,它希望客户端可以发送它指定的消息。该消息和普通消息的结构相同。通过设置该位并填入和信息相关的内容即可。

6、消息类型

名字流动方向描述
Reserved0禁止保留
CONNECT1客户端到服务端客户端到服务端的连接请求
CONNACK2服务端到客户端服务端对连接请求的响应
PUBLISH3两个方向都允许发布消息(QoS0)
PUBACK4两个方向都允许对QoS1发布消息的回应
PUBREC5两个方向都允许收到发布消息(QoS2保证传输第一步)
PUBREL6两个方向都允许释放发布消息(QoS2保证传输第二步)
PUBCOMP7两个方向都允许完成发布消息(QoS2保证传输第三步)
SUBSCRIBE8客户端到服务端客户端订阅请求
SUBACK9服务端到客户端订阅请求的确认
UNSUBSCRIBE10客户端到服务端客户端取消订阅请求
UNSUBACK11服务端到客户端取消订阅请求确认
PINGREQ12客户端到服务端Ping(心跳)请求(保持连接)
PINGRESP13服务端到客户端Ping(心跳)响应
DISCONNECT14客户端到服务端客户端断开连接
Reserved15禁止保留

开发一个MQTT库需要提供如下命令:

Connect :当一个TCP/IP套接字在服务器端和客户端连接建立时需要使用的命令。

publish : 是由客户端向服务端发送,告诉服务器端自己感兴趣的Topic。每一个publishMessage 都会与一个Topic的名字联系在一起。

pubRec: 是publish命令的响应,只不过使用了2级QoS协议。它是2级QoS协议的第二条消息

pubRel: 是2级QoS协议的第三条消息

publComp: 是2级QoS协议的第四条消息

subscribe: 允许一个客户端注册自已感兴趣的Topic 名字,发布到这些Topic的消息会以publish Message的形式由服务器端发送给客户端。

unsubscribe: 从客户端到服务器端,退订一个Topic。

Ping: 有客户端向服务器端发送的“are you alive”的消息。

disconnect:断开这个TCP/IP协议

3、MQTT服务端和客户端

https://github.com/mqtt/mqtt.github.io/wiki/servers

https://github.com/mqtt/mqtt.github.io/wiki/libraries

topic(主题)设计

1、topic

定阅与发布必须要有主题,只有当定阅了某个主题后,才能收到相应主题的payload,才能进行通信。

2、 主题层级分隔符—“/”

主题层级分隔符使得主题名结构化。如果存在分隔符,它将主题名分割为多个主题层级。斜杠(‘/’ U+002F)用于分割主题的每个层级,为主题名提供一个分层结构。当客户端订阅指定的主题过滤器包含两种通配符时,主题层级分隔符就很有用了。主题层级分隔符可以出现在主题过滤器或主题名字的任何位置。相邻的主题层次分隔符表示一个零长度的主题层级。

如主题:

room212/electric
room212/tv/contrl/sensor
room212/tv/contrl/light
room212/air/sensor
3、 多层通配符—-“#”

“#”是用于匹配主题中任意层级的通配符。多层通配符表示它的父级和任意数量的子层级。多层通配符必须位于它自己的层级或者跟在主题层级分隔符后面。不管哪种情况,它都必须是主题过滤器的最后一个字符 .

例如,如果客户端订阅主题 “china/xiangtan/#”,它会收到使用下列主题名发布的消息:

china/xiangtan
china/xiangtan/yuhu
china/xiangtan/yuetan/hnie
china/xiangtan/jiuhua/jiakao/kemusan

定阅主题示例

school/#                //也匹配单独的 “school” ,因为 # 包括它的父级。
#                       //是有效的,会收到所有的应用消息。
school/teacher/#        //有效的。
school/teacher#         //无效的。
school/teacher/#/lever  //无效的,必须是主题过滤器的最后一个字符
4、 单层通配符—-“+”

加号是只能用于单个主题层级匹配的通配符。在主题过滤器的任意层级都可以使用单层通配符,包括第一个和最后一个层级。然而它必须占据过滤器的整个层级 。可以在主题过滤器中的多个层级中使用它,也可以和多层通配符一起使用。

china/+ 只能匹配 china/guangzhou

china/+/+/zhongshanlu 能匹配china/guangzhou/tianhe/zhongshanlu和china/shenzhen/nanshan/zhongshanlu

5、 通配符 —-“$”

通配符“$”表示匹配一个字符,只要不是放在主题的最开头,即:

$xx
/$xx
/xx$

其它情况下都表示匹配一个字符。

如果客户端想同时接受以 “SYS/”开头主题的消息和不以 开头主题的消息, 它需要同时订阅 “#” 和 ““$SYS/#”。

6、 总结

1、所有的主题名和主题过滤器必须至少包含一个字符
2、主题名或主题过滤器以前置或后置斜杠 “/” 区分
3、只包含斜杠 “/” 的主题名或主题过滤器是合法的
4、主题名和主题过滤器是 UTF-8 编码字符串, 它们不能超过 65535 字节
5、主题名和主题过滤器是区分大小写的

主题层级分隔符 / : 用于分割主题层级,/分割后的主题,这是消息主题层级设计中很重要的符号。 比方说: aaa/bbb和 aaa/bbb/ccc 和aaa/bbb/ccc/ddd ,这样的消息主题格式,是一个层层递进的关系,可通过多层通配符同时匹配两者,或者单层通配符只匹配一个。 这在现实场景中,可以应用到:公司的部门层级推送、国家城市层级推送等包含层级关系的场景。

单层通配符 +: 单层通配符只能匹配一层主题。比如: aaa/+ 可以匹配 aaa/bbb ,但是不能匹配aaa/bbb/ccc。 单独的+号可以匹配单层的所有推送

多层通配符 #: 多层通配符可以匹配于多层主题。比如: aaa/# 不但可以匹配aaa/bbb,还可以匹配aaa/bbb/ccc/ddd。 也就是说,多层通配符可以匹配符合通配符之前主题层级的所有子集主题。单独的#匹配所有的消息主题.

注: 单层通配符和多层通配符只能用于订阅(subscribe)消息而不能用于发布(publish)消息,层级分隔符两种情况下均可使用。

MQTT连接阿里云

在阿里云【选择华东2(上海)】,创建产品和设备

image-20210318101604877

{
  "ProductKey": "a1VVB1j0aNY",
  "DeviceName": "camera",
  "DeviceSecret": "ef6c202cffd5abac0cbdc51e3d16122b"
}

客户端ID : *|securemode=3,signmethod=hmacsha1|
其中 * 表示设备名称,需要替换。

则有客户端ID:

camera|securemode=3,signmethod=hmacsha1|

使用网络调试助手转换成16进制:

63 61 6D 65 72 61 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 

用户名:

camera&a1VVB1j0aNY

MQTT连接OneNET

准备工作

OneNET开发者文档

OneMO线上服务平台可查看服务器的地址与端口号, 对于服务器的地址,onenet分为了不同的地区平台,不过我们一般正常使用的都是中心平台。

image-20210505135833958

使用MQTTX连接

现在我们打开MQTT软件,进行连接测试,与前面的实验操作是一样的,配置好相关信息,即可,这些信息都可以在平台上找到,需要注意的是服务器地址是183.230.40.39;端口号是6002,这与我们常见的1883是不一样的;Client ID是设备ID,在设备列表中是可以找到的;用户名是产品ID,在产品概况页面中可以找到;密码就是创建设备时候的鉴权信息,具体见:

mqtt-onenet006

mqtt-onenet007

mqtt-onenet008

接下来我们可以通过MQTT软件来测试一下能否正常连接,在这一步之前必须已在物联网平台控制台中创建产品和设备,并获取设备相关的信息。

其实连接是与百度天工差不多的,直接填写相关的内容即可:

mqtt-onenet009

配置好就可以连接,然后随便订阅一个主题,因为OneNet平台支持动态创建主题(除系统主题外),所以对我们来说是非常方便的,直接订阅就好了,然后再用客户端进行发布消息,如果收到消息,表明通信成功,

mqtt-onenet010

mqtt-onenet011

回到OneNet的设备列表界面,可以看到刚刚创建的设备是处于在线状态的:

mqtt-onenet012

使用网络调试助手连接

ONENET:MQTT服务器和端口号:183.230.40.39 6002
在ONENET平台创建一个多协议接入平台的设备

image-20210505140626015

image-20210505140559798

接入需要三个参数:
设备ID:692128707
产品ID:406707
APIKEY:123455
其中123455是自定义的

使用网络调试助手进行调试:(连接MQTT的服务器和端口)

image-20210505140819820

MQTT的数据报包括三部分:固定头+可变头+负载(也就是数据)

/*
固定头:10 ??
可变头:00 04 4D 51 54 54 04 C2 00 78
负载:00 09 36 39 32 31 32 38 37 30 37
	00 06 34 30 36 37 30 37 
	00 06 31 32 33 34 35 35
*/
/*??=可变头长度+负载长度=0x25*/
//于是,用于连接服务器所发送的数据报为:
10 25 00 04 4D 51 54 54 04 C2 00 78 00 09 36 39 32 31 32 38 37 30 37 00 06 34 30 36 37 30 37 00 06 31 32 33 34 35 35
image-20210505141155092

服务器返回:
20 02 01 00
20:服务器返回类型编号

MQTT连接ONENET——登录_第12张图片

02:长度
01:session信息在服务器已保持

MQTT连接ONENET——登录_第13张图片

00:成功

MQTT连接ONENET——登录_第14张图片

OneNET MQTT服务器地址和端口号  183.230.40.39:6002

客户端ID( 设备ID ) 692128707
用户名  ( 产品ID ) 406707
密码    (鉴权信息) 123455

CONNECT(1)
10 25 00 04 4D 51 54 54 04 C2 00 78 00 09 36 39 32 31 32 38 37 30 37 00 06 34 30 36 37 30 37 00 06 31 32 33 34 35 35

CONNACK(2)
20 02 01 00

DISCONNECT(14)
E0 00

PING(12)
C0 00

PINGRSP(13)
D0 00

SUBSCRIBE(8) 订阅 app_topic
固定报头:82 ??
可变字节

82 0E 00 0A 00 09 61 70 70 5F 74 6F 70 69 63 00 等级0
82 0E 00 0B 00 09 61 70 70 5F 74 6F 70 69 63 00 等级1

SUBACK(9)
固定:90 0303是剩余长度)
可变字节:
	00 0A订阅的是0A,回复就是0A
	00/01 订阅等级
90 03 00 0A 00
90 03 00 0B 01

UNSUBSCRIBE(10)
固定:A2 ??
可变:00 0C 00 09 61 70 70 5F 74 6F 70 69 63
A2 0D 00 0C 00 09 61 70 70 5F 74 6F 70 69 63

UNSUBACK(11)
固定:B0 02
可变:00 0C
B0 02 00 0C

MQTT.c

#include "stm32f10x.h"    //包含需要的头文件
#include "mqtt.h"         //包含需要的头文件
#include "string.h"       //包含需要的头文件
#include "stdio.h"        //包含需要的头文件
#include "usart1.h"       //包含需要的头文件
#include "wifi.h"         //包含需要的头文件

unsigned char  MQTT_RxDataBuf[R_NUM][BUFF_UNIT];            //数据的接收缓冲区,所有服务器发来的数据,存放在该缓冲区,缓冲区第一个字节存放数据长度
unsigned char *MQTT_RxDataInPtr;                            //指向接收缓冲区存放数据的位置
unsigned char *MQTT_RxDataOutPtr;                           //指向接收缓冲区读取数据的位置
unsigned char *MQTT_RxDataEndPtr;                           //指向接收缓冲区结束的位置

unsigned char  MQTT_TxDataBuf[T_NUM][BUFF_UNIT];            //数据的发送缓冲区,所有发往服务器的数据,存放在该缓冲区,缓冲区第一个字节存放数据长度
unsigned char *MQTT_TxDataInPtr;                            //指向发送缓冲区存放数据的位置
unsigned char *MQTT_TxDataOutPtr;                           //指向发送缓冲区读取数据的位置
unsigned char *MQTT_TxDataEndPtr;                           //指向发送缓冲区结束的位置

unsigned char  MQTT_CMDBuf[C_NUM][BUFF_UNIT];               //命令数据的接收缓冲区
unsigned char *MQTT_CMDInPtr;                               //指向命令缓冲区存放数据的位置
unsigned char *MQTT_CMDOutPtr;                              //指向命令缓冲区读取数据的位置
unsigned char *MQTT_CMDEndPtr;                              //指向命令缓冲区结束的位置

char ClientID[128];                                          //存放客户端ID的缓冲区
int  ClientID_len;                                           //存放客户端ID的长度

char Username[128];                                          //存放用户名的缓冲区
int  Username_len;											 //存放用户名的长度

char Passward[128];                                          //存放密码的缓冲区
int  Passward_len;											 //存放密码的长度

char ServerIP[128];                                          //存放服务器IP或是域名
int  ServerPort;                                             //存放服务器的端口号

int   Fixed_len;                       					     //固定报头长度
int   Variable_len;                     					 //可变报头长度
int   Payload_len;                       					 //有效负荷长度
unsigned char  temp_buff[BUFF_UNIT];						 //临时缓冲区,构建报文用

char Ping_flag;           //ping报文状态      0:正常状态,等待计时时间到,发送Ping报文
                          //ping报文状态      1:Ping报文已发送,当收到 服务器回复报文的后 将1置为0
char Connect_flag;        //同服务器连接状态  0:还没有连接服务器  1:连接上服务器了
char ConnectPack_flag;    //CONNECT报文状态   1:CONNECT报文成功
char SubcribePack_flag;   //订阅报文状态      1:订阅报文成功

/*----------------------------------------------------------*/
/*函数名:初始化接收,发送,命令数据的 缓冲区 以及各状态参数  */
/*参  数:无                                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_Buff_Init(void)
{	
	MQTT_RxDataInPtr=MQTT_RxDataBuf[0];               //指向发送缓冲区存放数据的指针归位
	MQTT_RxDataOutPtr=MQTT_RxDataInPtr;               //指向发送缓冲区读取数据的指针归位
    MQTT_RxDataEndPtr=MQTT_RxDataBuf[R_NUM-1];        //指向发送缓冲区结束的指针归位
	
	MQTT_TxDataInPtr=MQTT_TxDataBuf[0];               //指向发送缓冲区存放数据的指针归位
	MQTT_TxDataOutPtr=MQTT_TxDataInPtr;               //指向发送缓冲区读取数据的指针归位
	MQTT_TxDataEndPtr=MQTT_TxDataBuf[T_NUM-1];        //指向发送缓冲区结束的指针归位
	
	MQTT_CMDInPtr=MQTT_CMDBuf[0];                     //指向命令缓冲区存放数据的指针归位
	MQTT_CMDOutPtr=MQTT_CMDInPtr;                     //指向命令缓冲区读取数据的指针归位
	MQTT_CMDEndPtr=MQTT_CMDBuf[C_NUM-1];              //指向命令缓冲区结束的指针归位
	
	Connect_flag = 0;                                      //各个参数清零
	Ping_flag = ConnectPack_flag = SubcribePack_flag = 0;  //各个参数清零
}
/*----------------------------------------------------------*/
/*函数名:重新初始化接收,发送,命令缓冲区 以及各状态参数     */
/*参  数:无                                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_Buff_ReInit(void)
{			
	unsigned char *MQTT_TxDatatempPtr;                 //指向发送缓冲区位置的临时指针
	
	if(MQTT_TxDataOutPtr != MQTT_TxDataInPtr){         //if成立的话,说明发送缓冲区有数据了
		MQTT_TxDataOutPtr = MQTT_TxDataInPtr;          //OUT指针指向IN指针
		if(MQTT_TxDataOutPtr==MQTT_TxDataBuf[0]){      //如果,现在OUT指针在缓冲区顶部,进入if
			MQTT_TxDataOutPtr =MQTT_TxDataBuf[T_NUM-4];//重定位OUT指针
		}else if(MQTT_TxDataOutPtr==MQTT_TxDataBuf[1]){//如果,现在OUT指针在缓冲区顶部下一个单元,进入if
		    MQTT_TxDataOutPtr =MQTT_TxDataBuf[T_NUM-3];//重定位OUT指针
		}else if(MQTT_TxDataOutPtr==MQTT_TxDataBuf[2]){//如果,现在OUT指针在缓冲区顶部下两个单元,进入if
		    MQTT_TxDataOutPtr =MQTT_TxDataBuf[T_NUM-2];//重定位OUT指针
		}else{
			MQTT_TxDataOutPtr -= BUFF_UNIT;            //OUT指针上移一个单元
			MQTT_TxDataOutPtr -= BUFF_UNIT;            //OUT指针上移一个单元
			MQTT_TxDataOutPtr -= BUFF_UNIT;            //OUT指针上移一个单元
		}			
		MQTT_TxDatatempPtr = MQTT_TxDataInPtr;         //将当前IN指针的位置暂存在temp指针中
		MQTT_TxDataInPtr = MQTT_TxDataOutPtr;          //IN指针指向当前OUT指针
		MQTT_ConectPack();                             //发送缓冲区添加连接报文
		MQTT_Subscribe(S_TOPIC_NAME,0);	               //发送缓冲区添加订阅topic,等级0									
		MQTT_TxDataInPtr = MQTT_TxDatatempPtr;         //IN指针通过temp指针,返回原来的位置		
	}else{                                             //反之,说明发送缓冲区没有数据
		MQTT_ConectPack();                             //发送缓冲区添加连接报文
		MQTT_Subscribe(S_TOPIC_NAME,0);	               //发送缓冲区添加订阅topic,等级0		                                
	}
	Ping_flag = ConnectPack_flag = SubcribePack_flag = 0;  //各个参数清零
}
/*----------------------------------------------------------*/
/*函数名:OneNet云初始化参数,得到客户端ID,用户名和密码    */
/*参  数:无                                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void OneNetIoT_Parameter_Init(void)
{	
	memset(ClientID,128,0);                              //客户端ID的缓冲区全部清零
	sprintf(ClientID,"%s",DEVICEID);                     //构建客户端ID,并存入缓冲区
	ClientID_len = strlen(ClientID);                     //计算客户端ID的长度
	
	memset(Username,128,0);                              //用户名的缓冲区全部清零
	sprintf(Username,"%s",PRODUCTID);                    //构建用户名,并存入缓冲区
	Username_len = strlen(Username);                     //计算用户名的长度
	
	memset(Passward,128,0);                              //用户名的缓冲区全部清零
	sprintf(Passward,"%s",AUTHENTICATION);               //构建密码,并存入缓冲区
	Passward_len = strlen(Passward);                     //计算密码的长度
	
	memset(ServerIP,128,0);  
	sprintf(ServerIP,"%s","183.230.40.39");              //构建服务器域名
	ServerPort = 6002;                                   //服务器端口号6002
	
	u1_printf("服 务 器:%s:%d\r\n",ServerIP,ServerPort); //串口输出调试信息
	u1_printf("客户端ID:%s\r\n",ClientID);               //串口输出调试信息
	u1_printf("用 户 名:%s\r\n",Username);               //串口输出调试信息
	u1_printf("密    码:%s\r\n",Passward);               //串口输出调试信息
}

/*----------------------------------------------------------*/
/*函数名:连接服务器报文                                    */
/*参  数:无                                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_ConectPack(void)
{	
	Fixed_len = 2;                                                        //连接报文中,固定报头长度=2
	Variable_len = 10;                                                    //连接报文中,可变报头长度=10
	Payload_len = 2 + ClientID_len + 2 + Username_len + 2 + Passward_len; //总报文长度       
	
	temp_buff[0]=0x10;                              //第1个字节 :固定0x01	
	temp_buff[1]=Variable_len + Payload_len;        //第2个字节 :可变报头+有效负荷的长度
	temp_buff[2]=0x00;          				    //第3个字节 :固定0x00	            
	temp_buff[3]=0x04;                              //第4个字节 :固定0x04
	temp_buff[4]=0x4D;								//第5个字节 :固定0x4D
	temp_buff[5]=0x51;								//第6个字节 :固定0x51
	temp_buff[6]=0x54;								//第7个字节 :固定0x54
	temp_buff[7]=0x54;								//第8个字节 :固定0x54
	temp_buff[8]=0x04;								//第9个字节 :固定0x04
	temp_buff[9]=0xC2;								//第10个字节:使能用户名和密码校验,不使用遗嘱,不保留会话
	temp_buff[10]=0x00; 						    //第11个字节:保活时间高字节 0x00
	temp_buff[11]=0x64;								//第12个字节:保活时间高字节 0x64   100s
	
	/*     CLIENT_ID      */
	temp_buff[12] = ClientID_len/256;                			  			//客户端ID长度高字节
	temp_buff[13] = ClientID_len%256;               			  			//客户端ID长度低字节
	memcpy(&temp_buff[14],ClientID,ClientID_len);                 			//复制过来客户端ID字串	
	/*     用户名        */
	temp_buff[14+ClientID_len] = Username_len/256; 				  		    //用户名长度高字节
	temp_buff[15+ClientID_len] = Username_len%256; 				 		    //用户名长度低字节
	memcpy(&temp_buff[16+ClientID_len],Username,Username_len);              //复制过来用户名字串	
	/*      密码        */
	temp_buff[16+ClientID_len+Username_len] = Passward_len/256;			    //密码长度高字节
	temp_buff[17+ClientID_len+Username_len] = Passward_len%256;			    //密码长度低字节
	memcpy(&temp_buff[18+ClientID_len+Username_len],Passward,Passward_len); //复制过来密码字串

	TxDataBuf_Deal(temp_buff, Fixed_len + Variable_len + Payload_len);      //加入发送数据缓冲区
}
/*----------------------------------------------------------*/
/*函数名:SUBSCRIBE订阅topic报文                            */
/*参  数:QoS:订阅等级                                     */
/*参  数:topic_name:订阅topic报文名称                     */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_Subscribe(char *topic_name, int QoS)
{	
	Fixed_len = 2;                              //SUBSCRIBE报文中,固定报头长度=2
	Variable_len = 2;                           //SUBSCRIBE报文中,可变报头长度=2	
	Payload_len = 2 + strlen(topic_name) + 1;   //计算有效负荷长度 = 2字节(topic_name长度)+ topic_name字符串的长度 + 1字节服务等级
	
	temp_buff[0]=0x82;                                    //第1个字节 :固定0x82                      
	temp_buff[1]=Variable_len + Payload_len;              //第2个字节 :可变报头+有效负荷的长度	
	temp_buff[2]=0x00;                                    //第3个字节 :报文标识符高字节,固定使用0x00
	temp_buff[3]=0x01;		                              //第4个字节 :报文标识符低字节,固定使用0x01
	temp_buff[4]=strlen(topic_name)/256;                  //第5个字节 :topic_name长度高字节
	temp_buff[5]=strlen(topic_name)%256;		          //第6个字节 :topic_name长度低字节
	memcpy(&temp_buff[6],topic_name,strlen(topic_name));  //第7个字节开始 :复制过来topic_name字串		
	temp_buff[6+strlen(topic_name)]=QoS;                  //最后1个字节:订阅等级
	
	TxDataBuf_Deal(temp_buff, Fixed_len + Variable_len + Payload_len);  //加入发送数据缓冲区
}
/*----------------------------------------------------------*/
/*函数名:PING报文,心跳包                                  */
/*参  数:无                                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_PingREQ(void)
{
	temp_buff[0]=0xC0;              //第1个字节 :固定0xC0                      
	temp_buff[1]=0x00;              //第2个字节 :固定0x00 

	TxDataBuf_Deal(temp_buff, 2);   //加入数据到缓冲区
}
/*----------------------------------------------------------*/
/*函数名:等级0 发布消息报文                                */
/*参  数:topic_name:topic名称                             */
/*参  数:data:数据                                        */
/*参  数:data_len:数据长度                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_PublishQs0(char *topic, char *data, int data_len)
{	
	Fixed_len = 2;                             //固定报头长度:2字节
	Variable_len = 2 + strlen(topic);          //可变报头长度:2字节(topic长度)+ topic字符串的长度
	Payload_len = data_len;                    //有效负荷长度:就是data_len
	
	temp_buff[0]=0x30;                         //第1个字节 :固定0xC0                
	temp_buff[1]=Variable_len + Payload_len;   //第2个字节 :可变报头+有效负荷的长度	
	temp_buff[2]=strlen(topic)/256;            //第3个字节 :topic长度高字节
	temp_buff[3]=strlen(topic)%256;		       //第4个字节 :topic长度低字节
	memcpy(&temp_buff[4],topic,strlen(topic)); //第5个字节开始 :拷贝topic字符串	
	memcpy(&temp_buff[4+strlen(topic)],data,data_len);   //拷贝data数据
	
	TxDataBuf_Deal(temp_buff, Fixed_len + Variable_len + Payload_len);  //加入发送数据缓冲区
}
/*----------------------------------------------------------*/
/*函数名:处理服务器发来的等级0的推送                       */
/*参  数:redata:接收的数据                                */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void MQTT_DealPushdata_Qs0(unsigned char *redata)
{
	int  re_len;               	           //定义一个变量,存放接收的数据总长度
	int  pack_num;                         //定义一个变量,当多个推送一起过来时,保存推送的个数
    int  temp_len;                         //定义一个变量,暂存数据
    int  totle_len;                        //定义一个变量,存放已经统计的推送的总数据量
	int  topic_len;              	       //定义一个变量,存放推送中主题的长度
	int  cmd_len;                          //定义一个变量,存放推送中包含的命令数据的长度
	int  cmd_loca;                         //定义一个变量,存放推送中包含的命令的起始位置
	int  i;                                //定义一个变量,用于for循环
	unsigned char temp[BUFF_UNIT];	       //临时缓冲区
	unsigned char *data;                   //redata过来的时候,第一个字节是数据总量,data用于指向redata的第2个字节,真正的数据开始的地方
		
	re_len = redata[0];                                   			//获取接收的数据总长度
	data = &redata[1];                                              //data指向redata的第2个字节,真正的数据开始的 
	pack_num = temp_len = totle_len = 0;                			//各个变量清零
	do{
		pack_num++;                                     			//开始循环统计推送的个数,每次循环推送的个数+1
		temp_len = data[1+totle_len]+2;                 			//计算本次统计的推送的数据长度
		totle_len += temp_len;                          			//累计统计的总的推送的数据长度
		re_len -= temp_len ;                            			//接收的数据总长度 减去 本次统计的推送的总长度      
	}while(re_len!=0);                                  			//如果接收的数据总长度等于0了,说明统计完毕了
	if(pack_num!=1)u1_printf("本次接收了%d个推送数据\r\n",pack_num);//串口输出信息
	temp_len = totle_len = 0;                		            	//各个变量清零
	for(i=0;i<pack_num;i++){                                        //已经统计到了接收的推送个数,开始for循环,取出每个推送的数据 
		topic_len = data[2+totle_len]*256+data[3+totle_len] + 2;    //计算本次推送数据中主题占用的数据量
		cmd_len = data[1+totle_len]-topic_len;                      //计算本次推送数据中命令数据占用的数据量
		cmd_loca = totle_len + 2 +  topic_len;                      //计算本次推送数据中命令数据开始的位置
		memcpy(temp,&data[cmd_loca],cmd_len);                       //命令数据拷贝出来		                 
		CMDBuf_Deal(temp, cmd_len);                                 //加入命令到缓冲区
		temp_len = data[1+totle_len]+2;                             //记录本次推送的长度
		totle_len += temp_len;                                      //累计已经统计的推送的数据长度
	}	
}
/*----------------------------------------------------------*/
/*函数名:处理发送缓冲区                                    */
/*参  数:data:数据                                        */
/*参  数:size:数据长度                                    */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void TxDataBuf_Deal(unsigned char *data, int size)
{
	memcpy(&MQTT_TxDataInPtr[1],data,size);      //拷贝数据到发送缓冲区
	MQTT_TxDataInPtr[0] = size;                  //记录数据长度
	MQTT_TxDataInPtr+=BUFF_UNIT;                 //指针下移
	if(MQTT_TxDataInPtr==MQTT_TxDataEndPtr)      //如果指针到缓冲区尾部了
		MQTT_TxDataInPtr = MQTT_TxDataBuf[0];    //指针归位到缓冲区开头
}
/*----------------------------------------------------------*/
/*函数名:处理命令缓冲区                                    */
/*参  数:data:数据                                        */
/*参  数:size:数据长度                                    */
/*返回值:无                                                */
/*----------------------------------------------------------*/
void CMDBuf_Deal(unsigned char *data, int size)
{
	memcpy(&MQTT_CMDInPtr[1],data,size);      //拷贝数据到命令缓冲区
	MQTT_CMDInPtr[0] = size;                  //记录数据长度
	MQTT_CMDInPtr[size+1] = '\0';             //加入字符串结束符
	MQTT_CMDInPtr+=BUFF_UNIT;                 //指针下移
	if(MQTT_CMDInPtr==MQTT_CMDEndPtr)         //如果指针到缓冲区尾部了
		MQTT_CMDInPtr = MQTT_CMDBuf[0];       //指针归位到缓冲区开头
}

         */

/返回值:无 /
/
----------------------------------------------------------
/
void TxDataBuf_Deal(unsigned char data, int size)
{
memcpy(&MQTT_TxDataInPtr[1],data,size); //拷贝数据到发送缓冲区
MQTT_TxDataInPtr[0] = size; //记录数据长度
MQTT_TxDataInPtr+=BUFF_UNIT; //指针下移
if(MQTT_TxDataInPtr==MQTT_TxDataEndPtr) //如果指针到缓冲区尾部了
MQTT_TxDataInPtr = MQTT_TxDataBuf[0]; //指针归位到缓冲区开头
}
/
----------------------------------------------------------*/
/*函数名:处理命令缓冲区 */
/*参 数:data:数据 */
/*参 数:size:数据长度 */
/返回值:无 /
/
----------------------------------------------------------
/
void CMDBuf_Deal(unsigned char *data, int size)
{
memcpy(&MQTT_CMDInPtr[1],data,size); //拷贝数据到命令缓冲区
MQTT_CMDInPtr[0] = size; //记录数据长度
MQTT_CMDInPtr[size+1] = ‘\0’; //加入字符串结束符
MQTT_CMDInPtr+=BUFF_UNIT; //指针下移
if(MQTT_CMDInPtr==MQTT_CMDEndPtr) //如果指针到缓冲区尾部了
MQTT_CMDInPtr = MQTT_CMDBuf[0]; //指针归位到缓冲区开头
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南 城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值