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()方法了:
# 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对象里面的逻辑:
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的连接流程:
# 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()
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()
# 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()
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()
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()
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()
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()
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 - TLS 认证篇
关于我
我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆~