程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WCF後續之旅(9) 通過WCF雙向通信實現Session管理[Part II]

WCF後續之旅(9) 通過WCF雙向通信實現Session管理[Part II]

編輯:關於.NET

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按鈕,程序會退出

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved