MQTT-SN的一个重要设计原则是尽可能与MQTT相近。因此,所有的协议语义应保持尽可能与MQTT中定义的一致。接下来我们将聚焦于那些新的或偏离MQTT的地方。
6.1 网关通告与发现
这是一个全新的流程,MQTT中没有此流程。
网关通过周期性发送ADVERTISE消息来向当前网络中所有设备宣告自身的存在。网关应该只当自身连接到服务端时(或本身就是服务端时)才通告自身的存在。
同一网络中,同时可能激活多个网关。在这种情况下,它们将会有不同的网关ID。此时,将由客户端决定连接到哪个网关。但是,在任何时候,一个客户端只能允许连接到一个网关。
客户端应将网关与其网络地址维护在一个活动网关列表内。通过接收ADVERTISE消息和GWINFO消息来将新活动网关添加到列表中。
网关下一次发送ADVERTISE消息的持续时长TADV已在ADVERTISE消息中的Duration字段指定。客户端可以使用此信息来监控网关是否可用。例如,当连续NADV次未收到网关发来的ADVERTISE消息时,可以假定网关已经离线,并将其移出活动网关列表。同理,处于备用模式的网关将会激活,当它们几次丢失某一网关的通告时。
因为ADVERTISE消息将会在整个无线网络中广播,所以两次ADVERTISE消息间隔TADV应该足够长(例如大于15分钟),以避免造成网络带宽拥堵。
大的TADV值会导致新客户端在查找网关时花费较长时间等待网关广播。客户端可以通过广播SEARCHGW消息来缩短等待时间。为了防止多个客户端同时查找网关引起的广播风暴,SEARCHGW消息需要随机延迟0到TSEARCHGW发送。在延迟过程中,如果客户端接收到其它客户端发出的和它相同的SEARCHGW消息时,客户端应该取消自身SEARCHGW消息的发送,因为该SEARCHGW消息无论谁发送产生的效果都是相同的。
SEARCHGW消息的广播半径Rb是受限的,如假设在MQTT-SN客户端密集部署情况下,只会有一跳。
根据收到的SEARCHGW消息,网关回复一包含自身网关ID信息的GWINFO消息。类似的,客户端在其活动网关列表中至少有一个活动网关时,回复一GWINFO消息。客户端的列表中如果有多个活动网关,则选择其中一个的信息放到GWINFO消息中。
和SEARCHGW消息一样,GWINFO消息以SEARCHGW消息中的广播半径Rb广播。当这两种消息传输时,广播半径Rb同时也传给底层网络。
为了使网关优先,客户端将会随机延迟TGWINFO再发送GWINFO消息。如果在延迟过程中,客户端收到了GWINFO消息,则取消自身GWINFO消息的发送。
假如没有回应,则SEARCHGW消息将会重发。连续重发间隔以指数形式增长。
6.2 客户端连接的建立
和MQTT一样,在MQTT-SN客户端可以同网关交换信息前,客户端必须与网关建立连接。同网关建立连接的流程如图3所示。图3流程假定客户端要求网关发起遗嘱功能流程。通过置位CONNECT消息中的标志位的遗嘱功能指明。客户端根据接收到的相应WILLTOPICREQ、WILLMSGREQ消息发送这两段信息给网关。建立连接流程止于收到网关发送的CONNACK消息。
如果遗嘱功能标志未置位,网关将直接回复CONNACK消息。
万一网关无法接受连接请求(比如拥塞或不支持CONNECT消息中指定的特性),网关将会返回包含拒绝原因在内的CONNACK消息。
6.3 清理会话
在MQTT中,当客户端断开连接时,其在服务端的订阅信息不会被删除。它们会被持久化并在重连时可用,除非客户端显式退订或客户端以“clean session”标志符置位方式建立连接。
在MQTT-SN中,“clean session”的范围被扩展到遗嘱功能中,如,不仅订阅信息被保存,遗嘱主题及遗嘱内容也被保存。CONNECT消息中的“CleanSession”“Will”标志符意义如下:
- CleanSession=true, Will=true:网关将会删除客户端关联的订阅信息和遗嘱信息,然后发起遗嘱功能流程;
- CleanSession=true, Will=false:网关将会删除客户端关联的订阅信息和遗嘱信息,然后回复CONNACK消息(不会发起遗嘱功能流程);
- CleanSession=false, Will=true:网关将会保存客户端的所有数据,然后发起遗嘱功能流程,新的遗嘱数据将会覆盖已存储的数据;
- CleanSession=false, Will=false:网关将会保存客户端的所有数据,然后回复CONNACK消息(不会发起遗嘱功能流程)。
注意,当客户端在建立连接时仅想删除遗嘱数据,它可以发送一个“CleanSession=false, Will=true”的CONNECT消息,然后当网关要求遗嘱主题时发送空的WILLTOPIC消息给网关。也可以发送“CleanSession=false, Will=false”的CONNECT消息,然后使用6.4节介绍的流程来删除或修改遗嘱数据。
6.4 遗嘱数据更新流程
在连接生命期内,客户端随时可通过发送WILLTOPICUPD或WILLMSGUPD消息来更新存储在网关的遗嘱数据。这两种消息内的数据将会覆盖网关中存储的相应数据。网关都会响应这两消息。这两种消息可以单独使用。
注意,空的WILLTOPICUPD消息将会同时删除存储在网关的遗嘱主题和遗嘱消息。
6.5 主题名注册流程
由于无线传感网络带宽有限、消息载荷小,数据无法像MQTT一样和主题名一起分发。注册流程被引入来在开始使用短主题ID发送PUBLISH消息前,允许客户端和网关互相告知短主题ID和对应的主题名。
客户端向网关发送REGISTER消息来注册主题名。如果注册请求能接受,网关会为收到的主题名生成topicId,并放到REGACK消息内返回给客户端。如果不能处理注册请求,失败原因会放入REGACK的ReturnCode字段并返回给客户端。
当收到ReturnCode=“accepted”的REGACK消息后,客户端应该使用消息内的topicId来发布对应主题名的数据。如果REGACK消息包含有拒绝码,客户端可以稍候再尝试注册。如果返回码是“rejected: congestion”,客户端在开始注册流程前应等待TWAIT。
在任何时候,客户端只能有一个处理中的REGISTER消息,例如,在开始注册另一个主题名之前,客户端必须等待本次注册的REGACK消息。
网关发送REGISTER消息来告知客户端主题名和相应的主题ID,网关接下来将使用该主题ID发送PUBLISH消息。这会发生在客户端以未设置“CleanSession”标志位形式重连或客户端订阅的主题名包含通配符(#、+等)的情况下。
6.6 客户端发布流程
当成功向网关注册主题名后,客户端可以开始通过向网关发送PUBLISH消息来发布该主题相关的数据。PUBLISH消息内包含相应的主题ID。
MQTT-SN支持MQTT中定义的所有三种质量等级及相应的消息流程。唯一的不同点是MQTT-SN使用主题ID替代PUBLISH消息中的主题名。
无论何种质量等级的PUBLISH消息请求,客户端都可能收到包含下列值的PUBACK回复消息:
- ReturnCode=“Rejection: invalid topic Id”:此种情况下,客户需要再次注册该主题名;
- ReturnCode=“Rejection: congestion”:此种情况下,客户需要停止至少TWAIT再向网关发送消息。
在任何时候,客户端只能有一个处理中的质量等级为1或2的PUBLISH消息,例如,在开始新的质量等级为1或2的传输前,客户端必须等待本次PUBLISH消息交换完成。
6.7 预定义主题ID和短主题名
如6.5节所述,主题ID为2字节长,用于替换字符串形式的主题名。客户端须要使用注册流程来告知网关它想要使用的主题名,并从网关得到相应的主题ID。然后,客户端将会使用该主题ID来向网关发送PUBLISH消息。反过来,网关发送的PUBLISH消息同样也包含一个2字节的主题ID(替代字符串形式的主题名)。订阅流程和网关发起的注册流程会告知客户端主题ID和主题名间的关系。
“预定义”主题ID是一种客户端应用和网关都知道对应主题名的主题ID。使用PUBLISH消息的Flags字段来标明它。当使用预定义主题ID时,双方可以立即开始发送PUBLISH消息,而无须像“普通”主题ID一样需要执行注册流程。当接收到PUBLISH消息包含的预定义主题ID对应的主题名未知时,接收方应该返回ReturnCode=“Rejection: invalid topic Id”的PUBACK消息。注意,此错误无法像普通主题ID一样通过重新注册来修复。
客户端如果想接收预定义主题ID相关的PUBLISH消息,仍需订阅该主题ID。为了避免混淆预定义主题ID和2字节长的短主题名,SUBSCRIBE消息包含一个标志符来表明是订阅短主题名还是预定义主题ID。
短主题名是一种固定2字节长度的主题名。它可以和数据一起放到PUBLISH消息中,因此,短主题名无须注册流程。另外,普通主题名的所有规则同样适用短主题名。注意,以通配方式订阅短主题名是无意义的,因为仅使用两个字符来分层定义一个有意义的名称是不太可能的。
6.8 发布质量等级为-1的消息
这个特性定义给非常简单的客户端实现,它们除了此特性,不再支持其它特性。它们不用建立连接,也不用关闭连接,不用注册流程,也不用订阅流程。客户端就只是向网关(客户端预先知道网关地址)发送PUBLISH消息,然后丢弃它们。它并不关心网关地址是否正确、网关是否在线或者网关是否接收到那些消息。
只有底下的值被允许用于质量等级为-1的PUBLISH消息:
- QoS标志:置为“0b11”;
- TopicIdType标志:预定义主题ID置“0b01”,短主题名置“0b10”;
- TopicId字段:预定义主题ID或短主题名;
- Date字段:要发布的数据。
6.9 客户端主题订阅/退订流程
为订阅某主题名,客户向网关发送SUBSCRIBE消息,同时将主题名包含在消息中。如果网关接受订阅请求,则为收到的主题名生成主题ID并放入SUBACK消息返回给客户端。如果订阅有误,则将拒绝原因写入SUBACK消息的ReturnCode字段,并返回给客户端。如果拒绝原因是“rejected: congestion”,则客户端应等待TWAIT后,再发送SUBSCRIBE消息。
如果客户端订阅的主题名包含通配符,返回的SUBACK消息的主题ID值将为0x0000。当网关第一次向客户端匹配的主题名发送PUBLISH消息时,会使用注册流程来告知客户端即将使用的主题ID,参见6.10节。
类似客户端的发布流程,主题ID同样可以为某一主题名的预定义ID。短主题同样也可以用。这两种情况下,客户端同样需要订阅预定义主题ID或短主题名。
客户端发送UNSUBRSCRIBE消息向网关退订主题,将会收到UNSUBACK消息回复。
同样的,客户端同时只能有一个SUBSCRIBE或UNSUBCRIBE事务。
6.10 网关发布流程
类似6.6节所述客户端发布流程,网关发送包含主题ID的PUBLISH消息,然后接收客户端返回的SUBACK消息。
在发送PUBLISH消息前,网关可能发送REGISTER消息向客户端通告主题名和相应的主题ID。这会发生在客户端以未设置“CleanSession”标志位形式重连或客户端订阅的主题名包含通配符的情况下。客户端将根据接收到的REGISTER消息来回复REGACK消息。在向客户端发送PUBLISH消息前,网关会一直等待REGACK消息。
客户端可以使用REGACK消息来拒绝REGISTER消息,并在消息中指明拒绝原因,此方式相当于在网关退订该主题。注意,通配主题名的退订只能在6.9节描述的退订流程执行,不能通过拒绝REGISTER消息实现,因为REGISTER消息从不包含通配主题名。
如果客户端收到未知主题ID的PUBLISH消息,它应该回复ReturnCode=“Rejected: invalid Topic ID”的PUBACK消息。这会触使网关删除或纠正错误的主题ID分配值。
注意,当主题名或数据太长,以至于无法装入REGISTER或PUBLISH消息时,网关会静默中止发布流程,例如,不会向受到影响的订阅者发送警告。
6.11 心跳保活流程
和MQTT一样,心跳值在CONNECT消息中指定。客户端应在每一个心跳周期发送PINGREQ消息,网关发送PINGRESP消息来回应。
同理,当客户端接收到当前连接的网关发来的PINGREQ消息时,应回复PINGRESP消息给网关。否则忽略接收到的PINGREQ消息。
客户端应用使用此流程来监督它们在网关上的活力。如果客户端多次重发PINGREQ消息后,仍无法接收到网关发回的PINGRESP消息,则在尝试重连此网关之前,应首先尝试连接到其他网关(参见6.13节)。注意,因为客户端的心跳计时不是同步的,因此假如某一网关坏了,实际上不会有受影响的客户端几乎同时转向新网关而产生CONNECT消息风暴的危险。
6.12 客户端断开流程
客户端向网关发送DISCONNECT消息来声明意图关闭连接。在此之后,客户端要再次同网关交换信息,必须先同网关建立一个新的连接。类似MQTT,即便CleanSession标志符有置位,发送DISCONNECT消息也不会影响已经存在的订阅信息和遗嘱数据。它们会一直存在,直到客户端显式退订、删除、更新,或者客户端以CleanSession标志符置位方式建立新的连接。网关对收到的DISCONNECT消息回复一DISCONNECT消息给客户端。
客户端也可能收到网关主动发来的DISCONNECT消息。这有可能发生在网关因为某些错误无法确定接收到的消息所属的客户端时。客户端接收到这样的DISCONNECT消息时,应该再次发送CONNECT消息来和网关重建连接。
6.13 客户端重发流程
向网关发送的所有消息都采用“单播”方式(例如使用网关的单播地址发送而不是广播),使用重试间隔Tretry和重试次数Nretry来监控网关的预期回复。客户端发送消息后启动重试计时Tretry,当收到网关的回复后停止计时。如果计时时间到了,但还未收到网关的回复,客户端重发此消息。在Nretry次重发后,客户端中止此流程,并可以认为到网关的MQTT-SN连接已经断开了。此时客户端应该尝试连接到其他网关,只有在其他网关都连接失败时才再次重连此网关。
6.14 休眠客户端支持
休眠客户端指那些驻留在想尽可能节约能源的设备(电池驱动)上的客户端。这些设备每当未激活时就会进入休眠模式,每当需要发送数据或接收到数据时又会被唤醒。服务端/网关需要知道客户端的休眠状态,然后为它们缓存消息,并在唤醒时发送给它们。
如图4所示,在服务端/网关看来,客户端可能处于以下的几种状态:激活(active)、休眠(asleep)、唤醒(awake)、断开(disconnected)、丢失(lost)。当服务端/网关收到客户端发来的CONNECT消息时,此时客户端处于active状态,如6.2节所述。服务端/网关通过6.11节所述的心跳计时来监控此状态。如果超过心跳时长(在CONNECT消息中指定)服务端/网关未收到客户端发来的任何消息,网关将认为客户端已经lost,然后为该客户端启动遗嘱等功能。
当服务端/网关收到无duration字段的DISCONNECT消息时,客户端转入disconnected状态。此状态不受服务端/网关的计时监管。
如果客户端想要休眠,可以发送包含休眠时长的DISCONNECT消息。服务端/网关回复一个DISCONNECT消息,然后认为客户端已经转入asleep状态,参见图5。服务端/网关使用休眠时长来监控客户的asleep状态。如果超过休眠时长服务端/网关未收到客户端发来的任何消息,服务端/网关将认为客户端已经lost,然后和心跳流程一样为该客户端启动遗嘱等功能。客户端处于asleep状态时,服务端/网关需要缓存发给该客户端的所有消息。
当服务端/网关收到客户端发来的PINGREQ消息时,停止休眠计时。如同CONNECT消息,PINGREQ消息也包含Client Id。相应的客户端将变成awake状态。如果服务端/网关有该客户端的缓存消息,此时将会发送给客户端。和客户端的消息传输将在服务端/网关发送PINGRESP消息时关闭,发送PINGRESP消息后,服务端/网关将认为客户端进入asleep状态,重启休眠计时等。
如果服务端/网关没有客户端的缓存消息,则立即回复PINGRESP消息,使客户端回到asleep状态,重启该客户端的休眠计时。
当向服务端/网关发送PINGREQ消息后,客户端使用6.13节所述的“重发流程”来监督服务端/网关发回消息,例如,当它接收到PINGRESP以外的消息时重启Tretry计时,当接收到PINGRESP时停止计时。当Tretry超时,重发PINGREQ消息,并重启Tretry计时。为了避免过多重发PINGREQ消息(例如网关丢失时)而耗尽电池,客户端应该限制PINGREQ消息的重发(例如使用重试计数),当达到限制条件时转回休眠,此时将不会收到PINGRESP消息。
客户端处于asleep或awake状态时,可以通过发送CONNECT消息进入active状态,也可以通过发送普通DISCONNECT消息(无duration字段等)进入disconnected状态。客户端可以通过发送包含新的休眠时长值的DISONNECT消息来修改休眠时长。
注意,休眠客户端应该只在仅想确认服务端/网关是否有缓存消息时才进入awake状态,而后尽快切回asleep状态,此外,不向服务端/网关发送任何消息。否则,客户端应向服务端/网关发送CONNECT消息回到active状态。