物聯網協議之MQTT原始碼分析(一)

若丶相見發表於2019-05-12

不知不覺已經跟MQTT打交道半年了,才驚醒的發現我也算從事的物聯網方法(Android端),一直以來只是單純的使用MQTT連線、釋出和接收訊息等,也沒在乎其Client(Android)端的原始碼是怎樣實現的,要不是最近專案出現一個小問題困擾了很久可能也不會引發我看一看MQTT的原始碼實現。好啦讓我們開始瞭解MQTT的神奇之處吧。(注:若有什麼地方闡述有誤,敬請指正。)

前言

閱讀本文前,預設讀者已經熟知MQTT的Android端使用,Client代表客戶端,Broker代表服務端,此篇原始碼分析主要以MQTT客戶端和服務端建立連線過程為主線講解。基礎瞭解Mqtt報文格式等,可以參考下MQTT協議官網中文地址:

mcxiaoke.gitbooks.io/mqtt-cn/con…

org.eclipse.paho工程原始碼分析涉及到的類:

  • MqttAndroidClient
  • MqttService
  • MqttConnection
  • MqttAsyncClient
  • ConnectActionListener
  • ClientComms
  • CommsSender
  • CommsReceiver
  • ClientState
  • CommsCallback

原始碼分析準備

為方便分析原始碼先貼上一段工程裡連線MQTT的程式碼:

// 自己工程中關於MQTT連線類:
String uri = "";
if(isSSL){
    uri = "ssl://" + ip + ":" + port;
} else{
    uri = "tcp://" + ip + ":" + port;
}

MqttConnectOptions conOpt = new MqttConnectOptions();
try{
    conOpt.setSocketFactory(get2SSLSocketFactory(clientIns, ins, keypassword, keypassword));
} catch(MqttSecurityException e){
    e.printStackTrace();
}
conOpt.setUserName("mqttservice");
char[] password = "mqttservice".toCharArray();
conOpt.setPassword(password);
conOpt.setConnectionTimeout(5);
conOpt.setCleanSession(false);//設定是否清空session,這裡如果設定為false表示伺服器會保留客戶端的連線記錄,這裡設定為true表示每次連線到伺服器都以新的身份連線
conOpt.setKeepAliveInterval(60);//The default value is 60 seconds
String mClientId = NetUtil.getLocalMacAddress();// 獲取本地網路mac地址
String[] clientIds = new String[1];
clientIds[0]=mClientId;
clientInfo =uri +mClientId;
mMqttCallback =new MqttConnectCallback(mContext, clientInfo);
myClient =new MqttAndroidClient(mContext, uri, mClientId);
myClient.setCallback(mMqttCallback);
// IMqttActionListener的實現類,動態賦值為連線狀態CONNECT
final ActionListener callback = new ActionListener(ActionType.CONNECT);

String topic = "/client/" + UUID.randomUUID().toString();
int qos = 0;
boolean retained = true;
try{
    // 設定遺囑訊息:當客戶端斷開連線時,傳送給相關的訂閱者的遺囑訊息
    // 具體瞭解遺囑訊息請參考:https://www.jianshu.com/p/a5c6b768ed55
    conOpt.setWill(topic, "offline".getBytes(), qos, retained);
} catch(Exception e){
    callback.onFailure(null, e);
}
try{
    myClient.connect(conOpt, null, callback);
} catch(Exception e){
    callback.onFailure(null, e);
}
複製程式碼

根據上述程式碼可以看出,MQTT的連線是由MqttAndroidClient的connect函式而起,MqttAndroidClient物件初始化時傳入了uri和mClientId,呼叫connect函式時傳入了符合自己需求的Mqtt連線選項,因此我們先來看下MqttAndroidClient的connect函式:

MqttAndroidClient

// MqttAndroidClient類:
@Override
public IMqttToken connect(MqttConnectOptions options, Object userContext,
                          IMqttActionListener callback) throws MqttException {

    IMqttToken token = new MqttTokenAndroid(this, userContext, callback);

    connectOptions = options;
    connectToken = token;

    /*
     * 實際的連線取決於我們在這裡啟動和繫結的服務,
     * 但是在serviceConnection的onServiceConnected()方法執行(非同步)之前
     * 我們實際上不能使用它,所以連線本身發生在onServiceConnected()方法上
     */
    if (mqttService == null) { // First time - must bind to the service
        Intent serviceStartIntent = new Intent();
        serviceStartIntent.setClassName(myContext, SERVICE_NAME);
        Object service = myContext.startService(serviceStartIntent);
        if (service == null) {
            IMqttActionListener listener = token.getActionCallback();
            if (listener != null) {
                listener.onFailure(token, new RuntimeException(
                        "cannot start service " + SERVICE_NAME));
            }
        }

        // We bind with BIND_SERVICE_FLAG (0), leaving us the manage the lifecycle
        // until the last time it is stopped by a call to stopService()
        myContext.bindService(serviceStartIntent, serviceConnection,
                Context.BIND_AUTO_CREATE);

        if (!receiverRegistered) registerReceiver(this);
    } else {
        // 執行緒池執行
        pool.execute(new Runnable() {
            @Override
            public void run() {
                doConnect();
                //Register receiver to show shoulder tap.
                if (!receiverRegistered) registerReceiver(MqttAndroidClient.this);
            }

        });
    }
    return token;
}

    /**
     * 繫結MqttService服務的回撥
     */
    private final class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            mqttService = ((MqttServiceBinder) binder).getService();
            bindedService = true;
            // 最後還是執行的該方法
            doConnect();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mqttService = null;
        }
    }

    // Listener for when the service is connected or disconnected
    private final MyServiceConnection serviceConnection = new MyServiceConnection();
