MQTT(Message Queuing Telemetry Transport Protocal )是一个客户端服务端的发布/订阅模式的消息传输协议, 适用于机器与机器的通信(M2M)以及物联网环境(IoT).
参考: https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/0301-CONNECT.html
1. 固定报头
第1个字节的前4位表示控制报文的类型
名字 | 值 | 报文流动方向 | 描述 |
Reserved | 0 | 保留 | |
connect | 1 | 客户端到服务端 | 客户端请求连接服务端 |
connack | 2 | 服务端到客户端 | 连接报文确认 |
publish | 3 | 双向 | 发布消息 |
pubnack | 4 | 双向 | Qos 1消息发布收到确认 |
pubrec | 5 | 双向 | 发布收到(保证交付第一步) |
pubrel | 6 | 双向 | 发布释放(保证交付第二步) |
pubcomp | 7 | 双向 | Qos 2消息发布完成(保证交付第三步) |
subscribe | 8 | 客户端到服务端 | 客户端订阅请求 |
subnack | 9 | 服务端到客户端 | 订阅请求报文确认 |
unsubscribe | 10 | 客户端到服务端 | 客户端取消订阅请求 |
unsuback | 11 | 服务端到客户端 | 取消订阅报文确认 |
pingreq | 12 | 客户端到服务端 | 心跳请求 |
pingresp | 13 | 服务端到客户端 | 心跳响应 |
disconnect | 14 | 客户端到服务端 | 客户端断开连接 |
reserved | 15 |
固定报头的第1个字节的剩余4位包含每个MQTT控制报文类型特定的标志, 如果收到非法的标志, 接收者必须关闭网络连接.
从第2个字节开始, 为剩余长度, 表示当前报文剩余部分的字节数, 包括可变报头和负载的数据, 剩余长度不包括用于编码剩余字段本身的字节数.
剩余长度字段使用一个变长度编码方案, 对小于128的值它使用单字节编码. 更大的值按下面的方式处理, 低7位有效位用于编码数据, 最高有效位用于指示是否有更多的字节, 且按照大端方式进行编码, 剩余长度字段最大4个字节.
2. 可变报头
某些MQTT控制报文包含一个可变报头部分. 它在固定报头和负载之间. 可变报头的内容根据报文类型的不同而不同
很多控制报文的可变报头部分包含一个两字节的报文标识字段, 客户端每次发送一个新的这些类型的报文时都必须分配一个当前未使用的报文标识符, 如果一个客户端要重发这个特殊的控制报文, 在随后重发的报文时, 它必须使用相同的标识符. 当客户端处理完这个报文对应的确认后, 这个报文标识符就释放可重用.
1. connect
客户端与服务端的网络连接建立后, 客户端发送给服务端的第一个报文必须是connect报文, 在一次网络连接中, 客户端只能发送一次connect报文, 服务必须将客户端发送的第二个connect报文当作协议违规处理并断开客户端的连接.
connect报文的可变报头按下列次序包含四个字段: 协议名, 协议级别, 连接标志和保持连接.
(1) 协议名是以MQTT的UTF-8编码的字符串.
(2)客户端用8位的无符号值表示协议的修订版本, 对于3.1.1版协议, 协议级别字段的值是4(0x04)
(3)连接标志字节包含一些用于指定MQTT连接行为的参数.
//clean session 清理会话
这个二进制位指定了会话状态的处理方式, 如果清理会话标志被设置为0, 需要做会话保持, 如果清理会话标志被设置为1, 客户端和服务会话仅持续和网络连接同样长的时间.
//will flag 遗嘱标志
遗嘱标志被设置为1, 表示如果连接请求被接受了, 遗嘱消息必须被存储在服务端并且与这个网络连接关联. 之后网络连接关闭时, 服务端必须发布这个遗嘱消息, 除非服务端收到disconnect报文时删除了这个遗嘱消息.
//will qos
用于指定发布遗嘱时使用的服务质量等级.
// will retain
如果遗嘱消息被发布时需要保留, 需要指定这一位的值.
//user name flag
如果用户名标志被设置为0, 有效载荷不能包含用户名字段, 哪果用户名标志被设置为1, 有效载荷中必须包含用户名字段
//password flag
如果密码标志被设置为0, 有效载荷中不能包含密码字段, 设置为1, 有效载荷中必须包含密码字段.
//keep alive
保持连接是一个以秒为单位的时间间隔, 表示为一个16位的字, 它是指客户端传输完成一个控制报文的时刻到发送下一个报文的时刻, 两者之间允许空闲的最大时间间隔.
2. connack 确认连接请求
服务端发送给客户端的第一个报文必须是connack.
(1) 可变报头
第1个字节是连接确认标志, 第2个字节是连接返回码字段.
3. publish
(1) 可变报头
可变报头按顺序包含主题名和报文标识符.
// Topic Name
主题名用于识别有效载荷数据应该被发布到哪一个信息通道. 它必须用UTF-8编码, 不能包含通配符.
4. puback
puback报文是对QoS 1等级的publish报文的响应.
可变报头包含等待确认的publish的报文的报文标识符.
5. pubrec
pubrec报文是对qos2的publish报文的响应, 它是qos 2 等级协议交换的第二个报文.
6.pubrel
是对pubrec报文的响应, 它是qos 2协议交换的第三个报文.
7.pubcomp
是对pubrel报文的响应, 它是qos 2交换的第四个也是最后一个报文.
8. subscribe
客户端向服务端发送subscribe报文用于创建一个或多外订阅. 每个订阅注册客户端关心的一个或多个主题, subscribe报文也为每个订阅指定了最大qos等级.
9. suback
suback用于确认它已收到并且正在处理subscribe报文, 它包含一个返回码, 用于指定subscribe请求的订阅被授予的最大Qos等级.
10. unsubscribe
用于取消订阅主题.
11. unsuback
用于确认收到unsubscribe报文.
12. pingreq
客户端发送pingreq报文给服务端, 用于:
(1) 在没有任何其它控制报文从客户端发给服务时, 告知服务端客户端还活着.
(2) 请求服务端发送响应确认它还活着
(3) 使用网络以确认网络连接还没有断开.
13. pingresp
心跳响应, 表示服务端还活着. 没有可变报头.
14. disconnect
是客户端发送给服务端的最后一个控制报文, 表示客户端正常断开连接.
1. 主题通配符
主题层级分隔用于将结构化引入主题名, 订阅的主题过滤器可以包含特殊的通配符, 允许一次订阅多个主题, 但是主题名不能使用通配符.
2. 主题层级分隔符
斜杠用于分隔主题的每个层级.
3. 多层通配符
#, 它必须是主题过滤顺的最后一个字符.
4. 单层通配符
+,
5. 以$开头的主题
服务端不能将$字符开头的主题名匹配通配符开头的主题过滤器.
6.主题语义和用法
(1) 所有的主题名和主题过滤顺必须到少包含一个字符.
(2) 主题名和主题过滤器是区分大小写的.
(3) 主题名和主题过滤器是UTF-8编码, 它们不能超过65535字节.
1. qos 0
就是仅发一次包, 是否收到完全不管, 适合那些不是很重要的数据.
2. qos 1
相对于qos0 而言, 这个交互就是多了一次ack的作用, 没有ack, 就要找时机重发.
(1) 如果publish下发出现问题, 将没有puback回复, 服务器将找机会重新下发该msgid的消息.
(2) 如果ack回复出现问题, 服务器认为没有收到确认, 仍然重新找机会下发yynwmsgid的消息.
(3) 客户端会收重复收到该msgid的消息, 需要自行去重. 与Qos 1的至少一次送到
3. qos 2
事实上, publish仍旧可能重复发送多次. 但是这个协议能够保证协议上的应用层收到准确一次的消息.
(1) 接收者收到publish的Qos2的消息后, 客户端需要保存一个msgid的记录, 并且进入一个状态, 即之后不管来几个这个msgid的消息, 都不管他, 认为是重复的,.
(2) 接收到publish的Qos2消息之后, 不能马上投递给上层, 而是在本地做持久化, 将消息保存 起来.
(3) 收到publish的Qos2i消息之后, 马上回复一个pubrec给发送端.
(4) 服务器在收到pubrec之后, 应该认为客户端已经收到消息, 将publish的消息转入等待pubcomp的阶段, 不再重发publish, 转而下发pubrel.
(5) 客户端收到pubrel之后, 正式将消息投递给上层应用层.
(6) 投递之后, 销毁该msgid, 返回pubcomp给服务器, 销毁之前的持久化消息.
(7) 之后不管服务器来多少pubrel, 都没有messageid的记录, 只需要回复 pubcomp, 不需要投递给上层.
分析:
publish下发失败, 服务器重发publish
pubrec上报失败, 服务器重发publish. 这个时候, 客户端仍然是重复收到多冷色publish.
pubrel下发失败, 服务器重发pubrel
pubcomp上报失败,服务器重发pubrel
在pubrel之后, 投递给上层的时候, 还是存在重发投递的, 只是风险小了很多而已, 因为是本地投递 , 如果不使用本地化存储, 程序在中间出问题, 很难处理.