【XMPP】Smack原始碼之訊息接收與解析

Leo.cheng發表於2014-02-22

XmpPullParser

  鑑於xmpp協議都是以xml格式來傳輸,因此原始碼中解析協議都是用到XmpPullParser來解析xml

  XmpPullParser很簡單,先簡單介紹幾個比較常用的方法

//定義一個事件採用回撥方式,直到xml完畢
public int getEventType() throws XmlPullParserException ;
//遍歷下一個事件,返回一個事件型別
public int next() throws XmlPullParserException, IOException
//得到當前tag的名字
public String getName();
//獲得當前文字
public String getText();
//當前tag下的屬性數量
public int getAttributeCount() ;
//獲得當前指定屬性位置的名稱
public String getAttributeName(int index);
//獲得當前指定屬性位置的值
public String getAttributeValue(int index);
View Code

  為了更好的理解後面的原始碼,加一段程式碼來分析:

public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
   MUCUser mucUser = new MUCUser();
   boolean done = false;
   while (!done) {
       int eventType = parser.next();
       if (eventType == XmlPullParser.START_TAG) {
           if (parser.getName().equals("invite")) {
               mucUser.setInvite(parseInvite(parser));
           }
           if (parser.getName().equals("item")) {
               mucUser.setItem(parseItem(parser));
           }
           if (parser.getName().equals("password")) {
               mucUser.setPassword(parser.nextText());
           }
           if (parser.getName().equals("status")) {
               mucUser.setStatus(new MUCUser.Status(parser.getAttributeValue("", "code")));
           }
           if (parser.getName().equals("decline")) {
               mucUser.setDecline(parseDecline(parser));
           }
           if (parser.getName().equals("destroy")) {
               mucUser.setDestroy(parseDestroy(parser));
           }
       }
       else if (eventType == XmlPullParser.END_TAG) {
           if (parser.getName().equals("x")) {
               done = true;
           }
       }
   }

   return mucUser;
}
View Code

  裡面的物件先不用理它,只需看他是如何分析這段xml的:

  //協議解釋,從123456789傳送一段協議給12345678這個使用者,邀請使用者123456789進入房間,理由hi join us。

<message id="WEzG6-11" to="123456789@xxx-pc/Smack" from="12345678@xxx-pc/Smack" type="get">
<x xmlns="http://jabber.org/protocol/muc#user">
<invite to="123456789@xxx-pc">
<reason>hi join us</reason>
</invite>
</x>
</message>
View Code

  parser.next();

  1. 獲得第一個事件,判斷是否開始標籤(XmlPullParser.START_TAG)
  2. 然後再裡面判斷每個標籤的名字
  3. 處理完後判斷結尾標籤(XmlPullParser.END_TAG)是否需要結束本次迴圈。

  //取xmlpullparse物件

  1. XmlPullParserFactory factory = XmlPullParserFactory.newInstance();  
  2. XmlPullParser parser = factory.newPullParser(); 

  //設定關聯資料來源

  parser.setInput(reader);


接收訊息以及如何解析訊息

  1. 在android裡面用的smack包其實叫做asmack,該包提供了兩種不同的連線方式:socket和httpclient。
  2. 該包並且提供了很多操作xmpp協議的API,也方便各種不同自定義協議的擴充套件。
  3. 我們不需要自己重新去定義一套接收機制來擴充套件新的協議,只需繼承然後在類裡處理自己的協議就可以了。

總的思路

  1. 使用socket連線伺服器
  2. 將XmlPullParser的資料來源關聯到socket的InputStream
  3. 啟動執行緒不斷迴圈處理訊息
  4. 將接收到的訊息解析xml處理封裝好成一個Packet包
  5. 將包廣播給所有註冊事件監聽的類

