最近所做的一个项目需要用到的在线用户列表,上网搜索了一下发现现有的解决方案对用户意外退出的处理均不是太理想。一般来说,用户离开系统的方式有三种:主动注销、会话超时、直接关闭浏览器,对于前两种,我们很容易便可将该用户从在线列表中清除,关键是第三种(很多用户都是直接关闭窗口的~~郁闷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的类,存储单个活动用户数据。
  1. /// <summary>
  2. /// 单个在线用户数据,无法继承此类。
  3. /// </summary> 
  4. public sealed class ActiveUser
  5. {  
  6.   private readonly string _ticket;    //票据名称
  7.   private readonly string _username;   //登陆用户名
  8.   private readonly string _truename;   //登陆用户名
  9.   private readonly string _roleid;    //角色
  10.   private readonly DateTime _refreshtime;  //最新刷新时间
  11.   private readonly DateTime _activetime;  //最新活动时间
  12.   private readonly string _clientip;   //登陆IP
  13.   
  14.   public ActiveUser(string Ticket,string UserName,string TrueName,string RoleID,string ClientIP) {
  15.    this._ticket=Ticket;
  16.    this._username=UserName;
  17.    this._truename=TrueName;
  18.    this._roleid=RoleID;
  19.    this._refreshtime=DateTime.Now;
  20.    this._activetime=DateTime.Now;
  21.    this._clientip=ClientIP;
  22.   }
  23.   public ActiveUser(string Ticket,string UserName,string TrueName,string RoleID,DateTime RefreshTime,DateTime ActiveTime,string ClientIP)  {
  24.    this._ticket=Ticket;
  25.    this._username=UserName;
  26.    this._truename=TrueName;
  27.    this._roleid=RoleID;
  28.    this._refreshtime=RefreshTime;
  29.    this._activetime=ActiveTime;
  30.    this._clientip=ClientIP;
  31.   }
  32.   
  33.   public string Ticket  { get{return _ticket;}  }
  34.   public string UserName  { get{return _username;}  }
  35.   public string TrueName  { get{return _truename;}  }
  36.   public string RoleID  { get{return _roleid;}  }
  37.   public DateTime RefreshTime { get{return _refreshtime;} }
  38.   public DateTime ActiveTime { get{return _activetime;} }
  39.   public string ClientIP  { get{return _clientip;}  }//51aspx.com
  40. }
复制代码
2、 新建一个名为PassPort的类,存储在线用户列表。
  1. /// <summary>
  2. /// PassPort 存储在线用户列表。
  3. /// </summary>
  4. public class PassPort
  5. {
  6.   private  static  DataTable  _activeusers;
  7.   private  int  _activeTimeout;
  8.   private  int  _refreshTimeout;
  9.   /// <summary>
  10.   /// 初始化在线用户表。
  11.   /// </summary> 
  12.   private void userstableFormat()
  13.   {
  14.    if(_activeusers==null) {
  15.     _activeusers  =  new  DataTable("ActiveUsers");
  16.     DataColumn  myDataColumn;
  17.     System.Type mystringtype;
  18.     mystringtype = System.Type.GetType("System.String");
  19.     System.Type mytimetype;
  20.     mytimetype = System.Type.GetType("System.DateTime");
  21.     myDataColumn  =  new  DataColumn("Ticket",mystringtype);
  22.     _activeusers.Columns.Add(myDataColumn);
  23.     myDataColumn  =  new  DataColumn("UserName",mystringtype);
  24.     _activeusers.Columns.Add(myDataColumn);
  25.     myDataColumn  =  new  DataColumn("TrueName",mystringtype);
  26.     _activeusers.Columns.Add(myDataColumn);
  27.     myDataColumn  =  new  DataColumn("RoleID",mystringtype);
  28.     _activeusers.Columns.Add(myDataColumn);    
  29.     myDataColumn  =  new  DataColumn("RefreshTime",mytimetype);
  30.     _activeusers.Columns.Add(myDataColumn);
  31.     myDataColumn  =  new  DataColumn("ActiveTime",mytimetype);
  32.     _activeusers.Columns.Add(myDataColumn);
  33.     myDataColumn  =  new  DataColumn("ClientIP",mystringtype);
  34.     _activeusers.Columns.Add(myDataColumn);   
  35.    }
  36.   }
  37.   public PassPort()
  38.   {
  39.    userstableFormat(); //初始化在线用户表
  40.    //活动超时时间初始化 单位:分钟
  41.    try { _activeTimeout=int.Parse(ConfigurationSettings.AppSettings["ActiveTimeout"]); }
  42.    catch{ _activeTimeout=60; }   
  43.    //自动刷新超时时间初始化 单位:分钟
  44.    try { _refreshTimeout=int.Parse(ConfigurationSettings.AppSettings["RefreshTimeout"]); }
  45.    catch{ _refreshTimeout=1; }   
  46.   }
  47.   //全部用户列表
  48.   public  DataTable  ActiveUsers
  49.   {
  50.    get{return  _activeusers.Copy();}
  51.   }
  52.   
  53.   /// <summary>
  54.   /// 新用户登陆。
  55.   /// </summary>
  56.   public void Login(ActiveUser user,bool SingleLogin)
  57.   {
  58.    DelTimeOut();  //清除超时用户
  59.    if(SingleLogin){
  60.     //若是单人登陆则注销原来登陆的用户
  61.     this.Logout(user.UserName,false);
  62.    }
  63.    DataRow myRow;
  64.    try
  65.    {
  66.     myRow  =  _activeusers.NewRow();    
  67.     myRow["Ticket"]  =  user.Ticket.Trim();
  68.     myRow["UserName"]  =  user.UserName.Trim();
  69.     myRow["TrueName"]  =  ""+user.TrueName.Trim();
  70.     myRow["RoleID"]  =  ""+user.RoleID.Trim();
  71.     myRow["ActiveTime"]  =  DateTime.Now;
  72.     myRow["RefreshTime"]  =  DateTime.Now;
  73.     myRow["ClientIP"]  =  user.ClientIP.Trim();
  74.     _activeusers.Rows.Add(myRow);
  75.    }
  76.    catch(Exception  e)
  77.    {
  78.     throw(new  Exception(e.Message));
  79.    }  
  80.    _activeusers.AcceptChanges();
  81.    
  82.   }
  83.   /// <summary>
  84.   ///用户注销,根据Ticket或UserName。
  85.   /// </summary> 
  86.   private void Logout(string strUserKey,bool byTicket)
  87.   {
  88.    DelTimeOut();  //清除超时用户
  89.    strUserKey=strUserKey.Trim();
  90.    string  strExpr;   
  91.    strExpr =byTicket ? "Ticket=''" + strUserKey +"''" : "UserName=''" + strUserKey + "''";
  92.    DataRow[]  curUser;
  93.    curUser  =  _activeusers.Select(strExpr);
  94.    if  (curUser.Length  >0  )
  95.    {
  96.     for(int  i  =  0;  i  <  curUser.Length;  i  ++)
  97.     {
  98.      curUser[i].Delete();
  99.     }
  100.    }
  101.    _activeusers.AcceptChanges();   
  102.   }
  103.   /// <summary>
  104.   ///用户注销,根据Ticket。
  105.   /// </summary>
  106.   /// <param name="strTicket">要注销的用户Ticket</param>
  107.   public void Logout(string strTicket){
  108.    this.Logout(strTicket,true);
  109.   }
  110.   /// <summary>
  111.   ///清除超时用户。
  112.   /// </summary>
  113.   private  bool DelTimeOut()
  114.   {   
  115.    string  strExpr;   
  116.    strExpr = "ActiveTime < ''" + DateTime.Now.AddMinutes( 0 - _activeTimeout) + "''or RefreshTime < ''"+DateTime.Now.AddMinutes( 0 - _refreshTimeout)+"''";   
  117.    DataRow[]  curUser;
  118.    curUser  =  _activeusers.Select(strExpr);
  119.    if  (curUser.Length  >0  )
  120.    {
  121.     for(int  i  =  0;  i  <  curUser.Length;  i  ++)
  122.     {
  123.      curUser[i].Delete();     
  124.     }
  125.    }
  126.    _activeusers.AcceptChanges();
  127.    return  true;
  128.   }
  129.   /// <summary>
  130.   ///更新用户活动时间。
  131.   /// </summary>
  132.   public  void  ActiveTime(string  strTicket)
  133.   {
  134.    DelTimeOut();
  135.    string  strExpr;
  136.    strExpr  =  "Ticket=''"  +  strTicket  +  "''";  
  137.    DataRow[]  curUser;
  138.    curUser  =  _activeusers.Select(strExpr);
  139.    if  (curUser.Length  >0  )
  140.    {
  141.     for(int  i  =  0;  i  <  curUser.Length;  i  ++)
  142.     {
  143.      curUser[i]["ActiveTime"]=DateTime.Now;
  144.      curUser[i]["RefreshTime"]=DateTime.Now;
  145.     }
  146.    }
  147.    _activeusers.AcceptChanges();
  148.   }
  149.   /// <summary>
  150.   ///更新系统自动刷新时间。
  151.   /// </summary>
  152.   public  void  RefreshTime(string  strTicket)
  153.   {
  154.    DelTimeOut();
  155.    string  strExpr;
  156.    strExpr  =  "Ticket=''"  +  strTicket  +  "''";  
  157.    DataRow[]  curUser;
  158.    curUser  =  _activeusers.Select(strExpr);
  159.    if  (curUser.Length  >0  )
  160.    {
  161.     for(int  i  =  0;  i  <  curUser.Length;  i  ++)
  162.     {
  163.      curUser[i]["RefreshTime"]=DateTime.Now;
  164.     }
  165.    }
  166.    _activeusers.AcceptChanges();
  167.   }
  168.   private ActiveUser SingleUser(string strUserKey,bool byTicket)
  169.   {
  170.    strUserKey=strUserKey.Trim();
  171.    string  strExpr;
  172.    ActiveUser myuser;
  173.    strExpr =byTicket ? "Ticket=''" + strUserKey +"''" : "UserName=''" + strUserKey + "''"; 
  174.    DataRow[]  curUser;
  175.    curUser  =  _activeusers.Select(strExpr);
  176.    if  (curUser.Length  >0  )
  177. {
  178.     string myTicket=(string)curUser[0]["Ticket"];
  179.     string myUser=(string)curUser[0]["UserName"];
  180.     string myName=(string)curUser[0]["TrueName"];
  181.     string myRoleID=(string)curUser[0]["RoleID"];    
  182.     DateTime myActiveTime=(DateTime)curUser[0]["ActiveTime"];
  183.     DateTime myRefreshtime=(DateTime)curUser[0]["RefreshTime"];
  184.     string myClientIP =(string)curUser[0]["ClientIP"];
  185.     myuser=new ActiveUser(myTicket,myUser,myName,myRoleID,myActiveTime,myRefreshtime,myClientIP);  
  186.    }
  187.    else
  188.    {
  189.     myuser=new ActiveUser("","","","","");    
  190.    }
  191.    return  myuser;
  192.   }
  193.   /// <summary>
  194.   ///按Ticket获取活动用户。
  195.   /// </summary>
  196.   public ActiveUser SingleUser_byTicket(string strTicket)
  197.   {
  198.    return this.SingleUser(strTicket,true);
  199.   }
  200.   /// <summary>
  201.   ///按UserName获取活动用户。
  202.   /// </summary>
  203.   public ActiveUser SingleUser_byUserName(string strUserName)
  204.   {
  205.    return this.SingleUser(strUserName,false);
  206.   }
  207.   /// <summary>
  208.   ///按Ticket判断用户是否在线。
  209.   /// </summary>
  210.   public bool IsOnline_byTicket(string strTicket)
  211.   {
  212.    return (bool)(this.SingleUser(strTicket,true).UserName!="");
  213.   }
  214.   /// <summary>
  215.   ///按UserName判断用户是否在线。
  216.   /// </summary>
  217.   public bool IsOnline_byUserName(string strUserName)
  218.   {
  219.    return (bool)(this.SingleUser(strUserName,false).UserName!="");
  220.   }
  221. }
复制代码
3、 新建一个继承自PlaceHolder名为Refresh的类,执行更新自动刷新时间操作。
  1. /// <summary>
  2. /// Refresh 执行更新自动刷新时间操作。
  3. /// </summary>
  4. public class Refresh: PlaceHolder 
  5. {
  6.   /// <summary>
  7.   /// 设置存储Ticket的Session名称,默认为Ticket。
  8.   /// </summary>
  9.   public virtual string SessionName
  10.   {
  11.    get{
  12.     object obj1 = this.ViewState["SessionName"];
  13.     if (obj1 != null){ return ((string) obj1).Trim(); }
  14.     return "Ticket";
  15.    }
  16.    set{
  17.     this.ViewState["SessionName"] = value;
  18.    }
  19.   }
  20.   protected override void Render(HtmlTextWriter writer) 
  21.   {
  22.    string myTicket=(string)this.Page.Session[this.SessionName];
  23.    if(myTicket!=null)
  24.    {   
  25.     PassPort myPass = new PassPort();
  26.     myPass.RefreshTime(myTicket);
  27.     writer.Write("OK:"+DateTime.Now.ToString());
  28.    }
  29.    else{
  30.     writer.Write("Sorry:"+DateTime.Now.ToString());
  31.    }
  32.    base.Render(writer);
  33. }
  34. }
复制代码
4、 新建一个继承自PlaceHolder名为Script的类,生成执行xmlhttp的js脚本。。
  1. /// <summary>
  2. /// Script 生成执行xmlhttp的js脚本。
  3. /// </summary>
  4. public class Script: PlaceHolder
  5. {
  6.   /// <summary>
  7.   /// 设置js自动刷新的间隔时间,默认为25秒。
  8.   /// </summary>
  9.   public virtual int RefreshTime
  10.   {
  11.    get
  12.    {
  13.     object obj1 = this.ViewState["RefreshTime"];
  14.     if (obj1 != null){return int.Parse(((string) obj1).Trim());}
  15.     return 25;
  16.    }
  17.    set
  18.    {    
  19.     this.ViewState["RefreshTime"] = value;
  20.    }
  21.   }
  22.   protected override void Render(HtmlTextWriter writer) 
  23.   {
  24.    //从web.config中读取xmlhttp的访问地址
  25.    string refreshUrl=(string)ConfigurationSettings.AppSettings["refreshUrl"];
  26.    string scriptString = @" <script language=""JavaScript"">"+writer.NewLine;
  27.    scriptString += @"  window.attachEvent(""onload"", "+this.ClientID+@"_postRefresh);"+writer.NewLine;
  28.    scriptString += @"  var "+this.ClientID+@"_xmlhttp=null;"+writer.NewLine;
  29.    scriptString += @"  function "+this.ClientID+@"_postRefresh(){"+writer.NewLine;
  30.    scriptString += @"   var "+this.ClientID+@"_xmlhttp = new ActiveXObject(""Msxml2.XMLHTTP"");"+writer.NewLine;
  31.    scriptString += @"   "+this.ClientID+@"_xmlhttp.Open(""POST"", """+refreshUrl+@""", false);"+writer.NewLine;
  32.    scriptString += @"   "+this.ClientID+@"_xmlhttp.Send();"+writer.NewLine;
  33.    scriptString += @"   var refreshStr= "+this.ClientID+@"_xmlhttp.responseText;"+writer.NewLine;
  34.     
  35.    scriptString += @"   try {"+writer.NewLine;
  36.    scriptString += @"    var refreshStr2=refreshStr;"+writer.NewLine;
  37.    //scriptString += @"    alert(refreshStr2);"+writer.NewLine;
  38.    scriptString += @"   }"+writer.NewLine;
  39.    scriptString += @"   catch(e) {}"+writer.NewLine;
  40.    scriptString += @"   setTimeout("""+this.ClientID+@"_postRefresh()"","+this.RefreshTime.ToString()+@"000);"+writer.NewLine;
  41.    scriptString += @"  }"+writer.NewLine;
  42.    scriptString += @"<";
  43.    scriptString += @"/";
  44.    scriptString += @"script>"+writer.NewLine;
  45.    writer.Write(writer.NewLine);
  46.    writer.Write(scriptString);
  47.    writer.Write(writer.NewLine);
  48.    base.Render(writer);
  49.   }
  50. }
复制代码
注意以上四个类同属于一个名为OnlineUser的工程,他们的命名空间为OnlineUser,编译生成一个dll。 =============================================== 下面我简单介绍一下调用方法: 1、 新建一个名为OnlineUserDemo的asp.net web应用程序 2、 在vs的工具箱选项卡上右击,选择[添加/移除项],浏览定位到OnlineUser.dll,确定即可把Refresh 和Script添加到工具箱。 3、 把自动生成的WebForm1.aspx删除,并设置web.config
  1. <appSettings>
  2.    <add key="ActiveTimeout" value="30" />
  3.    <add key="RefreshTimeout" value="1" />
  4.    <add key="refreshUrl" value="refresh.aspx" />
  5. </appSettings>
复制代码
4、 添加一个名为Online.aspx的web窗体,给该窗体添加一个Script控件,一个DataGrid控件(id为DataGrid1),两个HyperLink控件(分别链接到login.aspx和logout.aspx,text属性分别设置为“登陆”和“注销”),调整好四个控件的位置,转到codebehind,在Page_Load中加入如下代码:
  1. string myTicket=(string)this.Page.Session["Ticket"];
  2.    if(myTicket!=null)
  3.    {
  4.     OnlineUser.PassPort myPassPort= new OnlineUser.PassPort();
  5.     if(myPassPort.IsOnline_byTicket(this.Session["Ticket"].ToString()))
  6.     {
  7.      myPassPort.ActiveTime(this.Session["Ticket"].ToString());
  8.      DataGrid1.DataSource=myPassPort.ActiveUsers;
  9.      DataGrid1.DataBind();
  10.     }
  11.     else{
  12.      //若在线用户列表中找不到当前用户,则定向到注销页面
  13.      Response.Redirect("Logout.aspx");
  14.     }
  15.    }
  16.    else{
  17.     Response.Redirect("Login.aspx");
  18.    }
复制代码
5、 添加一个名为login.aspx的web窗体,给该窗体添加一个label控件(id为Label1),设置text属性为“输入一个用户名”,再添加一个textbox控件(id为TextBox1)和一个button控件(id为Button1),调整好他们的位置,双击Button1控件转到codebehind,为Button1的Click事件加入如下代码:
  1. if(TextBox1.Text.Trim()=="")
  2.    {
  3.     //不能为空
  4.     String scriptString = @"<script language=JavaScript>";
  5.     scriptString += @"alert(""输入一个用户名\n"");"; 
  6.     scriptString += @"history.go(-1);";
  7.     scriptString += @"<";
  8.     scriptString += @"/";
  9.     scriptString += @"script>";
  10.     if(!this.Page.IsStartupScriptRegistered("Startup"))
  11.      this.Page.RegisterStartupScript("Startup", scriptString);
  12.    }
  13.    else{
  14.     OnlineUser.PassPort myPassPort= new OnlineUser.PassPort();
  15.     string myTicket=DateTime.Now.ToString("yyyyMMddHHmmss");
  16.     string myUser=TextBox1.Text.Trim();
  17.     string myClintIP=this.Request.UserHostAddress;
  18.     this.Session["Ticket"]=myTicket;
  19.     OnlineUser.ActiveUser myActiveUser=new OnlineUser.ActiveUser(myTicket,myUser,myUser,"test",myClintIP);
  20.     myPassPort.Login(myActiveUser,true);
  21.     Response.Redirect("Online.aspx");
  22.    }
复制代码
6、 添加一个名为logout.aspx的web窗体,给该窗体添加一个HyperLink控件,指向login.aspx,text属性设置为“重登陆”转到codebehind,在Page_Load中加入如下代码:
  1. OnlineUser.PassPort myPassPort= new OnlineUser.PassPort();
  2.   myPassPort.Logout(this.Session["Ticket"].ToString());
  3. this.Session["Ticket"]="";
复制代码
7、 添加一个名为Refresh.txt的文本文件,设置其内容为:
  1. <%@ Register TagPrefix="cc2" Namespace="OnlineUser" Assembly="OnlineUser" %>
  2. <%@ Page %>
  3. <cc2:Refresh id="myRefresh" runat="server"></cc2:Refresh>
  4. 把Refresh.txt改名为Refresh.aspx
复制代码
8、 编译生成工程。 =============================================== 下面进行功能测试: 1、 打开浏览器,在地址栏输入 http://你机器的IP地址/onlineuserdemo/Login.aspx 2、 输入一个用户名(假设是test1)登陆,自动转到online.aspx页面 3、 找同网段的另外一台机器(设你的机器为a,这台机器为b),重复执行第一步。 4、 输入一个用户名(假设是test2)登陆,自动转到online.aspx页面51aspx 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三步正常,则大功告成,否则就再调试调试~~