openfire的session與路由機制(三)核心流程分析

注意:源碼的研究是基於openfire_src_4_0_2源碼版本。

3.1  Session

3.1.1  生命週期

Session的生命週期可以大致分爲:預創建、創建、清除。預創建是指會話的要素基本已經建立,但沒有經過認證,部分功能特性不可用,而創建之後表明session已經被認證,擁有正常的會話功能。

3.1.1.1  預創建

以創建最常用的ClientSession爲例來說明session的生成流程(包含Connection的生成)。
我們先來關注下Connection是怎麼生成的:

在客戶端鏈接了服務端,nio層mina的session被創建後將會調用ConnectionHandler類的sessionOpened方法:

public void sessionOpened(IoSession session) throws Exception {
        // Create a new XML parser for the new connection. The parser will be used by the XMPPDecoder filter.
        final XMLLightweightParser parser = new XMLLightweightParser(StandardCharsets.UTF_8);
        session.setAttribute(XML_PARSER, parser);
        // Create a new NIOConnection for the new session
        final NIOConnection connection = createNIOConnection(session);
        session.setAttribute(CONNECTION, connection);
        session.setAttribute(HANDLER, createStanzaHandler(connection));
        // Set the max time a connection can be idle before closing it. This amount of seconds
        // is divided in two, as Openfire will ping idle clients first (at 50% of the max idle time)
        // before disconnecting them (at 100% of the max idle time). This prevents Openfire from
        // removing connections without warning.
        final int idleTime = getMaxIdleTime() / 2;
        if (idleTime > 0) {
            session.getConfig().setIdleTime(IdleStatus.READER_IDLE, idleTime);
        }
    }

Connection的實現類是NioConnection,生成了NioConnection的實例後,會被設置到StanzaHandler的屬性connection,供後續session的生成使用。
生成ClientSession的流程圖:
 
這一流程是在客戶端打開初始流的時候觸發的操作,客戶端與服務端的交互報文可參考協議:
 

我們來重點看下步驟4,調用LocalClientSession的方法(忽略了部分非核心代碼):

public static LocalClientSession createSession(String serverName, XmlPullParser xpp, Connection connection)
            throws XmlPullParserException {
                …..  check exception  …….
                ……  handle language and version ………
                …….  Indicate the TLS policy to use for this connection ………
                …….  Indicate the compression policy to use for this connection  …….
        // Create a ClientSession for this user.
        LocalClientSession session = SessionManager.getInstance().createClientSession(connection, language);

        // Build the start packet response
        StringBuilder sb = new StringBuilder(200);
        sb.append("<?xml version='1.0' encoding='");
        sb.append(CHARSET);
        sb.append("'?>");
        if (isFlashClient) {
            sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
        }
        else {
            sb.append("<stream:stream ");
        }
        sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
        sb.append(serverName);
        sb.append("\" id=\"");
        sb.append(session.getStreamID().toString());
        sb.append("\" xml:lang=\"");
        sb.append(language.toLanguageTag());
        …….
        connection.deliverRawText(sb.toString());

        // If this is a "Jabber" connection, the session is now initialized and we can
        // return to allow normal packet parsing.
        if (majorVersion == 0) {
            return session;
        }
        // Otherwise, this is at least XMPP 1.0 so we need to announce stream features.
        sb = new StringBuilder(490);
        sb.append("<stream:features>");
         ……..
        // Include available SASL Mechanisms
        sb.append(SASLAuthentication.getSASLMechanisms(session));
        // Include Stream features
        String specificFeatures = session.getAvailableStreamFeatures();
        if (specificFeatures != null) {
            sb.append(specificFeatures);
        }
        sb.append("</stream:features>");
        connection.deliverRawText(sb.toString());
        return session;
    }

在此方法中,LocalClientSession的生成後,將會返回響應流給客戶端,報文中的id字段爲通過session獲取的Stream id,這段代碼處理說明了協議報文的交互。

