解读Paho MQTT源码

这两天要重点突破一下MQTT的东西, 找到了它的源码,解读一下,作为下一步优化的路标。
Paho是基于socket开做的,本质上还是维持一个长socket。

以TCP socket为例:(org.eclipse.paho.client.mqttv3.internal.TCPNetworkModule)
发起连接
SocketAddress sockaddr = new InetSocketAddress(host, port);
socket = factory.createSocket();
socket.connect(sockaddr, conTimeout*1000);
//传出 接收消息
socket.getInputStream();
//传入 心跳、发布消息
socket.getOutputStream();
中断连接
if (socket != null) {
    socket.close();
}
这么简单的代码怎么搭起一个信息管理中心呢?

那怎么知道socket什么时候断呢?这时候就不得不提到一个接口:
public interface MqttCallback {
    public void connectionLost(Throwable cause);
    public void messageArrived(String topic, MqttMessage message) throws Exception;
    public void deliveryComplete(IMqttDeliveryToken token);
}
“connectionLost” 
好在connectionLost 只有一个入口:org.eclipse.paho.client.mqttv3.internal.CommsCallback.connectionLost
这个入口也只有一次调用:org.eclipse.paho.client.mqttv3.internal.CommsCallback.shutdownConnection

找了一下调用,发现就是在“每个消息接受/发出”时(即getInputStream、getOutputStream 读写处),检查Exception,有Exception就提示客户端中断连接。
由这样的设计可以知道,这个函数connectionLost 的最大误差在一个“心跳周期”。
(当然可以通过各种 “优化手段” 去优化,不过最恶劣的情况就是这样了。)

基于getInputStream、getOutputStream,Paho封装了两个方法:CommsReceiver 、CommsSender。很直观的名字,里面也很大方的开了两条线程,以CommsSender为例子:
线程在这里开启:(发现这个库很多地方都是这样开线程)
public void start(String threadName) {
    synchronized (lifecycle) {
        if (!running) {
            running = true;
            sendThread = new Thread(this, threadName);
            sendThread.start();
        }
    }
}
然后这样做:
out = new MqttOutputStream(clientState, OutputStream );

public void run() {
    while (running && (out != null)) {
        message = clientState.get();

        out.write(message);
        out.flush();
    }
}
简单的说 就是不断的从clientState里面拿信息,往socket里面送。其实也不是“不断”,在clientState.get()里面有锁,一个消息解一次锁,解开了就发一次,不然就在clientState.get死循环等 锁,那怎么等呢?
protected MqttWireMessage get() throws MqttException {
    synchronized (queueLock) {
        while (result == null) {
            queueLock.wait();

            result = (MqttWireMessage)pendingMessages.elementAt(0);
        }
    }
}
看到queueLock.wait();就是在这里 等,pendingMessages就是消息队列。要找什么时候发消息?就找什么时候这个锁被解开了。

写了这个多,其实也就想问一个简单的问题,怎么发起一次心跳?
最直接就找名字,找到了这个接口 org.eclipse.paho.client.mqttv3.MqttPingSender。
这个接口在库里面只看到一个实现org.eclipse.paho.client.mqttv3.TimerPingSender。
注意到里面有这样的代码:
public void schedule(long delayInMilliseconds) {
    timer.schedule(new PingTask(), delayInMilliseconds);       
}
private class PingTask extends TimerTask {
    public void run() {
        comms.checkForActivity();           
    }
}
checkForActivity进去,又包了一层,
public MqttToken checkForActivity(){
    MqttToken token = null;
    token = clientState.checkForActivity();
    return token;
}
再进去:
public MqttToken checkForActivity() throws MqttException {
    ...
    pingSender.schedule(nextPingTime);
    return token;
}
看到pingSender.schedule,这里定时,准备做下一次心跳。
这个是维持心跳的方法。

直到后面我看了Paho给的android的例子,才知道原来MqttPingSender这样做是为了易于扩展。
org.eclipse.paho.android.service.AlarmPingSender
@Override
public void schedule(long delayInMilliseconds) {
    long nextAlarmInMilliseconds = System.currentTimeMillis()
            + delayInMilliseconds;
    AlarmManager alarmManager = (AlarmManager) service
            .getSystemService(Service.ALARM_SERVICE);
    alarmManager.set(AlarmManager.RTC_WAKEUP, nextAlarmInMilliseconds,
            pendingIntent);
}
class AlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        ...
    }
}
在android里面,休眠后Timer无效,只能用这种迂回的方式去定时,也是因为有这种迂回的方式,Paho库才把发送Ping的方法收得这么深。
也是因为这么深,很多之前用IBM的MQTT库的人,比如我,才不得不去看看具体的代码怎么跑。其实上面还有一个问题,就是AlarmManager定时在小米系统上无效,被定义为300S对齐唤醒,代码中貌似没有看到对“这种行为”的处理。
有一个小问题,不知道是不是设计上的遗漏,org.eclipse.paho.client.mqttv3.internal.ClientComms,ClientComms是internal的方法,居然可以在外面使用。

回到问题原点,怎么发起一次心跳?在上面的分析中,看到checkForActivity好像有点痕迹,checkForActivity的注释里面也有写明。(Check and send a ping if needed and check for ping timeout)简化那一堆代码,简单的就是这样做:

public MqttToken checkForActivity() throws MqttException {
   
    pendingFlows.insertElementAt(pingCommand, 0);
    notifyQueueLock(); //Wake sender thread since it may be in wait state (in ClientState.get()) 这里解锁

    return token;
}

心跳包怎么做呢?
//org.eclipse.paho.client.mqttv3.internal.CommsSender
public void run() {
    MqttWireMessage message = clientState.get();
    out.write(message);
    out.flush();
}
//MqttOutputStream
public void write(MqttWireMessage message) throws IOException, MqttException {
    final String methodName = "write";
    byte[] bytes = message.getHeader();
    byte[] pl = message.getPayload();
    out.write(bytes,0,bytes.length);
    clientState.notifySentBytes(bytes.length);

    int offset = 0;
    int chunckSize = 1024;
    while (offset < pl.length) {
        int length = Math.min(chunckSize, pl.length - offset);
        out.write(pl, offset, length);
        offset += chunckSize;
        clientState.notifySentBytes(length);
    }       
}
//MqttWireMessage
public byte[] getHeader() throws MqttException {
    try {
        int first = ((getType() & 0x0f) << 4) ^ (getMessageInfo() & 0x0f);
        byte[] varHeader = getVariableHeader();
        int remLen = varHeader.length + getPayload().length;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        dos.writeByte(first);
        dos.write(encodeMBI(remLen));
        dos.write(varHeader);
        dos.flush();
        return baos.toByteArray();
    } catch(IOException ioe) {
        throw new MqttException(ioe);
    }
}
//MqttPingReq
public class MqttPingReq extends MqttWireMessage {
    protected byte[] getVariableHeader() throws MqttException {
        return new byte[0];
    }
}
皮很厚,MqttWireMessage getHeader中,一共有两个字节(不知道有没有数错...)
第一部分有Type和Message组成,ping的type是12,信息长度是0,记作first(头部),两个字节,回头看看协议:
bit
7 6 5 4 3 2 1 0
byte 1 Message Type DUP flag QoS level RETAIN
byte 2
Remaining Length
...http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html

怎么收到消息?
public void run() {
    final String methodName = "run";
    MqttToken token = null;

    while (running && (in != null)) {
        try {
            receiving = in.available() > 0;
            MqttWireMessage message = in.readMqttWireMessage();
            receiving = false;

            if (message instanceof MqttAck) {
                token = tokenStore.getToken(message);
                if (token!=null) {
                    synchronized (token) {
                        clientState.notifyReceivedAck((MqttAck)message);
                    }
                } else {
                    throw new MqttException(MqttException.REASON_CODE_UNEXPECTED_ERROR);
                }
            } else {
                clientState.notifyReceivedMsg(message);
            }
        }finally {
            receiving = false;
        }
    }
}
意思大概就是不断的循环读InputStream的数据流。这里有一个问题,休眠了,整个android世界是停止工作了。线程什么的都会被挂起。
问题是“都被挂起了,循环读还有作用吗?”记得之前有一篇文章:http://my.oschina.net/u/1999248/blog/591440
摘要就是:通讯协议栈运行于BP,一旦收到数据包,BP会将AP唤醒,唤醒的时间足够AP执行代码完成对收到的数据包的处理过程。
就是有数据包来的时候,BP会唤醒CPU,CPU起来干活之后就接着循环读,就读到推送送过来的新鲜的消息了。BP耗电低于AP的1/10,类似收到短信、电话都是由BP进行监控。

读了一轮,有几点收获:
1、重新认识了socket。
2、对MQTT PING的认知多了一点。
3、维持一条线程的开关,以及Paho的封装方式都挺有意思的。
4、通过源码,理解的MQTT的部分性能底线。例如,以前觉得MQTT lostconnect应该是准的,看完代码才知道原来还有一个心跳周期的误差,不是设计没做好,而是“设计+实际情况”使然。


转载请保留本文地址-http://blog.csdn.net/yeshennet/article/details/50708115

你可能感兴趣的:(android,mqtt)