Android MQTT 集控

2,448 阅读7分钟

Android MQTT 集控

AWS IoT MQTT Android 简单连接 - 掘金 (juejin.cn)紧接上文做一个简单介绍以及使用AWS IOT 遇到的问题。产品经理陆陆续续提了很多新需求,需要实现的功能也越来越多,这就是一个简单的集控平台。

MQTT(Message Queuing Telemetry Transport)协议非常适合用作集控平台的通信基础。以下是几个原因说明为什么MQTT可以作为集控平台的解决方案:

  • 轻量级与高效:MQTT设计用于资源受限的设备,如传感器和移动设备,这意味着它在带宽有限或网络条件不佳的情况下仍能高效运作。这对于需要连接众多设备的集控平台来说至关重要。

  • 发布/订阅模式:MQTT采用发布/订阅的消息模型,使得系统中的各个组件(如传感器、控制器、服务器)可以灵活地发布数据或订阅感兴趣的主题,非常适合实现设备间的数据交换和指令传递,便于集控平台集中处理和分发信息。

  • 低功耗:由于MQTT的高效性,它有助于减少设备的电力消耗,这对于依赖电池供电的远程设备尤为重要,有利于扩大集控平台覆盖的地理范围和应用场景。

  • 可靠性:MQTT支持服务质量(QoS)等级,确保消息的可靠传输,即使在网络不稳定的情况下也能保证重要信息的送达,这对于实时监控和控制操作至关重要。

  • 安全性:MQTT v3.1.1及之后的版本支持TLS/SSL加密,可以保障数据传输的安全性,防止数据被窃听或篡改,满足集控平台对数据安全的要求。

  • 跨平台兼容性:MQTT协议具有良好的跨平台兼容性,几乎所有的操作系统和编程语言都有对应的MQTT客户端库,便于在多样化的集控系统中集成。

  • 扩展性:随着集控平台规模的扩大,MQTT支持的集群部署和负载均衡特性能够确保系统的可扩展性和稳定性。

MQTT不仅能够满足集控平台对于设备连接、数据传输、指令控制的基本需求,还能提供高效、安全、可靠的通信机制,是构建集控平台的理想选择之一。许多现有的集控平台解决方案,特别是涉及物联网(IoT)和工业4.0应用的,都采用了MQTT作为其通信协议。

Android 实际应用

越来越多的学校会使用Android 大屏作为教学辅助的设备,为了方便学校管理人员统一管理大屏都会配备一个集控平台。这个集控应用是系统应用拥有系统权限,如果集成了厂商自己中间件还可以实现更多功能。

实现步骤(Android )

创建BootReceiver

class BootReceiver : BroadcastReceiver() {  
    @SuppressLint("UnsafeProtectedBroadcastReceiver")  
    override fun onReceive(context: Context?, intent: Intent?) {  
        LogUtil.d("BootReceiver onReceive")  
        val serviceIntent = Intent(context, MqttService::class.java)  
        context?.startService(serviceIntent)  //启动mqtt 连接服务
    }  
}

AndroidManifest.xml 声明

<receiver  
    android:name=".receive.BootReceiver"  
    android:exported="true">  
    <intent-filter >        
	    <action android:name="android.intent.action.BOOT_COMPLETED" />  
    </intent-filter>
</receiver>

遇到的问题

开机广播无法收到

首次开机在谷歌原生launcher 锁屏界面中是无法收到开机广播的,只有解锁之后才可以收到开机广播,这个目前也没有很好的解决方案。

开机之后概率性mqtt连接失败

有可能在接收到开机广播时,设备的网络(无论是Wi-Fi还是移动数据网络)尚未完全初始化并准备好。尤其是在系统刚启动后的短时间内,各种系统服务和应用程序都在初始化阶段,网络连接可能还没有完全建立。所以会连接失败。 解决方案

  1. 延时几百毫秒 启动mqtt 连接

  2. 使用ConnectivityManager 注册网络回调,注册成功之后会立马返回结果给你。(这种结果会更好)

object NetworkHelper {  
  
