基于 JMS 標準的 Android MQTT 客戶端

關鍵詞

JMS、ActiveMQ(ActivityMQ)、Apollo、MQTT、Android

摘要

由于項目開發需要,涉及到 Android 客戶端接收來自 JMS 中間件的消息推送,本文以學習過程為線索,進行記錄。

目錄

一、簡述 JMS (引出 ActiveMQ、Apollo)
二、MQTT 協議
三、MQTT 服務器搭建
四、 MQTT Android 客戶端具體實現
—— 1、添加依賴
—— 2、添加權限
—— 3、基本字段說明
—— 4、注冊Service
—— 5、Android 端具體實現

正文

一、簡述 JMS (引出 ActiveQM、Apollo)

企業消息系統(Java Message Service)
  • 又稱之為面向消息的中間件(MOM),提供了應用程序之間,異步數據的存儲和轉發,即應用程序彼此不直接通信,而是與作為中介的 MOM 通信。應用程序開發人員無需了解消息的發送和協議的細節。

  • 過程如下圖,應用程序 A 只需要將 Message 發送到服務器上,然后應用程序 B 即可從服務器中接收到 A 發來的消息,由此可知 JMS 具有提供消息靈活性,松散耦合等優點。

JMS通訊示意圖
  • JMS 由 SUN 提出,是一系列的接口及相關語義的集合,通過這些接口和和其中的方法,JMS 客戶端可以訪問消息系統,完成消息的創建、發送、接收及讀取。
  • JMS 通過 MOM 為 Java 程序提供了一個發送和接收消息的標準。根據 JMS 編寫的客戶端程序可以訪問任何實現 JMS 標準的 MOM。
JMS兩種消息模型:

發送消息的客戶端:生產者, 接收消息的客戶端:消費者, MOM :消息傳遞系統

點到點(P2P)消息傳遞模型
  • 生產者發送消息到MOM的一個特定隊列(Queue)中,而消費者從一個消息隊列中得到消息。
  • 每條消息只有一個消費者,如果一條消息被消息者接收,那么其他的消費者就不能得到這條消息了。
  • 收到消息后消費者必須確認消息已被接收,否則消息傳遞系統會認為該消息沒有被接收,那么這條消息仍然可以被其他人接收(程序可以自動進行確認,不需要人工干預)。
發布/訂閱(Pub/Sub)消息傳遞模型
  • 發布/訂閱傳遞消息類型與主題(Topic)有關。生產者發布消息,而消費者訂閱感興趣的消息,生產者將消息和一個特定的主題(Topic)連在一起,消息傳遞系統根據消費者注冊的興趣,將消息傳遞給消費者。這種類型非常類似出版報紙、雜志的形式。
  • 每個消息都可以有多個(0,1,……)訂閱者。即每條消息可以有多個消費者,如果報紙和雜志一樣。
  • 訂閱者只能消費他們訂閱之后出版的消息。這就要求訂閱者必須先訂閱,再等待生產者的運行,發布。
  • 訂閱者必須保持為活動狀態才能獲得這些消息,即訂閱者必須保持活動狀態等待發布者發布的消息。

關于 JMS 更詳細的介紹,推薦閱讀下面的貼,更加深入淺出,通俗易懂
深入淺出JMS(一)——JMS簡介
深入淺出JMS(二)——JMS的組成

ActiveMQ (或ActivityMQ)

由 Apache 貢獻的 ActiveMQ 正是 MOM 中優秀的一員。它是一個非常流行、強大、開源的消息和集成模式服務器,速度快、支持多種跨語言客戶端和協議,易于使用企業集成模式,擁有許多先進的特性,完全支持 JMS 1.1和 J2EE 1.4 規范。

Apollo

是以 ActiveMQ5.x 為基礎,采用全新的線程和消息調度架構重新實現的 MOM,針對多核處理器進行了優化處理,它的速度更快、更可靠、更易于維護。Apollo 與 ActiveMQ 一樣支持多協議:STOMP、AMQP、MQTT、Openwire、 SSL、WebSockets,本文只介紹 MQTT 協議的 Android 客戶端使用。

二、MQTT 協議

1、 Android 端實現消息推送的幾種方式
  • 輪詢:客戶端定時向服務器請求數據,屬于偽推送。缺點:費電,費流量。
  • XMPP:是基于可擴展標記語言(XML)的協議。缺點:XML 格式的,冗余很大,花費流量。
  • MQTT:由 IBM 開發的傳輸協議,它被設計用于輕量級的發布/訂閱式消息傳輸,旨在為低帶寬和不穩定的網絡環境中的設備提供可靠的網絡服務。相比于 XMPP 等傳統協議,MQTT 是專門針對移動互聯網開發的輕量級傳輸協議,這種傳輸協議連接穩定、心跳數據包小,所以具備耗電量低、耗流量低的優勢。推送服務的最佳協議!
