WCF中的Session

我们知道,WCF是MS基于SOA建立的一套在分布式环境中各个相对独立的Application进行Communication的构架。他实现了最新的基于WS-*规范。按照SOA的原则,相对独自的业务逻辑以service的形式封装,调用者通过Messaging的方式调用Service。对于承载着某个业务功能的实现的Service应该具有Context无关性、甚至是Solution无关性,也就是说个构成Service的operation不应该绑定到具体的调用上下文,对于任何调用,具有什么样的输入,就会有与之对应的输出。因为SOA的一个最大的目标就是尽可能地实现重用,只有具有Context无关性/Solution无关性,Service才能实现最大限度的重用。此外Service的Context无关性/Solution无关性还促进了另一个重要的面向服务的特征的实现:可组合性,把若干相关细粒度的Service封装成一个整体业务流程的Service。

在一个C/S(Client/Service)场景中,Context无关性体现在Client对Service的每次调用都是完全不相关的。但是在有些情况下,我们却希望系统为我们创建一个Session来保留某个Client和Service的进行交互的状态。所以,像Web Service一样,WCF也提供了对Session的支持。对于WCF来说,Client和Service之间的交互都通过Soap Message来实现的,每次交互的过程就是一次简单的Message Exchange。所以从Messaging的角度来讲,WCF的Session就是把某个把相关的Message Exchange纳入同一个Conversation。每个Session用一个Session ID来唯一标识。

WCF中的Session和ASP.NET的Session

在WCF中,Session属于Service Contract的范畴,是一个相对抽象的概念,并在Service Contract定义中通过SessionModel参数来实现。他具有以下几个重要特征:

  • Session的创建和结束都有来自Client端的调用来实现

我们知道,在WCF中Client通过创建的Proxy对象来和service的交互,在默认的支持Session的情况下,Session和具体的Proxy对象绑定在一起,当Client通过调用Proxy的某个方法来访问Service的时候,Session被初始化,直到Proxy被关闭,Session被终止,我们可以通过下面两种方式来关闭Proxy:

  1. 调用System.ServiceModel. ICommunicationObject对象(我们一般通过System.ServiceModel. ChannelFactory对象的CreateChannel方法获得)的Close方法。
  2. 调用System.ServiceModel. ClientBase对象(我们一半通过继承它来实现我们为某个特定的Service创建Proxy类)的Close方法。

此外,我们也可以人为地指定通过调用Service的某个operation来初始化、或者终止Session。我们一般通过System.ServiceModel. OperationContractAttribute的IsInitiating和IsTerminating参数来指定初始化和终止Session的Operation。

  • WCF保证处于某个Session中传递的Message按照他发送的次序被接收
  • WCF并没有为Session的支持而保存相关的状态数据。

说道WCF中的Session,我们很自然地联想到ASP.NET中的Session。实际上,他们之间具有很大的差异:

  • ASP.NET的Session总是在Server端初始化的。
  • ASP.NET并不提供Ordered Message Delivery的担保。
  • ASP.NET是通过在Serer以某种方式保存State来实现对Session的支持的,比如保存在Web Server的内存中,保存在State Server甚至是SQL Server中。

WCF中的Session的实现和Instancing Management

在上面我们说了,虽然WCF支持Session,但是并没有相关的状态信息被保存在某种介质中。WCF是通过怎样的方式来支持Session的呢?这就是我们本节要讲的Instancing Management。

对于Client来说,它实际上不能和Service进行直接交互,它只能通过客户端创建的Proxy来间接地实现和service的交互。Session的表现体现在以下两种方式:

  • Session的周期和Proxy的周期绑定,这种方式体现为默认的Session支持。
  • Session的周期绑定到开始和终止Session的方法调用之间的时间内,这种方式体现在我们在定义Operation Contract时通过IsInitiating和IsTerminating显式指定开始和终止Session的Operatoin。

