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() 再进行一次路由。



  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值