2、 MQTT 協議簡介

MQTT官網:http://mqtt.org/
MQTT介紹:http://www.ibm.com
MQTT Android github:https://github.com/eclipse/paho.mqtt.android
MQTT API:http://www.eclipse.org/paho/files/javadoc/index.html
MQTT Android API: http://www.eclipse.org/paho/files/android-javadoc/index.html

3、 Apollo + MQTT 協議的消息推送特征
  • Apollo 使用 MQTT 協議,加上 Android 上的 paho 包,即可簡單實現消息通知功能,但是 MQTT 協議僅支持主題消息廣播(Topic),即發布/訂閱消息傳遞模式,而且不能用Selector,不能直接實現點對點的消息投遞。
  • 利用上述的特征,可將 Android 客戶端劃分為眾多的部落,創建不同的主題, 針對特定訂閱者群體進行消息傳遞。該特征特別適用于智能家居等物聯網系統。

三、MQTT 服務器搭建

1、下載Apollo服務器,解壓(免安裝的)。
2、進入解壓后文件夾的 bin 目錄下(例: E:\MQTT\apache-apollo-1.7.1\bin),按住 Shift 鍵,點擊鼠標右鍵選擇 "在此處打開命令窗口";
3、在命令窗口,輸入 apollo create 服務器實例名稱(例:apollo create mybroker),之后會在 bin 目錄下創建該名稱的文件夾。該文件夾中, etc\apollo.xml 文件是配置服務器信息的文件。etc\users.properties 文件包含連接 MQTT 服務器時用到的用戶名和密碼,默認為 admin=password,即賬號為admin,密碼為 password,可自行更改。
4、在命令窗口,輸入 cd xxx/bin, 進入該實例的bin目錄下,執行 apollo-broker.cmd run 命令,開啟服務器,看到如下界面代表搭建完成。

cmd 窗口

之后在瀏覽器輸入 http://127.0.0.1:61680/,查看是否安裝成功。

四、 MQTT Android 客戶端具體實現

1、在 module 的 build.gradle 添加依賴

repositories {
    maven {
        url "https://repo.eclipse.org/content/repositories/paho-releases/"
    }
}

dependencies {
    compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
    compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.0'
}

2、添加權限

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

3、基本字段說明

topic:中文意思是“話題”。在 MQTT 中訂閱了( subscribe )同一話題(topic)的客戶端會同時收到消息推送。直接實現了“群聊”功能。
clientId:客戶身份唯一標識。
qos:服務質量。
retained:要保留最后的斷開連接信息。
MqttAndroidClient#subscribe():訂閱某個話題。
MqttAndroidClient#publish(): 向某個話題發送消息,之后服務器會推送給所有訂閱了此話題的客戶。
userName:連接到MQTT服務器的用戶名。
passWord :連接到MQTT服務器的密碼。

4、注冊Service

 <!-- Mqtt Service -->
        <service android:name="org.eclipse.paho.android.service.MqttService" />
        <service android:name="com.mqtt.demo.MQTTService"/>

5、Android 端具體實現

public class MQTTService extends Service {
    public static final String TAG = MQTTService.class.getSimpleName();
    private static MQTTService instance;
    private static MqttAndroidClient client;

