Mqtt及mosquitto、eclipse paho詳解

一.MQTT

1.簡介

???????MQTT(Message Queuing Telemetry Transport 消息隊列遙測傳輸)是ISO 標準(ISO/IEC PRF 20922)下基于發布/訂閱范式的消息協議。它工作在 TCP/IP協議族上,是為硬件性能低下的遠程設備以及網絡狀況糟糕的情況下而設計的發布/訂閱型消息協議,為此,它需要一個消息中間件
???????MQTT是IBM開發的一個基于客戶端-服務器的消息發布/訂閱傳輸協議。
???????MQTT協議是輕量、簡單、開放和易于實現的,這些特點使它適用范圍非常廣泛。在很多情況下,包括受限的環境中,如:機器與機器(M2M)通信和物聯網(IoT)。其在,通過衛星鏈路通信傳感器、偶爾撥號的醫療設備、智能家居、及一些小型化設備中已廣泛使用。

2.特性

  • 基于發布 / 訂閱范式的 “輕量級” 消息協議(頭部 2 字節)
  • 專為資源受限的設備、低帶寬占用高延時或者不可靠的網絡設計,適用于 IoT 與 M2M
  • 基于 TCP/IP 協議棧
  • 實時的 IoT 通訊的標準協議

二.Mosquitto

1.簡介

???????Mosquitto是一款實現了消息推送協議 MQTT v3.1 的開源消息代理軟件,提供輕量級的,支持可發布/可訂閱的的消息推送模式,使設備對設備之間的短消息通信變得簡單。

2.Broker

???????我們知道,網絡間進行通信需要有Server和Client,在Mqtt中Broker扮演了Server的角色,基于mosquitto源碼通過NDK進行編譯生成android系統端可執行的bin文件,通過mosquitto -c mosquitto.conf來啟動Broker;

3.版本和名稱

???????Mosquitto會支持不同的協議版本號和名稱,通過PROTOCOL_NAME和PROTOCOL_VERSION來進行區分,比如本文用到的mosquitto源碼版本支持MQTTV3.1和MQTTV3.1.1,MQTTV3.1對應的協議name為"MQIsdp";
MQTTV3.1.1對應的協議name為"MQTT";版本和name必須匹配。

三.Client端實現

???????Client端實現主要分為三部分:Client端的創建Client端連接Client端消息注冊

1.Client端創建

??????在創建client時,需要初始化一些指定的參數,通過這些參數來處理與broker端的交互,包括連接,心跳,斷開重連及設置狀態回調等。

  //用來存儲Qos=1和2的消息
  MemoryPersistence dataStore = new MemoryPersistence();
  //保存著一些控制客戶端如何連接到服務器的選項
  MqttConnectOptions mConOpt = new MqttConnectOptions();
  //set mqtt version
  mConOpt.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
  /**
  * set cleanSession
  * false:broker will save connection record for client
  * true:As a new client to connect broker every time[每次連接上都是一個新的客戶端]
  */
  mConOpt.setCleanSession(true);
  // set heartbeat 30s[30S去檢測一下broker是否有效,如果無效,會回調connectionLost]
  mConOpt.setKeepAliveInterval(30);
  // set username
  if (userName != null) {
      mConOpt.setUserName(userName);
  }
  // set password
  if (password != null) {
      mConOpt.setPassword(password.toCharArray());
  }
  //when disconnect unexpectly, broker will send "close" to clients which subscribe this topic to announce the connection is lost
  mConOpt.setWill(topic, "close".getBytes(), 2, true);
  //client reconnect to broker automatically[與broker斷開后會去重連]
  mConOpt.setAutomaticReconnect(true);
  // create Mqtt client
  if (sClient == null) {
      sClient = new MqttClient(brokerUrl, clientId, dataStore);
      // set callback[狀態回調]
      mCallback = new MqttCallbackBus(sInstance);
      sClient.setCallback(mCallback);
  }

2.Client端連接Broker

??????上一步創建了Client,并且初始化了各種參數,接下來調用connect進行連接,本文創建的是異步Client,設置了連接狀態的回調IMqttActionListener,連接成功后可以進行topic的訂閱,失敗后可以進行重連。

// connect to broker
sClient.connect(mConOpt);

