MQTT学习笔记

197 阅读20分钟

大部分资料都来源于www.emqx.com/zh/mqtt-gui…

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。

MQTT最大优点在于,用极少的代码有限的带宽,为连接远程设备提供实时可靠的消息服务。

作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

一、诞生

实现数千英里长的石油和天然气管道的无人值守监控,这种应用场景有如下几个特点:

  • 石油天然气管道线路非常长,要接许多沿线的数据采集网关;服务器要能接成千上万个通信客户端
  • 石油管道传感器的数据采集频率不高,不需要一下子传输大量数据
  • 现场采集网关由于量大,考虑到采购成本,CPU和存储等计算资源都很有限
  • 石油管道会穿越很多无人区,附近没有网络设施,因此使用卫星通讯最为经济;
  • 高轨道的GEO卫星站得高看得远,覆盖范围广,但轨道高延迟就大了。中低轨道的LEO/MEO卫星延迟小,但是覆盖区域有限,每天都会出现卫星切换时的网络中断。因此需要客户端和服务器端都能够保留消息收发状态,在网络恢复正常后继续发送
  • 卫星链路带宽低(当然也有高带宽的),通信流量费用高昂;因此需要尽量节省传输的消息的流量开销
  • 有些数据发送失败,不需要重发。但是有些消息比如阀门泄露告警或控制石油管道阀门的命令,就必须要在网络有问题的情况下也要能确保发送成功

二、一些基础信息

  1. 协议层级:MQTT是应用层协议;
  2. 协议定位:MQTT则是在低带宽高延迟不可靠的网络下进行数据相对可靠传输的应用层协议。
  3. 传输单位:MQTT的传输单位是消息,每条消息字节上限在MQTT Broker代理服务器上进行设置,可以设置超过1M大小的消息上限
  4. 解决的问题:在低带宽高延迟不可靠的网络下资源有限的硬件环境内,进行相对可靠的数据传输。
  5. 服务质量:MQTT提供三种可选的消息发布的QoS服务等级。MQTT客户端和MQTT代理服务器通过session机制保证消息的传输可靠性。

三、协议特点

MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT 与 HTTP 一样,MQTT 运行在传输控制协议/互联网协议 (TCP/IP) 之上。简单来说,整个流程就是,【MQTT客户端】发布某个主题消息到【MQTT broker】上,其负责转发给订阅了该主题的所有【客户端】。

  1. MQTT客户端

一般指的就是运行客户端程序的设备们,比如物联网环境中的各种硬件设备。

  1. MQTT Broker

是一个中转站。处理客户端请求,包括建立连接、断开连接、订阅和取消订阅等操作,同时还负责消息的转发。

  1. 发布和订阅

通常情况下,客户端的角色是发布者和订阅者,服务器的角色是代理,但实际上,服务器也可能主动发布消息或者订阅主题,客串一下客户端的角色.在 MQTT 中,主题和订阅无法被提前注册或创建,所以代理也无法预知某一个主题之后是否会有订阅者,以及会有多少订阅者,所以只能将消息转发给当前的订阅者,如果当前不存在任何订阅,那么消息将被直接丢弃。

  1. 主题

topic通过/来区分层级,类似URL路径。其中有通配符+#,前者单层, 后者多层。具体如下,发布三个topic,分别为"a/b","a/c",和"a/b/c",一个客户端订阅"a/+",一个客户端订阅"a/#".

// 发布topic
mqtt::message_ptr pubmsg_x = mqtt::make_message("a/b", message);
mqtt::message_ptr pubmsg_y = mqtt::make_message("a/c", message);
mqtt::message_ptr pubmsg_z = mqtt::make_message("a/b/c", message);

// 订阅topic  Message arrived: topic: a/b   topic: a/c
const std::string topic = "a/+";
client.subscribe(topic, 1)->wait();

// 订阅topic  Message arrived: topic: a/b   topic: a/c   topic: a/b/c
const std::string topic = "a/#";
client.subscribe(topic, 1)->wait();

主要是给不同的设备用的,比如传感器1和传感器2的topic就可以是:sensor/1/datasensor/2/data;接收方就去订阅这两个topic。但是缺点就是用通配符能自己收到自己的topic。

一般来说,大多数发布/订阅系统主要通过以下两种方式过滤并路由消息。

  • 根据主题:订阅者向代理订阅自己感兴趣的主题,发布者发布的所有消息中都会包含自己的主题,代理根据消息的主题判断需要将消息转发给哪些订阅者。
  • 根据消息内容:订阅者定义其感兴趣的消息的条件,只有当消息的属性或内容满足订阅者定义的条件时,消息才会被投递到该订阅者。
  1. 消息质量(Qos)

MQTT提供三种质量的服务:

  1. 至多一次QoS 0:可能会出现丢包的现象。使用在对实时性要求不高的情况。这一级别可应用于如下情景,如环境传感器数据,丢失一次读记录无所谓,因为很快下一次读记录就会产生。
  • 至少一次QoS 1:保证包会到达目的地,但是可能出现重包。
  • 正好一次QoS 2:保证包会到达目的地,且不会出现重包的现象。这一级别可用于如计费系统等场景,在计费系统中,消息丢失或重复可能会导致生成错误的费用。
  1. 一些名词

    1. 订阅(Subscription): 订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
    2. 会话(Session): 每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
    3. 主题名(Topic Name): 连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。
    4. 主题筛选器(Topic Filter): 一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。
    5. 负载(Payload): 消息订阅者所具体接收的内容。

四、MQTT数据结构

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

    • 消息类型4bit;
    • DUP 1bit;保证消息可靠传输的一个类型,1的话要接收端回复
    • Qos质量2bit
    • 发布保留标识,表示服务器要保留这次推送的信息,如果有新的订阅者出现,就把这消息推送给它,如果设有那么推送至当前订阅者后释放
    • 剩余长度:用来保存变长头部和消息体的总大小,但不是直接保存的。这一字节是可以扩展,前7位用于保存长度,后一部用做标识。当最后一位为 1时,表示长度不足,需要使用二个字节继续保存。
  • 可变头(Variable header),存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容;

  • 消息体(Payload),存在于部分MQTT数据包中,表示客户端收到的具体内容;比如:

    • CONNECT,消息体内容主要是:客户端的ClientID、订阅的Topic、Message以及用户名和密码。
    • SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS。
    • SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。
    • UNSUBSCRIBE,消息体内容是要订阅的主题。

剖析一个:

MQTT的具体数据为:10 0f 00 04 4d 51 54 54 04 02 00 14 00 03 63 5f 31

0x10:消息类型1,DUP、Qos等级和RET都是0;0x0f:剩余长度是15。上图wireshark的图片中也显示了后面是什么意思,协议名长度,协议名,协议版本,是否要清除会话,保活时长,以及客户端ID相关内容。

五、MQTT的连接

  1. 基本概念

MQTT 连接由客户端向服务器端发起,而MQTT服务器则负责接收客户端发起的连接,并将客户端发送的消息转发到另外一些符合条件的客户端。

