WCF双向通信实现Session管理[下篇]

一、Session Management Service的实现

  现在我们来看看Session Management真正的实现,和我以前的例子不同,我不是把所有的实现都写在WCF service上,而是定义了另一个class来实现所有的业务逻辑:SessionManager。我们分析一下具体的实现逻辑。

   1: namespace Artech.SessionManagement.Service

   2: {

   3:     public static class SessionManager

   4:     {

   5:         private static object _syncHelper = new object(); 

   6:  

   7:         internal static TimeSpan Timeout

   8:         { get; set; } 

   9:  

  10:         public static IDictionary<Guid, SessionInfo> CurrentSessionList

  11:         { get; set; } 

  12:  

  13:         public static IDictionary<Guid, ISessionCallback> CurrentCallbackList

  14:         { get; set; } 

  15:  

  16:         static SessionManager()

  17:         {

  18:             string sessionTimeout = ConfigurationManager.AppSettings["SessionTimeout"];

  19:             if (string.IsNullOrEmpty(sessionTimeout))

  20:             {

  21:                 throw new ConfigurationErrorsException("The session timeout application setting is missing");

  22:             } 

  23:  

  24:             double timeoutMinute;

  25:             if (!double.TryParse(sessionTimeout, out timeoutMinute))

  26:             {

  27:                 throw new ConfigurationErrorsException("The session timeout application setting should be of doubdle type.");

  28:             }           

  29:  

  30:             Timeout = new TimeSpan(0, 0, (int)(timeoutMinute * 60));

  31:             CurrentSessionList = new Dictionary<Guid, SessionInfo>();

  32:             CurrentCallbackList = new Dictionary<Guid, ISessionCallback>();

  33:         }

  34:         //...

  35:   }

  36: }

  首先来看Field、Property和static constructor的定义。_syncHelper 用于实现多线程同步之用;Timeout是session timeout的时间,可配置;CurrentSessionList和CurrentCallbackList两个dictionary在上面我们已经 作过介绍,分别代表当前活动的session列表和callback列表,key均为SessionID。在静态构造函数中,初始化session timeout的时间,和实例化CurrentSessionList和CurrentCallbackList。

  接着我们来看看StartSession和EndSession两个方法,这两个方法分别代表Session的开始和结束。

   1: public static Guid StartSession(SessionClientInfo clientInfo)

   2: {

   3:     Guid sessionID = Guid.NewGuid();

   4:     ISessionCallback callback = OperationContext.Current.GetCallbackChannel<ISessionCallback>();

   5:     SessionInfo sesionInfo = new SessionInfo() { SessionID = sessionID, StartTime = DateTime.Now,  LastActivityTime = DateTime.Now, ClientInfo = clientInfo };

   6:     lock (_syncHelper)

   7:     {

   8:         CurrentSessionList.Add(sessionID, sesionInfo);

   9:         CurrentCallbackList.Add(sessionID, callback);

  10:     }

  11:     return sessionID;

  12: } 

  13:  

  14: public static void EndSession(Guid sessionID)

  15: {

  16:     if (!CurrentSessionList.ContainsKey(sessionID))

  17:     {

  18:         return;

  19:     } 

  20:  

  21:     lock (_syncHelper)

  22:     {

  23:         CurrentCallbackList.Remove(sessionID);

  24:         CurrentSessionList.Remove(sessionID);

  25:     }

  26: } 

  在StartSession方法中,首先创建一个GUID作为SessionID。通过 OperationContext.Current获得callback对象,并根据client端传入的SessionClientInfo 对象创建SessionInfo 对象,最后将callback对象和SessionInfo 对象加入CurrentCallbackList和CurrentSessionList中。由于这两个集合会在多线程的环境下频繁地被访问,所以在对该 集合进行添加和删除操作时保持线程同是显得尤为重要,所在在本例中,所有对列表进行添加和删除操作都需要获得_syncHelper加锁下才能执行。与 StartSession相对地,EndSession方法仅仅是将SessionID标识的callback对象和SessionInfo 对象从列表中移除。

  然后我们来看看如何强行中止掉一个或多个活动的session:KillSessions。

   1: public static void KillSessions(IList<Guid> sessionIDs)

   2: {

   3:     lock (_syncHelper)

   4:     {

   5:         foreach (Guid sessionID in sessionIDs)

   6:         {

   7:             if (!CurrentSessionList.ContainsKey(sessionID))

   8:             {

   9:                 continue;

  10:             } 

  11:  

  12:             SessionInfo sessionInfo = CurrentSessionList[sessionID];

  13:             CurrentSessionList.Remove(sessionID);

  14:             CurrentCallbackList[sessionID].OnSessionKilled(sessionInfo);

  15:             CurrentCallbackList.Remove(sessionID);

  16:         }

  17:     }

  18: } 

  逻辑很简单,就是先从CurrentSessionList中获得对应的SessionInfo 对象,然后将其从CurrentSessionList中移除,然后根据SessionID获得对用的Callback对象,调用 OnSessionKilled方法实时通知client session被强行中止,最后将callback对象从CurrentCallbackList中清楚。需要注意的是OnSessionKilled是 One-way方式调用的,所以是异步的,时间的消耗可以忽略不计,也不会抛出异常,所以对_syncHelper的锁会很开释放,所以不会对并发造成太 大的影响。

  Session的管理最终要、也是作复杂的事对Timeout的实现,再我们的例子中,我们通过定期对CurrentSessionList中的每个session进行轮询实现。每次轮询通过RenewSessions方法实现,我们来看看该方法的定义:

   1: [MethodImpl(MethodImplOptions.Synchronized)]
   2: public static void RenewSessions()
   3: {
   4:     IList<WaitHandle> waitHandleList = new List<WaitHandle>(); 
   5:  
   6:     foreach (var session in CurrentSessionList)
   7:     {
   8:         RenewSession renewsession = delegate(KeyValuePair<Guid, SessionInfo> sessionInfo)
   9:         {
  10:             if (DateTime.Now - sessionInfo.Value.LastActivityTime < Timeout)
  11:             {
  12:                 return;
  13:             }
  14:             try
  15:             {
  16:                 TimeSpan renewDuration = CurrentCallbackList[sessionInfo.Key].Renew();
  17:                 if (renewDuration.TotalSeconds > 0)
  18:                 {
  19:                     sessionInfo.Value.LastActivityTime += renewDuration;
  20:                 }
  21:                 else
  22:                 {
  23:                     sessionInfo.Value.IsTimeout = true;
  24:                     CurrentCallbackList[session.Key].OnSessionTimeout(sessionInfo.Value);
  25:                 }
  26:             }
  27:             catch (CommunicationObjectAbortedException)
  28:             {
  29:                 sessionInfo.Value.IsTimeout = true;
  30:                 return;
  31:             }
  32:         }; 
  33:  
  34:         IAsyncResult result = renewsession.BeginInvoke(session, null, null);
  35:         waitHandleList.Add(result.AsyncWaitHandle);
  36:     } 
  37:  
  38:     if (waitHandleList.Count == 0)
  39:     {
  40:         return;
  41:     }
  42:             WaitHandle.WaitAll(waitHandleList.ToArray<WaitHandle>());
  43:             ClearSessions();
  44: } 
  45:  
  46: 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就显得很简单了:

   1: namespace Artech.SessionManagement.Service

   2: {

   3:     [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode =ConcurrencyMode.Multiple)]

   4:    public class SessionManagementService:ISessionManagement

   5:     {

   6:         #region ISessionManagement Members 

   7:  

   8:         public Guid StartSession(SessionClientInfo clientInfo,out TimeSpan timeout)

   9:         {

  10:             timeout = SessionManager.Timeout;

  11:             return SessionManager.StartSession(clientInfo);

  12:         } 

  13:  

  14:         public void EndSession(Guid sessionID)

  15:         {

  16:             SessionManager.EndSession(sessionID);

  17:         } 

  18:  

  19:         public IList<SessionInfo> GetActiveSessions()

  20:         {

  21:             return new List<SessionInfo>(SessionManager.CurrentSessionList.Values.ToArray<SessionInfo>());     

  22:         } 

  23:  

  24:         public void KillSessions(IList<Guid> sessionIDs)

  25:         {

  26:             SessionManager.KillSessions(sessionIDs);

  27:         } 

  28:  

  29:         #endregion

  30:     }

  31: } 