複製程式碼

這個函式會啟動paho mqtt唯一一個元件MqttService,這個元件不支援跨程式呼叫,如果需要將MqttService放在其他程式,需要將和mqtt相關的呼叫全部放在同一個程式內。由於需要使用MqttService元件中的函式,需要在啟動MqttService後對MqttService進行繫結。如果服務已經啟動,則直接執行建立連線操作。這時候建立的連線僅僅是網路連線,不是mqtt協議連線。由上面程式碼可以看出,無論是MqttService是否啟動並繫結,最終都是呼叫doConnect()方法繼續執行連線操作。

// MqttAndroidClient類:
    private void doConnect() {
        if (clientHandle == null) {
            clientHandle = mqttService.getClient(serverURI, clientId, myContext.getApplicationInfo().packageName,persistence);
        }
        mqttService.setTraceEnabled(traceEnabled);
        mqttService.setTraceCallbackId(clientHandle);

        String activityToken = storeToken(connectToken);
        try {
            mqttService.connect(clientHandle, connectOptions, null,
                    activityToken);
        } catch (MqttException e) {
            IMqttActionListener listener = connectToken.getActionCallback();
            if (listener != null) {
                listener.onFailure(connectToken, e);
            }
        }
    }
複製程式碼

直到此時出現了activityToken, connectToken, clientHandle,不要慌,我們來一個一個分析。MqttAndroidClient的connect函式時,會生成connectToken,具體生成如下:

// MqttAndroidClient類:
    IMqttToken token = new MqttTokenAndroid(this, userContext, callback);

    connectOptions = options;
    connectToken = token;
複製程式碼

token機制在paho mqtt實現中扮演著十分重要的角色,負責訊息各種回撥的實現,後面章節再單獨分析paho mqtt的token機制。再來看一下clientHandle的來源:

// MqttService類:
    public String getClient(String serverURI, String clientId
        , String contextId, MqttClientPersistence persistence) {
        String clientHandle = serverURI + ":" + clientId+":"+contextId;
        if (!connections.containsKey(clientHandle)) {
            MqttConnection client = new MqttConnection(this, serverURI,
                clientId, persistence, clientHandle);
            connections.put(clientHandle, client);
        }
        return clientHandle;
  }
複製程式碼

clientHandle是serverURI + ":" + clientId+":"+contextId組合形成的字串,contextId是應用包名。此段程式碼中引入了一個新的類MqttConnection,而MqttConnection代表著Mqtt的連線例項,MqttService內部使用connections記錄每一個連線例項。最後瞭解下activityToken,我們看下storeToken(connectToken)函式:

// MqttAndroidClient類:
    private synchronized String storeToken(IMqttToken token) {
        tokenMap.put(tokenNumber, token);
        return Integer.toString(tokenNumber++);
    }
複製程式碼

MqttAndroidClient內部使用tokenMap記錄每次呼叫生成的token, 將tokenNumber返回。activityToken會傳入MqttConnection中,並儲存於MqttConnection類中connect函式的Bundle變數resultBundle裡,而resultBundle最終會被用於傳送廣播觸發我們connect、publish、subscribe等的回撥監聽。這裡暫時先了解這些,我們接著看執行完doConnect函式後,函式呼叫到了MqttService元件中的connect函式:

MqttService

// MqttService類:
    public void connect(String clientHandle, MqttConnectOptions connectOptions,
      String invocationContext, String activityToken)
      throws MqttSecurityException, MqttException {
	  	MqttConnection client = getConnection(clientHandle);
	  	client.connect(connectOptions, null, activityToken);
  }
  
  private MqttConnection getConnection(String clientHandle) {
    MqttConnection client = connections.get(clientHandle);
    if (client == null) {
      throw new IllegalArgumentException("Invalid ClientHandle");
    }
    return client;
  }
複製程式碼

看到clientHandle是不是有點熟悉,上面我們講過connections將生成的MqttConnection例項儲存起來,這一步通過getConnection重新獲取。接下來,程式碼來到了MqttConnection.connect函式中:

// MqttConnection類:
    public void connect(MqttConnectOptions options, String invocationContext,
                        String activityToken) {

        connectOptions = options;
        reconnectActivityToken = activityToken;
        
        // //根據自己設定的連線選項cleanSession,判斷是否清除歷史訊息
        if (options != null) {
            cleanSession = options.isCleanSession();
        }

        if (connectOptions.isCleanSession()) {
            // discard old data
            service.messageStore.clearArrivedMessages(clientHandle);
        }

        service.traceDebug(TAG, "Connecting {" + serverURI + "} as {" + clientId + "}");
        final Bundle resultBundle = new Bundle();
        // 將activityToken儲存至resultBundle,驗證上面所敘述的activityToken
        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
                activityToken);
        resultBundle.putString(
                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
                invocationContext);
        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
                MqttServiceConstants.CONNECT_ACTION);

        try {
            if (persistence == null) {
                // ask Android where we can put files
                //2016.12 zhn change:for no permissions
                File myDir = service.getFilesDir();//File myDir = service.getExternalFilesDir(TAG);

                if (myDir == null) {
                    // No external storage, use internal storage instead.
                    myDir = service.getDir(TAG, Context.MODE_PRIVATE);

                    if (myDir == null) {
                        resultBundle.putString(
                                MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
                                "Error! No external and internal storage available");
                        resultBundle.putSerializable(
                                MqttServiceConstants.CALLBACK_EXCEPTION,
								new MqttPersistenceException());
                        service.callbackToActivity(clientHandle, Status.ERROR,
                                resultBundle);
                        return;
                    }
                }

                // 用它來設定MQTT客戶端永續性儲存
                persistence = new MqttDefaultFilePersistence(
                        myDir.getAbsolutePath());
            }
            // 用於監聽連線成功的回撥
            IMqttActionListener listener = new MqttConnectionListener(resultBundle) {

                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    doAfterConnectSuccess(resultBundle);
                    service.traceDebug(TAG, "connect success!");
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken,
                                      Throwable exception) {
                    resultBundle.putString(
                            MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
                            exception.getLocalizedMessage());
                    resultBundle.putSerializable(
                            MqttServiceConstants.CALLBACK_EXCEPTION, exception);
                    service.traceError(TAG,
                            "connect fail, call connect to reconnect.reason:"
                                    + exception.getMessage());

                    doAfterConnectFail(resultBundle);
                }
            };

            if (myClient != null) {//如果已經建立過MqttAsyncClient,即之前就呼叫過本connect()方法
                if (isConnecting) {//上次呼叫的connect()還在連線中,不做處理,等待connect()結果
                    service.traceDebug(TAG,
                            "myClient != null and the client is connecting. Connect return " +"directly.");
                    service.traceDebug(TAG, "Connect return:isConnecting:" + isConnecting +
							".disconnected:" + disconnected);
                } else if (!disconnected) {//當前已處於長連線,提示連線成功
                    service.traceDebug(TAG, "myClient != null and the client is connected and " +"notify!");
                    doAfterConnectSuccess(resultBundle);
                } else {//之前的連線未成功或者已掉線,重新嘗試連線
                    service.traceDebug(TAG, "myClient != null and the client is not connected");
                    service.traceDebug(TAG, "Do Real connect!");
                    setConnectingState(true);
                    myClient.connect(connectOptions, invocationContext, listener);
                }
            }else { // if myClient is null, then create a new connection
                alarmPingSender = new AlarmPingSender(service);//用於傳送心跳包
                // 建立MqttAsyncClient
                myClient = new MqttAsyncClient(serverURI, clientId,
                        persistence, alarmPingSender);
                myClient.setCallback(this);

                service.traceDebug(TAG, "Do Real connect!");
                // 設定連線狀態
                setConnectingState(true);
                // 連線
                myClient.connect(connectOptions, invocationContext, listener);
            }
        } catch (Exception e) {
            service.traceError(TAG, "Exception occurred attempting to connect: " + e.getMessage());
            setConnectingState(false);
            handleException(resultBundle, e);
        }
    }
複製程式碼

從上面程式碼以及註釋中可知,這段程式碼主要作用就是新建了MqttAsyncClient物件,然後註冊了回撥函式,然後去執行connect函式,同時將狀態置為正在連線狀態。接下來就分析下MqttAsyncClient.connect函式:

MqttAsyncClient

// MqttAsyncClient類:
        public IMqttToken connect(MqttConnectOptions options
            , Object userContext, IMqttActionListener callback)
			throws MqttException, MqttSecurityException {
		final String methodName = "connect";
		// 狀態判斷
		if (comms.isConnected()) {
			throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_CONNECTED);
		}
		if (comms.isConnecting()) {
			throw new MqttException(MqttException.REASON_CODE_CONNECT_IN_PROGRESS);
		}
		if (comms.isDisconnecting()) {
			throw new MqttException(MqttException.REASON_CODE_CLIENT_DISCONNECTING);
		}
		if (comms.isClosed()) {
			throw new MqttException(MqttException.REASON_CODE_CLIENT_CLOSED);
		}
		if (options == null) {
			options = new MqttConnectOptions();
		}
		this.connOpts = options;
		this.userContext = userContext;
		final boolean automaticReconnect = options.isAutomaticReconnect();

		// @TRACE 103=cleanSession={0} connectionTimeout={1} TimekeepAlive={2}
		// userName={3} password={4} will={5} userContext={6} callback={7}
		log.fine(CLASS_NAME, methodName, "103",
				new Object[] { Boolean.valueOf(options.isCleanSession()), new Integer(options.getConnectionTimeout()),
						new Integer(options.getKeepAliveInterval()), options.getUserName(),
						((null == options.getPassword()) ? "[null]" : "[notnull]"),
						((null == options.getWillMessage()) ? "[null]" : "[notnull]"), userContext, callback });
		// 設定網路連線
		comms.setNetworkModules(createNetworkModules(serverURI, options));
		// 設定重連回撥
		comms.setReconnectCallback(new MqttReconnectCallback(automaticReconnect));

		// Insert our own callback to iterate through the URIs till the connect
		// succeeds
		MqttToken userToken = new MqttToken(getClientId());
		// 初始化連線動作偵聽器connectActionListener
		ConnectActionListener connectActionListener = new ConnectActionListener(this, persistence, comms, options,
				userToken, userContext, callback, reconnecting);
		userToken.setActionCallback(connectActionListener);
		userToken.setUserContext(this);

		// If we are using the MqttCallbackExtended, set it on the
		// connectActionListener
		if (this.mqttCallback instanceof MqttCallbackExtended) {
			connectActionListener.setMqttCallbackExtended((MqttCallbackExtended) this.mqttCallback);
		}

		comms.setNetworkModuleIndex(0);
		// 連線動作偵聽器繼續執行connect
		connectActionListener.connect();

		return userToken;
	}
複製程式碼

MqttAsyncClient.connect函式的主要作用是設定了網路連線模組,設定重連回撥,最後執行connectActionListener.connect函式。這段程式碼又引進來一個新的類ClientComms,我們先來看下ClientComms的初始化:

