什么是会话?

  1.   A拨了B的电话
  2.   电话接通
  3.   A问道:Are you OK
  4.   B回复:I have a bug
  5.   A挂了电话

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

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

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

  SessionManager

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

  1. // 创建
  2. public LocalClientSession createClientSession(Connection conn, StreamID id, Locale language) ;
  3. // 添加
  4. public void addSession(LocalClientSession session) ;
  5. // 获取
  6. public ClientSession getSession(JID from) ;
  7. // 移除
  8. public boolean removeSession(LocalClientSession session) ;
  9.  
  10. ......

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

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

  特别注意的一点:

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

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

  Session 预创建

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

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

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

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

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

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

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

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

  1. public static LocalClientSession createSession(String serverName, XmlPullParser xpp, Connection connection)
  2. throws XmlPullParserException {
  3.  
  4. ......
  5.  
  6. // Create a ClientSession for this user.
  7. LocalClientSession session = SessionManager.getInstance().createClientSession(connection, language);
  8.  
  9. // Build the start packet response
  10. StringBuilder sb = new StringBuilder(200);
  11. sb.append("<?xml version='1.0' encoding='");
  12. sb.append(CHARSET);
  13. sb.append("'?>");
  14. if (isFlashClient) {
  15. sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
  16. }
  17. else {
  18. sb.append("<stream:stream ");
  19. }
  20. sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
  21. sb.append(serverName);
  22. sb.append("\" id=\"");
  23. sb.append(session.getStreamID().toString());
  24. sb.append("\" xml:lang=\"");
  25. sb.append(language.toLanguageTag());
  26. // Don't include version info if the version is 0.0.
  27. if (majorVersion != 0) {
  28. sb.append("\" version=\"");
  29. sb.append(majorVersion).append('.').append(minorVersion);
  30. }
  31. sb.append("\">");
  32. connection.deliverRawText(sb.toString());
  33.  
  34. // If this is a "Jabber" connection, the session is now initialized and we can
  35. // return to allow normal packet parsing.
  36. if (majorVersion == 0) {
  37. return session;
  38. }
  39. // Otherwise, this is at least XMPP 1.0 so we need to announce stream features.
  40.  
  41. sb = new StringBuilder(490);
  42. sb.append("<stream:features>");
  43. if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) {
  44. sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
  45. if (connection.getTlsPolicy() == Connection.TLSPolicy.required) {
  46. sb.append("<required/>");
  47. }
  48. sb.append("</starttls>");
  49. }
  50. // Include available SASL Mechanisms
  51. sb.append(SASLAuthentication.getSASLMechanisms(session));
  52. // Include Stream features
  53. String specificFeatures = session.getAvailableStreamFeatures();
  54. if (specificFeatures != null) {
  55. sb.append(specificFeatures);
  56. }
  57. sb.append("</stream:features>");
  58.  
  59. connection.deliverRawText(sb.toString());
  60.  
  61. return session;
  62. }

  创建了一个LocalClientSession对象之后,服务端调用了两次deliverRawText()给客户端发送报文。从协议报文的内容来看,其实就是登录过程中,服务端收到客户端第一个初始化流之后的两个应答:第一个是流回复,第二个是通知客户端进行STL协商。

  而LocalClientSession是由SessionManager生成。

  SessionManager.createClientSession()代码如下:

  1. public LocalClientSession createClientSession(Connection conn, Locale language) {
  2. return createClientSession(conn, nextStreamID(), language);
  3. }
  1. public LocalClientSession createClientSession(Connection conn, StreamID id, Locale language) {
  2. if (serverName == null) {
  3. throw new IllegalStateException("Server not initialized");
  4. }
  5. LocalClientSession session = new LocalClientSession(serverName, conn, id, language);
  6. conn.init(session);
  7. // Register to receive close notification on this session so we can
  8. // remove and also send an unavailable presence if it wasn't
  9. // sent before
  10. conn.registerCloseListener(clientSessionListener, session);
  11.  
  12. // Add to pre-authenticated sessions.
  13. localSessionManager.getPreAuthenticatedSessions().put(session.getAddress().getResource(), session);
  14. // Increment the counter of user sessions
  15. connectionsCounter.incrementAndGet();
  16. return session;
  17. } 

  由上面两个方法总结起来,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()方法如下,其中只保留资源绑定部分代码:

  1. public void route(IQ packet) {
  2. ......
  3. try {
  4. ......
  5. else if (session == null || session.getStatus() == Session.STATUS_AUTHENTICATED || (
  6. childElement != null && isLocalServer(to) && (
  7. "jabber:iq:auth".equals(childElement.getNamespaceURI()) ||
  8. "jabber:iq:register".equals(childElement.getNamespaceURI()) ||
  9. "urn:ietf:params:xml:ns:xmpp-bind".equals(childElement.getNamespaceURI())))) {
  10. handle(packet);
  11. }
  12. ......
  13. }
  14. catch (PacketRejectedException e) {
  15. ......
  16. }
  17. }

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

  IQRouter.handle():

  1. private void handle(IQ packet) {
  2. JID recipientJID = packet.getTo();
  3. ......
  4. if (isLocalServer(recipientJID)) {
  5. if (namespace == null) {
  6. ......
  7. }
  8. else {
  9. IQHandler handler = getHandler(namespace);
  10. if (handler == null) {
  11. ......
  12. }
  13. else {
  14. handler.process(packet);
  15. }
  16. }
  17. }
  18. ......
  19. }

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

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

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

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

  包处理方法IQHandler.process():

  1. @Override
  2. public void process(Packet packet) throws PacketException {
  3. IQ iq = (IQ) packet;
  4. try {
  5. IQ reply = handleIQ(iq);
  6. if (reply != null) {
  7. deliverer.deliver(reply);
  8. }
  9. }
  10. ......
  11. }

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

  1. @Override
  2. public IQ handleIQ(IQ packet) throws UnauthorizedException {
  3. LocalClientSession session = (LocalClientSession) sessionManager.getSession(packet.getFrom());
  4. IQ reply = IQ.createResultIQ(packet);
  5. Element child = reply.setChildElement("bind", "urn:ietf:params:xml:ns:xmpp-bind");
  6. ......
  7. if (authToken.isAnonymous()) {
  8. // User used ANONYMOUS SASL so initialize the session as an anonymous login
  9. session.setAnonymousAuth();
  10. }
  11. else {
  12. ......
  13. session.setAuthToken(authToken, resource);
  14. }
  15. child.addElement("jid").setText(session.getAddress().toString());
  16. // Send the response directly since a route does not exist at this point.
  17. session.process(reply);
  18. // After the client has been informed, inform all listeners as well.
  19. SessionEventDispatcher.dispatchEvent(session, SessionEventDispatcher.EventType.resource_bound);
  20. return null;
  21. }

  Session的认证后,其实就是将Session加入SessionManager中,如下:

  1. LocalClientSession.setAuthToken():
  2.  
  3. public void setAuthToken(AuthToken auth, String resource) {
  4. ......
  5. sessionManager.addSession(this);
  6. }

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

  SessionManager.addSession():

  1. public void addSession(LocalClientSession session) {
  2.  
  3. routingTable.addClientRoute(session.getAddress(), session);
  4. ....
  5. }

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

  Session 移除

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

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

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

  1. private class ClientSessionListener implements ConnectionCloseListener {
  2. /**
  3. * Handle a session that just closed.
  4. *
  5. * @param handback The session that just closed
  6. */
  7. @Override
  8. public void onConnectionClose(Object handback) {
  9. try {
  10. LocalClientSession session = (LocalClientSession) handback;
  11. try {
  12. if ((session.getPresence().isAvailable() || !session.wasAvailable()) &&
  13. routingTable.hasClientRoute(session.getAddress())) {
  14. // Send an unavailable presence to the user's subscribers
  15. // Note: This gives us a chance to send an unavailable presence to the
  16. // entities that the user sent directed presences
  17. Presence presence = new Presence();
  18. presence.setType(Presence.Type.unavailable);
  19. presence.setFrom(session.getAddress());
  20. router.route(presence);
  21. }
  22.  
  23. session.getStreamManager().onClose(router, serverAddress);
  24. }
  25. finally {
  26. // Remove the session
  27. removeSession(session);
  28. }
  29. }
  30. catch (Exception e) {
  31. // Can't do anything about this problem...
  32. Log.error(LocaleUtils.getLocalizedString("admin.error.close"), e);
  33. }
  34. }
  35. }

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


