【原创作者】:丛兴滋(cncxz)[E-mail:cncxz@126.com]
【关 键 词】:xmlhttp  ASP.NET在线用户列表  关闭浏览器  精确统计 单人登陆
【代码下载】:http://www.thisky.cn/down/onlineuser.rar

http://olu.desktopit.net/solution.aspx

    最近所做的一个项目需要用到的在线用户列表,上网搜索了一下发现现有的解决方案对用户意外退出的处理均不是太理想。一般来说,用户离开系统的方式有三种: 主动注销、会话超时、直接关闭浏览器,对于前两种,我们很容易便可将该用户从在线列表中清除,关键是第三种(很多用户都是直接关闭窗口的~~郁闷 ing),程序无法捕获窗口关闭的精确时间,只能等到会话超时后在能将该用户清除出在线列表,假设我们设置会话超时时间为60分钟,而用户登陆系统随便浏 览一个页面就以关闭浏览器的方式退出的话,我们要在将近1小时后才能从在线列表中将该用户清除出去(想象一下,系统显示n多人在线,可能除了你之外其他的 n-1人都关机走人了,汗一个先“`),而本文将尝试寻找一个解决方案把这种尴尬降至最低。
    我的大概思路是,给每在线用户增加一个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三步正常,则大功告成,否则就再调试调试~~

==========================================================

 

附:网上现有的一些在线人数统计的文章

ASP.NET 在线用户列表
http://www.mscenter.edu.cn/blog/jeffrey/archive/2005/03/25/1024.html
如何显示在线人数和所在位置
http://www.7880.com/Info/Article-40c2aca0.html
不用Golobal.asa和session实现在线人数统计
http://www.7880.com/Info/Article-31034d60.html 
利用文件属性结合Session实现在线人数统计
http://www.7880.com/Info/Article-2b369f40.html
网站当前的在线人数
http://www.7880.com/Info/Article-2a6d34c0.html
基于数据库的在线人数,日访问量等统计
http://www.7880.com/Info/Article-29f01580.html
网页在线人数统计的做法
http://www.7880.com/Info/Article-1d4ebf20.html
不用Global.asa实现在线人数统计
http://www.7880.com/Info/Article-1c09bc00.html 
如何实现网页在线人数统计
 http://www.7880.com/Info/Article-12bd5d00.html
两种统计当前在线人数的方法
http://www.7880.com/Info/Article-af79e0.html