Eclipse paho mqtt心跳机制
MqttPingSender
在Eclipse paho mqtt的源码中有心跳的接口类:org.eclipse.paho.client.mqttv.MqttPingSender。此接口类的实现有两个,分别是:org.eclipse.paho.client.mqttv3.TimerPingSender和org.eclipse.paho.client.mqttv3.ScheduledExecutorPingSender,通过类名,我们就可以猜测出,两者哥实现区别。TimerPingSender是通过java的Timer类进行实现的,ScheduledExecutorPingSender是通过线程进行实现的,今天我们主要分析TimerPingSender的实现和源码中的使用情况。
/**
* Represents an object used to send ping packet to MQTT broker
* every keep alive interval.
*/
public interface MqttPingSender {
/**
* Initial method. Pass interal state of current client in.
* @param comms The core of the client, which holds the state information for pending and in-flight messages.
*/
//初始化
void init(ClientComms comms);
/**
* Start ping sender. It will be called after connection is success.
*/
//开始ping的发送,会在连接成功后调用
void start();
/**
* Stop ping sender. It is called if there is any errors or connection shutdowns.
*/
//停止ping的发送,会在出现错误或者连接关闭时调用
void stop();
/**
* Schedule next ping in certain delay.
* @param delayInMilliseconds delay in milliseconds.
*/
//下一个ping发送需要延迟多久
void schedule(long delayInMilliseconds);
}
启动心跳
我们在new MqttClinet客户端时,最终会在MqttAsyncClient类中的构成函数中默认创建TimerPingSender服务。
public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence persistence) throws MqttException {
this(serverURI, clientId, persistence, new TimerPingSender());
}
我们来看MqttPingSender接口,心跳真正的启动是在start方法中,启动过程是在连接成功后,调用此方法。我们怎么才能知道,是否成功连接到broker呢,这就设计到mqtt的协议了。我们会在后续的博文中进行接收,在这里我们主要看CommsReceiver类中的run方法。此方法主要是从io流中读取数据,并判断消息是响应数据还是消息。如果是响应数据,则调用ClientState的notifyReceivedAck方法。每种响应都有各自不同的处理逻辑。
/**
* Called by the CommsReceiver when an ack has arrived.
*
* @param ack The {@link MqttAck} that has arrived
* @throws MqttException if an exception occurs when sending / notifying
*/
protected void notifyReceivedAck(MqttAck ack) throws MqttException {
final String methodName = "notifyReceivedAck";
this.lastInboundActivity = System.nanoTime();
// @TRACE 627=received key={0} message={1}
log.fine(CLASS_NAME, methodName, "627", new Object[] {
Integer.valueOf(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[] {
Integer.valueOf(ack.getMessageId())});
//mqtt发布接收
} 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);
//mqtt发布响应
} 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
//mqtt 的平响应
} 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[]{ Integer.valueOf(pingOutstanding)});
//mqtt的连接响应,我们主要看这
} else if (ack instanceof MqttConnack) {
int rc = ((MqttConnack) ack).getReturnCode();
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();
//连接成功,并在此方法中调用MqttPingSender的start方法,在start方法中调用schedule方法,在schedule方法中发送ping消息到broker
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();
}
ping消息生产
我们通过代码一路跟着,发现ping消息的产生在ClinetState的checkForActivity方法中,具体的发送逻辑是在CommsSender类中,发送逻辑我们会在其他博客中进行讲解,在这里主要讲解ping消息的生产。
/**
* Check and send a ping if needed and check for ping timeout.
* Need to send a ping if nothing has been sent or received
* in the last keepalive interval. It is important to check for
* both sent and received packets in order to catch the case where an
* app is solely sending QoS 0 messages or receiving QoS 0 messages.
* QoS 0 message are not good enough for checking a connection is
* alive as they are one way messages.
*
* If a ping has been sent but no data has been received in the
* last keepalive interval then the connection is deamed to be broken.
* @param pingCallback The {@link IMqttActionListener} to be called
* @return token of ping command, null if no ping command has been sent.
* @throws MqttException if an exception occurs during the Ping
*/
public MqttToken checkForActivity(IMqttActionListener pingCallback) throws MqttException {
final String methodName = "checkForActivity";
//@TRACE 616=checkForActivity entered
log.fine(CLASS_NAME,methodName,"616", new Object[]{});
synchronized (quiesceLock) {
// ref bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=440698
// No ping while quiescing
if (quiescing) {
return null;
}
}
MqttToken token = null;
long nextPingTime = TimeUnit.NANOSECONDS.toMillis(this.keepAliveNanos); // milliseconds relative time
if (connected && this.keepAliveNanos > 0) {
long time = System.nanoTime();
// Below might not be necessary since move to nanoTime (Issue #278)
//Reduce schedule frequency since System.currentTimeMillis is no accurate, add a buffer
//It is 1/10 in minimum keepalive unit.
int delta = 100000;
// ref bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=446663
synchronized (pingOutstandingLock) {
// Is the broker connection lost because the broker did not reply to my ping?
if (pingOutstanding > 0 && (time - lastInboundActivity >= keepAliveNanos + delta)) {
// lastInboundActivity will be updated once receiving is done.
// Add a delta, since the timer and System.currentTimeMillis() is not accurate.
// TODO - Remove Delta, maybe?
// A ping is outstanding but no packet has been received in KA so connection is deemed broken
//@TRACE 619=Timed out as no activity, keepAlive={0} lastOutboundActivity={1} lastInboundActivity={2} time={3} lastPing={4}
log.severe(CLASS_NAME,methodName,"619", new Object[]{ Long.valueOf(this.keepAliveNanos), Long.valueOf(lastOutboundActivity), Long.valueOf(lastInboundActivity), Long.valueOf(time), Long.valueOf(lastPing)});
// A ping has already been sent. At this point, assume that the
// broker has hung and the TCP layer hasn't noticed.
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_TIMEOUT);
}
// Is the broker connection lost because I could not get any successful write for 2 keepAlive intervals?
if (pingOutstanding == 0 && (time - lastOutboundActivity >= 2* keepAliveNanos)) {
// I am probably blocked on a write operations as I should have been able to write at least a ping message
log.severe(CLASS_NAME,methodName,"642", new Object[]{ Long.valueOf(this.keepAliveNanos), Long.valueOf(lastOutboundActivity), Long.valueOf(lastInboundActivity), Long.valueOf(time), Long.valueOf(lastPing)});
// A ping has not been sent but I am not progressing on the current write operation.
// At this point, assume that the broker has hung and the TCP layer hasn't noticed.
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_WRITE_TIMEOUT);
}
// 1. Is a ping required by the client to verify whether the broker is down?
// Condition: ((pingOutstanding == 0 && (time - lastInboundActivity >= keepAlive + delta)))
// In this case only one ping is sent. If not confirmed, client will assume a lost connection to the broker.
// 2. Is a ping required by the broker to keep the client alive?
// Condition: (time - lastOutboundActivity >= keepAlive - delta)
// In this case more than one ping outstanding may be necessary.
// This would be the case when receiving a large message;
// the broker needs to keep receiving a regular ping even if the ping response are queued after the long message
// If lacking to do so, the broker will consider my connection lost and cut my socket.
if ((pingOutstanding == 0 && (time - lastInboundActivity >= keepAliveNanos - delta)) ||
(time - lastOutboundActivity >= keepAliveNanos - delta)) {
//@TRACE 620=ping needed. keepAlive={0} lastOutboundActivity={1} lastInboundActivity={2}
log.fine(CLASS_NAME,methodName,"620", new Object[]{ Long.valueOf(this.keepAliveNanos), Long.valueOf(lastOutboundActivity), Long.valueOf(lastInboundActivity)});
// pingOutstanding++; // it will be set after the ping has been written on the wire
// lastPing = time; // it will be set after the ping has been written on the wire
//生产ping的token
token = new MqttToken(clientComms.getClient().getClientId());
if(pingCallback != null){
token.setActionCallback(pingCallback);
}
//存入tokenStore中
tokenStore.saveToken(token, pingCommand);
//将pingCommand放入pendingFlows集合中
pendingFlows.insertElementAt(pingCommand, 0);
//获取下一次生产心跳消息的周期间隔
nextPingTime = getKeepAlive();
//Wake sender thread since it may be in wait state (in ClientState.get())
//将发送线程唤醒,它此时可能是等待状态
notifyQueueLock();
}
else {
//@TRACE 634=ping not needed yet. Schedule next ping.
log.fine(CLASS_NAME, methodName, "634", null);
long elapsedSinceLastActivityNanos = time - lastOutboundActivity;
long elapsedSinceLastActivityMillis = TimeUnit.NANOSECONDS.toMillis( elapsedSinceLastActivityNanos );
nextPingTime = Math.max(1, getKeepAlive() - elapsedSinceLastActivityMillis);
}
}
//@TRACE 624=Schedule next ping at {0}
log.fine(CLASS_NAME, methodName,"624", new Object[]{Long.valueOf(nextPingTime)});
//调用pingSender的schedule方法,当下次发送时间到来是,会在此调用checkForActivity方法,生产ping消息
pingSender.schedule(nextPingTime);
}:
return token;
}
以上就是ping消息生产的代码,这里需要讲解的是异常的判断。pingOutstanding表示PingResp的数据,在发送成功后,这个数据会加1,收到MqttPingResp报文后,会相应减1,但是不会小于0。lastInboundActivity表示客户端最近一次收到服务端的报文的时间,lastOutboundActivity表示客户端最近一次成功发送报文的时间。
如果连接成功,且keepAlive大于0,则会进行ping报文的发送:
异常判断:
1:发送MqttPingReq请求后,在keepAliveNanos + delta时间间隔内没有收到服务器端的响应,会抛出异常,抛出异常后客户端会断开和服务端的连接
2:如果在lastOutboundActivity >= 2* keepAliveNanos时间间隔内,客户端没有发送成功MqttPingReq,会认为客户端到broker的连接已经断开,此时会排出异常,抛出异常后客户端会断开和服务端的连接
没有抛出异常,需要计算下次发送ping的时间间隔
(1) 如果在keepAlive - delta时间间隔内未收到服务端发送的报文或者在keepAlive - delta时间间隔内未成功发送过报文,则向broker发送PINGREQ报文,否则执行(2)
(2) 条件(1)不满足表示暂时不需要发送PINGREQ报文,经过Math.max(1, getKeepAlive() - (time - lastOutboundActivity))时间后再检查