还是从会话管理说起

  上一章,Session经过预创建、认证之后,才正常可用。认证时,最重要的操作,就是将Session加入到路由表,使之拥用了通信功能。

  添加到至路由表的操作,是在SessionManager中操作的,如下:

  SessionManager.addSession(LocalClientSession session):

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

  进入路由表模块, RoutingTableImpl.addClientRoute(session.getAddress(), session)方法:

  1. public boolean addClientRoute(JID route, LocalClientSession destination) {
  2. boolean added;
  3. boolean available = destination.getPresence().isAvailable();
  4. localRoutingTable.addRoute(route.toString(), destination);
  5. ......
  6. return added;
  7. }

  从这里可以看出,路由表的底层,是借助LocalRoutingTable类来实现。

  路由表的底层数据结构

  LocalRoutingTable类的成员构成,非常的简单:

  1. Map<String, RoutableChannelHandler> routes = new ConcurrentHashMap<>();

  也就是说,路由表的实质,就是一个Map的数据结构,其Key为JID地址,Velue为RoutableChannelHandler类型报文处理器。

  查看路由表RoutingTableImpl模块中的路由添加方法,可以看到表中存储的是以RoutableChannelHandler衍生出来的几个Session类型,总共提供了三种:

  LocalOutgoingServerSession(用于存储连接本机的远程服务端)、LocalClientSession(用于存储连接到本机的客户端)、RoutableChannelHandler(用于存储组件),类结构如下:

  1. |-- RoutableChannelHandler
  2. |-- Session
  3. |-- LocalSession
  4. |-- LocalClientSession
  5. |-- LocalServerSession
  6. |-- LocalOutgoingServerSession

  而LocalRoutingTable内的所有方法,就是一系列对这个Map结构的操作函数,核心的如下几个:

  添加路由:

  1. boolean addRoute(String address, RoutableChannelHandler route) {
  2. return routes.put(address, route) != route;
  3. }

  获取路由:

  1. RoutableChannelHandler getRoute(String address) {
  2. return routes.get(address);
  3. }

  获取客户端的Session列表:

  1. Collection<LocalClientSession> getClientRoutes() {
  2. List<LocalClientSession> sessions = new ArrayList<>();
  3. for (RoutableChannelHandler route : routes.values()) {
  4. if (route instanceof LocalClientSession) {
  5. sessions.add((LocalClientSession) route);
  6. }
  7. }
  8. return sessions;
  9. }

  移除路由

  1. void removeRoute(String address) {
  2. routes.remove(address);
  3. }

  还有一个每3分钟一次的定时任务,查询并关闭被闲置了的远程服务器Session,在路由表中启动该任务

  1. public void start() {
  2. int period = 3 * 60 * 1000;
  3. TaskEngine.getInstance().scheduleAtFixedRate(new ServerCleanupTask(), period, period);
  4. }

  路由表模块 RoutingTable

  路由表是Openfire的核心module之一,RoutingTable接口定义了一系列操作标准,主要围绕路由表进行,提供添加,删除,查询,消息路由等操作,而RoutingTableImpl负责具体实现。

  先来看看RoutingTableImpl的成员列表

  1. /**
  2. * 缓存外部远程服务器session
  3. * Key: server domain, Value: nodeID
  4. */
  5. private Cache<String, byte[]> serversCache;
  6. /**
  7. * 缓存服务器的组件
  8. * Key: component domain, Value: list of nodeIDs hosting the component
  9. */
  10. private Cache<String, Set<NodeID>> componentsCache;
  11. /**
  12. * 缓存已认证的客户端session
  13. * Key: full JID, Value: {nodeID, available/unavailable}
  14. */
  15. private Cache<String, ClientRoute> usersCache;
  16. /**
  17. * 缓存已认证匿名的客户端session
  18. * Key: full JID, Value: {nodeID, available/unavailable}
  19. */
  20. private Cache<String, ClientRoute> anonymousUsersCache;
  21. /**
  22. * 缓存已认证(包括匿名)的客户端Resource,一个用户,在每一端登录,都会有一个resource
  23. * Key: bare JID, Value: list of full JIDs of the user
  24. */
  25. private Cache<String, Collection<String>> usersSessions;
  26.  
  27. private String serverName; // 服务器的域名
  28. private XMPPServer server; // XMPP服务
  29. private LocalRoutingTable localRoutingTable; // 路由表底层
  30. private RemotePacketRouter remotePacketRouter; // 远程包路由器
  31. private IQRouter iqRouter; // IQ包路由器
  32. private MessageRouter messageRouter; // Message包路由器
  33. private PresenceRouter presenceRouter; // Presence包路由器
  34. private PresenceUpdateHandler presenceUpdateHandler; // 在线状态更新处理器

  成员列表中,除了LocalRoutingTable之外,还定义了一堆的缓存。这些缓存干嘛用?

  Openfire支持集群机制,即在多台服务器上分别运行一个Openfire实例,并使各个实例的数据同步。算法一致,数据一致,用户不管连接到任意一台服务器,效果就都一样。

  集群中的数据同步,除了数据库之外,其他的都是用通过缓存来处理,而上面的这些缓存正是集群同步的一部分,用于同步用户路由信息,每个服务器都会有缓存的副本。

  总的来说,LocalRoutingTable用于存储本机的路由数据,而Cache中是存储了整个集群的路由数据。

  但是,需要注意的一点,LocalRoutingTable与Cache,这两者的数据结构并不相同:

  (1)LocalRoutingTable中记录了本机中所有的Session实例,可以用来通信

  (2)Cache中只存储了用户路由节点信息,需要通过集群管理组件来获取Session实例

  路由表的操作

  路由表的操作,实际上就是在会话管理中,对会话实例的操作。为免与上面混淆,这一节的功能说明,以会话代称。

  添加路由(会话)

  代码如下:

  1. @Override
  2. public boolean addClientRoute(JID route, LocalClientSession destination) {
  3. boolean added;
  4. boolean available = destination.getPresence().isAvailable();
  5.  
  6. // 加入到路由表
  7. localRoutingTable.addRoute(route.toString(), destination);
  8.  
  9. // 若为匿名客户端,添加到anonymousUsersCache、usersSessions缓存队列中
  10. if (destination.getAuthToken().isAnonymous()) {
  11. Lock lockAn = CacheFactory.getLock(route.toString(), anonymousUsersCache);
  12. try {
  13. lockAn.lock();
  14. added = anonymousUsersCache.put(route.toString(), new ClientRoute(server.getNodeID(), available)) ==
  15. null;
  16. }
  17. finally {
  18. lockAn.unlock();
  19. }
  20. // Add the session to the list of user sessions
  21. if (route.getResource() != null && (!available || added)) {
  22. Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
  23. try {
  24. lock.lock();
  25. usersSessions.put(route.toBareJID(), Arrays.asList(route.toString()));
  26. }
  27. finally {
  28. lock.unlock();
  29. }
  30. }
  31. }
  32.  
  33. // 非匿名客户端,添加到usersCache、usersSessions缓存队列中
  34. else {
  35. Lock lockU = CacheFactory.getLock(route.toString(), usersCache);
  36. try {
  37. lockU.lock();
  38. added = usersCache.put(route.toString(), new ClientRoute(server.getNodeID(), available)) == null;
  39. }
  40. finally {
  41. lockU.unlock();
  42. }
  43. // Add the session to the list of user sessions
  44. if (route.getResource() != null && (!available || added)) {
  45. Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
  46. try {
  47. lock.lock();
  48. Collection<String> jids = usersSessions.get(route.toBareJID());
  49. if (jids == null) {
  50. // Optimization - use different class depending on current setup
  51. if (ClusterManager.isClusteringStarted()) {
  52. jids = new HashSet<>();
  53. }
  54. else {
  55. jids = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
  56. }
  57. }
  58. jids.add(route.toString());
  59. usersSessions.put(route.toBareJID(), jids);
  60. }
  61. finally {
  62. lock.unlock();
  63. }
  64. }
  65. }
  66. return added;
  67. }

  主要两步:

  (1)添加到路由表
  (2)添加到对应的缓存中

  移除路由(会话)

  代码如下:

  1. @Override
  2. public boolean removeClientRoute(JID route) {
  3. boolean anonymous = false;
  4. String address = route.toString();
  5. ClientRoute clientRoute = null;
  6.  
  7. // 从缓存中移除客户端的Session信息
  8. Lock lockU = CacheFactory.getLock(address, usersCache);
  9. try {
  10. lockU.lock();
  11. clientRoute = usersCache.remove(address);
  12. }
  13. finally {
  14. lockU.unlock();
  15. }
  16. if (clientRoute == null) {
  17. Lock lockA = CacheFactory.getLock(address, anonymousUsersCache);
  18. try {
  19. lockA.lock();
  20. clientRoute = anonymousUsersCache.remove(address);
  21. anonymous = true;
  22. }
  23. finally {
  24. lockA.unlock();
  25. }
  26. }
  27. if (clientRoute != null && route.getResource() != null) {
  28. Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
  29. try {
  30. lock.lock();
  31. if (anonymous) {
  32. usersSessions.remove(route.toBareJID());
  33. }
  34. else {
  35. Collection<String> jids = usersSessions.get(route.toBareJID());
  36. if (jids != null) {
  37. jids.remove(route.toString());
  38. if (!jids.isEmpty()) {
  39. usersSessions.put(route.toBareJID(), jids);
  40. }
  41. else {
  42. usersSessions.remove(route.toBareJID());
  43. }
  44. }
  45. }
  46. }
  47. finally {
  48. lock.unlock();
  49. }
  50. }
  51.  
  52. // 将对应客户端的Session信息,移出路由表
  53. localRoutingTable.removeRoute(address);
  54. return clientRoute != null;
  55. }

  操作与添加类似:
  (1)移除缓存里的路由信息
  (2)移除路由表中的信息

  获取路由(会话)

  1. @Override
  2. public ClientSession getClientRoute(JID jid) {
  3. // Check if this session is hosted by this cluster node
  4. ClientSession session = (ClientSession) localRoutingTable.getRoute(jid.toString());
  5. if (session == null) {
  6. // The session is not in this JVM so assume remote
  7. RemoteSessionLocator locator = server.getRemoteSessionLocator();
  8. if (locator != null) {
  9. // Check if the session is hosted by other cluster node
  10. ClientRoute route = usersCache.get(jid.toString());
  11. if (route == null) {
  12. route = anonymousUsersCache.get(jid.toString());
  13. }
  14. if (route != null) {
  15. session = locator.getClientSession(route.getNodeID().toByteArray(), jid);
  16. }
  17. }
  18. }
  19. return session;
  20. }

  从上面的方法代码中可以看到,获取路由的方法是:先查找本地路由表,若获取不到对应Session时,则通过集群获取。RemoteSessionLocator是用于适配不同的集群组件所抽象的接口,为不同集群组件提供了透明处理。

  至于如何从集群中获取Session,主要就在于sersCache和anonymousUsersCache这两个cache,它们记录了每个客户端的路由节点信息,通过它可以取得对应的Session实例。详见第八章《集群管理》

  消息路由

  根据发送的形式,分为两种:一是广播、二是单点路由

  1、以广播的形式,向所有在线的客户端发送消息

  1. @Override
  2. public void broadcastPacket(Message packet, boolean onlyLocal) {
  3. // Send the message to client sessions connected to this JVM
  4. for(ClientSession session : localRoutingTable.getClientRoutes()) {
  5. session.process(packet);
  6. }
  7.  
  8. // Check if we need to broadcast the message to client sessions connected to remote cluter nodes
  9. if (!onlyLocal && remotePacketRouter != null) {
  10. remotePacketRouter.broadcastPacket(packet);
  11. }
  12. }

  2、单点发送的形式,向某个指定的客户端发送消息

  1. @Override
  2. public void routePacket(JID jid, Packet packet, boolean fromServer) throws PacketException {
  3.  
  4. boolean routed = false;
  5. try {
  6. if (serverName.equals(jid.getDomain())) {
  7. // Packet sent to our domain.
  8. routed = routeToLocalDomain(jid, packet, fromServer);
  9. Log.info("routeToLocalDomain");
  10. }
  11. else if (jid.getDomain().endsWith(serverName) && hasComponentRoute(jid)) {
  12. // Packet sent to component hosted in this server
  13. routed = routeToComponent(jid, packet, routed);
  14. Log.info("routeToComponent");
  15. }
  16. else {
  17. // Packet sent to remote server
  18. routed = routeToRemoteDomain(jid, packet, routed);
  19. Log.info("routeToRemoteDomain");
  20. }
  21. } catch (Exception ex) {
  22. // Catch here to ensure that all packets get handled, despite various processing
  23. // exceptions, rather than letting any fall through the cracks. For example,
  24. // an IAE could be thrown when running in a cluster if a remote member becomes
  25. // unavailable before the routing caches are updated to remove the defunct node.
  26. // We have also occasionally seen various flavors of NPE and other oddities,
  27. // typically due to unexpected environment or logic breakdowns.
  28. Log.error("Primary packet routing failed", ex);
  29. }
  30.  
  31. if (!routed) {
  32. if (Log.isDebugEnabled()) {
  33. Log.debug("Failed to route packet to JID: {} packet: {}", jid, packet.toXML());
  34. }
  35. if (packet instanceof IQ) {
  36. iqRouter.routingFailed(jid, packet);
  37. }
  38. else if (packet instanceof Message) {
  39. messageRouter.routingFailed(jid, packet);
  40. }
  41. else if (packet instanceof Presence) {
  42. presenceRouter.routingFailed(jid, packet);
  43. }
  44. }
  45. }

  路由表中的功能,最后由SessionManager集中处理,详见上一章的分析,这里不再赘述。

  特点提一点,比较有用:路由表做为一个module已经在Openfire主服务启动时完成实例化,所以,在自定义的插件、或者其他任何需要发送消息的地方,只需选择调用如下两个方法中之一,即可完成消息发送:

  1. XMPPServer.getInstance().getRoutingTable().routePacket(jid, packet, fromServer);
  2.  
  3. XMPPServer.getInstance().getRoutingTable().broadcastPacket(packet, onlyLocal);

  而消息发送中,最后消息如何送到网卡实现发送,在第三章《消息路由》中已经详细分析,同样不再赘述。


  本章就到此结束,OVER!