客户端与服务器建立网络连接后,需要先发送一个 CONNECT 数据包给服务器。服务器收到 CONNECT 包后会回复一个 CONNACK 给客户端,客户端收到 CONNACK 包后表示 MQTT 连接建立成功。如果客户端在超时时间内未收到服务器的 CONNACK 数据包,就会主动关闭连接。

  1. 连接参数

  1. 连接地址:服务器 IP 或者域名、服务器端口、连接协议
  2. 客户端ID:MQTT 服务器使用 Client ID 识别客户端,连接到服务器的每个客户端都必须要有唯一的 Client ID。如果客户端使用一个重复的 Client ID 连接至服务器,将会把已使用该 Client ID 连接成功的客户端踢下线。 下图就是使用相同的客户端ID登录,前者被踢下线了。

  1. 用户名与密码:MQTT 协议可以通过用户名和密码来进行相关的认证和授权,但是如果此信息未加密,则用户名和密码将以明文方式传输。大多数 MQTT 服务器默认为匿名认证,匿名认证时用户名与密码设置为空字符串即可

  2. 连接超时:连接超时时长,收到服务器连接确认前的等待时间,等待时间内未收到连接确认则为连接失败。

  3. 保活周期:以秒为单位。无报文发送时,将按 Keep Alive 设定的值定时向服务端发送心跳报文,确保连接不被服务端断开。服务器没有在 Keep Alive 的 1.5 倍时间内收到来自客户端的任何包,断开和客户端的连接。

  4. 清除会话

    1. false 时表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。为 true 时表示创建一个新的临时会话,在客户端断开时,会话自动销毁。
    2. 持久会话避免了客户端掉线重连后消息的丢失,并且免去了客户端连接后重复的订阅开销。这一功能在带宽小,网络不稳定的物联网场景中非常实用。
    3. 服务器为持久会话保存的消息数量取决于服务器的配置,比如 EMQ 提供的离线消息保存时间为 5 分钟,最大消息数为 1000 条,且不保存 QoS 0 消息(至多1次)。
    4. 实验,设置不清除会话:conn_opts.set_clean_session(false)
  5. 遗嘱消息:MQTT 为那些可能出现意外断线的设备提供的将遗嘱优雅地发送给其他客户端的能力。设置了遗嘱消息消息的 MQTT 客户端异常下线时,MQTT 服务器会发布该客户端设置的遗嘱消息。

std::string die_topic = "die";
mqtt::will_options will_opts;
will_opts.set_topic(die_topic);
will_opts.set_payload(client_id + " client die");
conn_opts.set_will(will_opts);

下图就是在客户端c_1掉线了,c_2收到了遗嘱消息

  1. 在5.0的协议中,把清楚会话拆分成了Clean Start:true时丢弃任何会话建立新的,false是必须使用和clientID关联的会话恢复;Session Expiry Interval:用于指定网络连接断开后会话的过期时间

六、会话相关

  1. 为什么需要会话?

在物联网场景中,设备可能因为网络问题或者电源问题而频繁地断开连接。如果客户端和服务端总是以全新的上下文建立连接,那么将带来以下几个问题:

  1. 客户端在重连后必须重新订阅主题才能继续接收消息,这会给服务器带来额外的开销。 (这个其实是引入MQTT导致的)
  2. 客户端将会错过离线期间的消息。(有很多想发给它的信息,因为离线没有发给他,等他上线再发)
  3. QoS 1 和 QoS 2 的服务质量将无法得到保证。(至少发1次和只发一次)
  1. 什么是 MQTT 会话?

本质上就是一组需要服务端和客户端额外存储的上下文数据,这些数据可以仅持续与网络连接一样长的时间,也可以跨越多个连续的网络连接存在。当客户端与服务端借助这些会话数据恢复通信时,可以让网络中断就像从未发生过一样。

MQTT 为服务端和客户端分别定义了它们需要存储的会话状态。对于 服务端 来说,它需要存储以下内容:

  1. 会话是否存在。
  2. 客户端的订阅列表。
  3. 已发送给客户端,但是还没有完成确认的 QoS 1 和 QoS 2 消息。
  4. 等待传输给客户端的 QoS 0 消息(可选),QoS 1 和 QoS 2 消息。
  5. 从客户端收到的,但是还没有完成确认的 QoS 2 消息。
  6. 遗嘱消息与遗嘱过期间隔
  7. 会话过期时间。

对于 客户端 来说,它需要存储以下内容:

  1. 已发送给服务端,但是还没有完成确认的 QoS 1 和 QoS 2 消息。
  2. 从服务端收到的,但是还没有完成确认的 QoS 2 消息。

