• 即时通信系统Openfire分析之五:会话管理


      什么是会话?

      A拨了B的电话
      电话接通
      A问道:Are you OK? 
      B回复:I have a bug!
      A挂了电话

      上面所喻整个过程就是所谓的会话。

      会话(Session)是一个客户与服务器之间的不中断的请求响应序列。注意其中“不中断”一词。

      Openfire的通信,是以服务器为中转站的消息转发机制,客户端与服务器要实现通信,必须保持连接,即持有会话。Session的管理,集中在SessionManager模块中。

      SessionManager

      SessionManager提供了一系列与Session生命周期相关的管理功能,例如:

    // 创建
    public LocalClientSession createClientSession(Connection conn, StreamID id, Locale language) ;
    // 添加
    public void addSession(LocalClientSession session) ;
    // 获取
    public ClientSession getSession(JID from) ;
    // 移除
    public boolean removeSession(LocalClientSession session) ;
    
    ......

      Session的整个生命周期,大致的讲可以分为:预创建、认证、移除

    •   预创建:在连接打开后,服务端收到客户端的第一个消息请求(即初始化流)时完成,此时的Session还不能用于通信
    •   认证:在资源绑定时完成,此时的Session被添加到会话管理队列以及路由表中,象征着已具备通信功能
    •   移除:当连接空闲或者关闭时,Session被移除

      特别注意的一点:

      预创建、认证这两个过程,是在客户端登录的时候完成,从后面分析中看到的回应报文,以及第一章《Openfire与XMPP协议》中提到登录报文协议,可以清楚的看到这一点。而移除则是在客户端掉线的时候完成。

      下面,就重点来看看,Openfire是具体是如何实现对Session的管理。

      Session 预创建

      回顾一下上一章的内容:ConnectionHandler类作为MINA的处理器,ConnectionHandler中的messageReceived()方法是消息的接收入口,接收到的消息交由StanzaHandler类处理。

      StanzaHandler.process()方法在处理消息时,首先调用本类中createSession()方法,完成了对Session的预创建。

    abstract boolean createSession(String namespace, String serverName, XmlPullParser xpp, Connection connection)
    throws XmlPullParserException;

      上面的createSession()是一个抽象方法,由其子类完成。本文我们以C2S通信为研究对象,故其实现子类为:ClientStanzaHandler类

      ClientStanzaHandler.createSession()方法代码如下:

    @Override
    boolean createSession(String namespace, String serverName, XmlPullParser xpp, Connection connection)
            throws XmlPullParserException {
        if ("jabber:client".equals(namespace)) {
            // The connected client is a regular client so create a ClientSession
            session = LocalClientSession.createSession(serverName, xpp, connection);
            return true;
        }
        return false;
    }

    这里创建了一个LocalClientSession类型的Session对象。

    LocalClientSession.createSession()方法如下,只保留与创建流程相关的代码:

    public static LocalClientSession createSession(String serverName, XmlPullParser xpp, Connection connection)
            throws XmlPullParserException {
      
        ......
    
        // 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());
        // Don't include version info if the version is 0.0.
        if (majorVersion != 0) {
            sb.append("" version="");
            sb.append(majorVersion).append('.').append(minorVersion);
        }
        sb.append("">");
        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>");
        if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) {
            sb.append("<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">");
            if (connection.getTlsPolicy() == Connection.TLSPolicy.required) {
                sb.append("<required/>");
            }
            sb.append("</starttls>");
        }
        // 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对象之后,服务端调用了两次deliverRawText()给客户端发送报文。从协议报文的内容来看,其实就是登录过程中,服务端收到客户端第一个初始化流之后的两个应答:第一个是流回复,第二个是通知客户端进行STL协商。

      而LocalClientSession是由SessionManager生成。

      SessionManager.createClientSession()代码如下:

    public LocalClientSession createClientSession(Connection conn, Locale language) {
        return createClientSession(conn, nextStreamID(), language);
    }
    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't
        // sent 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;
    } 

      由上面两个方法总结起来,Session的预创建流程为:

      (1)生成一个新streamID,并创建一个LocalClientSession对象的session

      (2)调用conn.registerCloseListener(),注册了Session的关闭监听。作用是当Connection关掉时,Session也相应清除掉

      (3)将生成的session添加到preAuthenticatedSessions队列中,表示预创建完成。但此时的Session并没有加入到路由表,还不能用来通信

      Session 认证

      在第一章,《Openfire与XMPP协议》一文中已经介绍,资源绑定其实是用户登录过程的其中一步。亦即,在这里完成了Session的认证。

      资源绑定是一个IQ消息,结合上一章《消息路由》中的分析,对于IQ消息,PacketRouterImpl模块使用IQRouter来完成路由。

      IQRouter.route()方法如下,其中只保留资源绑定部分代码:

    public void route(IQ packet) {
        ......
        try {
            ......
            else if (session == null || session.getStatus() == Session.STATUS_AUTHENTICATED || (
                    childElement != null && isLocalServer(to) && (
                        "jabber:iq:auth".equals(childElement.getNamespaceURI()) ||
                        "jabber:iq:register".equals(childElement.getNamespaceURI()) ||
                        "urn:ietf:params:xml:ns:xmpp-bind".equals(childElement.getNamespaceURI())))) {
                handle(packet);
            } 
            ......
        }
        catch (PacketRejectedException e) {
            ......
        }
    }

      其中,handle()方法创建了处理该IQ的IQHandler,并调用IQandler中的process()进行包处理。

      IQRouter.handle():

    private void handle(IQ packet) {
        JID recipientJID = packet.getTo();
        ......
        if (isLocalServer(recipientJID)) {
            if (namespace == null) {
                ......
            }
            else {
                IQHandler handler = getHandler(namespace);
                if (handler == null) {
                   ......
                }
                else {
                    handler.process(packet);
                }
            }
        }
        ......
    }

      传入的参数"namespace",是IQ的唯一标识码,将决定了这个IQ由谁来处理。

      资源绑定的namespace为:urn:ietf:params:xml:ns:xmpp-bind,也就是说,这个IQ,最终将交给IQBindHandler来处理。

      可以看到,IQBindHandler的构造方法:

    public IQBindHandler() {
        super("Resource Binding handler");
        info = new IQHandlerInfo("bind", "urn:ietf:params:xml:ns:xmpp-bind");
    }

      包处理方法IQHandler.process():

    @Override
    public void process(Packet packet) throws PacketException {
        IQ iq = (IQ) packet;
        try {
            IQ reply = handleIQ(iq);
            if (reply != null) {
                deliverer.deliver(reply);
            }
        }
       ......
    }

      IQBindHandler.handleIQ()中,setAuthToken()方法实现对Session认证。

    @Override
    public IQ handleIQ(IQ packet) throws UnauthorizedException {
        LocalClientSession session = (LocalClientSession) sessionManager.getSession(packet.getFrom());
        IQ reply = IQ.createResultIQ(packet);
        Element child = reply.setChildElement("bind", "urn:ietf:params:xml:ns:xmpp-bind");
        ......
        if (authToken.isAnonymous()) {
            // User used ANONYMOUS SASL so initialize the session as an anonymous login
            session.setAnonymousAuth();
        }
        else {
            ......
            session.setAuthToken(authToken, resource);
        }
        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加入SessionManager中,如下:

    LocalClientSession.setAuthToken():
    
    public void setAuthToken(AuthToken auth, String resource) {
        ......
        sessionManager.addSession(this);
    }

      在第四章分析消息路由时,发送消息前,首先用ToJID从路由表中获取Session,接着再进行消息路由。也就是说,一条消息能否被接收到,取决于接收者的Session是否存在于路由表中。
      而SessionManager.addSession()刚好就是将Session加入路由表,如下:

      SessionManager.addSession():

    public void addSession(LocalClientSession session) {
    
        routingTable.addClientRoute(session.getAddress(), session);
        ....
    }

      此时,就代表了这个Session拥有了全部的功能,可以用来进行通信了。

      Session 移除

      移除工作就相对简单一些了,当监听到Connection关闭时,应清除掉相应的Session。

      在SessionManager的私有类ClientSessionListener实现了ConnectionCloseListener,能及时地监听到Connection关闭并进行Session的清除工作。监听是在Session预创建时注册,上文已经介绍。

      Session的关闭监听,ClientSessionListener类如下:

    private class ClientSessionListener implements ConnectionCloseListener {
        /**
         * Handle a session that just closed.
         *
         * @param handback The session that just closed
         */
        @Override
        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);
            }
        }
    }

      先关闭Sessoin,然后移除出队列,清除完毕!


    Over!

  • 相关阅读:
    js中.和[]的区别
    JavaScript【引用方法】操作方法
    JavaScript【引用方法】重排序方法
    JavaScript【引用方法】队列方法
    JavaScript【引用类型】栈方法
    JavaScript【引用类型】转换方法
    JavaScript【引用类型】检测类型
    JavaScript【引用类型】Array 类型
    JavaScript【引用类型】Object 类型
    【HTML5 绘图与动画】 使用图像 1.导入图像 2.缩放图像 3.裁切图像
  • 原文地址:https://www.cnblogs.com/Fordestiny/p/7487053.html
Copyright © 2020-2023  润新知