JSP环境基于Session的在线用户统计深入分析 (1分)

  • 主题发起人 主题发起人 曹晓钢
  • 开始时间 开始时间

曹晓钢

Unregistered / Unconfirmed
GUEST, unregistred user!
JSP作为后起之秀能够在服务器编程环境中占据一定地位,是和它良好支持一系列业界标准
密切相关的。Session就是它提供的基础设施之一。作为一个程序员,你可以不介意具体在
客户端是如何实现,就方便的实现简单的基于session的用户管理。
现在对于处理在线用户,有几种不同的处理方法。
一种是叶面刷新由用户控制,服务器端控制一个超时时间比如30分钟,到了时间之后用户
没有动作就被踢出。这种方法的优点是,如果用户忘了退出,可以防止别人恶意操作。
缺点是,如果你在做一件很耗时间的事情,超过了这个时间限制,submit的时候可能要
再次面临登陆。如果原来的叶面又是强制失效的话,就有可能丢失你做的工作。在实现
的角度来看,这是最简单的,Server端默认实现的就是这样的模式。
另一种方式是,站点采用框架结构,有一个Frame或者隐藏的iframe在不断刷新,这样你
永远不会被踢出,但是服务器端为了判断你是否在线,需要定一个发呆时间,如果超过
这个发呆时间你除了这个自动刷新的叶面外没有刷新其他叶面的话,就认为你已经不在
线了。采取这种方式的典型是xici.net。 他的优点是可以可以利用不断的刷新实现一些
类似server-push的功能,比如网友之间发送消息。
不管哪一种模式,为了实现浏览当前所有的在线用户,还需要做一些额外的工作。
servlet API中没有得到Session列表的API。
可以利用的是Listener. Servlet 2.2和2.3规范在这里略微有一些不一样。
2.2中HttpSessionBindingListener可以实现当一个HTTPSession中的Attribute变化的
时候通知你的类。而2.3中还引入了HttpSessionAttributeListener.鉴于我使用的环境
是Visual age for java 4和JRun server 3.1,他们还不直接支持Servlet 2.3的编程,
这里我用的是HttpSessionBindingListener.
需要做的事情包括做一个新的类来实现HttpSessionBindingListener接口。这个接口有
两个方法:
public void valueBound(HttpSessionBindingEvent event),和
public void valueUnbound(HttpSessionBindingEvent event)。
当你执行Session.addAttribute(String,Object)的时候,如果你已经把一个实现了
HttpSessionBindingListener接口的类加入为Attribute,Session会通知你的类,调用
你的valueBound方法。相反,Session.removeAttribute方法对应的是valueUndound方法。
代码:
public class HttpSessionBinding implements javax.servlet.http.HttpSessionBindingListener 
{
	ServletContext application = null;
	public HttpSessionBinding(ServletContext application)
	{
		super();
		if (application ==null)
			throw new IllegalArgumentException("Null application is not accept.");
		
		this.application = application;
		
	}
	public void valueBound(javax.servlet.http.HttpSessionBindingEvent e) 
	{ 
		Vector activeSessions = (Vector) application.getAttribute("activeSessions");
		if (activeSessions == null)
		{
			activeSessions = new Vector();
		}
		JDBCUser sessionUser = (JDBCUser)e.getSession().getAttribute("user");
		if (sessionUser != null)
		{
			activeSessions.add(e.getSession());
		}
		application.setAttribute("activeSessions",activeSessions);
	}
	public void valueUnbound(javax.servlet.http.HttpSessionBindingEvent e) 
	{
		JDBCUser sessionUser = (JDBCUser)e.getSession().getAttribute("user");
		if (sessionUser == null)
		{
			Vector activeSessions = (Vector) application.getAttribute("activeSessions");
			if (activeSessions != null)
			{
				activeSessions.remove(e.getSession().getId());
				application.setAttribute("activeSessions",activeSessions);
			}
		}
	}
}
假设其中的JDBCUser类是一个任意User类。
在执行用户登录时,把User类和HttpSessionBinding类都加入到Session中去。
这样,每次用户登录后,在application中的attribute "activeSessions"这个vector中
都会增加一条记录。
每当session超时,valueUnbound被触发,在这个vector中删去将要被超时的session.
代码:
public void login()
throws ACLException,SQLException,IOException
{
	/* get JDBC User Class */
	if (user != null)
	{
		logout();
	}
	{
		// if session time out, or user didn't login, save the target url temporary.
		JDBCUserFactory uf = new JDBCUserFactory();
		if ( (this.request.getParameter("userID")==null) 
			|| (this.request.getParameter("password")==null)  )
		{
			throw new ACLException("Please input a valid userName and password.");
		}
		
		JDBCUser user = 
			(JDBCUser) uf.UserLogin(
				this.request.getParameter("userID"),
				this.request.getParameter("password") );
		user.touchLoginTime();
		this.session.setAttribute("user",user);
		this.session.setAttribute("BindingNotify",new HttpSessionBinding(application));
	}
}
Login的时候,把User和这个BindingNotofy目的的类都加入到session中去。
logout的时候,就要主动在activeSessions这个vector中删去这个session.
代码:
public void logout()
throws SQLException,ACLException
{
	if (this.user == null 
		&&
this.session.getAttribute("user")==null)
	{
		return;
	}
	Vector activeSessions = (Vector) this.application.getAttribute("activeSessions");
	if (activeSessions != null)
	{
		activeSessions.remove(this.session);
		application.setAttribute("activeSessions",activeSessions);
	}
	java.util.Enumeration e = this.session.getAttributeNames();
	while (e.hasMoreElements())
	{
		String s = (String)e.nextElement();
		this.session.removeAttribute(s);
	}
	this.user.touchLogoutTime();
	this.user = null;
}
这两个函数位于一个HttpSessionManager类中.这个类引用了jsp里面的application全局
对象。
这个类的其他代码和本文无关且相当长,我就不贴出来了。
下面来看看jsp里面怎么用。
假设一个登录用的表单被提交到doLogin.jsp, 表单中包含UserName和password域。
节选部分片段:
代码:
<%
	HttpSessionManager hsm = new HttpSessionManager(application,request,response);
	try
	{
		hsm.login();
	}
	catch ( UserNotFoundException e)
	{
		response.sendRedirect("InsufficientPrivilege.jsp?detail=User%20does%20not%20exist.");
		return;
	}
	catch (	InvalidPasswordException e2)
	{	
		response.sendRedirect("InsufficientPrivilege.jsp?detail=Invalid%20Password");
		return;
	}
	catch ( Exception e3)
	{
	%>	Error:<%=e3.toString() %><br>
		Press <a href="login.jsp">Here</a> to relogin.
<%		return;
	}
	response.sendRedirect("index.jsp");
