WCF后续之旅(9):通过WCF的双向通信实现Session管理[上篇]

我们都知道,WCF支持Duplex的消息交换模式,它允许在service的执行过程中实现对client的回调。WCF这种双向通信的方式是我们可以以Event Broker或者订阅/发布的方式来定义和调用WCF Service。今天我们就给大家一个具体的例子:通过WCF的duplex communication方式现在Session管理。

一、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之后,自动退出。

二、Session Timeout的实现原理

在该例子中,最重要的是如何实现timeout的功能,而该功能的核心在于如何探测session的状态(Active、Timeout、Killed)。一般地我们有两种截然不同的方式来实现这样的功能:

1、客户端驱动

这是大多数人会想得到的方式,通过这样的方式实现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端的压力正大。

2、服务端驱动

设计服务端驱动模型是从.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端的时间是一致的。很显然,我们不能作出这样的假设。

三、整个应用的结构

在介绍具体实现之前,我们先来了解一下整个solution的总体结构:

我依然采用我常用的4层结构(Contract、Service、Hosting和Client),其中client采用一个windows application来模拟客户端。熟悉我文章的人应该对这个结果有一定的了解了,在这里就不多做介绍了。

1、Data Contract、Service Contract和Callback Contract

我们先来定义一些抽象层的东西Contract, 通过这些contract你会对提供的功能有一个大致的了解,首先来看看在client和service端传输的数据的定义:

Client端描述:SessionClientInfo

   1: namespace Artech.SessionManagement.Contract
   2: {
   3:     [DataContract]
   4:     public class SessionClientInfo
   5:     {
   6:         [DataMember]
   7:         public string IPAddress{ get; set; } 
   8:  
   9:         [DataMember]
  10:         public string HostName{ get; set; } 
  11:  
  12:         [DataMember]
  13:         public string UserName{ get; set; } 
  14:  
  15:         [DataMember]
  16:         public IDictionary<string, string> ExtendedProperties{ get; set; }
  17:     }
  18: } 

定义了一个描述述client的基本信息:IP地址、主机名称、用户名,同时定义了一个用于保存额外信息的ExtendedProperties。

Session的描述:SessionInfo

   1: namespace Artech.SessionManagement.Contract
   2: {
   3:     [DataContract]
   4:     [KnownType(typeof(SessionClientInfo))]
   5:     public class SessionInfo
   6:     {
   7:         [DataMember]
   8:         public Guid SessionID{ get; set; } 
   9:         [DataMember]
  10:         public DateTime StartTime{ get; set; } 
  11:         [DataMember]
  12:         public DateTime LastActivityTime{get;set;} 
  13:         [DataMember]
  14:         public SessionClientInfo ClientInfo{ get; set; } 
  15:         public bool IsTimeout{ get; set; }
  16:     }
  17: } 
  18:  

定义了Session的基本信息:Session的ID、开始的时间、最后一次活动的时间、客户端基本信息以及表明Session是否Timeout的Flag。

Callback Contract:ISessionCallback

   1: namespace Artech.SessionManagement.Contract
   2: {    
   3:     public interface ISessionCallback
   4:     {
   5:         [OperationContract]
   6:         TimeSpan Renew(); 
   7:  
   8:         [OperationContract(IsOneWay = true)]
   9:         void OnSessionKilled(SessionInfo sessionInfo); 
  10:  
  11:         [OperationContract(IsOneWay = true)]
  12:         void OnSessionTimeout(SessionInfo sessionInfo);
  13:     }
  14: } 

Renew()通过获得Session需要延长的时间;OnSessionKilled和OnSessionTimeout实现Session被杀掉和Timeout时的实时通知。

ServiceContract:ISessionManagement

   1: namespace Artech.SessionManagement.Contract
   2: {
   3:     [ServiceContract(CallbackContract = typeof(ISessionCallback))]
   4:     public interface ISessionManagement
   5:     {
   6:         [OperationContract]
   7:         Guid StartSession(SessionClientInfo clientInfo, out TimeSpan timeout); 
   8:  
   9:         [OperationContract]
  10:         void EndSession(Guid sessionID); 
  11:  
  12:         [OperationContract]
  13:         IList<SessionInfo> GetActiveSessions(); 
  14:  
  15:         [OperationContract]
  16:         void KillSessions(IList<Guid> sessionIDs);
  17:     }
  18: }

