【MQTT】基于 Android 设备接入物联网平台最佳实践

一、关于 MQTT

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

网址:https://mqtt.org/

1.1 Eclipse Paho

Paho 项目旨在为机器对机器 (M2M) 和物联网 (IoT) 领域的新兴、现有及未来应用提供可靠的开源实现,这些实现基于开放且标准的即时通讯协议。Paho 充分考虑了设备连接固有的物理和成本限制,其目标包括有效实现设备和应用之间的解耦,旨在保持市场开放,并促进可扩展的 Web 和企业中间件及应用的快速发展。

Paho 包含用于嵌入式平台的MQTT发布/订阅客户端实现,以及社区确定的相应服务器支持。Paho Java Client 是一个用 Java 编写的 MQTT 客户端库,用于开发运行在 JVM 或其他 Java 兼容平台(例如 Android)上的应用程序。

网址:https://eclipse.dev/paho/

1.2 MQTT.fx

MQTT.fx® 5 是我们用于测试开发和生产环境中物联网路由的工具。它允许您连接到开发和/或生产代理,并在编写代码之前测试您的项目。由于无需编程即可运行这些测试用例,这不仅意味着可以显著减少您的手动工作量,而且还能显著提高软件质量。

MQTT.fx 是一款基于Eclipse Paho,使用Java语言编写的MQTT客户端工具。支持通过Topic订阅和发布消息,用来前期和物理云平台调试非常方便。

网址:https://www.softblade.de/

二、基于Android的MQTT网络架构

设计一个基于Android的MQTT网络架构,核心在于解耦健壮性 (断连重连)和易用性

我们将采用 单例模式 (Singleton) 结合 观察者模式 (Observer) 的设计。底层使用成熟的 Eclipse Paho Android Client 库,因为它提供了对Android Service的封装,能够更好地处理后台运行和心跳保活。

2.1 架构设计概览

  • MqttManager (Core) : 单例核心类,负责管理 MqttAndroidClient 实例,处理连接、断开、重连逻辑。
  • IMqttEventListener: 接口,用于业务层接收消息、连接状态变化。
  • TopicDispatcher: 内部逻辑,将收到的MQTT消息根据Topic分发给不同的监听器。
  • Config: 独立的配置参数,便于项目移植。

2.2 依赖准备 (build.gradle)

app/build.gradle 中添加 Paho MQTT 依赖(注意:Paho Android Service 需要添加 localbroadcastmanagersupport-v4 的相关支持,或者使用兼容版本):

groovy 复制代码
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'
    // 如果是AndroidX项目,可能需要添加此依赖以支持Paho Service
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
}

AndroidManifest.xml 中注册 Service:

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application ...>
    <!-- MqttService -->
    <service android:name="org.eclipse.paho.android.service.MqttService" />
</application>

2.3 核心代码实现 (Java)

2.3.1 定义回调接口 (IMqttEventListener.java)

这个接口用于统一业务层接收消息和状态。

java 复制代码
package com.example.mqtt.core;

public interface IMqttEventListener {
    // 收到消息
    void onMessageArrived(String topic, String message);
    
    // 连接成功
    void onConnected();
    
    // 连接断开(包含异常断开)
    void onDisconnected(String reason);
}

2.3.2 MQTT 管理核心类 (MqttManager.java)

这是最关键的模块,封装了连接配置、自动重连、消息分发和线程切换(确保回调在主线程)。

java 复制代码
package com.example.mqtt.core;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * MQTT 管理单例
 * 负责统一连接、断开、发布、订阅以及网络状态处理
 */
public class MqttManager {
    private static final String TAG = "MqttManager";
    private static MqttManager instance;
    private MqttAndroidClient client;
    private Context context;
    
    // 保存每个Topic对应的订阅列表(用于断线重连后自动重新订阅)
    private final List<String> subscribedTopics = new ArrayList<>();
    
    // 消息监听器列表(支持多处订阅)
    private final List<IMqttEventListener> eventListeners = new ArrayList<>();

    // 主线程Handler,用于将消息抛回UI线程
    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    private MqttManager() {}

    public static MqttManager getInstance() {
        if (instance == null) {
            synchronized (MqttManager.class) {
                if (instance == null) {
                    instance = new MqttManager();
                }
            }
        }
        return instance;
    }