%>
再来看看现在我们怎么得到一个当前在线的用户列表。
代码:
<body bgcolor="#FFFFFF">
<table cellspacing="0" cellpadding="0" width="100%">
	<tr >
	  <td style="width:24px">SessionId
	  </td>
	  <td style="width:80px" >User
	  </td>
	  <td style="width:80px" >Login Time
	  </td>
	  <td style="width:80px" >Last access Time
	  </td>
	</tr>
<%
	Vector activeSessions = (Vector) application.getAttribute("activeSessions");
	if (activeSessions == null)
	{
		activeSessions = new Vector();
		application.setAttribute("activeSessions",activeSessions);
	}
	
	Iterator it = activeSessions.iterator();
	while (it.hasNext())
	{
		HttpSession sess = (HttpSession)it.next();
		JDBCUser sessionUser = (JDBCUser)sess.getAttribute("user");
		String userId = (sessionUser!=null)?sessionUser.getUserID():"None";
%>
	<tr>
	  	<td nowrap=''><%= sess.getId() %></td>
	  	<td nowrap=''><%= userId %></td>
	  	<td nowrap=''>
		<%=  BeaconDate.getInstance( new java.util.Date(sess.getCreationTime())).getDateTimeString()%></td>
	  	<td class="<%= stl %>3" nowrap=''>
		<%=  BeaconDate.getInstance( new java.util.Date(sess.getLastAccessedTime())).getDateTimeString()%></td>
	</tr>
<%
	}