    fun registerNetworkObserver(context: Context) {  
        val connectivityManager =  
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
        val networkRequest = NetworkRequest.Builder()  
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)  
            .build()  
        connectivityManager.registerNetworkCallback(networkRequest,networkCallback)  
    }  
  
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {  
  
        override fun onAvailable(network: Network) {  
            super.onAvailable(network)  
            LogUtil.d("onAvailable: 网络已连接")  
            listener?.onNetworkConnected(true, null)  
        }  
  
        override fun onLost(network: Network) {  
            super.onLost(network)  
            LogUtil.d("onLost: 网络已断开")  
            listener?.onNetworkConnected(false, null)  
        }  
  
        override fun onCapabilitiesChanged(  
            network: Network,  
            networkCapabilities: NetworkCapabilities  
        ) {  
            super.onCapabilitiesChanged(network, networkCapabilities)  
            LogUtil.d("onCapabilitiesChanged: 网络状态发生变化")  
            val isNetworkValidated = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)  
            if (isNetworkValidated) {  
                LogUtil.d("网络已验证,可以确定连接到互联网")  
            } else {  
                LogUtil.d("网络未经过验证,虽然连接但不确定能否访问互联网")    
            }  
        }  
    }  
  
    fun unregisterNetworkCallback(context: Context) {  
        val connectivityManager = context  
            .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
        connectivityManager.unregisterNetworkCallback(networkCallback)  
    }  
  
    /**  
     * 添加回调的监听者  
     */  
    private var listener: NetworkConnectedListener? = null  
    fun addListener(listener: NetworkConnectedListener) {  
        this.listener = listener  
    }  
  
    interface NetworkConnectedListener {  
        /**  
         * @param isConnected  
         * @param networkStatus  
         */  
        fun onNetworkConnected(  
            isConnected: Boolean,  
            networkStatus: Int?  
        )  
    }  
}

一开始是这样使用的,后来发现有些网络未经过验证但是可以上网,所以直接在 override fun onAvailable(network: Network) 调用listener?.onNetworkConnected(true, null)

override fun onCapabilitiesChanged(  
    network: Network,  
    networkCapabilities: NetworkCapabilities  
) {  
    super.onCapabilitiesChanged(network, networkCapabilities)  
    LogUtil.d("onCapabilitiesChanged: 网络状态发生变化")  
    if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {  
        when {  
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {  
                listener?.onNetworkConnected(true, NetworkCapabilities.TRANSPORT_WIFI)  
            }  
  
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {  
                listener?.onNetworkConnected(true, NetworkCapabilities.TRANSPORT_CELLULAR)  
            }  
  
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> {  
                listener?.onNetworkConnected(true, NetworkCapabilities.TRANSPORT_ETHERNET)  
            }  
  
            else -> {  
                LogUtil.d("onCapabilitiesChanged#其他网络")  
            }  
        }  
    } else{
        listener?.onNetworkConnected(false, null)
    }
}

断网重连

MQTT客户端是否会自动重连取决于客户端库的配置和实现。在AWS IoT SDK 我找不到明确的开关,经过测试发现AWS Iot SDK 已经帮我们实现了断网自动重连的。(短时间断网之后是可以重连的,长时间的断网之后重连没有进行测试)。

由于长时间断网重连没测试,为了减少开发时间该如何确保断网重连呢,我换了一种方式,每次断网之后手动调用 connection.disconnect(),有网络之后重新初始化MqttClientConnection 对象,重新调用connection.connect()

在MQTT协议中,ClientID(客户端标识符)是用来唯一标识每一个连接到MQTT服务器的客户端的字符串。每个连接到MQTT代理的客户端必须拥有一个独一无二的ClientID,原因包括但不限于以下几点:

  • 会话管理:MQTT协议允许通过ClientID来维持会话状态,包括订阅关系和未确认的消息队列。如果两个客户端使用相同的ClientID连接到同一个MQTT代理,它们将会共享这些会话状态,导致混乱,比如订阅覆盖、消息错乱等问题。

  • 连接排他性:使用唯一的ClientID可以确保同一时间只有一个客户端以该标识连接到服务器,这对于需要独占资源或状态的应用场景非常重要。

  • 消息路由:MQTT服务器依据ClientID来路由消息给正确的客户端,特别是在QoS(服务质量)大于0的消息传输中。如果多个客户端共享相同的ClientID,服务器将无法准确判断应将消息发送给哪个客户端。

确保每个MQTT客户端拥有独特的ClientID是维护消息系统正确性、可靠性和高效性的基础要求。在这里我们可以使用四位随机数加上机器序列号 0258_madf1234567890258

  
class MqttManager {  
    private var connection: MqttClientConnection ?=null
  
