转载: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. 使用jackson序列化json时遇到的坑

    公司使用Springboot进行开发,里面默认使用了jackson进行序列化. 但是序列化的过程中一直报错,因此记录一下. ⒈jackson默认大小写敏感,且首字母转小写 在类上添加一下注解即可 @J ...

  2. T100——报表的小计数量、小计金额,总计金额

    范例:cxmr540_g01 范例代码: ON EVERY ROW #add-point:rep.everyrow.before name="rep.everyrow.before" ...

  3. CentOS7 Python3上安装paramiko

    1. CentOS 7下安装Python3.5 CentOS7默认安装了python2.7.5,要用 Python3需要自己手动安装.注意不要删除python2. 1.1 下载python3源码包 w ...

  4. go install

    go get使用时的附加参数 使用 go get 时可以配合附加参数显示更多的信息及实现特殊的下载和安装操作,详见下表所示. go get 使用时的附加参数 附加参数 备 注 -v 显示操作流程的日志 ...

  5. [多平台]pymo – 手机上的 GalGame 引擎

    [多平台]pymo – 手机上的 GalGame 引擎 介绍下这个能在手机上玩移植 GalGame 的游戏引擎,不知道有多少人听过呢?相信如果有喜欢在手机上玩 GalGame 的同学肯定听过类似的东西 ...

  6. java线程的方便调用方式

    一直用java的线程,总感觉写起来阅读或书写起来不是方便,改进之. 实现类: public class Task<R> { private ExecutorService executor ...

  7. 怎样理解 instanceof

    instanceof 运算符用来判断一个对象在其原型链中是否存在一个构造函数的 prototype 属性. 也就是说, instanceof 判断的实际上是某个对象是否为某个构造函数的实例, 因为es ...

  8. 使用.netcore部署window服务完成过程(使用nssm,Topshelf)

    一,新建.netcore控制台应用程序.本文使用.netcore2.2版本,结构如下 二,negut引用Topshelf.Log4Net,Topshelf 三,代码如下:1>Program.cs ...

  9. loj 3014「JOI 2019 Final」独特的城市

    loj 我本来是直接口胡了一个意思一样的做法的,但是因为觉得有点假+实现要用并查集(?)就卡了好一会儿... 对于一个点\(x\)来说,独特的点一定在它的最长链上,如果有独特的点不在最长链上,那么最长 ...

  10. form表单中的enctype 属性以及post请求里Content-Type方式

    对于form表单中的enctype 属性之前理解的一般,就知道是类似于一种编码形式.后来公司做一个form表单提交数据的时候,重点是这个form表单里有文件上传,而我又要用vue来模拟form表单提交 ...