即时通信系统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!
即时通信系统Openfire分析之五:会话管理的更多相关文章
- 即时通信系统Openfire分析之三:ConnectionManager 连接管理
Openfire是怎么实现连接请求的? XMPPServer.start()方法,完成Openfire的启动.但是,XMPPServer.start()方法中,并没有提及如何监听端口,那么Openfi ...
- 即时通信系统Openfire分析之四:消息路由
两个人的孤独 两个人的孤独,大抵是,你每发出去一句话,都要经由无数网络.由几百个计算机处理后,出在他的面前,而他就在你不远处. 连接管理之后 Openfire使用MINA网络框架,并设置Connect ...
- 即时通信系统Openfire分析之六:路由表 RoutingTable
还是从会话管理说起 上一章,Session经过预创建.认证之后,才正常可用.认证时,最重要的操作,就是将Session加入到路由表,使之拥用了通信功能. 添加到至路由表的操作,是在SessionMan ...
- 即时通信系统Openfire分析之八:集群管理
前言 在第六章<路由表>中,客户端进行会话时,首先要获取对方的Session实例.获取Session实例的方法,是先查找本地路由表,若找不到,则通过路由表中的缓存数据,由定位器获取. 路由 ...
- 即时通信系统Openfire分析之一:Openfire与XMPP协议
引言 目前互联网产品使用的即时通信协议有这几种:即时信息和空间协议(IMPP).空间和即时信息协议(PRIM).针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)以及XMPP.PRIM与 ...
- 即时通信系统Openfire分析之七:集群配置
前言 写这章之前,我犹豫了一会.在这个时候提集群,从章节安排上来讲,是否合适?但想到上一章<路由表>的相关内容,应该不至于太突兀.既然这样,那就撸起袖子干吧. Openfire的单机并发量 ...
- 即时通信系统Openfire分析之二:主干程序分析
引言 宇宙大爆炸,于是开始了万物生衍,从一个连人渣都还没有的时代,一步步进化到如今的花花世界. 然而沧海桑田,一百多亿年过去了…. 好复杂,但程序就简单多了,main()函数运行,敲个回车,一行Hel ...
- tomcat会话之持久化会话管理器
前面提到的标准会话管理器已经提供了基础的会话管理功能,但在持久化方面做得还是不够,或者说在某些情景下无法满足要求,例如把会话以文件或数据库形式存储到存储介质中,这些都是标准会话管理器无法做到的,于是另 ...
- Openfire分析之三:ConnectionManager 连接管理(1)
Openfire是怎么实现连接请求的? XMPPServer.start()方法,完成Openfire的启动.但是,XMPPServer.start()方法中,并没有提及如何监听端口,那么Openfi ...
随机推荐
- docker的简单搭建(java/tomcat 环境)
1.一副图简单了解下docker的布局,它是虚拟的,docker分为私服.镜像.容器三个模块 一般从私服pull镜像,镜像run一个容器,我们把容器作为一个虚拟服务,里面可以独立运行进程有独立的内网I ...
- 团队作业4——第一次项目冲刺 SeCOnd DaY
项目冲刺--Double Kill 喂喂喂,你好你好,听得见吗?这里是天霸动霸.tua广播站,我是主播小学生¥-¥ 第一次敏捷冲刺平稳的度过了第一天,第一天的任务大家也圆满完成啦[拍手庆祝],那么今天 ...
- 201521123013 《Java程序设计》第4周学习总结
1. 本章学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. 1.多态是面向对象的三大特性之一.多态的意思:相同的形态,可以实不同的行为.Java中实现多 ...
- 201521123078《java程序设计》第四次总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. 继承是面向对象最显著的一个特性.继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性 ...
- 201521123105 第四周Java学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. 继承与多态的概念与实现父类与之类的关系解决代码复用的办法 2. 书面作业 2.1 将在网上商 ...
- 201521123122《Java程序设计》第1周学习总结
1. 本周学习总结 因为寒假里也没有好好预习java,第一周上课还是有点懵逼. ①.了解了JVM,JRE,JDK的区别,能够熟练安装JDK. ②.编了我人生中的第一个java程序"hello ...
- 201521123080《Java程序设计》第11周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 多线程: 操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己 ...
- 201521123020 《Java程序设计》第9周学习总结
1.本周学习总结 2. 书面作业 1.常用异常 题目5-1 1.1 截图你的提交结果(出现学号) 1.2 自己以前编写的代码中经常出现什么异常.需要捕获吗(为什么)?应如何避免? 答:数组越界:不需要 ...
- JS中有关分支结构、循环结构以及函数应用的一些简单练习
案例一:搬桌子 年龄大于七岁男女都可以搬桌子,年龄小于七岁大于五岁的男生可以搬桌子: var num =parseInt(prompt("请输入你的年龄")) var sex ...
- Java项目生成Jar文件
打开 Jar 文件向导 Jar 文件向导可用于将项目导出为可运行的 jar 包. 打开向导的步骤为: 在 Package Explorer 中选择你要导出的项目内容.如果你要导出项目中所有的类和资源, ...