让服务端和客户端永久存储这些会话数据(存储在内存吧? ),不仅会带来很多额外的存储成本,而且在很多场景中也没有必要。譬如我们只是为了避免网络连接短暂中断导致的消息丢失,那么一般将会话数据设置为在连接断开后保留短暂的几分钟即可。

会话的实验在 【五、6】 里面有做清除会话开启与否的实验,如果没有开启,那收不到离线阶段订阅的topic,如果开启,上线阶段就会收到,类似于补发。

七、QoS相关

  1. QoS 0:至多1次

QoS 0 的缺点是可能会丢失消息,消息的可靠性完全依赖于底层的 TCP 协议,一旦出现连接关闭、重置,仍有可能丢失当前处于网络链路或操作系统底层缓冲区中的消息。不过优点是投递的效率较高。

传输一些高频且不那么重要的数据,比如 传感器 数据,周期性更新,即使遗漏几个周期的数据也可以接受。

  1. QoS 1 - 至少交付一次

为了保证消息到达,QoS 1 加入了应答与重传机制,发送方只有在收到接收方的 PUBACK 报文以后,才能认为消息投递成功,在此之前,发送方需要存储该 PUBLISH 报文以便下次重传。

QoS 1 需要在 PUBLISH 报文中设置 Packet ID,而作为响应的 PUBACK 报文,则会使用与 PUBLISH 报文相同的 Packet ID,以便发送方收到后删除正确的 PUBLISH 报文缓存。

QoS 1可以保证消息到达,所以适合传输一些较为重要的数据,比如下达关键指令、更新重要的有实时性要求的状态等。

下图就是发生重复的情况

去重手段:比如消息流水号,比如带上时间戳,根据当前收到消息中的时间戳或计数是否大于自己上一次接收的消息中的时间戳或计数来判断这是否是一个新消息。

协议层面其实有个字段来标识是否是重复报文,但是Qos 1无法避免重复的原因是:

发送方在确定自己的包收到broker的puback后,未来会有一个数据使用相同的PacketID。这样,假如收到某一包带有重发标识的消息,协议层并不能根据PacketID和重发标识来区分出该消息是否曾经收到过,QoS 2通过加入接收方释放PacketID的流程,解决了这个问题,但是开销变大了。

  1. QoS 2 - 只交付一次

QoS 2 解决了 QoS 0、1 消息可能丢失或者重复的问题,但相应地,它也带来了最复杂的交互流程和最高的开销。其复杂流程发生的主要原因是PacketID是循环使用的,只有在接收方确认释放了该PacketID,发送方才能重新启用。

对于接收方来说,能够以 PUBREL 报文为界限,PacketID相同,凡是在 PUBREL 报文之前到达的 PUBLISH 报文,都必然是重复的消息;而凡是在 PUBREL 报文之后到达的 PUBLISH 报文,都必然是全新的消息。

如果我们不愿意自行实现去重方案,并且能够接受 QoS 2 带来的额外开销,那么 QoS 2 将是一个合适的选择。通常我们会在金融、航空等行业场景下会更多地见到 QoS 2 的使用。

八、各种消息和属性

  1. 保留消息(这个适合那种一上线就必须让其他人收到的消息)

MQTT 服务器会为每个主题存储最新一条保留消息,以方便消息发布后才上线的客户端在订阅主题时仍可以接收到该消息。主要是为了解决某个消息,发布方可能1天才发布一次,但是订阅方在上线后需要马上知道发布方上次发布的状态,不能等到它再次发布。

保留消息保留多久?怎么删除?

从服务端来说,如果保存在内存,重启就没了;如果保存在磁盘,会一直存在。它不属于会话,所以即使上一个会话结束,它还是存在的,想要删除:1.发个空消息;2.直接在服务器上删除;3.版本5协议有一个消息过期间隔,等过期了就不发了。

  1. 遗嘱消息