    /**
     * 初始化并连接
     * @param context 上下文
     * @param brokerUrl 服务器地址 (tcp://xxx:1883)
     * @param clientId 客户端ID
     * @param username 用户名 (可选)
     * @param password 密码 (可选)
     */
    public void init(Context context, String brokerUrl, String clientId, String username, String password) {
        this.context = context.getApplicationContext();
        
        if (client != null && client.isConnected()) {
            disconnect();
        }

        client = new MqttAndroidClient(this.context, brokerUrl, clientId);
        
        // 设置回调
        client.setCallback(new MqttCallbackExtended() {
            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                Log.d(TAG, "Connected! Reconnect=" + reconnect);
                if (reconnect) {
                    //如果是自动重连成功,需要重新订阅之前的Topic
                    reSubscribeAllTopics();
                }
                notifyConnected();
            }

            @Override
            public void connectionLost(Throwable cause) {
                Log.w(TAG, "Connection Lost", cause);
                notifyDisconnected(cause != null ? cause.getMessage() : "Unknown");
            }

            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {
                String msgContent = new String(message.getPayload());
                Log.d(TAG, "Msg Arrived: [" + topic + "] " + msgContent);
                notifyMessageArrived(topic, msgContent);
            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {
                // 消息发送成功确认,如有需要可处理
            }
        });

        connect(username, password);
    }

    private void connect(String username, String password) {
        MqttConnectOptions options = new MqttConnectOptions();
        // 自动重连是Paho的关键特性,处理网络不稳定
        options.setAutomaticReconnect(true); 
        // 清除会话:false表示服务器会保留之前的订阅信息和未接收的消息(QoS>0)
        // 建议设为true,由客户端在connectComplete中手动控制订阅,逻辑更清晰
        options.setCleanSession(true); 
        options.setConnectionTimeout(30); // 超时时间
        options.setKeepAliveInterval(60); // 心跳间隔

        if (username != null && !username.isEmpty()) {
            options.setUserName(username);
            options.setPassword(password.toCharArray());
        }

        try {
            client.connect(options, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    // 首次连接成功由 connectComplete 回调处理
                    Log.d(TAG, "Connect Action Success");
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                    Log.e(TAG, "Connect Failed", exception);
                    notifyDisconnected("Initial Connect Failed: " + exception.getMessage());
                }
            });
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

    /**
     * 订阅主题
     * @param topic 主题
     * @param qos 服务质量 (0, 1, 2)
     */
    public void subscribe(String topic, int qos) {
        if (client != null && client.isConnected()) {
            try {
                client.subscribe(topic, qos);
                if (!subscribedTopics.contains(topic)) {
                    subscribedTopics.add(topic);
                }
                Log.d(TAG, "Subscribed to: " + topic);
            } catch (MqttException e) {
                Log.e(TAG, "Subscribe Error", e);
            }
        } else {
            // 如果当前未连接,记录下来,等连接成功后自动订阅
            if (!subscribedTopics.contains(topic)) {
                subscribedTopics.add(topic);
            }
            Log.w(TAG, "Client not connected, topic added to pending list: " + topic);
        }
    }

    /**
     * 发布消息
     */
    public void publish(String topic, String msg, int qos, boolean retained) {
        if (client != null && client.isConnected()) {
            try {
                MqttMessage message = new MqttMessage();
                message.setPayload(msg.getBytes());
                message.setQos(qos);
                message.setRetained(retained);
                client.publish(topic, message);
                Log.d(TAG, "Published to " + topic + ": " + msg);
            } catch (MqttException e) {
                Log.e(TAG, "Publish Error", e);
            }
        } else {
            Log.e(TAG, "Cannot publish, client not connected.");
        }
    }

    /**
     * 断开连接
     */
    public void disconnect() {
        if (client != null) {
            try {
                client.disconnect();
                client = null;
                subscribedTopics.clear();
                Log.d(TAG, "Disconnected manually");
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 注册全局监听器
     */
    public void registerListener(IMqttEventListener listener) {
        if (!eventListeners.contains(listener)) {
            eventListeners.add(listener);
        }
    }

    /**
     * 移除监听器
     */
    public void unregisterListener(IMqttEventListener listener) {
        eventListeners.remove(listener);
    }

    // --- 内部辅助方法 ---

    private void reSubscribeAllTopics() {
        for (String topic : subscribedTopics) {
            try {
                client.subscribe(topic, 1);
                Log.d(TAG, "Re-subscribed: " + topic);
            } catch (MqttException e) {
                Log.e(TAG, "Re-subscribe failed for " + topic, e);
            }
        }
    }

    private void notifyMessageArrived(String topic, String message) {
        mainHandler.post(() -> {
            for (IMqttEventListener listener : eventListeners) {
                listener.onMessageArrived(topic, message);
            }
        });
    }

    private void notifyConnected() {
        mainHandler.post(() -> {
            for (IMqttEventListener listener : eventListeners) {
                listener.onConnected();
            }
        });
    }

    private void notifyDisconnected(String reason) {
        mainHandler.post(() -> {
            for (IMqttEventListener listener : eventListeners) {
                listener.onDisconnected(reason);
            }
        });
    }
    
    public boolean isConnected() {
        return client != null && client.isConnected();
    }
}

2.4. 项目中的使用方法

你只需要在项目中按照以下步骤调用即可。

2.4.1 初始化 (通常在 Application 或 MainActivity)

java 复制代码
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // 替换你的配置
        String brokerUrl = "tcp://broker.emqx.io:1883";
        String clientId = "Android_App_" + System.currentTimeMillis();
        
        // 初始化并连接
        MqttManager.getInstance().init(this, brokerUrl, clientId, null, null);
    }
}

2.4.2 订阅和接收消息 (在 Activity 或 ViewModel 中)

java 复制代码
public class MainActivity extends AppCompatActivity implements IMqttEventListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 注册监听
        MqttManager.getInstance().registerListener(this);