//異步的client,同步連接沒有狀態回調
mClient = new MqttAsyncClient(brokerUrl, clientId, dataStore);
mClient.connect(mConOpt, null, mIMqttActionListener);
//連接狀態回調
private IMqttActionListener mIMqttActionListener = new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
        try {
            Log.i(TAG, "connect success");
            ......
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
        try {
            Log.e(TAG, "connect failure, reconnect");
            ......
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};

3.狀態回調

??????在第一步進行client創建時傳入了MqttCallback,在與broker斷開連接、新消息到達、消息發送完成后,通過該MqttCallback會收到對應的回調,具體如下:

public class MqttCallbackBus implements MqttCallback {
    private static final String TAG = MqttCallbackBus.class.getSimpleName();
    private MqttManager mMqttManager;
    public MqttCallbackBus(MqttManager mqttManager) {
        mMqttManager = mqttManager;
    }
    @Override
    public void connectionLost(Throwable cause) {
        Log.e(TAG, "cause : " + cause.toString());
        //與broker斷開后回調,[雖然上邊屬性中設置了自動重連,但是連上后不會去訂閱topic,即使連上也接收不到topic,因此選擇在此手動連接,然后在連接成功后訂閱topic]
        mMqttManager.reconnectBroker();
    }
    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        Log.e(TAG, "topic : " + topic + "\t MqttMessage : " + message.toString());
        //訂閱的消息接收到后回調
    }
    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        Log.e(TAG, "token : " + token.toString());
        //消息publish完成后回調
    }
}

四.Eclipse paho源碼分析

???????Client端是基于Eclipse Paho提供的mqtt開源庫進行實現,接下來對Eclipse Paho源碼進行分析:

1.MqttConnectOptions.java

//設置是否重連
public void setAutomaticReconnect(boolean automaticReconnect) {
    this.automaticReconnect = automaticReconnect;
}
//獲取是否設置了重連標志,確定后續是否進行重連
public boolean isAutomaticReconnect() {
    return automaticReconnect;
}

2.MqttAsyncClient.java

???????創建mqtt async client,包括一些實例初始化等,程序最重要的入口類。

2.1.構造方法

public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence persistence, MqttPingSender pingSender) throws MqttException {
    ....
    ....
    MqttConnectOptions.validateURI(serverURI);

    this.serverURI = serverURI;
    this.clientId = clientId;

    this.persistence = persistence;
    if (this.persistence == null) {
    }
    this.persistence.open(clientId, serverURI);
    //創建了ClientComms,最終去跟broker建立連接
    this.comms = new ClientComms(this, this.persistence, pingSender);
    this.persistence.close();
    this.topics = new Hashtable();
}

???????在構造方法內,進行了一些變量賦值,然后創建ClientComms實例,該實例用來跟broker建立連接,后面會進行分析;

2.2.connect()

???????在創建完實例后,調用connect()去跟broker建立連接,看一下connect()方法的具體實現:

public IMqttToken connect(MqttConnectOptions options, Object userContext, IMqttActionListener callback)
            throws MqttException, MqttSecurityException {
    ....
    this.connOpts = options;
    this.userContext = userContext;
    final boolean automaticReconnect = options.isAutomaticReconnect();
    ....
    comms.setNetworkModules(createNetworkModules(serverURI, options));
    comms.setReconnectCallback(new MqttCallbackExtended() {
        public void messageArrived(String topic, MqttMessage message) throws Exception {
        }
        public void deliveryComplete(IMqttDeliveryToken token) {
        }
        public void connectComplete(boolean reconnect, String serverURI) {
        }
        public void connectionLost(Throwable cause) {
            if(automaticReconnect){
               // Automatic reconnect is set so make sure comms is in resting state
               comms.setRestingState(true);
               reconnecting = true;
               //設置了重連,在收到connectionLost后,進行重連
               startReconnectCycle();
             }
         }
    });

    // Insert our own callback to iterate through the URIs till the connect succeeds
    MqttToken userToken = new MqttToken(getClientId());
    ConnectActionListener connectActionListener = new ConnectActionListener(this, persistence, comms, options, userToken, userContext, callback, reconnecting);
    userToken.setActionCallback(connectActionListener);
    userToken.setUserContext(this);

    ......
    comms.setNetworkModuleIndex(0);
    connectActionListener.connect();
}

???????在connect()內部主要做了以下幾件事:
???????1.通過createNetworkModules()創建NetworkModule,包含serverURl,最終創建的URI_TYPE_TCP,對應的是TCPNetworkModule;
???????2.調用setReconnectCallback()來設置重連,狀態斷開時會進行自動重連;
???????3.創建ConnectActionListener對象,傳入了comms、callback等參數,連接狀態onSuccess()和onFailure()是在ConnectActionListener里面進行回調的;
???????4.執行ConnectActionListener的connect()進行連接;

