一、引言
由前面幾篇博文我們知道,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