转载:https://www.jianshu.com/p/de88edf8e023

什么是MQTT

​ MQTT是基于二进制消息的发布/订阅编程模式的消息协议,最早由IBM提出的,如今已经成为OASIS规范。由于规范很简单,非常适合需要低功耗和网络带宽有限的IoT场景。

 
MQTT使用场景

与XMPP相比有什么特点

​ 同MQTT类似的是XMPP协议,他们的特点点见下表:

  MQTT XMPP
基于协议层 TCP TCP,也可以基于HTTP
体积 小巧 庞大
适用场景 物联网 聊天
省流 省流量 费流量
省电 省电 费电
成熟度 不成熟 成熟

发布/订阅模式

与请求/回答这种同步模式不同,发布/订阅模式解耦了发布消息的客户(发布者)与订阅消息的客户(订阅者)之间的关系,这意味着发布者和订阅者之间并不需要直接建立联系。打个比方,你打电话给朋友,一直要等到朋友接电话了才能够开始交流,是一个典型的同步请求/回答的场景;而给一个好友邮件列表发电子邮件就不一样,你发好电子邮件该干嘛干嘛,好友们到有空了去查看邮件就是了,是一个典型的异步发布/订阅的场景。

熟悉编程的同学一定非常熟悉这种设计模式了,因为它带来了这些好处:

  • 发布者与订阅者不必了解彼此,只要认识同一个消息代理即可。

  • 发布者和订阅者不需要交互,发布者无需等待订阅者确认而导致锁定。

  • 发布者和订阅者不需要同时在线,可以自由选择时间来消费消息。

主题

MQTT是通过主题对消息进行分类的,本质上就是一个UTF-8的字符串,不过可以通过反斜杠表示多个层级关系。主题并不需要创建,直接使用就是了。

主题还可以通过通配符进行过滤。其中,+可以过滤一个层级,而#只能出现在主题最后表示过滤任意级别的层级。

举个例子:

  • building-b/floor-5:代表B楼5层的设备。

  • +/floor-5:代表任何一个楼的5层的设备。

  • building-b/#:代表B楼所有的设备。

注意,MQTT允许使用通配符订阅主题,但是并不允许使用通配符广播。

协议介绍

​ MQTT的通信协议并不复杂,最核心的部分,我认为是他的16种控制类型,如下表:

名字 报文流动方向 描述
Reserved 0 禁止 保留
CONNECT 1 客户端到服务端 客户端请求连接服务端
CONNACK 2 服务端到客户端 连接报文确认
PUBLISH 3 两个方向都允许 发布消息
PUBACK 4 两个方向都允许 QoS 1消息发布收到确认
PUBREC 5 两个方向都允许 发布收到(保证交付第一步)
PUBREL 6 两个方向都允许 发布释放(保证交付第二步)
PUBCOMP 7 两个方向都允许 QoS 2消息发布完成(保证交互第三步)
SUBSCRIBE 8 客户端到服务端 客户端订阅请求
SUBACK 9 服务端到客户端 订阅请求报文确认
UNSUBSCRIBE 10 客户端到服务端 客户端取消订阅请求
UNSUBACK 11 服务端到客户端 取消订阅报文确认
PINGREQ 12 客户端到服务端 心跳请求
PINGRESP 13 服务端到客户端 心跳响应
DISCONNECT 14 客户端到服务端 客户端断开连接
Reserved 15 禁止 保留

​ 我看MQTT协议内容的时候,发现有趣的一点就是他的字符长度是可变的,可以用ASCII,也可以用UTF-8,这个在我接触的其他协议里面是没有的,这样子的好处,显而易见的就是能减少传输流量。

什么是QoS

​ Qos的全称是服务质量(Quality of Service)。MQTT支持三种QoS,分别是0、1、2。级别越高,交互越复杂,越能保证正确性和到达率,但是开销也更大。他们的交互流程图如下:

  • QoS 0: 尽力而为。消息发送者会想尽办法发送消息,但是遇到意外并不会重试。
 
image
  • QoS 1: 至少一次。消息接收者如果没有知会或者知会本身丢失,消息发送者会再次发送以保证消息接收者至少会收到一次,当然可能造成重复消息。
 
image
  • QoS 2: 恰好一次。保证这种语义肯定会减少并发或者增加延时,不过丢失或者重复消息是不可接受的时候,级别2是最合适的。

     
    image

​ 服务质量是个老话题了。级别2所提供的不重不丢很多情况下是最理想的,不过往返多次的确认一定对并发和延迟带来影响。级别1提供的至少一次语义在日志处理这种场景下是完全OK的,所以像Kafka这类的系统利用这一特点减少确认从而大大提高了并发。级别0适合鸡肋数据场景,食之无味弃之可惜,就这么着吧。

相关阅读

Paho UML

 
paho uml.png