它解决了只有服务端才能知道客户端是否在线的问题,使我们能够为意外离线的客户端优雅地完成善后事宜。

客户端可以在连接时在服务端中注册一个遗嘱消息,与普通消息类似,我们可以设置遗嘱消息的主题、有效载荷等等。当该客户端意外断开连接,服务端就会向其他订阅了相应主题的客户端发送此遗嘱消息。这些接收者也因此可以及时地采取行动,例如向用户发送通知、切换备用设备等等。

具体运作流程:

遗嘱消息在客户端发起连接时指定,它和 Client ID、Clean Start 这些字段一起包含在客户端发送的 CONNECT 报文中。

发布遗嘱消息的时机:

  1. 服务端检测到了一个 I/O 错误或者网络故障
  2. 客户端在 Keep Alive 时间内未能通讯
  3. 客户端在没有发送 Reason Code 为 0x00(正常关闭)的 DISCONNECT 报文的情况下关闭了网络连接
  4. 服务端在没有收到 Reason Code 为 0x00(正常关闭)的 DISCONNECT 报文的情况下关闭了网络连接,例如客户端的报文或行为不符合协议要求而被服务端关闭连接。

Will Delay Interval 与延迟发布

这个属性决定了服务端将在网络连接关闭后延迟多久发布遗嘱消息,并以秒为单位。所以服务端最终何时发布遗嘱消息,取决于 Will Delay Interval 到期和会话结束这两种情况谁先发生。

  1. 请求/响应-MQTT5.0的"请求/响应"

响应主题

在 MQTT 5.0 中,请求方可以在请求消息中指定一个自己期望的响应主题 (Response Topic)。响应方根据请求内容采取适当的操作后,向请求中携带的响应主题发布响应消息。如果请求方订阅了该响应主题,那么就会收到响应。请求方可以将自己的 Client ID 作为响应主题的一部分,这可以有效避免不同的请求方不小心使用了相同的响应主题而造成冲突

关联数据

请求方还可以在请求中携带关联数据 (Correlation Data),响应方必须在响应中将关联数据原封不动地返回,请求方因此可以识别响应所属的原始请求。可以避免在响应方没有按请求顺序返回响应或者由于网络连接断开导致丢失了某个响应 (QoS 0) 时,请求方不正确地关联响应与原始请求。

MQTT “请求 / 响应”的使用建议

  1. MQTT 的 QoS 1 或 2 只能确保消息到达服务端,如果想要确认消息是否到达订阅端,可以借助“请求 / 响应”机制。
  2. 在发送请求前订阅响应主题以免错过响应。
  3. 确保响应方和请求方拥有发布和订阅响应主题的必要权限,响应信息可以帮助我们构建符合权限要求的响应主题。
  4. 存在多个请求方时,请求方需要使用不同的响应主题以免响应混淆,使用 Client ID 作为主题的一部分是常见的做法。
  5. 存在多个响应方时,请求方最好在请求中设置关联数据 (Correlation Data) 以免响应混淆。
  6. 遗嘱消息也可以使用“请求 / 响应”,只需要在连接时为遗嘱消息设置响应主题即可。这可以帮助客户端知道在自己离线期间遗嘱消息是否被消费,以便做出适当的调整。
  1. 用户属性

是一种自定义属性,允许用户向 MQTT 消息添加自己的元数据,传输额外的自定义信息以扩充更多应用场景,确保了用户可扩展标准协议的功能。该功能与 HTTP 的 Header 的概念非常类似。有几个简单的用法:

  • 文件传输:不是放在Payload里,而是放在用户属性的key-value里传输。
  • 资源解析:在用户属性标识自己的消息格式内容,是json还是字节流形式。
  • 消息路由:在用户属性中记录该条消息是给实时显示的服务,还是给存储的服务