即时通信系统Openfire分析之六:路由表 RoutingTable的更多相关文章

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

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

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

    什么是会话? A拨了B的电话 电话接通 A问道:Are you OK? B回复:I have a bug! A挂了电话 这整个过程就是会话. 会话(Session)是一个客户与服务器之间的不中断的请求 ...

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

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

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

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

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

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

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

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

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

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

  8. 基于XMPP的即时通信系统的建立(二)— XMPP详解

    XMPP详解 XMPP(eXtensible Messaging and Presence Protocol,可扩展消息处理和现场协议)是一种在两个地点间传递小型结构化数据的协议.在此基础上,XMPP ...

  9. 基于XMPP的即时通信系统的建立 — XMPP IQ详解

    XMPP详解 XMPP(eXtensible Messaging and Presence Protocol,可扩展消息处理和现场协议)是一种在两个地点间传递小型结构化数据的协议.在此基础上,XMPP ...

随机推荐

  1. 通过createObjectURL实现图片预览

    实现原理:通过createObjectURL 创建一个临时指向某地址的二进制对象. 过程:点击触发隐藏的 input   file  的点击事件,使用createObjectURL读取 file,创建 ...

  2. C# 反向生成工具(DAL BLL Modle)

    VS2015  ADO.NET无果后果断~! 动软生成:http://pan.baidu.com/s/1gfIf0ZL

  3. 201521123083《Java程序设计》第11周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 上周这张图没理解完,继续 2. 书面作业 本次PTA作业题集多线程 1互斥访问与同步访问完成题集4-4(互斥访问) ...

  4. 201521123096《Java程序设计》第八周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 本次作业题集集合 1.List中指定元素的删除(题目4-1) 1.1 实验总结 实验中使用了s ...

  5. Android事件机制

    一句话描述: 用户和程序之间的互动机制 什么是事件? 用户和程序交互时触发的程序操作. 只要是事件,必须具备三方面: 1 事件的发生者 2 事件接受者 3 事件触发和传递 事件处理的方法 观察者模式: ...

  6. 201521123012 《Java程序设计》第十二周学习总结

     作业参考文件 1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 2. 书面作业 将Student对象(属性:int id, String name,int ag ...

  7. 201521123068《Java程序设计》第12周学习总结

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

  8. 控制结构(6) 最近最少使用(LRU)

    // 上一篇:必经之地(using) // 下一篇:程序计数器(PC) 基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构. There are only two hard thin ...

  9. 通过SDK和API获取阿里云RDS的监控数据

    阿里云的RDS自带的监控系统获取数据不怎么直观,想要通过API获取数据通过zabbix显示,因为网上资料缺乏和其他一些原因,获取API签名很困难,但使用阿里云的SDK可以完美避开获取签名的步骤. 阿里 ...

  10. Android 之JDBC

    JDBC(Java DataBase Connectivity)是使用 Java 存取数据库系统的标准解决方案,它将不同数据库间各自差异API与标准 SQL语句分开看待,实现数据库无关的 Java操作 ...