逐步擊破

  先理解一下smack的使用,看下smack類圖

  下圖只顯示解釋所要用到的類和方法,減縮了一些跟本文主題無關的程式碼,只留一條貫穿著從建立連線到接收訊息的線。

  

  1.解析這塊東西打算從最初的呼叫開始作為入口,抽絲剝繭,逐步揭開。

PacketListener packetListener = new PacketListener() {
    @Override
    public void processPacket(Packet packet) {
        System.out.println("Activity----processPacket" + packet.toXML());
    }
};

PacketFilter packetFilter = new PacketFilter() {

    @Override
    public boolean accept(Packet packet) {
        System.out.println("Activity----accept" + packet.toXML());
        return true;
    }
};
View Code

  解釋:建立包的監聽以及包的過濾,當有訊息到時就會廣播到所有註冊的監聽,當然前提是要通過packetFilter的過濾。

  2.在這建構函式裡面主要配置ip地址和埠

super(new ConnectionConfiguration("169.254.141.109", 9991));
View Code

  3.註冊監聽,開始初始化連線。

connection.addPacketListener(packetListener, packetFilter); 

connection.connect();
View Code

  4.通過之前設定的ip和埠,建立socket物件

public void connect() {
        // Stablishes the connection, readers and writers
        connectUsingConfiguration(config);
}