2.3.startReconnectCycle()

???????前面講到,如果設置了automaticReconnect,則在異常斷開后會調用startReconnectCycle()進行重連:

//1.重連循環
private void startReconnectCycle() {
    ....
    reconnectTimer = new Timer("MQTT Reconnect: " + clientId);
    reconnectTimer.schedule(new ReconnectTask(), reconnectDelay);
}

//2.重連task
private class ReconnectTask extends TimerTask {
    private static final String methodName = "ReconnectTask.run";
    public void run() {
        attemptReconnect();
    }
}

//3.重連入口
private void attemptReconnect(){
    ....
    try {
        //連接
        connect(this.connOpts, this.userContext,new IMqttActionListener() {
            public void onSuccess(IMqttToken asyncActionToken) {
                ....
                comms.setRestingState(false);
                //重連成功,結束重連循環
                stopReconnectCycle();
            }

            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                ....
                //繼續重連,下一次重連時間是上一次的兩倍,最高是128s
                if(reconnectDelay < 128000){
                   reconnectDelay = reconnectDelay * 2;
                }
                rescheduleReconnectCycle(reconnectDelay);
             }
         });
    ....
}

//設置Mqttcallback
public void setCallback(MqttCallback callback) {
     this.mqttCallback = callback;
     //將MqttCallbackBus回調設置給ClientComms,后續的回調供client使用,此處主要用到onConnectionLost()
     comms.setCallback(callback);
}

3.ConnectActionListener.java

???????通過以上可以看到,connect()方法中,最終調用的是connectActionListener的connect()方法,一起看一下該方法的具體實現:

3.1.connect()

public void connect() throws MqttPersistenceException {
    //創建MqttToken
    MqttToken token = new MqttToken(client.getClientId());
    //設置callback,由于connectActionListener實現了IMqttActionListener,即把自己注冊進去
    token.setActionCallback(this);
    token.setUserContext(this);

    ......

    try {
      //調用comms的connect,comms是在創建client里面創建的,在connect時傳入connectActionListener里面
      comms.connect(options, token);
    } catch (MqttException e) {
      onFailure(token, e);
    }
  }

3.2.onSuccess()和onFailure()

public void onSuccess(IMqttToken token) {
    ....
    if (userCallback != null) {
      //回調傳入的IActionListener回調,該userCallback是在AsyncClient.connect()是傳入的IMqttActionListener
      userCallback.onSuccess(userToken);
    }
    ....  
  }

public void onFailure(IMqttToken token, Throwable exception) {
     ....
     if (userCallback != null) {
       //回調傳入的IActionListener回調,該userCallback是在AsyncClient.connect()是傳入的IMqttActionListener
        userCallback.onFailure(userToken, exception);
      }
    ....
    }
}

4.ClientComms.java

???????該類也是一個非常重要的類,主要創建了三個線程和ClientState實例,構造方法中創建了CommsCallback線程和ClientState實例,接著上步會調用到connect()方法,看一下該方法的實現邏輯:

4.1.connect()

public void connect(MqttConnectOptions options, MqttToken token) throws MqttException {
    final String methodName = "connect";
    synchronized (conLock) {
        if (isDisconnected() && !closePending) {
            //設置狀態為連接中
            conState = CONNECTING;
            conOptions = options;
            //創建MqttConnect,表示與broker連接的message
            MqttConnect connect = new MqttConnect(client.getClientId(),
                        conOptions.getMqttVersion(),
                        conOptions.isCleanSession(),
                        conOptions.getKeepAliveInterval(),
                        conOptions.getUserName(),
                        conOptions.getPassword(),
                        conOptions.getWillMessage(),
                        conOptions.getWillDestination());
            ......
            
            tokenStore.open();
            ConnectBG conbg = new ConnectBG(this, token, connect);
            conbg.start();
        }
    }
}

???????在connect()內部創建了MqttConnect,表示是連接message,然后創建ConnectBG實例并執行start();

4.2.ConnectBG

private class ConnectBG implements Runnable {
        ClientComms     clientComms = null;
        Thread          cBg = null;
        MqttToken       conToken;
        MqttConnect     conPacket;

        ConnectBG(ClientComms cc, MqttToken cToken, MqttConnect cPacket) {
            clientComms = cc;
            conToken    = cToken;
            conPacket   = cPacket;
            cBg = new Thread(this, "MQTT Con: "+getClient().getClientId());
        }