Over!

即时通信系统Openfire分析之五:会话管理的更多相关文章

  1. 即时通信系统Openfire分析之三:ConnectionManager 连接管理

    Openfire是怎么实现连接请求的? XMPPServer.start()方法,完成Openfire的启动.但是,XMPPServer.start()方法中,并没有提及如何监听端口,那么Openfi ...

  2. 即时通信系统Openfire分析之四:消息路由

    两个人的孤独 两个人的孤独,大抵是,你每发出去一句话,都要经由无数网络.由几百个计算机处理后,出在他的面前,而他就在你不远处. 连接管理之后 Openfire使用MINA网络框架,并设置Connect ...

  3. 即时通信系统Openfire分析之六:路由表 RoutingTable

    还是从会话管理说起 上一章,Session经过预创建.认证之后,才正常可用.认证时,最重要的操作,就是将Session加入到路由表,使之拥用了通信功能. 添加到至路由表的操作,是在SessionMan ...

  4. 即时通信系统Openfire分析之八:集群管理

    前言 在第六章<路由表>中,客户端进行会话时,首先要获取对方的Session实例.获取Session实例的方法,是先查找本地路由表,若找不到,则通过路由表中的缓存数据,由定位器获取. 路由 ...

  5. 即时通信系统Openfire分析之一:Openfire与XMPP协议

     引言 目前互联网产品使用的即时通信协议有这几种:即时信息和空间协议(IMPP).空间和即时信息协议(PRIM).针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)以及XMPP.PRIM与 ...

  6. 即时通信系统Openfire分析之七:集群配置

    前言 写这章之前,我犹豫了一会.在这个时候提集群,从章节安排上来讲,是否合适?但想到上一章<路由表>的相关内容,应该不至于太突兀.既然这样,那就撸起袖子干吧. Openfire的单机并发量 ...

  7. 即时通信系统Openfire分析之二:主干程序分析

    引言 宇宙大爆炸,于是开始了万物生衍,从一个连人渣都还没有的时代,一步步进化到如今的花花世界. 然而沧海桑田,一百多亿年过去了…. 好复杂,但程序就简单多了,main()函数运行,敲个回车,一行Hel ...

  8. tomcat会话之持久化会话管理器

    前面提到的标准会话管理器已经提供了基础的会话管理功能,但在持久化方面做得还是不够,或者说在某些情景下无法满足要求,例如把会话以文件或数据库形式存储到存储介质中,这些都是标准会话管理器无法做到的,于是另 ...

  9. Openfire分析之三:ConnectionManager 连接管理(1)

    Openfire是怎么实现连接请求的? XMPPServer.start()方法,完成Openfire的启动.但是,XMPPServer.start()方法中,并没有提及如何监听端口,那么Openfi ...