说明:

  1. 上面三个类(MqttAndroidClient, MqttService, MqttConnection)是存在于android.paho库中,也就是说,这三个类是paho基于android的封装,而其余的类都是封装在java.paho中,处于底层库。

  2. 中间的纵轴线从上往下,MqttService,MqttConnection, MqttAsyncClient, ClientComms,这四个类都具备connect, publish, subscribe等开放给上层的基础方法,因为上层的调用都是通过这四个类传递到底层。

  3. Paho运行的核心在于CommsCallback, CommsSender和CommsReceiver三个线程,分别用来做触发回调、发送消息和接收消息三个动作。

  4. ClientState和CommsTokenStore维护着一套复杂的队列,三个线程通过使用这两个类,来实现线程间的同步和消息的排队。

  5. ClientComms的功能除了基础方法接口之外,还维护底层三个线程的状态,这些状态主要包括:

    1. connected

    2. connecting

    3. disconnected

    4. disconnecting

    5. closed

    6. resting

  6. MqttAsyncClient的功能除了基础方法接口之外,主要负责连接的重连,通过下面介绍的流程分析可以看出来,mqtt的重连机制就是在MqttAsynClient发起的。

  7. MqttConnection是一个独立的mqtt连接,用于维护当前连接的所有状态。

流程分析

因为整个库的代码量比较大,我这里就不把代码贴出来了,这是我通过阅读代码整理出来的所有关键方法的流程,包括connect, publish 和reconnect,希望能对读者您有帮助。

  • connect()
  1. MqttAndroidClient.bindService()

  2. MqttService.connect()

    1. MqttService.createConnection is not exists
  3. MqttConnection.connect()

    1. if option.isCleanSession(), then discard old data (we set cleanSession true)

    2. callback action connect to activity when start connect fail, result fail or success

    3. if MqttAsyncClient created

      1. if isConnecting, return

      2. if is connected, doAfterConnectSuccess

      3. else MqttAsyncClient.connect

    4. new MqttAsyncClient and connect

  4. MqttAsyncClient.connect()

    1. judge if need connect by ClientComms

      1. if connected, throw exception

      2. if connecting, throw exception

      3. if disconnected, throw exception

      4. if closed, throw exception

    2. ClientComms.setReconnectCallback if options set automaticReconnect

    3. new MqttToken as userToken

    4. new ConnectActionListener and set comms, userToken

  5. ConnectActionListener.connect()

    1. persistence.open

    2. comms.connect

  6. ClientComms.connect()

    1. if not disconnected or is closing, throw exception

    2. tokenStore.open(), just set closeResponse null

    3. ConnectBG.run

  7. ConnectBG.run()

    1. CommsReceiver.start()

      1. runningSemaphore.acquire(), ensure just one receiver thread is running

      2. while in.available && message instance of MqttAck

      3. tokenStore.getToken

        1. if token != null, synchronized token and clientState.notifyReceivedAck

        2. else if instance of MqttPubRec MqttPubComp MqttPubAck, log itthese signal only receive after send

        3. else throw exception

    2. CommsSender.start()

      1. runningSemaphore.acquire(), ensure just one sender thread is running

      2. await clientState.pendingMessages.removeElementAt(0)

      3. out.write(message)

    3. CommsCallback.start()

      1. runningSemaphore.acquire(), ensure just one callback thread is running

      2. workAvailable.wait()

      3. check if exists complete token, if true then handleActionComplete

        1. if token.isComplete, notifyComplete

        2. token.internalTok.notifyComplete

        3. if token.isNotified

          1. if mqttCallback != null and is DeliveryToken, notify deliveryComplete

          2. if token.getActionCallback != null, notify onSuccess or onFail

        4. token.setNotifiyed(true)

      4. check if exists MqttPublish, if true then handleMessage(MqttPublish)this happens when register topic, not exists in DataCollector

    4. internalSend

      1. clientState.send()
  8. clientState.send()

    1. token.setMessageId()

    2. if MqttPublish

      1. tokenStore.saveToken

      2. pedingMessages.addElement

      3. queueLock.notifyAll()

    3. if MqttConnect

      1. tokenStore.saveToken

      2. pedingFlows.insertElementAt(0)

      3. queueLock.notifyAll()

  • publish()
  1. MqttAndroidClient.publish()

  2. MqttService.publish()

  3. MqttConnection.publish()

    1. if MqttAsyncClient.isConnected

      1. MqttAsyncClient.publish

      2. storeSendDetails

    2. else, callbackToActivity error

  4. MqttAsyncClient.publish()

    1. new MqttPublish

    2. comms.sendNoWait

  5. ClientComms.sendNoWait()

    1. if connected or trying to connect or trying to disconnect

      1. if disconnectedBuffer is not empty, put message to thatafter reconnect, it will send disconnectBuffer message first, before send real-time message

      2. internalSend()

        1. token.setClient

        2. clientState.send

    2. else if disconnectedBuffer is not null, put message to that

    3. else throw exception

  6. ClientState.send()go to connect

  • Reconnect automatic
  1. ClientComms.shutdownConnection()

  2. CommsCallback.connectionLost()

  3. MqttAsyncClient.MqttReconnectCallback.connectinLost()

    1. startReconnectCycle

    2. run ReconnectTask after 1s

    3. attempReconnect()

  4. MqttAsyncClient.connect()go to connect .4

一篇文章让您了解MQTT的更多相关文章

  1. 一篇文章让Oracle程序猿学会MySql【未完待续】

    一篇文章让Oracle DB学会MySql[未完待续] 随笔前言: 本篇文章是针对已经能够熟练使用Oracle数据库的DB所写的快速学会MySql,为什么敢这么说,是因为本人认为Oracle在功能性方 ...

  2. 由一篇文章引发的思考——多线程处理大数组

    今天领导给我们发了一篇文章文章,让我们学习一下. 文章链接:TAM - Threaded Array Manipulator 这是codeproject上的一篇文章,花了一番时间阅读了一下.文章主要是 ...

  3. 一篇文章告诉你为何GitHub估值能达20亿美元

    软件开发平台GitHub今日宣布,已获得硅谷多家知名风投2.5亿美元融资,这也让其融资总额达到了3.5亿美元,此轮融资对GitHub的估值约为20亿美元. GitHub有何特别之处? GitHub创立 ...

  4. DEDECMS教程:上/下一篇文章标题长度的截取方法

    对dedecms了解的朋友们,想必对如何获取上一篇.下一篇文章的标签也是非常熟悉.dedecms获取上一篇.下一篇文章的标签分别为:{dede:prenext get='pre'/}.{dede:pr ...

  5. Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

  6. Android:学习AIDL,这一篇文章就够了(上)

    前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了.不过又转念一想, ...

  7. 一篇文章一张思维导图看懂Android学习最佳路线

    一篇文章一张思维导图看懂Android学习最佳路线 先上一张android开发知识点学习路线图思维导图 Android学习路线从4个阶段来对Android的学习过程做一个全面的分析:Android初级 ...

  8. (转) TensorFlow深度学习,一篇文章就够了

    TensorFlow深度学习,一篇文章就够了 2016/09/22 · IT技术 · TensorFlow, 深度学习 分享到:6   原文出处: 我爱计算机 (@tobe迪豪 )    作者: 陈迪 ...

  9. php下删除一篇文章生成的多个静态页面

    php自定义函数之删除一篇文章生成的多个静态页面,可能有多页的文章,都是需要考虑到的. 复制代码代码如下: //– 删除一篇文章生成的多个静态页面  //– 生成的文章名为 5.html 5_2.ht ...

随机推荐

  1. Lua开发环境

    下载并解压Lua源码: wget http://www.lua.org/ftp/lua-5.1.5.tar.gz tar zxvf lua-5.1.5.tar.gz cd lua-5.1.5 打开Ma ...

  2. 腾讯云CentOS 7搭建简单Tomcat+nginx集群

    1.安装Tomcat 进入 /usr/local/ 目录 cd /usr/local 下载 wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-9/v ...

  3. yaml语言格式

    YAML是"YAML Ain't a Markup Language"(YAML不是一种标记语言),强调这种语言以数据做为中心,而不是以置标语言为重点. 转载2篇比较好的关于yam ...

  4. ######【Python】【基础知识】Python的介绍 ######

    Python 是一种面向对象.解释型计算机程序设计语言. Python是什么? Python(英国发音:/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/), 是一种面向对象的解释型计算机程序设计语言 ...

  5. docker容器创建MariaDB镜像

    基于commit命令方式创建 docker的安装 [root@test01 ~]# yum install docker [root@test01 ~]# systemctl enable docke ...

  6. Oracle——学习之路(视图——虚拟表)

    语法: create [or replace] view 视图名  as  查询相关语句                                 ps: or replace 表示新视图可以覆 ...

  7. [POI2011]ROT-Tree Rotations 题解

    题面 这道题咋看都是无法从dp入手,那么就从数据结构入手!: 首先你要会权值线段树和线段树合并. 然后你要知道: 对于任意一个节点,交换左右子树对当前节点和前面的所有节点没有影响. 因为这是前序遍历: ...

  8. PDO原生分页

    ** PDO分页** 1.PDO连接数据库 $dbh=new PDO('mysql:host=127.0.0.1;dbname=03a','root','root');//使用pdo 2.接收页码 $ ...

  9. nginx.conf and dockerfile带颜色

    wget http://www.vim.org/scripts/download_script.php?src_id=14376 -O nginx.vim mv nginx.vim /usr/shar ...

  10. X86逆向12:内存补丁的制作

    本章我们将学习各种打补丁的方式,补丁在软件的破解过程中非常的重要,比如软件无法脱壳我们就只能通过打补丁的方式来破解程序,补丁原理就是当程序运行起来会被释放到内存并解码,然后补丁就通过地址或特征码定位到 ...