// MqttAsyncClient類:
        public MqttAsyncClient(String serverURI, String clientId
            , MqttClientPersistence persistence,MqttPingSender pingSender, ScheduledExecutorService executorService) throws MqttException {
		final String methodName = "MqttAsyncClient";
		...
		// 建立大小為10的執行緒池
		this.executorService = executorService;
		if (this.executorService == null) {
			this.executorService = Executors.newScheduledThreadPool(10);
		}
		...
		// 初始化ClientComms,並傳入大小為10的執行緒池
		this.comms = new ClientComms(this
		    , this.persistence, pingSender,this.executorService);
		this.persistence.close();
		this.topics = new Hashtable();
	}
// 	ClientComms類中:
	public ClientComms(IMqttAsyncClient client, MqttClientPersistence persistence, MqttPingSender pingSender, ExecutorService executorService) throws MqttException {
		this.conState = DISCONNECTED;
		this.client 	= client;
		this.persistence = persistence;
		this.pingSender = pingSender;
		this.pingSender.init(this);
		this.executorService = executorService;

		this.tokenStore = new CommsTokenStore(getClient().getClientId());
		this.callback 	= new CommsCallback(this);
		this.clientState = new ClientState(persistence, tokenStore, this.callback, this, pingSender);

		callback.setClientState(clientState);
		log.setResourceName(getClient().getClientId());
	}
複製程式碼

可以看出ClientComms是在MqttAsyncClient初始化時完成初始化的,並且將心跳的傳送器pingSender和大小為10的執行緒池一起傳入ClientComms。ClientComms類的初始化中又初始化了CommsTokenStore、CommsCallback和ClientState幾個類。我們再來看下重連回撥,重連程式碼有點多,我們只關注一下重連的回撥函式即可:

// MqttReconnectCallback類(MqttAsyncClient類中的內部類):
        class MqttReconnectCallback implements MqttCallbackExtended {
		...
		// 連線失敗,重連時會呼叫該方法
		public void connectionLost(Throwable cause) {
			if (automaticReconnect) {
				// Automatic reconnect is set so make sure comms is in resting
				// state
				comms.setRestingState(true);
				reconnecting = true;
				startReconnectCycle();
			}
		}
        ...
	}
	
	private void startReconnectCycle() {
		String methodName = "startReconnectCycle";
		// @Trace 503=Start reconnect timer for client: {0}, delay: {1}
		log.fine(CLASS_NAME, methodName, "503", new Object[] { this.clientId, new Long(reconnectDelay) });
		reconnectTimer = new Timer("MQTT Reconnect: " + clientId);
		reconnectTimer.schedule(new ReconnectTask(), reconnectDelay);
	}
	
	private class ReconnectTask extends TimerTask {
		private static final String methodName = "ReconnectTask.run";

		public void run() {
			// @Trace 506=Triggering Automatic Reconnect attempt.
			log.fine(CLASS_NAME, methodName, "506");
			attemptReconnect();
		}
	}
	
	private void attemptReconnect() {
		final String methodName = "attemptReconnect";
		...
		try {
			connect(this.connOpts, this.userContext, new MqttReconnectActionListener(methodName));
		} catch (MqttSecurityException ex) {
			// @TRACE 804=exception
			log.fine(CLASS_NAME, methodName, "804", null, ex);
		} catch (MqttException ex) {
			// @TRACE 804=exception
			log.fine(CLASS_NAME, methodName, "804", null, ex);
		}
	}
	
	class MqttReconnectActionListener implements IMqttActionListener {
		...
		public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
			...
			if (reconnectDelay < 128000) {
			    //reconnectDelay初始值為1000,每次重連失敗時*2
				reconnectDelay = reconnectDelay * 2;
			}
			rescheduleReconnectCycle(reconnectDelay);
		}
		...
	}
複製程式碼

自動重連的實現主要在的attemptReconnect()方法裡,重連失敗會繼續重連直到連線成功,不過重連的間隔時間會隨著重連次數增加最大到128s。最後我們再分析一下網路連線的設定createNetworkModules函式:

// MqttAsyncClient類:
            protected NetworkModule[] createNetworkModules(String address, MqttConnectOptions options)
			throws MqttException, MqttSecurityException {
		final String methodName = "createNetworkModules";
		// @TRACE 116=URI={0}
		log.fine(CLASS_NAME, methodName, "116", new Object[] { address });

		NetworkModule[] networkModules = null;
		String[] serverURIs = options.getServerURIs();
		String[] array = null;
		if (serverURIs == null) {
			array = new String[] { address };
		} else if (serverURIs.length == 0) {
			array = new String[] { address };
		} else {
			array = serverURIs;
		}

		networkModules = new NetworkModule[array.length];
		for (int i = 0; i < array.length; i++) {
			networkModules[i] = createNetworkModule(array[i], options);
		}

		log.fine(CLASS_NAME, methodName, "108");
		return networkModules;
	}
複製程式碼

options例項在建立連線的過程中,我們也僅僅是設定了和連線相關的一些狀態,並沒有設定serverURI,故options.getServerURIS返回為null。NetworkModule為paho定義的介面,規定了網路模組需要實現的方法。目前paho定義的網路連線模組有TCPNetworkModule,SSLNetworkModule,WebsocketNetworkModule,WebSocketSecureNetworkModule,可以看下createNetworkModule根據uri使用的協議型別建立對應的NetworkModule。建立完所有的NetworkModule後,執行comms.setNetworknModule(0),先使用第一個NetworkModule進行連線。comms是ClientComms型別的例項,在paho的實現中佔有非常重要的地位,後序部分會進行分析。來看下createNetwokModule函式的實現:

// MqttAsyncClient類:
        private NetworkModule createNetworkModule(String address, MqttConnectOptions options) throws MqttException, MqttSecurityException {
		final String methodName = "createNetworkModule";

		NetworkModule netModule;
		SocketFactory factory = options.getSocketFactory();

		int serverURIType = MqttConnectOptions.validateURI(address);

		URI uri;
		try {
			uri = new URI(address);
			...
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException("Malformed URI: " + address + ", " + e.getMessage());
		}

		String host = uri.getHost();
		int port = uri.getPort(); // -1 if not defined

		switch (serverURIType) {
		case MqttConnectOptions.URI_TYPE_TCP :
			...
			netModule = new TCPNetworkModule(factory, host, port, clientId);
			((TCPNetworkModule)netModule).setConnectTimeout(options.getConnectionTimeout());
			break;
		case MqttConnectOptions.URI_TYPE_SSL:
			...
			netModule = new SSLNetworkModule((SSLSocketFactory) factory, host, port, clientId);
			...
			break;
		case MqttConnectOptions.URI_TYPE_WS:
			...
			netModule = new WebSocketNetworkModule(factory, address, host, port, clientId);
			((WebSocketNetworkModule)netModule).setConnectTimeout(options.getConnectionTimeout());
			break;
		case MqttConnectOptions.URI_TYPE_WSS:
			...
			netModule = new WebSocketSecureNetworkModule((SSLSocketFactory) factory, address, host, port, clientId);
			...
			break;
		default:
			log.fine(CLASS_NAME,methodName, "119", new Object[] {address});
			netModule = null;
		}
		return netModule;
	}
複製程式碼

可以看出,createNetwokModule函式主要是根據serverURIType來判斷需要使用TCPNetworkModule,SSLNetworkModule,WebsocketNetworkModule,WebSocketSecureNetworkModule中的那種網路模組實現網路連線。

現在可以繼續往下繼續分析connectActionListener.connect()函式啦:

ConnectActionListener

// ConnectActionListener類:
public void connect() throws MqttPersistenceException {
    MqttToken token = new MqttToken(client.getClientId());
    token.setActionCallback(this);
    token.setUserContext(this);
    // 開啟持久化儲存
    persistence.open(client.getClientId(), client.getServerURI());

    if (options.isCleanSession()) {
      persistence.clear();
    }
    // 設定版本
    if (options.getMqttVersion() == MqttConnectOptions.MQTT_VERSION_DEFAULT) {
      options.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
    }
    
    try {
        // 開始連線
        comms.connect(options, token);
    }
    catch (MqttException e) {
      onFailure(token, e);
    }
  }
複製程式碼

從這段程式碼中可以看出,連線已交給comms.connect(options, token)函式,而comms的初始化上面也提到過,ClientComms是在MqttAsyncClient初始化時完成初始化的

ClientComms

// ClientComms類:
    public void connect(MqttConnectOptions options, MqttToken token) throws MqttException {
        final String methodName = "connect";
        synchronized (conLock) {
            if (isDisconnected() && !closePending) {
                //@TRACE 214=state=CONNECTING
                log.fine(CLASS_NAME,methodName,"214");
                // 設定連線狀態
                conState = CONNECTING;
                conOptions = options;
                // 構建CONNECT資料包
                MqttConnect connect = new MqttConnect(client.getClientId(),
                        conOptions.getMqttVersion(),
                        conOptions.isCleanSession(),
                        conOptions.getKeepAliveInterval(),
                        conOptions.getUserName(),
                        conOptions.getPassword(),
                        conOptions.getWillMessage(),
                        conOptions.getWillDestination());    
                // 設定clientState屬性
                this.clientState.setKeepAliveSecs(conOptions.getKeepAliveInterval());
                this.clientState.setCleanSession(conOptions.isCleanSession());
                this.clientState.setMaxInflight(conOptions.getMaxInflight());
                tokenStore.open();
                ConnectBG conbg = new ConnectBG(this, token, connect, executorService);
                conbg.start();
            }else {
                ...
            }
        }
    }
複製程式碼

從comms.connect函式的程式碼中可以看出,最後呼叫了conbg.start()函式,而ConnectBG是實現了Runnable的類,並且執行線上程池中:

// ClientComms類:
    private class ConnectBG implements Runnable {
        ...
        void start() {
            executorService.execute(this);
        }
        
        public void run() {
            Thread.currentThread().setName(threadName);
	    final String methodName = "connectBG:run";
	    MqttException mqttEx = null;
	    //@TRACE 220=>
	    log.fine(CLASS_NAME, methodName, "220");
	    
	    try {
	  	  // Reset an exception on existing delivery tokens.
	  	  // This will have been set if disconnect occured before delivery was
	  	  // fully processed.
	  	  MqttDeliveryToken[] toks = tokenStore.getOutstandingDelTokens();
	  	  for (int i=0; i<toks.length; i++) {
	  	 	 toks[i].internalTok.setException(null);
	  	  }
	    
	  	  // Save the connect token in tokenStore as failure can occur before send
	  	  tokenStore.saveToken(conToken,conPacket);
	    
	  	  // 啟動網路模組,發起網路連線
	  	  NetworkModule networkModule = networkModules[networkModuleIndex];
	  	  networkModule.start();
	  	  // 連線完成後,啟動receiver,負責從broker接收訊息
	  	  receiver = new CommsReceiver(clientComms, clientState, tokenStore, networkModule.getInputStream());
	  	  receiver.start("MQTT Rec: "+getClient().getClientId(), executorService);
	  	  // 連線完成後,啟動sender,負責向broker傳送訊息
	  	  sender = new CommsSender(clientComms, clientState, tokenStore, networkModule.getOutputStream());
	  	  sender.start("MQTT Snd: "+getClient().getClientId(), executorService);
	  	  // 連線完成後,啟動回撥監聽
	  	  /**
	  	   * CommsCallback:接收器和外部API之間的橋接。此類由Receiver呼叫
	  	   *    ,然後將以comms為中心的MQTT訊息物件轉換為外部API可理解的。
	  	  */
	  	  callback.start("MQTT Call: "+getClient().getClientId(), executorService);
	  	  // 向broker傳送CONNECT資料包
	  	  internalSend(conPacket, conToken);
	    } catch (MqttException ex) {
	  	  //@TRACE 212=connect failed: unexpected exception
	  	  log.fine(CLASS_NAME, methodName, "212", null, ex);
	  	  mqttEx = ex;
	    } catch (Exception ex) {
	  	  //@TRACE 209=connect failed: unexpected exception
	  	  log.fine(CLASS_NAME, methodName, "209", null, ex);
	  	  mqttEx =  ExceptionHelper.createMqttException(ex);
	    }
	    
	    if (mqttEx != null) {
	  	  shutdownConnection(conToken, mqttEx);
	    }
        }
    }