    val callback: MqttClientConnectionEvents = object : MqttClientConnectionEvents {  
        override fun onConnectionInterrupted(p0: Int) {  
            LogUtil.d(TAG, "onConnectionInterrupted: ")  
        }  
  
        override fun onConnectionResumed(p0: Boolean) {  
            LogUtil.d(TAG, "onConnectionResumed: ")  
        }  
  
        override fun onConnectionSuccess(data: OnConnectionSuccessReturn?) {  
            super.onConnectionSuccess(data)  
            LogUtil.d(TAG, "onConnectionSuccess: ")  
            subscribeAllTopic() //连接成功之后,可以订阅主题,发布主题    
        }  
  
        override fun onConnectionFailure(data: OnConnectionFailureReturn?) {  
            super.onConnectionFailure(data)  
            LogUtil.d(TAG, "onConnectionFailure: ")  
        }  
  
        override fun onConnectionClosed(data: OnConnectionClosedReturn?) {  
            super.onConnectionClosed(data)  
            LogUtil.d(TAG, "onConnectionClosed: ")  
        }  
    }  
  
    fun start(callback: MqttClientConnectionEvents) {  
        try {  
            connection?.disconnect()
            val clientId =
                "client" + String.format("%04d", Random.nextInt(10000)) + "_" +"madf1234567890258"
            val builder = AwsIotMqttConnectionBuilder  
                .newMtlsBuilder(certificateStr, privateKeyStr)    
                .withConnectionEventCallbacks(callback)  
                .withClientId(clientId)
                .withEndpoint(clientEndpoint)  
                .withPort(yourPort)  
                .withCleanSession(true)  
                .withKeepAliveSecs(60)  
                .withProtocolOperationTimeoutMs(60000)  
                .withWill(getLiveMessage(false))  
            val connection: MqttClientConnection = builder.build()  
            builder.close()
            connection?.onMessage(messageHandle)
            connection?.connect()  
        } catch (ex: CrtRuntimeException) {  
            println("Exception encountered: $ex")  
        }  
    }  
  

    private val messageHandle = Consumer<MqttMessage> { message: MqttMessage ->
        LogUtil.d("onMessageArrived--->${message.topic}")
    }

  
    //订阅主题  这里可以
    private fun subscribe(  
        topic: String,  
        qos: QualityOfService = QualityOfService.AT_LEAST_ONCE
    ) {  
        connection?.subscribe(topic, qos)
    }  
  
  
    //发布主题  
    private fun publish(  
        topic: String,  
        payload: String,  
        qos: QualityOfService = QualityOfService.AT_LEAST_ONCE  
    ) {  
        connection?.publish(  
            MqttMessage(  
                topic, payload.toByteArray(), qos, false  
            )  
        )  
    }
    
    fun disconnect() {  
	    try {  
	        connection?.disconnect()  
	        connection = null  
	    } catch (ex: CrtRuntimeException) {  
	        println("Exception encountered: $ex")  
	    }  
	}
}

Service

override fun onCreate() {  
    super.onCreate()  
    NetworkHelper.registerNetworkObserver(this)  
    NetworkHelper.addListener(object : NetworkHelper.NetworkConnectedListener {  
        override fun onNetworkConnected(isConnected: Boolean, networkStatus: Int?) {  
            if (isConnected) {  
               MqttManager.start()
            } else {  
               MqttManager.disconnect()
            }  
        }  
    })  
}

打包apk 给测试同事验证,断网重连失败以及开机概率性mqtt连接失败的情况很难复现到了。

小小优化

1、MQTT的主题设计允许使用通配符来进行一定程度的模式匹配。MQTT提供了两个通配符:

  • +:用于表示一个级别的通配符。它只能匹配主题中的一个单词(即主题级别的一个部分)。客户端订阅sports/tennis/+将会接收所有发送到sports/tennis/event1sports/tennis/event2等的消息,但不会接收sports/tennis/player1/score

  • #:用于表示多级通配符。它可以匹配主题中剩余的部分,包括零个或多个级别。
    客户端订阅sports/#将会接收所有以sports/开头的主题的消息,包括sports/tennis/event1sports/football/match1等。

我们可以跟后端的同事约定好主题的模板,减少订阅开销。