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的輸出也可以驗證這一點: