[WCF-Discovery]服務如何能被”發現”
要讓作為服務消費者的客戶端能夠動態地發現可用的服務,首先的要求服務本身具有可被發現的特性。那么到底一個可被發現的服務和一個一般的服務有何不同呢?或者說如何讓一個一般的服務在寄宿的時候能夠被它潛在的消費者“探測”到呢?
我們知道,WCF本質上就是消息交換的通信框架。不論是針對普通的服務操作的調用,還是定義在WS-Discovery中的服務的探測(Probe/PM)和解析(Resolve/RM),本質上都是一種消息的交換。它們并沒有本質的不同,或者說唯一不同的就是消息的內容,前者是基于某個服務操作的請求和回復,而后者這是針對服務相關元信息的請求和回復。
從消息交換的角度講,服務發現和元數據的獲取比較類似,因為它們都交換的消息都關于服務本身的一些信息。在《如何將一個服務發布成WSDL》中,我們介紹了元數據的發布具有兩種不同的方式:HTTP-GET和MEX終結點。服務發現機制對服務信息交換的實現與基于MEX終結點進行服務元數據交換的實現比較類似,因為它也需要一個特殊類型的終結點,即DiscoveryEndpoint。
一、DiscoveryEndpoint
在前面介紹“標準終結點”的時候,我們列出的一系列標準終結點列表中就有一個DiscoveryEndpoint。一個服務必須具有一個DiscoveryEndpoint才能成為一個可被發現的服務,而客戶端也正是通過DiscoveryEndpoint來發現這相應的服務的。為了能夠更加深刻地認識這個標準終結點,我們不妨先來看看它的定義。
public class DiscoveryEndpoint : ServiceEndpoint { //其他成員 public DiscoveryEndpoint(); public DiscoveryEndpoint(Binding binding, EndpointAddress endpointAddress); public DiscoveryEndpoint(DiscoveryVersion discoveryVersion, ServiceDiscoveryMode discoveryMode); public DiscoveryEndpoint(DiscoveryVersion discoveryVersion, ServiceDiscoveryMode discoveryMode, Binding binding, EndpointAddress endpointAddress); public ServiceDiscoveryMode DiscoveryMode { get; } public DiscoveryVersion DiscoveryVersion { get; } public TimeSpan MaxResponseDelay { get; set; } }
WS-Discovery版本通過類型DiscoveryVersion表示,下面的代碼片斷給出了DiscoveryVersion的定義。DiscoveryVersion具有三個靜態的只讀屬性分別代表了三個主要的WS-Discovery版本。其中WSDiscoveryApril2005和WSDiscovery11代表兩個正式的版本1.0和1.1,而WSDiscoveryCD1則代表在2009年1月份針對WS-Discovery 1.1的第一個委員會草案(CD:Committee Draft)
public sealed class DiscoveryVersion { //其他成員 public string Name { get; } public string Namespace {get; } public Uri AdhocAddress { get; } public MessageVersion MessageVersion { get; } public static DiscoveryVersion WSDiscovery11 { get; } public static DiscoveryVersion WSDiscoveryApril2005 { get; } public static DiscoveryVersion WSDiscoveryCD1 { get; } }
而我們之前介紹的兩種典型的服務發現模式(《[WCF-Discovery] WCF-Discovery的協議基礎:WS-Discovery》),即Ad-Hoc和Managed則定義在枚舉ServiceDiscoveryMode中,該枚舉定義如下。在默認情況下,DiscoveryEndpoint的DiscoveryMode屬性值為Managed。
public enum ServiceDiscoveryMode { Adhoc, Managed }
DiscoveryEndpoint endpoint1; DiscoveryEndpoint endpoint2; Console.WriteLine("{0,-25}{1,-35}{2, -30}", "", "Ad-Hoc", "Managed"); endpoint1 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryApril2005, ServiceDiscoveryMode.Adhoc); endpoint2 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryApril2005, ServiceDiscoveryMode.Managed); Console.WriteLine("{0,-25}{1,-35}{2, -30}", "WSDiscoveryApril2005", endpoint1.Contract.ContractType.Name, endpoint2.Contract.ContractType.Name); endpoint1 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscovery11, ServiceDiscoveryMode.Adhoc); endpoint2 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscovery11, ServiceDiscoveryMode.Managed); Console.WriteLine("{0,-25}{1,-35}{2, -30}", "WSDiscovery11", endpoint1.Contract.ContractType.Name, endpoint2.Contract.ContractType.Name); endpoint1 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryCD1, ServiceDiscoveryMode.Adhoc); endpoint2 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryCD1, ServiceDiscoveryMode.Managed); Console.WriteLine("{0,-25}{1,-35}{2, -30}", "WSDiscoveryCD1", endpoint1.Contract.ContractType.Name, endpoint2.Contract.ContractType.Name);
Ad-Hoc | Managed | |
WSDiscoveryApril2005 | IDiscoveryContractAdhocApril2005 | IDiscoveryContractManagedApril2005 |
WSDiscovery11 | IDiscoveryContractAdhoc11 | IDiscoveryContractManaged11 |
WSDiscoveryCD1 | IDiscoveryContractAdhocCD1 | IDiscoveryContractManagedCD1 |
上述的6個契約類型對應著6個接口。不過,這些都是內部接口,并不對外公布,不過我們可以通過Reflector察看它們的定義。現在我們就來簡單看看針對WS-Discovery 1.1下分別針對Ad-Hoc和Managed模式的服務契約接口IDiscoveryContractAdhoc11和IDiscoveryContractManaged11的定義。
IDiscoveryContractAdhoc11:
[ServiceContract(Name = "TargetService", Namespace = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01", CallbackContract = typeof(IDiscoveryResponseContract11))] internal interface IDiscoveryContractAdhoc11 { [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe", IsOneWay = true, AsyncPattern = true)] IAsyncResult BeginProbeOperation(ProbeMessage11 request, AsyncCallback callback, object state); [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Resolve", IsOneWay = true, AsyncPattern = true)] IAsyncResult BeginResolveOperation(ResolveMessage11 request, AsyncCallback callback, object state); void EndProbeOperation(IAsyncResult result); void EndResolveOperation(IAsyncResult result); [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe", IsOneWay = true)] void ProbeOperation(ProbeMessage11 request); [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Resolve", IsOneWay = true)] void ResolveOperation(ResolveMessage11 request); }
[ServiceContract(Name = "DiscoveryProxy", Namespace = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01")] internal interface IDiscoveryContractManaged11 { [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe", ReplyAction = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/ProbeMatches", AsyncPattern = true)] IAsyncResult BeginProbeOperation(ProbeMessage11 request, AsyncCallback callback, object state); [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Resolve", ReplyAction = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/ResolveMatches", AsyncPattern = true)] IAsyncResult BeginResolveOperation(ResolveMessage11 request, AsyncCallback callback, object state); ProbeMatchesMessage11 EndProbeOperation(IAsyncResult result); ResolveMatchesMessage11 EndResolveOperation(IAsyncResult result); [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe", ReplyAction = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/ProbeMatches")] ProbeMatchesMessage11 ProbeOperation(ProbeMessage11 request); [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Resolve", ReplyAction = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/ResolveMatches")] ResolveMatchesMessage11 ResolveOperation(ResolveMessage11 request); }
我們說服務契約本質上定義了采用的消息交換模式和被交換的消息的格式。對于客戶端驅動的惡服務發現來說,采用的服務交換不外乎兩種類型:服務探測(Probe/PM)和服務解析(Resolve/RM),這在前面針對WS-Discovery的部分有過詳細的介紹。所以,服務契約IDiscoveryContractAdhoc11和IDiscoveryContractManaged11實際定義了兩組代表著這兩種消息交換類型的操作ProbeOperation和ResolveOperation,一組是同步操作,另一組是異步操作。至于契約的名稱、命名空間,以及操作的Action,ReplyAction在通過相應的ServiceContractAttribute和OperationContractAttribute特性進行相應的定義,以確保和WS-Discovery 1.1規范保持一致。
除了DiscoveryMode和DiscoveryVersion這兩個只讀屬性,DiscoveryEndpoint還具有一個可讀可寫的屬性MaxResponseDelay,表示服務相應Probe請求的PM消息延遲發送允許的時間范圍。在此MaxResponseDelay屬性規定的時間范圍內,服務的用于響應單個Probe請求的所有PM都將發送出去。如果同時發送所有的PM,則可能發生網絡風暴(Network Storming)。為了防止發生這種情況,響應服務在每個PM發送之間具有一個隨機延遲。隨機延遲的范圍是從0到MaxResponseDelay。如果MaxResponseDelay設置為 0(默認值),則在不使用任何延遲的緊湊循環中發送PM消息。否則,在發送PM消息時將應用隨機延遲,以便發送所有PM消息所用的總時間不會超過MaxResponseDelay。
如果你采用編程的方式使用DiscoveryEndpoint,你可以通過在構造函數中傳入相應的參數決定采用的WS-Discovery版本和服務發現模式,并通過屬性賦值的方式決定MaxResponseDelay的值。如果采用配置的方式,這個標準終結點對應的配置元素也同樣提供相應的配置屬性。
<standardEndpoints> <udpDiscoveryEndpoint> <standardEndpoint name="adhocDiscoveryEndpointConfiguration" discoveryVersion="WSDiscovery11" maxResponseDelay="00:00:00.600"/> </udpDiscoveryEndpoint> </standardEndpoints>
由于DiscoveryEndpoint需要顯式地指定其地址,所以它只能以單播的方式進行消息交換。由于WS-Discovery中的Ad-Hoc模式采用廣播形式的消息交換,為此WCF為我們創建另一個標準的終結點UdpDiscoveryEndpoint。如下面的代碼片斷所示,UdpDiscoveryEndpoint具有兩個基本的屬性MulticastAddress和TransportSettings。前者代表采用的廣播地址,默認值為“soap.udp://239.255.255.250:3702”,該值也是代表默認IPV4廣播地址的靜態只讀屬性DefaultIPv4MulticastAddress的值。而另一個代表IPV6默認廣播地址的只讀屬性DefaultIPv4MulticastAddress的值為“soap.udp://[FF02::C]:3702”。后者代表針對UDP傳輸層的相關設置。
public class UdpDiscoveryEndpoint : DiscoveryEndpoint { //其他成員 public static readonly Uri DefaultIPv4MulticastAddress; public static readonly Uri DefaultIPv6MulticastAddress; public Uri MulticastAddress { get; set; } public UdpTransportSettings TransportSettings { get; } }
public class UdpTransportSettings { public int DuplicateMessageHistoryLength { get; set; } public long MaxBufferPoolSize { get; set; } public int MaxMulticastRetransmitCount { get; set; } public int MaxPendingMessageCount { get; set; } public long MaxReceivedMessageSize { get; set; } public int MaxUnicastRetransmitCount { get; set; } public string MulticastInterfaceId { get; set; } public int SocketReceiveBufferSize { get; set; } public int TimeToLive { get; set; } }
- DuplicateMessageHistoryLength:傳輸用于標識重復消息的最大消息哈希數,默認值為 4112;
- MaxBufferPoolSize:傳輸使用的任何緩沖池的最大大小,默認值為524288;
- MaxMulticastRetransmitCount:應重新傳輸多播消息的最大次數(第一次發送除外),默認值為2;
- MaxPendingMessageCount:已經接收但尚未從每個通道實例的輸入隊列中移除的消息的最大數量,默認值為32;
- MaxReceivedMessageSize:綁定可處理的消息的最大大小,默認值為65507;
- MaxUnicastRetransmitCount:應重新傳輸單播消息的最大次數(第一次發送除外),默認值為1;
- MulticastInterfaceId:該值唯一地標識在發送和接收多播消息時所使用的網絡適配器,默認值為null;
- SocketReceiveBufferSize:基礎 WinSock 套接字上的接收緩沖區的大小,默認值為55536;
- TimeToLive:多播數據包可以遍歷的網絡段躍點數,默認值為1。
標準終結點UdpDiscoveryEndpoint對應的配置元素同樣定義了相應的配置屬性是你能過對它采用的廣播地址以及UDP傳輸層進行自由的配置。下面給出了一個配置實例。
<udpDiscoveryEndpoint> <standardEndpoint name="adhocDiscoveryEndpointConfiguration" discoveryVersion="WSDiscovery11"> <transportSettings duplicateMessageHistoryLength="2048" maxPendingMessageCount="5" maxReceivedMessageSize="8192" maxBufferPoolSize="262144"/> </standardEndpoint> </udpDiscoveryEndpoint>
我們之前就已經說,客戶端用于獲取可用服務發起的請求,和基于普通服務調用的消息請求并沒有本質的不同。匹配的服務在接收到客戶端發送的Probe/Resolve請求后,會將自己的信息包含在PM/RM消息中進行回復。現在我們討論是一個核心的問題:消息的內容如何產生?
對于普通的服務調用,回復消息的內容最初來源于針對服務實例的操作方法的調用的結果。針對服務發現的Probe/Resolve請求也是一樣,服務端依然存在一個用于返回目標服務信息的“發現服務”,并且這個服務的實現了添加到目標服務的DiscoveryEndpoint的契約接口。這個服務的類型就是抽象類DiscoveryService的子類。DiscoveryService的定于如下,可見它實現了DiscoveryEndpoint基于不同WS-Discovery版本在Ad-Hoc和Managed模式下的6個契約接口。
public abstract class DiscoveryService : IDiscoveryContractAdhocApril2005, IDiscoveryContractManagedApril2005, IDiscoveryContractAdhoc11, IDiscoveryContractManaged11, IDiscoveryContractAdhocCD1, IDiscoveryContractManagedCD1, ... { //省略成員 }
知道真正用于實現服務發現的服務,我們需要考慮另一個問題:這個繼承自DiscoveryService的發現服務在接收到服務發現請求后被激活的。當用于寄宿服務的ServiceHost對象被開啟之后,服務的每個終結點都會轉換成一個EndpointDispatcher對象,這當然也包括上述的DiscoveryEndpoint。激活的服務實例被封裝在一個InstanceContext對象中,而服務對象和用于封裝服務對象的InstanceContext分別通過針對EndpointDispatcher對DispatchRuntime的兩個特殊的組件來提供,即InstanceProvider和InstanceContextProvider。如果我們能夠自定義用于激活發現服務的InstanceProvider和InstanceContextProvider,并且通過擴展將其應用到針對DiscoveryEndpoint的DispatchRuntime上,就能夠徹底解決發現服務的激活問題。右圖大體上揭示了整個發現服務的激活機制。
在WCF的具體實現中,這個自定義的InstanceProvider和InstanceContextProvider是一個內部的類型ServiceDiscoveryInstanceContextProvider(它同時實現了IInstanceProvider和IInstanceContextProvider兩個接口)。而最終將它應用到DiscoveryEndpoint對應的EndpointDispatcher的則是通過一個服務行為來實現的,這個服務行為的類型是System.ServiceModel.Discovery.ServiceDiscoveryBehavior。所以,一個服務能夠成為一個可被發現的服務除了具有一個DiscoveryEndpoint之外,還必須應用這個ServiceDiscoveryBehavior服務行為。