Mqtt5 - 會話保留與離線消息接收(完整)

       最近在做IM(及時通信)相關的應用,長連接(消息收發)採用的Mqtt5,mqtt支持會話保存(cleanStart)、會話超期(sessionExpire)、Qos等設置,可以實現移動端弱網絡(或網絡斷開後再重連)的離線消息接收。
       之所以選擇Mqtt協議,因爲mqtt是一種極其輕量級的發佈/訂閱消息傳輸協議(專爲受限設備和低帶寬、高延遲或不可靠的網絡而設計),且代碼體積小、功耗低,適合移動設備、車機等終端,且需要支持手機、車機等在網絡信號不穩定(弱網、斷網、進隧道沒有網絡等)且之後再恢復網絡時,可以繼續收發消息、且可以收到之前離線時消息的補充推送。關於離線消息的補充推送亦可由IM服務端自己控制,但若Mqtt協議原生支持離線推送,豈不是省的開發者再去自己處理。同時秉承着用新不用舊的觀點,果斷選用Mqtt5而棄用Mqtt3,Mqtt5相較於Mqtt3有了很多升級,如:原因代碼(PUBACK / PUBREC)、共享訂閱、會話過期、請求/響應模式(ResponseTopic, CorrelationData)、Will Delay等。

關於Mqtt的服務端、客戶端選型可參考如下鏈接:
Mqtt官網
Mqtt中文網
Mqtt Server端
Mqtt Client端

實際開發過程中,Server端選用的Emq,Client端選用的HiveMq,二者均支持Mqtt5。

Mqtt5支持離線消息接收的幾個核心設置:
ClientId
CleanStart: false
SessionExpiry
Qos:2
CONNACK中的session present flag

ClientId用於唯一標識用戶session。
CleanStart設置爲0,表示創建一個持久會話,在客戶端斷開連接時,會話仍然保持並保存離線消息,直到會話超時註銷。CleanStart設置爲1,表示創建一個新的臨時會話,在客戶端斷開時,會話自動銷燬。
SessionExpiry即指定在CleanStart爲0時,會話的保存時長,如果客戶端未在用戶定義的時間段內連接,則可以丟棄狀態(例如,訂閱和緩衝的消息)而無需進行清理。
Qos即消息的Quality of Service,若要支持離線消息,需要訂閱端、發佈端Qos >= 1
session present即在connect到mqtt服務器的返回結果ConnAck中,包含session present標識,該標識表示當前clientId是否存在之前的持久會話(persistent session),若之前已存在session(此時千萬不要再次重複訂閱topic,若再次訂閱則之前的消息都將收不到),則session會保留之前的訂閱關係、客戶端離線時的消息(Qos>=1)、未ack的消息。重點說明一下session present的使用,在客戶端連接到mqtt服務器並獲取到connack中的isSessionPresent標識時,若isSessionPresent=true則已存在會話,此時無需再重複訂閱topic(訂閱關係已保存到session中,若再重複訂閱則收不到之前的離線消息),可通過全局接收來處理離線消息和之後的新消息;若isSessionPresent=false則不存在session(又或者session已超期),此時需要重新訂閱topic,且之前離線的消息都已接收不到,只能通過其他方式獲取離線消息(例如IM後端服務的全量同步服務)。

xxx
圖片截取自:mqtt-essentials-part-3-client-broker-connection-establishment

在這裏插入圖片描述
圖片截取自:mqtt-essentials-part-7-persistent-session-queuing-messages

如ClientId=1, CleanStart=false, SessionExpiry=3600s, Qos=2即指定clientId=1的會話爲持久會話,用戶在離線後3600s的的離線消息都會被Mqtt服務器保存,用戶在離線時間不超過3600s且再次以ClientId=1重新上線時,是可以收到離線期間消息的補充推送的,同時Qos=2(exactly once)保證消息只會被客戶端收到一次且一定一次。

以HiveMq客戶端代碼爲例:

注意:asyncClient.publishes全局消息接收一定要放在connect方法調用之前

package com.mx.mqtt.sys;

import com.hivemq.client.mqtt.MqttGlobalPublishFilter;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.lifecycle.MqttClientConnectedContext;
import com.hivemq.client.mqtt.lifecycle.MqttClientConnectedListener;
import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedContext;
import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedListener;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException;
import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5SimpleAuth;
import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck;
import com.mx.mqtt.jwt.JwtUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.UnsupportedEncodingException;

