ASP.NET在线用户列表精确版——解决用户意外退出在线列表无法及时更新问题
我的大概思路是,给每在线用户增加一个RefreshTime属性,建立一个负责将当前用户的RefreshTime属性设置为当前时间的单独页面(Refresh.aspx),然后在系统的主要页面(也可以是所有页面)中通过xmlhttp不断地请求Refresh.aspx页面,一旦用户关闭了与本系统相关的所有窗口,即以直接关闭浏览器的方式退出系统,那么该用户的RefreshTime属性便不会自动更新了,我们再设置一个自动刷新的超时时间(这个要比会话超时短很多_refreshTimeout),当发现某用户超过_refreshTimeout的时间没有自动刷新,就能判定该用户已经以直接关闭浏览器的方式退出了。
假设我们设置会话超时时间为60分钟,自动刷新超时时间为1分钟,在客户端通过xmlhttp每隔25秒(之所以不设1分钟,是防止网速慢的时候访问Refresh.aspx超时,个人感觉,不一定正确)访问一次Refresh.aspx页面,在用户登陆、用户注销、检测用户是否在线的时候都执行清理超时用户(包括会话超时和自动刷新超时)操作,这样一来,在线用户列表的统计误差就由60分钟降至1分钟了。
==========================================
具体实现如下:
1、 新建一个名为ActiveUser的类,存储单个活动用户数据。
/// <summary>
/// 单个在线用户数据,无法继承此类。
/// </summary>
public sealed class ActiveUser
{
private readonly string _ticket; //票据名称
private readonly string _username; //登陆用户名
private readonly string _truename; //登陆用户名
private readonly string _roleid; //角色
private readonly DateTime _refreshtime; //最新刷新时间
private readonly DateTime _activetime; //最新活动时间
private readonly string _clientip; //登陆IP
public ActiveUser(string Ticket,string UserName,string TrueName,string RoleID,string ClientIP) {
this._ticket=Ticket;
this._username=UserName;
this._truename=TrueName;
this._roleid=RoleID;
this._refreshtime=DateTime.Now;
this._activetime=DateTime.Now;
this._clientip=ClientIP;
}
public ActiveUser(string Ticket,string UserName,string TrueName,string RoleID,DateTime RefreshTime,DateTime ActiveTime,string ClientIP) {
this._ticket=Ticket;
this._username=UserName;
this._truename=TrueName;
this._roleid=RoleID;
this._refreshtime=RefreshTime;
this._activetime=ActiveTime;
this._clientip=ClientIP;
}
public string Ticket { get{return _ticket;} }
public string UserName { get{return _username;} }
public string TrueName { get{return _truename;} }
public string RoleID { get{return _roleid;} }
public DateTime RefreshTime { get{return _refreshtime;} }
public DateTime ActiveTime { get{return _activetime;} }
public string ClientIP { get{return _clientip;} }
}
2、 新建一个名为PassPort的类,存储在线用户列表。
/// <summary>
/// PassPort 存储在线用户列表。
/// </summary>
public class PassPort
{
private static DataTable _activeusers;
private int _activeTimeout;
private int _refreshTimeout;
/// <summary>
/// 初始化在线用户表。
/// </summary>
private void userstableFormat()
{
if(_activeusers==null) {
_activeusers = new DataTable("ActiveUsers");
DataColumn myDataColumn;
System.Type mystringtype;
mystringtype = System.Type.GetType("System.String");
System.Type mytimetype;
mytimetype = System.Type.GetType("System.DateTime");
myDataColumn = new DataColumn("Ticket",mystringtype);
_activeusers.Columns.Add(myDataColumn);
myDataColumn = new DataColumn("UserName",mystringtype);
_activeusers.Columns.Add(myDataColumn);
myDataColumn = new DataColumn("TrueName",mystringtype);
_activeusers.Columns.Add(myDataColumn);
myDataColumn = new DataColumn("RoleID",mystringtype);
_activeusers.Columns.Add(myDataColumn);
myDataColumn = new DataColumn("RefreshTime",mytimetype);
_activeusers.Columns.Add(myDataColumn);
myDataColumn = new DataColumn("ActiveTime",mytimetype);
_activeusers.Columns.Add(myDataColumn);
myDataColumn = new DataColumn("ClientIP",mystringtype);
_activeusers.Columns.Add(myDataColumn);
}
}
public PassPort()
{
userstableFormat(); //初始化在线用户表
//活动超时时间初始化 单位:分钟
try { _activeTimeout=int.Parse(ConfigurationSettings.AppSettings["ActiveTimeout"]); }
catch{ _activeTimeout=60; }
//自动刷新超时时间初始化 单位:分钟
try { _refreshTimeout=int.Parse(ConfigurationSettings.AppSettings["RefreshTimeout"]); }
catch{ _refreshTimeout=1; }
}
//全部用户列表
public DataTable ActiveUsers
{
get{return _activeusers.Copy();}
}
/// <summary>
/// 新用户登陆。
/// </summary>
public void Login(ActiveUser user,bool SingleLogin)
{
DelTimeOut(); //清除超时用户
if(SingleLogin){
//若是单人登陆则注销原来登陆的用户
this.Logout(user.UserName,false);
}
DataRow myRow;
try
{
myRow = _activeusers.NewRow();
myRow["Ticket"] = user.Ticket.Trim();
myRow["UserName"] = user.UserName.Trim();
myRow["TrueName"] = ""+user.TrueName.Trim();
myRow["RoleID"] = ""+user.RoleID.Trim();
myRow["ActiveTime"] = DateTime.Now;
myRow["RefreshTime"] = DateTime.Now;
myRow["ClientIP"] = user.ClientIP.Trim();
_activeusers.Rows.Add(myRow);
}
catch(Exception e)
{
throw(new Exception(e.Message));
}
_activeusers.AcceptChanges();
}
/// <summary>
///用户注销,根据Ticket或UserName。
/// </summary>
private void Logout(string strUserKey,bool byTicket)
{
DelTimeOut(); //清除超时用户
strUserKey=strUserKey.Trim();
string strExpr;
strExpr =byTicket ? "Ticket=" + strUserKey +"" : "UserName=" + strUserKey + "";
DataRow[] curUser;
curUser = _activeusers.Select(strExpr);
if (curUser.Length >0 )
{
for(int i = 0; i < curUser.Length; i ++)
{
curUser[i].Delete();
}
}
_activeusers.AcceptChanges();
}
/// <summary>
///用户注销,根据Ticket。
/// </summary>
/// <param name="strTicket">要注销的用户Ticket</param>
public void Logout(string strTicket){
this.Logout(strTicket,true);
}
/// <summary>
///清除超时用户。
/// </summary>
private bool DelTimeOut()
{
string strExpr;
strExpr = "ActiveTime < " + DateTime.Now.AddMinutes( 0 - _activeTimeout) + "or RefreshTime < "+DateTime.Now.AddMinutes( 0 - _refreshTimeout)+"";
DataRow[] curUser;
curUser = _activeusers.Select(strExpr);
if (curUser.Length >0 )
{
for(int i = 0; i < curUser.Length; i ++)
{
curUser[i].Delete();
}
}
_activeusers.AcceptChanges();
return true;
}
/// <summary>
///更新用户活动时间。
/// </summary>
public void ActiveTime(string strTicket)
{
DelTimeOut();
string strExpr;
strExpr = "Ticket=" + strTicket + "";
DataRow[] curUser;
curUser = _activeusers.Select(strExpr);
if (curUser.Length >0 )
{
for(int i = 0; i < curUser.Length; i ++)
{
curUser[i]["ActiveTime"]=DateTime.Now;
curUser[i]["RefreshTime"]=DateTime.Now;
}
}
_activeusers.AcceptChanges();
}
/// <summary>
///更新系统自动刷新时间。
/// </summary>
public void RefreshTime(string strTicket)
{
DelTimeOut();
string strExpr;
strExpr = "Ticket=" + strTicket + "";
DataRow[] curUser;
curUser = _activeusers.Select(strExpr);
if (curUser.Length >0 )
{
for(int i = 0; i < curUser.Length; i ++)
{
curUser[i]["RefreshTime"]=DateTime.Now;
}
}
_activeusers.AcceptChanges();
}
private ActiveUser SingleUser(string strUserKey,bool byTicket)
{
strUserKey=strUserKey.Trim();
string strExpr;
ActiveUser myuser;
strExpr =byTicket ? "Ticket=" + strUserKey +"" : "UserName=" + strUserKey + "";
DataRow[] curUser;
curUser = _activeusers.Select(strExpr);
if (curUser.Length >0 )
{
string myTicket=(string)curUser[0]["Ticket"];
string myUser=(string)curUser[0]["UserName"];
string myName=(string)curUser[0]["TrueName"];
string myRoleID=(string)curUser[0]["RoleID"];
DateTime myActiveTime=(DateTime)curUser[0]["ActiveTime"];
DateTime myRefreshtime=(DateTime)curUser[0]["RefreshTime"];
string myClientIP =(string)curUser[0]["ClientIP"];
myuser=new ActiveUser(myTicket,myUser,myName,myRoleID,myActiveTime,myRefreshtime,myClientIP);
}
else
{
myuser=new ActiveUser("","","","","");
}
return myuser;
}
/// <summary>
///按Ticket获取活动用户。
/// </summary>
public ActiveUser SingleUser_byTicket(string strTicket)
{
return this.SingleUser(strTicket,true);
}
/// <summary>
///按UserName获取活动用户。
/// </summary>
public ActiveUser SingleUser_byUserName(string strUserName)
{
return this.SingleUser(strUserName,false);
}
/// <summary>
///按Ticket判断用户是否在线。
/// </summary>
public bool IsOnline_byTicket(string strTicket)
{
return (bool)(this.SingleUser(strTicket,true).UserName!="");
}
/// <summary>
///按UserName判断用户是否在线。
/// </summary>
public bool IsOnline_byUserName(string strUserName)
{
return (bool)(this.SingleUser(strUserName,false).UserName!="");
}
}
3、 新建一个继承自PlaceHolder名为Refresh的类,执行更新自动刷新时间操作。
/// <summary>
/// Refresh 执行更新自动刷新时间操作。
/// </summary>
public class Refresh: PlaceHolder
{
/// <summary>
/// 设置存储Ticket的Session名称,默认为Ticket。
/// </summary>
public virtual string SessionName
{
get{
object obj1 = this.ViewState["SessionName"];
if (obj1 != null){ return ((string) obj1).Trim(); }
return "Ticket";
}
set{
this.ViewState["SessionName"] = value;
}
}
protected override void Render(HtmlTextWriter writer)
{
string myTicket=(string)this.Page.Session[this.SessionName];
if(myTicket!=null)
{
PassPort myPass = new PassPort();
myPass.RefreshTime(myTicket);
writer.Write("OK:"+DateTime.Now.ToString());
}
else{
writer.Write("Sorry:"+DateTime.Now.ToString());
}
base.Render(writer);
}
}
4、 新建一个继承自PlaceHolder名为Script的类,生成执行xmlhttp的js脚本。。
/// <summary>
/// Script 生成执行xmlhttp的js脚本。
/// </summary>
public class Script: PlaceHolder
{
/// <summary>
/// 设置js自动刷新的间隔时间,默认为25秒。
/// </summary>
public virtual int RefreshTime
{
get
{
object obj1 = this.ViewState["RefreshTime"];
if (obj1 != null){return int.Parse(((string) obj1).Trim());}
return 25;
}
set
{
this.ViewState["RefreshTime"] = value;
}
}
protected override void Render(HtmlTextWriter writer)
{
//从web.config中读取xmlhttp的访问地址
string refreshUrl=(string)ConfigurationSettings.AppSettings["refreshUrl"];
string scriptString = @" <script language=""JavaScript"">"+writer.NewLine;
scriptString += @" window.attachEvent(""onload"", "+this.ClientID+@"_postRefresh);"+writer.NewLine;
scriptString += @" var "+this.ClientID+@"_xmlhttp=null;"+writer.NewLine;
scriptString += @" function "+this.ClientID+@"_postRefresh(){"+writer.NewLine;
scriptString += @" var "+this.ClientID+@"_xmlhttp = new ActiveXObject(""Msxml2.XMLHTTP"");"+writer.NewLine;
scriptString += @" "+this.ClientID+@"_xmlhttp.Open(""POST"", """+refreshUrl+@""", false);"+writer.NewLine;
scriptString += @" "+this.ClientID+@"_xmlhttp.Send();"+writer.NewLine;
scriptString += @" var refreshStr= "+this.ClientID+@"_xmlhttp.responseText;"+writer.NewLine;
scriptString += @" try {"+writer.NewLine;
scriptString += @" var refreshStr2=refreshStr;"+writer.NewLine;
//scriptString += @" alert(refreshStr2);"+writer.NewLine;
scriptString += @" }"+writer.NewLine;
scriptString += @" catch(e) {}"+writer.NewLine;
scriptString += @" setTimeout("""+this.ClientID+@"_postRefresh()"","+this.RefreshTime.ToString()+@"000);"+writer.NewLine;
scriptString += @" }"+writer.NewLine;
scriptString += @"<";
scriptString += @"/";
scriptString += @"script>"+writer.NewLine;
writer.Write(writer.NewLine);
writer.Write(scriptString);
writer.Write(writer.NewLine);
base.Render(writer);
}
}
注意以上四个类同属于一个名为OnlineUser的工程,他们的命名空间为OnlineUser,编译生成一个dll。
===============================================
下面我简单介绍一下调用方法:
1、 新建一个名为OnlineUserDemo的asp.net web应用程序
2、 在vs的工具箱选项卡上右击,选择[添加/移除项],浏览定位到OnlineUser.dll,确定即可把Refresh 和Script添加到工具箱。
3、 把自动生成的WebForm1.aspx删除,并设置web.config
<appSettings>
<add key="ActiveTimeout" value="30" />
<add key="RefreshTimeout" value="1" />
<add key="refreshUrl" value="refresh.aspx" />
</appSettings>
4、 添加一个名为Online.aspx的web窗体,给该窗体添加一个Script控件,一个DataGrid控件(id为DataGrid1),两个HyperLink控件(分别链接到login.aspx和logout.aspx,text属性分别设置为“登陆”和“注销”),调整好四个控件的位置,转到codebehind,在Page_Load中加入如下代码:
string myTicket=(string)this.Page.Session["Ticket"];
if(myTicket!=null)
{
OnlineUser.PassPort myPassPort= new OnlineUser.PassPort();
if(myPassPort.IsOnline_byTicket(this.Session["Ticket"].ToString()))
{
myPassPort.ActiveTime(this.Session["Ticket"].ToString());
DataGrid1.DataSource=myPassPort.ActiveUsers;
DataGrid1.DataBind();
}
else{
//若在线用户列表中找不到当前用户,则定向到注销页面
Response.Redirect("Logout.aspx");
}
}
else{
Response.Redirect("Login.aspx");
}
5、 添加一个名为login.aspx的web窗体,给该窗体添加一个label控件(id为Label1),设置text属性为“输入一个用户名”,再添加一个textbox控件(id为TextBox1)和一个button控件(id为Button1),调整好他们的位置,双击Button1控件转到codebehind,为Button1的Click事件加入如下代码:
if(TextBox1.Text.Trim()=="")
{
//不能为空
String scriptString = @"<script language=JavaScript>";
scriptString += @"alert(""输入一个用户名\n"");";
scriptString += @"history.go(-1);";
scriptString += @"<";
scriptString += @"/";
scriptString += @"script>";
if(!this.Page.IsStartupScriptRegistered("Startup"))
this.Page.RegisterStartupScript("Startup", scriptString);
}
else{
OnlineUser.PassPort myPassPort= new OnlineUser.PassPort();
string myTicket=DateTime.Now.ToString("yyyyMMddHHmmss");
string myUser=TextBox1.Text.Trim();
string myClintIP=this.Request.UserHostAddress;
this.Session["Ticket"]=myTicket;
OnlineUser.ActiveUser myActiveUser=new OnlineUser.ActiveUser(myTicket,myUser,myUser,"test",myClintIP);
myPassPort.Login(myActiveUser,true);
Response.Redirect("Online.aspx");
}
6、 添加一个名为logout.aspx的web窗体,给该窗体添加一个HyperLink控件,指向login.aspx,text属性设置为“重登陆”转到codebehind,在Page_Load中加入如下代码:
OnlineUser.PassPort myPassPort= new OnlineUser.PassPort();
myPassPort.Logout(this.Session["Ticket"].ToString());
this.Session["Ticket"]="";
7、 添加一个名为Refresh.txt的文本文件,设置其内容为:
<%@ Register TagPrefix="cc2" Namespace="OnlineUser" Assembly="OnlineUser" %>
<%@ Page %>
<cc2:Refresh id="myRefresh" runat="server"></cc2:Refresh>
把Refresh.txt改名为Refresh.aspx
8、 编译生成工程。
===============================================
下面进行功能测试:
1、 打开浏览器,在地址栏输入
http://你机器的IP地址/onlineuserdemo/Login.aspx
2、 输入一个用户名(假设是test1)登陆,自动转到online.aspx页面
3、 找同网段的另外一台机器(设你的机器为a,这台机器为b),重复执行第一步。
4、 输入一个用户名(假设是test2)登陆,自动转到online.aspx页面
5、 在b机器不断刷新online.aspx,若发现test1用户RefreshTime每过25秒自动更新一次而ActiveTime不变(这个时候a机器不要刷新页面啊),则证明a机器的自动刷新生效。
6、 在a机器不断刷新online.aspx,若发现test2用户RefreshTime每过25秒自动更新一次而ActiveTime不变(这个时候b机器不要刷新页面啊),则证明b机器的自动刷新生效。
7、 直接关闭一台机器(假设是a)上的online.aspx浏览窗口,在另一台机器(就是b啦)上刷新online.aspx,若发现1分钟后test1掉线在线用户只剩下test2,证明通过_refreshTimeout清除在线用户成功。
8、 若5、6、7三步正常,则大功告成,否则就再调试调试~~
==========================================================