站在Android开发者的角度认识MQTT - 源码篇

MQTT 是用于物联网 (IoT) 的 OASIS 标准消息传递协议。它被设计为一种极其轻量级的发布/订阅消息传输,非常适合以较小的代码占用空间和最小的网络带宽连接远程设备。如今,MQTT 已广泛应用于各个行业,例如汽车、制造、电信、石油和天然气等。

本篇文章旨在和大家一起较为深入的去了解下MQTT的连接流程,在前面的两篇文章中我们已经基本了解了如何去使用MQTT和MQTT认证的相关知识,现在就一起走入MQTT的源码中探索吧。

入口 MqttAndroidClient#connect

通过第一篇文章我们了解到,MQTT的连接是通过MqttAndroidClient.connect()方法实现,那么我们就从此方法入手,看看第一步是如何迈出去的。

在看connect()方法之前,我们先看下MqttAndroidClient类,它不仅实现了IMqttAsyncClient接口,而且继承了BroadcastReceiver。

  • 实现IMqttAsyncClient接口是为了处理connect()、disConnect()、publish()等一些操作;
  • 继承BroadcastReceiver则是为了接收一些本地广播的消息,此广播消息包含了连接成功失败回调、接收到消息的回调、发送消息的结果回调,具体消息处理可以看MqttAndroidClient#onReceive方法,这里就不再过多介绍。

然后我们就可以进入connect()方法了:

ini 复制代码
# MqttAndroidClient.connect()

@Override
public IMqttToken connect(MqttConnectOptions options, Object userContext,
		IMqttActionListener callback) throws MqttException {
	IMqttToken token = new MqttTokenAndroid(this, userContext,
			callback);
	connectOptions = options;
	connectToken = token;
	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));
			}
		}
		myContext.bindService(serviceStartIntent, serviceConnection,
				Context.BIND_AUTO_CREATE);
		if (!receiverRegistered) registerReceiver(this);
	}
	else {
		pool.execute(new Runnable() {
			@Override
			public void run() {
				doConnect();
				if (!receiverRegistered) registerReceiver(MqttAndroidClient.this);
			}
		});
	}
	return token;
}

在上面第5行时创建了一个MqttTokenAndroid对象,它实现了IMqttToken接口,此对象可以获取MqttAndroidClient、topic等信息;然后在第11行判断了mqttService是否为空,如果为空那么就启动一个Service,并且通过bindService()形式进行绑定,这里需要留意下serviceConnection,它是非常关键的地方之一;绑定完Service之后,调用了registerReceiver()方法,其内部就是通过本地广播将自身注册起来,用于接收一些回调信息,也就是我们上面提到的一些信息。

然后在25行地方,如果mqttService不为空,代表服务已经启动过了,那么直接通过线程池启动一个子线程来执行doConnect()方法并且注册本地广播。

下面我们看看serviceConnection对象里面的逻辑:

typescript 复制代码
private final class MyServiceConnection implements ServiceConnection {

   @Override
   public void onServiceConnected(ComponentName name, IBinder binder) {
      mqttService = ((MqttServiceBinder) binder).getService();
      bindedService = true;
      // now that we have the service available, we can actually
      // connect...
      doConnect();
   }

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

serviceConnect是MyServiceConnection对象,继承了ServiceConnection,通过第4行处可以看到,当服务连接成功之后,首先将mqttService赋值,并且也是执行doConnect()方法,这和上面mqttService不为空的逻辑是一致的,都是调用了doConnect()方法。

那么我们就可以追溯到doConnect()方法,看看里面是如何进行MQTT的连接流程:

ini 复制代码
# MqttAndroidClient.doConnect()
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);
      }
   }
}

# MqttService.getClient()
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;
}

此方法核心的地方在11行,它直接调用了mqttService.connect()方法,此时MqttAndroidClient内部的流程就结束了,转而进入MqttService内部。

这里需要注意的一点就是clientHandle,他通过sercerURI和clientId等信息生成,然后会存入到MqttService的connects中,connects是一个Map对象。生成clientHandle的同时,也会创建一个MqttConnection对象,这个对象下面会重点提到,这里知道在此创建的即可,它和clientHandle是一个key-value的关系。

MqttService.connect()

