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还是移动数据网络)尚未完全初始化并准备好。尤其是在系统刚启动后的短时间内,各种系统服务和应用程序都在初始化阶段,网络连接可能还没有完全建立。所以会连接失败。 解决方案
-
延时几百毫秒 启动mqtt 连接
-
使用
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/event1、sports/tennis/event2等的消息,但不会接收sports/tennis/player1/score。 -
#:用于表示多级通配符。它可以匹配主题中剩余的部分,包括零个或多个级别。
客户端订阅sports/#将会接收所有以sports/开头的主题的消息,包括sports/tennis/event1、sports/football/match1等。
我们可以跟后端的同事约定好主题的模板,减少订阅开销。