我们很清楚,真正的逻辑实现是通过调用真正的Service instance中。在一个分布式环境中,我们把通过Client的调用来创建最终的Service Instance的过程叫做Activation。在Remoting中我们有两种Activation方式:Server Activation(Singleton和SingleCall),Client Activation。实际上对WCF也具有相似的Activation。不过WCF不仅仅创建对应的Service Instance,而且还构建相关的Context, 我们把这些统称为Instance Context。不同的Activation方式在WCF中体现为的Instance context model。不同的Instance Context Mode体现为Proxy、Service 调用和Service Instance之间的对应关系。可以这么说,Instance Context Mode决定着不同的Session表现。在WCF中,支持以下3中不同级别的Instance Context Mode:

  • PerCall:WCF为每个Serivce调用创建 一个Service Instance,调用完成后回收该Instance。这种方式和Remoting中的SingleCall相似。
  • PerSession:在Session期间的所有Service调用绑定到某一个Service Instance,Session被终止后,Service Instance被回收。所以在Session结束后使用同一个Proxy进行调用,会抛出Exception。这种方式和Remoting中的CAO相似。
  • Singleton:这种方式和Remoting的Singelton相似。不过它的激活方式又有点特别。当为对应的Service type进行Host的时候,与之对应的Service Instance就被创建出来,此后所有的Service调用都被forward到该Instance。

WCF的默认的Instance Context Mode为PerSession,但是对于是否对Session的支持,Instancing的机制有所不同。如果通过以下的方式定义ServiceContract使之不支持Session,或者使用不支持Session的Binding(顺便说一下,Session的支持是通过建立Sessionful Channel来实现的,但是并不是所有的Binding都支持Session,比如BasicHttpBinding就不支持Session),WCF实际上会为每个Service调用创建一个Service Instance,这实质上就是PerCall的Instance Context Mode,但我为什么会说默认的是PerSession呢?我个人觉得我们可以这样地来看看Session:Session按照本意就是Client和Service之间建立的一个持续的会话状态,不过这个Session状态的持续时间有长有短,可以和Client的生命周期一样,也可以存在于某两个特定的Operation调用之间,最短的则可以看成是每次Service的调用,所以按照我的观点,PerCall也可以看成是一种特殊的Session(我知道会有很多人不认同我的这种看法。)




我的WCF之旅(8):WCF中的Session和Instancing Management_Server

   [ServiceContract(SessionMode 

=  SessionMode.NotAllowed)]

Simple

接下来我们来看看一个简单的Sample,相信大家会对Session和Instancing Management会有一个深入的认识。这个Sample沿用我们Calculator的例子,Solution的结构如下,4个Project分别用于定义SeviceContract、Service Implementation、Hosting和Client。


我的WCF之旅(8):WCF中的Session和Instancing Management_WCF_02


我们先采用默认的Session和Instance Context Modle,在这之前我们看看整个Solution各个部分的定义:

1.    Service Contract:ICalculator



我的WCF之旅(8):WCF中的Session和Instancing Management_Server

using  System;
 using
  System.Collections.Generic;
 using
  System.Text;
 using
  System.ServiceModel;

 namespace
  Artech.SessionfulCalculator.Contract
 {
    [ServiceContract]
    public interface ICalculator
    {
        [OperationContract(IsOneWay = true)]
        void  Adds(double x);

        [OperationContract]
        double GetResult();
    }
} 
 
 2.    Service Implementation:CalculatorService
 
 
 
  
using  System;
 using
  System.Collections.Generic;
 using
  System.Text;
 using
  System.ServiceModel;
 using
  Artech.SessionfulCalculator.Contract;

 namespace
  Artech.SessionfulCalculator.Service
 {
    public class CalculatorService:ICalculator
    {

        private double _result;

        ICalculator Members

        public CalculatorService()
        {
            Console.WriteLine("Calculator object has been created");
        }

        ~CalculatorService()
        {
            Console.WriteLine("Calculator object has been destoried");
        }

    }
} 
 
 为了让大家对Service Instance的创建和回收有一个很直观的认识,我特意在Contructor和Finalizer中作了一些指示性的输出。同时在每个Operation中输出的当前的Session ID
 3.    Hosting
 Program
 
 
  