    private MqttConnectOptions conOpt;
    private String host = "tcp://192.168.7.31:61613";
    private String userName = "admin";
    private String passWord = "password";
    private static String myTopic = "topic";
    private String clientId = "test2";

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        init();
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        instance = null;
        try {
            client.disconnect();
            client.unregisterResources();

        } catch (MqttException e) {
            e.printStackTrace();
        }
        super.onDestroy();
    }

    public static void publish(String msg){
        String topic = myTopic;
        Integer qos = 0;
        Boolean retained = false;
        try {
            client.publish(topic, msg.getBytes(), qos.intValue(), retained.booleanValue());
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }


    // MQTT監聽連接情況
    private IMqttActionListener iMqttActionListener = new ActionListener();

    // MQTT監聽并且接受消息
    private MqttCallback mqttCallback = new CallBack();

    private void init() {
        clientId = MacAddressUtil.getLocalMacAddress(this);

        // 服務器地址(協議+地址+端口號)
        String uri = host;
        client = new MqttAndroidClient(this, uri, clientId);
        // 設置MQTT監聽并且接受消息
        client.setCallback(mqttCallback);

        conOpt = new MqttConnectOptions();
        // 清除緩存
        conOpt.setCleanSession(true);
        // 設置超時時間,單位:秒
        conOpt.setConnectionTimeout(10);
        // 心跳包發送間隔,單位:秒
        conOpt.setKeepAliveInterval(20);
        // 用戶名
        conOpt.setUserName(userName);
        // 密碼
        conOpt.setPassword(passWord.toCharArray());

        // last will message
        boolean doConnect = true;
        String message = "{\"terminal_uid\":\"" + clientId + "\"}";
        String topic = myTopic;
        Integer qos = 0;
        Boolean retained = false;
        if (TextUtils.isEmpty(message) || TextUtils.isEmpty(topic)) {
            // 最后發送的消息
            try {
                conOpt.setWill(topic, message.getBytes(), qos.intValue(), retained.booleanValue());
            } catch (Exception e) {
                Log.i(TAG, "Exception Occured", e);
                doConnect = false;
                iMqttActionListener.onFailure(null, e);
            }
        }

        if (doConnect) {
            doClientConnection();
        }

    }

    /** 連接MQTT服務器 */
    private void doClientConnection() {
        if (!client.isConnected() && isConnectIsNomarl()) {
            try {
                client.connect(conOpt, null, iMqttActionListener);
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    }

    /** 判斷網絡是否連接 */
    private boolean isConnectIsNomarl() {
        ConnectivityManager connectivityManager = (ConnectivityManager) this.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo info = connectivityManager.getActiveNetworkInfo();
        if (info != null && info.isAvailable()) {
            String name = info.getTypeName();
            Log.i(TAG, "MQTT當前網絡名稱:" + name);
            return true;
        } else {
            Log.i(TAG, "MQTT 沒有可用網絡");
            return false;
        }
    }

    public static boolean isConnect(){
        if (instance != null && client != null){
            return instance.client.isConnected();
        }
        return false;
    }

    public static void connect(){
        if (instance != null && client != null){
            instance.doClientConnection();
        }
    }

    public static void disconnect(){
        if (instance != null){
            try {
                instance.client.disconnect();
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    }

    public static class ActionListener implements IMqttActionListener {

        @Override
        public void onSuccess(IMqttToken arg0) {
            Log.i(TAG, "連接成功 ");
            try {
                // 訂閱myTopic話題
                client.subscribe(myTopic,1);
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onFailure(IMqttToken arg0, Throwable arg1) {
            arg1.printStackTrace();
            // 連接失敗,重連
            Log.i("mtqq", "onFailure");
        }
    }


    public static class CallBack implements MqttCallback{

        @Override
        public void messageArrived(String topic, MqttMessage message) throws Exception {
            String str1 = new String(message.getPayload());
            MQTTMessage msg = new MQTTMessage();
            msg.setMessage(str1);
            EventBus.getDefault().post(msg);
            String str2 = "topic:" + topic + ", qos:" + message.getQos() + ", retained:" + message.isRetained();
            Log.i(TAG, "messageArrived:" + str1);
            Log.i(TAG, str2);
        }

        @Override
        public void deliveryComplete(IMqttDeliveryToken arg0) {
            Log.i("mtqq", "deliveryComplete");
        }

        @Override
        public void connectionLost(Throwable arg0) {
            // 失去連接,重連
            String message = "null";
            if (arg0 != null) {
                message = arg0.getMessage();
            }
            Log.i("mtqq", "connectionLost:"+message);
        }
    }
}

5、手機顯示

Paste_Image.png

6、服務端顯示

打開服務端http://127.0.0.1:61680/,看到的是這個樣子

serverPic

7、例子代碼下載

https://github.com/jkjk66/AndroidMQTT.git

注:本文是通過搜集網絡資源,項目實踐后,整合撰寫,轉載請備注出處。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,665評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 寫在前面 最近有需求要了解一下各個推送的協議,目前了解到實現推送的三個主要方式:MQTT、XMPP和Google ...
    xiasuhuei321閱讀 5,288評論 3 9
  • 前言 MQTT是IBM開發的一個即時通訊協議,面向M2M和物聯網的連接,采用輕量級發布和訂閱消息傳輸機制,并且有可...
    閼男秀閱讀 10,108評論 8 46
  • 筆點柔存,墨染心門。繪一紙、意暖情深。 微風雨后,霧靄清晨。 念弦中曲,詩中景,夢中人。 難尋繾綣,承藏盈夢。任浮...
    小薇_閱讀 897評論 9 23