Smack网络层分析

概述

Smack是一个开源的实现了XMPP协议的库,特别是4.1.0版本以后,直接支持Android系统,无需再使用以前那个专门针对Android系统的aSmack移植库了.虽然在移动端上,用XMPP协议来做IM并不是一个最优选择,市面上这些大公司基本都是用自己定制的私有协议,没有采用XMPP协议的,不过我们可以抛开协议层面,只分析一下Smack库在网络层的实现,也是有借鉴意义的。

总体结构

network.png

Smack抽象出一个XMPPConnection的概念,要想收发消息,首先得建立这个connection,而且这种connection是可以由多个实例的。XMPPConnection只是一个接口,AbstractXMPPConnection实现了这个接口并加入了login,connect,processStanza等方法。AbstractXMPPConnection有两个实现类,XMPPBOSHConnection和XMPPTCPConnection。其中XMPPBOSHConnection是基于Http协议来实现的,而XMPPTCPConnection是直接用Socket来实现的长连接通信,本文分析的也就是XMPPTCPConnection。一个简单的使用实例如下:

XMPPTCPConnection con = new XMPPTCPConnection("igniterealtime.org");
  // Connect to the server
  con.connect();
  // Most servers require you to login before performing other tasks.
  con.login("jsmith", "mypass");
  // Start a new conversation with John Doe and send him a message.
  Chat chat = ChatManager.getInstanceFor(con).createChat("jdoe@igniterealtime.org", new MessageListener() {
      public void processMessage(Chat chat, Message message) {
          // Print out any messages we get back to standard out.
          System.out.println("Received message: " + message);
      }
  });
  chat.sendMessage("Howdy!");
  // Disconnect from the server
  con.disconnect();

接口介绍

XMPPConnection这个接口里有几个主要的方法 :

public void sendStanza(Stanza stanza) throws NotConnectedException, InterruptedException;
public void addConnectionListener(ConnectionListener connectionListener);
public void addPacketInterceptor(StanzaListener packetInterceptor, StanzaFilter packetFilter);
public void addPacketSendingListener(StanzaListener packetListener, StanzaFilter packetFilter);
public PacketCollector createPacketCollector(StanzaFilter packetFilter);
public void addAsyncStanzaListener(StanzaListener packetListener, StanzaFilter packetFilter);
public void addSyncStanzaListener(StanzaListener packetListener, StanzaFilter packetFilter);
  • sendStanza 发送包到服务器。在最新版的Smack中,Stanza就是以前版本中的Packet

  • addConnectionListener 添加ConnectionListener到XMPPConnection中。在该Listener中,监听者可以得到连接是否成功建立,连接关闭,连接异常关闭,重连是否成功等事件

  • addPacketInterceptor 向Connection中注册拦截器StanzaListener,所有发往服务器的包都会先过一遍拦截器,你可以在拦截器中对这些包进行处理;StanzaFilter过滤器可以允许你定制哪些包才需要拦截; StanzaListener和StanzaFilter常常配对使用,代码中有各种wrapper类(如ListenerWrapper、InterceptorWrapper等),就是把这两个接口组合在一个类中,一个负责过滤包,一个负责实际处理包

  • addPacketSendingListener 注册一个Listener,当把包通过Socket写出去后,会回调这个Listener告知正在发送状态

  • createPacketCollector 当你想接收某种类型的包时,可以新建一个包收集器。和StanzaListener不同,包收集器是阻塞式的,直到指定的包收到或者出现超时(我们可以设置等待一个包的最大时间)等异常

PacketCollector messageCollector = connection.createPacketCollector(messageFilter);
        try {
            connection.createPacketCollectorAndSend(request).nextResultOrThrow();
            // Collect the received offline messages
            Message message = messageCollector.nextResult();
            while (message != null) {
                messages.add(message);
                message = messageCollector.nextResult();
            }
        }
        finally {
            // Stop queuing offline messages
            messageCollector.cancel();
        }
        return messages;
  • addAsyncStanzaListener和addSyncStanzaListener 添加处理收到的包的回调接口;其中一个叫同步一个叫异步区别在于,执行回调方法所用的线程池不一样,其中异步用的是Executors.newCachedThreadPool,而同步用的是一个Executors.newSingleThreadExecutor,可以保证执行顺序
// First handle the async recv listeners. Note that this code is very similar to what follows a few lines below,
        // the only difference is that asyncRecvListeners is used here and that the packet listeners are started in
        // their own thread.
        final Collection<StanzaListener> listenersToNotify = new LinkedList<StanzaListener>();
        synchronized (asyncRecvListeners) {
            for (ListenerWrapper listenerWrapper : asyncRecvListeners.values()) {
                if (listenerWrapper.filterMatches(packet)) {
                    listenersToNotify.add(listenerWrapper.getListener());
                }
            }
        }

        for (final StanzaListener listener : listenersToNotify) {
            asyncGo(new Runnable() {
                @Override
                public void run() {
                    try {
                        listener.processPacket(packet);
                    } catch (Exception e) {
                        LOGGER.log(Level.SEVERE, "Exception in async packet listener", e);
                    }
                }
            });
        }

        // Loop through all collectors and notify the appropriate ones.
        for (PacketCollector collector: collectors) {
            collector.processPacket(packet);
        }

        // Notify the receive listeners interested in the packet
        listenersToNotify.clear();
        synchronized (syncRecvListeners) {
            for (ListenerWrapper listenerWrapper : syncRecvListeners.values()) {
                if (listenerWrapper.filterMatches(packet)) {
                    listenersToNotify.add(listenerWrapper.getListener());
                }
            }
        }

        // Decouple incoming stanza processing from listener invocation. Unlike async listeners, this uses a single
        // threaded executor service and therefore keeps the order.
        singleThreadedExecutorService.execute(new Runnable() {
            @Override
            public void run() {
                for (StanzaListener listener : listenersToNotify) {
                    try {
                        listener.processPacket(packet);
                    } catch(NotConnectedException e) {
                        LOGGER.log(Level.WARNING, "Got not connected exception, aborting", e);
                        break;
                    } catch (Exception e) {
                        LOGGER.log(Level.SEVERE, "Exception in packet listener", e);
                    }
                }
            }
        });

