1、在线用户列表的实现
在ASP时代,要实现一个网站的在线用户列表显示功能的惯用做法是修改global.asa文件中的:Application_Start、Session_Start和Session_End这三个函数。在ASP.NET时代,我依然这样做。但是必须注意很多问题。首先来看看最简单的代码实现:
protected void Application_Start(Object sender, EventArgs e)
{
Application.Lock();
Application["OnlineUsers"]=null;
Application.UnLock();
}
protected void Session_Start(Object sender, EventArgs e)
{
Application.Lock();
if(Application["OnlineUsers"]==null)
Application["OnlineUsers"]=new Hashtable();
Hashtable onlineUsersHash=(Hashtable)Application["OnlineUsers"];
onlineUsersHash.Add(Request.UserHostAddress, Request.Cookies["UserName"].Value);
Application.UnLock();
}
protected void Session_End(Object sender, EventArgs e)
{
Hashtable onlineUsersHash=(Hashtable)Application["OnlineUsers"];
onlineUsersHash.Remove(Request.UserHostAddress);
}
这就是一个简单的能实现记录在线用户列表的代码。呵呵,简单吧?你可以传到服务器上去试试!如果你和我一样,看到自己的用户名已经出现在列表中,就欢呼雀跃地告诉许多网友很简单就实现了一个在线用户列表显示功能,然后就关了机器去睡觉了的话,那么第二天清晨你会大吃一惊!你的网站上的在线用户列表中的人名会多的数不清,而且你会知道其实那些人根本就不在线上… 哦,真是个灾难!算法思想没有任何错误,但是却得出错误的结果,为什么呢?虽然是一个小小的功能,但是却隐藏了许多玄机,这个就要看你是否能解开了…
首先我要肯定一点,用Hashtable这样的数据结构来存储在线用户的名称的确是个不错的选择。主键使用用户的IP地址,主键值存放用户名称。因为网络中的IP地址是唯一的,所以用它来充当主键时对的。出现上述错误的原因是在Session_End函数中,Hashtable没有将主键删除掉?!
我想所有初学者都会和我一样,会问:既然IP地址在整个网络中是唯一的,那为什么还无法删除Hashtable中的键呢?答案是:Hashtable没有找到主键名,即用户的IP地址:Request.UserHostAddress!这听起来真是笑话,用户IP地址怎么会找不到呢?只要用户登陆Internet,就必有IP地址!它怎么会找不到呢?
我告诉你,原因是:用户根本就没有登陆Internet!
什么在线,又不在线的?我想你现在应该已经晕了… 不过,看了下面的图例,我想你就明白了…
如果觉得示意图有些小,可以调整显示比例(Word菜单 à 视图 à 显示比例)。
左图是假设一个用户先登录Bincess论坛,而后就去了WadeLau.org(WadeLau.net)这个网站。但是他一直没有断开连接,一直都在Internet上。而当AfritXia.net的服务器结束了用户的Session时,就会调用:
protected void Session_End(Object sender, EventArgs e)
{
Hashtable onlineUsersHash=(Hashtable)Application["OnlineUsers"];
onlineUsersHash.Remove(Request.UserHostAddress);
}
来清除在线用户列表中的用户名称,这样做是对的!
而右图,则是用户在AfritXia.net服务器结束用户的Session之前就已经断开连接离开Internet了。那么服务器端在获取用户的IP地址时,会是什么结果呢?我也不知道会是什么结果,但总之,肯定不是我们想要的结果,也不会出现在onlineUsersHash数据结构中主键名称中。
就是这样,由于找不到主键名称,所以onlineUsersHash就无法移除对应的值,所以就出现了已经离线的人的名字还出现在在线用户列表中(这样的情况被我称之为:僵尸)。
只要知道问题所在,那么就能想出解决办法。对于这个问题来说,不幸的是它已经被发现了。那么解决它的对策也就很快地被制定出来了。在新的算法中采用SessionID来作为主键来记录用户登录信息。但是还有很多问题需要注意!例如:一个打开的IE浏览器,服务器会给它分配一个SessionID,但是再次打开一个新的IE浏览器,服务器照旧还是要给它分配一个SessionID。这就是说同一个用户、同一个PC机、同一个IP地址,服务器却给它分配了多个Session。IE对服务器的请求能力也太强了?!因为一个开启的IE浏览器在计算机里就是一个进程,服务器给客户端的一个进程分配Session乃是天经地义。而如果是使用MyIE,它是MDI程序,不管开多少个子窗口,都只是属于一个进程。所以对于MyIE,ASP.NET只给了它一个Session。注意!这也是为什么在MyIE中时而会出现在线用户0人的一个原因,虽然你还在线上。还有个问题,例如:一个用户刚登陆Bincess不久,就因为线路故障掉下线去了。可是没过多久他就回来了,而此时他的Cookie还没过期,但是IP地址和SessionID全变了。而如果只考虑用SessionID来记录在线用户列表的话,对于这种情况就会出现一个用户名称出现两次的尴尬。还是以一个示意图来说明新算法的情况:
示意图中的SessionID_1和SessionID_2说明UserName_1开启了两个IE窗体。
建立两个个哈希表结构OnlineUsersHash和OnlineUsers_SessionIPHash,当用户访问Bincess时,会为他分配一个SessionID。令用户的IP地址和用户名称建立一个一一对应的关系。如果用户开启了新的窗口,则检查用户的IP地址或用户名是否已经在OnlineUsersHash中出现过?如果出现过,就让新的SessionID指向现有的IP地址。而当一个Session结束时,则将该SessionID从OnlineUsers_SessionIPHash中移除。判断是否还有其他SessionID指向这个IP地址,如果没有,那么从在线用户列表中移除用户名称。客户端的情况相当复杂,必须要考虑周全。下面则是新的算法的代码:
// 在Global.asax.cs 文件中
//
// 在线用户列表主键名
public const string KEY_ONLINEUSERS="OnlineUsers";
// 在线用户列表 Session 表主键名
public const string KEY_ONLINEUSERS_SESSIONIP="OnlineUsers_SessionIP";
protected void Application_Start(Object sender, EventArgs e)
{
Application.Lock();
Application[KEY_ONLINEUSERS]=null;
Application[KEY_ONLINEUSERS_SESSIONIP]=null;// 目的是将用户的SessionID和IP对应起来
Application.UnLock();
}
protected void Session_Start(Object sender, EventArgs e)
{
Application.Lock();
/* ... */
Hashtable onlineUsersHash=(Hashtable)Application[KEY_ONLINEUSERS];
Hashtable onlineUsersSessionIPHash=(Hashtable)Application[KEY_ONLINEUSERS_SESSIONIP];
if(Visitor.Current.IsGuest)// 如果用户是来宾
{
if(onlineUsersHash.ContainsKey(Request.UserHostAddress))
{
onlineUsersHash[Request.UserHostAddress]="";
}
else
{
onlineUsersHash.Add(Request.UserHostAddress, "");
}
}
else
{
if(!onlineUsersHash.ContainsKey(Request.UserHostAddress)
&& !onlineUsersHash.ContainsValue(Visitor.Current.UserName))
{
// 如果用户的 IP 地址和用户名称在列表中找不到,则将添加在线用户列表中
onlineUsersHash.Add(Request.UserHostAddress, Request.Cookies[″UserName″].Value);
}
else if(onlineUsersHash.ContainsValue(Request.Cookies[“UserName”].Value))
{
// 如果用户的 Cookie 信息能够找到,则更新(先删除再添加)在线用户的 IP 地址
//
// 说明:用户可能刚登陆不久,便因为线路故障,断线并重新拨号
// 而当用户回到网站时,用户的 Cookie 还未过期,但是 IP 地址却发生了改变
string userName=Request.Cookies[″UserName″].Value;
foreach(object key in onlineUsersHash.Keys)
{
if(((string)onlineUsersHash[key]).Equals(userName))
{
// 删除用户刚才使用过的 IP 地址
onlineUsersHash.Remove(key);
break;
}
}
// 添加在线用户
onlineUsersHash.Add(Request.UserHostAddress, Request.Cookies[″UserName″].Value);
}
else if(onlineUsersHash.ContainsKey(Request.UserHostAddress))
{
// 如果用户的 IP 地址能找到,则更新在线用户的名称
//
// 说明:用户登录后,注销并重新登陆。可能是去换个用户名
onlineUsersHash[Request.UserHostAddress]=Request.Cookies[″UserName″].Value;
}
}
// 将用户的 IP 地址和 SessionID 对应起来
if(!onlineUsersSessionIPHash.ContainsKey(Session.SessionID))
onlineUsersSessionIPHash.Add(Session.SessionID, Request.UserHostAddress);
Application.UnLock();
}
protected void Session_End(Object sender, EventArgs e)
{
Application.Lock();
if(Application[KEY_ONLINEUSERS]!=null)
{
Hashtable onlineUsersHash=(Hashtable)Application[KEY_ONLINEUSERS];
Hashtable onlineUsersSessionIPHash=(Hashtable)Application[KEY_ONLINEUSERS_SESSIONIP];
// 获取用户的IP地址
string IP=(string)onlineUsersSessionIPHash[Session.SessionID];
// 移除用户的IP地址
onlineUsersSessionIPHash.Remove(Session.SessionID);
// 如果没有一个Session指向这个IP了,则说明这个用户确实已经离开了网站
// 可以删除该用户的用户名称了
if(!onlineUsersSessionIPHash.ContainsValue(IP))
onlineUsersHash.Remove(IP);
}
Application.UnLock();
}
有很多其它的在线用户列表的算法,但多半都是要借助数据库才可以。CSDN上的一个网友写了一个比较精确的算法,是通过记录用户每次最后活动的时间来定时地、不断刷地新DataSet的做法实现的。我的算法是另一种思想的算法!
看了一下yangzixp(扬子(四川·巴中),原理基本一致,不同的是你使用的FORMS身份验证,而且可以改进 -- 每次Application_AuthenticateRequest就检查并删除超时用户,肯定是太频繁了,改用Timer吧
总的来说,要做个在线人数统计简单,但是要做在线名单并且保存用户的访问日志,就需要耗费比较多的系统资源,是否划算就难说了(我只看需求文档,其他不管...);
前面用过的IHttpModule方法也不错,原先每用过,也学了一招...
感谢思归老大的帮忙,分就散了吧~
using System;
using System.ComponentModel;
using System.Web;
using System.Web.SessionState;
using System.Data;
using System.Data.OleDb;
namespace XsExam
{
/// <summary>
/// Global 的摘要说明。
/// </summary>
public class Global : System.Web.HttpApplication
{
private static System.Threading.Timer timer;
private const int interval = 1000 * 60 * 10;//检查在线用户的间隔时间
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
public Global()
{
InitializeComponent();
}
protected void Application_Start(Object sender, EventArgs e)
{
if (timer == null)
timer = new System.Threading.Timer(new System.Threading.TimerCallback(ScheduledWorkCallback),
sender, 0, interval);
DataTable userTable = new DataTable();
userTable.Columns.Add("UserID");//用户ID
userTable.Columns.Add("UserName");//用户姓名
userTable.Columns.Add("FirstRequestTime");//第一次请求的时间
userTable.Columns.Add("LastRequestTime");//最后一次请求的时间
userTable.Columns.Add("ClientIP");//
userTable.Columns.Add("ClientName");//
userTable.Columns.Add("ClientAgent");//
//userTable.Columns.Add("LastRequestPath");//最后访问的页面
userTable.PrimaryKey = new DataColumn[]{userTable.Columns[0]};
userTable.AcceptChanges();
Application.Lock();
Application["UserOnLine"] = userTable;
Application.UnLock();
}
protected void Session_Start(Object sender, EventArgs e)
{
}
protected void Application_BeginRequest(Object sender, EventArgs e)
{
}
protected void Application_EndRequest(Object sender, EventArgs e)
{
}
protected void Application_AcquireRequestState(Object sender, EventArgs e)
{
HttpApplication mApp = (HttpApplication)sender;
if(mApp.Context.Session == null) return;
if(mApp.Context.Session["UserID"]==null ) return;
string userID = mApp.Context.Session["UserID"].ToString();
DataTable userTable = (DataTable)Application["UserOnLine"];
DataRow curRow = userTable.Rows.Find(new object[]{userID});
if(curRow != null)
{
this.GetDataRowFromHttpApp(mApp,ref curRow);
}
else
{
DataRow newRow = userTable.NewRow();
this.GetDataRowFromHttpApp(mApp,ref newRow);
userTable.Rows.Add(newRow);
}
userTable.AcceptChanges();
Application.Lock();
Application["UserOnLine"] = userTable;
Application.UnLock();
}
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
}
protected void Application_Error(Object sender, EventArgs e)
{
}
protected void Session_End(Object sender, EventArgs e)
{
}
protected void Application_End(Object sender, EventArgs e)
{
}
#region Web 窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要使用代码编辑器修改
/// 此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
}
#endregion
private void GetDataRowFromHttpApp(HttpApplication mApp,ref DataRow mRow)
{
if(mApp.Context.Session == null) return;
if(mApp.Context.Session["UserID"]==null || mApp.Context.Session["UserName"]==null) return;
string userID = mApp.Context.Session["UserID"].ToString();
string userName = mApp.Context.Session["UserName"].ToString();
//string requestPath = mApp.Request.Path;
if(mRow["UserID"].ToString().Length<1)
{
mRow["UserID"] = userID;
mRow["UserName"] = userName;
mRow["FirstRequestTime"] = System.DateTime.Now;
mRow["ClientIP"] = mApp.Context.Request.UserHostAddress;
mRow["ClientName"] = mApp.Context.Request.UserHostName;
mRow["ClientAgent"] = mApp.Context.Request.UserAgent;
}
mRow["LastRequestTime"] = System.DateTime.Now;
//mRow["LastRequestPath"] = requestPath;
}
private void ScheduledWorkCallback (object sender)
{
string filter = "Convert(LastRequestTime,'System.DateTime') < Convert('" + System.DateTime.Now.AddSeconds(-interval/1000).ToString() + "','System.DateTime')";
DataTable userTable = (DataTable)Application["UserOnLine"];
DataRow[] lineOutUsers = userTable.Select(filter);
for(int i=0;i<lineOutUsers.Length;i++)
{
DataRow curRow = lineOutUsers[i];
//保存到数据库
XsStudio.Database db = new XsStudio.Database();
curRow.Delete();
}
userTable.AcceptChanges();
Application.Lock();
Application["UserOnLine"] = userTable;
Application.UnLock();
}
}
}
按照思归老大的提点,修改方案如下:
使用IHttpModule,加分讨论
首先创建实现IHttpModule接口的类MyModule:
using System;
using System.Web;
using System.Data;
namespace Test2004_5_13
{
public class MyModule : IHttpModule
{
public void Init(HttpApplication application)
{
application. AcquireRequestState += (new
EventHandler(this.Application_AcquireRequestState));
}
private void Application_AcquireRequestState (Object source,
EventArgs e)
{
HttpApplication mApplication = (HttpApplication)source;
HttpResponse Response=mApplication.Context.Response;
DataTable dt = null;
if(mApplication.Context.Application["UserOnLine"] != null)
{
dt = (DataTable)mApplication.Context.Application["UserOnLine"];
}
else
{
dt = new DataTable();
dt.Columns.Add("UserName");
dt.Columns.Add("FirstLoadTime");
dt.Columns.Add("LastLoadTime");
}
//将当前用户添加到在线用户列表
if(mApplication.Context.Session!=null)
{
if(mApplication.Context.Session["UserID"]!=null)
{
string userName = mApplication.Context.Session["UserID"].ToString();
DataRow[] rows = dt.Select("UserName='" + userName + "'");
if(rows.Length>0)
rows[0][2] = System.DateTime.Now.ToString();
else
dt.Rows.Add(new object[]{userName,System.DateTime.Now.ToString(),System.DateTime.Now.ToString()});
}
}
dt.AcceptChanges();
mApplication.Context.Application["UserOnLine"] = dt;
Response.Write("Beginning of Request" + dt.Rows.Count.ToString());
}
public void Dispose()
{
}
}
}
2)在web.config中注册
<httpModules>
<add type="Test2004_5_13.MyModule,Test2004_5_13" name="MyModule"/>
</httpModules>