        // 2. 订阅主题 (可以在连接成功回调里做,也可以直接调用,Manager内部做了缓存处理)
        MqttManager.getInstance().subscribe("device/sensor/data", 1);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 3. 必须移除监听,防止内存泄漏
        MqttManager.getInstance().unregisterListener(this);
    }

    // --- 接口回调实现 ---

    @Override
    public void onMessageArrived(String topic, String message) {
        // 这里已经是主线程,可以直接更新UI
        if ("device/sensor/data".equals(topic)) {
            // 处理具体的业务逻辑,例如解析JSON
            System.out.println("Received: " + message);
        }
    }

    @Override
    public void onConnected() {
        Toast.makeText(this, "MQTT Connected", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDisconnected(String reason) {
        Toast.makeText(this, "MQTT Disconnected: " + reason, Toast.LENGTH_SHORT).show();
    }
    
    // --- 按钮点击发布消息 ---
    public void onSendClick(View view) {
        MqttManager.getInstance().publish("device/control", "{\"cmd\":\"ON\"}", 1, false);
    }
}

2.5 设计亮点与可维护性分析

  1. 自动重连机制 (setAutomaticReconnect):
  • 代码中配置了Paho自带的自动重连。当网络从断开变为连接(如4G切换WiFi,或进入电梯后出来),Paho Client 会尝试重连。
  • 关键点 : 我们使用了 MqttCallbackExtended。当重连成功时,会触发 connectComplete(boolean reconnect, ...)。代码中通过判断 reconnect 标志,自动执行 reSubscribeAllTopics(),解决了**"重连后丢失之前订阅Topic"**的常见痛点。
  1. 统一的订阅管理 (subscribedTopics List) :

    即使用户在网络未连接时调用了 subscribe,我们也会将其加入列表。一旦连接建立,或者重连成功,管理器会自动遍历列表进行实际订阅。业务层无需关心当前是否在线。

  2. 线程安全与UI交互 :

    MQTT的回调通常在后台线程。MqttManager 内部持有一个 Handler(Looper.getMainLooper()),确保分发给 IMqttEventListener 的事件都在主线程 执行。业务层更新UI时不需要再写 runOnUiThread

  3. 易扩展性 :

    如果未来需要解析复杂的JSON消息,可以在 MqttManagermessageArrived 中引入一个 MessageParser 接口,或者直接在业务层的 onMessageArrived 中通过 Topic 字符串进行 switch-case 分流处理。

  4. 生命周期安全 :

    使用 ApplicationContext 初始化Client,避免持有 Activity 的 Context 导致内存泄漏。提供 registerListenerunregisterListener,符合Android组件生命周期规范。

相关推荐
alexhilton3 小时前
深入理解withContext和launch的真正区别
android·kotlin·android jetpack
TDengine (老段)7 小时前
TDengine 转换函数 TO_JSON 用户手册
android·大数据·数据库·json·时序数据库·tdengine·涛思数据
q***42827 小时前
SpringCloudGateWay
android·前端·后端
卫生纸不够用7 小时前
Appium-锁屏-Android
android·appium
阿拉斯攀登7 小时前
安卓工控机 OTA 升级方案(SpringBoot+MQTT)
android·spring boot·物联网·iot
顾林海8 小时前
从0到1搭建Android网络框架:别再让你的请求在"路上迷路"了
android·面试·架构
花花鱼8 小时前
android room中实体类变化以后如何迁移
android
Jomurphys9 小时前
设计模式 - 适配器模式 Adapter Pattern
android
雨白9 小时前
电子书阅读器:解析 EPUB 底层原理与实战
android·html