1.什么是MQTT
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
在网络模型里,TCP是传输层协议,HTTP和MQTT是应用层的协议。TCP是HTTP和MQTT底层的协议
2.MQTT的特点
MQTT的优点:协议简洁轻巧,数据冗余量低。并且支持的设备从智能硬件到智能手机无所不包。MQTT 使用的底层传输协议基础设施。 客户端使用它连接服务端。 它提供有序的、可靠的、双向字节流传输。
MQTT的缺点:服务器端实现难度大,虽然已经有了C++版本的服务端组件,但是并不开源。而且在推送数量较大时如何处理并发是十分考验后台人员的技术水平的。
1.MQTT 协议提供一对多的消息发布,可以解除应用程序耦合,信息冗余小。该协议需要客户端和服务端,而协议中主要有三种身份:发布者(Publisher)、代理(Broker,服务器)、订阅者(Subscriber)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,而消息发布者可以同时是订阅者,实现了生产者与消费者的脱耦。
2、使用 TCP/IP 提供网络连接,提供有序、无损、双向连接;
MQTT 是一种连接协议,它指定了如何组织数据字节并通过 TCP/IP 网络传输它们。设备联网,也需要连接到互联网中,在大万维的世界中,TCP 如同汽车,有轮子就能用来运输数据,MQTT 就像是交通规则。在网络模型中,TCP是传输层协议,而 MQTT是在应用层,在 TCP 的上层,因此MQTT 也是基于这个而构建的,提高了可靠性。
3、对负载内容屏蔽的消息传输;
可以对消息订阅者所接受到的内容有所屏蔽。
4、具体有三种消息发布的服务质量:
至多一次,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。
至少一次,确保消息到达,但消息重复可能会发生。
只有一次,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。
5、小型传输,开销小,固定长度的头部是 2 字节,协议交换最小化,以降低网络流量;整体上协议可拆分为:固定头部+可变头部+消息体,这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集"。
6、使用Last Will和Testament特性通知有关各方客户端异常中断的机制;
Last Will:用于通知同一主题下的其他设备发送遗言的设备已经断开了连接。
Testament:功能类似于Last Will。
基本概念
1、MQTT 客户端
- 一个使用 MQTT 协议的设备、应用程序等,它总是建立到服务器的网络连接。
- 可以发布信息,其他客户端可以订阅该信息
- 订阅其它客户端发布的消息
- 退订或删除应用程序的消息
- 断开与服务器连接
2、MQTT 服务器
- MQTT 服务器以称为 Broker(消息代理),是一个应用程序或一台设备。它是位于消息发布者 和订阅者之间。
- 接受来自客户端的网络连接
- 接受客户端发布的应用信息
- 处理来自客户端的订阅和退订请求
- 向订阅的客户转发应用程序消息
3、主题(Topic)
- 连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。
- 1️⃣、要订阅的主题。一个主题可以有多个级别,级别之间用斜杠字符分隔。例如/world 和 emq/emqtt/emqx 是有效的主题。
- 2️⃣、订阅者的Topic name支持通配符#和+ :
- # 支持一个主题内任意级别话题
- +只匹配一个主题级别的通配符
- 3️⃣、客户端成功订阅某个主题后,代理会返回一条 SUBACK 消息,其中包含一个或多个 returnCode 参数。
4、主题筛选器(Topic Filter)
- 一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。
5、QoS(消息传递的服务质量水平)
- 服务质量,标志表明此主题范围内的消息传送到客户端所需的一致程度。
- 值 0:不可靠,消息基本上仅传送一次,如果当时客户端不可用,则会丢失该消息。
- 值 1:消息应传送至少 1 次。
- 值 2:消息仅传送一次。
6、会话(Session)
- 每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
7、订阅(Subscription)
- 订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
- 客户端在成功建立TCP连接之后,发送CONNECT消息,在得到服务器端授权允许建立彼此连接的CONNACK消息之后,客户端会发送SUBSCRIBE消息,订阅感兴趣的Topic主题列表(至少一个主题)
- 订阅的主题名称采用UTF-8编码,然后紧跟着对应的QoS值
8、发布(publish)
- 控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息,MQTT 客户端发送消息请求,发送完成后返回应用程序线程。
- 比如安卓的推送服务,还有一些即时通信软件如微信等也是采用的推送技术。
9、负载(Payload)
- 消息订阅者所具体接收的内容
简单示例
MQTT 协议主要是根据以下情况设计的:
- M2M(Machine to Machine),机器或设备间端到端通信,比如传感器之间的数据通讯。
- 设备(Machine)中,例如传感器,硬件能力很弱,协议要考虑尽量小的资源消耗,比如计算能力和存储等。
根据 MQTT 的基础了解后并结合简单的架构,在这里做一个简单的示例图,可以更直观的理解MQTT协议的通信模型。MQTT Broker 就选择 EMQ 作为示范。比如有1个温度传感器(1个Machine),1个移动设备,1个电脑,一个服务器(3个Machine),都可以得到或者显示温度传感器的温度值,需要先通过 MQTT 协议subscribe(订阅)一个比如叫 temperature 的 topic(主题)如下:
图中移动设备,服务器,电脑需要先通过 EMQ subscribe 一个叫 temperature 的 topic,当温度传感器 publish 温度数据,三个设备就可以收到了。
进一步了解MQTT 3
MQTT 3 (当前版本3.1.1)是目前使用的最为广泛的MQTT协议标准。尽管MQTT5标准已经发布,并且带来了一些令人振奋的新特性,但是在整个应用场景上,从后台服务到消息中间件再到客户端SDK等环节上的产品升级并没有都完成,再加上既有部署的维护,业界从版本3到5的过渡可能会持续相当长一段时间,所以,对于刚加入物联网行业的生力军来说,现在来学习MQTT 3依然是一件很有意义的事情。
MQTT协议的工作方式
前面简介中讲到,在一个QMTT协议中有三个角色会参与到整个通信过程,发布者(publisher)、代理(broker)和订阅者(subscriber)。有别于传统的客户端/服务器通讯协议,MQTT协议并不是端到端的,消息传递通过代理,包括会话(session)也不是建立在发布者和订阅者之间,而是建立在端和代理之间。代理解除了发布者和订阅者之间的耦合。
除了发布者和订阅者之间传递普通消息,代理还可以为发布者处理保留消息和遗愿消息,并可以更改服务质量(QoS)等级。
MQTT控制报文
MQTT协议工作在TCP之上,端和代理之间通过交换预先定义的控制报文来完成通信。MQTT报文有3个部分组成,并按下表顺序出现:
所有的MQTT控制报文都有一个固定报头,其格式如下:
协议版本3定义了14种MQTT报文,用于建立/断开连接、发布消息、订阅消息和维护连接。固定报头的第一字节的4-7位的值指定了报文类型,其取值如下表。0和15为系统保留值;0-3位为标志位,依照报文类型有不同的含义,事实上,除了PUBLISH报文以外,其他报文的标志位均为系统保留。如果收到报文的标志位无效,代理应断开连接。
固定报头的第二字节起表示报文的剩余长度。最大4个字节,每字节可以编码至127,并含有一位继续位,如继续位非0,则下一字节依然为剩余长度。由此,理论上一个控制报文最长可以到256MB。
一些报文在固定报头和荷载之间可以有一个可变报头。可变报头的内容根据报文类型不同而不同。最常见的可变报头是报文标识符(PacketIdentifier)。
一些报文可以在最后携带一个荷载。不同的报文可以无荷载,可选荷载,或必须带有荷载。
限于篇幅,在这里我们仅以CONNECT和CONNACK为例解释一下MQTT报文的构成和报文响应行为。其他报文请查阅MQTT标准文档。
CONNECT报文
限于篇幅,在这里我们仅以CONNECT为例解释一下MQTT报文的构成。其他报文请查阅MQTT标准文档。
CONNECT是客户端连接到代理的第一个报文,如果在连接已经存在,代理收到该报文将会断开现有连接。
CONNECT报文的固定报头
CONNECT报文的可变报头
CONNECT报文的可变报头由4部分组成:
协议名。协议名是UTF-8编码的大写的MQTT。
协议级别。MQTT 3.1.1的协议级别为4.
连接标志位。定义连接行为的参数。见下表。
Keep Alive。2字节,客户端和代理之间的无活动时间超过该值后,应关闭连接。如果该值置0表示客户端不要求代理启用KEEPALIVE功能。
连接标志位:
清除会话标志位:
这个标志位定义了如何处理会话状态。如果设置为0,客户端和代理可以恢复上一次连接时的会话状态,如果上一次连接的会话状态不存在,代理将会为客户端建立一个新的会话。如果该位设置为1,则双方将清除掉上一次连接的会话状态并建立一个新的会话。
遗愿标志位:
如果遗愿标志为1,则遗愿消息会被存储在代理上,当连接关闭时,代理将发布这个消息,除非在客户端断开连接时把遗愿消息清除了。
遗愿QoS标志位:
指定了遗愿消息的服务质量等级。
保留遗愿消息标志位:
指定在发布遗愿消息的时候,是否把该消息作为保留消息存储在代理。
用户名标志位:
如果设置为1,则用户名必须出现在荷载中,反之,用户名不允许出现在荷载中。
密码标志位:
如果该位为1,则密码必须出现在荷载中;如果该位为0,则密码不允许出现在荷载中。如果用户名标志位为0,则该位必须也为0。
CONNECT报文的荷载
CONNECT报文的荷载由一个或者多个字段组成,这些字段是否出现由可变报头中的标志位决定。字段总是以长度开始。字段出现的顺序必须是:客户端标识符,遗愿主题,遗愿消息,用户名,密码。
CONNECT报文的响应
在代理在为MQTT协议开放的端口上接收到TCP连接请求并建立连接后应该会收到CONNECT报文,如果在一定时间内代理没有收到CONNECT报文,则应该关闭这个TCP连接。
在收到CONNECT报文后,代理应该检查报文格式是否符合协议标准。如果不符合协议标准,代理应关闭连接,且不发送CONNACK报文给客户端。
代理可以检查CONNECT报文的内容并执行响应的认证和鉴权。如果这些检查没有通过,代理应该向客户端发送一个带有非0返回码的CONNACK报文。
CONNACK报文
CONNACK是代理用来响应客户端CONNECT的报文。代理向客户端发送的第一个报文必须是CONNACT。CONNACK有一个固定报头,一个可变报头,但是不带有荷载。
CONNACK的固定报头
CONNACT报文只有固定报头和一个2字节的可变报头,所以它的剩余长度总是2。
CONNACK报文的可变报头
CONNACK报文的可变报头为定长2字节。第一字节的0位表示是否有会话存在。如果代理上已经有请求连接的客户端的会话,且连接请求的清除会话标识为0,则该位为1,否则该位为0。客户端可以根据这一位的值采取响应行为,比如(重新)订阅主题等。
CONNACK报文的可变报头的第二字节为返回码。如果CONNECT请求的格式正确,但是代理依然不能允许客户端连接,则返回码为一个非零值。如果连接成功,则返回0。
返回码的定义:
客户端接受到代理的CONNACK的返回码为0,则连接建立完成,双方可以开始通信。
清除会话、保留消息和QoS的组合
清除会话、保留消息等概念,在传统的客户端/服务器方式的通信中不一定会出现,这些概念有时候不太容易理解,特别是当他们被组合起来用的时候。
下面的表格汇总了当一个客户端连接上来时,它能收到消息的各种情况。
mosquitto示例
市面上有相当多的高质量MQTT代理,其中mosquitto是一个开源的轻量级的C实现,完全兼容了MQTT 3.1和MQTT 3.1.1。下面我们就以mosquitto为例演示一下MQTT的使用。
环境是百度开放的云服务器以及Ubuntu 18.04,简单起见MQTT代理和客户端都安装在同一台云服务器上了。
方法一
首先SSH到云服务器,安装mosquitto以及搭配的客户端:
apt-get install mosquitto
apt-get install mosquitto-clients
现在在云端模拟云服务,订阅某办公楼5层的温度作为主题:
mosquitto_sub -d -t 'floor-5/temperature'
Received CONNACK
Received SUBACK
Subscribed (mid: 1): 0
然后另外打开一个SSH连接,模拟温度计发送温度消息:
mosquitto_pub -d -t 'floor-5/temperature' -m '15'
Received CONNACK
Sending PUBLISH (d0, q0, r0, m1, 'floor-5/temperature', ... (2 bytes))
此时回到第一个SSH客户端可以看到信息已经接收到了,之后便是心跳消息:
Received PUBLISH (d0, q0, r0, m0, 'floor-5/temperature', ... (2 bytes))
15
Sending PINGREQ
Received PINGRESP
需要注意的是mosquitto客户端默认使用QoS 0,下面我们使用QoS 2订阅这个主题:
mosquitto_sub -d -q 2 -t 'floor-5/temperature'
Received CONNACK
Received SUBACK
Subscribed (mid: 1): 2
切换到另外SSH连接然后在这个主题里面发送温度消息:
mosquitto_pub -d -q 2 -t 'floor-5/temperature' -m '15'
Received CONNACK
Sending PUBLISH (d0, q2, r0, m1, 'floor-5/temperature', ... (2 bytes))
Received PUBREC (Mid: 1)
Sending PUBREL (Mid: 1)
Received PUBCOMP (Mid: 1)
此时回到第一个SSH客户端可以看到信息已经接收到了,以及相应的多次握手消息:
Received PUBLISH (d0, q2, r0, m1, 'floor-5/temperature', ... (2 bytes))
Sending PUBREC (Mid: 1)
Received PUBREL (Mid: 1)
15
Sending PUBCOMP (Mid: 1)
方法二
下载
对于Ubuntu系统,可以使用sudo apt-get 来安装mosquitto,但是这种方法虽然简单,但是对于配置文件的修改和管理比较麻烦,配置文件需要自己写好然后启动时载入,因此不太推荐。
本人更推荐的是第二种方法,也就是本文重点要讲的,下载tar.gz安装包,进行安装。
首先,到mosquitto的官方网站(http://mosquitto.org/files/source/)下载,tar.gz安装包。本人使用的是1.4.15版本。如果你使用的是服务器版本,你可以使用wget http://mosquitto.org/files/source/mosquitto-1.4.15.tar.gz 这个命令直接下载。
wget http://mosquitto.org/files/source/mosquitto-1.4.15.tar.gz
解压
tar -xvf mosquitto-1.4.15.tar.gz
安装
cd mosquitto-1.4.15
make
make install
1)提示:缺少ssl.h
解决办法:使用 sudo apt-get install libssl-dev
2)提示:缺少ares.h
解决办法:使用 sudo apt-get install libc-ares-dev
打开config.mk文件。
把WITH_SRV:=yes 改成WITH_SRV:=no 就可以编译了
3)提示:没有UUID
解决办法: 使用 sudo apt-get install uuid-dev
首先要注意的是,安装完后的mosquitto的配置文件在根目录下的etc下的mosquitto,使用 cd /etc/mosquitto 就可以找到相应的位置
使用 cp mosquitto.conf.example mosquitto.conf 命令,复制一份新的conf配置文件
入门的话,首先修改这几个地方:
1) user :默认是mosquitto,要更换成你当前的用户
2)port:打开1883端口,如果使用ssl或tls打开8883端口
3)protocol:mqtt
修改好后保存,回到安装目录
启动
使用, mosquitto -c /etc/mosquitto/mosquitto.conf 开启服务器。
在新建一个端口,使用, netstat -tunlp|grep 1883 查看端口是否被监听
使用 mosquitto_sub -t 'topic' 请求一个订阅
再新建一个终端,使用 mosquitto_pub -t 'topic' -m 'msg1'发布一个订阅,这时候即可查看到订阅消息
【-t】指定主题
【-m】指定消息内容
【-f】 是可以发送文件,通过-f传递的文件上限默认是256M。逻辑中有对文件大小的判断,超过256M的文件则不传。
【测试说明】
测试环境:ubuntu 18.04 虚拟机
在本例中,发布者、代理和订阅者均为localhsot,但是在实际的情况下三种并不是同一个设备,在mosquitto中可通过-h(--host)设置主机名称(hostname)。
如果不是在同一个设备,可通过-h指定代理的IP。
(另外还有一个mosquitto_passwd,用于管理密码,应该是关闭SSL的原因)
为了实现这个简单的测试案例,需要在linux中打开三个控制台,分别代表代理服务器、发布者和订阅者
#cd /usr/local/bin/
cmake ctest curve_keygen mosquitto_sub
cpack curl mosquitto_passwd openssl
c_rehash curl-config mosquitto_pub
# ls -l /usr/local/sbin/
total 152
-rwxr-xr-x 1 root root 155320 Dec 9 07:47 mosquitto
root@intest:/home/intest#
mosquitto -c /etc/mosquitto/mosquitto.conf
项目示例
mosquitto部分,嵌入音箱端及手机微信端,可实现广域网内手机微信对音箱的控制
服务器部分
mosquitto服务器部分。音箱端与手机端pub和sub都是通过服务器进行消息传递。IP地址为"120.76.30.18";
音箱部分
进入网络模式下,则需要开启一个线程,用来开启mosquitto subscribe 订阅一个主体。一个音箱实体对应一个主体。其他客户端可通过这个主题,来与音箱端实现链接。
手机微信部分
手机客户端如何得知音箱端这个主题,方法一,是微信客户端有提供对应开发包,局域网设备发现,但是这个功能依赖微信端,尚不稳定,最后并没采用;
核心内容是
两个回调函数的注册
mosquitto_connect_callback_set(mosq, my_connect_callback);
mosquitto_message_callback_set(mosq, my_message_callback);
其中my_connect_callback是mosquitto_loop_forever这个一直存在的循环函数在收到服务器传递回来CONNACK信息(见_mosquitto_handle_connack),则会被触发,用来呼叫mosquitto_subscribe完成订阅。
其中my_message_callback则是用于音箱端应用程序的开发,用来接收手机微信端传递过来的内容。与手机客户端定义交互的协议,用于控制音箱的推歌、上下首切换、音源切换、网络模式的切换、音量大小的改变等等。
交互的内容采用json包,因此需要进行json包的解析和封装。
正确完成音箱端的订阅,
注意设定好:
cfg.port = 1883;
cfg.topics[cfg.topic_count-1] = strdup(deviceName);
cfg.host = strdup(serverIp); //服务器地址
这里初始化了一个属于音箱端全局的mosq,可用它关联pub和sub;(pub和sub共用一个全局mosq即可,并不需要分别申请一个mosq)
音箱端部分,需要pub消息给手机微信端。采用mosquitto_pub_start发送消息。这个不同于sub是一个一直存在的线程,pub属于一次性,消息传递完毕即结束。
mosquitto_pub_start 中 全局变量pub_topic ,在mosquitto_sub_start里面有初始化。