5、Session Management Service的實現
現在我們來看看Session Management真正的實現,和我以前的例子不同,我不是把所有的實現都寫在WCF service上,而是定義了另一個class來實現所有的業務邏輯:SessionManager。我們分析一下具體的實現邏輯。
namespace Artech.SessionManagement.Service
{
public static class SessionManager
{
private static object _syncHelper = new object();
internal static TimeSpan Timeout
{ get; set; }
public static IDictionary<Guid, SessionInfo> CurrentSessionList
{ get; set; }
public static IDictionary<Guid, ISessionCallback> CurrentCallbackList
{ get; set; }
static SessionManager()
{
string sessionTimeout = ConfigurationManager.AppSettings["SessionTimeout"];
if (string.IsNullOrEmpty(sessionTimeout))
{
throw new ConfigurationErrorsException("The session timeout application setting is missing");
}
double timeoutMinute;
if (!double.TryParse(sessionTimeout, out timeoutMinute))
{
throw new ConfigurationErrorsException("The session timeout application setting should be of doubdle type.");
}
Timeout = new TimeSpan(0, 0, (int)(timeoutMinute * 60));
CurrentSessionList = new Dictionary<Guid, SessionInfo>();
CurrentCallbackList = new Dictionary<Guid, ISessionCallback>();
}
… … … … … … … … … … … … … … … …… … … … … … … … … … … … … … … …
}
}
首先來看Field、Property和static constructor的定義。_syncHelper 用於實現多線程同步之用;Timeout是session timeout的時間,可配置;CurrentSessionList和CurrentCallbackList兩個dictionary在上面我們已經作過介紹,分別代表當前活動的session列表和callback列表,key均為SessionID。在靜態構造函數中,初始化session timeout的時間,和實例化CurrentSessionList和CurrentCallbackList。
接著我們來看看StartSession和EndSession兩個方法,這兩個方法分別代表Session的開始和結束。
public static Guid StartSession(SessionClientInfo clientInfo)
{
Guid sessionID = Guid.NewGuid();
ISessionCallback callback = OperationContext.Current.GetCallbackChannel<ISessionCallback>();
SessionInfo sesionInfo = new SessionInfo() { SessionID = sessionID, StartTime = DateTime.Now, LastActivityTime = DateTime.Now, ClientInfo = clientInfo };
lock (_syncHelper)
{
CurrentSessionList.Add(sessionID, sesionInfo);
CurrentCallbackList.Add(sessionID, callback);
}
return sessionID;
}
public static void EndSession(Guid sessionID)
{
if (!CurrentSessionList.ContainsKey(sessionID))
{
return;
}
lock (_syncHelper)
{
CurrentCallbackList.Remove(sessionID);
CurrentSessionList.Remove(sessionID);
}
}
在StartSession方法中,首先創建一個GUID作為SessionID。通過OperationContext.Current獲得callback對象,並根據client端傳入的SessionClientInfo 對象創建SessionInfo 對象,最後將callback對象和SessionInfo 對象加入CurrentCallbackList和CurrentSessionList中。由於這兩個集合會在多線程的環境下頻繁地被訪問,所以在對該集合進行添加和刪除操作時保持線程同是顯得尤為重要,所在在本例中,所有對列表進行添加和刪除操作都需要獲得_syncHelper加鎖下才能執行。與StartSession相對地,EndSession方法僅僅是將SessionID標識的callback對象和SessionInfo 對象從列表中移除。
然後我們來看看如何強行中止掉一個或多個活動的session:KillSessions。
public static void KillSessions(IList<Guid> sessionIDs)
{
lock (_syncHelper)
{
foreach (Guid sessionID in sessionIDs)
{
if (!CurrentSessionList.ContainsKey(sessionID))
{
continue;
}
SessionInfo sessionInfo = CurrentSessionList[sessionID];
CurrentSessionList.Remove(sessionID);
CurrentCallbackList[sessionID].OnSessionKilled(sessionInfo);
CurrentCallbackList.Remove(sessionID);
}
}
}
邏輯很簡單,就是先從CurrentSessionList中獲得對應的SessionInfo 對象,然後將其從CurrentSessionList中移除,然後根據SessionID獲得對用的Callback對象,調用OnSessionKilled方法實時通知client session被強行中止,最後將callback對象從CurrentCallbackList中清楚。需要注意的是OnSessionKilled是One-way方式調用的,所以是異步的,時間的消耗可以忽略不計,也不會拋出異常,所以對_syncHelper的鎖會很開釋放,所以不會對並發造成太大的影響。
Session的管理最終要、也是作復雜的事對Timeout的實現,再我們的例子中,我們通過定期對CurrentSessionList中的每個session進行輪詢實現。每次輪詢通過RenewSessions方法實現,我們來看看該方法的定義:
[MethodImpl(MethodImplOptions.Synchronized)]
public static void RenewSessions()
{
IList<WaitHandle> waitHandleList = new List<WaitHandle>();
foreach (var session in CurrentSessionList)
{
RenewSession renewsession = delegate(KeyValuePair<Guid, SessionInfo> sessionInfo)
{
if (DateTime.Now - sessionInfo.Value.LastActivityTime < Timeout)
{
return;
}
try
{
TimeSpan renewDuration = CurrentCallbackList[sessionInfo.Key].Renew();
if (renewDuration.TotalSeconds > 0)
{
sessionInfo.Value.LastActivityTime += renewDuration;
}
else
{
sessionInfo.Value.IsTimeout = true;
CurrentCallbackList[session.Key].OnSessionTimeout(sessionInfo.Value);
}
}
catch (CommunicationObjectAbortedException)
{
sessionInfo.Value.IsTimeout = true;
return;
}
};
IAsyncResult result = renewsession.BeginInvoke(session, null, null);
waitHandleList.Add(result.AsyncWaitHandle);
}
if (waitHandleList.Count == 0)
{
return;
}
WaitHandle.WaitAll(waitHandleList.ToArray<WaitHandle>());
ClearSessions();
}
public delegate void RenewSession(KeyValuePair<Guid, SessionInfo> session);
首先我定義了一個delegate:RenewSession,來實現表示對單個session的renew操作。在RenewSessions方法中,我們遍歷CurrentSessionList中的每個SessionInfo對象,根據LastActivityTime判斷是否需要對該Session進行Renew操作(DateTime.Now - sessionInfo.Value.LastActivityTime < Timeout,意味著單單從server來看,Session都尚未過期),如何需要,則通過SessionID從CurrentCallbackList中取出callback對象,調用Renew方法。如何返回的的Timespan大於零,則表明,client端需要延長session的生命周期,則讓LastActivityTime 加上該值。如何返回的值小於零,表明session真的過期了,那麼通過調用callback對象的OnSessionTimeout方法實現對client的實時的通知,並將SessionInfo對象的IsTimeout 設置為true。等所以得操作結束之後,在將IsTimeout 為true的SessionInfo對象和對應的callback對象從列表中移除。
在這裡有3點需要注意:
1)由於在client過多的情況下,CurrentSessionList得數量太多,按照同步的方式逐個進行狀態的檢測、callback的調用可以需要很長的時間,會嚴重影響實時性。所以我們采用的是異步的方式,這是通過將操作定義到RenewSession delegate中,並掉用BeginInvoke方法實現的。
2)在調用Callback的Renew方法的時候,很有可以client端的程序已經正常或者非正常關閉,在這種情況下會拋出CommunicationObjectAbortedException異常,我們應該把這種情況視為timeout。所以我們也將IsTimeout 設置為true。
3)我們之所以現在遍歷之後才對session進行清理,主要考慮到我們的操作時在對線程環境中執行,如何在並發操作的情況下對集合進行刪除,會出現一些意想不到的不同步情況下。我們通過WaitHandle保證所有的並發操作都結束了:我先創建了一個IList<WaitHandle>對象waitHandleList ,將每個基於session對象的異步操作的WaitHandle添加到該列表(waitHandleList.Add(result.AsyncWaitHandle);)通過WaitHandle.WaitAll(waitHandleList.ToArray<WaitHandle>());保證所有的操作都結束了。
有了SessionManager,我們的Service就顯得很簡單了:
namespace Artech.SessionManagement.Service
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode =ConcurrencyMode.Multiple)]
public class SessionManagementService:ISessionManagement
{
#region ISessionManagement Members
public Guid StartSession(SessionClientInfo clientInfo,out TimeSpan timeout)
{
timeout = SessionManager.Timeout;
return SessionManager.StartSession(clientInfo);
}
public void EndSession(Guid sessionID)
{
SessionManager.EndSession(sessionID);
}
public IList<SessionInfo> GetActiveSessions()
{
return new List<SessionInfo>(SessionManager.CurrentSessionList.Values.ToArray<SessionInfo>());
}
public void KillSessions(IList<Guid> sessionIDs)
{
SessionManager.KillSessions(sessionIDs);
}
#endregion
}
}
基本上就是調用SessionManager對應的方法。
6、Service Hosting
在Artech.SessionManagement.Hosting.Program中的Main()方法中,實際上是做了兩件事情:
I、對SessionManagementService的Host。
II、通過Timer對象實現對Session列表的定期(5s)輪詢。
namespace Artech.SessionManagement.Hosting
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost host = new ServiceHost(typeof(SessionManagementService)))
{
host.Opened += delegate
{
Console.WriteLine("The session management service has been started up!");
};
host.Open();
Timer timer = new Timer(
delegate { SessionManager.RenewSessions(); }, null, 0, 5000);
Console.Read();
}
}
}
}
這是configuration,除了system.serviceModel相關配置外,還定義了配置了session timeout的時間,單位為”分”:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="SessionTimeout" value="0.5"/>
</appSettings>
<system.serviceModel>
<services>
<service name="Artech.SessionManagement.Service.SessionManagementService">
<endpoint binding="netTcpBinding" bindingConfiguration="" contract="Artech.SessionManagement.Contract.ISessionManagement" />
<host>
<baseAddresses>
<add baseAddress="net.tcp://127.0.0.1:9999/sessionservice" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
7、如何定義Client
這個service的實現已經完成,我們最後來介紹如何根據service的特點來定義我們的client程序了。我們的client是一個GUI應用(WinForm)。為了簡便,我們把所有的邏輯定義在一個facade class上面:SessionUtility。
namespace Artech.SessionManagement.Client
{
public static class SessionUtility
{
static SessionUtility()
{
Callback = new SessionCallback();
Channel = new DuplexChannelFactory<ISessionManagement>(Callback, "sessionservice").CreateChannel();
}
private static ISessionManagement Channel
{ get; set; }
private static ISessionCallback Callback
{ get; set; }
public static DateTime LastActivityTime
{ get; set; }
public static Guid SessionID
{ get; set; }
public static TimeSpan Timeout
{ get; set; }
public static void StartSession(SessionClientInfo clientInfo)
{
TimeSpan timeout;
SessionID = Channel.StartSession(clientInfo, out timeout);
Timeout = timeout;
}
public static IList<SessionInfo> GetActiveSessions()
{
return Channel.GetActiveSessions();
}
public static void KillSessions(IList<Guid> sessionIDs)
{
Channel.KillSessions(sessionIDs);
}
}
}
SessionUtility定義了連個public property:SessionID代表當前session的ID,Timeout代表Session timeout的時間,這兩個屬性都在StartSession中被初始化,而LastActivityTime代表的是最後一次用戶交互的時間。上面的代碼和簡單,在這裡就不多作介紹了。這裡需要著重介紹我們的Callback class:
public class SessionCallback : ISessionCallback
{
#region ISessionCallback Members
public TimeSpan Renew()
{
return SessionUtility.Timeout - (DateTime.Now - SessionUtility.LastActivityTime);
}
public void OnSessionKilled(SessionInfo sessionInfo)
{
MessageBox.Show("The current session has been killed!", sessionInfo.SessionID.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Information);
Application.Exit();
}
public void OnSessionTimeout(SessionInfo sessionInfo)
{
MessageBox.Show("The current session timeout!", sessionInfo.SessionID.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Information);
Application.Exit();
}
#endregion
}
Renew()方法根據Timeout 和LastActivityTime計算出需要對該session延長的時間;OnSessionKilled和OnSessionTimeout在通過MessageBox顯示相應的message後將程序退出。
我們簡單簡單一下本例子提供的client application。具有一個Form。我們把所有的功能集中在該Form中:開始一個新session、獲得所有的活動的session列表、強行中止一個或多個Session。
這是StartSession按鈕的click event handler:
private void buttonStartSession_Click(object sender, EventArgs e)
{
string hostName = Dns.GetHostName();
IPAddress[] ipAddressList = Dns.GetHostEntry(hostName).AddressList;
string ipAddress = string.Empty;
foreach (IPAddress address in ipAddressList)
{
if (address.AddressFamily == AddressFamily.InterNetwork)
{
ipAddress += address.ToString() + ";";
}
}
ipAddress = ipAddress.TrimEnd(";".ToCharArray());
string userName = this.textBoxUserName.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
return;
}
SessionClientInfo clientInfo = new SessionClientInfo() { IPAddress = ipAddress, HostName = hostName, UserName = userName };
SessionUtility.StartSession(clientInfo);
this.groupBox2.Enabled = false;
}
獲得當前PC的主機名稱和IP地址,連同輸入的user name創建SessionClientInfo 對象,調用SessionUtility的StartSession開始新的Session。
“Get All Active Session”,獲取當前所有的活動的session,綁定到Datagrid:
private void buttonGet_Click(object sender, EventArgs e)
{
IList<SessionInfo> activeSessions = SessionUtility.GetActiveSessions();
this.dataGridViewSessionList.DataSource = activeSessions;
foreach (DataGridViewRow row in this.dataGridViewSessionList.Rows)
{
Guid sessionID = (Guid)row.Cells["SessionID"].Value;
row.Cells["IPAddress"].Value = activeSessions.Where(session=> session.SessionID == sessionID).ToList<SessionInfo>()[0].ClientInfo.IPAddress;
row.Cells["UserName"].Value = activeSessions.Where(session => session.SessionID == sessionID).ToList<SessionInfo>()[0].ClientInfo.UserName;
}
}
“Kill Selected Session”按鈕被點擊,強行中止選中的Session:
private void buttonKill_Click(object sender, EventArgs e)
{
IList<Guid> sessionIDs = new List<Guid>();
foreach ( DataGridViewRow row in this.dataGridViewSessionList.Rows)
{
if ((string)row.Cells["Select"].Value == "1")
{
Guid sessionID = new Guid(row.Cells["SessionID"].Value.ToString());
if (sessionID == SessionUtility.SessionID)
{
MessageBox.Show("You cannot kill your current session!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
sessionIDs.Add(sessionID);
}
}
SessionUtility.KillSessions(sessionIDs);
}
由於不能中止自己當前的Session,所以當選中的列表中包含自己的SessionID,會顯示一個messagebox提示不應該殺掉屬於自己session。
到這裡,實際上還有一件重要的事情沒有解決,那就是如何動態修正SessionUtility.LastActivityTime。我們希望的事SessionUtility.LastActivityTime能夠真正反映最後一次用戶交互的時間。為此我們遞歸地注冊每個control的MouseMove事件:
private void RegisterMouseMoveEvent(Control control)
{
control.MouseHover += delegate
{
SessionUtility.LastActivityTime = DateTime.Now;
};
foreach (Control child in control.Controls)
{
this.RegisterMouseMoveEvent(child);
}
}
private void FormSessionManagement_Load(object sender, EventArgs e)
{
this.dataGridViewSessionList.AutoGenerateColumns = false;
this.RegisterMouseMoveEvent(this);
}
如何你運行我們程序,輸入user name開始session後,如果在30s內沒有任何鼠標操作,下面的MessageBox將會彈出,當你點擊OK按鈕,程序會退出。
如何你同時開啟多個client端程序,點擊“Kill Selected Session”按鈕,將會列出所有的Active session,就象我們在上面的截圖所示的一樣。你可以選擇某個session,然後通過點擊“Kill selected sessions”按鈕強行中止它。通過另一個client application將馬上得到反饋:彈出下面一個MessageBox。當你點擊OK按鈕,程序會退出