複製程式碼

從conbg.start()函式中可以看出,線上程池啟動執行了ConnectBG,因此現在所有的操作來到了ConnectBG的run()函式中,run()裡啟動了網路模組、接收broker訊息和傳送訊息的Runnable(CommsReceiver和CommsSender)、回撥監聽。此處需要說明一下NetworkModule為介面,實現它的子類呼叫start()方法,其實就是啟動Socket連線,而CommsReceiver、CommsSender和callback都是與ConnectBG一樣,皆是實現了Runnable的子類,執行於執行緒池中。最後呼叫internalSend方法傳送CONNECT資料包:

// ClientComms類:
    void internalSend(MqttWireMessage message, MqttToken token) throws MqttException {
	......
	try {
		// Persist if needed and send the message
		this.clientState.send(message, token);
	} catch(MqttException e) {
		......
	}
    }
複製程式碼

clientState負責在receiver和sender之間進行訊息處理,可以將sender看做是clientState的消費者, receiver負責接收來自broker的訊息。接下來看看clientState.send(message, token)函式:

ClientState

// ClientState類:
public void send(MqttWireMessage message, MqttToken token) throws MqttException {
	final String methodName = "send";
	......
	if (token != null ) {
		try {
			token.internalTok.setMessageID(message.getMessageId());
		} catch (Exception e) {
		}
	}
		
	if (message instanceof MqttPublish) {
		......
	} else {
		//@TRACE 615=pending send key={0} message {1}
		log.fine(CLASS_NAME,methodName,"615", new Object[]{new Integer(message.getMessageId()), 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 {
			......
		}
	}
}
複製程式碼

我們本文分析的原始碼為MQTT連線,因此訊息肯定是MqttConnect,send函式將訊息體新增到pendingFlows中,等待sender的排程併傳送。sender時Runnable例項,看下sender是如何排程傳送的,以下是sender的run函式:

CommsSender

// CommsSender類中:
    public void run() {
        sendThread = Thread.currentThread();
        sendThread.setName(threadName);
        final String methodName = "run";
        MqttWireMessage message = null;

        try {
            runningSemaphore.acquire();
        } catch (InterruptedException e) {
            running = false;
            return;
        }

        try {
            // 輪詢不斷取訊息,out為OutputStream的Socket
            while (running && (out != null)) {
                try {
                    //從 clientState中取訊息
                    message = clientState.get();
                    if (message != null) {
                        //@TRACE 802=network send key={0} msg={1}
                        log.fine(CLASS_NAME, methodName, "802", new Object[]{message.getKey(),message});
                        // mqttAck為響應訊息
                        if (message instanceof MqttAck) {
                            out.write(message);// 寫資料
                            out.flush();// 傳送
                        } else {
                            MqttToken token = tokenStore.getToken(message);
                            // While quiescing the tokenstore can be cleared so need
                            // to check for null for the case where clear occurs
                            // while trying to send a message.
                            if (token != null) {
                                synchronized (token) {
                                    out.write(message);// 寫資料
                                    try {
                                        out.flush();// 傳送
                                    } catch (IOException ex) {
                                        // The flush has been seen to fail on disconnect of a SSL
                                        // socket
                                        // as disconnect is in progress this should not be 
                                        // treated as an error
                                        if (!(message instanceof MqttDisconnect)) {
                                            throw ex;
                                        }
                                    }
                                    clientState.notifySent(message);
                                }
                            }
                        }
                    } else { // null message
                        //@TRACE 803=get message returned null, stopping}
                        log.fine(CLASS_NAME, methodName, "803");

                        running = false;
                    }
                } catch (MqttException me) {
                    handleRunException(message, me);
                } catch (Exception ex) {
                    handleRunException(message, ex);
                }
            } // end while
        } finally {
            running = false;
            runningSemaphore.release();
        }

        //@TRACE 805=<
        log.fine(CLASS_NAME, methodName, "805");
    }
複製程式碼

可以看出sender不斷迴圈從clientState獲取待傳送的訊息,clientState.get函式大家可以自行分析。 MQTT連線的訊息傳送出去啦,前面說到receiver是負責接收broker傳送回來的資料的,receiver也是Runnable型別,看下receiver的run函式實現:

CommsReceiver

// CommsReceiver類中:
    public void run() {
        recThread = Thread.currentThread();
        recThread.setName(threadName);
        final String methodName = "run";
        MqttToken token = null;

        try {
            runningSemaphore.acquire();
        } catch (InterruptedException e) {
            running = false;
            return;
        }
        輪詢不斷等待訊息,in為InputStream的Socket
        while (running && (in != null)) {
            try {
                //@TRACE 852=network read message
                log.fine(CLASS_NAME, methodName, "852");
                receiving = in.available() > 0;
                MqttWireMessage message = in.readMqttWireMessage();
                receiving = false;

                // mqttAck為響應訊息
                if (message instanceof MqttAck) {
                    token = tokenStore.getToken(message);
                    if (token != null) {
                        synchronized (token) {
                            // Ensure the notify processing is done under a lock on the token
                            // This ensures that the send processing can complete  before the
                            // receive processing starts! ( request and ack and ack processing
                            // can occur before request processing is complete if not!
                            clientState.notifyReceivedAck((MqttAck) message);
                        }
                    } else if (message instanceof MqttPubRec || message instanceof MqttPubComp || message instanceof MqttPubAck) {
                        ...
                    } else {
                        throw new MqttException(MqttException.REASON_CODE_UNEXPECTED_ERROR);
                    }
                } else {
                    if (message != null) {
                        // A new message has arrived
                        clientState.notifyReceivedMsg(message);
                    }
                }
            } 
            ......
        }
    }
複製程式碼

receiver收到訊息後,響應訊息的訊息型別為MqttAck,由於CONACK資料包是MqttAck型別,且token不為null,故會執行clientState.notifyReceivedAck函式.

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

        // @TRACE 627=received key={0} message={1}
        log.fine(CLASS_NAME, methodName, "627", new Object[]{
                new Integer(ack.getMessageId()), ack});

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

        if (token == null) {
            // @TRACE 662=no message found for ack id={0}
            log.fine(CLASS_NAME, methodName, "662", new Object[]{
                    new Integer(ack.getMessageId())});
        } else if (ack instanceof MqttPubRec) {
            // Complete the QoS 2 flow. Unlike all other
            // flows, QoS is a 2 phase flow. The second phase sends a
            // PUBREL - the operation is not complete until a PUBCOMP
            // is received
            MqttPubRel rel = new MqttPubRel((MqttPubRec) ack);
            this.send(rel, token);
        } else if (ack instanceof MqttPubAck || ack instanceof MqttPubComp) {
            // QoS 1 & 2 notify users of result before removing from
            // persistence
            notifyResult(ack, token, mex);
            // Do not remove publish / delivery token at this stage
            // do this when the persistence is removed later 
        } 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) {
            int rc = ((MqttConnack) ack).getReturnCode();
            // 根據CONACK資料包中的返回碼判斷協議連線是否已經建立,0表示服務端接受連線,協議正常建立。
            if (rc == 0) {
                synchronized (queueLock) {
                    if (cleanSession) {
                        clearState();
                        // Add the connect token back in so that users can be  
                        // notified when connect completes.
                        tokenStore.saveToken(token, ack);
                    }
                    inFlightPubRels = 0;
                    actualInFlight = 0;
                    restoreInflightMessages();
                    connected();
                }
            } else {
                mex = ExceptionHelper.createMqttException(rc);
                throw mex;
            }

            clientComms.connectComplete((MqttConnack) ack, mex);
            notifyResult(ack, token, mex);
            tokenStore.removeToken(ack);

            // Notify the sender thread that there maybe work for it to do now
            synchronized (queueLock) {
                queueLock.notifyAll();
            }
        } else {
            notifyResult(ack, token, mex);
            releaseMessageId(ack.getMessageId());
            tokenStore.removeToken(ack);
        }

        checkQuiesceLock();
    }
    
    public void connected() {
        final String methodName = "connected";
        //@TRACE 631=connected
        log.fine(CLASS_NAME, methodName, "631");
        // 設定連線完成狀態
        this.connected = true;
        // 開始心跳
        pingSender.start(); //Start ping thread when client connected to server.
    }