%>
</table>
</body>
以上的代码从application中取出activeSessions,并且显示出具体的时间。其中
BeaconDate类假设为格式化时间的类。
这样,我们得到了一个察看在线用户的列表的框架。至于在线用户列表分页等功能,
与本文无关,不予讨论。
这是一个非刷新模型的例子,依赖于session的超时机制。我的同事sonymusic指出很
多时候由于各个厂商思想的不同,这有可能是不可信赖的。考虑到这种需求,需要在
每个叶面刷新的时候都判断当前用户距离上次使用的时间是否超过某一个预定时间值。
这实质上就是自己实现session超时。
如果需要实现刷新模型,就必须使用这种每个叶面进行刷新判断的方法。
曹晓钢
2001年12月3日
本文版权属于南京丰柏信息技术有限公司。保留一切权力。
(若不保留此版权信息,请勿转载)
 
谢谢!大开眼界啊。请容学生提问:
你讲述了在 Servlet 2.2 (不用 2.3 的 HttpSessionBindingListener)的条件下利用
Session 统计在线用户。您的前提是:session 的超时机制和一个用户表。
问题一:匿名用户(不验证密码的访客)如何处理?
问题二:非刷新模型比刷新模型的优越在什么地方?
刷新模型有两种,一种是定时刷新,就是您描述的frame+发呆时间,这个没有多大意思。
另外一种是访问刷新,这个还有点意思,因为附加功能是可以显示用户在干什么,而且还不
依赖于不可靠的 session 的超时机制。大富翁为访问刷新模型,做法是:建个在线用户表,
用户 login 就写进去,logout 就删除,匿名的以 IP 为用户名,在加个时间字段,每访问
一个页就记录用户的时间、IP,定义用户时间与当前时间之差小于8分钟为在线。
 
说得对。
问题一:匿名用户(不验证密码的访客)如何处理?
匿名用户仍然是有session的。所以基本框架仍然成立。不同的只是这里的User对象是一个不
存在的临时对象而已。
问题二:非刷新模型比刷新模型的优越在什么地方?
他们没有优劣之分。这篇文章的目的是在与可以得出一个当前在线用户列表。我并不关心
用户超时由何种方式触发。我做成非刷新模式,只不过是我的客户要求要能实现类似于windows
的发呆自动踢出的功能。所以我一开始就没有考虑刷新模型。
其实从实用的角度来说,为了不依赖于Application server的超时机制,还是应该每调用
一个叶面就去刷新时间。
关于这篇文章中提到的,还有一点要说明一下。这里有个关键在于,如果用户没有主动logout,
在session超时的时候,他把自己从application的一个全局在线session列表中清除出去。
要保证这一点,必须注意User对象应该在实现了BindingListener的这个对象之前被卸载。否则,
这个对象的valueUnbound中if (sessionUser == null) 的条件就不能成立。
实际上这一点依赖于Application server的实现。我的实验是在JRun 3.1下成立的,不能保证其
他AS也成立。
所以,如果要把这篇文章修改完善一点,至少要做一些修改:提供一个Custom Tag用于每个叶面
之中调用,来刷新Session; 在显示用户列表的时候要做一次检查,如果某个session超时过长,
就自动删除。
这样,这个Listener好像就没有什么用处了??困惑中...
 
感到矛盾了?再请教您客户要求的“发呆自动踢出的功能”具体是什么意思?
 
As my understnding, if you set a session idle time, the appliction server would
handle it, you needn't write code to kick the timeout client out.
besides, I remember you can get the session list of server side. there are a method
called getSessionContext or something.
 
but in Servlet 2.2's JavaDoc:
getSessionContext
public HttpSessionContext getSessionContext()Deprecated.
As of Version 2.1, this method is deprecated and has no replacement.
It will be removed in a future version of the Java Servlet API.
 