基本上就是调用SessionManager对应的方法。

二、Service Hosting

  在Artech.SessionManagement.Hosting.Program中的Main()方法中,实际上是做了两件事情:

  • 对SessionManagementService的Host。
  • 通过Timer对象实现对Session列表的定期(5s)轮询。
   1: namespace Artech.SessionManagement.Hosting
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             using (ServiceHost host = new ServiceHost(typeof(SessionManagementService)))
   8:             { 
   9:                 host.Opened += delegate
  10:                 {
  11:                     Console.WriteLine("The session management service has been started up!");
  12:                 };
  13:                 host.Open(); 
  14:  
  15:                 Timer timer = new Timer(
  16:                     delegate { SessionManager.RenewSessions(); }, null, 0, 5000); 
  17:  
  18:                 Console.Read();
  19:             }
  20:         }
  21:     }
  22: } 
  23:  

这是configuration,除了system.serviceModel相关配置外,还定义了配置了session timeout的时间,单位为”分”:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <appSettings>
   4:         <add key="SessionTimeout" value="0.5"/>
   5:     </appSettings>
   6:     <system.serviceModel>
   7:         <services>
   8:             <service name="Artech.SessionManagement.Service.SessionManagementService">
   9:                 <endpoint binding="netTcpBinding" bindingConfiguration="" contract="Artech.SessionManagement.Contract.ISessionManagement" />
  10:                 <host>
  11:                     <baseAddresses>
  12:                         <add baseAddress="net.tcp://127.0.0.1:9999/sessionservice" />
  13:                     </baseAddresses>
  14:                 </host>
  15:             </service>
  16:         </services>
  17:     </system.serviceModel>
  18: </configuration> 
  19:  