複製程式碼

notifyReceivedAck函式中,處理各種broker返回訊息,而連線訊息處理最後會到connected()連線完成的方法中,該方法設定連線完成狀態以及開始傳送心跳。 至此,MQTT連線原始碼分析已完成。

resultBundle

現在我們回頭看一下前面說的resultBundle,前面說到resultBundle最終會被用於傳送廣播觸發我們connect、publish、subscribe等的回撥監聽。我們先取一處簡單說明一下,前面也說到MqttConnection.connect函式中IMqttActionListener listener用於監聽連線成功的回撥。

簡單說明下listener呼叫過程:listener會被傳入MqttAsyncClient類裡,隨後又通過初始化ConnectActionListener類並儲存於其成員變數userCallback中,最後是在ConnectActionListener裡的onSuccess和onFailure兩回撥方法中呼叫了listener的onSuccess和onFailure兩個方法,而ConnectActionListener裡的onSuccess和onFailure兩函式一般是CommsCallback類所呼叫(也會被MqttTokenAndroid類的notifyComplete函式呼叫,notifyComplete函式被MqttAndroidClient類的simpleAction和disconnected兩方法呼叫,而simpleAction函式又會被連線、訂閱、解除訂閱、傳送等呼叫,暫時只簡單說一下這種情況)。上面程式碼註釋中也說過CommsCallback是接收器和外部API之間的橋接。此類由Receiver呼叫,然後將以comms為中心的MQTT訊息物件轉換為外部API可理解的。 CommsReceiver接收器裡輪詢會呼叫ClientState.notifyReceivedAck((MqttAck)message);函式,該函式裡有幾種訊息會呼叫notifyResult(ack, token, mex);函式,notifyResult方法對呼叫(CommsCallback)callback.asyncOperationComplete(token);對CommsCallback裡成員變數completeQueue(Vector)進行addElement操作,而CommsCallback的run方法又是一直輪詢監聽completeQueue裡是否有元素,有則呼叫handleActionComplete方法--》fireActionEvent方法--》ConnectActionListener裡的onSuccess和onFailure。 大致流程參考如圖:

