程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 我的WCF之旅(8):WCF中的Session和Instancing Management

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

編輯:關於.NET

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:

調用System.ServiceModel. ICommunicationObject對象(我們一般通過System.ServiceModel. ChannelFactory對象的CreateChannel方法獲得)的Close方法。

調用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(我知道會有很多人不認同我的這種看法。)

 [ServiceContract(SessionMode=SessionMode.NotAllowed)]

Simple

接下來我們來看看一個簡單的Sample,相信大家會對Session和Instancing Management會有一個深入的認識。這個Sample沿用我們Calculator的例子,Solution的結構如下,4個Project分別用於定義SeviceContract、Service Implementation、Hosting和Client。

我們先采用默認的Session和Instance Context Modle,在這之前我們看看整個Solution各個部分的定義:

1.Service Contract:ICalculator

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#region ICalculator Members

    public void Adds(double x)
    {
      Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
      this._result += x;
    }

    public double GetResult()
    {
      Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
      return this._result;
    }

    #endregion

    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 version="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 version="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端:

雖然我們我們兩次調用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的輸出證明了這一點。

既然我們說上面的執行結構是由於不支持Session的basicHttpBinding造成的,那麼我們現在來使用一個支持Session的Binding:wsHttpBinding。我們只需改變Hosting的Endpoint的配置:

<endpoint address="" binding="wsHttpBinding" bindingConfiguration=""
     contract="Artech.SessionfulCalculator.Contract.ICalculator" />

和Client的Endpoint的配置:

<endpoint address="http://localhost:9999/SessionfulCalculator"
         binding="wsHttpBinding" contract="Artech.SessionfulCalculator.Contract.ICalculator"
        name="httpEndpoint" />

現在再來看看執行的結果,首先看看Client:

從兩個Proxy的最後 結果返回3,可以看出我們默認的Session起作用了。而且我們會容易想到,此時Server端會有兩個Service Instance被創建。進一步地,由於Client的Proxy還依然存在,Service Instance也不會被回收掉,我們通過Host的輸出來驗證這一點:

從輸出可以看出,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被調用了:

上面演示了默認的Session和Instancing Management,我們現在來顯式地制定Session Model,我們先修改ServiceContract使之不支持Session:

[ServiceContract(SessionMode = SessionMode.NotAllowed)]
  public interface ICalculator
  {
    [OperationContract(IsOneWay = true)]
    void Adds(double x);

    [OperationContract]
    double GetResult();
  }

看看Client的輸出:

從最後的結果為0可以知道Session確實沒有起作用。我們說用Client基於Session的表現,其根本是Server端的Instancing。從上面可以看出,Server實際上是采用PerCall的Instance Context Model。我們可以從Hosting的輸出得到驗證:

上面對不支持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的輸出結果:

我們發現當我們調用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,看看運行的結果是否如此:

看來並非如此,所以我們說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。

在這之前我們都是Client通過Proxy調用相應的Service之後,Service Instance才開始創建,但是對於InstanceContextMode.Single,Service Instance卻早在Service Type被Host的時候就已經被創建了。

現在啟動Client:

同原來不一樣的是,第二個Proxy返回的結果是6而不是3,這是因為只有一個Service Instance,所有調用的狀態都將保留。從Hosting的輸出也可以驗證這一點:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved