ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 跟我一起学WCF(8)——WCF中Session、实例管理详解 ## 一、引言 由前面几篇博文我们知道,WCF是微软基于SOA建立的一套在分布式环境中各个相对独立的应用进行交流(Communication)的框架,它实现了最新的基于WS-*规范。按照SOA的原则,相对独自的业务逻辑以Service的形式进行封装,调用者通过消息(Messaging)的方式来调用服务。对于承载某个业务功能实现的服务应该具有上下文(Context)无关性,意思就是说构造服务的操作(Operation)不应该绑定到具体的调用上下文,对于任何的调用,具有什么的样输入就会对应怎样的输出。因为SOA一个最大的目标是尽可能地实现重用,只有具有Context无关性,服务才能最大限度的重用。即从软件架构角度理解为,一个模块只有尽可能的独立,即具有上下文无关性,才能被最大限度的重用。软件体系一直在强调低耦合也是这个道理。 但是在某些场景下,我们却希望系统为我们创建一个Session来保留Client和Service的交互的状态,如Asp.net中Session的机制一样,WCF也提供了对Session的支持。下面就具体看看WCF中对Session的实现。 ## 二、WCF中Session详细介绍 ## 2.1 Asp.net的Session与WCF中的Session 在WCF中,Session属于Service Contract的范畴,并在Service Contract定义中通过[SessionModel](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.servicecontractattribute.sessionmode(v=vs.90).aspx)参数来实现。WCF中会话具有以下几个重要的特征: * Session都是由Client端显示启动和终止的。 在WCF中Client通过创建的代理对象来和服务进行交互,在支持Session的默认情况下,Session是和具体的代理对象绑定在一起,当Client通过调用代理对象的某个方法来访问服务时,Session就被初始化,直到代理的关闭,Session则被终止。我们可以通过两种方式来关闭Proxy:一是调用[ICommunicationObject.Close 方法](http://msdn.microsoft.com/zh-cn/library/ms195520(v=vs.110).aspx),二是调用[ClientBase&lt;TChannel&gt;.Close 方法](http://msdn.microsoft.com/zh-cn/library/ms575273(v=vs.110).aspx) 。我们也可以通过服务中的某个操作方法来初始化、或者终止Session,可以通过OperationContractAttribute的IsInitiating和[IsTerminating](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.operationcontractattribute.isterminating(v=vs.110).aspx)参数来指定初始化和终止Session的Operation。 * 在WCF会话期间,传递的消息按照它发送的顺序被接收。 * WCF并没有为Session的支持保存相关的状态数据。 讲到Session,做过Asp.net开发的人,自然想到的就是Asp.net中的Session。它们只是名字一样,在实现机制上有很大的不同。Asp.net中的Session具有以下特性: * Asp.net的Session总是由服务端启动的,即在服务端进行初始化的。 * Asp.net中的Session是无需,不能保证请求处理是有序的。 * Asp.net是通过在服务端以某种方式保存State数据来实现对Session的支持,例如保存在Web Server端的内存中。 ## 2.2 WCF中服务实例管理 对于Client来说,它实际上不能和Service进行直接交互,它只能通过客户端创建的Proxy来间接地和Service进行交互,然而真正的调用而是通过服务实例来进行的。我们把通过Client的调用来创建最终的服务实例过程称作激活,在.NET Remoting中包括Singleton模式、SingleCall模式和客户端激活方式,WCF中也有类似的服务激活方式:单调服务(PerCall)、会话服务(PerSession)和单例服务(Singleton)。 * **单调服务(Percall)**:为每个客户端请求分配一个新的服务实例。类似.NET Remoting中的SingleCall模式 * **会话服务(Persession)**:在会话期间,为每次客户端请求共享一个服务实例,类似.NET Remoting中的客户端激活模式。 * **单例服务(Singleton)**:所有客户端请求都共享一个相同的服务实例,类似于.NET Remoting的Singleton模式。但它的激活方式需要注意一点:当为对于的服务类型进行Host的时候,与之对应的服务实例就被创建出来,之后所有的服务调用都由这个服务实例进行处理。 WCF中服务激活的默认方式是PerSession,但不是所有的Bingding都支持Session,比如BasicHttpBinding就不支持Session。你也可以通过下面的方式使ServiceContract不支持Session. ``` [ServiceContract(SessionMode = SessionMode.NotAllowed)] ``` 下面分别介绍下这三种激活方式的实现。 ## 三、WCF中实例管理的实现 WCF中服务激活的默认是PerSession的方式,下面就看看PerSession的实现方式。我们还是按照前面几篇博文的方式来实现使用PerSession方式的WCF服务程序。 第一步:自然是实现我们的WCF契约和契约的服务实现。具体的实现代码如下所示: ``` 1 // 服务契约的定义 2 [ServiceContract] 3 public interface ICalculator 4 { 5 [OperationContract(IsOneWay = true)] 6 void Increase(); 7 8 [OperationContract] 9 int GetResult(); 10 } 11 12 // 契约的实现 13 public class CalculatorService : ICalculator, IDisposable 14 { 15 private int _nCount = 0; 16 17 public CalculatorService() 18 { 19 Console.WriteLine("CalulatorService object has been created"); 20 } 21 22 // 为了看出服务实例的释放情况 23 public void Dispose() 24 { 25 Console.WriteLine("CalulatorService object has been Disposed"); 26 } 27 28 #region ICalulator Members 29 public void Increase() 30 { 31 // 输出Session ID 32 Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId); 33 this._nCount++; 34 } 35 36 public int GetResult() 37 { 38 Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId); 39 return this._nCount; 40 } 41 #endregion 42 } ``` 为了让大家对服务对象的创建和释放有一个直观的认识,我特意对服务类实现了构造函数和IDisposable接口,同时在每个操作中输出当前的Session ID。 第二步:实现服务宿主程序。这里还是采用控制台程序作为服务宿主程序,具体的实现代码如下所示: ``` 1 // 服务宿主程序 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 using (ServiceHost host = new ServiceHost(typeof(CalculatorService))) 7 { 8 host.Opened += delegate 9 { 10 Console.WriteLine("The Calculator Service has been started, begun to listen request..."); 11 }; 12 13 host.Open(); 14 Console.ReadLine(); 15 } 16 } 17 } ``` 对应的配置文件为: ``` <!--服务宿主的配置文件--> <configuration> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name ="CalculatorBehavior"> <serviceMetadata httpGetEnabled="true"/> </behavior> </serviceBehaviors> </behaviors> <services> <service name="WCFContractAndService.CalculatorService" behaviorConfiguration="CalculatorBehavior"> <endpoint address="" binding="basicHttpBinding" contract="WCFContractAndService.ICalculator"/> <host> <baseAddresses> <add baseAddress="http://localhost:9003/CalculatorPerSession"/> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration> ``` 第三步:实现完了服务宿主程序,接下来自然是实现客户端程序来访问服务操作。这里的客户端也是控制台程序,具体的实现代码如下所示: ``` 1 // 客户端程序实现 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 // Use ChannelFactory<ICalculator> to create WCF Service proxy 7 ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("HttpEndPoint"); 8 Console.WriteLine("Create a calculator proxy :proxy1"); 9 ICalculator proxy1 = calculatorChannelFactory.CreateChannel(); 10 Console.WriteLine("Invoke proxy1.Increate() method"); 11 proxy1.Increase(); 12 Console.WriteLine("Invoke proxy1.Increate() method again"); 13 proxy1.Increase(); 14 Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult()); 15 16 Console.WriteLine("Create another calculator proxy: proxy2"); 17 ICalculator proxy2 = calculatorChannelFactory.CreateChannel(); 18 Console.WriteLine("Invoke proxy2.Increate() method"); 19 proxy2.Increase(); 20 Console.WriteLine("Invoke proxy2.Increate() method again"); 21 proxy2.Increase(); 22 Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult()); 23 24 Console.ReadLine(); 25 } 26 } ``` 客户端对应的配置文件内容如下所示: ``` <!--客户端配置文件--> <configuration> <system.serviceModel> <client> <endpoint address="http://localhost:9003/CalculatorPerSession" binding="basicHttpBinding" contract="WCFContractAndService.ICalculator" name="HttpEndPoint"/> </client> </system.serviceModel> </configuration> ``` 经过上面三步,我们就完成了PerSession方式的WCF程序了,下面看看该程序的运行结果。 首先,以管理员权限运行服务寄宿程序,运行成功后,你将看到如下图所示的画面: ![](https://box.kancloud.cn/2016-01-23_56a2eb48b3006.png) 接下来,运行客户端对服务操作进行调用,运行成功后,你将看到服务宿主的输出和客户端的输出情况如下图所示: ![](https://box.kancloud.cn/2016-01-23_56a2eb48c191d.png) 从客户端的运行结果可以看出,虽然我们两次调用了Increase方法来增加_nCount的值,但是最终的运行结果仍然是0。这样的运行结果好像与我们之前所说的WCF默认Session支持矛盾,因为如果WCF默认的方式PerSession的话,则服务实例是和Proxy绑定在一起,当Proxy调用任何一个操作的时候Session开始,从此Session将会与Proxy具有一样的生命周期。按照这个描述,客户端运行的结果应该是2而不是0。这里,我只能说运行结果并没有错,因为有图有真相嘛,那到底是什么原因导致客户端获得_nCount值是0呢?其实在前面已经讲到过,并不是所有的绑定都是支持Session的,上面程序的实现我们使用的basicHttpBinding,而basicHttpBinding是不支持Session方式的,所以WCF会采用PerCall的方式创建Service Instance,所以在服务端中对于每一个Proxy都有3个对象被创建,两个是对Increase方法的调用会导致服务实例的激活,另一个是对GetResult方法的调用导致服务实例的激活。因为是PerCall方式,所以每次调用完之后,就会对服务实例进行释放,所以对应的就有3行服务对象释放输出。并且由于使用的是不支持Session的binding,所以Session ID的输出也为null。所以,上面WCF程序其实是PerCall方式的实现。 既然,上面的运行结果是由于使用了不支持Session的basicHttpBinding导致的,下面看看使用一个支持Session的Binding:wsHttpBinding来看看运行结果是怎样的,这里的修改很简单,只需要把宿主和客户端的配置文件把绑定类型修改为wsHttpBinding就可以了。 ``` <!--客户端配置文件--> <configuration> <system.serviceModel> <client> <endpoint address="http://localhost:9003/CalculatorPerSession" binding="**wsHttpBinding**" contract="WCFContractAndService.ICalculator" name="HttpEndPoint"/> </client> </system.serviceModel> </configuration> <!--服务宿主的配置文件--> <configuration> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name ="CalculatorBehavior"> <serviceMetadata httpGetEnabled="true"/> </behavior> </serviceBehaviors> </behaviors> <services> <service name="WCFContractAndService.CalculatorService" behaviorConfiguration="CalculatorBehavior"> <endpoint address="" binding="**wsHttpBinding**" contract="WCFContractAndService.ICalculator"/> <host> <baseAddresses> <add baseAddress="http://localhost:9003/CalculatorPerSession"/> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration> ``` 现在我们再运行下上面的程序来看看此时的执行结果,具体的运行结果如下图所示: ![](https://box.kancloud.cn/2016-01-23_56a2eb48d9d9e.png) 从上面的运行结果可以看出,此时两个Proxy的运行结果都是2,可以看出此时服务激活方式采用的是PerSession方式。此时对于服务端就只有两个服务实例被创建了,并且对于每个服务实例具有相同的Session ID。 另外由于Client的Proxy还依然存在,服务实例也不会被回收掉,从上面服务端运行的结果也可以证实这点,因为运行结果中没有对象呗Disposable的输出。你可以在客户端显式调用ICommunicationObject.Close方法来显式关闭掉Proxy,在客户端添加对Proxy的显示关闭代码,此时客户端的代码修改为如下所示: ``` 1 // 客户端程序实现 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 // Use ChannelFactory<ICalculator> to create WCF Service proxy 7 ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("HttpEndPoint"); 8 Console.WriteLine("Create a calculator proxy :proxy1"); 9 ICalculator proxy1 = calculatorChannelFactory.CreateChannel(); 10 Console.WriteLine("Invoke proxy1.Increate() method"); 11 proxy1.Increase(); 12 Console.WriteLine("Invoke proxy1.Increate() method again"); 13 proxy1.Increase(); 14 Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult()); 15 **(proxy1 as ICommunicationObject).Close(); // 显示关闭Proxy** 16 17 Console.WriteLine("Create another calculator proxy: proxy2"); 18 ICalculator proxy2 = calculatorChannelFactory.CreateChannel(); 19 Console.WriteLine("Invoke proxy2.Increate() method"); 20 proxy2.Increase(); 21 Console.WriteLine("Invoke proxy2.Increate() method again"); 22 proxy2.Increase(); 23 Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult()); 24 **(proxy2 as ICommunicationObject).Close();** 25 26 Console.ReadLine(); 27 } 28 } ``` 此时,服务对象的Dispose()方法将会调用,此时服务端的运行结果如下图所示: ![](https://box.kancloud.cn/2016-01-23_56a2eb48edc84.png) 上面演示了默认支持Session的情况,下面我们修改服务契约使之不支持Session,此时只需要知道ServiceContract的SessionMode为NotAllowed即可。 ``` [ServiceContract**(SessionMode= SessionMode.NotAllowed)**] // 是服务契约不支持Session public interface ICalculator { [OperationContract(IsOneWay = true)] void Increase(); [OperationContract] int GetResult(); } ``` 此时,由于服务契约不支持Session,此时服务激活方式采用的仍然是PerCall。运行结果与前面采用不支持Session的绑定的运行结果一样,这里就不一一贴图了。 除了通过显式修改ServiceContract的SessionMode来使服务契约支持或不支持Session外,还可以定制操作对Session的支持。定制操作对Session的支持可以通过OperationContract的IsInitiating和InTerminating属性设置。 ``` 1 // 服务契约的定义 2 [ServiceContract(SessionMode= SessionMode.Required)] // 显式使服务契约支持Session 3 public interface ICalculator 4 { 5 // IsInitiating:该值指示方法是否实现可在服务器上启动会话(如果存在会话)的操作,默认值是true 6 // IsTerminating:获取或设置一个值,该值指示服务操作在发送答复消息(如果存在)后,是否会导致服务器关闭会话,默认值是false 7 [OperationContract(IsOneWay = true, IsInitiating =true, IsTerminating=false )] 8 void Increase(); 9 10 [OperationContract(IsInitiating = true, IsTerminating = true)] 11 int GetResult(); 12 } ``` 在上面代码中,对两个操作都设置InInitiating的属性为true,意味着调用这两个操作都会启动会话,而把GetResult操作的IsTerminating设置为true,意味着调用完这个操作后,会导致服务关闭掉会话,因为在Session方式下,Proxy与Session有一致的生命周期,所以关闭Session也就是关闭proxy对象,所以如果后面再对proxy对象的任何一个方法进行调用将会导致异常,下面代码即演示了这种情况。 ``` 1 // 客户端程序实现 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 // Use ChannelFactory<ICalculator> to create WCF Service proxy 7 ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("HttpEndPoint"); 8 Console.WriteLine("Create a calculator proxy :proxy1"); 9 ICalculator proxy1 = calculatorChannelFactory.CreateChannel(); 10 Console.WriteLine("Invoke proxy1.Increate() method"); 11 proxy1.Increase(); 12 Console.WriteLine("Invoke proxy1.Increate() method again"); 13 proxy1.Increase(); 14 Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult()); 15 **try 16 { 17 proxy1.Increase(); // session关闭后对proxy1.Increase方法调用将会导致异常 18 } 19 catch (Exception ex) // 异常捕获 20 { 21 Console.WriteLine("在Session关闭后调用Increase方法失败,错误信息为:{0}", ex.Message); 22 }** 23 24 Console.WriteLine("Create another calculator proxy: proxy2"); 25 ICalculator proxy2 = calculatorChannelFactory.CreateChannel(); 26 Console.WriteLine("Invoke proxy2.Increate() method"); 27 proxy2.Increase(); 28 Console.WriteLine("Invoke proxy2.Increate() method again"); 29 proxy2.Increase(); 30 Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult()); 31 32 Console.ReadLine(); 33 } 34 } ``` 此时运行结果也验证我们上面的分析,客户端和服务端的运行结果如下图所示: ![](https://box.kancloud.cn/2016-01-23_56a2eb490d13e.png) 上面演示了PerSession和PerCall的两种服务对象激活方式,下面看看Single的激活方式运行的结果。首先通过ServiceBehavior的InstanceContextMode属性显式指定激活方式为Single,由于[ServiceBehaviorAttribute](http://msdn.microsoft.com/zh-cn/library/System.ServiceModel.ServiceBehaviorAttribute(v=vs.110).aspx)特性只能应用于类上,所以把该特性应用于CalculatorService类上,此时服务实现的代码如下所示: ``` // 契约的实现 // ServiceBehavior属性只能应用在类上 **[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] // 显示指定PerSingle方式** public class CalculatorService : ICalculator, IDisposable { private int _nCount = 0; public CalculatorService() { Console.WriteLine("CalulatorService object has been created"); } // 为了看出服务实例的释放情况 public void Dispose() { Console.WriteLine("CalulatorService object has been Disposed"); } #region ICalulator Members public void Increase() { // 输出Session ID Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId); this._nCount++; } public int GetResult() { Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId); return this._nCount; } #endregion } ``` 此时运行服务宿主的输出结果如下图所示: ![](https://box.kancloud.cn/2016-01-23_56a2eb49265d7.png) 从运行结果可以看出,对于Single方式,服务实例在服务类型被寄宿的时候就已经创建了,对于PerCall和PerSession方式而是在通过Proxy调用相应的服务操作之后,服务实例才开始创建的。下面运行客户端程序,你将看到如下图所示的运行结果: ![](https://box.kancloud.cn/2016-01-23_56a2eb493b301.png) 此时,第二个Proxy返回的结果是4而不是2,这是因为采用Single方式只存在一个服务实例,所有的调用状态都将保留,所以_nCount的值在原来的基础上继续累加。 ## 四、总结 到这里,本文的分享就结束了,本文主要分享了WCF中实例管理的实现。从WCF的实例实现可以看出,WCF实例实现是借鉴了.NET Remoting中实例实现,然后分别分享了服务实例三种激活方式在WCF中的实现,并通过对运行结果进行对比来让大家理解它们之间的区别。 本文所以源码:[WCFInstanceManager.zip](http://files.cnblogs.com/zhili/WCFInstanceManager.zip)