三、如何定义Client

  这个service的实现已经完成,我们最后来介绍如何根据service的特点来定义我们的client程序了。 我们的client是一个GUI应用(WinForm)。为了简便,我们把所有的逻辑定义在一个facade class上面:SessionUtility。

   1: namespace Artech.SessionManagement.Client

   2: {

   3:     public static class SessionUtility

   4:     {

   5:         static SessionUtility()

   6:         {

   7:             Callback = new SessionCallback();

   8:             Channel = new DuplexChannelFactory<ISessionManagement>(Callback, "sessionservice").CreateChannel();            

   9:         } 

  10:  

  11:         private static ISessionManagement Channel

  12:         { get; set; } 

  13:  

  14:         private static ISessionCallback Callback

  15:         { get; set; } 

  16:  

  17:         public static DateTime LastActivityTime

  18:         { get; set; } 

  19:  

  20:         public static Guid SessionID

  21:         { get; set; } 

  22:  

  23:         public static TimeSpan Timeout

  24:         { get; set; } 

  25:  

  26:         public static void StartSession(SessionClientInfo clientInfo)

  27:         {

  28:             TimeSpan timeout;

  29:             SessionID = Channel.StartSession(clientInfo, out timeout);

  30:             Timeout = timeout;

  31:         } 

  32:  

  33:         public static IList<SessionInfo> GetActiveSessions()

  34:         {

  35:             return Channel.GetActiveSessions();

  36:         } 

  37:  

  38:         public static void KillSessions(IList<Guid> sessionIDs)

  39:         {

  40:             Channel.KillSessions(sessionIDs);

  41:         }

  42:     }

  43: } 

  SessionUtility定义了连个public property:SessionID代表当前session的ID,Timeout代表Session timeout的时间,这两个属性都在StartSession中被初始化,而LastActivityTime代表的是最后一次用户交互的时间。上面的 代码和简单,在这里就不多作介绍了。这里需要着重介绍我们的Callback class:

   1: public class SessionCallback : ISessionCallback

   2: {

   3:     #region ISessionCallback Members 

   4:  

   5:     public TimeSpan Renew()

   6:     {

   7:         return SessionUtility.Timeout - (DateTime.Now - SessionUtility.LastActivityTime);

   8:     } 

   9:  

  10:     public void OnSessionKilled(SessionInfo sessionInfo)

  11:     {

  12:         MessageBox.Show("The current session has been killed!", sessionInfo.SessionID.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Information);

  13:         Application.Exit();

  14:     } 

  15:  

  16:     public void OnSessionTimeout(SessionInfo sessionInfo)

  17:     {

  18:         MessageBox.Show("The current session timeout!", sessionInfo.SessionID.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Information);

  19:         Application.Exit();

  20:     } 

  21:  

  22:     #endregion

  23: } 

  Renew()方法根据Timeout 和LastActivityTime计算出需要对该session延长的时间;OnSessionKilled和OnSessionTimeout在通过MessageBox显示相应的message后将程序退出。

  我们简单简单一下本例子提供的client application。具有一个Form。我们把所有的功能集中在该Form中:开始一个新session、获得所有的活动的session列表、强行中止一个或多个Session。