        void start() {
            cBg.start();
        }

        public void run() {
            ......
            try {
                ........
                NetworkModule networkModule = networkModules[networkModuleIndex];
                networkModule.start();
                receiver = new CommsReceiver(clientComms, clientState, tokenStore, networkModule.getInputStream());
                receiver.start("MQTT Rec: "+getClient().getClientId());
                sender = new CommsSender(clientComms, clientState, tokenStore, networkModule.getOutputStream());
                sender.start("MQTT Snd: "+getClient().getClientId());
                //CommsCallback本身是一個線程,啟動
                callback.start("MQTT Call: "+getClient().getClientId());                
                internalSend(conPacket, conToken);
            }
            .......

            if (mqttEx != null) {
                 //不為空,說明進入了catch,則shut down
                shutdownConnection(conToken, mqttEx);
            }
        }
}

//接著AsyncClient.setCallback,會調用ClientComms設置MqttCallbackBus回調
public void setCallback(MqttCallback mqttCallback) {
    this.callback.setCallback(mqttCallback);
}

???????從上面的代碼可以看到,在執行connect()方法后,主要做了以下幾項工作:
??????1.networkModule.start():創建socket,與broker建立連接;

//TcpNetworkModule.java
public void start() throws IOException, MqttException 
    try {
        ......
        SocketAddress sockaddr = new InetSocketAddress(host, port);
        socket = factory.createSocket();
        socket.connect(sockaddr, conTimeout*1000);
        ......
    }
    .......
}

???????2.創建CommsReceiver()然后start(),不斷循環讀取Broker端來的消息;