发呆自动踢出,就是说过用户最后一次访问server 30分钟之后,session超时。
 
高处不胜寒啊,努力学习中..........................................
 
类似的程序我做过,JSP+BEAN,处于公司已申请著作权,不便公开
我当时做过两种,一种是基于SERVLET API 2.1 以上版本
写了个几个BEAN专门处理SESSION,通过数据库验证过滤不需要的SESSION
实现了一些功能很类似QQ
另一个针对HttpSessionContext对象(API 2.1以上不支持)可以取得列表的功能
更直接地实现了相同的功能,JSP更短
同时页面刷新时间更短,更改SESSION过期时间更短,基本表现了在线用户实时状态
大家可以多交流(冠群联想软件有限公司牟啸天xiaotian.mu@cajvc.com)

 
赫赫,这就可以申请著作权了?
其实做QQ什么的我一点都不感兴趣,我只是想尽量把这个部分做的完善...
这个题目来源于我和sonymusic的一次争论。
现在看起来根本就不需要Listener,在这里application server本身的
session超时机制完全可以被放弃,
我们利用session只是利用它为区分每个用户。
实现起来不会很复杂的。
 
to supermxt:
有没有更进一步的建议?或者,在你实现的时候,有什么更好的思路?
 
哈哈,如果这么一点就申请著作权,那专利局不累死才怪
我说的只是其中的一个附加的模块,完整的软件是个B/S的销售管理系统
我实现这个部分时利用了SESSION机制,但在JSP里将过期时间改成较小
用BEAN保存SESSION信息(由数据库验证通过的合法用户的SESSION)
用很短的时间刷新,造成类实时的现象,以实现在线交流
咱们两个实现的基本方式一样,只是我只用了valueUnbound清除保存的信息同时写了较多的类
因为我固定使用TOMCAT因此所谓的不可信任问题不存在

 
那你是用一个隐藏帧刷新的罗?
 
本来是各位高手的讨论,菜鸟的我不该插嘴。有不对的地方,见笑了。[:D]
我想html有个onload事件,当用户关闭了浏览器时,可以执行一段代码,但好象只有javascript才可以。
这段代码可以执行把数据库中的登入的人员名单删除。
当然在登入时把人员名单写入数据库。这样可以不要服务器发消息,来验正是否离线了吧。
致于刷新问题,我想可以只刷新要显示的数据。不是整个页面,这样速度会快。
<script language="javascript" src='./ls.php'></script>
<html>
<body onunload="mdel();">
<SCRIPT language="JavaScript">
function mdel()
{
window.open("./mydel.php","","toolbar=0,menubar=0,width=1,height=1");
}
</body>
</html>
好象不用open方法直接把mydel.php内容写出来不行,php代码不在onload发生时,
也会执行。从而失去了效用了。
望高手指教了。
 
是的,我用隐藏帧刷新,就象你的第二个方法
jbas兄弟说得是
这也是一个好办法,用SUBMIT也可以
我曾写过这样的东东
但容易出现名单数据库与实际不一致的问题
主要是由于非法退出线路/不稳定/断线等情况使名单数据库中有垃圾项目没有清除
所以在商业化的程序中还是信任SESSION比较妥当
 
1、软件和文章一样,其著作权不是申请得来的,软件创作完成,
自然拥有著作权,我国实行的是软件登记制度,这种登记在
维护著作权时,可以成为有力的证据;
(嘻嘻,这是坐在我身边的法律专家---ppmm说的)
2、我们公司的软件申请的时候,假模假样地交了一堆头文件上去
(申请知识产权需要交xx页的源代码,我倒,居然用“页”),
而且北京区分软件是否独创的办法是:你交xx页(打印的)源
代码公开,如果没有人有意见,就ok了;
3、他们申请著作权的动机是为了成为软件企业----少缴税......

手痒难忍,写一句。
我在做CE上的软件,ce上不能用Delphi,我等到花儿也谢了......
 
热烈祝贺茶叶蛋归来!
[:)][:)][:)][:)][:)][:)]
 
后退
顶部