站在Android开发者的角度认识MQTT - 使用篇

2,993 阅读12分钟

MQTT 是用于物联网 (IoT) 的 OASIS 标准消息传递协议。它被设计为一种极其轻量级的发布/订阅消息传输,非常适合以较小的代码占用空间和最小的网络带宽连接远程设备。如今,MQTT 已广泛应用于各个行业,例如汽车、制造、电信、石油和天然气等。

这是MQTT官方对于它的一段介绍,本文也将从下方思维导图中列出的五部分逐一认识下MQTT。

简介

从官方的介绍中可以了解到,MQTT是一种基于发布/订阅式的轻量级协议,目前广泛用于物联网生态设备中,而且现在市面上大多数智能家居都采用了MQTT协议通信的方式。

这里稍微提下我对于MQTT为什么适用于IOT设备的一些理解吧,对于IOT设备来说,它们是长期在线并且不会主动去请求某些指令,它们执行动作都是收到指令才会去执行,这也就非常符合发布/订阅模式,IOT设备只需要订阅对应的指令主题,在客户端下发指定之后,设备一旦收到就会去执行;并且MQTT的传输消息非常轻量,这也帮助设备可以更加高效的去接收指令和返回指令结果;最后MQTT是支持组广播消息的,客户端可以向同一主题下的所有设备发送指令,只需要发送一次,所有设备都可以接收到。

MQTT中有两个关键的角色分别为MQTT客户端(Client)MQTT代理(Broker):

  • MQTT客户端:它既可以作为发布者也可以作为订阅者,可收发消息;
  • MQTT代理:它负责接收客户端发送的消息,并将此消息转发给另外一些符合条件(订阅此消息主题)的客户端。

上图就是一个简单的发送和订阅的流程,当Publisher发送了主题为A的消息Name之后,此时Broker会收到消息Name,接着就会寻找订阅者中已经订阅了主题A的所有订阅者,并将消息Name转发给其寻找到的订阅者,这样就达到了一个发布多个订阅的效果。

下面就进入实战环节,来看看Android是如何使用MQTT进行消息的传输。

创建MQTT客户端

首先需要将MQTT的依赖添加到项目当中,添加的依赖的方式采用的是VersionCatalog,具体可以看上一篇文章:使用Google推荐的VersionCatalog管理统一版本

toml文件:
[versions]
mqtt = "1.2.4"
mqtt-android = "1.1.1"

[libraries]
mqtt = { group = "org.eclipse.paho", name = "org.eclipse.paho.client.mqttv3", version.ref = "mqtt" }
mqtt-android = { group = "org.eclipse.paho", name = "org.eclipse.paho.android.service", version.ref = "mqtt-android" }

[bundles]
mqtt = ["mqtt", "mqtt-android"]

build.gradle.kts文件:
dependencies {
	implementation(libs.bundles.mqtt)
}

这里有一点要注意的是,如果你的项目是基于AndroidX开发,那么还需要额外添加下localbroadcastmanager的依赖,并且在gradle.properties文件中添加android.enableJetifier=true配置,这是因为org.eclipse.paho.android.service中引用了v4的localbroadcastmanager。

连接MQTT

class MqttManager {
    companion object {
        private const val TAG = "MqttManager"
        private const val MQTT_URL = "tcp://broker.emqx.io:1883"
        private const val MQTT_USER_NAME = ""
        private const val MQTT_USER_PWD = ""
    }

    private lateinit var mqttClient: MqttAndroidClient

    fun connect(context: Context) {
        mqttClient = MqttAndroidClient(context, MQTT_URL, "android_mqtt")
        mqttClient.setCallback(object : MqttCallback {
            override fun connectionLost(cause: Throwable?) {
            }

            override fun messageArrived(topic: String?, message: MqttMessage?) {
            }

            override fun deliveryComplete(token: IMqttDeliveryToken?) {
            }
        })
        val mqttConnectOptions = MqttConnectOptions()
        mqttConnectOptions.apply {
            userName = MQTT_USER_NAME
            password = MQTT_USER_PWD.toCharArray()
        }
        mqttClient.connect(mqttConnectOptions, null, object : IMqttActionListener {
            override fun onSuccess(asyncActionToken: IMqttToken?) {
                Log.d(TAG, "onSuccess")
            }

            override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
                Log.d(TAG, "onFailure: ${exception?.stackTraceToString()}")
            }
        })
    }
}

MQTT的连接主要依赖于MqttAndroidClient对象,它的构造方法中有三个参数,context是为了它后续创建Service使用;serverURI是MQTT连接的地址;clientId是标志客户端的id,需要保证唯一性。