AbstractXMPPConnection实现了XMPPConnection接口,各种Listener的注册和回调就是在这个类里完成的,但如login,connect,shutdown等方法的具体实现是位于其子类中的。

连接过程

真正执行连接动作的是XMPPTCPConnection中connectInternal的方法

protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException {
        closingStreamReceived.init();
        // Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if
        // there is an error establishing the connection
        connectUsingConfiguration();

        // We connected successfully to the servers TCP port
        initConnection();

        // Wait with SASL auth until the SASL mechanisms have been received
        saslFeatureReceived.checkIfSuccessOrWaitOrThrow();

        // Make note of the fact that we're now connected.
        connected = true;
        callConnectionConnectedListener();
    }

connectUsingConfiguration方法中,用配置类XMPPTCPConnectionConfiguration提供的hostAddress,timeout等数据创建一个Socket连接出来。随后进行了一些初始化,例如初始化reader,writer变量:

private void initReaderAndWriter() throws IOException {
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();
        if (compressionHandler != null) {
            is = compressionHandler.getInputStream(is);
            os = compressionHandler.getOutputStream(os);
        }
        // OutputStreamWriter is already buffered, no need to wrap it into a BufferedWriter
        writer = new OutputStreamWriter(os, "UTF-8");
        reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));

        // If debugging is enabled, we open a window and write out all network traffic.
        initDebugger();
    }

PacketWriter对包的发送进行了封装,该类里维护一个BlockingQueue,所有要发送的包都先插入到这个队列中,同时起一个线程不停消费这个队列,最终是通过writer把数据写往服务器

                        while (!queue.isEmpty()) {
                            Element packet = queue.remove();
                            writer.write(packet.toXML().toString());
                        }
                        writer.flush();

而PacketReader则是对包的读取和解析进行了封装,类里面有个XmlPullParser,通过reader进行了初始化

 packetReader.parser = PacketParserUtils.newXmppParser(reader);

然后起了一个线程不停进行包的解析

            Async.go(new Runnable() {
                public void run() {
                    parsePackets();
                }
            }, "Smack Packet Reader (" + getConnectionCounter() + ")");
         }

解析出来的包回调到AbstractXMPPConnection类中的parseAndProcessStanza方法,最终调用各种已注册好的StanzaListener、PacketCollector来处理

XMPPConnectionRegistry

这个静态类中有个ConnectionCreationListener的集合

private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
            new CopyOnWriteArraySet<ConnectionCreationListener>();

当XMPPConnection初始化的时候,会通知给各个Listener

protected AbstractXMPPConnection(ConnectionConfiguration configuration) {
        saslAuthentication = new SASLAuthentication(this, configuration);
        config = configuration;
        // Notify listeners that a new connection has been established
        for (ConnectionCreationListener listener : XMPPConnectionRegistry.getConnectionCreationListeners()) {
            listener.connectionCreated(this);
        }
    }

像ReconnectionManager,PingManager等策略管理类,会在静态代码块中直接注册ConnectionCreationListener

static {
        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
            public void connectionCreated(XMPPConnection connection) {
                if (connection instanceof AbstractXMPPConnection) {
                    ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection);
                }
            }
        });
    }

ReconnectionManager

由于可以创建多个XMPPConnection的实例,ReconnectionManager的实例也有多个,和XMPPConnection一一对应,实际上ReconnectionManager持有了XMPPConnection的弱引用,用于进行与Connection相关的操作。

类里面还定义了不同的重连策略ReconnectionPolicy,有按固定频率重连的,也有按随机间隔重连的,

private int timeDelay() {
                attempts++;

                // Delay variable to be assigned
                int delay;
                switch (reconnectionPolicy) {
                case FIXED_DELAY:
                    delay = fixedDelay;
                    break;
                case RANDOM_INCREASING_DELAY:
                    if (attempts > 13) {
                        delay = randomBase * 6 * 5; // between 2.5 and 7.5 minutes (~5 minutes)
                    }
                    else if (attempts > 7) {
                        delay = randomBase * 6; // between 30 and 90 seconds (~1 minutes)
                    }
                    else {
                        delay = randomBase; // 10 seconds
                    }
                    break;
                default:
                    throw new AssertionError("Unknown reconnection policy " + reconnectionPolicy);
                }

                return delay;
            }

ReconnectionManager向XMPPConnection注册了ConnectionListener,当XMPPConnection中发生连接异常时,如PacketWriter、PacketReader读写包异常时,会通过ConnectionListener中的connectionClosedOnError方法,通知ReconnectionManager进行重连重试。

PingManager、ServerPingWithAlarmManager

PingManager实现了协议规定的定时发送Ping消息到服务器的策略,默认是30分钟的间隔。ServerPingWithAlarmManager是针对Android平台的实现,用AlarmManager来实现的定时策略,在代码里写死是30分钟的频率,这在移动端肯定是不适用的,另外也没看到针对各种网络环境的处理,看来为保证长连接的稳定性,需要开发者自己再去实现一些心跳和重连策略

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,573评论 18 139
  • 一、Smack库概述     Smack是一个开源、易用的XMPP/Jabber客户端库,它使用Java语言开发,...
    AndryYu阅读 5,999评论 2 13
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,165评论 11 349
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,550评论 18 399
  • 换手机
    10e45c2d55b2阅读 293评论 0 1