我們都知道,WCF支持Duplex的消息交換模式,它允許在service的執行過程中實現對client的回調。WCF這種雙向通信的方式是我們可以以Event Broker或者訂閱/發布的方式來定義和調用WCF Service。今天我們就給大家一個具體的例子:通過WCF的duplex communication方式現在Session管理。
1、Session 管理提供的具體功能
我們的例子實現了下面一些Session Management相關的功能:
Start/End Session:可以調用service開始一個新的Session或者結束掉一個現有的Session。當開始一個Session的時候,service根據client端傳入的client相關的信息(ClientInfo),創建一個SessionInfo對象,該對象由一個GUID類型的SessionID唯一標識,代表一個具體的Client和Service的Session。在service端,通過一個dictionary維護者一個當前所有的active session列表,key為SessionID,value是SessionInfo對象。當client調用相應的service,傳入對應的SessionID,該SessionID對應的SessionInfo從該session列表中移除。
Session Timeout:如同ASP.NET具有一個Timeout的時間一樣,我們的例子也具有timeout的機制。在client可以注冊timeout事件,某個session timeout,service會通過在start session中指定的callback回調相應的操作(OnTimeout)並處罰client注冊的timeout事件。session timeout後,SessionInfo對象從active session列表中移除。 比如在本例中,我們通過注冊事件使得timeout後,程序在顯示timeout message之後,自動退出。
Session Renew:session timeout判斷的依據是client最後活動的時間(last activity time),而該事件反映的是最後一次鼠標操作的時間(假設我們的client是一個GUI應用)。所以從session的生命管理來講,用戶的每次鼠標操作實際上將session的時間延長到session timeout的時間。
Session Listing Viewing:Administrator或者某個具有相應權限的用戶,可以查看當前活動的session列表和session相關的信息,比如IP地址、主機名稱、用戶名、session開始的時間和最後一次活動的時間,見下圖。
Session Killing:如何發現某個用戶正在做一些不該做的事情,或者發現當前的並發量太大,管理員可以強行殺掉某個正在活動的Session。同session timeout一樣,client端可以注冊session killed事件。當session被強行中止後,service回調client相應的方法(OnSessionKilled),觸發該事件。比如在本例中,我們通過注冊事件使得某個client對應的session被殺掉後,該client程序在顯示message之後,自動退出。
2、Session Timeout的實現原理
在該例子中,最重要的是如何實現timeout的功能,而該功能的核心在於如何探測session的狀態(Active、Timeout、Killed)。一般地我們有兩種截然不同的方式來實現這樣的功能:
I、客戶端驅動:這是大多數人會想得到的方式,通過這樣的方式實現session status的檢測功能:如下圖所示,client端調用相應的service開始一個session,並獲得SessionID。client端每隔一定的時間調用相應的操作(CheckSessionStatus),並將自己的SessionID傳入,進行session status的檢測(步驟1),根據返回的狀態進行相應的處理;用戶的鼠標操作將會調用相應的操作(RenewSession)將session的last active time修正為service端的當前時間(不應該是client的時間)(步驟2)。然而,不可能每次鼠標操作都進行service的調用,這樣會頻繁的調用service調用肯定會使程序不堪重負。所以會一般會設置一個service調用的時間間隔,也就是在一定的時間端內,只有一次鼠標操作會觸發service的調用。由於CheckSessionStatus和RenewSession的調用都是基於某個時間間隔的,所以實時性是怎麼也解決不了的。此外,這種形如輪詢方式的機制在高並發的情況下也會讓service端的壓力正大。
II、服務端驅動:設計服務端驅動模型是從.NET Remoting的remote instance生命周期管理機制得到的靈感。我們知道和WCF3種InstanceContext Mode(PerCall、PerSession和Single)相對應,Remoting也具有3種不同的對象激活方式(Object Activation):SingleCall、CAO(client activated object)和Singleton。SingleCall和Singleton是兩個極端,不需要特殊的對象回收機制,而CAO模式下,Remoting采用了一種基於“租約”(lease)的service instance 生命周期管理機制:remote object被一個租約一個“租約”(lease:實現了System.Runtime.Remoting.Lifetime.ILease interface)對象引用。client端通過一個Sponsor( System.Runtime.Remoting.Lifetime.ISponsor)引用lease對象. 當Lease Manager檢測到某個remote object的lease超時,Remoting不會馬上對其進行垃圾回收,而是找到該lease的Sponsor對象,通過Sponsor對象回調Renewal方法(Sponsor處於client端),返回一個Timespan對象,表明需要將remote object的lifetime延長的時間,如何該值小於或者等於零,則不需要延長,該對象將會被回收掉;否則將lifetime延長至相應的時間。同時,client的每次遠程調用,都會自動實現對lifetime的Renew功能。(詳細內容可以參考我的文章:[原創]我所理解的Remoting (2) :遠程對象的生命周期管理-Part II)
我們實現與此相似的Session Management的功能,具體的流程如下圖所示:
步驟一:client端調用Guid StartSession(SessionClientInfo clientInfo, out TimeSpan timeout)方法,其中SessionClientInfo 表述client的一些基本的信息,比如IP地址、主機名稱、用戶名等等。service端接收到請求後,創建一個SessionInfo對象,該對象代表一個具體基於某個client的session,並同通過一GUID形式的SessionID唯一標識。同時將此SessionClientInfo 對象加入到表示當前所有活動的Session列表中,該列表通過一個dictionary表示(IDictionary<Guid, SessionInfo> CurrentSessionList),其中key是SessionID。最後service將SessionID和session timeout的時間返回到client端。
此外,client調用StartSession,除了指定SessionClientInfo 之外,還提供了一個Callback對象,Callback用在service在相應的時機(session輪詢、session timeout,kill session)實現對client的回調,下面是3個主要的callback操作:
TimeSpan Renew():對Session生命周期的延長。
void OnSessionKilled(SessionInfo sessionInfo):當client對應的session被殺掉之後,調用該方法實現實時通知。
void OnSessionTimeout(SessionInfo sessionInfo):當client對應的session timeout後,調用該方法實現實時通知。
除了維護一個當前活動session的列表之外,service還維護一個Callback列表(IDictionary<Guid, ISessionCallback> CurrentCallbackList),key仍然是SessionID。當StartSession被調用後,callback被加入到CurrentCallbackList中。
步驟二:service以一定的時間間隔對session列表進行輪詢(polling),根據SessionClientInfo的最後活動時間(LastActivityTime)和session timeout的時間判斷是否需要renew session(DateTime.Now - sessionInfo.LastActivityTime 〉 Timeout)。考慮到對實時性的要求,對於列表中每個session的狀態檢查都是通過異步的方式同時進行的。
步驟三:如何需要進行session renewal,則通過SessionID,從callback列表中找出與此對應的callback對象,調用Renew方法,並返回一個Timespan類型的值,如何該值大於零,表明需要延長session的生命周期,則將SessionInfo的LastActivityTime 加上該值;
步驟四: 當Renew方法返回Timespan小於或者等於零,表明session真正timeout,則調用callback對象的OnSessionTimeout通知client端session timeout。
步驟五:該步驟和上面的步驟二、三、四並沒時間上的先後順序。他的主要功能是,維護一個反映真正最後活動時間的全局變量,每個鼠標操作都將此值設為當前時間(這個通過注冊MouseMove事件很容易實現)。對於Renew方法的返回值,就是通過此全局變量和session timeout時間(通過StartSession獲得)計算得到:Timeout - (DateTime.Now - LastActivityTime)。
注:可能有人會說,為什麼不將LastActivityTime返回到service端,service將session的LastActivityTime設定成該值就可以了呀?實際上,這樣做依賴於這樣的一個假設:client端的時間和server端的時間是一致的。很顯然,我們不能作出這樣的假設。
3、整個應用的結構
在介紹具體實現之前,我們先來了解一下整個solution的總體結構:
我依然采用我常用的4層結構(Contract、Service、Hosting和Client),其中client采用一個windows application來模擬客戶端。熟悉我文章的人應該對這個結果有一定的了解了,在這裡就不多做介紹了。
4、Data Contract、Service Contract和Callback Contract
我們先來定義一些抽象層的東西Contract, 通過這些contract你會對提供的功能有一個大致的了解,首先來看看在client和service端傳輸的數據的定義:
I、Client的:SessionClientInfo
namespace Artech.SessionManagement.Contract
{
[DataContract]
public class SessionClientInfo
{
[DataMember]
public string IPAddress
{ get; set; }
[DataMember]
public string HostName
{ get; set; }
[DataMember]
public string UserName
{ get; set; }
[DataMember]
public IDictionary<string, string> ExtendedProperties
{ get; set; }
}
}
定義了一個描述述client的基本信息:IP地址、主機名稱、用戶名,同時定義了一個用於保存額外信息的ExtendedProperties。
II、Session的描述:SessionInfo
namespace Artech.SessionManagement.Contract
{
[DataContract]
[KnownType(typeof(SessionClientInfo))]
public class SessionInfo
{
[DataMember]
public Guid SessionID
{ get; set; }
[DataMember]
public DateTime StartTime
{ get; set; }
[DataMember]
public DateTime LastActivityTime
{get;set;}
[DataMember]
public SessionClientInfo ClientInfo
{ get; set; }
public bool IsTimeout
{ get; set; }
}
}
定義了Session的基本信息:Session的ID、開始的時間、最後一次活動的時間、客戶端基本信息以及表明Session是否Timeout的Flag。
III、Callback Contract:ISessionCallback
namespace Artech.SessionManagement.Contract
{
public interface ISessionCallback
{
[OperationContract]
TimeSpan Renew();
[OperationContract(IsOneWay = true)]
void OnSessionKilled(SessionInfo sessionInfo);
[OperationContract(IsOneWay = true)]
void OnSessionTimeout(SessionInfo sessionInfo);
}
}
Renew()通過獲得Session需要延長的時間;OnSessionKilled和OnSessionTimeout實現Session被殺掉和Timeout時的實時通知。
IV、ServiceContract:ISessionManagement
namespace Artech.SessionManagement.Contract
{
[ServiceContract(CallbackContract = typeof(ISessionCallback))]
public interface ISessionManagement
{
[OperationContract]
Guid StartSession(SessionClientInfo clientInfo, out TimeSpan timeout);
[OperationContract]
void EndSession(Guid sessionID);
[OperationContract]
IList<SessionInfo> GetActiveSessions();
[OperationContract]
void KillSessions(IList<Guid> sessionIDs);
}
}
StartSession和EndSession用戶Session的啟動和中止,GetActiveSessions獲得當前所有活動的Sesssion列表,KillSessions用於強行結束一個或多個Session。
本文配套源碼