using  System;
 using
  System.Collections.Generic;
 using
  System.Text;
 using
  System.ServiceModel;
 using
  Artech.SessionfulCalculator.Service;
 using
  System.Threading;

 namespace
  Artech.SessionfulCalculator.Hosting
 {
    class Program
    {
        static void Main(string[] args)
        {
            using(ServiceHost host = new ServiceHost(typeof(CalculatorService)))
            {
                host.Opened += delegate
                {
                    Console.WriteLine("The Calculator service has begun to listen
");
                };
                host.Open();
                Timer timer = new Timer(delegate { GC.Collect(); }, null, 0, 100);
                Console.Read();
            }
        }
    }
} 
 
 除了Host CalculatorService之外,我还通过一个Timer对象每隔一个很短的时间(0.1s)作一次强制的垃圾回收,使我们通过输出看出Service Instance是否被回收了。
 Configuration
 
 
  
<? xml versinotallow="1.0" encoding="utf-8"  ?> 
 <
 configuration
 >
 
   < system .serviceModel >    
     < behaviors > 
       < serviceBehaviors > 
         < behavior  name ="CalculatorBehavior" >
 
           < serviceMetadata  httpGetEnabled ="true"  
 />
 
         </ behavior > 
       </ serviceBehaviors > 
     </ behaviors > 
     < services > 
       < service  behaviorConfiguration ="CalculatorBehavior"  name
 ="Artech.SessionfulCalculator.Service.CalculatorService"
 >
 
         < endpoint  address =""  binding
 ="basicHttpBinding"
  bindingConfiguration
 =""
 
          contract ="Artech.SessionfulCalculator.Contract.ICalculator"   /> 
         < host > 
           < baseAddresses > 
             < add  baseAddress ="http://localhost:9999/SessionfulCalculator"  
 />
 
           </ baseAddresses > 
         </ host > 
       </ service > 
     </ services > 
   </ system.serviceModel > 
 </
 configuration
 >
 
 
 我们使用的是basicHttpBinding
 4.    Client
 
 
  
using  System;
 using
  System.Collections.Generic;
 using
  System.Text;
 using
  System.ServiceModel;
 using
  Artech.SessionfulCalculator.Contract;

 namespace
  Artech.SessionfulCalculator.Client
 {
    class Program
    {
        static void Main(string[] args)
        {
            ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("httpEndpoint");
            Console.WriteLine("Create a calculator proxy: proxy1");
            ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy1.Adds(1)");
            proxy1.Adds(1);
            Console.WriteLine("Invocate  proxy1.Adds(2)");
            proxy1.Adds(2);
            Console.WriteLine("The result return via proxy1.GetResult() is : {0}", proxy1.GetResult());

            Console.WriteLine("Create a calculator proxy: proxy2");
            ICalculator proxy2= calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy2.Adds(1)");
            proxy2.Adds(1);
            Console.WriteLine("Invocate  proxy2.Adds(2)");
            proxy2.Adds(2);
            Console.WriteLine("The result return via proxy2.GetResult() is : {0}", proxy2.GetResult());

            Console.Read();
        }
    }
} 
 
 我创建了两个Proxy:Proxy1和Proxy2,并以同样的方式调用它们的方法:Add->Add->GetResult。
 Configuration
 
 
  
<? xml versinotallow="1.0" encoding="utf-8"  ?> 
 <
 configuration
 >
 
     < system .serviceModel > 
         < client > 
             < endpoint  address ="http://localhost:9999/SessionfulCalculator" 
                binding ="basicHttpBinding"  contract ="Artech.SessionfulCalculator.Contract.ICalculator" 
                name ="httpEndpoint"   /> 
         </ client > 
     </ system.serviceModel > 
 </
 configuration
 >


我们来看看运行的结果:

Client端:


我的WCF之旅(8):WCF中的Session和Instancing Management_WCF_04


虽然我们我们两次调用Add方法进行累加,但是最终的结果 依然是0。这好像和我们开始所说的WCF默认的Session支持不相符,默认的Session支持是这样:Service Instance和Proxy绑定在一起,当调用Proxy的任何一个方法的时候Session开始,从此Session将会和Proxy具有一样的生命周期。但是这样的一个前提的,我们需要通过支持Session的Binding来创建我们的Sessionful Channel。显然basicHttpBinding是不支持Session的,所以WCF会采用PerCall的方式创建Service Instance。同时由于不支持Session的Binding,Session ID为null。所以我们会很容易想到,我们进行的每次Service的调用都会在Service端创建一个不同Instance,Host的输出证明了这一点。


我的WCF之旅(8):WCF中的Session和Instancing Management_Server_05


既然我们说上面的执行结构是由于不支持Session的basicHttpBinding造成的,那么我们现在来使用一个支持Session的Binding:wsHttpBinding。我们只需改变Hosting的Endpoint的配置:


我的WCF之旅(8):WCF中的Session和Instancing Management_Server

< endpoint  address =""  binding ="wsHttpBinding"  bindingConfiguration =""

我的WCF之旅(8):WCF中的Session和Instancing Management_Server

          contract ="Artech.SessionfulCalculator.Contract.ICalculator"   />

我的WCF之旅(8):WCF中的Session和Instancing Management_Server


和Client的Endpoint的配置:



我的WCF之旅(8):WCF中的Session和Instancing Management_Server

< endpoint  address ="http://localhost:9999/SessionfulCalculator"

我的WCF之旅(8):WCF中的Session和Instancing Management_Server

                binding ="wsHttpBinding"  contract ="Artech.SessionfulCalculator.Contract.ICalculator"

我的WCF之旅(8):WCF中的Session和Instancing Management_Server

                name ="httpEndpoint"   />

我的WCF之旅(8):WCF中的Session和Instancing Management_Server


现在再来看看执行的结果,首先看看Client:


我的WCF之旅(8):WCF中的Session和Instancing Management_System_13


从两个Proxy的最后 结果返回3,可以看出我们默认的Session起作用了。而且我们会容易想到,此时Server端会有两个Service Instance被创建。进一步地,由于Client的Proxy还依然存在,Service Instance也不会被回收掉,我们通过Host的输出来验证这一点:


我的WCF之旅(8):WCF中的Session和Instancing Management_System_14


从输出可以看出,Constructor来两次调用,这说明了两个Service Instance被创建,基于同一个Service Instance的调用具有相同的Session ID。没有Finalizer相应的输出,说明Service Instance依然存在。除非你在Client端Close掉Proxy。

我现在就来通过修改Client端的来Close掉Proxy:通过ICommunicationObject.Close来显式地close掉Proxy

static   void  Main( string [] args)
         {
            ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("httpEndpoint");
            Console.WriteLine("Create a calculator proxy: proxy1");
            ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy1.Adds(1)");
            proxy1.Adds(1);
            Console.WriteLine("Invocate  proxy1.Adds(2)");
            proxy1.Adds(2);
            Console.WriteLine("The result return via proxy1.GetResult() is : {0}", proxy1.GetResult());
            (proxy1 as ICommunicationObject).Close();

            Console.WriteLine("Create a calculator proxy: proxy2");
            ICalculator proxy2= calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy2.Adds(1)");
            proxy2.Adds(1);
            Console.WriteLine("Invocate  proxy2.Adds(2)");
            proxy2.Adds(2);
            Console.WriteLine("The result return via proxy2.GetResult() is : {0}", proxy2.GetResult());
            (proxy1 as ICommunicationObject).Close();

            Console.Read();
        }

那么我们现在看运行后Host的输出,就会发现Finalizer被调用了:


我的WCF之旅(8):WCF中的Session和Instancing Management_Server_15


上面演示了默认的Session和Instancing Management,我们现在来显式地制定Session Model,我们先修改ServiceContract使之不支持Session:


我的WCF之旅(8):WCF中的Session和Instancing Management_Server

[ServiceContract(SessionMode  =  SessionMode.NotAllowed)]

我的WCF之旅(8):WCF中的Session和Instancing Management_Server

     public   interface  ICalculator

我的WCF之旅(8):WCF中的Session和Instancing Management_System_18

     {

我的WCF之旅(8):WCF中的Session和Instancing Management_System_19

        [OperationContract(IsOneWay = true)]

我的WCF之旅(8):WCF中的Session和Instancing Management_System_19

        void  Adds(double x);

我的WCF之旅(8):WCF中的Session和Instancing Management_System_19


我的WCF之旅(8):WCF中的Session和Instancing Management_System_19

        [OperationContract]

我的WCF之旅(8):WCF中的Session和Instancing Management_System_19

        double GetResult();

我的WCF之旅(8):WCF中的Session和Instancing Management_Server_24

    }

我的WCF之旅(8):WCF中的Session和Instancing Management_Server


看看Client的输出:


我的WCF之旅(8):WCF中的Session和Instancing Management_System_26


从最后的结果为0可以知道Session确实没有起作用。我们说用Client基于Session的表现,其根本是Server端的Instancing。从上面可以看出,Server实际上是采用PerCall的Instance Context Model。我们可以从Hosting的输出得到验证:


我的WCF之旅(8):WCF中的Session和Instancing Management_WCF_27


上面对不支持Session作了实验,我们现在来显式地允许Session,并制定开始和终止Session的Operation:


using  System;
 using
  System.Collections.Generic;
 using
  System.Text;
 using
  System.ServiceModel;

 namespace
  Artech.SessionfulCalculator.Contract
 {
    [ServiceContract(SessionMode = SessionMode.Required)]
    public interface ICalculator
    {
        [OperationContract(IsOneWay = true, IsInitiating = true, IsTerminating = false)]
        void  Adds(double x);

        [OperationContract(IsInitiating = false,IsTerminating =true)]
        double GetResult();
    }
} 
 
 为了模拟当Session终止后继续调用Proxy的场景,我进一步修改了Client的代码:
 
 
  
class  Program
     {
        static void Main(string[] args)
        {
            ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("httpEndpoint");
            Console.WriteLine("Create a calculator proxy: proxy1");
            ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy1.Adds(1)");
            proxy1.Adds(1);
            Console.WriteLine("Invocate  proxy1.Adds(2)");
            proxy1.Adds(2);
            Console.WriteLine("The result return via proxy1.GetResult() is : {0}", proxy1.GetResult());
            Console.WriteLine("Invocate  proxy1.Adds(1)");
            try
            {
                proxy1.Adds(1);
            }
            catch (Exception ex)
            {
                Console.WriteLine("It is fail to invocate the Add after terminating session because \"{0}\"", ex.Message);
            }


            Console.WriteLine("Create a calculator proxy: proxy2");
            ICalculator proxy2= calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy2.Adds(1)");
            proxy2.Adds(1);
            Console.WriteLine("Invocate  proxy2.Adds(2)");
            proxy2.Adds(2);
            Console.WriteLine("The result return via proxy2.GetResult() is : {0}", proxy2.GetResult());
            

            Console.Read();
        }

现在看看 Client的输出结果:


我的WCF之旅(8):WCF中的Session和Instancing Management_System_28


我们发现当我们调用GetResult之后再次调用Add方法,Exception被抛出。原因很简单,因为我们把GetResult方法标识为终止Session的Operation。所以当该方法被调用之后,Session被终止,对应的Service Instance也标识为可回收对象,此时再次调用,显然不能保证有一个对应的Service Instance来Handle这个调用,显然这是不允许的。

以上我们对采用默认的Instance Context Model,不同的Session Model。现在我们反过来,在Session支持的前提下,采用不同Instance Context Model,看看结果又如何:

我们把Client端的代码回到最初的状态:


static   void  Main( string [] args)
         {
            ChannelFactory<ICalculator> calculatorChannelFactory = new ChannelFactory<ICalculator>("httpEndpoint");
            Console.WriteLine("Create a calculator proxy: proxy1");
            ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy1.Adds(1)");
            proxy1.Adds(1);
            Console.WriteLine("Invocate  proxy1.Adds(2)");
            proxy1.Adds(2);
            Console.WriteLine("The result return via proxy1.GetResult() is : {0}", proxy1.GetResult());

            Console.WriteLine("Create a calculator proxy: proxy2");
            ICalculator proxy2= calculatorChannelFactory.CreateChannel();
            Console.WriteLine("Invocate  proxy2.Adds(1)");
            proxy2.Adds(1);
            Console.WriteLine("Invocate  proxy2.Adds(2)");
            proxy2.Adds(2);
            Console.WriteLine("The result return via proxy2.GetResult() is : {0}", proxy2.GetResult());

            Console.Read();
        } 
 
 通过在Calculator Service上面运用ServiceBehavior,并指定InstanceContextMode为PerCall:
 
  
[ServiceBehavior(InstanceContextMode  =   InstanceContextMode.PerCall)]
     public   class  CalculatorService:ICalculator
 {
  

}

虽然我们ServiceContract被显式指定为支持Session,看看运行的结果是否如此:


我的WCF之旅(8):WCF中的Session和Instancing Management_System_29


看来并非如此,所以我们说client端表现出的Session实际上是对应的Instancing来实现的,现在采用PerCall的Instance Context Mode, Proxy的状态是不可能被保留的。如果现在我们把Instance Context Mode设为PerSession,运行结果将会如我们所愿,现在我就不再演示了。

我们来看看Single的Instance Context Mode:


ServiceBehavior(InstanceContextMode  =   InstanceContextMode.Single)]
 public
  
 class
  CalculatorService:ICalculator
 {
  

}

我们这次先来看Hosting的输出结果,这是在刚刚启动Hosting,Client尚未启动时的Screenshot。


我的WCF之旅(8):WCF中的Session和Instancing Management_Server_30


在这之前我们都是Client通过Proxy调用相应的Service之后,Service Instance才开始创建,但是对于InstanceContextMode.Single,Service Instance却早在Service Type被Host的时候就已经被创建了。

现在启动Client:


我的WCF之旅(8):WCF中的Session和Instancing Management_System_31


同原来不一样的是,第二个Proxy返回的结果是6而不是3,这是因为只有一个Service Instance,所有调用的状态都将保留。从Hosting的输出也可以验证这一点:

我的WCF之旅(8):WCF中的Session和Instancing Management_System_32