Eclipse paho mqtt心跳机制

  • MqttPingSender
  • 启动心跳
  • ping消息生产


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的实现和源码中的使用情况。

java MQTT心跳设置 mqtt 心跳机制_java MQTT心跳设置

/**
 * 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))时间后再检查