解读Paho MQTT源码

  • 摘要:这个两天要重点突破一下MQTT的东西,找到了它的源码,解读一下,作为下一步优化的路标。Paho是基于socket开做的,本质上还是维持一个长socket。以TCPsocket为例:(org.eclipse.paho.client.mqttv3.internal.TCPNetworkModule)发起连接SocketAddresssockaddr=newInetSocketAddress(host,port);socket=factory.createSocket();sock
  • 这个两天要重点突破一下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]; 


    皮很厚,MqttWireMessagegetHeader中,一共有两个字节(不知道有没有数错...) 
    第一部分有Type和Message组成,ping的type是12,信息长度是0,记作first(头部),两个字节,回头看看协议: 
    bit 
















    byte 1 
    Message Type 
    DUP flag 
    QoS level 
    RETAINbyte 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进行监控。 
    (AlarmManager唤醒后面会越来越严格,什么“对齐唤醒”都会慢慢加进来,如果能直接用BP来唤醒手机,手机回应。那就不用用一堆“wakelock”、“AlarmManager”。不过MQTT的后台本来就薄弱,如果主动发心跳,估计并发客户端数得减一半。说句不好听的,你一个Android工程师,这么弱,舔着脸皮去叫后台来帮你写代码,陪你“连坐”,你乐意人家还不乐意呢) 读了一轮,有几点收获: 
    1、重新认识了socket。 
    2、对MQTT PING的认知多了一点。 
    3、维持一条线程的开关,以及Paho的封装方式都挺有意思的。 
    4、通过源码,理解的MQTT的部分性能底线。例如,以前觉得MQTT lostconnect应该是准的,看完代码才知道原来还有一个心跳周期的误差,不是设计没做好,而是“设计+实际情况”使然。
  • 以上是解读Paho MQTT源码的内容,更多 源码 解读 paho MQTT 的内容,请您使用右上方搜索功能获取相关信息。
  • 上一篇 1 2 3 4 下一篇

猜你喜欢

转载自blog.csdn.net/xswy1/article/details/81742346
今日推荐