要在安卓开发中使用 MQTT 协议进行通讯,需要使用 Eclipse Paho 客户端组件。在此组件的基础上我们结合 Android 架构模型进行封装,以便更好地调用 MQTT 服务。本文基于安卓 Java 代码开发。
添加 Eclipse Paho 依赖
在安卓工程的build.gradle中添加 Eclipse Paho 的依赖:
arduino
dependencies {
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
implementation 'com.blankj:utilcodex:1.31.0'
}
添加后在 Android Studio 中刷新下工程依赖。
核心 Service
MQTT 组件基本功能有:
- 创建连接及断开连接
- 收到 MQTT 消息(MQTT 的订阅功能)以及发布 MQTT 消息
进阶的功能要求:
- 网络连接断开后能够自动重连
- 订阅多个 Topic
- 已订阅的 Topic 支持修改为新的 Topic
- 多个 MQTT 实例,不同地址的多个实例(暂未实现)
- 在手机端上,保证 MQTT 连接在应用后台也能维持
下面是这个服务的核心源码MetaMqttService ,它继承自 Android 的 Service。它在后台运行,负责管理 MQTT 客户端的生命周期、连接、订阅、发布和重连。
java
package com.ajaxjs.android.mqtt;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.NetworkUtils;
import com.blankj.utilcode.util.TimeUtils;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MetaMqttService extends Service {
private MqttClient mqttClient;
private MqttConnectOptions options;
private ScheduledExecutorService scheduler;
public MqttConfig config;
@Nullable
@Override
public IBinder onBind(Intent intent) {
// Log.i(MqttManager.TAG, "onBind:::config" + config);
//
// if (config == null) { // 组件时候不知为什么 config 提前访问 所以变成 null
// config = new MqttConfig()
// .url(intent.getStringExtra("serverUrl"))
// .port(intent.getStringExtra("serverPort"))
// .client(intent.getStringExtra("clientId"))
// .username(intent.getStringExtra("username"))
// .password(intent.getStringExtra("password"))
// .topic(intent.getStringExtra("topic"))
// .timeout(intent.getIntExtra("timeout", 10))
// .beat(intent.getIntExtra("beatTime", 20))
// .retry(intent.getIntExtra("reConnectTime", 10));
// }
return new MsgBinder();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(this::toConnectMqtt, 1000, TimeUnit.MICROSECONDS);
return super.onStartCommand(intent, flags, startId);
}
public class MsgBinder extends Binder {
public MetaMqttService getService() {
return MetaMqttService.this;
}
}
public void setConfig(MqttConfig config) {
this.config = config;
}
private void toConnectMqtt() {
Log.i(MqttManager.TAG, "NetworkUtils.isAvailable():" + NetworkUtils.isAvailable());
if (config == null) {
Log.e(MqttManager.TAG, "MQTT config not set");
return;
}
if (config.mCallBack == null) {
Log.e(MqttManager.TAG, "MQTT callback not set");
return;
}
if (NetworkUtils.isAvailable()) {
String url = "tcp://" + config.serverUrl + ":" + config.serverPort;
try {
options = new MqttConnectOptions();
options.setCleanSession(false);
options.setUserName(config.username);
options.setWill("died/", ("mqtt died at time -- " + TimeUtils.getNowString()).getBytes(StandardCharsets.UTF_8), 1, false);
options.setPassword(config.password.toCharArray());
options.setConnectionTimeout(config.timeout);
options.setKeepAliveInterval(config.beatTime);
mqttClient = new MqttClient(url, config.clientId, new MemoryPersistence());
mqttClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
Log.e(MqttManager.TAG, "连接丢失");
config.mCallBack.connectLost();
startConnectMachine();
}
@Override
public void messageArrived(String arriveTopic, MqttMessage message) {
Log.i(MqttManager.TAG, "收到MQTT消息" + arriveTopic);
config.mCallBack.messageArrived(arriveTopic, message);
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
config.mCallBack.pushComplete();
}
});
mqttClient.connect(options);
subscribeTopics();
Log.i(MqttManager.TAG, "连接 MQTT 成功");
config.mCallBack.connectSuccess(mqttClient);
stopConnectMachine();
} catch (MqttException e) {
Log.e(MqttManager.TAG, "Connect failed" + e, e);
config.mCallBack.connectFailed(e);
startConnectMachine();
}
} else
config.mCallBack.connectIntentError();
}
private void subscribeTopics() {
try {
if (config.topics != null && !config.topics.isEmpty()) {
String[] topicArray = config.topics.toArray(new String[0]);
mqttClient.subscribe(topicArray);
Log.i(MqttManager.TAG, "订阅多个主题成功: " + config.topics.toString());
} else if (config.topic != null && !config.topic.isEmpty()) {
mqttClient.subscribe(config.topic, 1);
Log.i(MqttManager.TAG, "订阅单个主题成功: " + config.topic);
}
} catch (MqttException e) {
Log.e(MqttManager.TAG, "订阅主题失败: " + e, e);
}
}
public void startConnectMachine() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleWithFixedDelay(() -> {
if (mqttClient != null) {
if (!mqttClient.isConnected()) {
try {
mqttClient.connect(options);
subscribeTopics();
Log.i(MqttManager.TAG, "重连MQTT成功");
config.mCallBack.reConnectSuccess();
stopConnectMachine();
} catch (Exception e) {
Log.e(MqttManager.TAG, "重连MQTT失败", e);
config.mCallBack.reConnectFailed();
}
} else
Log.i(MqttManager.TAG, "检测MQTT正常");
} else {
Log.e(MqttManager.TAG, "Client为空");
config.mCallBack.connectClientError();
}
}, 0, config.reConnectTime * 1000L, TimeUnit.MILLISECONDS);
}
public void stopConnectMachine() {
if (scheduler != null) {
if (!scheduler.isShutdown())
scheduler.shutdown();
}
}
/**
* 断开MQTT连接
* // 断开连接
* MetaMqtt.with(context).disconnect();
*/
public void disconnect() {
try {
if (mqttClient != null && mqttClient.isConnected()) {
mqttClient.disconnect();
mqttClient.close();
Log.i(MqttManager.TAG, "MQTT连接已断开");
}
} catch (MqttException e) {
Log.e(MqttManager.TAG, "断开MQTT连接失败: " + e, e);
} finally {
stopConnectMachine();
}
}
/**
* 发布MQTT消息
*
* @param topic 主题
* @param message 消息内容
* @param qos 服务质量 0, 1, 2
* @param retained 是否保留
*/
public void publish(String topic, String message, int qos, boolean retained) {
try {
if (mqttClient != null && mqttClient.isConnected()) {
MqttMessage mqttMessage = new MqttMessage();
mqttMessage.setPayload(message.getBytes(StandardCharsets.UTF_8));
mqttMessage.setQos(qos);
mqttMessage.setRetained(retained);
mqttClient.publish(topic, mqttMessage);
Log.i(MqttManager.TAG, "发布消息成功: " + topic + " - " + message);
} else
Log.e(MqttManager.TAG, "MQTT 未连接,无法发布消息");
} catch (MqttException e) {
Log.e(MqttManager.TAG, "发布消息失败: " + e, e);
}
}
/**
* 发布MQTT消息(默认QoS=1,不保留)
*
* @param topic 主题
* @param message 消息内容
*/
public void publish(String topic, String message) {
publish(topic, message, 1, false);
}
public void changeTopic(String newTopic) {
try {
// 先取消所有之前的订阅
// mqttClient.unsubscribe("#");
mqttClient.unsubscribe(config.topic);
mqttClient.subscribe(newTopic);
} catch (MqttException e) {
Log.e(MqttManager.TAG, "取消所有之前的订阅失败:" + config.topic);
return;
}
config.topic = newTopic;
}
}
启动
启动入口在MqttManager 单例类。它与 MetaMqttService 紧密配合,提供了更高层次的封装和易于使用的 API。MqttManager 是应用层代码与后台 MetaMqttService 之间的桥梁。它负责启动并绑定到 MetaMqttService,获取服务的 IBinder(即 MsgBinder),从而能够调用服务内部的方法。
start(Context ctx, MqttConfig cfg)是初始化的入口。- 它首先创建一个指向
MetaMqttService.class的Intent。 - 调用
ctx.bindService(...),传入Intent和一个匿名的ServiceConnection实现。BIND_AUTO_CREATE标志意味着如果服务尚未运行,Android 系统会先启动它。onServiceConnected回调:当服务成功绑定后,系统会调用此方法。在这里,IBinder service参数就是MetaMqttService.MsgBinder的实例。MqttManager将其保存到私有成员msgBinder中。然后,它通过msgBinder.getService()获取到MetaMqttService的实例,并调用setConfig(cfg)将配置信息传递给服务。onServiceDisconnected回调:当服务意外断开(例如进程被杀死)时调用。在此实现中,它只是记录日志。
- 重要 : 代码中还调用了
ctx.startService(serviceIntent);。在大多数情况下,如果bindService时指定了BIND_AUTO_CREATE,服务会自动启动。同时调用startService可能会导致服务被多次启动(尽管 Android 通常只会创建一个实例,但onStartCommand会被多次调用)。通常,如果只需要绑定服务,仅调用bindService即可。如果希望服务在所有绑定都解除后仍能在后台运行,才需要startService。这里的意图可能是确保服务即使在没有绑定的情况下也能保持运行一段时间,但这可能不是最佳实践,因为unbindService后服务的行为可能不符合预期(取决于onUnbind和onStartCommand的返回值)。startService确保了MetaMqttService.onStartCommand一定会被调用以启动连接过程。
MqttManager 源码如下:
java
package com.ajaxjs.android.mqtt;
import static android.content.Context.BIND_AUTO_CREATE;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
public class MqttManager {
public static final String TAG = "MQTT";
private static MqttManager INSTANCE;
/**
* 链式顺序调用,MqttManager
*
* @return MetaDriver
*/
public static MqttManager getInstance() {
if (INSTANCE == null)
INSTANCE = new MqttManager();
return INSTANCE;
}
/**
* service binder
*/
private MetaMqttService.MsgBinder msgBinder;
/**
* 开启 MQTT 服务
*/
public void start(Context ctx, MqttConfig cfg) {
Intent serviceIntent = new Intent(ctx, MetaMqttService.class);
ctx.bindService(serviceIntent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
msgBinder = (MetaMqttService.MsgBinder) service;
msgBinder.getService().setConfig(cfg);
Log.e(MqttManager.TAG, "MetaServiceConnection.onServiceConnected.setConfig(cfg)>>>>>>");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(MqttManager.TAG, "MetaServiceConnection.onServiceDisconnected");
}
}, BIND_AUTO_CREATE);
ctx.startService(serviceIntent);
Log.i(TAG, "MQTT 初始化成功");
}
/**
* 修改主题
* e.g: MqttManager.getInstance().changeTopic("/wyndme/robot/3CDC7585ACD1/chat");
*
* @param newTopic New topic name
*/
public void changeTopic(String newTopic) {
if (msgBinder != null)
msgBinder.getService().changeTopic(newTopic);
}
/**
* 断开MQTT连接
*/
public void disconnect() {
if (msgBinder != null)
msgBinder.getService().disconnect();
}
/**
* 发布 MQTT 消息
*
* @param topic 主题
* @param message 消息内容
* @param qos 服务质量 0, 1, 2
* @param retained 是否保留
*/
public void publish(String topic, String message, int qos, boolean retained) {
if (msgBinder != null)
msgBinder.getService().publish(topic, message, qos, retained);
}
/**
* 发布 MQTT 消息(默认QoS=1,不保留)
*
* @param topic 主题
* @param message 消息内容
*/
public void publish(String topic, String message) {
publish(topic, message, 1, false);
}
}
MqttManager 通过单例模式和 ServiceConnection,将 MetaMqttService 封装起来。应用开发者只需要关心 MqttManager 提供的 start, publish, changeTopic, disconnect 等方法,而不需要直接与 Android 的 Service 生命周期和 Paho 库打交道。这种分层设计提高了代码的模块化程度和易用性。
MqttManager 与 Service 之间的关系
MqttManager 是应用层代码与后台 MetaMqttService 之间的桥梁。它负责启动并绑定到 MetaMqttService,获取服务的 IBinder(即 MsgBinder),从而能够调用服务内部的方法。
MqttManager负责:- 启动和绑定
MetaMqttService。 - 将
MqttConfig配置传递给服务。 - 提供一个简单的、应用层友好的接口(
publish,changeTopic,disconnect)。
- 启动和绑定
MetaMqttService负责:- 管理实际的 Paho
MqttClient。 - 处理连接、重连、订阅、发布等核心 MQTT 逻辑。
- 通过回调通知
MqttManager(间接通知应用)状态变化。
- 管理实际的 Paho
通过 IBinder,应用层组件可以方便地与这个后台服务交互,实现可靠的 MQTT 通信。当其他组件(通常是 Activity 或另一个 Service)尝试绑定到 MetaMqttService 时,onBind 方法被调用。它返回一个 MsgBinder 实例。MsgBinder 继承自 android.os.Binder,允许绑定的组件通过 getService() 方法获取到 MetaMqttService 的实例,从而可以直接调用服务内部的方法(如 publish, changeTopic, disconnect)。
当服务被启动时(即使已经存在实例),onStartCommand 会被调用。这里创建了一个 ScheduledExecutorService (scheduler),并安排 toConnectMqtt 方法在 1 秒后执行。这是建立 MQTT 连接的入口点。
下面是相关的核心组件:
MqttClient: 来自 Eclipse Paho 库,代表一个 MQTT 客户端实例。它是与 MQTT 代理(Broker)进行交互的主要对象。MqttConnectOptions: 配置连接参数的对象,如用户名、密码、超时时间、心跳间隔等。MqttCallback: 一个接口,用于处理来自 MQTT 代理的异步事件,如连接丢失、收到消息、消息发送完成等。ScheduledExecutorService: Java 并发包中的一个工具,用于执行定时任务。在此代码中,它主要用于实现自动重连机制。
建立连接 (toConnectMqtt)的工作流程分析:
- 检查网络可用性(
NetworkUtils.isAvailable())和config、callback是否已设置。 - 根据
config中的信息构建 MQTT 服务器 URL (tcp://...)。 - 创建
MqttConnectOptions实例options,并设置各种连接参数,包括:setCleanSession(false): 设置为非干净会话。这意味着服务器会存储客户端的订阅信息和离线消息。当客户端重新连接时,可以接收离线期间发送给它的消息(前提是消息的 QoS >= 1)。setWill(...): 设置"遗嘱"消息。如果客户端异常断开连接(如崩溃或网络中断),服务器会将这条消息发布到指定的died/主题。setUserName,setPassword: 设置认证凭据。setConnectionTimeout,setKeepAliveInterval: 设置连接超时时间和心跳间隔(保持连接活跃的频率)。
- 创建
MqttClient实例mqttClient,传入 URL、客户端 ID 和持久化策略。 - 为
mqttClient设置一个匿名的MqttCallback实现,定义了三个核心回调方法:connectionLost: 当与代理的连接意外丢失时调用。它会记录日志,通知上层回调connectLost(),并调用startConnectMachine启动重连机制。messageArrived: 当客户端收到发布到其订阅主题的消息时调用。它记录日志并将消息转发给上层回调messageArrived。deliveryComplete: 当客户端成功向代理发布一条消息并收到确认(对于 QoS > 0)时调用。通知上层回调pushComplete。
- 调用
mqttClient.connect(options)尝试连接到服务器。 - 连接成功后,调用
subscribeTopics()订阅配置中指定的一个或多个主题。 - 记录成功日志,通知上层回调
connectSuccess(mqttClient),并调用stopConnectMachine停止可能正在进行的重连任务。
使用这个 Service
你需要在AndroidManifest.xml中声明这个 Service:
xml
<!-- MQTT 组件 -->
<service android:name="com.ajaxjs.android.mqtt.MetaMqttService" />
如下图所示 
重连机制 (startConnectMachine / stopConnectMachine)
ScheduledExecutorService是 Java 并发包中的一个工具,用于执行定时任务。在此代码中,它主要用于实现自动重连机制。
startConnectMachine: 启动一个固定延迟的周期性任务(间隔为config.reConnectTime秒)。- 该任务检查
mqttClient是否存在且未连接 (!mqttClient.isConnected()). - 如果满足条件,尝试调用
mqttClient.connect(options)重新连接。 - 连接成功后,重新订阅主题(
subscribeTopics()),通知上层回调reConnectSuccess(),并调用stopConnectMachine停止重连任务。 - 如果连接失败,通知上层回调
reConnectFailed(),重连任务继续按设定间隔执行。 stopConnectMachine: 关闭并清理scheduler,停止重连循环。
更换订阅主题 (changeTopic)
- 使用
mqttClient.unsubscribe(oldTopic)取消对旧主题的订阅。 - 使用
mqttClient.subscribe(newTopic)订阅新主题。 - 更新
config.topic字段为新的主题名称。
配置类
MqttConfig是一个自定义配置类,用于存储 MQTT 服务器地址、端口、客户端ID、用户名、密码、需要订阅的主题列表、超时时间、心跳时间、重连间隔以及一个回调接口 mCallBack。源码如下:
java
package com.ajaxjs.android.mqtt;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class MqttConfig implements Serializable {
/**
* 服务器地址
*/
public String serverUrl;
/**
* 服务器端口
*/
public String serverPort;
/**
* MQTT 客户端ID
*/
public String clientId;
/**
* MQTT 账号
*/
public String username;
/**
* MQTT 密码
*/
public String password;
/**
* MQTT 主题(单个)
*/
public String topic;
/**
* MQTT 主题列表(多个)
*/
public List<String> topics = new ArrayList<>();
/**
* MQTT 超时时间(默认 10s)
*/
public int timeout = 10;
/**
* MQTT 心跳时间(默认 20s)
*/
public int beatTime = 20;
/**
* MQTT 重连时间(默认 10s)
*/
public int reConnectTime = 10;
/**
* MQTT 遗嘱,1.0.1版本不用
*/
public String will;
/**
* 回调接口
*/
public MetaMqttCallBack mCallBack;
/**
* 设置服务器地址,必须
* <br/>get the Url
*
* @param url 地址
* @return MetaMqtt
*/
public MqttConfig url(String url) {
this.serverUrl = url;
return this;
}
/**
* 设置服务器端口,必须
* <br/>get the Port
*
* @param port 端口
* @return MetaMqtt
*/
public MqttConfig port(String port) {
this.serverPort = port;
return this;
}
/**
* 设置MQTT客户端ID,非必须,默认为"mqttApp"
* <br/>get the client
*
* @param client 客户端ID
* @return MetaMqtt
*/
public MqttConfig client(String client) {
this.clientId = client;
return this;
}
/**
* 设置MQTT 用户名,必须
* <br/>get the username
*
* @param username 用户名
* @return MetaMqtt
*/
public MqttConfig username(String username) {
this.username = username;
return this;
}
/**
* 设置MQTT 密码
*
* @param password 密码
* @return MetaMqtt
*/
public MqttConfig password(String password) {
this.password = password;
return this;
}
/**
* 设置MQTT 主题,必须
*
* @param topic 主题
* @return MetaMqtt
*/
public MqttConfig topic(String topic) {
this.topic = topic;
return this;
}
/**
* 添加MQTT 主题(支持多主题订阅)
*
* @param topic 主题
* @return MetaMqtt
*/
public MqttConfig addTopic(String topic) {
if (topics == null)
topics = new ArrayList<>();
topics.add(topic);
return this;
}
/**
* 设置MQTT超时时间,非必须,默认10S
*
* @param timeout 超时时间
* @return MetaMqtt
*/
public MqttConfig timeout(int timeout) {
this.timeout = timeout;
return this;
}
/**
* 设置MQTT超时时间,非必须,默认10S
*
* @param reConnectTime 重连时间
* @return MetaMqtt
*/
public MqttConfig retry(int reConnectTime) {
this.reConnectTime = reConnectTime;
return this;
}
/**
* 设置MQTT心跳时间,非必须,默认20S
*
* @param beatTime 心跳时间
* @return MetaMqtt
*/
public MqttConfig beat(int beatTime) {
this.beatTime = beatTime;
return this;
}
/**
* 接收到数据回调,必须
*
* @param callBack 回调
* @return MetaMqtt
*/
public MqttConfig callback(MetaMqttCallBack callBack) {
this.mCallBack = callBack;
return this;
}
}
这里没有使用繁琐的 getter/setter,直接使用字段访问。
MqttManager.Callback是一个自定义的回调接口用于将 MQTT 服务的状态变化(如连接成功、失败、消息到达等)通知给使用该服务的应用层代码。
java
package com.ajaxjs.android.mqtt;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
/**
* Callback for MQTT
*/
public interface MetaMqttCallBack {
/**
* 连接成功,可以使用返回的 mqttClient 发送消息
*/
void connectSuccess(MqttClient mqttClient);
void connectFailed(MqttException e);
/**
* 连接丢失不用理会,有自动重连机制
*/
void connectLost();
void messageArrived(String topic, MqttMessage message);
void pushComplete();
void reConnectSuccess();
/**
* 重连失败不用理会,有自动重连机制
*/
void reConnectFailed();
/**
* client 为 null 此错误可能无法通过重连机制恢复 需检查是否正常配置 MQTT
*/
void connectClientError();
/**
* 没有连接到网络
*/
void connectIntentError();
}
使用例子
例子安排在最后。
java
package com.example.myapplication.mqtt;
import android.content.Context;
import com.ajaxjs.android.mqtt.MqttConfig;
import com.ajaxjs.android.mqtt.MqttManager;
public class Mqtt {
public static void init(Context context) {
MqttConfig cfg = new MqttConfig().url("1.1.1.1") // 服务器URL
.port("1883") // 服务器端口
.client("mqtttest") // MQTT客户端ID,默认为mqttApp,非必须,但多端不能重复,否则导致无限重连
.username(null) // MQTT 用户名,这是匿名的使用方式
.password("") // MQTT 密码
.topic("/xxxxx/chat")// MQTT 订阅的主题,不能为空
// .addTopic("/wyndme/robot/3CDC7585ACD0/chat") // 多个主题的使用方式
// .addTopic("/wyndme/robot/3CDC7585ACD1/chat")
.timeout(10) // MQTT 超时时间,默认10S,非必须
.beat(20) // MQTT 心跳时间,默认20S,非必须
.retry(10) // MQTT 重试时间,默认10S,非必须
.callback(new MessageMqttCallback());
MqttManager.getInstance().start(context, cfg);
}
}
当前是单例模式 (Singleton) 通过 getInstance() 方法确保整个应用进程中只有一个 MqttManager 实例。在将来的版本中提供多个 MQTT 通讯实例的功能。
鸣谢
本组件的开发一开始是得益于开源代码 metamqtt 之贡献的,特此鸣谢!没有它就没有本文以及该组件的研发!