Android MQTT长连接实战:从协议选型到心跳保活与自动重连

Android MQTT长连接实战:从协议选型到心跳保活与自动重连 简介Android MQTT长连接Demo是一份面向Android开发者的实时通信示例工程聚焦如何在移动端集成MQTT协议实现消息订阅、主动推送、断线自动重连及离线消息补发等核心机制适合具备基础Android开发经验、希望掌握物联网或即时通讯长连接技术的读者。压缩包共1278个文件约21.23MB包含java源码、aidl接口、gradle构建脚本、jar依赖库及APK成品其中大量xml为布局与配置文件png为界面资源class为编译产物json多为项目配置整体呈现出可直接导入运行的完整Android Studio工程结构。目前已有1217人学习下载作者为d38825。通过此Demo读者可对照源码理清Paho客户端初始化、连接回调、QoS等级选择、SSL/TLS安全连接等关键编码细节并借助内置的APK快速验证效果是一份适合入门MQTT长连接开发的实操参考。 之前做停车场项目对接海康车牌识别相机时我一直在纠结同一个问题Android端怎么实时收到相机推送的识别结果。一开始用的HTTP轮询效果很糟服务器压力大现场还反馈识别结果有延迟。后来换成了基于MQTT协议的长连接方案几行代码就解决了延迟从秒级降到了毫秒级。也是从那次之后我意识到Android长连接这件事选对协议比堆代码重要得多。这篇文章就围绕一个完整的Android MQTT长连接demo展开从协议选型、环境搭建、依赖引入到连接建立、订阅发布、心跳保活、自动重连再到真实项目里的常见坑。适合刚接触长连接、想把设备消息实时推送到手机App的人也适合正在做车联网、智能家居、物流追踪、停车场设备对接这类项目的开发同学参考。代码以Kotlin Android Studio为例基于Eclipse Paho客户端库实现拿到就能跑。1. 为什么是MQTTAndroid长连接方案的选型对比1.1 HTTP轮询为什么越来越难用不少人对“长连接”的第一反应是WebSocket或者干脆自己写一个Socket线程。但在物联网场景里设备端、服务端、App端三端通信MQTT的发布订阅模型比点对点模型更省心。先说说为什么HTTP轮询在设备消息推送场景撑不住。轮询的逻辑很简单App每隔几秒请求一次接口看服务端有没有新消息。问题在于大部分时间请求回来都是空的网络开销和电量消耗却一点没少。如果设备数量上来了比如一个停车场几十个相机每台相机都要推识别结果服务器每秒钟要扛几千个无效请求纯属浪费。更麻烦的是轮询的实时性取决于间隔间隔设短了压力大设长了消息延迟这是一种天生没法调优的矛盾。1.2 MQTT与WebSocket、自研TCP的取舍WebSocket也是全双工长连接适合浏览器和App之间Push消息但它没有主题订阅、没有QoS分级、没有遗嘱消息服务端要自己维护每个连接和消息路由。业务一复杂这部分逻辑全得自己写。自研TCP长连接就更重了要解决粘包拆包、心跳超时、重连退避、消息确认一套下来至少一两个月的开发量而且稳定性完全依赖团队经验。MQTT走的是一条轻量级路线。协议本身基于发布订阅模型客户端通过主题订阅消息不用关心消息是从哪台设备来的Broker负责转发和存储QoS机制保证消息不丢KeepAlive心跳由协议内置不需要自己设计。另外MQTT的报文头非常小固定头最少只有2个字节对低带宽、弱网环境非常友好这一点在移动网络下特别重要。1.3 什么场景适合用这个demo我这边实际用的场景是停车场项目车牌识别相机作为消息发布端识别到车牌后把结果推到BrokerAndroid端作为订阅端实时接收用于开闸、计费、推送通知。这个模式换成智能门锁的状态上报、充电桩的充电进度推送、物流扫码枪的消息同步逻辑完全一样。所以不要把这个demo当成一个单纯的技术练习它是“设备端到手机端消息实时触达”这一类问题的基础模板。把这个链路跑通后续接任何物联网设备都有了一个可复用的底座。2. 搭建最小可跑工程依赖引入与Broker准备2.1 选型Eclipse Paho还是其他客户端库Android上可选的MQTT客户端库不少常见的有Eclipse Paho、HiveMQ MQTT Client以及各云厂商的SDK。个人项目和学习demo我推荐Eclipse Paho理由有三个开源时间最长社区案例多踩坑资料丰富同时提供MqttAndroidClient和MqttAsyncClient两套APIService场景和后台场景都能覆盖和EMQX、Mosquitto等主流Broker完全兼容不需要为某个云平台绑定。注意Paho有两个包一个是我们需要的基础Mqttv3库协议实现一个是Android Service库负责在后台维护连接生命周期。两个都要引入缺一个就没法在Android上跑长连接。dependencies { implementation org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5 implementation org.eclipse.paho:org.eclipse.paho.android.service:1.1.1 }2.2 Broker环境怎么准备没有Broker就没有消息中转站。本地调试最简单的方案是Docker跑一个EMQX或者Mosquitto。以Mosquitto为例# 本地快速启动 docker run -d --name mqtt-broker -p 1883:1883 -p 9001:9001 eclipse-mosquitto:2如果不需要本地部署也可以直接用EMQX云服务或者公共测试Broker。但我不建议把公共Broker用在生产环境原因后面讲安全时会说。局域网调试时Android模拟器访问宿主机要用10.0.2.2代替localhost这个细节曾经卡了我半小时。2.3 权限与Service注册AndroidManifest.xml里要补上网络权限和Service声明。MQTT长连接需要在后台运行所以WAKE_LOCK权限也要带上避免屏幕关闭后CPU休眠导致心跳发送失败。uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.WAKE_LOCK / service android:nameorg.eclipse.paho.android.service.MqttService /这里有个容易忽略的点MqttService如果没注册运行时会直接报ServiceNotFoundException而不是一个明确的MQTT错误排查起来很迷茫。所以先把Service声明检查一遍。3. 核心代码连接、订阅、发布一条龙3.1 建立连接的正确姿势连接这一步是踩坑重灾区。先看一段相对完整的初始化代码class MqttManager(context: Context) { private val mqttClient: MqttAndroidClient MqttAndroidClient( context.applicationContext, tcp://192.168.1.100:1883, android_ System.currentTimeMillis() ) fun connect() { val options MqttConnectOptions().apply { isCleanSession false connectionTimeout 10 keepAliveInterval 30 isAutomaticReconnect true userName app_client password your_password.toCharArray() } mqttClient.setCallback(object : MqttCallbackExtended { override fun connectComplete(reconnect: Boolean, serverURI: String?) { Log.d(TAG, 连接完成是否重连: $reconnect) // 连接成功后再订阅防止订阅丢失 subscribe(device//report, 1) } override fun connectionLost(cause: Throwable?) { Log.e(TAG, 连接断开: ${cause?.message}) } override fun messageArrived(topic: String?, message: MqttMessage?) { val payload String(message!!.payload, Charsets.UTF_8) Log.d(TAG, 收到消息: $topic - $payload) } override fun deliveryComplete(token: IMqttDeliveryToken?) { Log.d(TAG, 消息发送完成) } }) mqttClient.connect(options, null, object : IMqttActionListener { override fun onSuccess(asyncActionToken: IMqttToken?) { Log.d(TAG, connect onSuccess) } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.e(TAG, connect onFailure: ${exception?.message}) } }) } }几个关键点clientId必须保证唯一如果两个客户端用了同一个clientId连接同一个Broker后一个会把前一个踢下线。所以我在clientId后面拼了时间戳。isCleanSession false表示Broker会保留客户端的会话和离线期间的订阅消息。如果业务不需要离线补发设成true就行可以减少Broker内存占用。keepAliveInterval 30表示客户端每30秒发一次心跳这个值不是拍脑袋定的网络环境差就设小一点正常业务30到60秒都合理。connect()是异步的不要等它成功后再立即去subscribe正确做法是在connectComplete回调里订阅这样断线重连后也能自动恢复订阅。3.2 订阅与发布主题设计先行MQTT的消息路由靠主题“怎么设计主题”直接决定后期代码好不好维护。我常用的规则是类型主题示例说明设备上报device/{deviceId}/report设备状态、识别结果平台下发server/{deviceId}/commandApp或服务端下发指令广播通知broadcast/{type}所有在线客户端关注订阅方可以用通配符减少订阅次数// 订阅某个设备的所有上报消息 mqttClient.subscribe(device/device_001/report, 1) // 或者订阅所有设备的上报消息 mqttClient.subscribe(device//report, 1)发布消息一般用JSON结构体。我通常会把业务类型和内容分开val json JSONObject().apply { put(type, open_door) put(timestamp, System.currentTimeMillis()) put(target, gate_01) } val message MqttMessage(json.toString().toByteArray(Charsets.UTF_8)) message.qos 1 message.isRetained false mqttClient.publish(server/device_001/command, message)注意payload是byte数组编码统一UTF-8避免不同手机默认编码不一致导致中文乱码。这个坑在messageArrived里用String(message.payload)直接转时最容易出现。3.3 QoS到底该怎么选MQTT的QoS有三个级别很多新手看到就晕其实不用纠结按业务对消息可靠性的要求选就行QoS 0至多一次消息可能丢适合实时性高、重复无所谓的场景比如设备秒级上报的温度数据QoS 1至少一次消息不丢但可能重复适合指令下发、订单状态变更QoS 2恰好一次性能和开销最大Android端能不用就不用绝大多数业务QoS 1就够。之前处理停车场开闸指令时我用的就是QoS 1偶尔会收到重复指令所以指令处理逻辑里一定要做幂等校验比如根据消息里的事务ID判断是否已经处理过。4. 长连接稳定的命门心跳保活与重连策略4.1 KeepAlive是怎么工作的MQTT协议内置了心跳机制。客户端如果在一个keepAliveInterval周期内没有发送任何数据包就会自动发送一个PINGREQ控制报文Broker收到后回PINGRESP这样双方都知道连接还活着。但如果Android端进程被系统冻结、网络被切换、或者WiFi信号弱到连心跳包都发不出去Broker在超时时间内一般是1.5倍keepAliveInterval没收到心跳就会主动断开这条连接。从App的角度看就是“我明明没干什么连接自己掉了”。所以长连接App不能只靠协议内置心跳还要在系统层面做配合。前台Service WAKE_LOCK是Android上的标准方案避免屏幕关闭后应用进程被系统挂起。如果你只是做个demo不转后台可以不管只要目标是生产可用这步绕不开。4.2 自动重连Paho替你做了但没完全做Paho的MqttConnectOptions里有个isAutomaticReconnect属性置为true之后底层在连接断开时会自动按指数退避策略发起重连。最初间隔是1秒然后2秒、4秒、8秒……逐渐拉大避免断线瞬间所有客户端同时重连把Broker冲垮。但有两点它处理不了必须由开发者在代码里补一是重连成功后订阅关系可能丢失必须在connectComplete回调里重新subscribe。这也是我强调用MqttCallbackExtended而不是普通MqttCallback的原因普通回调里没有connectComplete这个方法。二是如果网络本身断开了自动重连是空转的因为它只管TCP和MQTT层感知不到系统的网络状态。正确做法是监听网络变化网络恢复时主动触发一次重连。class NetworkReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val cm context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val activeNetwork cm.activeNetworkInfo if (activeNetwork?.isConnected true) { MqttManager.getInstance().checkAndReconnect() } } }4.3 手动重连的兜底逻辑我在项目里还加过一层手动兜底监听connectionLost回调如果发现连接断开且当前网络可用立刻关闭旧clientnew一个MqttAndroidClient重新connect。不是我不信任Paho的自动重连而是某些Android定制ROM环境下自动重连触发不够及时手动重建连接更可控。这里有个细节MqttAndroidClient连接失败之后状态可能卡在connecting直接再connect会报“Client is currently connecting”这类错误。所以手动重建之前一定要先try { mqttClient.disconnect() mqttClient.close() } catch (e: Exception) { // 连接已断开时这里会抛异常忽略即可 }然后再new新的客户端实例。这套组合拳下来长连接在真机上的稳定性明显提升。5. 从demo到上线的进阶我在这套方案里踩过的坑5.1 Android系统的省电策略是最容易忽视的杀手Android 6.0之后系统引入了Doze模式和应用待机设备静止且屏幕关闭一段时间网络访问会被暂停MQTT心跳自然也就发不出去。Broker那边一旦判定超时连接就断开了。解决方式除了前台Service WAKE_LOCK还可以申请电池优化白名单REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限但这需要向用户弹窗申请而且在国内定制ROM上不一定有效。最稳妥的做法是核心功能必须用前台服务承载告知用户“正在运行”并配一个常驻通知。这个体验虽然重一点但这是Android平台对长连接应用提出的硬性要求。5.2 掉线风暴重连策略不能只靠客户端想象一个场景地下停车场里手机信号本来就差大量车主的App同时断连一旦某辆车开到信号好的地方所有客户端几乎同时重连Broker会瞬间收到大量连接请求。如果不做任何限流服务端CPU和连接数会直接被打满。做法有几个层面客户端侧重连前加随机延时让重连时间散开Broker侧EMQX有连接速率限制必要时开启消息侧业务量大的推送不要在topic长时间堆积消息Broker内网带宽和转发压力都要提前压测。5.3 生产环境务必上TLS默认的tcp://协议是明文传输设备识别结果、指令这类数据在链路上可以被抓包看到。我在测试环境用明文没问题但一到客户现场就会要求加密。Paho支持ssl://协议加上正确的SocketFactory就行。如果用的是EMQX可以配置单向TLSApp端只需信任服务器证书val sslContext SSLContext.getInstance(TLS) sslContext.init(null, trustStore, SecureRandom()) val options MqttConnectOptions() options.socketFactory sslContext.socketFactory另外用户名密码不要硬编码在客户端至少要从服务端接口动态下发或者做证书级别的设备认证避免APK被反编译后Broker直接裸奔。5.4 messageArrived回调线程与UI刷新Paho收到消息后默认在后台线程回调直接在这里更新TextView会崩溃。记得用Handler或runOnUiThread切线程。另外回调里不要做耗时操作收到的消息先放进队列由业务线程消费否则消息一多会阻塞Paho的内部消息分发。截个我当时调试的教训我在messageArrived里直接解析JSON并写数据库结果消息频率一高回调越来越慢最后整个App卡到掉线。后来改成消息加入ConcurrentLinkedQueue单独线程处理问题立刻消失。写在最后的一点体会这个demo虽然名字叫“长连接demo”但它真正的价值不在于跑通连接而在于理解MQTT和Android后台机制之间互相配合的那套逻辑。我见过很多项目倒在了细节上主题规划不合理、QoS选错、断线重连后订阅丢失、后台被系统杀死。这些问题的共性在于大家把MQTT当成了一个简单的“网络库”而忽略了它自带的那套可靠性语义需要在Android这种“随时可能限制后台”的系统上做额外适配。如果要从这个demo往生产项目走建议按这个顺序验证先保证App在前台时连接稳定、收发消息正常再测试锁屏和切后台观察心跳和连接是否保持最后模拟弱网环境看断线重连后的订阅恢复是否正确。这三步都通过了这套长连接方案才算真正落地。本文还有配套的精品资源点击获取