client application

  这是StartSession按钮的click event handler:

   1: private void buttonStartSession_Click(object sender, EventArgs e)

   2: {

   3:     string hostName = Dns.GetHostName();

   4:     IPAddress[] ipAddressList = Dns.GetHostEntry(hostName).AddressList;

   5:     string ipAddress = string.Empty;

   6:     foreach (IPAddress address in ipAddressList)

   7:     {

   8:         if (address.AddressFamily == AddressFamily.InterNetwork)

   9:         {

  10:             ipAddress += address.ToString() + ";";

  11:         }

  12:     }

  13:     ipAddress = ipAddress.TrimEnd(";".ToCharArray()); 

  14:  

  15:     string userName = this.textBoxUserName.Text.Trim();

  16:     if (string.IsNullOrEmpty(userName))

  17:     {

  18:         return;

  19:     } 

  20:  

  21:     SessionClientInfo clientInfo = new SessionClientInfo() { IPAddress = ipAddress, HostName = hostName, UserName = userName };

  22:     SessionUtility.StartSession(clientInfo);

  23:     this.groupBox2.Enabled = false;

  24: } 

  获得当前PC的主机名称和IP地址,连同输入的user name创建SessionClientInfo 对象,调用SessionUtility的StartSession开始新的Session。

“Get All Active Session”,获取当前所有的活动的session,绑定到Datagrid:

   1: private void buttonGet_Click(object sender, EventArgs e)
   2: {
   3:     IList<SessionInfo> activeSessions = SessionUtility.GetActiveSessions();
   4:     this.dataGridViewSessionList.DataSource = activeSessions;
   5:     foreach (DataGridViewRow row in this.dataGridViewSessionList.Rows)
   6:     {
   7:         Guid sessionID = (Guid)row.Cells["SessionID"].Value;
   8:         row.Cells["IPAddress"].Value = activeSessions.Where(session=> session.SessionID == sessionID).ToList<SessionInfo>()[0].ClientInfo.IPAddress;
   9:         row.Cells["UserName"].Value = activeSessions.Where(session => session.SessionID == sessionID).ToList<SessionInfo>()[0].ClientInfo.UserName;
  10:     }
  11: } 

“Kill Selected Session”按钮被点击,强行中止选中的Session:

   1: private void buttonKill_Click(object sender, EventArgs e)
   2: {
   3:     IList<Guid> sessionIDs = new List<Guid>();
   4:     foreach ( DataGridViewRow row in this.dataGridViewSessionList.Rows)
   5:     {
   6:         if ((string)row.Cells["Select"].Value == "1")
   7:         {
   8:             Guid sessionID = new Guid(row.Cells["SessionID"].Value.ToString());
   9:             if (sessionID == SessionUtility.SessionID)
  10:             {
  11:                 MessageBox.Show("You cannot kill your current session!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  12:                 return;
  13:             }
  14:             sessionIDs.Add(sessionID);
  15:         }
  16:     }   
  17:  
  18:     SessionUtility.KillSessions(sessionIDs);           
  19: } 
  20:  

  由于不能中止自己当前的Session,所以当选中的列表中包含自己的SessionID,会显示一个messagebox提示不应该杀掉属于自己session。

  到这里,实际上还有一件重要的事情没有解决,那就是如何动态修正 SessionUtility.LastActivityTime。我们希望的事SessionUtility.LastActivityTime能够真 正反映最后一次用户交互的时间。为此我们递归地注册每个control的MouseMove事件:

   1: private void RegisterMouseMoveEvent(Control control)
   2: {
   3:     control.MouseHover += delegate
   4:     {
   5:         SessionUtility.LastActivityTime = DateTime.Now;
   6:     }; 
   7:  
   8:     foreach (Control child in control.Controls)
   9:     {
  10:         this.RegisterMouseMoveEvent(child);
  11:     }
  12: } 
  13:  
  14: private void FormSessionManagement_Load(object sender, EventArgs e)
  15: {
  16:     this.dataGridViewSessionList.AutoGenerateColumns = false;
  17:     this.RegisterMouseMoveEvent(this);
  18: } 
  19:  

  如何你运行我们程序,输入user name开始session后,如果在30s内没有任何鼠标操作,下面的MessageBox将会弹出,当你点击OK按钮,程序会退出。
session timeout

  如何你同时开启多个client端程序,点击“Kill Selected Session”按钮,将会列出所有的Active session,就象我们在上面的截图所示的一样。你可以选择某个session,然后通过点击“Kill selected sessions”按钮强行中止它。通过另一个client application将马上得到反馈:弹出下面一个MessageBox。当你点击OK按钮,程序会退出

Kill Selected Session

 

作者:Artech
出处:http://artech.cnblogs.com/

加支付宝好友偷能量挖...


评论(0)网络
阅读(92)喜欢(0)Asp.Net/C#/WCF