物聯網協議之MQTT原始碼分析(一)

listener的onSuccess函式中裡呼叫了doAfterConnectSuccess(resultBundle);

// MqttConnection類中:
    private void doAfterConnectSuccess(final Bundle resultBundle) {
        acquireWakeLock();
        service.callbackToActivity(clientHandle, Status.OK, resultBundle);
        deliverBacklog();
        setConnectingState(false);
        disconnected = false;
        releaseWakeLock();
    }
複製程式碼

連線成功後會呼叫MqttService.callbackToActivity(),resultBundle就作為其中一個引數被傳入,接下來我們看看這個方法的實現:

// MqttService類:
void callbackToActivity(String clientHandle, Status status, Bundle dataBundle) {
    Intent callbackIntent = new Intent(MqttServiceConstants.CALLBACK_TO_ACTIVITY);
    if (clientHandle != null) {
        callbackIntent.putExtra(MqttServiceConstants.CALLBACK_CLIENT_HANDLE, clientHandle);
    }
    callbackIntent.putExtra(MqttServiceConstants.CALLBACK_STATUS, status);
    if (dataBundle != null) {
        callbackIntent.putExtras(dataBundle);
    }
    LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
}
複製程式碼

callbackToActivity()方法用於傳送本地廣播,廣播中攜帶resultBundle,其實包括publish、subscribe等行為不論成功失敗都會呼叫此方法,發出一個指示行為型別及狀態的本地廣播。那麼傳送的廣播是在哪接收的呢?其實前面一直沒有說MqttAndroidClient類繼承自BroadCastReceiver,因此我們檢視其onReceive()方法:

// MqttAndroidClient類:
    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle data = intent.getExtras();

        String handleFromIntent = data
                .getString(MqttServiceConstants.CALLBACK_CLIENT_HANDLE);

        if ((handleFromIntent == null)
                || (!handleFromIntent.equals(clientHandle))) {
            return;
        }

        String action = data.getString(MqttServiceConstants.CALLBACK_ACTION);

        if (MqttServiceConstants.CONNECT_ACTION.equals(action)) {
            connectAction(data);
        } else if (MqttServiceConstants.CONNECT_EXTENDED_ACTION.equals(action)) {
            connectExtendedAction(data);
        } else if (MqttServiceConstants.MESSAGE_ARRIVED_ACTION.equals(action)) {
            messageArrivedAction(data);
        } else if (MqttServiceConstants.SUBSCRIBE_ACTION.equals(action)) {
            subscribeAction(data);
        } else if (MqttServiceConstants.UNSUBSCRIBE_ACTION.equals(action)) {
            unSubscribeAction(data);
        } else if (MqttServiceConstants.SEND_ACTION.equals(action)) {
            sendAction(data);
        } else if (MqttServiceConstants.MESSAGE_DELIVERED_ACTION.equals(action)) {
            messageDeliveredAction(data);
        } else if (MqttServiceConstants.ON_CONNECTION_LOST_ACTION
                .equals(action)) {
            connectionLostAction(data);
        } else if (MqttServiceConstants.DISCONNECT_ACTION.equals(action)) {
            disconnected(data);
        } else if (MqttServiceConstants.TRACE_ACTION.equals(action)) {
            traceAction(data);
        } else {
            mqttService.traceError(MqttService.TAG, "Callback action doesn't exist.");
        }
    }
    
    private void messageArrivedAction(Bundle data) {
        if (callback != null) {
            String messageId = data
                    .getString(MqttServiceConstants.CALLBACK_MESSAGE_ID);
            String destinationName = data
                    .getString(MqttServiceConstants.CALLBACK_DESTINATION_NAME);

            ParcelableMqttMessage message = data
                    .getParcelable(MqttServiceConstants.CALLBACK_MESSAGE_PARCEL);
            try {
                if (messageAck == Ack.AUTO_ACK) {
                    callback.messageArrived(destinationName, message);
                    mqttService.acknowledgeMessageArrival(clientHandle, messageId);
                } else {
                    message.messageId = messageId;
                    callback.messageArrived(destinationName, message);
                }

                // let the service discard the saved message details
            } catch (Exception e) {
                // Swallow the exception
            }
        }
    }
複製程式碼

data.getString(MqttServiceConstants.CALLBACK_ACTION)獲取的就是我們前面存放在resultBundle中的action,然後根據action去呼叫對應的方法去回撥callback的對應方法,例如:action為MESSAGE_ARRIVED_ACTION時,呼叫messageArrivedAction函式,如果需要監聽action為MqttServiceConstants.MESSAGE_ARRIVED_ACTION的行為,則要求我們傳入的callback必須為MqttCallback的實現,而如果需要監聽action為MqttServiceConstants.CONNECT_EXTENDED_ACTION的行為,則要求我們傳入的callback必須為MqttCallbackExtended的實現,MqttCallbackExtended是MqttCallback的子類。這裡的callback就是我們建立連線前初始化MqttAndroidClient時設定的MqttCallback物件:

// 本文最初建立MQTT連線部分程式碼:
    // MqttConnectCallback為MqttCallback的實現類
    mMqttCallback =new MqttConnectCallback(mContext, clientInfo);
    myClient =new MqttAndroidClient(mContext, uri, mClientId);
    myClient.setCallback(mMqttCallback);
複製程式碼

至此,分析完連線MQTT的原始碼,下一篇分析MQTT釋出訊息publish。

參考連結:

blog.csdn.net/rockstore/a…

blog.csdn.net/Dovar_66/ar…

...

(注:若有什麼地方闡述有誤,敬請指正。)

相關文章