这里简单做一些小结和对比,针对前面的协议翻译部分,一阶段的学习完结。
mqtt-sn基于mqtt原有语义,但做了很多的调整。比如: 一个connect消息被拆分为3个消息 主题名称需要使用主题标识符替代 * 网关地址可以广播、查询得知
mqtt-sn 与 mqtt对比,使用一张图介绍
比较类型 | mqtt | mqtt-sn |
---|---|---|
传输类型 | 可靠点对点流模式 | 不可靠的数据报 |
通信方式 | tcp/ip | non-ip 或 udp |
网络传输 | ethernet, wifi, 3g | zigbee,bluetooth,rf |
最小消息 | 2个字节 | 2个字节 |
最大消息 | ≤24mb | < 128个字节 |
电池供电 | x | √ |
休眠支持 | x | √ |
qos -1(哑客户端) | x | √ |
主题标识符 | x | √ |
网关自动发现和回退 | x | √ |
有关qos,mqtt-sn新增增加了哑客户端(qos :-1)支持:
qos level | 消息传输次数 | 传输语义 | 传输保证 |
---|---|---|---|
-1 | ≤ 1 | 至多一次 | 无连接,只用于传输,尽力而为,无保证;只有mqtt-sn支持 |
0 | ≤ 1 | 至多一次 | 尽力而为,无保证 |
1 | ≥ 1 | 至少一次 | 保证送达,可能存在重复 |
-1 | ≡ 1 | 只有一次 | 保证送达,并且不存在重复 |
和mqtt 3.1协议类似,在上一次的客户端成功连接时在connect中设置了清理会话标志clean session为false,遗嘱特性will也为true,再次连接时,那么服务器为其缓存订阅数据和遗嘱数据是否已经被删除,对客户端不透明,因为就算是服务器因内存压力等清理了缓存,但没有通知到客户端,会造成订阅、遗嘱的误解。
还好,mqtt-sn协议中,网关在清理掉遗嘱数据后,可以咨询客户端,或主动通知客户端断开,重新建立会话流程。
在mqtt 3.1.1协议中,服务器会在connack中标记会话是否已经被持久的标记。
确切来说,mqtt-sn协议存在三种格式主题名称(topic name),可由消息标志位包含topicidtype属性决定:
所有主题被替换成标识符,在发布publish消息时,直接使用被指定的主题标识符topic id、short topic name即可。
下面对mqtt-sn常用流程进行的流程简单梳理:
client gateway server/broker
| | |
generic process | --- serchgw ----> | |
| <-- gwinfo ----- | |
| --- connect ----> | |
| <--willtopicreq-- | |
| --- willtopic --> | |
| <-- willmsgreq -- | |
| --- willmsg ----> | ---- connect ----> |(accepted)
| <-- connack ----- | <--- connack ----- |
| --- publish ----> | |
| <-- puback ----- | (invalid topicid) |
| --- register ---> | |
| <-- regack ----- | |
| --- publish ----> | ---- publish ----> |(accepted)
| <-- puback ----- | <---- puback ----- |
| | |
// // //
| | |
subscribe -->| --- subscribe --> | ---- subscribe --> |
[var callback] | <-- suback ------ | <--- suback ------ |
| | |
// // //
| | |
| <-- register ---- | <--- publish ----- |<-- publish
| --- regack ----> | |
[exec callback] | <-- publish ---- | |
| --- puback ---> | ---- puback ----> |--> puback
| | |
// // //
| | |
active -> asleep| --- disconnect -> | (with duration) |
| <-- disconnect -- | (without duration) |
| | |
// // //
| | |
| | <--- publish ----- |<-- publish
| | ----- puback ----> |
| | <--- publish ----- |<-- publish
| | ----- puback ----> |
| | (buffered messages)|
asleep -> awake | | |
| --- pingreq ----> | |
awake state | <-- publish ---- | |
| <-- publish ---- | |
| <-- pingresp ---- | |
asleep <-awake | | |
mqtt-sn可以运行在不同的无线协议上,只要可以满足mqtt-sn 所定义即可:支持双向数据传输和网关即可,mqtt-sn完全可以运行在其上面。
mqtt-sn可以在zigbee、bluetooth、rf、udp、6lowpan等底层协议层面运行。
下面是来自网友的基于mqtt-sn运行的架构图:
但实际上的其网络拓扑可能更为复杂,比如两个不同的传感器网络:
传感器和制动器,合称为sa。传感器汇报状态数值(自身发布publish消息),制动器会被某参数值触发(接收到的publish消息)。好比,文件的输入-输出模式,传感器用于文件的读取,制动器用于文件写入。或者使用管道阀门,某指标超过阀值触发制动器报警,sa一起作用便于更好追踪数据。大部分时间,publish消息被用于触发制动器,这建立在后端服务器的分析结果基础上。
mqtt-sn比较知名的实现,比如( mosquitto],(rmsb)[ forwarder),就鲜有实现,但实现不难嘛,可能缺乏相应的场景支持吧。
mqtt-sn支持类似于传感器的网关,稍强的网络可与适用于mqtt协议,这样看来,mqtt要做到连接一切(connect anything),如ibm所发布的红皮书所说,要使用mqtt打造一个智能星球,有戏!
本篇是mqtt-sn 1.2协议最后一篇翻译了,主要涉及实现要点,很简短。
qos虽默认设置有0,1,2三个值,但还有一种情况其值为-1。来自客户端的publish消息中若qos为-1的情况下,此刻客户端不会关心和网关有没有建立连接,也不在乎时间点,有消息就需要发出去。透明的网关需要维护此类消息并与远程的mqtt server建立一个专用tcp连接。聚合网关或hybird混杂网关可使用已有的mqtt server连接转发此类消息。
定时器/计数器 | 说明 | 推荐值 |
---|---|---|
t_adv | 广播频率 | 大于15分钟 |
n_adv | 没有接收到adverse广播次数 | 2-3次 |
t_searchgw | 发送searchgw延迟 | 5秒 |
t_gwinfo | 等待网关响应gwinfo广播延迟时长 | 5秒 |
t_wait | 等待时长 | 大于5分钟 |
t_retry | 重试时长 | 10s - 15s |
n_retry | 重试次数 | 3-5次 |
网关处理客户端的休眠和存活定时器,需要根据客户端在所发送消息中延续时间的定义值。例如,定时器值应该高出10%大于指定值持续时间1分钟,如果不高出50%。
协议严重建议所有客户端的topic id和topic name之间对应关系不应该使用一个共享池对象,因为这样可以避免不同客户端topic id和topic name匹配错误,将publish消息发错地方(客户端接收者),可能会导致引发潜在的不可恢复的灾难性后果。
正确做法是按照客户端的维度为维护topic id和topic name的对应关系。任何两个客户端之间可能会存在同样的topic name,但对应的topic id不一样。可能topic id一致,但topic name不一样。
mqtt-sn 1.2协议到此翻译(非直译)完毕,嗯,有种想要吐血的感觉,但也是坚持了下来 (^_^)。
我的生涯一片无悔,我想起那天夕阳下的奔跑,那是我逝去的青春。
---《万万没想到》王大锤
紧接上文,这是第三篇,主要是对mqtt-sn 1.2协议进行总体性功能描述。
嗯,这一部分可以结合着mqtt协议对比着来看。
网关只能在成功连接到mqtt server之后,才能够周期性的在无线个人区域网wpns内对所有客户端广播advertise消息,便于客户端被动知道网关的存在。
在同一网络下,多个拥有不同id的网关可有同时运行中,但会由客户端根据信号强弱决定连接具体网关,无论何时只能连接一个网关。
客户端可维护一份可用网关列表(包含网关地址),在接收到包含有新的网关id的advertise和gwinfo消息后,其列表需要添加新的网关元素进去。
advertise广播消息包含的下一次广播间隔时长duration属性,单位秒,设为变量t_adv,应该尽可能大与15分钟(900秒),频率降低是为了避免低速个人区域网络的拥塞。
针对接收advertise消息频率,处理能力较强客户端可以用于监督网关是否可用。eg:客户端连续n_adv次接收不到某个网关advertise广播消息,可认为此网关经死掉不可用并且从已维护的网关列表中移除。同样的,作为备用的网关认为主网关已挂掉,此时可处于激活状态,正常发挥作用。
网关发送广播消息advertise的时间间隔很长,这对导致新加入的客户端不利,但客户端可以直接发送searchgw广播消息进行查询网关。大量的新入设备会造成广播风暴造成网络拥挤,每一个新加入的客户端在发送searchgw广播消息之前都需要获取一个随机的延迟发送值(0-tsearchgw),在延迟等待发送期间若接收到其它客户端发送的searchgw广播消息,会取消掉自己的searchgw广播消息发送,等待网关gwinfo消息通知。
searchgw消息属性radius广播半径,记为变量rb,1跳(1 hop)在一般密集部署下的mqtt-sn客户端基本可用。
网关接收到searchgw会即刻回复包含自身id的gwinfo消息。客户端收到searchgw后,若有需要延迟发送的searchgw会取消掉,若自身维护一份多个可用网关列表,在等待t_gwinfo时间内没有收到gwinfo消息,会从列表中取出一条网关信息组装成gwinfo消息并广播出去。这就要求客户端已运行多时,并且维护多个可用网关列表。
gwinfo和searchgw所包含半径radius属性值一致,这就要求底层网络在传输时进行决定是否需要传输到其它类型网络中。
若没有接收到响应,searchgw消息可能被重新传输。两个连续的searchgw消息重传间隔应该呈指数形式增加,避免太密集传输。
无论是基于哪一种传输协议,tcp or udp,客户端都需要建立连接,并且保持心跳,逻辑上和服务器端保持一条不断线的双向通道。下面一张图,演示了客户端建立连接的过程,并且设定客户端在connect消息中标志位字段中遗嘱will属性为true,然后就有了遗嘱主题/消息的请求过程。
很多情况下,连接connect是不需要遗嘱支持的,网关会直接返回connack消息,但网关会因为拥塞或不支持一些connet特性,connack所包含返回代码字段returncode中包含拒绝代码,要求客户端检查是否连接成功,区别对待。比如:
connack消息返回状态码为0x01(rejected: congestion,因拥塞被拒绝),客户端需要在t_wait时间间隔后进行重试。
已经连接的客户端断线后,若之前在connect中没有设置过会话清理(clean session)标识,那么之前的订阅等信息在网关处将会持久存在。相比mqtt,mqtt-sn中的“clean session”标识被扩展到遗嘱特性中。在connect消息中,cleansession和will组合将会产生以下效果:
受限于无线传感器网络的有限带宽和微小消息负载,publish消息中不能够包含完整的主题名称topic name。这就需要客户端和网关之间通过注册流程,获取主题名称对应的(16位的自然数)topic id,然后塞入publish消息的topicid属性中。
客户端发送register消息,网关返回regack消息,其所包含的returencode属性决定注册成功与否:
任意时间,只能执行一个register消息,有没有完成注册流程,需要等待。
网关->客户端方向,网关发送register消息给通知客户端指定topicid对应某个主题,以便后面发送publish消息使用。若客户端在订阅subscribe消息时使用了通配符(#/ ),那么与之相匹配的topic name也将被一一通知到。因此不建议使用通配符,较为低效。
客户端一旦获取到topic name对应topic id,就可以直接发送publish消息了。这和mqtt协议相比,publish消息中topic name被替换成topic id,除此之外,还要注意returncode:
qos 1和 qos 2在任一时间,都必须等待已有publish消息完成,才能进行下面的publish消息发布流程。
预定义的topic id已提前指派好对应的topic name,需要客户端和网关在代码层级支持,省略了中间注册流程,在连接建立之后可以马上进行publish消息,但这需要在publish标志flags字段中设置topicidtype值为0b01(0b10表示两个字节长度的短topic name)。虽然可以快速发送publish消息,但客户端想订阅预定义的topic id,接收对应的publish消息,一样需要发送subscrible消息请求进行订阅。若乱指定预定义topic id,会收到returncode=“rejection: invalid topic id”的异常。
预定义的短topic name只有两个字符长度的字符串(也是两个字节),topic id为两个字节表示的一个自然数(0-65535),两者使用场景一致,都需要在标志位flags设置topicidtype具体值,0b01表示预定义topic id,0b10表示两个字节长度的短topic name,需要分清。
这对仅仅支持publish qos -1的非常简单的客户端实现而言,除此之外不支持任何特性。它不关心连接是否建立,也没有注册、订阅这一说,按照已经固化到代码中的网关地址直接发送publish消息,不关心网关地址是否正确、网关是否存活、消息是否发送成功。
下面的publish属性值依赖于qos -1的情况:
客户端对某个主题感兴趣,可以发起subscrible流程,携带上感兴趣的主题名(topic id),服务器一般会返回包含有指定主题id(topic id)的suback消息。订阅失败,可以从puback的returncode中获知:
有一种情况是subscrible订阅主题包含通配符,网关的处理就很简单,在suback中返回的topic id为0x0000。稍后,网关向客户端发送register消息走注册流程,通知通配符匹配到的主题对应的topic id值。
来自客户端的subscrible消息一样支持预定义topic id,以及短topic name,这和publish消息差不多。
退订就很简单,客户端发送unsubscrible消息,网关返回unsuback消息。
但同一时刻,客户端只允许处理订阅subscrible或取消订阅unsubscrible按照串行化顺序,下一个操作依赖于上一个操作完全成功。
服务器发布流程和客户端类似,在发布之前需要检测其主题是否已经向客户端提前注册过,若无需要把主题和指定的topic id放入register消息中发送给客户端进行注册流程,然后等待客户端处理结果regack。注册通过,然后才能正常发送publish消息。
网关需要确保register的主题以及publish消息的内容负载都不能太长超过当前网络负载上限(比如在zigbee环境下不能超过60个字节),取消注册/发布流程就好了。
网关发布publish消息时,客户端检测到未知的topic id,把拒绝理由封装到puback后,网关遇到returncode=“rejected: invalid topic id”非法topic id,需要考虑删除或重新注册。
客户端或许会拒绝其注册,或许会不允许publish消息,网关如上静默处理就好了,失败就失败了,不需要告知别人。
客户端发布流程于此类似,需要在发布之前进行主题注册以获取指定的topic id,提交publish消息后,同样需要检查puback所包含的returncode字段是接受还是拒绝,因网络拥塞而产生的拒绝,客户端需要在t_wait时间后再次重试。
客户端的发布必须是串行方式,下一个需要发送到publish消息需要等待上一个发送成功被网关接受之后才能进行处理。
一般是客户端->网关,网关->客户端也没有问题。但要求pingreq -> pingresp 一定要单个时针循环,pingreq发送者不能也是pingresp的发送者,那样不但乱了流程,也浪费了网络资源。嗯,不允许双向互发。
客户端可基于心跳机制监测已连接网关健康与否,连续多次接收不到来自网关的pingresp消息后,客户端连接下一个可替换的网关。因为客户端的连接和心跳和其它客户端状态属性不同步,但这可能会带来一个问题,同一时间若有大量的客户端洪水般同时连接一个网关,网关可能毫无征兆的会被冲垮掉。这就要求网关要有批量的连接处理能力,并发特性增强才行。
客户端主动发送disconnect消息告知网关需要断线之后,若有交换信息的需要可以重新发起一个新的会话连接。disconnect消息之后,网关不会清理掉已有订阅和遗嘱数据,除非在之前的connect消息中已硬性设置了cleansession会话清理标识为true。网关接收到disconnect消息之后会返回一个disconnect消息作为响应。
有一种情况是客户端会突然接收到来自网关的disconnect消息,这也许是网关自身发生了异常错误,或网关无法定位客户端的消息归属(客户端的消息和客户端无法关联到一起),此时客户端需要发送connect消息重建与网关的会话连接。
客户端->网关的消息都是单路传播的,这依赖于客户端所持有的已连接网关的单播地址。
客户端发送一个消息之后,需要启动一个重试定时器tretry和一个重试计数器nretry用以监督网关消息响应。定时器会被客户端在指定时间内接收到来自网关的消息后取消掉,若没有准时接收到则会触发定时器执行消息重发流程,连续nretry次重发后,客户端会直接取消掉当前流程,判断当前网关已经断线,需要连接到另外一个可用的网关。假如另外的网关也是连接失败,会尝试重连之前的网关。
若在休眠状态下,一旦超过重试计数器值,客户端直接进入休眠状态。
这里所说的客户端指的是依赖电池驱动的电子设备,你要明白一个事实,节省电池资源是多麽的重要,省电就是关键,没电了就没得玩了嘛。当不处于激活状态时为了省电就得需要进入睡眠/休眠状态,当有数据需要接收或发送时就可以醒过来。网关嘛需要追踪设备的休眠状态并且支持缓存需要发送给休眠设备的消息,在设备唤醒时一一发送。
下面是客户端的状态转换图,很清晰描述了各种状态之间的交互:
客户端具有五种状态:激活(active),休眠(asleep),唤醒(awake),断线(disconnected),丢失(lost),每次只能是其中一种。
网关需要监督客户端的状态,开始于connect消息中存活时长字段(keep alive),在大于存活时长时间内网关接收不到来自客户端消息,网关认为客户端已经处于丢失状态(lost),会激活对应的遗嘱特性若存在的话。
客户端发送disconnect消息但没有duration休眠时长字段,网关这将处于没有时间监督的断线状态。一旦包含duration休眠时长字段,表示客户端需要休眠一段时间,网关这客户端被转换为休眠状态,休眠时长为duration所定义在值。超过此休眠时长的一段时间内,网关若接收不到客户端发送过来的任何消息,那么客户端会被转化为丢失状态,若已设置遗嘱特性,此时遗嘱特性会生效。客户端休眠期间需要被发送的消息都会被网关缓存。
睡眠状态下流程图会更形象的说明流程:
毫无疑问,网关可使用一个休眠定时器维护客户端的休眠状态等,休眠定时器会被停掉当网关接收到客户端发送过的pingreq消息,网关从pingreq消息所包含的client id检索是否存在已缓存的publish消息,若有会一一按照顺序发送到客户端。所有对应已缓存消息发送完毕后,会随之发送一个pingresp消息。若没有缓存消息,网关直接返回一个pingresp消息。网关会重新启动休眠定时器,网关维护的客户端状态被转换为休眠状态,客户端在接收到pingresp消息之后,将直接转向休眠状态,节省用电。
客户端在唤醒状态下处理消息,遵守“客户端重传流程”行为,一旦达到重试计数器限制,将进入睡眠状态。
客户端从休眠状态转向唤醒状态用于检查网关是否为其缓存消息时,需要发送一个pingreq消息到网关;从休眠/唤醒状态转换为激活状态,需要发送一个connect消息告知网关;转换为断线状态时需要发送两个字节的disconnect(没有休眠时长字段duration)消息;需要重新定义的休眠时长,发送一个disconnect消息(包含新的duration时长值)通知网关即可。
功能性描述介绍完了,基本上mqtt-sn协议介绍已接近尾声,最后面的篇章就是短短的实现描述了。
紧接着上篇初步介绍,本文为第二篇,主要梳理mqtt-sn 1.2协议中定义的消息格式。
消息头 | 其它可变部分 |
---|---|
2/4字节表示 | n字节组成 |
长度 | 消息类型 |
---|---|
1或3个字节 | 1个字节 |
备注:
mqtt-sn不支持消息的分片和重组。底层网络所定义数据包长度若小于mqtt-sn消息的最大长度,自身进行的分片和重组,对mqtt-sn协议本身不受影响。
mqtt-sn定义的消息类型数量众多,超过25个,感觉有些头大。
消息类型值 | 消息类型名称 | 说明 |
---|---|---|
0x00 | advertise | 广播消息 |
0x01 | searchgw | 寻找网关 |
0x02 | gwinfo | 网关信息 |
0x03 | reserved | 没有使用到 |
0x04 | connect | 发起连接 |
0x05 | connack | 连接确认 |
0x06 | willtopicreq | 遗嘱主题请求 |
0x07 | willtopic | 遗嘱主题确认 |
0x08 | willmsgreq | 遗嘱消息请求 |
0x09 | willmsg | 遗嘱消息确认 |
0x0a | register | 注册主题 |
0x0b | regack | 注册确认 |
0x0c | publish | 发布消息 |
0x0d | puback | 发布确认 |
0x0e | pubcomp | 发布环节消息 |
0x0f | pubrec | 发布环节消息 |
0x10 | pubrel | 发布环节消息 |
0x11 | reserved | 保留字段 |
0x12 | subscribe | 订阅主题 |
0x13 | suback | 订阅确认 |
0x14 | unsubscribe | 退订 |
0x15 | unsuback | 退订确认 |
0x16 | pingreq | ping请求 |
0x17 | pingresp | ping响应 |
0x18 | disconnect | 断开 |
0x19 | reserved | 保留字段 |
0x1a | willtopicupd | 遗嘱主题更新 |
0x1b | willtopicresp | 遗嘱主题更新确认 |
0x1c | willmsgupd | 遗嘱消息更新 |
0x1d | willmsgresp | 遗嘱消息更新确认 |
0x1e-0xfd | reserved | 保留字段 |
0xfe | 转发封装标志 | 用于转发 |
可变字段很多,与mqtt相比,多了:
返回值 | 返回值含义 |
---|---|
0x00 | 接受请求(accepted) |
0x01 | 因拥塞拒绝(rejected: congestion),一般需要接收方等待t_wait时间长 |
0x02 | 因非法主题标识符拒绝(rejected: invalid topic id) |
0x03 | 因不支持拒绝(rejected: not supported) |
0x04 - 0xff | 保留,没有使用到 |
网关周期性会对当前网络下所有客户端、节点进行广播,用于客户端发现可用网关。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x05 |
1 | msgtype | 0x00 |
2 | gwid | 网关需要吧自身标识符包含其中 |
3-4 | duration | 网关的下次广播间隔时长,单位秒 |
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x03 |
1 | msgtype | 0x01 |
2 | radius | 广播半径深度,同时也是只是给当前网络传输层 |
客户端主动寻找网关进行广播的消息,广播路径范围受限于当前网络环境下的客户端部署密度,比如只有1跳广播在非常密集的网络环境下客户端都可以彼此互相访问。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态确定 |
1 | msgtype | 0x02 |
2 | gwid | 网关id |
3-n | gwadd* | 一个网关地址,仅仅由客户端发出消息时,此字段才存在 |
gwinfo作为对searchgw消息的响应:
客户端向网关发出建立连接的消息。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x04 |
2 | flags | 标志位 |
3 | protocolid | 0x01,表示协议版本和协议名称 |
4-5 | duration | 存活持续时长 |
6-n | clientid | 客户端标识符,1-23个字节表示的字符串 |
在connect消息标志位具体表示如下:
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
x | x | x | 0/1 | 0/1 | x |
在flags中使用到的标志位:
网关对客户端发出connect消息的响应。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x03 |
1 | msgtype | 0x05 |
2 | returncode | 接受值0x00,拒绝为0x01-0x03,具体见上文recodecode定义 |
根据客户端connect标志位中will字段为true情况下,网关向客户端发出遗嘱主题请求,格式如下:
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x02 |
1 | msgtype | 0x06 |
只有头部部分,很简单。
客户端作为网关willtopicreq请求响应消息。下面是一个正常版本的willtopic消息:
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x07 |
2 | flags | 标志位 |
3-n | willtopic | 遗嘱主题 |
此时的标志位如下
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
x | 0x00-0x02 | 0/1 | x | x | x |
而空的willtopic也是允许存在的,就两个字节表示,用于客户端请求删除已存在于服务器端的对应遗嘱主题和消息。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x02 |
1 | msgtype | 0x07 |
根据客户端connect标志位中will字段为真情况下,网关向客户端发出遗嘱消息请求,格式如下:
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x02 |
1 | msgtype | 0x08 |
只有头部部分,没有别的。
客户端对网关willmsgreq请求的响应,从而把遗嘱消息传递给网关进行保存。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x09 |
2-n | willmsg | 客户端遗嘱 |
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x0a |
2-3 | topicid | 客户端发出,此值为0x0000;服务器发出,需要包含对应于topic name的主题标识符 |
4-5 | msgid | 自然数,用以标识对应的regack确认 |
6-n | topicname | 主题名称,不能太长,尽量不要使用通配符 |
客户端或网关针对register消息的响应。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x07 |
1 | msgtype | 0x0b |
2-3 | topicid | 对应于topic name的主题标识符,被用于publish消息发布 |
4-5 | msgid | 自然数,用以标识对应的register消息 |
6 | returncode | 0x00被接受,其它值被拒绝 |
publish消息用于客户端或网关发布消息用:
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x0c |
2 | flags | 标志位 |
3-4 | topicid | 主题标识符 |
5-6 | msgid | qos 1-2时需要填充自然值;qos 0时,值为0x0000 |
7-n | data | 用于发布的具体消息内容 |
标识位具体如下:
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
0/1 | 0x00-0x02 | 0/1 | x | x | 0b00/0b01/0b10 |
标识位里面各个字段和mqtt协议一致,无须多解释。
客户端/网关仅仅对qos 1/2的publish消息做出响应。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x07 |
1 | msgtype | 0x0d |
2-3 | topicid | 对应publish消息中topicid |
4-5 | msgid | 自然数,用以标识对应的register消息 |
6 | returncode | 0x00被接受,其它值被拒绝,不同值表示不同拒绝理由 |
处理publish消息异常?在puback消息中的returncode字段中以相应值体现出来,这就要求接收者处理拒绝理由。
只有在publish消息中qos 2时,pubrec, pubrel, pubcomp才会一起登场,否则是没有出场机会的。消息格式嘛,都很统一:
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x04 |
1 | msgtype | 0x0f/0x10/0x0e |
2-3 | msgid | 对应publish消息中的msgid |
subscribe用于客户端订阅某个主题的消息。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x12 |
2 | flags | 标志位 |
3-4 | msgid | 用于确定对应的订阅确认suback消息 |
5-n | topicid/topicname | 具体需要根据flags标志位中topicidtype进行填充 |
标识位具体如下:
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
0/1 | 0x00-0x02 | x | x | x | 0b00/0b01/0b10 |
此处,标志位中topicidtype决定了subscribe消息中topicid/topicname字段具体填充值:预定义topic id,或短小两个字符表示主题(topic name),或直接填写主题。
网关->客户端,订阅处理情况的确认回执,接受订阅或出于其它原因拒绝之。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x08 |
1 | msgtype | 0x13 |
2 | flags | 标志位 |
3-4 | topicid | 网关接受其注册,此处对应具体指派的topicid |
5-6 | msgid | subscribe消息中对应msgid值 |
7 | returncode | 0x00被接受,其它值被拒绝 |
标识位具体如下:
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
x | 0x00-0x02 | x | x | x | x |
suback消息标志位中qos为网关根据实际情况授权后的qos具体值,这也应该是客户端需要知道并处理的。
unsubscribe用于客户端取消订阅某个主题的消息。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x14 |
2 | flags | 标志位 |
3-4 | msgid | 用于确定对应的退订确认unsuback消息 |
5-n | topicid/topicname | 具体需要根据flags标志位中topicidtype进行填充 |
标识位具体如下:
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
x | x | x | x | x | 0b00/0b01/0b10 |
unsubscribe消息标志位中唯一可用属性topicidtype决定了unsubscribe消息中topicid/topicname字段具体填充值。
网关->客户端,取消订阅处理情况的确认回执,很简单,4个字节表示。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x04 |
1 | msgtype | 0x15 |
2-3 | msgid | unsubscribe消息中对应msgid值 |
和mqtt协议中的pingreq一致,存活检测。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x16 |
2-n | clientid | 可选项,表示客户端休眠状态转换为唤醒状态用于检查网关是否为其缓存消息 |
接受pingreq消息的一方,如网关响应pingresp消息表示自己现在运行ok。
另外一个意图,若唤醒状态客户端发送pingreq消息之后,直接收到pingresp消息,表示网关当前暂时没有为其缓存的消息可供发送。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x02 |
1 | msgtype | 0x17 |
很简单,两个字节表示足矣。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x18 |
2-3 | duration | 可选项,表示客户端即将进入睡眠状态的持续时间值 |
网关接收到要进入休眠状态的客户端发送的包含有duration字段disconnect消息时,可以直接返回2个字节的(不能包含有duration字段)disconnect消息以示确认。
客户端发送请求网关更新其遗嘱主题。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x1a |
2 | flags | 标志位 |
3-n | willtopic | 用于更新的遗嘱主题 |
标识位具体如下:
dup | qos | retain | will | cleansession | topicidtype |
---|---|---|---|---|---|
bit 7 | 6,5 | 4 | 3 | 2 | 1,0 |
x | 0x00-0x02 | 0/1 | x | x | x |
协议规定只有两个字节空willtopicupd也是允许存在的,存在意义用于客户端请求网关删除已保存的遗嘱主题和遗嘱消息等。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x02 |
1 | msgtype | 0x1a |
willtopicresp为网关收到willtopicupd后作出的应答消息。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x03 |
1 | msgtype | 0x1b |
2 | returncode | 0x00被接受,其它值被拒绝 |
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 动态计算 |
1 | msgtype | 0x1c |
2-n | willmsg | 用于更新的遗嘱消息 |
客户端->网关,确认更新的遗嘱消息。
willmsgresp为网关收到willmsgupd后作出的应答消息。
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 0x03 |
1 | msgtype | 0x1d |
2 | returncode | 0x00被接受,其它值被拒绝 |
在mqtt-sn架构图中,mqtt-sn forwarder转发器适用于客户端无法直接访问网关或当前传感器网络区域中不存在网关时,转发器作用就体现出来了:
转发器作用于消息的封装转发,解封发送,针对消息不做修改。
转发器对mqtt-sn消息封装格式:
字节索引 | 表示内容 | 说明 |
---|---|---|
0 | length | 十进制表示长度就是n |
1 | msgtype | 0xfe |
2 | ctrl | 包含网关和转发器之间的控制交换信息,主要是前两位包含了半径范围 |
3-n | wireless node id | 标识所发目的或需要接收封装消息的无线节点 |
n 1-m | mqtt-sn message | 一个mqtt-sn消息消息 |
无线节点id(wireless node id):
控制交换字段ctrl,单个字节,位表示含义:
bit 7,2 | 1,0 |
---|---|
x | 0x00-0x03 |
mqtt-sn 1.2规范中所定义消息格式介绍完毕,下一篇将对mqtt-sn主要流程功、能进行阐述。
这一段时间在翻看mqtt-sn的协议,对针对不依赖于tcp传输的mqtt协议十分感兴趣,总是再想着这货到底是怎么定义的。一系列文章皆有mqtt-sn 1.2协议所拼装组成,原文档地址:
mqtt-sn文档分为7个部分,我直接按照从前到后的顺序,直接组装成四个小篇。嗯,若放在一篇文章中,文字太长,造成排版难度。
非直译,完全按照自己理解整理而成,请知晓。
原名是mqtt-s,但会引起人们的误解,因此更名成mqtt-sn:
as part of the job of applying the same or similar license terms to the mqtt-s specification as those on the mqtt specification, we are proposing a small name change. the new name would be mqtt-sn, standing for exactly the same long name, mqtt for sensor networks. some people had assumed that the s in mqtt-s stood for secure, so we hope this change will avoid that confusion. -- ian craggs
mqtt for sensor networks is aimed at embedded devices on non-tcp/ip networks, such as zigbee. mqtt-sn is a publish/subscribe messaging protocol for wireless sensor networks (wsn), with the aim of extending the mqtt protocol beyond the reach of tcp/ip infrastructure for sensor and actuator solutions.
针对适配传感装置(缩写为sa)的特定版mqtt协议,一般运行在嵌入式电池驱动的电子元件中,传输通过基于ieee 802.15.4规范无线低速网络构成的无线传感网络(wsn),同样具有企业级别特性具有以数据为核心的(data-centric)订阅/发布特性。
总之,针对低功耗、电池驱动、处理存储受限的设备、不支持tcp/ip协议栈网络的电子器件而定制,比如常见的zigbee(或xbee),对所依赖的底层传输网络不可知,但只要网络支持双向数据传输和网关,都是可以支持较为上层的mqtt-sn协议传输。比如简单数据报服务,只要支持一个源端点发送数据到一个特定目的地端点,这对支持mqtt-sn协议,就足够了。广播数据报传输服务也是必须的用于网关和终端的自动发现流程。为了降低广播风暴,mqtt-sn定义了广播路径深度(广播范围或广播半径)。
尽管mqtt-sn被设计成尽可能接近于mqtt,但那些低功耗、电池驱动、资源受限的设备所在网络场景为低速带宽、高连接失败、物理层数据包上线为128字节。文档提出了以下不同点:
在mqtt-sn架构图中,存在三种组件:
mqtt-sn网关传输方式,下面的图片一目了然。
网关需要抉择哪些消息需要和远程的mqtt server进行交互,比如只选择客户端发送的publish、subscrible消息等。
上面简单介绍了mqtt-sn,下面将会介绍mqtt-sn消息头部和格式。
以前看英文文章或资料,看完之后,摘要或者忘记。这一次选择感兴趣的mqtt 3.1.1介绍文章资料,引文见文末,作为练手;非完全翻译,去除掉一些广告性描述,若侵权,请告知。
在沉寂了四年之后,于2014年10月30号正式发布,与此同时mqtt 3.1.1已成为oasis(结构化信息标准促进组织)开放物联网消息传递协议标准( ),换种说法就是mqtt 3.1.1已升级为国际物联网标准。
正如http为人们通过万维网分享信息铺平了道路一样,mqtt能将几十亿低成本、嵌入式数据采集遥测设备连接到网络。
与mqtt 3.1(还不是国际物联网标准呢)规范相比,mqtt 3.1.1目标在于消除歧义,尽可能的向后兼容,事实上一些大众所需的新特性被包含在这个版本(更多的是物联网标准推动),因此不仅是一个维护版本,也是一种巨大的进步呢。除了概念的澄清和陈旧规范重写外,有一些很有趣的变化是值得注意的。
如果一个终端与服务器之间建立一个持久会话连接(假设这个终端没有使用到一个“clean session”标记清除已有回话标志), 一个新增的“session present”标志(会话表示标志,逻辑值为true或false)会在connack中出现,表明mqtt服务器已经拥有当前客户端上次连接会话信息,比如订阅的主题,排队信息和其它信息等。
会话表示标志若为true,客户端可减少了一次发送订阅subscrible交互步骤,有助于更有效的数据通信;为false,客户端需要再次发送订阅subscrible消息,不可略过。
mqtt 3.1.1之前,终端连接之后无法知道其发送的订阅主题是否被mqtt服务器接受与否。此新特性较适用于细粒度权限mqtt主题管理;若无授权,服务器会把错误代码(0x80)附加在suback中,客户端就可以知道订阅失败。
需要支持临时或匿名?客户端仅仅需要在发送connect时把客户端标识符( client identifier )置空(零长度)即可,mqtt服务器会为此类请求生成一个随机、唯一客户端标记符。但这要求客户端必须设置clean session标记为1,否则服务器端会直接返回包含0x02 (identifier rejected)代码的connack,同时关闭连接。
可用于后端程序(不需要维护回话状态)向终端发送消息的客户端,mqtt服务器程序可区别对待。
这是一个新增的特别有用的特性,客户端可以在发送connect之后,可无须等待mqtt服务器返回的connack,根据需要即刻发送publish、subscrible、disconneect等消息,可避免客户端资源等待。此特性也适用于突发模式(burst-mode)客户端需求,只关心数据要尽快的发送出去,而不是去担心是否需要维护一个长连接。
这需要mqtt服务器实现在分发消息之前检查客户端是否有权限发布到这些主题上。
mqtt 3.1针对客户端标识符( client identifier)限制是23个字节,实际环境下会有所不便,已有遗留系统可能使用uuid作为客户端的标识符,这样服务器端需要做一些彼此之间的map映射。 mqtt 3.1.1中上限为65535个字节,毕竟成为业界标准,需要兼容大量的遗留设备和基础设施。
当前mqtt 3.1.1已经在很多活跃开源项目/商业产品得到支持。比如eclipse paho,mosquitto,jboss a-mq 6.1, apache activemq 5.10-snapshot,apache camel 2.13.0,hivemq等。
关于eclipse paho:
包含mqtt 3.1.1和mqtt 3.1的客户端可以混合使用,彼此可以共存于同一个mqtt服务器下,在基本消息传输层面没有多大修改,同样的publish消息可以在mqtt客户端中自由流转,这个需要服务器端编码支持。
已有的mqtt 3.1客户端可以用着急升级,但升级之后可以从新增特性中收益良多。
mqtt 3.1协议在弱网络环境下(比如2g/3g等)表现不够好,因此才有了反思。
手机等终端在弱网络环境下丢包情况会非常明显,连接mqtt server成功率很低。相比单纯的请求-相应模型的http,其成功率会比mqtt订阅成功高很多。
手机终端在每次tcp断开或断网后,会即刻发起tcp重连,连接成功,会重复以前步骤依次发送连接命令(connect),订阅命令(subscrible),表明上看,这些过程没有任何问题,但问题就在于从终端成功建立到服务器的连接,到发送订阅命令,在弱网情况下,这个过程将会变得很昂贵:
从tcp建立开始的三次握手到完整的订阅命令发送完毕,考虑到tcp堆栈的每次接收数据方响应ack,这中间终端和服务器端至少产生了10次数据交互。
在网络变化频繁或者不太稳定的2g/3g网络环境下,这种过程显得有些冗长和不适应,同时会加重已经不堪的弱网络负载的负担。
弱网下,在任何一个阶段的执行过程中,都有可能产生突发性的网络中断的问题:
无法成功建立tcp链接,或死在三次握手期间,或数据包丢失在握手之后,或客户端连接超时过小
建立连接后,发送connect命令后,或没接收到tcp ack确认包,或客户端等待延时太小,导致订阅命令交互失败
发送subscrible命令后,但服务器端没收到,或因为丢包,或网络已断开,导致发送subscrible命令失败
成功发送subscrible命令后,或移动网络断开了(有些运营商针对认为http的请求有超时判断),或等待超时,导致订阅失败
tcp是无感知的虚拟连接,中间断开两端不会立刻得到通知,否则就用不着心跳保活机制了。
举一个例子,线上的服务器根据日志分析,只接收到连接命令(connect)但没有后续的订阅命令(subscrible)的情况,每天有上百万级别的数量。
总之,针对低速率弱网络环境,mqtt表现不怎么好。
业务改进点:
协议改进点:
有些建议看似冗余,批量或打包处理总比单个处理更高效一些、更节省资源,弱网络环境要求交互要尽可能的少,数据嘛要的是瞬间抵达,越快越好。
严格的分层和业务解耦,会导致性能问题。好比当前linux内核的tcp/ip网络堆栈分层很清晰,每一层都各司其职,但和直接略过内核态直接运行在用户态(user space)的packet i/o相比,处理性能不是在一个档次上,比如netmap 、dpdk等。
针对没有tcp/ip等网络堆栈支持的终端环境,mqtt爱莫能助了。
在一些类似于传感器电子元件中,资源十分受限,计算能力不足,嵌入tcp/ip网络堆栈不现实,比较好的方式基于ieee 802.15.4用于低速无线个人域网(lr-wpan)的物理层和媒体接入控制层规范之上发送udp数据包,每一个数据包最大128个字节。
mqtt-sn(mqtt for sensor networks)协议就是为了非常受限类似传感器而设的,协议流程架构比较有趣:
更多协议细节,有待进一步阅读。
先来算一下网络传输的字节数。
以太网帧头至少18个字节,ip头固定20个字节,tcp头20个字节(udp头部8个字节),再加上电信宽带计费的pppoe的8个字节:
udp可以比tcp节省12个字节。
mqtt-sn协议选择使用udp,可以看出其在节省资源方面的努力。
再看看弱网环境。
在网络不好的情况下,udp的时效性会好于tcp,tcp长连接中间交换过多、使之建立完整交互的过程成功率就很低。此种情况udp的低延迟和实时性呈现的结果会表现的很突出。
tcp或http理论上是可靠连接,但是在网络不好的时候,也不是那么可靠。客户端一般提交http请求之后,没有确认是否提交成功,在弱网环境下会产生丢包,服务器端嘛收不到。另tcp网络堆栈会存在数据包重发机制 应用层重发请求,可能会导致内核处理多次数据包的重发(还有拥塞窗口会收缩,发包速度减慢),可能会加重弱网络的负载。
和tcp相比,udp的无连接,代表了它快速,资源消耗小,突出表现就是延迟较小。至于数据包丢失没有重传,上层的业务层面应用协议/机制可以确保丢失的数据包重发或补发等,并且会更透明,安全的控性权。而tcp的包重发,上层应用没有控制权限。
连接协议方面:
总之,要实时性特诊,或者快速抵达终端的特性,不妨考虑一下udp。不过呢,很多时候udp和tcp大家会混合着使用,会互相弥补其不足。
若mqtt协议不能够满足业务需求,或许可考虑选择定制,或简化流程,或使用udp重新实现,或者使用tcp/http作为补充等,不一而足。
想想,还真有点小激动呢! ------ 《万万没想到》王大锤
mqtt协议诞生之初,就未曾考虑通过http传输。这也正常,网络受限、不稳定网络不太适合http(2g/3g网络大家使用wap不也ok嘛)。在网络较为充裕的桌面端而言,虽纯文本对比二进制而言没多大优势,受制于历史遗留和使用习惯,以及一大票传统基础设施方便控制事宜,传统互联网/企业型应用,http协议都是默认最佳选择,安全可控,人机友好。选择http也在情理之中。
虽桌面端日渐式微,但做统一的全平台化消息系统/消息中间件,也是趋势。
mqtt over http,为web环境提供http通道协议支持,在统一平台化这样的考量下,就显得合情合理。 mqtt相比其它基于http的交换协议而言(比如socket.io),流量节省,消息质量可控。
一句话,比它强大的,没有它轻量。比它还轻的,没有它可控。
总之,在资源受限环境下表现如此优异,那么桌面端会表现的更为优秀。
二进制支持,针对浏览器端javascript而言,处理起来,如同在石器时代要处理工业时代一些通讯方式,非常吃力。支持javascript二进制操作的浏览器现状:
来源于:
这和支持websocket的浏览器的基本重叠。
要想让http传输mqtt具体消息二进制,然后由浏览器javascript脚本进行解析,无法做到支持所有常见浏览器,需要考虑纯文本方式的传输。
ajax方式,支持跨域,支持所有主流平台,桌面 移动设备;所有浏览器,移动的,桌面的,包括ie6 。那么最理想方式就是jsonp,基于文本通信的long-polling jsonp方式。
mqtt 协议关键交互点: 1. 连接获取授权 1. 订阅/退订主题 1. 发布消息 1. 等待订阅消息 1. 关闭连接
若支持http方式完成以上功能/步骤,服务器端需要支持接收http纯文本内容请求,拼装、转换成java对象,业务层面实现数据流转、交换,直接插入到更具体业务中,这样就很容易。虽类似于桥接,但减少了桥接的路径(http —》mqtt),减少资源占用,性能上有所保证。
http 文本方式,和mqtt二进制之间需要某些规则的转换,为了兼容,需要单独定义接口传输接口,channelentry,双通道和单通道处理数据的方式不同,单通道的http jsonp需要支持短暂缓存消息并等待客户端的依次获取。发布/接收订阅消息时,tcp/websocket利用双通道对应channel可直接发送。
http通道已经预留出接口,便于支持其它类型的http传输通道,比如需要在非浏览器环境中实现常规的long polling/http streaming,仅仅需要做到实现相应接口即可。
客户端id的生成方式,一般是由服务器端生成的sessionid决定。传输纯文本方式比较结构化json结构比较合适。
jsonp只支持http get方式,这一点需要牢记。
要求返回的消息类型都是json字符串形式,订阅/发布的消息,一定要包含{id:10, msg:'具体消息内容'}类似json字符串。
一般传输的是文字内容,但具有结构化的,非json莫属。无论是走tcp方式二进制流还是jsonp传输的内容体,json都是不错的可结构化数据的选择。
浏览器端jquery,支持jsonp请求,这里有一个简单示范:
填写好地址,自动执行连接,订阅,接收消息,一个最简单的demo表现了其流程。
简单实现jsonp形式的mqtt over http,做到文本和二进制彼此之间交换数据。总之在纯http环境下使用mqtt协议,是一个不错的选择。
mqtt协议专注于网络、资源受限环境,建立之初不曾考虑web环境,倒也正常。虽然如此,但不代表它不适合html5环境。
html5 websocket是建立在tcp基础上的双通道通信,和tcp通信方式很类似,适用于web浏览器环境。虽然mqtt基因层面选择了tcp作为通信通道,但我们添加个编解码方式,mqtt over websocket也可以的。
这样做的好处,mqtt的使用范畴被扩展到html5、桌面端浏览器、移动端webapp、hybrid等,多了一些想像空间。这样看来,无论是移动端,还是web端,mqtt都会有自己的使用空间。
话说,现代化浏览器都已经支持websocket,这里有一个所有浏览器支持列表:
更详细列表,请直接访问:
毫无疑问,火狐和谷歌浏览器带动了现代浏览器的发展,对html5标准的支持也是如此。支持websocket的浏览器单纯从上面数字来讲,73.88%的支持率。但实际上还得参考浏览器市场占有率:
上图数据,来源于:
超过60%用户机器上浏览器的支持websocket,数据很可观。
针对不支持websocker的部分历史浏览器,可以考虑一下flash socket,虽然使用flashsocket用以模拟websocket就很容易理解,但条件如下: - 需要单独占用一个端口专用于安全跨域访问策略 - 需要浏览器支持二进制blob 支持二进制操作的浏览器现状:
来源于:
比较一下支持websocket和xhr2的桌面浏览器,重叠率很高,使用flash socket用以模拟websocket必要性不大,在类似于ie平台上,不如直接使用flash版本的
不是所有浏览器都支持websocket,尤其是阻碍历史发展的ie6/ie7/ie8/ie9。mqtt协议为二进制协议压根和http纯文本不兼容,尤其浏览器端javascript处理文本很合适,但二进制就显得笨手笨角,除非支持xhr2。
这部分后面专门会讲到。
现有一些凯发天生赢家一触即发官网的解决方案可能是后面为mqtt broker,前面是添加一层代理。比如:例如 ,对应在线示范:
表面上看着很解藕的,实际上模仿的还是传统型的短连接反向代理架构:nginx/apache +java/php/python/ruby。
客户端建立一条连接,服务器端需要使用到至少两个文件句柄,中间多了一层路径。优雅的凯发天生赢家一触即发官网的解决方案,可以向socket.io看起。一套服务端程序,同时提供若干种协议供终端选择。其实,一台mqtt broker中间件服务器,可以绑定多个端口,一个面向纯tcp的1883端口,一个面向websocket的80/8080端口,共享基础逻辑,面向不同协议。
服务器添加对websocket支持,基本不用做多大改动。对比tcp的附加到单个channel的处理器列表:
websocket对应单个channel的处理器列表:
为了支持websocket协议,仅仅额外增加了:
啰啰嗦嗦的讲了一大通websocket,总之对websocket的支持还算容易。后面有时间写写如何使用http协议达到mqtt over http的效果。
mqtt定义了物联网传输协议,其标准倾向于原始tcp实现。构建于tcp的上层协议堆栈,诸如http等,在空间上多了一些处理路径,稍微耗费了cpu和内存,虽看似微乎其微,但对很多处理能力不足的嵌入式设备而言,选择原始的tcp却是最好的选择。
但单纯tcp不是所有物件联网的最佳选择,提供构建与tcp基础之上的传统的http通信支持,尤其是浏览器、性能富裕的桌面涉及领域,还是企业最 可信赖、最可控的传输方式之一。支持多种多样的连接通道,让目前所有一切皆可联网,除了原始tcp socket,还要支持构建于其之上的http、html5 websocket,就很有必要。
mqtt.io,pub/sub中间件,也可以称之为推送服务器,涵盖所有主流桌面系统、浏览器平台,并且倾斜 于移动互联网,以及物联网的广阔适应天地。使用一句英文概括可能更为合适:"make everything connect”,让所有物件都可连接。其业务目标,可用下图概括:
mqtt.io致力于做下一代支持所有主流桌面平台、所有主流浏览器、所有可联网物件都可以联网的pub/sub消息推送系统。
构建此系统,在于降低传统企业各自分散的推送系统,统一运营,统一管理,节省人员、运维开支。
用于转换二进制流到java对象的过程:
对所有要写入网卡缓冲区的java对象转换成二进制:
借助于mqtt-library项目,编解码不复杂。
更具体的可以查看项目。
简单介绍了一个简单的不能再简单的mqtt server,只具有最基本的qos 0类型的消息订阅等。
后面,对html 5 websocket,会在现有基础代码之上,不做多大改动,增加对mqtt over websocket的支持。
前面的笔记已把所有消息类型都过了一遍,这里从消息流的角度尝试解读一下。
在任何网络环境下,都会出现一方连接失败,比如离开公司大门那一刻没有了wifi信号。但持续连接的另一端-服务器可能不能立即知道对方已断开。类似网络异常情况,都有可能在消息发送的过程中出现,消息发送出去,就丢失了。
mqtt协议假定客户端和服务器端稳定情况一般,彼此之通信管道不可靠,一旦客户端网络断开,情况就会很严重,很难恢复原状。
但别忘记,很多客户端会有永久性存储设备支持,比如闪存rom、存储卡等,在通信出现异常的情况下可以用于保存关键数据或状态信息等。
总之,异常网络情况很复杂,只能小心处理之。
在qos > 0情况下,publish、pubrel、subscribe、unsubscribe等类型消息在发送者发送完之后,需要等待一个响应消息,若在一个指定时间段内没有收到,发送者可能需要重试。重发的消息,要求dup标记要设置为1.
等待响应的超时应该在消息成功发送之后开始算起,并且等待超时应该是可以配置选项,以便在下一次重试的时候,适当加大。比如第一次重试超时10秒,下一次可能为20秒,再一次重试可能为60秒呢。当然,还要有一个重试次数限制的。
还有一种情况,客户端重新连接,但未在可变头部中设置clean session标记,但双方(客户端和服务器端)都应该重试先前未发送的动态消息(in-flight messages)。客户端不被强制要求发送未被确认的消息,但服务器端就得需要重发那些未被去确认的消息。
qos level为quality of service level的缩写,翻译成中文,服务质量等级。
mqtt 3.1协议在"4.1 quality of service levels and flows"章节中,仅仅讨论了客户端到服务器的发布流程,不太完整。因为决定消息到达率,能够提升发送质量的,应该是服务器发布publish消息到订阅者这一消息流方向。
至多发送一次,发送即丢弃。没有确认消息,也不知道对方是否收到。
client | message and direction | server |
---|---|---|
qos = 0 | publish ----------> | action: publish message to subscribers then forget reception: <=1 |
针对的消息不重要,丢失也无所谓。
网络层面,传输压力小。
所有qos level 1都要在可变头部中附加一个16位的消息id。
subscribe和unsubscribe消息使用qos level 1。
针对消息的发布,qos level 1,意味着消息至少被传输一次。
发送者若在一段时间内接收不到puback消息,发送者需要打开dub标记为1,然后重新发送publish消息。因此会导致接收方可能会收到两次publish消息。针对客户端发布消息到服务器的消息流:
client | message and direction | server |
---|---|---|
qos = 1 dup = 0 message id = x action: store message | publish ----------> | actions:
reception: >=1 |
action: discard message | puback <---------- | message id = x |
针对服务器发布到订阅者的消息流:
server | message and direction | subscriber |
---|---|---|
qos = 1 dup = 0 message id = x | publish ----------> | actions:
reception: >=1 |
puback <---------- | message id = x |
发布者(客户端/服务器)若因种种异常接收不到puback消息,会再次重新发送publish消息,同时设置dup标记为1。接收者以服务器为例,这可能会导致服务器收到重复消息,按照流程,broker(服务器)发布消息到订阅者(会导致订阅者接收到重复消息),然后发送一条puback确认消息到发布者。
在业务层面,或许可以弥补mqtt协议的不足之处:重试的消息id一定要一致接收方一定判断当前接收的消息id是否已经接受过
但一样不能够完全确保,消息一定到达了。
仅仅在publish类型消息中出现,要求在可变头部中要附加消息id。
级别高,通信压力稍大些,但确保了仅仅传输接收一次。
先看协议中流程图,client -> server方向,会有一个总体印象:
client | message and direction | server |
---|---|---|
qos = 2 dup = 0 message id = x action: store message | publish ----------> | action(a) store message or actions(b):
|
pubrec <---------- | message id = x | |
message id = x | pubrel ----------> | actions(a):
or action(b): delete message id |
action: discard message | pubcomp <---------- | message id = x |
server -> subscriber:
server | message and direction | subscriber |
---|---|---|
qos = 2 dup = 0 message id = x | publish ----------> | action: store message |
pubrec <---------- | message id = x | |
message id = x | pubrel ----------> | actions:
|
pubcomp <---------- | message id = x |
server端采取的方案a和b,都包含了何时消息有效,何时处理消息。两个方案二选一,server端自己决定。但无论死采取哪一种方式,都是在qos level 2协议范畴下,不受影响。若一方没有接收到对应的确认消息,会从最近一次需要确认的消息重试,以便整个(qos level 2)流程打通。
消息顺序会受许多因素的影响,但对于服务器程序,必须保证消息传递流程的每个阶段要和开始的顺序一致。例如,在qos level 2定义的消息流中,pubrel流必须和publish流具有相同的顺序发送:
client | message and direction | server |
---|---|---|
publish 1----------> publish 2 ----------> publish 3 ----------> | ||
pubrec 1<---------- pubrec 2 <---------- | ||
pubrel 1----------> | ||
pubrec 3<---------- | ||
pubrel 2----------> | ||
pubcomp 1<---------- | ||
pubrel 3----------> | ||
pubcomp 2<---------- pubcomp 3 <---------- |
流动消息(in-flight messages)数量允许有一个可保证的效果:
在mqtt协议中,publish消息固定头部retain标记,只有为1才要求服务器需要持久保存此消息,除非新的publish覆盖。
对于持久的、最新一条publish消息,服务器不但要发送给当前的订阅者,并且新的订阅者(new subscriber,同样需要订阅了此消息对应的topic name)会马上得到推送。
tip:新来乍到的订阅者,只会取出最新的一个retain flag = 1的消息推送,不是所有。
mqtt协议中,由目前定义的14种类型消息在客户端和服务器端之间数据进行交互。若以java语言构建mqtt服务器,可选择netty作为基础。
在netty中,数据的进入和流出,代表了一次完整的交互。无论是要进入的还是要流出的数据(单独以服务器为例),都可看做字节流。若把每种类型消息抽象为一个具体对象,那么处理起来就不难了。
客户端->服务器,进入的字节流,逐个字节/单位读取,可还原成一个具体的消息对象(解码的过程)。
要发送到客户端的消息对象,转换(编码)成字节流,然后由tcp通道流转到接收者。
断断续续记录了mqtt 3.1协议的若干阅读笔记,总之是把协议个人认为不够清晰,或者我不好理解的地方,着重进行了分析。也便于自己以后回过来头来翻阅,不是那么快的忘却。
这次要讲到客户端/服务器的发布消息行为,与publish相关的消息类型,会在这里看到。
客户端发布消息经由服务器分发到所有对应的订阅者那里。一个订阅者可以订阅若干个主题(topic name),但一个publish消息只能拥有一个主题。
消息架构一览:
description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
---|---|---|---|---|---|---|---|---|---|---|
fixed header/固定头部 | ||||||||||
byte 1 | message type(3) | dup flag | qos level | retain | ||||||
0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | |||
byte 2 | remaining length | |||||||||
variable header/可变头部 | ||||||||||
topic name | ||||||||||
byte 1 | length msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
byte 2 | length lsb (3) | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | |
byte 3 | 'a' (0x61) | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | |
byte 4 | '/' (0x2f) | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | |
byte 5 | 'b' (0x62) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | |
message identifier | ||||||||||
byte 6 | message id msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
byte 7 | message id lsb (10) | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | |
playload/消息体 | ||||||||||
blob,二进制对象形式。二进制具体包含的内容和格式,可有应用程序自身定义。若消息体为空(0长度)也是可能的。 |
dup flag,设为0,表示当前为第一次发送。
retain flag,只有在publish消息中才有效。
topic name,utf-8编码字符串形式,不支持通配符!
一般作为utf-8编码写入接口,但不排除自定义的消息格式。
空的消息体(zero-length)的publish消息也可以是合法的。
当服务器接收到空消息体(zero-length payload)、retain = 1、具有topic name的一个publish特殊消息,表示同时满足retain = 1、相同topic name的这两个特征的被持久化publish消息,可被删除。
固定头部qos level决定了消息中间件针对发布者具体需要响应的内容:
qos level | expected response |
qos 0 | none |
qos 1 | puback |
qos 2 | pubrec |
备注:仅仅针对发布publish消息的发布者。
无论是订阅者还是服务器接收到publish消息之后,需要根据qos level执行不同动作。
qos level | expected action |
qos 0 | 发送到所有感兴趣者 |
qos 1 | 持久化记录下来,发送到所有感兴趣的参与者,返回一个puback消息给发送者 |
qos 2 | 持久化记录下来,暂时不发送所有感兴趣的参与者,返回一个pubrec消息给发送者 |
如果服务器收到publish消息,参与者指的是订阅者。如果订阅者收到publish消息,参与者就是服务器。 需要注意:
未经授权的发布者提交的publish消息,服务器会忽略掉,客户端不会被通知。
作为订阅者/服务器接收(qos level = 1)publish消息之后对发送者的响应,整个消息不复杂。
description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|
fixed header/固定头部 | |||||||||
byte 1 | message type (4) | dup flag | qos flags | retain | |||||
0 | 1 | 0 | 0 | x | x | x | x | ||
byte 2 | remaining length (2) | ||||||||
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | ||
variable header/可变头部 | |||||||||
message identifier | |||||||||
byte 1 | message id msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | message id lsb (10) | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
虽没有消息体,但可变头部附加一个16位的无符号short类型。
字面意思为assured publish received,作为订阅者/服务器对qos level = 2的发布publish消息的发送方的响应,确认已经收到,为qos level = 2消息流的第二个消息。 和puback相比,除了消息类型不同外,其它都是一样。
description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|
fixed header/固定头部 | |||||||||
byte 1 | message type (5) | dup flag | qos flags | retain | |||||
0 | 1 | 0 | 1 | x | x | x | x | ||
byte 2 | remaining length (2) | ||||||||
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | ||
variable header/可变头部 | |||||||||
message identifier | |||||||||
byte 1 | message id msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | message id lsb (10) | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
无论是订阅者还是服务器,在消费pubrec消息之后需要发送一个pubrel消息给发送者(和pubrec具有同样的消息id),确认已收到。
qos level = 2的协议流的第三个消息,有publish消息的发布者发送,参与方接收。完整示范如下:
description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|
fixed header/固定头部 | |||||||||
byte 1 | message type (6) | dup flag | qos flags | retain | |||||
0 | 1 | 1 | 0 | 0 | 0 | 1 | x | ||
byte 2 | remaining length (2) | ||||||||
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | ||
variable header/可变头部 | |||||||||
message identifier | |||||||||
byte 1 | message id msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | message id lsb (10) | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
qos level 1,pubrel消息要求如此。
dup flag 为0,表示消息第一次被发送。
毫无疑问,剩余长度为2个byte长度。
可变头部中,消息id和发布者接收到的pubrec所包含的消息id是一致的。
作为qos level = 2消息流第四个,也是最后一个消息,由收到pubrel的一方向另一方做出的响应消息。
完整的消息一览,和pubrel一致,除了消息类型。
description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|
fixed header/固定头部 | |||||||||
byte 1 | message type (7) | dup flag | qos flags | retain | |||||
0 | 1 | 1 | 1 | x | x | x | x | ||
byte 2 | remaining length (2) | ||||||||
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | ||
variable header/可变头部 | |||||||||
message identifier | |||||||||
byte 1 | message id msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | message id lsb (10) | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
当客户端接收一个pubcomp消息时,客户端摒弃原来的消息,因为它已经成功发送消息到服务器。
消息的发布和确认等一些流程,主要是跟消息发布者所设定的qos level有关;稍加整理,绘制了下面一张图,理解起来可能会更清晰些:
上图针对的是客户端发布消息到服务器端的方向。
为了确保消息已经成功传递过去,只有收到了确认,才会让人特别放心。
在qos level = 2时,通信双方都需要知道各自的确认流程以及所处阶段等,交互很多,数据量大的情况下,可能会造成数据线路传递拥塞。服务器选择qos = 0/1,大部分情况都是可以应对的。 比如重要消息,就要确保对方都要收到,然后彼此确认,ok,这个消息是真实、有效的。
无论qos level为0、1,还是2,服务器(具备所有条件都满足之后)总要把收到的具体内容和topic组装成一个新的publish message(也不一定要重新构造,但要求推送的publish消息,一定要具有明确的主题和内容,retain标志不能设置为1)推送到所有感兴趣的订阅者。
嗯,消息的发布来源别忘记还有可能来自connect消息中的will topic和will message,若是设置了will flag标记的话。