跟我一起学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参数来实现。WCF中会话具有以下几个重要的特征:

  • Session都是由Client端显示启动和终止的。

  在WCF中Client通过创建的代理对象来和服务进行交互,在支持Session的默认情况下,Session是和具体的代理对象绑定在一起,当Client通过调用代理对象的某个方法来访问服务时,Session就被初始化,直到代理的关闭,Session则被终止。我们可以通过两种方式来关闭Proxy:一是调用ICommunicationObject.Close 方法,二是调用ClientBase<TChannel>.Close 方法 。我们也可以通过服务中的某个操作方法来初始化、或者终止Session,可以通过OperationContractAttribute的IsInitiating和IsTerminating参数来指定初始化和终止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程序了,下面看看该程序的运行结果。

  首先,以管理员权限运行服务寄宿程序,运行成功后,你将看到如下图所示的画面:

  接下来,运行客户端对服务操作进行调用,运行成功后,你将看到服务宿主的输出和客户端的输出情况如下图所示:

  从客户端的运行结果可以看出,虽然我们两次调用了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>

  现在我们再运行下上面的程序来看看此时的执行结果,具体的运行结果如下图所示:

  从上面的运行结果可以看出,此时两个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()方法将会调用,此时服务端的运行结果如下图所示:

  上面演示了默认支持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     }

  此时运行结果也验证我们上面的分析,客户端和服务端的运行结果如下图所示:

  上面演示了PerSession和PerCall的两种服务对象激活方式,下面看看Single的激活方式运行的结果。首先通过ServiceBehavior的InstanceContextMode属性显式指定激活方式为Single,由于ServiceBehaviorAttribute特性只能应用于类上,所以把该特性应用于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 
    }

  此时运行服务宿主的输出结果如下图所示:

  从运行结果可以看出,对于Single方式,服务实例在服务类型被寄宿的时候就已经创建了,对于PerCall和PerSession方式而是在通过Proxy调用相应的服务操作之后,服务实例才开始创建的。下面运行客户端程序,你将看到如下图所示的运行结果:

  此时,第二个Proxy返回的结果是4而不是2,这是因为采用Single方式只存在一个服务实例,所有的调用状态都将保留,所以_nCount的值在原来的基础上继续累加。

四、总结

  到这里,本文的分享就结束了,本文主要分享了WCF中实例管理的实现。从WCF的实例实现可以看出,WCF实例实现是借鉴了.NET Remoting中实例实现,然后分别分享了服务实例三种激活方式在WCF中的实现,并通过对运行结果进行对比来让大家理解它们之间的区别。

  本文所以源码:WCFInstanceManager.zip

 

posted @ 2014-11-01 17:11  Learning hard  阅读(...)  评论(...编辑  收藏