站在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,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆~

相关推荐
Kapaseker8 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton11 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
ji_shuke12 小时前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday042614 小时前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理15 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台15 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐15 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极15 小时前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan15 小时前
setHintTextColor不生效
android
洞窝技术18 小时前
从0到30+:智能家居配网协议融合的实战与思考
android