接下來我們深入看看SessionManager是怎麼來生成session的:

/**
     * Creates a new <tt>ClientSession</tt> with the specified streamID.
     */
    public LocalClientSession createClientSession(Connection conn, StreamID id, Locale language) {
        if (serverName == null) {
            throw new IllegalStateException("Server not initialized");
        }
        LocalClientSession session = new LocalClientSession(serverName, conn, id, language);
        conn.init(session);
        // Register to receive close notification on this session so we can
        // remove  and also send an unavailable presence if it wasn'tsent before
        conn.registerCloseListener(clientSessionListener, session);
        // Add to pre-authenticated sessions.
        localSessionManager.getPreAuthenticatedSessions().put(session.getAddress().getResource(), session);
        // Increment the counter of user sessions
        connectionsCounter.incrementAndGet();
        return session;
    }


創建LocalClientSession實例後,調用conn.init(session)將session關聯到Connection中;接着conn註冊對其關閉事件的監聽;然後添加預認證session。
LocalSessionManager.getPreAuthenticatedSessions()獲取到的是Map<String, LocalClientSession>結構的對象,用來存儲已經被創建但未被認證的session。

3.1.1.2  創建

Session從未被認證狀態變成完整可用、已認證狀態是在資源綁定時處理的:

public IQ handleIQ(IQ packet) throws UnauthorizedException {
        LocalClientSession session = (LocalClientSession) sessionManager.getSession(packet.getFrom());
        ……..
        String username = authToken.getUsername().toLowerCase();
        String clientVersion = packet.getChildElement().elementTextTrim("version");
        // If the connection was not refused due to conflict, log the user in
        session.setAuthToken(authToken, resource,clientVersion);
        ……..
        child.addElement("jid").setText(session.getAddress().toString());
        // Send the response directly since a route does not exist at this point.
        session.process(reply);
        // After the client has been informed, inform all listeners as well.
        SessionEventDispatcher.dispatchEvent(session, SessionEventDispatcher.EventType.resource_bound);
        return null;
    }

獲取了未認證的session後,進行一系列的處理之後,調用session的setAuthToken方法進行資源的綁定和session的認證:

public void setAuthToken(AuthToken auth, String resource,String clientVersion) {
    if(clientVersion != null) {
        setClientVersion(clientVersion);
    }
    setAddress(new JID(auth.getUsername(), getServerName(), resource));
    authToken = auth;
    setStatus(Session.STATUS_AUTHENTICATED);

    // Set default privacy list for this session
      setDefaultList(PrivacyListManager.getInstance().getDefaultPrivacyList(auth.getUsername()));
    // Add session to the session manager. The session will be added to the routing table as well
    sessionManager.addSession(this);
}


用一個有效的身份認證token和resource來設置session。這個方法自動地升級了session,使其狀態變爲已認證,且啓用了多種認證後才支持的特性(比如獲取managers)。最重要的操作是把session加入到了sessionManager中:

public void addSession(LocalClientSession session) {
    // Add session to the routing table (routing table will know session is not available yet)
    routingTable.addClientRoute(session.getAddress(), session);
    // Remove the pre-Authenticated session but remember to use the temporary ID as the key
    localSessionManager.getPreAuthenticatedSessions().remove(session.getStreamID().toString());
    SessionEventDispatcher.EventType event = session.getAuthToken().isAnonymous() ?
            SessionEventDispatcher.EventType.anonymous_session_created :
            SessionEventDispatcher.EventType.session_created;
    // Fire session created event.
    SessionEventDispatcher.dispatchEvent(session, event);
    if (ClusterManager.isClusteringStarted()) {
        // Track information about the session and share it with other cluster nodes
        sessionInfoCache.put(session.getAddress().toString(),new ClientSessionInfo(session));
    }
}


可以看出已認證的session會加入到路由表中,預認證session的Map會將之前預先保存的未認證session刪掉,並且會觸發相應的監聽事件。
到此,我們瞭解了session創建的完成流程