arduino 复制代码
public void connect(String clientHandle, MqttConnectOptions connectOptions,
    String invocationContext, String activityToken)
    throws MqttSecurityException, MqttException {
   MqttConnection client = getConnection(clientHandle);
   client.connect(connectOptions, null, activityToken);
}

乍一看,此方法如此简洁基本上没做任何逻辑处理,只是通过clientHandle从connects从取出之前存入的MqttConnection对象,然后就将连接的流程都给它了,MqttService撒手不管了,那我们只能进入到MqttConnect.connect()方法寻找真相了🤣

MqttConnect.connect()

scss 复制代码
# MqttConnect.connect
public void connect(MqttConnectOptions options, String invocationContext,
		String activityToken) {
	
	connectOptions = options;
	reconnectActivityToken = activityToken;
	if (options != null) {
		cleanSession = options.isCleanSession();
	}
	if (connectOptions.isCleanSession()) { // if it's a clean session,
		// discard old data
		service.messageStore.clearArrivedMessages(clientHandle);
	}
			
	try {
		if (persistence == null) {
			... 省略
			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) {
    			...省略
				doAfterConnectFail(resultBundle);
			}
		};
		
		if (myClient != null) {
			if (isConnecting ) {
				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);
			}
		}
		
		// if myClient is null, then create a new connection
		else {
			alarmPingSender = new AlarmPingSender(service);
			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);
	}
}

MqttConnect.connect()代码稍微有一点点多,我省略了中间一些不重要的代码。在第10行的地方会根据配置中isCleanSession来进行历史消息的清除,这里的历史消息会保存在似有目录的文件中;第16行的地方会创建一个MqttDefaultFilePersistence对象,此对象就是用来保存历史消息的;紧接着第22行的listener对象是用于连接处理连接结果的回调,它接收到成功或者失败之后会将此结果转而通知到MqttService中的本地广播,通过发送广播消息通知到MqttCall回调中,这样在最初的连接地方就可以得到连接结果的回调;剩余的逻辑就是处理连接的流程:

  • 第37行会先判断MqttAsyncClient对象是否为空,如果不为空继续判断是否正在连接、是否连接未断开等情况,如果都不满足则会走到46行的else中,最终会调用MqttAsyncClient.connect()方法;
  • 在55行会处理MqttAsyncClient为空的情况,首先就是创建MqttAsyncClient对象,然后调用它的connect()方法。

根据上面的流程就可以将逻辑转入到MqttAsyncClient.connect()方法中。

MqttAsyncClient.connect()

java 复制代码
public IMqttToken connect(MqttConnectOptions options, Object userContext, IMqttActionListener callback)
		throws MqttException, MqttSecurityException {
	final String methodName = "connect";
	...省略
	this.connOpts = options;
	this.userContext = userContext;
	final boolean automaticReconnect = options.isAutomaticReconnect();
	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 = 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);
	connectActionListener.connect();
	return userToken;
}

在上面第4行处省略了一部分代码,都是些异常场景的判断,真正注意的地方是从第8行开始,这里会createNetworkModules()方法中创建一个NetworkModule对象传入comms中,并且设置它是否可以自动重连的配置,这里的createNetworkModules()方法会根据连接URL中scheme创建对应的NetworkModule对象,还记得在第一篇连接地址长啥样么,第一篇中连接的是EMQX开放的一个URL为:tcp://broker.emqx.io:1883,它是以tcp://开头,端口为1883,根据这个规则就会创建处TCPNetworkModule对象,具体的代码见:NetworkModuleService#createInstance,跳转的地方有点多就不详细介绍了,大家可以自己看下源码,逻辑不复杂,除了TCPNetworkModule还有SSLNetworkModule等,SSLNetworkModule就是TSL认证会创建的对象。

接着后面的第13行说,第13行的地方会创建一个ConnectActionListener对象,此对象一是转发MQTT连接逻辑,二是处理连接结果的回调,它会第一时间监听到回调然后转发出来,方法最后的第22行comms.setNetworkModuleIndex(0)将NetworkModule的下标设置为0,需要留意一下此处后面会从此下标取出NetworkModule对象,第23行就直接调用了ConnectActionListener.connect()方法。

ConnectActionListener.connect()

scss 复制代码
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);
  }
}

此方法的逻辑还是比较简单的,主要就是两点,一是在第5行地方初始化持久存储,说明了就是根据clientId和serverURI创建文件;二是在第14行将连接逻辑转而又交给comms去处理。