//CommsReceiver.java
public void run() {
    ......
    while (running && (in != null)) {
        try {
            receiving = in.available() > 0;
            MqttWireMessage message = in.readMqttWireMessage();
            receiving = false;
                
            if (message instanceof MqttAck) {
                token = tokenStore.getToken(message);
                if (token!=null) {
                    synchronized (token) {
                        clientState.notifyReceivedAck((MqttAck)message);
                    }
                }
            } else {
                // A new message has arrived
                clientState.notifyReceivedMsg(message);
            }
        }
        ........
    ........
}

???????3.創建CommsSender實例,然后start(),主要用來向Broker發送消息;

//CommsSender.java 
public void run() {
    ......
    while (running && (out != null)) {
        try {
            message = clientState.get();
            if (message != null) {
                if (message instanceof MqttAck) {
                    out.write(message);
                    out.flush();
                } else {
                    MqttToken token = tokenStore.getToken(message);
                    if (token != null) {
                        synchronized (token) {
                            out.write(message);
                            try {
                                out.flush();
                            } ......
                            clientState.notifySent(message);
                        }
                    }
                }
            }
        }
    }
    ......
}

??????4.CommsCallback.start():通過該類來實現broker返回消息的回調處理入口,后面會講到。
??????5.internalSend(conPacket, conToken):發送連接action

5.ClientState.java

??????client在進行消息publish時,先經過ClientComms,最終會調用到ClientState里面的send()方法,看一下send()方法的實現邏輯:

public void send(MqttWireMessage message, MqttToken token) throws MqttException {
        final String methodName = "send";
        ......
        //message是publish型的message
        if (message instanceof MqttPublish) {
            synchronized (queueLock) {
                ......
                //獲取到要發送的Message
                MqttMessage innerMessage = ((MqttPublish) message).getMessage();
               //獲取到要發送Message的Qos
                switch(innerMessage.getQos()) {
                    case 2:
                        outboundQoS2.put(new Integer(message.getMessageId()), message);
                        persistence.put(getSendPersistenceKey(message), (MqttPublish) message);
                        break;
                    case 1:
                        outboundQoS1.put(new Integer(message.getMessageId()), message);
                        persistence.put(getSendPersistenceKey(message), (MqttPublish) message);
                        break;
                }
                tokenStore.saveToken(token, message);
                //加入pendingMessages,CommsSender通過get()方法獲取到message后就會進行發送
                pendingMessages.addElement(message);
                queueLock.notifyAll();
            }
        } else {
            //其他類型的message
            if (message instanceof MqttConnect) {
                synchronized (queueLock) {
                    // Add the connect action at the head of the pending queue ensuring it jumps
                    // ahead of any of other pending actions.
                    tokenStore.saveToken(token, message);
                    pendingFlows.insertElementAt(message,0);
                    queueLock.notifyAll();
                }
            } else {
                if (message instanceof MqttPingReq) {
                    this.pingCommand = message;
                }
                else if (message instanceof MqttPubRel) {
                    outboundQoS2.put(new Integer(message.getMessageId()), message);
                    persistence.put(getSendConfirmPersistenceKey(message), (MqttPubRel) message);
                }
                else if (message instanceof MqttPubComp)  {
                    persistence.remove(getReceivedPersistenceKey(message));
                }
                
                synchronized (queueLock) {
                    if ( !(message instanceof MqttAck )) {
                        tokenStore.saveToken(token, message);
                    }
                    pendingFlows.addElement(message);
                    queueLock.notifyAll();
                }
            }
        }
    }

    //連接成功
    public void connected() {
        final String methodName = "connected";
        //@TRACE 631=connected
        log.fine(CLASS_NAME, methodName, "631");
        this.connected = true;
        //啟動pingSender,來在keepAliveInterval內發送心跳包
        pingSender.start(); //Start ping thread when client connected to server.
    }

??????在CommsReceiver收到broker的ack及普通消息后,會先經過clientState,具體會調用以下兩個方法:
??????notifyReceivedAck():連接成功ack、qos=1和2時publish消息后的ack、心跳相關ack等都會通過該方法調用CommsCallback的方法。
??????notifyReceivedMsg():正常的publish消息等。
??????pingSender.start():表示client在連上broker后會在aliveInterval后發送心跳包,pingSender是在創建client時就創建了TimerPingSender實例,一步步先傳給clientComms,再傳給ClientState,執行start()來創建Timer,執行TimerTask來最終clientState的checkForActivity(),將PingReq加入pendingFlows后,queueLock.notifyAll(),調用CommsSender來進行發送,然后加入下一輪執行。如果正常的話,會收到PingResp,修改lastInboundActivity和pingOutstanding的值來在下一輪執行check時來判斷是否收到心跳,異常就拋出REASON_CODE_CLIENT_TIMEOUT(32000)碼。

6.CommsCallback.java

??????通過該類來實現broker返回消息的回調處理入口,包括處理斷開、消息發送成功,消息到達、連接狀態回調等

    public void start(String threadName) {
        synchronized (lifecycle) {
            if (!running) {
                // Preparatory work before starting the background thread.
                // For safety ensure any old events are cleared.
                messageQueue.clear();
                completeQueue.clear();

                running = true;
                quiescing = false;
                callbackThread = new Thread(this, threadName);
                callbackThread.start();
            }
        }
    }

    public void run() {
        final String methodName = "run";
        while (running) {
            try {
                //沒有work時,wait(),當有message來時,通過workAvailable.notifyAll()來喚醒
                try {
                    synchronized (workAvailable) {
                        if (running && messageQueue.isEmpty()
                                && completeQueue.isEmpty()) {
                            workAvailable.wait();
                        }
                    }
                } catch (InterruptedException e) {
                }

                if (running) {
                    // Check for deliveryComplete callbacks...
                    MqttToken token = null;
                    synchronized (completeQueue) {
                        if (!completeQueue.isEmpty()) {
                            // First call the delivery arrived callback if needed
                            token = (MqttToken) completeQueue.elementAt(0);
                            completeQueue.removeElementAt(0);
                        }
                    }
                    if (null != token) {
                        handleActionComplete(token);
                    }
                    
                    // Check for messageArrived callbacks...
                    MqttPublish message = null;
                    synchronized (messageQueue) {
                        if (!messageQueue.isEmpty()) {
                            // Note, there is a window on connect where a publish
                            // could arrive before we've
                            // finished the connect logic.
                            message = (MqttPublish) messageQueue.elementAt(0);

                            messageQueue.removeElementAt(0);
                        }
                    }
                    .......
            } finally {
                synchronized (spaceAvailable) {
                    spaceAvailable.notifyAll();
                }
            }
        }
    }

//連接回調方法入口,通過handleActionComplete()來調用
public void fireActionEvent(MqttToken token) {
        final String methodName = "fireActionEvent";
        if (token != null) {
           //此處的asyncCB就是在MqttAsyncClient.connect()里面傳入的ConnectActionListener,參考如下:
           //ConnectActionListener connectActionListener = new ConnectActionListener(this, persistence, comms, options, userToken, userContext, callback, reconnecting);
           //userToken.setActionCallback(connectActionListener);
            IMqttActionListener asyncCB = token.getActionCallback();
            if (asyncCB != null) {
                if (token.getException() == null) {
                    //回調ConnectActionListener的onSuccess的方法
                    asyncCB.onSuccess(token);
                } else {
                    //回調ConnectActionListener的onFailure的方法
                    asyncCB.onFailure(token, token.getException());
                }
            }
        }
}

//設置MqttCallbackBus回調
public void setCallback(MqttCallback mqttCallback) {
    this.mqttCallback = mqttCallback;
}

//斷開回調MqttCallbackBus接口
public void connectionLost(MqttException cause) {
    try {
       if (mqttCallback != null && cause != null) {
            //回調MqttCallbackBus的connectionLost接口
            mqttCallback.connectionLost(cause);
      ....
       }
       ....
     } 
    ....
}

7.客戶端連接流程圖

??????總結一下client端連接broker的流程圖,黑色的線代表client端從創建到跟broker進行connect()的調用流程,綠色的線代表從broker收到回復消息后的調用流程。


image.png

8.客戶端發送及訂閱接收消息流程圖

image.png

五.qos值及其含義

1.至多一次

???????消息發布完全依賴底層 TCP/IP 網絡。會發生消息丟失或重復。這一級別可用于如下情況,環境傳感器數據,丟失一次讀記錄無所謂,因為不久后還會有第二次發送。


Qos0.png

2.至少一次

???????確保消息到達,但消息可能會重復發生。


Qos1.png

3.只有一次

???????確保消息到達一次。這一級別可用于如下情況,在計費系統中,消息重復或丟失會導致不正確的結果。


Qos2.png

4.源碼分析

???????從Broker來的ack消息是通過CommsReceiver來接收的,接收后會調用ClientState的notifyReceiveAck()方法,結合上面的圖及代碼一起看一下:

protected void notifyReceivedAck(MqttAck ack) throws MqttException {
        final String methodName = "notifyReceivedAck";
        this.lastInboundActivity = System.currentTimeMillis();

        MqttToken token = tokenStore.getToken(ack);
        MqttException mex = null;

        if (token == null) {
        } else if (ack instanceof MqttPubRec) {
            MqttPubRel rel = new MqttPubRel((MqttPubRec) ack);
            //收到MqttPubRec后,創建MqttPubRel進行回復
            this.send(rel, token);
        } else if (ack instanceof MqttPubAck || ack instanceof MqttPubComp) {
            //qos = 1或2時,收到MqttPubAck或MqttPubComp來通知deliveryComplete回調,及刪除message
            notifyResult(ack, token, mex);
        } else if (ack instanceof MqttPingResp) {
            synchronized (pingOutstandingLock) {
                pingOutstanding = Math.max(0,  pingOutstanding-1);
                notifyResult(ack, token, mex);
                if (pingOutstanding == 0) {
                    tokenStore.removeToken(ack);
                }
            }
            //@TRACE 636=ping response received. pingOutstanding: {0}                                                                                                                                                     
            log.fine(CLASS_NAME,methodName,"636",new Object[]{ new Integer(pingOutstanding)});
        } else if (ack instanceof MqttConnack) {
            ......
            //連接成功的回調
            ......
        } else {
            ......
        }
        
        checkQuiesceLock();
    }

???????Subscriber在收到message后,會執行到ClientState.notifyReceivedMsg()方法,該方法會根據qos的值來做相應的處理,qos=0或1,直接會調用messageArrived,然后刪除persistence,send(PubAck);
???????qos=2時,先存儲,然后send(PubRec),接下來會收到broker的PubRel,如果能找到msg,則會調用messageArrived,然后send(PubComp),刪除persistence;如果再次收到PubRel,找不到msg,則直接send(PubComp),確保只執行一次messageArrived。

//ClientState.java
protected void notifyReceivedMsg(MqttWireMessage message) throws MqttException {
        final String methodName = "notifyReceivedMsg";
        this.lastInboundActivity = System.currentTimeMillis();

        // @TRACE 651=received key={0} message={1}
        log.fine(CLASS_NAME, methodName, "651", new Object[] {
                new Integer(message.getMessageId()), message });
        
        if (!quiescing) {
            if (message instanceof MqttPublish) {
                MqttPublish send = (MqttPublish) message;
                switch (send.getMessage().getQos()) {
                case 0:
                case 1:
                    //Qos=1或0,直接執行
                    if (callback != null) {
                        callback.messageArrived(send);
                    }
                    break;
                case 2:
            //Qos=2,先存儲,然后發送PubRec
                    persistence.put(getReceivedPersistenceKey(message),
                            (MqttPublish) message);
                    inboundQoS2.put(new Integer(send.getMessageId()), send);
                    this.send(new MqttPubRec(send), null);
                    break;

                default:
                    //should NOT reach here
                }
            } else if (message instanceof MqttPubRel) {
                //收到PubRel后,先從inboundQoS2找msg
                MqttPublish sendMsg = (MqttPublish) inboundQoS2
                        .get(new Integer(message.getMessageId()));
              //找到msg,表示還未執行messageArrived,先執行
                if (sendMsg != null) {
                    if (callback != null) {
                        callback.messageArrived(sendMsg);
                    }
                } else {
              //找不到說明已經執行了messageArrived,直接發送PubComp
                    // Original publish has already been delivered.
                    MqttPubComp pubComp = new MqttPubComp(message
                            .getMessageId());
                    this.send(pubComp, null);
                }
            }
        }
    }

??????clientState的notifyResult()方法,經過一系列調用,最終會通過CommsCallback中的workAvailable.notifyAll()---->run()---->handleActionComplete()方法,看一下該方法的實現:

private void handleActionComplete(MqttToken token)
            throws MqttException {
        final String methodName = "handleActionComplete";
        synchronized (token) {
            if (token.isComplete()) {
                //刪除message
                clientState.notifyComplete(token);
            }
            
            // Unblock any waiters and if pending complete now set completed
            token.internalTok.notifyComplete();
            
            if (!token.internalTok.isNotified()) {
                // If a callback is registered and delivery has finished 
                // call delivery complete callback. 
                if ( mqttCallback != null 
                    && token instanceof MqttDeliveryToken 
                    && token.isComplete()) {
                        //回調deliveryComplete()方法
                        mqttCallback.deliveryComplete((MqttDeliveryToken) token);
                }
             //內部邏輯只有在connect()時才會調用,publish()時,Listener為null,不會執行
                fireActionEvent(token);
            }
            
            // Set notified so we don't tell the user again about this action.
            if ( token.isComplete() ){
               if ( token instanceof MqttDeliveryToken || token.getActionCallback() instanceof IMqttActionListener ) {
                    token.internalTok.setNotified(true);
                }
            }
        }
    }

??????以下是在收到MqttPubAck或MqttPubComp后,會將存儲的persistence刪除掉。

//ClientState.java
protected void notifyComplete(MqttToken token) throws MqttException {
        
        final String methodName = "notifyComplete";

        MqttWireMessage message = token.internalTok.getWireMessage();

        if (message != null && message instanceof MqttAck) {
            
            // @TRACE 629=received key={0} token={1} message={2}
            log.fine(CLASS_NAME, methodName, "629", new Object[] {
                     new Integer(message.getMessageId()), token, message });

            MqttAck ack = (MqttAck) message;

            if (ack instanceof MqttPubAck) {
                
                // QoS 1 - user notified now remove from persistence...
                persistence.remove(getSendPersistenceKey(message));
                outboundQoS1.remove(new Integer(ack.getMessageId()));
                decrementInFlight();
                releaseMessageId(message.getMessageId());
                tokenStore.removeToken(message);
                // @TRACE 650=removed Qos 1 publish. key={0}
                log.fine(CLASS_NAME, methodName, "650",
                        new Object[] { new Integer(ack.getMessageId()) });
            } else if (ack instanceof MqttPubComp) {
                // QoS 2 - user notified now remove from persistence...
                persistence.remove(getSendPersistenceKey(message));
                persistence.remove(getSendConfirmPersistenceKey(message));
                outboundQoS2.remove(new Integer(ack.getMessageId()));

                inFlightPubRels--;
                decrementInFlight();
                releaseMessageId(message.getMessageId());
                tokenStore.removeToken(message);

                // @TRACE 645=removed QoS 2 publish/pubrel. key={0}, -1 inFlightPubRels={1}
                log.fine(CLASS_NAME, methodName, "645", new Object[] {
                        new Integer(ack.getMessageId()),
                        new Integer(inFlightPubRels) });
            }

            checkQuiesceLock();
        }
    }

??????以上邏輯主要是發送或接收Qos=1和Qos=2的message后,publisher與Broker、Broker與Subscriber之間的交互流程。
??????看一下總的流程圖:


image.png

5.總結

??????Qos0:消息不存persistence,publish后直接通過notifySent()后來complete;
??????Qos1:publisher:消息存persistence,publish后收到PubAck后來進行complete及persistence.remove();
??????????????????subscriber:notifyReceivedMsg后,先deliverMessage(),然后send(PubAck);
??????Qos2:publisher:消息存persistence,publish后會收到PubRec,然后發送PubRel,再收到PubComp后進行complete及persistence.remove();
??????????????????subscriber:notifyReceivedMsg后,先persistence.put(),inboundQos2.put(),然后send(PubRec)到Broker,收到來自Broker的PubRel后,再次notifyReceivedMsg,執行deliverMessage(),send(PubComp),刪除persistence。如果再次收到PubRel,不會進行deliverMessage(),直接send(PubComp)。

六.心跳機制

??????1.Keep Alive指定連接最大空閑時間T,當客戶端檢測到連接空閑時間超過T時,必須向Broker發送心跳報文PINGREQ,Broker收到心跳請求后返回心跳響應PINGRESP。
??????2.若Broker超過1.5T時間沒收到心跳請求則斷開連接,并且投遞遺囑消息到訂閱方;同樣,若客戶端超過一定時間仍沒收到Broker心跳響應PINGRESP則斷開連接。
??????3.連接空閑時發送心跳報文可以降低網絡請求,弱化對帶寬的依賴。

七.保留消息定義[retained]

??????如果Publish消息的retained標記位被設置為1,則稱該消息為“保留消息”;
??????Broker對保留消息的處理如下
??????Broker會存儲每個Topic的最后一條保留消息及其Qos,當訂閱該Topic的客戶端上線后,Broker需要將該消息投遞給它。
??????保留消息作用
??????可以讓新訂閱的客戶端得到發布方的最新的狀態值,而不必要等待發送。
??????保留消息的刪除
??????方式1:發送空消息體的保留消息;
??????方式2:發送最新的保留消息覆蓋之前的(推薦);

八.完全實現解耦

??????MQTT這種結構替代了傳統的客戶端/服務器模型,可以實現以下解耦:
??????空間解耦:發布者和訂閱者不需要知道對方;
??????時間解耦:發布者和訂閱者不需要同時運行(離線消息, retained = 1的話,可以實現);
??????同步解耦:發布和接收都是異步通訊,無需停止任何處理;

九.與HTTP比較:

??????MQTT最長可以一次性發送256MB數據;
??????HTTP是典型的C/S通訊模式:請求從客戶端發出,服務端只能被動接收,一條連接只能發送一次請求,獲取響應后就斷開連接;
??????HTTP的請求/應答方式的會話都是客戶端發起的,缺乏服務器通知客戶端的機制,客戶端應用需要不斷地輪詢服務器;

十.mosquitto.conf

??????可以通過以下輸出mosquitto日志:

log_dest file /storage/emulated/0/mosquitto.log //Android系統輸出到文件
log_dest stdout // linux系統直接輸出到工作臺
log_type all

十一.mqtt本地調試

1.啟動broker,mosquitto – 代理器主程序

./mosquitto &

??????其中:broker ip 為電腦端ip;port:默認1883;

2.mosquitto_pub – 用于發布消息的命令行客戶端,向已訂閱的topic發布消息

./mosquitto_pub -h host -p port -t topic -m message

??????其中:-h:broker ip;-p:端口號,默認1883;-t:已訂閱的topic;-m:發布的消息
??????舉例

./mosquitto_pub -h 10.10.20.10 -p 1883 -t baiduMap -m "{"origin": "西直門","end":"東直門"}"

3.mosquitto_sub – 用于訂閱消息的命令行客戶端,訂閱topic

./mosquitto_sub -h host -p port -t topic

??????其中:-h:broker ip;-p:端口號,默認1883;-t:需要訂閱的topic

4.運行環境

??????ubuntu系統,將libmosquitto.so.1放入系統變量中:

export LD_LIBRARY_PATH= 附件libmosquitto.so.1路徑:$PATH

5.查看連接broker的客戶端

??????broker默認的端口號1883,可以通過端口號來查看已連接的客戶端

netstat -an | grep :1883
tcp        0      0 0.0.0.0:1883            0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:1883             127.0.0.1:44618         ESTABLISHED
tcp        0      0 172.27.117.1:1883       172.27.117.1:45256      ESTABLISHED
tcp        0      0 172.27.117.1:1883       172.27.117.2:49612      ESTABLISHED
tcp        0      0 172.27.117.1:1883       172.27.117.2:49508      ESTABLISHED

??????可以看到連接broker的客戶端為4個;

??????以上分別介紹了MQTT協議以及服務端mosquitto、客戶端Eclipse paho的使用及源碼介紹!

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

推薦閱讀更多精彩內容