這是轉載的別人的一篇,解決了困擾我已久的問題,雖然文章裡少了兩張圖,但是不影響閱讀。
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>