Android 二維碼被掃後接收通知(使用MQTT協議實現消息推送)

現如今掃碼付款和收款已經很便利了,比如超市買完東西付款時有兩種方式 : 顧客可以出示付款碼給收銀員掃碼收款,也可以由顧客掃描超市的收款碼進行付款 。兩種方式在付款完成後超市端會進行語音播報收到xx元。 前者收銀員主動掃碼,其使用的系統可以實時地收到付款反饋信息然後進行語音播報。後者顧客掃碼付款則沒有這麼簡單了,因爲發起付款請求的是顧客端,收銀端系統是不能實時地知道“我的二維碼何時被掃了“,也就是說付款成功通知得由服務器推送過來,這裏就涉及到了消息推送技術。

MQTT簡介

MQTT (MQ Telemetry Transport)is a machine-to-machine (M2M)/“Internet of Things” connectivity protocol.
MQTT stands for MQ Telemetry Transport. It is a publish/subscribe, extremely simple and lightweight messaging protocol, designed for constrained devices and low-bandwidth, high-latency or unreliable networks. The design principles are to minimise network bandwidth and device resource requirements whilst also attempting to ensure reliability and some degree of assurance of delivery. These principles also turn out to make the protocol ideal of the emerging “machine-to-machine” (M2M) or “Internet of Things” world of connected devices, and for mobile applications where bandwidth and battery power are at a premium.
MQTT即MQ遙測傳輸協議,它是一個發佈/訂閱協議,非常簡單和輕量級的消息傳遞協議,專爲受限設備和低帶寬、高延遲或不可靠的網絡設計。設計原則是儘量減少網絡帶寬和設備資源需求,同時也試圖確保可靠性和一定程度的交付保證。這些原則也使協議成爲新興的“機器對機器”(M2M)或“物聯網”世界的理想連接設備,以及帶寬和電池電力的移動應用程序。其在,通過衛星鏈路通信傳感器、偶爾撥號的醫療設備、智能家居、及一些小型化設備中已廣泛使用。特性如下:
1、使用發佈/訂閱消息模式,提供一對多的消息發佈,解除應用程序耦合;
2、對負載內容屏蔽的消息傳輸;
3、使用 TCP/IP 提供網絡連接;
4、有三種消息發佈服務質量:

  • qos爲0: “至多一次”,消息發佈完全依賴底層 TCP/IP 網絡。會發生消息丟失或重複。這一級別可用於如下情況,環境傳感器數據,丟失一次讀記錄無所謂,因爲不久後還會有第二次發送。
  • qos爲1:“至少一次”,確保消息到達,但消息重複可能會發生。
  • qos爲2:“只有一次”,確保消息到達一次。這一級別可用於如下情況,在計費系統中,消息重複或丟失會導致不正確的結果。

5、小型傳輸,開銷很小(固定長度的頭部是 2 字節),協議交換最小化,以降低網絡流量;
6、使用 Last Will 和 Testament 特性通知有關各方客戶端異常中斷的機制。
MQTT官網
Android消息推送MQTT實戰

MQTT Broker(代理服務器)

有很多開源的MQTT Broker代理服務器,參考MQTT Broker 選型進行Broker選型,下面簡單介紹使用Mosquitto Broker。mosquitto官網下載

Mosquitto的windows下的安裝與使用請參考:Windows環境下安裝配置Mosquitto服務及入門操作介紹

注意:Mosquitto的安裝目錄的路徑中不能包含空格(如C:\Program Files),否則會出現意想不到的錯誤!!!

若要配置賬號密碼,需要在配置文件mosquitto.conf中加上下面兩行代碼:

#設置不允許匿名登錄
allow_anonymous false
#設置賬戶密碼文件位置(絕對路徑)
password_file C:/MosquittoTest/pwfile.example

添加賬號密碼請使用mosquitto_passwd命令。參考 mosquitto_passwd man page
注意password_file需要跟文件的絕對路徑,否則作爲Windows服務後將無法啓動。

Mosquitto Broker服務安裝好後,啓動它。默認監聽端口時1883,需要在防火牆中添加入口規則,指定該端口允許通信。

服務器端安裝Mosquitto Broker即可,不需要編寫任何代碼!!!!

客戶端(測試工具及Android端)

客戶端可以發佈消息,也可以訂閱消息。

客戶端測試工具使用的是Eclipse paho,請自行下載 。Graphical MQTT Client Tools下載

安裝Eclipse paho MQTT測試工具進行發佈/訂閱消息測試。

在這裏插入圖片描述
下面介紹Android端的實現。
github:eclipse paho.mqtt.android
gradle添加依賴

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


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

AndroidManifest清單中添加service

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

MQTT輔助類MqttHelper.java

package com.jykj.hg.mqtt;

import android.util.Log;

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
//
public class MqttHelper {
    public static final String TAG = MqttHelper.class.getSimpleName();
    /**
     *  clientId前綴,後面可以接userid
     */
    public static final String CLIENT_PREFIX="android_client_";//
    private static MqttClient client;
    private static MqttConnectOptions connectOptions;

    private static String mqtt_username="username";
    private static String mqtt_password="password";
    private static String mqtt_uri= "tcp://192.168.0.107:1883";

