[WCF-Discovery]服务如何能被”发现”
要让作为服务消费者的客户端能够动态地发现可用的服务,首先的要求服务本身具有可被发现的特性。那么到底一个可被发现的服务和一个一般的服务有何不同呢?或者说如何让一个一般的服务在寄宿的时候能够被它潜在的消费者“探测”到呢?
我们知道,WCF本质上就是消息交换的通信框架。不论是针对普通的服务操作的调用,还是定义在WS-Discovery中的服务的探测(Probe/PM)和解析(Resolve/RM),本质上都是一种消息的交换。它们并没有本质的不同,或者说唯一不同的就是消息的内容,前者是基于某个服务操作的请求和回复,而后者这是针对服务相关元信息的请求和回复。
从消息交换的角度讲,服务发现和元数据的获取比较类似,因为它们都交换的消息都关于服务本身的一些信息。在《如何将一个服务发布成WSDL》中,我们介绍了元数据的发布具有两种不同的方式:HTTP-GET和MEX终结点。服务发现机制对服务信息交换的实现与基于MEX终结点进行服务元数据交换的实现比较类似,因为它也需要一个特殊类型的终结点,即DiscoveryEndpoint。
一、DiscoveryEndpoint
在前面介绍“标准终结点”的时候,我们列出的一系列标准终结点列表中就有一个DiscoveryEndpoint。一个服务必须具有一个DiscoveryEndpoint才能成为一个可被发现的服务,而客户端也正是通过DiscoveryEndpoint来发现这相应的服务的。为了能够更加深刻地认识这个标准终结点,我们不妨先来看看它的定义。
1: public class DiscoveryEndpoint : ServiceEndpoint
2: {
3: //其他成员
4: public DiscoveryEndpoint();
5: public DiscoveryEndpoint(Binding binding, EndpointAddress endpointAddress);
6: public DiscoveryEndpoint(DiscoveryVersion discoveryVersion, ServiceDiscoveryMode discoveryMode);
7: public DiscoveryEndpoint(DiscoveryVersion discoveryVersion, ServiceDiscoveryMode discoveryMode, Binding binding, EndpointAddress endpointAddress);
8:
9: public ServiceDiscoveryMode DiscoveryMode { get; }
10: public DiscoveryVersion DiscoveryVersion { get; }
11: public TimeSpan MaxResponseDelay { get; set; }
12: }
对于一个终结点来说,当然也包括标准终结点,其核心永远是地址、绑定和契约三要素。从上面的代码片断中我们不难发现,通过构造函数我们可以为DiscoveryEndpoint指定地址和绑定,却无从指定其契约。无需显式指定终结点契约是我们为何要定义这么一个标准终结点的根本目的,而终结点最终采用的契约取决于两个要素:其一、采用的WS-Discovery的版本;其二、选择的服务发现的模式(Ad-Hoc或者Managed)。
WS-Discovery版本通过类型DiscoveryVersion表示,下面的代码片断给出了DiscoveryVersion的定义。DiscoveryVersion具有三个静态的只读属性分别代表了三个主要的WS-Discovery版本。其中WSDiscoveryApril2005和WSDiscovery11代表两个正式的版本1.0和1.1,而WSDiscoveryCD1则代表在2009年1月份针对WS-Discovery 1.1的第一个委员会草案(CD:Committee Draft)
1: public sealed class DiscoveryVersion
2: {
3: //其他成员
4: public string Name { get; }
5: public string Namespace {get; }
6: public Uri AdhocAddress { get; }
7: public MessageVersion MessageVersion { get; }
8:
9: public static DiscoveryVersion WSDiscovery11 { get; }
10: public static DiscoveryVersion WSDiscoveryApril2005 { get; }
11: public static DiscoveryVersion WSDiscoveryCD1 { get; }
12: }
DiscoveryVersion具有四个只读属性,Name表示相应版本的名称。对于表示上述三个版本的DiscoveryVersion对象,该属性的值分别为WSDiscoveryApril2005、WSDiscovery11和WSDiscoveryCD1(与上述的三个静态只读属性的名称一致)。而Namespace和AdhocAddress则表示具体版本的WS-Discovery中采用的命名空间和在Ad-Hoc模式下各种广播消息中使用的目标地址(即基于WS-Addressing的<To>报头携带的地址)。MessageVersion表示选择的消息版本(SOAP版本加上WS-Addressing的版本)。默认情况下,DiscoveryEndpoint的DiscoveryVersion属性为WSDiscovery11。
而我们之前介绍的两种典型的服务发现模式(《[WCF-Discovery] WCF-Discovery的协议基础:WS-Discovery》),即Ad-Hoc和Managed则定义在枚举ServiceDiscoveryMode中,该枚举定义如下。在默认情况下,DiscoveryEndpoint的DiscoveryMode属性值为Managed。
1: public enum ServiceDiscoveryMode
2: {
3: Adhoc,
4: Managed
5: }
我们说DiscoveryEndpoint采用的契约由WS-Discovery的版本和服务发现模式决定,那么对于这两个要素的不同组合,最终被选用的终结点契约类型是什么呢?为此,我编写了如下一段测试程序:基于不同的DiscoveryVersion和ServiceDiscoveryMode创建DiscoveryEndpoint对象,最终打印出终结点契约类型的名称。
1: DiscoveryEndpoint endpoint1;
2: DiscoveryEndpoint endpoint2;
3: Console.WriteLine("{0,-25}{1,-35}{2, -30}", "", "Ad-Hoc", "Managed");
4:
5: endpoint1 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryApril2005, ServiceDiscoveryMode.Adhoc);
6: endpoint2 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryApril2005, ServiceDiscoveryMode.Managed);
7: Console.WriteLine("{0,-25}{1,-35}{2, -30}", "WSDiscoveryApril2005", endpoint1.Contract.ContractType.Name, endpoint2.Contract.ContractType.Name);
8:
9: endpoint1 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscovery11, ServiceDiscoveryMode.Adhoc);
10: endpoint2 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscovery11, ServiceDiscoveryMode.Managed);
11: Console.WriteLine("{0,-25}{1,-35}{2, -30}", "WSDiscovery11", endpoint1.Contract.ContractType.Name, endpoint2.Contract.ContractType.Name);
12:
13: endpoint1 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryCD1, ServiceDiscoveryMode.Adhoc);
14: endpoint2 = new DiscoveryEndpoint(DiscoveryVersion.WSDiscoveryCD1, ServiceDiscoveryMode.Managed);
15: Console.WriteLine("{0,-25}{1,-35}{2, -30}", "WSDiscoveryCD1", endpoint1.Contract.ContractType.Name, endpoint2.Contract.ContractType.Name);
我们将输出的结果通过如下面的表格来表示。从中我们不难发现,针对不同的WS-Discovery版本和服务发现模式组合,最终选择的服务契约类型都是不同的。服务契约类型的名称的命名规则为IDiscoveryContract{Adhoc/Managed}{Discovery Version}。
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:
1: [ServiceContract(Name = "TargetService", Namespace = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01", CallbackContract = typeof(IDiscoveryResponseContract11))]
2: internal interface IDiscoveryContractAdhoc11
3: {
4: [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe", IsOneWay = true, AsyncPattern = true)]
5: IAsyncResult BeginProbeOperation(ProbeMessage11 request, AsyncCallback callback, object state);
6: [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Resolve", IsOneWay = true, AsyncPattern = true)]
7: IAsyncResult BeginResolveOperation(ResolveMessage11 request, AsyncCallback callback, object state);
8: void EndProbeOperation(IAsyncResult result);
9: void EndResolveOperation(IAsyncResult result);
10: [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe", IsOneWay = true)]
11: void ProbeOperation(ProbeMessage11 request);
12: [OperationContract(Action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Resolve", IsOneWay = true)]
13: void ResolveOperation(ResolveMessage11 request);
14: }
IDiscoveryContractManaged11:
1: [ServiceContract(Name = "DiscoveryProxy", Namespace = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01")]
2: internal interface IDiscoveryContractManaged11
3: {
4: [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)]
5: IAsyncResult BeginProbeOperation(ProbeMessage11 request, AsyncCallback callback, object state);
6: [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)]
7: IAsyncResult BeginResolveOperation(ResolveMessage11 request, AsyncCallback callback, object state);
8: ProbeMatchesMessage11 EndProbeOperation(IAsyncResult result);
9: ResolveMatchesMessage11 EndResolveOperation(IAsyncResult result);
10: [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")]
11: ProbeMatchesMessage11 ProbeOperation(ProbeMessage11 request);
12: [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")]
13: ResolveMatchesMessage11 ResolveOperation(ResolveMessage11 request);
14: }
我们说服务契约本质上定义了采用的消息交换模式和被交换的消息的格式。对于客户端驱动的恶服务发现来说,采用的服务交换不外乎两种类型:服务探测(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的值。如果采用配置的方式,这个标准终结点对应的配置元素也同样提供相应的配置属性。
1: <standardEndpoints>
2: <udpDiscoveryEndpoint>
3: <standardEndpoint name="adhocDiscoveryEndpointConfiguration" discoveryVersion="WSDiscovery11" maxResponseDelay="00:00:00.600" />
4: </udpDiscoveryEndpoint>
5: </standardEndpoints>
二、UdpDiscoveryEndpoint
由于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传输层的相关设置。
1: public class UdpDiscoveryEndpoint : DiscoveryEndpoint
2: {
3: //其他成员
4: public static readonly Uri DefaultIPv4MulticastAddress;
5: public static readonly Uri DefaultIPv6MulticastAddress;
6:
7: public Uri MulticastAddress { get; set; }
8: public UdpTransportSettings TransportSettings { get; }
9: }
而针对UDP传输层的设置又具有若干选项,这可以从UdpTransportSettings的属性成员的定义就可以看出来。
1: public class UdpTransportSettings
2: {
3: public int DuplicateMessageHistoryLength { get; set; }
4: public long MaxBufferPoolSize { get; set; }
5: public int MaxMulticastRetransmitCount { get; set; }
6: public int MaxPendingMessageCount { get; set; }
7: public long MaxReceivedMessageSize { get; set; }
8: public int MaxUnicastRetransmitCount { get; set; }
9: public string MulticastInterfaceId { get; set; }
10: public int SocketReceiveBufferSize { get; set; }
11: public int TimeToLive { get; set; }
12: }
下面的列表列出了针对UdpTransportSettings每个属性所代表的含义:
- DuplicateMessageHistoryLength:传输用于标识重复消息的最大消息哈希数,默认值为 4112;
- MaxBufferPoolSize:传输使用的任何缓冲池的最大大小,默认值为524288;
- MaxMulticastRetransmitCount:应重新传输多播消息的最大次数(第一次发送除外),默认值为2;
- MaxPendingMessageCount:已经接收但尚未从每个通道实例的输入队列中移除的消息的最大数量,默认值为32;
- MaxReceivedMessageSize:绑定可处理的消息的最大大小,默认值为65507;
- MaxUnicastRetransmitCount:应重新传输单播消息的最大次数(第一次发送除外),默认值为1;
- MulticastInterfaceId:该值唯一地标识在发送和接收多播消息时所使用的网络适配器,默认值为null;
- SocketReceiveBufferSize:基础 WinSock 套接字上的接收缓冲区的大小,默认值为55536;
- TimeToLive:多播数据包可以遍历的网络段跃点数,默认值为1。
标准终结点UdpDiscoveryEndpoint对应的配置元素同样定义了相应的配置属性是你能过对它采用的广播地址以及UDP传输层进行自由的配置。下面给出了一个配置实例。
1: <udpDiscoveryEndpoint>
2: <standardEndpoint name="adhocDiscoveryEndpointConfiguration" discoveryVersion="WSDiscovery11">
3: <transportSettings duplicateMessageHistoryLength="2048"
4: maxPendingMessageCount="5"
5: maxReceivedMessageSize="8192"
6: maxBufferPoolSize="262144"/>
7: </standardEndpoint>
8: </udpDiscoveryEndpoint>
三、ServiceDiscoveryBehavior
我们之前就已经说,客户端用于获取可用服务发起的请求,和基于普通服务调用的消息请求并没有本质的不同。匹配的服务在接收到客户端发送的Probe/Resolve请求后,会将自己的信息包含在PM/RM消息中进行回复。现在我们讨论是一个核心的问题:消息的内容如何产生?
对于普通的服务调用,回复消息的内容最初来源于针对服务实例的操作方法的调用的结果。针对服务发现的Probe/Resolve请求也是一样,服务端依然存在一个用于返回目标服务信息的“发现服务”,并且这个服务的实现了添加到目标服务的DiscoveryEndpoint的契约接口。这个服务的类型就是抽象类DiscoveryService的子类。DiscoveryService的定于如下,可见它实现了DiscoveryEndpoint基于不同WS-Discovery版本在Ad-Hoc和Managed模式下的6个契约接口。
1: public abstract class DiscoveryService :
2: IDiscoveryContractAdhocApril2005,
3: IDiscoveryContractManagedApril2005,
4: IDiscoveryContractAdhoc11,
5: IDiscoveryContractManaged11,
6: IDiscoveryContractAdhocCD1,
7: IDiscoveryContractManagedCD1, ...
8: {
9: //省略成员
10: }
知道真正用于实现服务发现的服务,我们需要考虑另一个问题:这个继承自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服务行为。