随机推荐

  1. JS中的phototype JS的三种方法(类方法、对象方法、原型方法)

    JS中的phototype是JS中比较难理解的一个部分 本文基于下面几个知识点: 1 原型法设计模式 在.Net中可以使用clone()来实现原型法 原型法的主要思想是,现在有1个类A,我想要创建一个 ...

  2. JavaWeb程序利用Servlet的对SQLserver增删改查操作

    声明:学了几天终于将增删改查的操作掌握了,也发现了一些问题,所以总结一下. 重点:操作数据库主要用的是SQL语句跟其他无关. 一:前提知识:PreparedStatement PreperedStat ...

  3. 微信小程序开发历程

    小程序:    帮助文档:(https://mp.weixin.qq.com/debug/wxadoc/dev/index.html) 优势:    一种无须安装即可运行的程序,与订阅号,服务号是同一 ...

  4. 九度OJ 1017 还是畅通工程

    #include <iostream> #include <string.h> #include <sstream> #include <math.h> ...

  5. 结对作业1----基于GUI的四则运算生成器

    组员:201421123015 陈麟凤 201421123019 张志杰 201421123020 黄海鸿 coding 地址:代码点这里 需求分析: 1.除了整数的四则运算还要支持分数的四则运算: ...

  6. 201521123061 《Java程序设计》第八周学习总结

    201521123061 <Java程序设计>第八周学习总结 1. 本周学习总结 2. 书面作业 1.List中指定元素的删除(题目4-1) 1.1 实验总结 主要是应用到了list中的a ...

  7. 201521123006 《Java程序设计》第6周学习总结

    1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 注1:关键词与内容不求多,但概念之间的联系要清晰,内容覆盖 ...

  8. 201521123068《Java程序设计》第4周学习总结

    1. 本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 点击查看->高清脑图 1.2 使用常规方法总结其他上课内容. 答:学习继承与多态的知识,了解它们之间的关系:super.ext ...

  9. 201521145048《Java程序设计管理》第一周学习总结

    1. 本周学习总结 学习并了解Java的发展与历史 在网上视频中学习Java 了解并区分JVM JRE JDK 将java与已学语言做比较,发现相同处 2. 书面作业 Q1.为什么java程序可以跨平 ...

  10. 201521123042 《Java程序设计》第12周学习总结

    本次作业参考文件 正则表达式参考资料 1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 2. 书面作业 将Student对象(属性:int id, String ...