StartSession和EndSession用户Session的启动和中止,GetActiveSessions获得当前所有活动的Sesssion列表,KillSessions用于强行结束一个或多个Session。具体实现请参阅Part II.

WCF后续之旅:

WCF后续之旅(1): WCF是如何通过Binding进行通信的

WCF后续之旅(2): 如何对Channel Layer进行扩展——创建自定义Channel

WCF后续之旅(3): WCF Service Mode Layer 的中枢—Dispatcher

WCF后续之旅(4):WCF Extension Point 概览

WCF后续之旅(5): 通过WCF Extension实现Localization

WCF后续之旅(6): 通过WCF Extension实现Context信息的传递

WCF后续之旅(7):通过WCF Extension实现和Enterprise Library Unity Container的集成

WCF后续之旅(8):通过WCF Extension 实现与MS Enterprise Library Policy Injection Application Block 的集成

WCF后续之旅(9):通过WCF的双向通信实现Session管理[Part I]

WCF后续之旅(9): 通过WCF双向通信实现Session管理[Part II]

WCF后续之旅(10): 通过WCF Extension实现以对象池的方式创建Service Instance

WCF后续之旅(11): 关于并发、回调的线程关联性(Thread Affinity)

WCF后续之旅(12): 线程关联性(Thread Affinity)对WCF并发访问的影响

WCF后续之旅(13): 创建一个简单的WCF SOAP Message拦截、转发工具[上篇]

WCF后续之旅(13):创建一个简单的SOAP Message拦截、转发工具[下篇]

WCF后续之旅(14):TCP端口共享

WCF后续之旅(15): 逻辑地址和物理地址

WCF后续之旅(16): 消息是如何分发到Endpoint的--消息筛选(Message Filter)

WCF后续之旅(17):通过tcpTracer进行消息的路由

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非典型程序猿

Golang任务队列machinery使用与源码剖析(二)

在Golang任务队列machinery使用与源码剖析(一)一文中,我们主要对golang中任务队列machinery的设计结构以及具体模块的功能与源码实现进行...

1.2K8
来自专栏技术博客

Entity Framework CodeFirst数据迁移

我们知道无论是“Database First”还是“Model First”当模型发生改变了都可以通过Visual Studio设计视图进行更新,那么对于Cod...

1433
来自专栏博客园

深入浅出话命令

WPF为我们准备了完善的命令系统,你可能会问:“有了路由事件为什么还需要命令系统呢?”。事件的作用是发布、传播一些消息,消息传达到了接收者,事件的指令也就算完成...

1014
来自专栏葡萄城控件技术团队

七天学会ASP.NET MVC (六)——线程问题、异常处理、自定义URL

? 本节又带了一些常用的,却很难理解的问题,本节从文件上传功能的实现引出了线程使用,介绍了线程饥饿的解决方法,异常处理方法,了解RouteTable自定义路...

27210
来自专栏Jed的技术阶梯

zookeeper编程01-循环监听

客户端发起对节点的事务操作(以NodeChildrenChanged事件为例) 服务端监听到对应的事件后进行相应的操作

3512
来自专栏walterlv - 吕毅的博客

使用 Task.Wait()?立刻死锁(deadlock)

发布于 2017-10-27 15:54 更新于 2018-04...

921
来自专栏XAI

微信OAuth授权获取用户OpenId-JAVA(个人经验)

源码在这里。https://zb.oschina.net/market/opus/1444646_161 维护服务器。一份5元。自愿购买

1.9K8
来自专栏一个爱瞎折腾的程序猿

在asp.net core2.1中添加中间件以扩展Swashbuckle.AspNetCore3.0支持简单的文档访问权限控制

在此之前的接口项目中,若使用了 Swashbuckle.AspNetCore,都是控制其只在开发环境使用,不会就这样将其发布到生产环境(安全第一) 。 那么,...

1621
来自专栏程序你好

不同的.Net版本客户端软件调用Java Web Service区别

最近的系统中需要.Net开发的离线端软件通过Web Service技术和Java开发的在线系统进行数据交互。

1073
来自专栏大内老A

[WCF权限控制]利用WCF自定义授权模式提供当前Principal[实例篇]

在《原理篇》中我们谈到:如果采用自定义安全主体权限模式,我们可以通过自定义AuthorizationPolicy或者ServiceAuthorizationMa...

25910

扫码关注云+社区

领取腾讯云代金券