private void connectUsingConfiguration(ConnectionConfiguration config) {
    String host = config.getHost();
    int port = config.getPort();
    try {
        this.socket = new Socket(host, port);
    } catch (UnknownHostException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    initConnection();
}
View Code

  5.建立reader和writer的物件關聯到socket的InputStream

private void initReaderAndWriter() {
    try {
        reader = new BufferedReader(new InputStreamReader(socket
                .getInputStream(), "UTF-8"));
    } catch (UnsupportedEncodingException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    initDebugger();
}
View Code

  6.例項化ConsoleDebugger,該類主要是列印出接收到的訊息,給reader設定了一個訊息的監聽

protected void initDebugger() {
    Class<?> debuggerClass = null;
    try {
        debuggerClass = Class.forName("com.simualteSmack.ConsoleDebugger");

        Constructor<?> constructor = debuggerClass.getConstructor(
                Connection.class, Writer.class, Reader.class);
        debugger = (SmackDebugger) constructor.newInstance(this, writer,
                reader);
        reader = debugger.getReader();
    } catch (ClassNotFoundException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
    } catch (Exception e) {
        throw new IllegalArgumentException(
                "Can't initialize the configured debugger!", e);
    }
}
View Code

  7.接著建立PacketReader物件,並啟動。PacketReader主要負責訊息的處理和通知

private void initConnection() {
    // Set the reader and writer instance variables
    initReaderAndWriter();

    packetReader = new PacketReader(this);

    addPacketListener(debugger.getReaderListener(), null);
    // Start the packet reader. The startup() method will block until we
    // get an opening stream packet back from server.
    packetReader.startup();
}
View Code

  看看PacketReader

public class PacketReader {
    private ExecutorService listenerExecutor;
    private boolean done;
    private XMPPConnection connection;
    private XmlPullParser parser;
    private Thread readerThread;

    protected PacketReader(final XMPPConnection connection) {
        this.connection = connection;
        this.init();
    }

    /**
     * Initializes the reader in order to be used. The reader is initialized
     * during the first connection and when reconnecting due to an abruptly
     * disconnection.
     */
    protected void init() {
        done = false;

        readerThread = new Thread() {
            public void run() {
                parsePackets(this);
            }
        };

        readerThread.setName("Smack Packet Reader ");
        readerThread.setDaemon(true);

        // create an executor to deliver incoming packets to listeners.
        // we will use a single thread with an unbounded queue.
        listenerExecutor = Executors
                .newSingleThreadExecutor(new ThreadFactory() {

                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r,
                                "smack listener processor");
                        thread.setDaemon(true);
                        return thread;
                    }
                });
        resetParser();
    }

    /**
     * Starts the packet reader thread and returns once a connection to the
     * server has been established. A connection will be attempted for a maximum
     * of five seconds. An XMPPException will be thrown if the connection fails.
     * 
     */
    public void startup() {
        readerThread.start();
    }

    /**
     * Shuts the packet reader down.
     */
    public void shutdown() {
        done = true;
        // Shut down the listener executor.
        listenerExecutor.shutdown();
    }

    private void resetParser() {
        try {
            parser = XmlPullParserFactory.newInstance().newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
            parser.setInput(connection.reader);
        } catch (XmlPullParserException xppe) {
            xppe.printStackTrace();
        }
    }

    /**
     * Parse top-level packets in order to process them further.
     * 
     * @param thread
     *            the thread that is being used by the reader to parse incoming
     *            packets.
     */
    private void parsePackets(Thread thread) {
        try {
            int eventType = parser.getEventType();
            do {
                if (eventType == XmlPullParser.START_TAG) {
                    if (parser.getName().equals("message")) {
                        processPacket(PacketParserUtils.parseMessage(parser));
                    }
                    System.out.println("START_TAG");
                } else if (eventType == XmlPullParser.END_TAG) {
                    System.out.println("END_TAG");
                }
                eventType = parser.next();
            } while (!done && eventType != XmlPullParser.END_DOCUMENT
                    && thread == readerThread);
        } catch (Exception e) {
            e.printStackTrace();
            if (!done) {
            }
        }
    }

    private void processPacket(Packet packet) {
        if (packet == null) {
            return;
        }

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

        // Deliver the incoming packet to listeners.
        listenerExecutor.submit(new ListenerNotification(packet));
    }

    /**
     * A runnable to notify all listeners of a packet.
     */
    private class ListenerNotification implements Runnable {

        private Packet packet;

        public ListenerNotification(Packet packet) {
            this.packet = packet;
        }

        public void run() {
            for (ListenerWrapper listenerWrapper : connection.recvListeners
                    .values()) {
                listenerWrapper.notifyListener(packet);
            }
        }
    }

}
View Code
  1. 建立該類時就初始化執行緒和ExecutorService
  2. 接著呼叫resetParser() 方法為parser設定輸入源(這裡是重點,parser的資料都是通過這裡獲取)
  3. 呼叫startup啟動執行緒,迴圈監聽parser
  4. 如果接收到訊息根據訊息協議的不同將呼叫PacketParserUtils類裡的不同方法,這裡呼叫parseMessage()該方法主要處理message的訊息,在該方法裡分析message訊息並返回packet包。
  5. 返回的包將呼叫processPacket方法,先通知所有註冊了PacketCollector的監聽,接著訊息(listenerExecutor.submit(new ListenerNotification(packet)); )傳遞給所有註冊了PacketListener的監聽。
  6. 這樣在activity開始之前註冊的那個監聽事件就會觸發,從而完成了整個流程。

輔助包

  比如PacketCollector 這個類,它的用處主要用來處理一些需要在傳送後需要等待一個答覆這樣的請求。

protected synchronized void processPacket(Packet packet) {
    System.out.println("PacketCollector---processPacket");
    if (packet == null) {
        return;
    }
    if (packetFilter == null || packetFilter.accept(packet)) {
        while (!resultQueue.offer(packet)) {
            resultQueue.poll();
        }
    }
}
View Code

  該方法就是將獲取到的包,先過濾然後放到佇列裡,最後通過nextResult來獲取包,這樣就完成一個請求收一個答覆。

public Packet nextResult(long timeout) {
    long endTime = System.currentTimeMillis() + timeout;
    System.out.println("nextResult");
    do {
        try {
            return resultQueue.poll(timeout, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) { /* ignore */
        }
    } while (System.currentTimeMillis() < endTime);
    return null;
}
View Code

  這樣整個流程就完成了,最後總結一下,如圖

  

 


參考文章

  http://www.cnblogs.com/not-code/archive/2011/08/01/2124340.html

 

相關文章