ClientComms.connect()

ini 复制代码
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;
            MqttConnect connect = new MqttConnect(client.getClientId(),
                    conOptions.getMqttVersion(),
                    conOptions.isCleanSession(),
                    conOptions.getKeepAliveInterval(),
                    conOptions.getUserName(),
                    conOptions.getPassword(),
                    conOptions.getWillMessage(),
                    conOptions.getWillDestination());
            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 {
			... 省略掉处理异常的代码
		}
	}
}

在第3行处进行了加锁处理,防止多线程下重复执行连接操作,然后在第9行处创建MqttConnect对象,并将ConnectOption的配置传入到MqttConnect对象中,重点在第21行创建了一个ConnectBG对象,然后调用它的start()方法,ConnectBG是一个Runnable对象,它的构造方法的最后一个参数为线程池对象,在它的start()方法中也是调用了线程池的execute(this)来启动一个子线程执行逻辑,其中this对象就是ConnectBG,也就是调用了ConnectBG.run()方法。

ConnectBG.run()

scss 复制代码
public void run() {
	try {
		NetworkModule networkModule = networkModules[networkModuleIndex];
		networkModule.start();
		receiver = new CommsReceiver(clientComms, clientState, tokenStore, networkModule.getInputStream());
		receiver.start("MQTT Rec: "+getClient().getClientId(), executorService);
		sender = new CommsSender(clientComms, clientState, tokenStore, networkModule.getOutputStream());
		sender.start("MQTT Snd: "+getClient().getClientId(), executorService);
		callback.start("MQTT Call: "+getClient().getClientId(), executorService);
		internalSend(conPacket, conToken);
	}
}

精简后的代码如上,首先会从networkModules数组中获取到NetworkModule对象,networkModuleIndex就是MqttAsyncClient.connect()中传入的0,根据0下标会获取到TCPNetworkModule对象,然后调用TCPNetworkModule.start()方法执行最终的连接逻辑,下面的CommsReceiver和CommsSender一个是用来接收消息,另一个是用来处理发送消息,它们都是Runnable对象,都会在子线程中循环执行,大家这里了解到就行,最后我们进入TCPNetworkModule.start()方法看看最终的连接是如何进行的。

终点 TCPNetworkModule.start()

ini 复制代码
public void start() throws IOException, MqttException {
	final String methodName = "start";
	try {
		// @TRACE 252=connect to host {0} port {1} timeout {2}
		log.fine(CLASS_NAME,methodName, "252", new Object[] {host, Integer.valueOf(port), Long.valueOf(conTimeout*1000)});
		SocketAddress sockaddr = new InetSocketAddress(host, port);
		socket = factory.createSocket();
		socket.connect(sockaddr, conTimeout*1000);
		socket.setSoTimeout(1000);
	}
	catch (ConnectException ex) {
		//@TRACE 250=Failed to create TCP socket
		log.fine(CLASS_NAME,methodName,"250",null,ex);
		throw new MqttException(MqttException.REASON_CODE_SERVER_CONNECT_ERROR, ex);
	}
}

最终的连接逻辑并不复杂,通过主机名host和端口号port创建了InetSocketAddress对象,然后通过工厂模式创建套接字Socket对象,接触到Socket是不是就恍然大悟了,原来MQTT也是通过Java的套接字来进行通信的,Socket底层采用的就是TCP/IP协议来进行数据传输。

到这为止整个MQTT连接流程就梳理完全了,为了巩固下整体流程,还是需要通过流程图来展示出来,这样可以加深下记忆。

在上面的流程中每一步的右边都添加了此步骤的主要作用和职责,方便大家更清晰的了解。

到此为止MQTT相关知识就介绍完了,如果你在阅读的过程中觉得又不正确或者不理解的地方,欢迎大家在评论区一起沟通,谢谢~

MQTT系列文章:

站在Android开发者的角度认识MQTT - 使用篇

站在Android开发者的角度认识MQTT - TSL认证篇

站在Android开发者的角度认识MQTT - 源码篇

关于我

我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆~

相关推荐
拭心5 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王7 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡7 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道8 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库8 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道9 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe9 小时前
Android Hook - 动态加载so库
android
居居飒10 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He13 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗13 小时前
Android笔试面试题AI答之Android基础(1)
android