3.1.1.3  清除

當監聽到Connection關閉時,應清除掉相應的Session。在SessionManager的私有類ClientSessionListener實現了ConnectionCloseListener,能及時地監聽到Connection關閉並進行Session的清除工作:

public void onConnectionClose(Object handback) {
    try {
        LocalClientSession session = (LocalClientSession) handback;
        try {
            if ((session.getPresence().isAvailable() || !session.wasAvailable()) &&
                    routingTable.hasClientRoute(session.getAddress())) {
                // Send an unavailable presence to the user's subscribers
                // Note: This gives us a chance to send an unavailable presence to the
                // entities that the user sent directed presences
                Presence presence = new Presence();
                presence.setType(Presence.Type.unavailable);
                presence.setFrom(session.getAddress());
                router.route(presence);
            }

            session.getStreamManager().onClose(router, serverAddress);
        }
        finally {
            // Remove the session
            removeSession(session);
        }
    }
    catch (Exception e) {
        // Can't do anything about this problem...
        Log.error(LocaleUtils.getLocalizedString("admin.error.close"), e);
    }
}
具體的清理細節可深入跟蹤此類的方法removeSession(session)

3.1.2  處理報文流程

Session的骨架類LocalSession定義了一套處理流程:

    public void process(Packet packet) {
        // Check that the requested packet can be processed
        if (canProcess(packet)) {
            // Perform the actual processing of the packet. This usually implies sending
            // the packet to the entity
            try {
                // Invoke the interceptors before we send the packet
                InterceptorManager.getInstance().invokeInterceptors(packet, this, false, false);
                deliver(packet);
                // Invoke the interceptors after we have sent the packet
                InterceptorManager.getInstance().invokeInterceptors(packet, this, false, true);
            }
            catch (Exception e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        } else {
            // http://xmpp.org/extensions/xep-0016.html#protocol-error
            if (packet instanceof Message) {
      // For message stanzas, the server SHOULD return an error, which SHOULD be <service-unavailable/>.
                Message message = (Message) packet;
                Message result = message.createCopy();
                result.setTo(message.getFrom());
                result.setError(PacketError.Condition.service_unavailable);
                XMPPServer.getInstance().getRoutingTable().routePacket(message.getFrom(), result, true);
            } else if (packet instanceof IQ) {
                // For IQ stanzas of type "get" or "set", the server MUST return an error, 
//which SHOULD be <service-unavailable/>.
                // IQ stanzas of other types MUST be silently dropped by the server.
                IQ iq = (IQ) packet;
                if (iq.getType() == IQ.Type.get || iq.getType() == IQ.Type.set) {
                    IQ result = IQ.createResultIQ(iq);
                    result.setError(PacketError.Condition.service_unavailable);
                    XMPPServer.getInstance().getRoutingTable().routePacket(iq.getFrom(), result, true);
                }
            }
        }
    }


對報文的處理流程如下:
1. 驗證session的實現類是否能處理傳來的這條報文(使用抽象方法canProcess方法,每個實現類都應實現這個方法);
2. 如果可以處理這條報文,則在調用真正的處理方法deliver(packet)前後進行攔截器列表的環繞(這樣的設計思路類似於web框架的處理請求的思路,通過攔截器來擴充對報文的處理。)調用deliver(packet)會執行真正的處理邏輯,和canProcess一樣,每個實現類也都應實現這個方法;
3. 如果不能處理這條報文,則進行異常處理

從流程中可以看出是否能處理報文和對報文的處理邏輯是定義在具體的LocalSession的實現類中的,這採用了常見的模板方法設計模式:定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
模板方法模式是一種基於繼承的代碼複用技術,它是一種類行爲型模式。模板方法模式是結構最簡單的行爲型設計模式,在其結構中只存在父類與子類之間的繼承關係。通過使用模板方法模式,可以將一些複雜流程的實現步驟封裝在一系列基本方法中,在抽象父類中提供一個稱之爲模板方法的方法來定義這些基本方法的執行次序,而通過其子類來覆蓋某些步驟,從而使得相同的算法框架可以有不同的執行結果。模板方法模式提供了一個模板方法來定義算法框架,而某些具體步驟的實現可以在其子類中完成(可參考資料:http://blog.csdn.net/lovelion/article/details/8299794)


再來看看常用的實現類是怎麼實現canProcess和deliver方法的。

LocalClientSession的實現

public boolean canProcess(Packet packet) {
       PrivacyList list = getActiveList();
       if (list != null) {
           // If a privacy list is active then make sure that the packet is not blocked
           return !list.shouldBlockPacket(packet);
       }
       else {
           list = getDefaultList();
           // There is no active list so check if there exists a default list and make
           // sure that the packet is not blocked
           return list == null || !list.shouldBlockPacket(packet);
       }
}
先來看看PrivacyList類的介紹:PrivacyList類包含了一系列的規則,這些規則定義了和列表的擁有者進行通信是被允許還是被拒絕。用戶可能有0個,1個或多個privacy list。當一個列表爲默認的列表,則它將會默認地被所有用戶sesion使用或者分析。一個用戶能夠按照自己的意願是否配置一個默認的list。當沒有默認的list被定義,通信將不會被阻塞。然而,用戶能夠爲一個特定的session定義一個active list。Active list如果存在的話,在session的生存期間,將會覆蓋默認的list。

再來深入看看PrivacyList的shouldBlockPacket(Packet packet)方法:

public boolean shouldBlockPacket(Packet packet) {
    if (packet.getFrom() == null) {
        // Sender is the server so it's not denied
        return false;
    }
    // Iterate over the rules and check each rule condition
    Roster roster = getRoster();
    for (PrivacyItem item : items) {
        if (item.matchesCondition(packet, roster, userJID)) {
            if (item.isAllow()) {
                return false;
            }
            if (Log.isDebugEnabled()) {
                Log.debug("PrivacyList: Packet was blocked: " + packet);
            }
            return true;
        }
    }
    // If no rule blocked the communication then allow the packet to flow
    return false;
}
如果在privacy list規則下,報文被阻塞則返回true。通過升序排列的Privacy list的規則進行報文的驗證。

LocalSession的deliver方法的功能就是發送、轉發報文,其子類實現比較類似。且看下LocalClientSession對deliver抽象方法的實現:

public void deliver(Packet packet) throws UnauthorizedException {
       conn.deliver(packet);
       streamManager.sentStanza(packet);
}

3.2  路由

請求報文的路由及處理流程如下所示:
 

在此我們不詳細描述openfire的mina層是如何接受消息並且流轉到StanzaHandler中進行處理的過程(具體可參考:http://hbiao68.iteye.com/blog/2028893)。

因爲手頭上的openfire項目對Message、presence、Iq三種類型報文的路由有做二次開發的大改動,故不在此詳述這三種報文的路由過程。

三類報文需要用到路由的邏輯都調用了 RoutingTableImpl類的方法routePacket(JID jid, Packet packet, boolean fromServer),首先會根據jid(報文接收者的JID)來判斷路由的分支(本地域名分支、Component分支、遠程域名分支),轉入相應的分支進行處理,處理後根據返回判斷是否路由成功,路由失敗的的則會根據報文類型調用相應的處理(比如iq會返回service_unavailable響應報文,而message會根據策略進行離線消息的處理):

public void routePacket(JID jid, Packet packet, boolean fromServer) throws PacketException {
    boolean routed = false;
    try {
     if (serverName.equals(jid.getDomain())) {
       // Packet sent to our domain.
         routed = routeToLocalDomain(jid, packet, fromServer);
     }
     else if (jid.getDomain().endsWith(serverName) && hasComponentRoute(jid)) {
         // Packet sent to component hosted in this server
         routed = routeToComponent(jid, packet, routed);
     }
     else {
         // Packet sent to remote server
         routed = routeToRemoteDomain(jid, packet, routed);
     }
    } catch (Exception ex) {
        Log.error("Primary packet routing failed", ex);
    }
    if (!routed) {
        if (Log.isDebugEnabled()) {
            Log.debug("Failed to route packet to JID: {} packet: {}", jid, packet.toXML());
        }
        if (packet instanceof IQ) {
            iqRouter.routingFailed(jid, packet);
        }
        else if (packet instanceof Message) {
            messageRouter.routingFailed(jid, packet);
        }
        else if (packet instanceof Presence) {
            presenceRouter.routingFailed(jid, packet);
        }
    }
}
三類分支場景的處理最後都會在LocalRoutingTable(本地路由表類,路由表的底層實現)中路由到接收者JID所對應的RoutableChannelHandler,然後調用process(packet)方法進行具體業務的處理。

2.3   S2S(服務端到服務端)

OutgoingSessionPromise類

OutgoingSessionPromise類提供了一個異步發送報文到遠程server的方法。

當尋找到一個沒有存在鏈接的遠程server的路由,一個session promise被返回。

這個類將把報文排成隊列,在另一個線程執行處理報文。執行線程將使用一個實際做艱難工作的線程池。在池中的線程將嘗試去鏈接遠程server,發送報文。如果在建立鏈接或發送報文時,產生了一個錯誤,這個錯誤將被返回到報文的發送者。

 

S2S報文轉發流程

發送給其它服務器的消息由@domain 部分區分,在進入到服務器路由後在RoutingTableImpl.routePacket(Packetpacket) 中判斷域名部分,路由到遠程域名分支:

public void routePacket(JID jid,......){  
  boolean routed = false;  
  if(serverName.equals(jid.getDomain())){  
     routed = routeToLocalDomain(jid,packet,fromServer);  
  }  
  else if(jid.getDomain().contains(serverName)){  
     routed = routeToComponent(jid,packet,routed);  
  }  
  else{  
     routed = routeToRemoteDomain(jid,packet,routed);  
  }  
}  
......

在初次發送消息給外部服務器時兩臺服務器的連接還沒有建立,這種情況下會將包交由一個OutgoingSessionPromise 對象來處理,將消息加入它的隊列。

private boolean routeToRomoteDomain(JID jid,Packet packet,boolean routed){  
     byte[] nodeID = serverCache.get(jid.getDomain);  
     if(nodeID!=null){  
        ......  
     }  else{  
        OutgoingSessionPromise.getInstance().process(packet);  
        routed = true;  
     }  
     return routed;  
 }

在OutgoingSessionPromise 中保有一個線程池和一個獨立線程。

獨立線程不斷從消息隊列中讀取要處理的packet,並針對每個domain建立一個PacketsProcessor線程,將消息交給這個線程,然後把此線程放入線程池中運行。

final Packet packet = packets.take();  
boolean newProcessor = false;  
PacketsProcessor packetsProcessor;  
String domain = packet.getTo().getDomain();  
synchronized (domain.intern()){  
    packetsProcessor = packetsProcessors.get(domain);  
    if(packetsProcessor == null){  
       packetsProcessor = new PacketsProcessor(OutgoingSessionPromise.this,domain);  
       packetsProcessors.put(domain,packetsProcessor);  
       newProcessor = true;  
    }  
    packetsProcessor.addPacket(packet);  
}  
if(newProcessor){  
   threadPool.execute(packetsProcessor);  
}  

PacketsProcessor在發送消息包時會判斷到外部服務器的連接是否已經建立。未建立的情況下會調用LocalOutgoingServerSession.authenticateDomain() 方法建立連接。

具體的Socket連接建立是在authenticateDomain()方法中經過一系列的驗證和鑑權後調用createOutgoingSession(domain,hostname,port)來完成。建立好連接後則重新調用routingTable.routePacket() 再進行一次路由。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章