    /**
     * 完全配置,在應用程序啓動的時候調用此方法進行初始化
     * @param _uri tcp://localhost:1883  or  ssl://localhost:8883
     */
    public static void initMqttConfig(String _uri,String _username,String _password){
        mqtt_uri =_uri;
        mqtt_username = _username;
        mqtt_password = _password;

    }
    //創建mqttClient對象,以及連接的對象
    public static MqttClient connect(String clientId) throws Exception {
        Log.e(TAG, "getMqttClient: "+mqtt_uri+","+mqtt_username+","+mqtt_password );
        if(client==null) client = new MqttClient(mqtt_uri,clientId,new MemoryPersistence());
        if(!client.isConnected()) client.connect(getOptions());
        return client;
    }
    public static MqttConnectOptions getOptions(){
        if(connectOptions!=null) return connectOptions;
        connectOptions = new MqttConnectOptions();
        connectOptions.setUserName(mqtt_username);
        connectOptions.setPassword(mqtt_password.toCharArray());
        connectOptions.setConnectionTimeout(6);
        //connectOptions.setCleanSession(false);
        return connectOptions;
    }
    public static MqttMessage getMessage(String msg){
        MqttMessage message = new MqttMessage();
        message.setQos(2);
        message.setRetained(false);
        message.setPayload(msg.getBytes());
        return message;
    }
    public static String getTopicRegular(int userid,String ccbh,String fcrq){
        return userid+"/"+ccbh+"_"+fcrq;
    }
    public static String getClientId(int userid){
        return CLIENT_PREFIX+userid;
    }
}

Activity中的使用TestActivity.java ,佈局文件中就一個輸入框和發佈消息按鈕

public class TestActivity extends AppCompatActivity {
	 private static final String TAG="TestActivity";
    private EditText etMsg;
    private MqttClient client;
     private Handler mhandler = new Handler();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bk_meal);
        final EditText etMsg = findViewById(R.id.etMsg);

        findViewById(R.id.btnPub).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                publish(etMsg.getText().toString());
            }
        });
        //連接並訂閱消息,需要開啓子線程,否則會阻塞主線程
        new Thread(){
            @Override
            public void run() {
                try {
                    client = MqttHelper.connect(MqttHelper.getClientId(5));
                    subscribeTopic();
                    client.setCallback(new MqttCallback() {
                        @Override
                        public void connectionLost(Throwable cause) {
                            Log.i(TAG, "connection lost");
                            subscribeTopic();
                        }
                        @Override
                        public void messageArrived(String topic, MqttMessage message) throws Exception {
                            Log.i(TAG, "received topic : " + topic+","+message);
                            //使用Handler將消息拉回到主線程,更新UI
                            final String msg = message.toString();
                            mhandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    //Update UI
                                    /* ...Update UI code .... */
                                    Toast.makeText(TestActivity.this,"收到消息:"+msg,Toast.LENGTH_SHORT).show();
                                }
                            });
                        }
                        @Override
                        public void deliveryComplete(IMqttDeliveryToken token) {
                            Log.i(TAG, "deliveryComplete");//消息發佈成功
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                    mhandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            subscribeTopic();
                        }
                    },1000*30);//30秒後重新訂閱
                }
            }
        }.start();
    }
     private void publish(final String msg){
        new Thread(){
            @Override
            public void run() {
                try {
                    client = MqttHelper.connect(MqttHelper.getClientId(5));
                    MqttMessage message = MqttHelper.getMessage(msg);
                    client.publish("testTopic", message);
                    Log.e(TAG, "mqtt publish: "+msg);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
    private void subscribeTopic(){
        new Thread(){
            @Override
            public void run() {
             try {
                String topic = "testTopic";
                client = MqttHelper.connect(MqttHelper.getClientId(5));
                client.subscribe(topic);
                Log.e(TAG, "訂閱主題: "+topic);
             } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

可以讓EClipse paho MQTT測試工具作爲一個客戶端與Android進行測試。

注意:在收到消息回調函數中需要使用Handler.post()方法將消息拉回到主線程,然後更新UI界面等操作。另外發布/訂閱操作需要開啓子線程,否則會阻塞主線程!

上面的測試端調通了後,實現二維碼被掃接收通知就不難了。這裏topic主題的規則很重要,一般用‘username/boo/foo’的形式來定義主題,username保證在系統中是用戶的唯一標識,這樣可以精準的發佈消息給訂閱者。例如B客戶端訂閱了主題"B/scanpay" ,A客戶端掃描B客戶端出示的二維碼,業務操作成功後(如付款)發佈一個topic爲 “B/scanpay” 的消息, 消息內容爲 “pay 30 元 success”,然後B客戶端由於訂閱了該主題就會收到該消息,然後就可以進行相關操作(如語音播報).

Topic主題定義

topic可以包含通配符 + or #

+通配符可以用作單個層次結構的通配符,例如主題"a/b/c/d", 下面的訂閱可以匹配:

a/b/c/d
+/b/c/d
a/+/c/d
a/+/+/d
+/+/+/+

而下面的訂閱將不匹配:

a/b/c
b/+/c/d
+/+/+

#可以用作所有剩餘層次結構的通配符,這意味着它必須是訂閱中的最後一個字符。對於主題"a/b/c/d", 下面的訂閱可以匹配:

a/b/c/d
#
a/#
a/b/#
a/b/c/#
+/b/c/#

長度爲零的主題級別是有效的,這可能會導致一些稍微不明顯的行爲。例如,“a//topic”的主題可以與“a/+/topic”的訂閱正確匹配。同樣,在主題字符串的開頭和結尾都可以存在零長度的主題級別,因此“/a/topic”將與“+/a/topic”、“#”或“/#”訂閱相匹配,而“a/topic/”將與“a/topic/+”或“a/topic/#”訂閱相匹配。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章