即时通信系统Openfire分析之六:路由表 RoutingTable
还是从会话管理说起
上一章,Session经过预创建、认证之后,才正常可用。认证时,最重要的操作,就是将Session加入到路由表,使之拥用了通信功能。
添加到至路由表的操作,是在SessionManager中操作的,如下:
SessionManager.addSession(LocalClientSession session):
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));
}
}
进入路由表模块, RoutingTableImpl.addClientRoute(session.getAddress(), session)方法:
public boolean addClientRoute(JID route, LocalClientSession destination) {
boolean added;
boolean available = destination.getPresence().isAvailable();
localRoutingTable.addRoute(route.toString(), destination);
......
return added;
}
从这里可以看出,路由表的底层,是借助LocalRoutingTable类来实现。
路由表的底层数据结构
LocalRoutingTable类的成员构成,非常的简单:
Map<String, RoutableChannelHandler> routes = new ConcurrentHashMap<>();
也就是说,路由表的实质,就是一个Map的数据结构,其Key为JID地址,Velue为RoutableChannelHandler类型报文处理器。
查看路由表RoutingTableImpl模块中的路由添加方法,可以看到表中存储的是以RoutableChannelHandler衍生出来的几个Session类型,总共提供了三种:
LocalOutgoingServerSession(用于存储连接本机的远程服务端)、LocalClientSession(用于存储连接到本机的客户端)、RoutableChannelHandler(用于存储组件),类结构如下:
|-- RoutableChannelHandler
|-- Session
|-- LocalSession
|-- LocalClientSession
|-- LocalServerSession
|-- LocalOutgoingServerSession
而LocalRoutingTable内的所有方法,就是一系列对这个Map结构的操作函数,核心的如下几个:
添加路由:
boolean addRoute(String address, RoutableChannelHandler route) {
return routes.put(address, route) != route;
}
获取路由:
RoutableChannelHandler getRoute(String address) {
return routes.get(address);
}
获取客户端的Session列表:
Collection<LocalClientSession> getClientRoutes() {
List<LocalClientSession> sessions = new ArrayList<>();
for (RoutableChannelHandler route : routes.values()) {
if (route instanceof LocalClientSession) {
sessions.add((LocalClientSession) route);
}
}
return sessions;
}
移除路由
void removeRoute(String address) {
routes.remove(address);
}
还有一个每3分钟一次的定时任务,查询并关闭被闲置了的远程服务器Session,在路由表中启动该任务
public void start() {
int period = 3 * 60 * 1000;
TaskEngine.getInstance().scheduleAtFixedRate(new ServerCleanupTask(), period, period);
}
路由表模块 RoutingTable
路由表是Openfire的核心module之一,RoutingTable接口定义了一系列操作标准,主要围绕路由表进行,提供添加,删除,查询,消息路由等操作,而RoutingTableImpl负责具体实现。
先来看看RoutingTableImpl的成员列表
/**
* 缓存外部远程服务器session
* Key: server domain, Value: nodeID
*/
private Cache<String, byte[]> serversCache;
/**
* 缓存服务器的组件
* Key: component domain, Value: list of nodeIDs hosting the component
*/
private Cache<String, Set<NodeID>> componentsCache;
/**
* 缓存已认证的客户端session
* Key: full JID, Value: {nodeID, available/unavailable}
*/
private Cache<String, ClientRoute> usersCache;
/**
* 缓存已认证匿名的客户端session
* Key: full JID, Value: {nodeID, available/unavailable}
*/
private Cache<String, ClientRoute> anonymousUsersCache;
/**
* 缓存已认证(包括匿名)的客户端Resource,一个用户,在每一端登录,都会有一个resource
* Key: bare JID, Value: list of full JIDs of the user
*/
private Cache<String, Collection<String>> usersSessions; private String serverName; // 服务器的域名
private XMPPServer server; // XMPP服务
private LocalRoutingTable localRoutingTable; // 路由表底层
private RemotePacketRouter remotePacketRouter; // 远程包路由器
private IQRouter iqRouter; // IQ包路由器
private MessageRouter messageRouter; // Message包路由器
private PresenceRouter presenceRouter; // Presence包路由器
private PresenceUpdateHandler presenceUpdateHandler; // 在线状态更新处理器
成员列表中,除了LocalRoutingTable之外,还定义了一堆的缓存。这些缓存干嘛用?
Openfire支持集群机制,即在多台服务器上分别运行一个Openfire实例,并使各个实例的数据同步。算法一致,数据一致,用户不管连接到任意一台服务器,效果就都一样。
集群中的数据同步,除了数据库之外,其他的都是用通过缓存来处理,而上面的这些缓存正是集群同步的一部分,用于同步用户路由信息,每个服务器都会有缓存的副本。
总的来说,LocalRoutingTable用于存储本机的路由数据,而Cache中是存储了整个集群的路由数据。
但是,需要注意的一点,LocalRoutingTable与Cache,这两者的数据结构并不相同:
(1)LocalRoutingTable中记录了本机中所有的Session实例,可以用来通信
(2)Cache中只存储了用户路由节点信息,需要通过集群管理组件来获取Session实例
路由表的操作
路由表的操作,实际上就是在会话管理中,对会话实例的操作。为免与上面混淆,这一节的功能说明,以会话代称。
添加路由(会话)
代码如下:
@Override
public boolean addClientRoute(JID route, LocalClientSession destination) {
boolean added;
boolean available = destination.getPresence().isAvailable(); // 加入到路由表
localRoutingTable.addRoute(route.toString(), destination); // 若为匿名客户端,添加到anonymousUsersCache、usersSessions缓存队列中
if (destination.getAuthToken().isAnonymous()) {
Lock lockAn = CacheFactory.getLock(route.toString(), anonymousUsersCache);
try {
lockAn.lock();
added = anonymousUsersCache.put(route.toString(), new ClientRoute(server.getNodeID(), available)) ==
null;
}
finally {
lockAn.unlock();
}
// Add the session to the list of user sessions
if (route.getResource() != null && (!available || added)) {
Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
try {
lock.lock();
usersSessions.put(route.toBareJID(), Arrays.asList(route.toString()));
}
finally {
lock.unlock();
}
}
} // 非匿名客户端,添加到usersCache、usersSessions缓存队列中
else {
Lock lockU = CacheFactory.getLock(route.toString(), usersCache);
try {
lockU.lock();
added = usersCache.put(route.toString(), new ClientRoute(server.getNodeID(), available)) == null;
}
finally {
lockU.unlock();
}
// Add the session to the list of user sessions
if (route.getResource() != null && (!available || added)) {
Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
try {
lock.lock();
Collection<String> jids = usersSessions.get(route.toBareJID());
if (jids == null) {
// Optimization - use different class depending on current setup
if (ClusterManager.isClusteringStarted()) {
jids = new HashSet<>();
}
else {
jids = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
}
}
jids.add(route.toString());
usersSessions.put(route.toBareJID(), jids);
}
finally {
lock.unlock();
}
}
}
return added;
}
主要两步:
(1)添加到路由表
(2)添加到对应的缓存中
移除路由(会话)
代码如下:
@Override
public boolean removeClientRoute(JID route) {
boolean anonymous = false;
String address = route.toString();
ClientRoute clientRoute = null; // 从缓存中移除客户端的Session信息
Lock lockU = CacheFactory.getLock(address, usersCache);
try {
lockU.lock();
clientRoute = usersCache.remove(address);
}
finally {
lockU.unlock();
}
if (clientRoute == null) {
Lock lockA = CacheFactory.getLock(address, anonymousUsersCache);
try {
lockA.lock();
clientRoute = anonymousUsersCache.remove(address);
anonymous = true;
}
finally {
lockA.unlock();
}
}
if (clientRoute != null && route.getResource() != null) {
Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
try {
lock.lock();
if (anonymous) {
usersSessions.remove(route.toBareJID());
}
else {
Collection<String> jids = usersSessions.get(route.toBareJID());
if (jids != null) {
jids.remove(route.toString());
if (!jids.isEmpty()) {
usersSessions.put(route.toBareJID(), jids);
}
else {
usersSessions.remove(route.toBareJID());
}
}
}
}
finally {
lock.unlock();
}
} // 将对应客户端的Session信息,移出路由表
localRoutingTable.removeRoute(address);
return clientRoute != null;
}
操作与添加类似:
(1)移除缓存里的路由信息
(2)移除路由表中的信息
获取路由(会话)
@Override
public ClientSession getClientRoute(JID jid) {
// Check if this session is hosted by this cluster node
ClientSession session = (ClientSession) localRoutingTable.getRoute(jid.toString());
if (session == null) {
// The session is not in this JVM so assume remote
RemoteSessionLocator locator = server.getRemoteSessionLocator();
if (locator != null) {
// Check if the session is hosted by other cluster node
ClientRoute route = usersCache.get(jid.toString());
if (route == null) {
route = anonymousUsersCache.get(jid.toString());
}
if (route != null) {
session = locator.getClientSession(route.getNodeID().toByteArray(), jid);
}
}
}
return session;
}
从上面的方法代码中可以看到,获取路由的方法是:先查找本地路由表,若获取不到对应Session时,则通过集群获取。RemoteSessionLocator是用于适配不同的集群组件所抽象的接口,为不同集群组件提供了透明处理。
至于如何从集群中获取Session,主要就在于sersCache和anonymousUsersCache这两个cache,它们记录了每个客户端的路由节点信息,通过它可以取得对应的Session实例。详见第八章《集群管理》
消息路由
根据发送的形式,分为两种:一是广播、二是单点路由
1、以广播的形式,向所有在线的客户端发送消息
@Override
public void broadcastPacket(Message packet, boolean onlyLocal) {
// Send the message to client sessions connected to this JVM
for(ClientSession session : localRoutingTable.getClientRoutes()) {
session.process(packet);
} // Check if we need to broadcast the message to client sessions connected to remote cluter nodes
if (!onlyLocal && remotePacketRouter != null) {
remotePacketRouter.broadcastPacket(packet);
}
}
2、单点发送的形式,向某个指定的客户端发送消息
@Override
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);
Log.info("routeToLocalDomain");
}
else if (jid.getDomain().endsWith(serverName) && hasComponentRoute(jid)) {
// Packet sent to component hosted in this server
routed = routeToComponent(jid, packet, routed);
Log.info("routeToComponent");
}
else {
// Packet sent to remote server
routed = routeToRemoteDomain(jid, packet, routed);
Log.info("routeToRemoteDomain");
}
} catch (Exception ex) {
// Catch here to ensure that all packets get handled, despite various processing
// exceptions, rather than letting any fall through the cracks. For example,
// an IAE could be thrown when running in a cluster if a remote member becomes
// unavailable before the routing caches are updated to remove the defunct node.
// We have also occasionally seen various flavors of NPE and other oddities,
// typically due to unexpected environment or logic breakdowns.
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);
}
}
}
路由表中的功能,最后由SessionManager集中处理,详见上一章的分析,这里不再赘述。
特点提一点,比较有用:路由表做为一个module已经在Openfire主服务启动时完成实例化,所以,在自定义的插件、或者其他任何需要发送消息的地方,只需选择调用如下两个方法中之一,即可完成消息发送:
XMPPServer.getInstance().getRoutingTable().routePacket(jid, packet, fromServer); XMPPServer.getInstance().getRoutingTable().broadcastPacket(packet, onlyLocal);
而消息发送中,最后消息如何送到网卡实现发送,在第三章《消息路由》中已经详细分析,同样不再赘述。
本章就到此结束,OVER!
即时通信系统Openfire分析之六:路由表 RoutingTable的更多相关文章
- 即时通信系统Openfire分析之四:消息路由
两个人的孤独 两个人的孤独,大抵是,你每发出去一句话,都要经由无数网络.由几百个计算机处理后,出在他的面前,而他就在你不远处. 连接管理之后 Openfire使用MINA网络框架,并设置Connect ...
- 即时通信系统Openfire分析之五:会话管理
什么是会话? A拨了B的电话 电话接通 A问道:Are you OK? B回复:I have a bug! A挂了电话 这整个过程就是会话. 会话(Session)是一个客户与服务器之间的不中断的请求 ...
- 即时通信系统Openfire分析之八:集群管理
前言 在第六章<路由表>中,客户端进行会话时,首先要获取对方的Session实例.获取Session实例的方法,是先查找本地路由表,若找不到,则通过路由表中的缓存数据,由定位器获取. 路由 ...
- 即时通信系统Openfire分析之一:Openfire与XMPP协议
引言 目前互联网产品使用的即时通信协议有这几种:即时信息和空间协议(IMPP).空间和即时信息协议(PRIM).针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)以及XMPP.PRIM与 ...
- 即时通信系统Openfire分析之七:集群配置
前言 写这章之前,我犹豫了一会.在这个时候提集群,从章节安排上来讲,是否合适?但想到上一章<路由表>的相关内容,应该不至于太突兀.既然这样,那就撸起袖子干吧. Openfire的单机并发量 ...
- 即时通信系统Openfire分析之三:ConnectionManager 连接管理
Openfire是怎么实现连接请求的? XMPPServer.start()方法,完成Openfire的启动.但是,XMPPServer.start()方法中,并没有提及如何监听端口,那么Openfi ...
- 即时通信系统Openfire分析之二:主干程序分析
引言 宇宙大爆炸,于是开始了万物生衍,从一个连人渣都还没有的时代,一步步进化到如今的花花世界. 然而沧海桑田,一百多亿年过去了…. 好复杂,但程序就简单多了,main()函数运行,敲个回车,一行Hel ...
- 基于XMPP的即时通信系统的建立(二)— XMPP详解
XMPP详解 XMPP(eXtensible Messaging and Presence Protocol,可扩展消息处理和现场协议)是一种在两个地点间传递小型结构化数据的协议.在此基础上,XMPP ...
- 基于XMPP的即时通信系统的建立 — XMPP IQ详解
XMPP详解 XMPP(eXtensible Messaging and Presence Protocol,可扩展消息处理和现场协议)是一种在两个地点间传递小型结构化数据的协议.在此基础上,XMPP ...
随机推荐
- URL.createObjectURL() 与 URL.revokeObjectURL()
.URL.createObjectURL URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. 这个URL的生命仅存在于它被创建的这个文档里. 新的对象URL ...
- Git简易参考手册
如果用过mercury(HG),那么理解Git的运作方式就轻松多了.两者是相同的分布式版本管理工具,只是某些功能有着细微的差别 - Git的管理粒度更加细腻,因此操作上也比HG复杂一点.例如,修改文件 ...
- sublime编辑器代码背景刺眼怎么修改?
有些人觉得如上图大括号刺眼,怎么把它改得不那么刺眼呢? [第一步]打开Bracket Hightlighter插件的用户配置文件: 然后按ctrl+G跳转到第330行, 如图位置改为"sty ...
- java 反射详解
反射的概念和原理 类字节码文件是在硬盘上存储的,是一个个的.class文件.我们在new一个对象时,JVM会先把字节码文件的信息读出来放到内存中,第二次用时,就不用在加载了,而是直接使用之前缓存的这个 ...
- 【Alpha】阶段 第六次 Scrum Meeting
每日任务 1.本次会议为第 六次 Meeting会议: 2.本次会议在上午09:35,大课间休息时间在陆大召开,召开本次会议为20分钟,讨论统一下时间安排的问题以及一些程序上的该进: 一.今日站立式会 ...
- 第二次作业——个人项目实战(Sudoku)
Github:Sudoku 项目相关要求 利用程序随机构造出N个已解答的数独棋盘 . 输入 数独棋盘题目个数N 输出 随机生成N个 不重复 的 已解答完毕的 数独棋盘,并输出到sudoku.txt中, ...
- 201521123108 《Java程序设计》第2周学习总结
1. 本章学习总结 学习了java的知识,虽然还不是太懂,以后一定会取得进步的 2. 书面作业 Q1. 使用Eclipse关联jdk源代码,并查看String对象的源代码(截图)? 答: Q2. 为什 ...
- Git与码云(Git@OSC)入门-如何在实验室和宿舍同步你的代码(2)
4. 处理冲突 4.1 向远程仓库push时无法提交成功,提示在push前应该先pull 如图所示: 有可能是因为远程仓库的版本与本地仓库的版本不一致,所以应先git pull将远程仓库的内容合并到本 ...
- 201521123096《Java程序设计》第十二周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 2. 书面作业 将Student对象(属性:int id, String name,int age,doubl ...
- Java程序设计——学生基本信息管理系统
1.团队课程设计博客链接 http://www.cnblogs.com/handsome321/p/7067121.html 2.个人负责模块说明 本组课题:学生信息管理系统 本人任务:插入.删除学生 ...