/**
 * emqx - Session
 *
 * @Ahthor luohq
 * @Date 2020-04-09
 */
public class EmqxOfflineClient {

    /**
     * 日誌
     */
    private static final Logger logger = LogManager.getLogger(EmqxOfflineClient.class);


    private static final String MQTT_JWT_SECRET = "xxxx";
    private static final String MQTT_SERVER_HOST = "192.168.xxx.xxx";
    private static final Integer MQTT_SERVER_PORT = 1883;
    private static final String MQTT_CLIENT_ID = "luohq-offline";
    public static final String MQTT_SUB_TOPIC = "luohq/offline";
    public static final Long SESSION_EXPIRATION = 5 * 60L;


    private static Boolean isSessionPresent = false;
    private static Mqtt5BlockingClient client;
    private static Mqtt5AsyncClient asyncClient;


    public static void main(String[] args) {
        /** 構建mqtt客戶端 */
        buildMqtt5Client();


        /** 若session不存在,則需要再訂閱主題 */
        if (!isSessionPresent) {
            logger.info("【CLIENT-SUB】訂閱主題:" + MQTT_SUB_TOPIC);
            //訂閱主題
            asyncClient.subscribeWith()
                    .topicFilter(MQTT_SUB_TOPIC)
                    .qos(MqttQos.EXACTLY_ONCE)
                    .send();
        }

    }


    public static Mqtt5BlockingClient buildMqtt5Client() {
        /** blocking客戶端 */
        client = Mqtt5Client.builder()
                .identifier(MQTT_CLIENT_ID)
                .serverHost(MQTT_SERVER_HOST)
                .serverPort(MQTT_SERVER_PORT)
                .addConnectedListener(new MqttClientConnectedListener() {
                    @Override
                    public void onConnected(MqttClientConnectedContext context) {
                        logger.info("mqtt onConnected context");
                    }
                })
                .addDisconnectedListener(new MqttClientDisconnectedListener() {
                    @Override
                    public void onDisconnected(MqttClientDisconnectedContext context) {
                        logger.info("mqtt onDisconnected context");
                    }
                })
                //自動重連(指數級延遲重連(起始延遲1s,之後每次2倍,到2分鐘封頂) delay : 1s-> 2s -> 4s -> ... -> 2min)
                .automaticReconnectWithDefaultConfig()
                .buildBlocking();
        asyncClient = client.toAsync();


        /** Emqx JWT認證 */
        String authJwt = JwtUtils.generateJwt(MQTT_CLIENT_ID, MQTT_JWT_SECRET);
        Mqtt5SimpleAuth auth = Mqtt5SimpleAuth.builder()
                .username(MQTT_CLIENT_ID)
                .password(authJwt.getBytes())
                .build();
        Mqtt5ConnAck connAck = null;




        /** 全局消息處理(放在connect之前) */
        asyncClient.publishes(MqttGlobalPublishFilter.ALL, mqtt5Publish -> {
            try {
                byte[] msg = mqtt5Publish.getPayloadAsBytes();
                String msgStr = new String(mqtt5Publish.getPayloadAsBytes(), "UTF-8");
                logger.info("【CLIENT-RECV】" + msgStr);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        });


        /** 連接邏輯 */
        try {
            connAck = client.connectWith()
                    .simpleAuth(auth)
                    /** cleanSession=false */
                    .cleanStart(false)
                    /** session 7天過期 */
                    .sessionExpiryInterval(SESSION_EXPIRATION)
                    /** keepalive 時長*/
                    //.keepAlive(60)
                    .send();
        } catch (Mqtt5ConnAckException e) {
            e.printStackTrace();
            connAck = e.getMqttMessage();
        }


        /** 連接(普通無密碼連接) */
        //Mqtt5ConnAck connAck = client.connect();

        /** 檢查之前是否已存在session */
        isSessionPresent = connAck.isSessionPresent();
        if (connAck.isSessionPresent()) {
            logger.info("session is present: " + connAck.getSessionExpiryInterval().orElse(-1));
        }



        logger.info(connAck.getReasonCode() + ":" + connAck.getReasonString() + ":" + connAck.getResponseInformation());


        if (connAck.getReasonCode().isError()) {
            logger.error("Mqtt5連接失敗!");
            System.exit(-1);
        }
        return client;
    }
}

以上的幾個核心設置:
clientId,
cleanStart=fasle,
sessionExpiry > 0,
Qos>=1,
CONNACK session present處理,
缺一不可,少一項設置便無法實現離線消息的接受。

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