MqttAndroidClient还可以设置MqttCallback回调,此回调接口内部有三个方法,作用分别为:

  • connectionLost():连接断开丢失了,会触发此回调方法,可以在此方法中进行重连和主题的重新订阅;
  • messageArrived():订阅主题对应的消息送达时,会触发此回调方法,可以在此方法中处理接收到的消息;
  • deliveryComplete():这个回调方法主要是告诉客户端发布的消息已经发布成功。

最后可以通过MqttAndroidClient.connect()方法进行真正的连接动作,它接收三个参数,MqttConnectOptions可以设置连接时的一些配置,具体下面单独对它解释;userContext是一个可选参数,用于传递上下文;IMqttActionListener它是一个接口,主要作用是监听连接的结果,内部有onSuccess()和onFailure()两个方法,分别表示连接成功和失败。

接着对MqttConnectOptions单独解释下,它是对MQTT连接时设置配置所用,通常的配置有:

  • userName/password:用户名和密码
  • connectionTimeout:连接超时时间
  • cleanSession:这个配置是用于在重新连接时,是否清除客户端的会话状态
  • automaticReconnect:配置在断开连接后是否自动重连,MqttAndroidClient内部维护了一个Task,自动重连开启后会每1s重连一次
  • willMessage:此配置是遗嘱的意思,在客户端断开连接之后会发送此遗嘱消息,一般可用作判断某客户端已经离线
  • socketFactory:用于设置证书信息,MQTT可设置TSL和SSL认证方式,这个后续会单独详细介绍认证流程

以上就是在日常开发中常用的几个配置项。

tcp://broker.emqx.io:1883此地址为EMQX提供的开放的地址,大家在刚开始接触MQTT时可用此地址体验和调试,并且此地址没有设置用户名和密码选项。

然后我们调用mqttManager.connect(this)方法之后,就可以在Log中看到onSuccess的打印信息,代表着我们已经连接上EMQX。

订阅取消订阅主题

了解了如何连接MQTT之后,我们再来看看如何去订阅和取消订阅MQTT的主题,这一方面还是比较简单的,订阅主题就是为了让客户端去接收特定信息,如果客户端不去订阅主题那么就不会收到消息,相反如果客户端订阅了所有的主题那么在处理消息时也会遇到信息过大的问题。

// 订阅主题
fun subscribeTopic(topic: String) {
    mqttClient.subscribe(topic, qos = 0)
}

// 取消订阅主题
fun unSubscribeTopic(topic: String) {
    mqttClient.unsubscribe(topic)
}

// 订阅chat/person/receiver/1
mqttClient.subscribe("chat/person/receiver/1", 0)

订阅主题和取消订阅操作都是非常简单的,只需要将主题字符串传入给MqttAndroidClient即可,在订阅的方法中除了topic主题参数外,还有一个qos参数也是必传的,qos是一个非常关键的参数,它决定了客户端在订阅消息时是否保证一定可以收到。qos的解释我们放在发布和接收消息一起,发布和接收消息的部分也涉及到qos的知识,放在一起更容易理解它的作用。下面我们先看下主题一般的格式是什么样的。

MQTT的主题类似于URL路径,使用反斜杠 / 来分层,比如一个聊天系统,大家可以自行发布消息给某人,也可以接收其它人发给自己的消息,而且也可以接收群聊的消息,下面我们就可以自己定义一套主题来满足上述的要求。

  • 定义发送消息给好友的主题:chat/person/send/{好友的id}
  • 定义接收好友消息的主题:chat/person/receiver/{好友的id}
  • 定义发送给某个群聊的消息:chat/room/send/{群聊id}
  • 定义接收某个群聊的消息:chat/room/receiver/{群聊id}

这样定义好的主题分层就很清晰,四个层次分别为chat代表聊天;person和room代表聊天的类型;send和receiver代表发送和接收类型;最后的id表示用户id和群聊的id。在topic中还可以使用通配符来大范围订阅消息的作用,通常有单层通配符+和多层通配符#,单层通配符表示可以接收此层级所有的消息,比如chat/person/receiver/+此主题就可以chat/person/receiver/下所有id的消息,不限定某个好友id,多层通配符表示可以接收多个层级下所有的消息,比如chat/#表示所有的聊天信息都可以接收到,不限定单人还是群聊的消息。

限定符在客户端开发中需要谨慎使用,如果限定的范围过大可能造成消息过多的接收,导致处理消息时压力过大。

发布和接受消息

在订阅部分我们订阅了一个chat/person/receiver/1主题,下面我们尝试给此主题发送一个消息,看看是否可以顺利的接收到此消息。

