本書第四章介紹了關於執行個體管理的相關技術。“WCF支援三種執行個體啟用的類型:單調服務(Per-Call Service)會為每次的用戶端請求分配(銷毀)一個新的服務執行個體。會話服務(Sessionful Service)則為每次用戶端串連分配一個服務執行個體。最後一種是單例服務(Singleton Service),所有的用戶端會為所有的串連和啟用物件共用一個相同的服務執行個體。”
對於Per-Call Service的翻譯,我躊躇良久,最後還是決定按照Singleton服務的翻譯,將其譯為單調服務,意即為每次調用建立一個服務執行個體,與單例服務相對應。不知是否妥當?其實最好的翻譯就是保持原文不變,但對於整本書而言,如果保持英文術語,也有許多不便的地方。
執行個體模式的配置通過ServiceBehavior完成,以下是ServiceBehaviorAttribute的定義:
public enum InstanceContextMode
{
PerCall,
PerSession,
Single
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : Attribute,...
{
public InstanceContextMode InstanceContextMode
{get;set;}
//More members
}
單調服務(Per-Call Service)
單調服務:
執行步驟如下:
1. 用戶端調用代理,代理將調用轉寄給服務。
2. WCF建立一個服務執行個體,然後調用服務執行個體的方法。
3. 當方法調用返回時,如果對象實現了IDisposable介面,WCF將調用IDisposable.Dispose()方法。
4. 用戶端調用代理,代理將調用轉寄給服務。
5. WCF建立一個對象,然後調用對象的方法。
單調服務的一個最重要優勢在於它能夠節省資源,支援系統的延展性。由於服務執行個體的生命週期只存在於一次調用期間,特別對於那些持有昂貴資源的服務執行個體而言,這種方式可以有效地提高系統效能。而且,銷毀服務執行個體時,WCF不會斷開與用戶端(通過用戶端的代理)的串連,這比建立執行個體與串連所消耗的資源要少得多。
單調服務體現的優勢在事務編程與佇列服務中更為明顯,它可以保證在事務編程中執行個體狀態的同步;而對於隊列斷開調用而言,則單調服務能夠建立服務執行個體與離散隊列訊息之間的簡易對應。
單調服務的配置通過ServiceBehavior,如下所示:
[ServiceContract]
interface IMyContract
{...}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{...}
注意,ServiceBehavior特性只能應用到類上面,這在前面介紹的特性定義可以看出。實際上,這也是合理的約束,因為如果將ServiceBehavior應用到介面上,由於介面是不能執行個體化的,自然會出現錯誤。
單調服務執行個體是狀態相關的,但是由於執行個體會在調用之初建立,而在調用之後被銷毀,因而這樣的單調服務執行個體是無法儲存狀態的。為瞭解決這一問題,我們可以引入資料庫或者檔案儲存狀態,或者利用全域變數臨時儲存狀態。那麼為了擷取狀態,潛在的含義是單調服務的每個操作應該定義一個參數,用來傳遞狀態或狀態的ID。
書中有一句話非常重要:“如果單調服務真的與狀態無關,就根本不需要單調啟用模式。準確地講,正是因為狀態,特別是代價昂貴的狀態,才需要使用單調模式。”很多時候,我們認為單調服務執行個體的生命週期存在於每次調用,因而想當然的認為這樣的服務執行個體應該是無狀態的。這樣的操作可能只是簡單執行某項任務,而不會對服務物件的屬性進行操作。表面看起來確實如此,但在這裡我們卻忽略了我們採用單調服務的根本原因,是在於單調啟用模式的本質就在於能夠適時釋放執行個體所持有的昂貴資源,這裡的資源大體上講就是一種狀態。如果不需要維護狀態,則以為著效能上沒有太大的損耗,我們就沒有必要採用單調啟用模式了,畢竟頻繁地建立與銷毀執行個體,仍然會對效能造成一定的影響。
對於WCF服務而言,單調服務可以算是最佳的執行個體啟用模式。書中介紹:“一個有力的論據是單調服務更利於系統的延展性。為了更好的支援延展性,服務設計有一個黃金法則是10X,即設計出的每個服務應該能夠處理至少多於需求一個量級以上的負載。這是一個工程學準則,工程師在設計系統時,絕不能夠“鼠目寸光”,只考慮當前指定負載的處理。如果一幢大樓,只能夠支撐當前需求確定的承重,還會有人膽敢居住嗎?如果一座電梯,只能夠承受規定的六位乘客的重量,還會有人願意乘坐嗎?軟體系統同樣如此。為什麼不能針對當前指定的負載進行系統設計?假設採用這樣的設計方式,一旦系統的每位使用者增加了業務量,那麼系統就會變得岌岌可危。設計良好的系統必須能夠經久不衰,經得起時間的考驗。為了實現這一目的,就需要應用10X的黃金法則,有效地利用單調服務所能提供的延展性。採用單調服務的另一個有力論據是關於事務的處理。正如第7章介紹的那樣,事務絕對是每個系統所必需的,單調服務有利於實現事務編程模型,而不用考慮系統的負載。”
會話服務
從執行方式與啟用方式來看,會話服務相當於.NET Remoting中的用戶端啟用模式。也就是為每個用戶端建立一個專門的服務執行個體。只要會話沒有結束,該執行個體就不會被銷毀。
“用戶端工作階段是一個代理對應一個服務端點。如果用戶端為相同或不同的終結點建立了另外的代理,則建立的代理就會與新的執行個體和會話建立關聯。”根據這句話的內容,可以理解到對於會話服務而言,是一個用戶端代理對應一個服務執行個體。也就是說,會話服務中的服務是與代理相對應的,而不是對應於一個用戶端。這是它與.NET Remoting的用戶端啟用模式不同的地方。
此外,會話服務存在延展性的問題。由於每個用戶端都需要維護一個會話,如果存在多個獨立的用戶端,則建立專門的服務執行個體的代價太大。
配置會話服務的方式仍然是使用ServiceBehavior特性,如下所示:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
class MyService : IMyContract
{...}
然而,InstanceContextMode的預設值為InstanceContextMode.PerSession,如果沒有設定InstanceContextMode,則服務預設為會話服務。
僅僅為服務配置InstanceContextMode是不夠的,因為會話服務必須要求用戶端維持一個會話,這就需要讓用戶端的WCF運行時知道服務是否使用了會話,因此,我們需要通過ServiceContract特性提供的SessionMode屬性,設定服務契約。SessionMode的定義如下:
public enum SessionMode
{
Allowed,
Required,
NotAllowed
}
“SessionMode的預設值為SessionMode.Allowed。當用戶端匯入契約中繼資料時,服務中繼資料將包含SessionMode值,並會如實地反映它的內容。”
如果服務的SessionMode被配置為SessionMode.Allowed,並不必然代表格服務為會話服務。以下是對各種情況的說明:
1、 如果服務被配置為單調服務,則服務與SessionMode無關;
2、 如果服務被配置為會話服務,且SessionMode為Allowed,則:
(1)如果服務使用的綁定為BasicHttpBinding,服務為單調服務;
(2)如果服務使用的綁定為沒有包含安全與可靠訊息傳輸的WSHttpBinding綁定,服務為單調服務;
(3)如果服務使用的WSHttpBinding綁定包含了安全(為預設配置)或者可靠的訊息傳輸,或者使用NetTcpBinding綁定、NetNamedPipeBinding綁定,服務為會話服務。
當SessionMode為Required時,服務不能使用BasicHttpBinding綁定或者沒有包含安全與可靠訊息傳輸的WSHttpBinding綁定,在裝載服務時會對此進行驗證。作者建議,“若要設計一個會話契約,我主張使用SessionMode.Required,而非SessionMode.Allowed預設值。”
如果SessionMode為NotAllowed,則不管服務配置如何,它總是採用單調服務方式。但如果契約使用了NetTcpBinding或NetNamedPipeBinding綁定,則不能將服務的SessionMode配置為NotAllowed。作者的建議是“是在選擇使用SessionMode.NotAllowed的同時,總是將服務配置為單調服務”。
應該避免將單調服務與會話契約混合定義在相同的會話服務類型中,即使WCF允許這樣的配置:
[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{...}
[ServiceContract(SessionMode = SessionMode.NotAllowed)]
interface IMyOtherContract
{...}
//Avoid
class MyService : IMyContract,IMyOtherContract
{...}
會話應該保證是可靠的,一個實現了會話契約的服務,它包含的所有終結點所公開的契約都應該使用支援可靠傳輸會話的綁定。
“通常,一旦用戶端關閉了代理,會話就會終止。但是,用戶端也可以強行終止會話,也可能因為通訊故障而終止會話。每個會話還包含了一個空閑逾時值,預設為10分鐘。如果用戶端在10分鐘內沒有任何操作,那麼即使用戶端期望繼續使用該會話,會話仍然會自動終止。會話如果是因為空白閑逾時的原因被終止,那麼當用戶端試圖使用它的代理時,會獲得一個CommunicationObjectFaultedException異常。在綁定中通過配置不同的值,可以為用戶端和服務配置不同的逾時值。支援可靠傳輸層會話的綁定提供了ReliableSession屬性,類型為ReliableSession或者OptionalReliableSession。ReliableSession類定義了InactivityTimeout屬性,屬於TimeSpan類型,通過它可以配置一個新的空閑逾時值。”
注意,InactivityTimeout屬性的預設值為10分鐘。不能將該值設定為小於或等於0的值,否則會拋出ArgumentOutOfRangeException異常。
例如,下面的代碼利用編程方式將TCP綁定的空閑逾時值配置為25分鐘:
NetTcpBinding tcpSessionBinding = new NetTcpBinding( );
tcpSessionBinding.ReliableSession.Enabled = true;
tcpSessionBinding.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(25);
這等同於配置config檔案:
<netTcpBinding>
<binding name = "TCPSession">
<reliableSession enabled = "true" inactivityTimeout = "00:25:00"/>
</binding>
</netTcpBinding>
如果用戶端與服務都配置了逾時值,則以短的逾時值為準。
單例服務
如果我們熟悉設計模式,可以以單例模式的方式思考單例服務。所謂單例服務,就是針對所有用戶端而言,都只有一個服務執行個體。“單例服務的生存期是無限的,只有在關閉宿主時,才會被釋放。建立宿主時,單例服務會被建立,並且只能被建立一次。”
可以通過InstanceContextMode.Single的InstanceContextMode屬性配置單例服務:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class MySingleton : ...
{...}
只要是單例服務,即使該服務支援多個契約,這些契約中有的需要會話,有的不需要會話,在不同終結點的調用仍然是通過相同的執行個體進行傳遞。即使關閉了代理,也不會終止單例服務。
在執行個體化單例服務物件時,可能需要執行一些初始化的工作。如果使用預設的建構函式,並通過ServiceHost託管服務,是沒有辦法做到這一點的。當然,我們也可以在預設建構函式中實現這些初始化的工作,然而如果初始化工作需要一些特別的定製步驟,特別是需要操作狀態或者需要傳入參數時,預設的建構函式就顯得捉襟見肘了。
WCF提供了另外一種初始化單例服務的辦法,就是利用ServiceHost類提供的專門的建構函式,可以接收一個object對象:
public class ServiceHost : ServiceHostBase,...
{
public ServiceHost(object singletonInstance,
params Uri[] baseAddresses);
public virtual object SingletonInstance
{get;}
//More members
}
注意,建構函式中的singletonInstance必須是配置為單例方式的服務物件。因此,初始化以及託管單例服務的方式可以如下實現:
//Service code
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod( );
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class MySingleton : IMyContract
{
int m_Counter = 0;
public int Counter
{
get
{
return m_Counter;
}
set
{
m_Counter = value;
}
}
public void MyMethod( )
{
m_Counter++;
Trace.WriteLine("Counter = " + Counter);
}
}
//Host code
MySingleton singleton = new MySingleton( );
singleton.Counter = 42;
ServiceHost host = new ServiceHost(singleton);
host.Open( );
//Do some blocking calls then
host.Close( );
//Client code
MyContractClient proxy = new MyContractClient( );
proxy.MyMethod( );
proxy.Close( );
//Output:
Counter = 43
很顯然,ServiceHost提供的這個建構函式還有改進的餘地,那就是object型別參數顯然不具備型別安全,因而本書作者定義了新的ServiceHost類,引入了泛型:
public class ServiceHost<T> : ServiceHost
{
public ServiceHost(T singleton,params Uri[] baseAddresses)
: base(singleton,baseAddresses)
{}
public virtual T Singleton
{
get
{
if(SingletonInstance == null)
{
return default(T);
}
return (T)SingletonInstance;
}
}
//More members
}
單例服務與延展性之間的關係可謂“水火不容”。因而除非是在特殊情況,應盡量避免使用單例服務。