mqttManager.publishMsg("chat/person/receiver/1", "Hello MQTT")

fun publishMsg(topic: String, message: String) {
    mqttClient.publish(topic, message.toByteArray(), 0, false)
}

当我们调用pulishMsg()方法之后,我们在MqttCall.messageArrived()回调方法中就会接收到此消息。

发送消息可直接调用AndroidMqttClient.publish()方法进行操作,此方法一共有四个参数,分别为:

  • topic:主题参数,在这里我们传入的为chat/person/receiver/1
  • payload:消息具体内容参数,此参数为字节数组
  • qos:服务质量参数,在上面订阅主题的地方也提到了,下面会详细介绍它的作用
  • retained:字面意思为保留,它表示发送了此消息之后,服务器是否保留此消息,以便客户端在每次连接后并订阅对应主题时都会立即收到此主题下的最新一条消息,默认为false

qos全称Quality of Service(服务质量),它是一个int值,它一共有三种类型,分别为0,1和2

  • qos=0:最多送达一次,消息有可能在传输过程中丢失;
  • qos=1:至少送达一次,消息至少保证送达到接受者一次,可能会送达多次;
  • qos=2:恰好送达一次,消息只会送达一次到接受方,不会出现多次或者0次的情况,此qos最为复杂,涉及发送者和接受者之间多次交互。
qos为0

qos为0时,消息即发即弃,发送方发送完消息之后,它不会等待确认也不会存储和重传,在此场景下一旦接收方网络不稳定就会出现消息未接收成功的情况。

具体传输过程如下图所示:

qos为1

qos为1时,消息的传输过程中增加了应答和重传机制,发送方在发送消息之后,必须等待接收方回应ACK,此时才会认为消息发送成功,在发送成功之前,发送方会存储此消息方便下次重传。

具体传输过程如下图所示:

此传输过程可能会造成重复发送消息,比如在发送方发送了消息A之后,接收方由于网络延时等情况确实没能及时接收到消息A,导致了无法回复消息ACK,在到达ACK超时时间之后,发送方会再次发送消息A,此时接收方网络延迟恢复之后会收到两条相同id的消息,它就会执行ACK的消息回复,这样就导致接收方重复接收消息的问题。

qos为2

qos为2时,为了解决消息丢失和重复发送的问题,加强了消息的确认流程,同时也增加了开销,每一次发送消息发送方和接收方都会执行两次请求和回应流程,还是以流程图的方式展示下具体传输过程:

此流程一共分为四部分:

  • 首先发送发发送一条QOS为2的PUBLISH消息,并存储此消息,等待接收方回应PUBREC消息;
  • 当接收方接收到PUBLISH消息后,会回应一条PUBREC消息,此时发送方接收到PUBREC消息时,会将存储的PUBLISH消息删除,然后会执行发送PUBREL消息,并将此消息存储下来,以便后续使用此消息重传,此时重传就不会使用PUBLISH消息了;
  • 当接收方接收到PUBREL消息之后,就确认发送方不会再传递PUBLISH消息了,它就会回复PUBCOMP消息;
  • 最终在发送发接收到PUBCOMP消息时,就确认QOS为2的消息发送成功。

此流程相对于QOS为0和1的流程都复杂很多,在我们项目当中对于一些重要的动作指令可以使用此QOS,而对于一些心跳消息则可以选择重量级轻一些的0和1。

断开MQTT

通过文章上面的内容介绍之后,MQTT的使用基本上已经掌握了,还剩下一个断开连接的操作,其实断开连接也是非常简单的,只需要通过MqttAndroidClient.disconnect()方法即可

fun disConnect(){
    mqttClient.disconnect()
    mqttClient.unregisterResources()
}

如果想更具体的感知断开的结果,也可以使用disconnect(Object userContext, IMqttActionListener callback)方法,callback会回调断开成功和失败的结果。除了调用disconnect()方法之外,别忘记unregisterResources()一下,此方法内部会解注册内部的广播和Service。

上面就是Android使用MQTT的最简单的一些内容,后面还打算将TSL和SSL认证、MQTT源码分析的一些内容完善到此系列中,一方面帮助自己更全面的了解MQTT的运行机制,另一方面也可以让大家更深入的认识下MQTT。到这为止本篇就算结束了,谢谢大家的阅读。

MQTT系列文章:

站在Android开发者的角度认识MQTT - 使用篇

站在Android开发者的角度认识MQTT - TLS 认证篇

站在Android开发者的角度认识MQTT - 源码篇


关于我

我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆~