65.9K
CodeProject 正在变化。 阅读更多。
Home

WS-Discovery for WCF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (30投票s)

2008 年 11 月 27 日

CPOL

12分钟阅读

viewsIcon

143042

downloadIcon

3289

本文描述了 WS-Discovery for Windows Communication Foundation (WCF) 的设计、实现和用法。

引言

Windows Communication Foundation 代表了 .NET 环境中最先进的通信库。这个设备的灵活性令人惊叹,它允许通过简单的属性和对象装饰,轻松地远程化特定服务上的函数,并自动序列化签名中的所有参数。

WCF 允许通过 Web 服务标准通信或自定义通信来与服务进行互连,只需修改配置文件即可,支持大量的传输协议(HTTP、net TCP、MSMQ 等)和通信协议(安全、可靠消息等)。

该库的另一个主要优点是其可扩展性:可以创建在服务级别或终结点级别定义的新的协议和行为。

通过 WCF 实现服务或服务使用者是基于三个概念,这些概念由 ABC 概括:服务响应的地址(Address),服务响应的方式(Binding),以及服务暴露的方法(Contract)。本文旨在描述一个组件,该组件从配置方面扩展 WCF,以便服务能够通过 WS-Discovery 规范在运行时被服务使用者发现。

背景

WS-Discovery 是元数据中 WS-* 协议堆栈中的一个规范。

StackProtocolli.gif

WS-Discovery 规范规定,服务不以其 ABC 参数来表征,而仅以合同(Contract)和“作用域”(scopes)来表征,这些作用域是有用的信息,用于区分具有相同接口的两个服务。每个服务都实现一个称为 ScopeMatchBy 的作用域方案,并可能以 URI(统一资源标识符)格式表示作用域。服务在其生命周期中可以更改其元数据或作用域本身,但它必须通过称为 MetadataVersion 的信息来跟踪版本。

在启动时,服务通过包含上述信息的组播 Hello 消息向网络介绍自己。同样,在关闭时,它会向网络发送一个组播 Bye 消息。如果元数据或作用域发生更改,服务只需发送一个带有新 MetadataVersion 号的 Hello 消息。

客户端通过类型(即合同)来查找服务,并可能附加有关作用域和 ScopeMatchBy 的信息。搜索可以通过两种方式进行:无发现代理,有发现代理。

在没有发现代理的情况下,客户端发送一个称为 Probe 的组播消息,其中包含搜索参数(合同类型、ScopeMatchBy 和作用域)。

Probe-ProbeMatch.Png

上图显示了红色的组播通信和蓝色的单播通信。

每个认为满足请求的服务都会通过单播 ProbeMatch 消息进行响应。如果 ProbeMatch 信息足够,客户端就可以连接到服务,请求服务的元数据以首先收集有关绑定的信息。否则,客户端必须发送一个称为 'Resolve' 的新组播消息,服务必须通过单播 ResolveMatch 消息进行响应,提供必需的缺失信息。

如果网络中存在发现代理,通信将使用相同的消息(Probe、ProbeMatch...),但模式将从组播变为向发现代理的单播。

为了提醒自己通信是如何进行的,我引入了一个非 IT 领域的类比。我想象一个教室里的几位学生,每当新学生进来时,他都会大声打招呼并说出自己的名字(Hello 组播)。突然,一个学生需要 Robert,但他不认识他。于是,他在安静的教室里大声呼唤 Robert(Probe 组播)。结果,所有叫 Robert 的人都站起来走向呼唤他们的人(ProbeMatch 单播)。好吧,发现代理代表教室里的老师:当有人大声呼唤 Robert 时,除了所有叫 Robert 的人,老师也会站起来。他走到学生面前,介绍自己:“我是老师,下次你需要什么,可以来找我。(Hello 单播)”之后,学生站起来,在他需要某人时询问老师(Probe 单播)。

在这个实现中,我不会考虑发现代理的版本,但只要我有时间,我就会扩展我的代码以包含发现代理,也许会集成 Roman Kiss 的 WS-Transfer 的 IStorageAdapter

使用代码

WCF 的 WS-Discovery 实现可以分为两部分(如果包含发现代理实现,则分为三部分):一部分涉及服务,另一部分涉及客户端。

从客户端角度来看,将发现方法集成到 WCF 配置中是很有问题的,因为 WCF 标准代理通过指定地址、绑定和合同来工作,而发现代理只需要合同;其他信息只是在运行时发现的。

此外,我认为客户端配置方面并不重要。作用域是可选的:如果未指定作用域,则所有实现该合同的服务都将满足作用域请求,除非存在特定的服务配置。

发现代理的实现不需要像 WCF 代理那样继承自某个类,声明一个 DiscoveryClient<TChannel> 类就足够了。事实上,Channel 属性允许与远程服务的接口进行交互。

DiscoveryClient<IServiceSample> proxy = new DiscoveryClient<IServiceSample>();

构造函数可以接收所需服务的可选作用域,然后开始通过向网络发送 Probe 消息来搜索所需服务。尽快拥有代理的实例很重要,这样在第一次调用代理方法时,要连接的服务已经找到,避免了不必要的等待。

ProbeDiscoveryClient.GIF

当 ProbeMatch 消息到达时,客户端可以释放 WaitHandle,调用远程调用,并返回结果。如果客户端收到多个 ProbeMatch,第一个会解锁信号量,在调用方法时,通过虚拟方法 GetBestMemento() 找到最佳服务。可能在第一次调用时,使用的是第一个发送 ProbeMatch 的服务,除非在实例化和方法调用之间有足够的时间来接收所有 ProbeMatch,然后在方法调用之前。一旦客户端使用了一个通道,它就会一直使用它。通过重新创建客户端,所有早期存储的 ClientMemento 都将保留,因此 GetBestMemento 将能够更仔细地找到最佳候选。

如前所述,GetBestMemento 是一个虚拟函数,这意味着可以继承 DiscoveryClient<IServiceSample> 类并执行重写的方法来运行自定义选择逻辑。

ClientMemento 是表示与服务互连的基本信息的类。通过从探测接收到的数据,可以实例化 memento,该 memento 负责获取其他可能的信息,例如绑定,通过服务暴露的元数据。如果客户端和服务器使用此发现实现,则存在一个快捷方式来避免元数据往返。ProbeMatch 消息通过 EndpointReference XML 结构(请参阅 WS-Addressing)预测发送服务地址终结点,该结构不仅包含地址字段,还在 ReferenceParameters 字段内包含可扩展性范例。在此字段内,服务输入有关通信绑定的所有信息。如果存在此信息,ClientMemento 则不需要元数据往返,即可随时通信。

客户端实现的一个主要优点是 AutomaticChangeChannelWhenFaulted 属性,该属性提供了对服务的容错性。事实上,当有多个候选服务时,GetBestMemento 会返回一个指向所选服务的通道。如果代理操作失败,客户端会自动尝试对另一个可用候选服务执行相同的操作,只有当没有其他候选服务可以执行该操作时,才会抛出异常。通过将该属性设置为 false,此客户端功能将被禁用;因此,即使存在其他候选者,也会抛出异常。

使用 WS-Discovery 发送消息时,处理自定义标头的客户端主要担忧之一。WCF 允许通过使用 OperationContextScope 类添加自定义标头,但不幸的是,该类被声明为 sealed,因此对发现功能不可扩展。

IMyContract proxy = cf.CreateChannel();
using (new OperationContextScope((IClientChannel)proxy))
{
    OperationContext.Current.OutgoingMessageHeaders.Add(
       MessageHeader.CreateHeader("otherHeaderName",
       "http://otherHeaderNs", "otherValue"));
    Console.WriteLine(proxy.echo("Hello World"));
}

问题在于 DiscoveryClient 通道在实际通道在发现过程结束时创建之前,至少在那时之前,不实现 IClientChannel 接口。这就是为什么我必须创建一个不那么优雅但仍然可以绕过问题的解决方案。我说的是 DiscoveryOperationContextScope<TChannel> 类,它以类似的方式允许执行所需的操作。

DiscoveryClient<IMyContract> proxy = new DiscoveryClient<IMyContract>();
using (DiscoveryOperationContextScope<IMyContract> os 
        = new DiscoveryOperationContextScope<IMyContract>(proxy))
{
    os.OutgoingHeaders.Add(MessageHeader.CreateHeader("MyHeaderName",
                           "" ,"MyheaderValue"));
    Console.WriteLine("Output string: " + proxy.Channel.GetString("qqq"));
}

服务部分的实现似乎更容易,行为会将服务信息添加到静态 ServiceContext 类。ServiceContext 钩住 ServiceHost 的 OpenedClosing 事件以发送 Hello 和 Bye 消息,并继续监听组播端口以接收 Probe/Resolve 消息。

与可扩展性相关的一个功能是动态作用域。在实现服务时,我想让作用域可以动态管理,以便在服务生命周期中更改它们,并始终提醒自己,每个元数据更改都必须通过新的 Hello 消息通知客户端。

可发现服务的配置

服务部分要求 WS-Discovery 可以在配置阶段通过特定的行为和相关的扩展来设置。

<extensions>
  <behaviorExtensions>
    <add name="serviceDiscoverableBehavior"
      type="Masieri.ServiceModel.WSDiscovery.Behaviors.DiscoveryBehaviorSection,
           Masieri.ServiceModel.WSDiscovery, Version=1.0.0.0, 
           Culture=neutral, PublicKeyToken=18ad931e67d285bd"
    />
  </behaviorExtensions>
</extensions>
<services>
  <service behaviorConfiguration="serviceDiscoverable"
    name="ServiceTest.IServiceSample">
    ...

   </service>
</services>

因此,可以通过引入配置文件的设置来引入发现功能设置,而无需重新编译代码。相关的行为必须使用特定节进行配置。

<behavior name="serviceDiscoverable">
      <serviceMetadata
    httpGetEnabled="true"
    httpGetUrl="https://:8080/Mex" />
  <serviceDiscoverableBehavior
       scopesMatchBy="http://schemas.xmlsoap.org/ws/2005/04/discovery/rfc2396">
     <scopes>
    <add url="http://myscope.tempuri.org/"/>
          </scopes>
  </serviceDiscoverableBehavior>
    </behavior>
</serviceBehaviors>

必须在 ServiceBehavior 设置中指定 ServiceMetadata 行为,以允许通过 WSDL 恢复绑定设置。实际上,如果客户端和服务器都使用此发现库,则此设置将不必要,因为已开发了一种机制来避免元数据往返并允许更快的通信。WCF 的自动元数据创建非常方便,但速度不是很快:这是真的,它在第一次创建,然后在后续请求中保存在内存中;在 90% 的情况下是正常的,但在某些架构情况下,我发现服务经常被实例化和销毁,给我带来了很多麻烦。例如,如果 WSDL 创建者遇到问题,由于未知自定义协议(我在 SDK soap.udp 的实现中注意到),创建可能会减慢几秒钟。

通过配置进行的服务设置已完成,但也可以以编程方式配置服务。

Log

日志管理以特别细致的方式实现,以允许与 WCF 日志完全集成,从而全面了解通信场景。通过使用配置文件,可以将 System.ServiceModel.WSDiscovery 源添加到 System.ServiceModel 源,并将日志消息流向特定的侦听器。WS-Discovery 消息则像所有 WCF 消息一样添加到 System.ServiceModel.MessageLogging 源。

<system.diagnostics>
    <sources>
      <source name="System.ServiceModel.WSDiscovery" 
                switchValue="Warning, Error">
        <listeners>
          <add initializeData="InfoServiceDebug.e2e"
          type="System.Diagnostics.XmlWriterTraceListener, 
        System, Version=2.0.0.0, Culture=neutral, 
        PublicKeyToken=b77a5c561934e089"
             name="ServiceModel Listener"
             traceOutputOptions="LogicalOperationStack,  DateTime, 
                                 Timestamp, ProcessId, ThreadId, Callstack" />
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging" 
              switchValue="Warning, Error" >
        <listeners>
          <clear />
          <add type="System.Diagnostics.DefaultTraceListener" name="Default"
                        traceOutputOptions="None" />
          <add initializeData="MessageLog.e2e"
            type="System.Diagnostics.XmlWriterTraceListener, 
                  System, Version=2.0.0.0, Culture=neutral, 
                  PublicKeyToken=b77a5c561934e089"
            name="MessageLogging Listener"
            traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, 
                                ProcessId, ThreadId, Callstack" />
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add type="System.Diagnostics.DefaultTraceListener"
            name="Default" />
    </sharedListeners>
</system.diagnostics>

Log.Png

关注点

在这一段中,我想分享我关于使用 WS-Discovery 的架构经验。

服务冗余

基于 WS-Discovery 的架构的关键方面是实现可能的服务的冗余。很久以前,我在意大利铁路网络(RFI)的系统上工作。谈论使用的解决方案,它像任何如此庞大的系统一样多样且异构,但整个系统通过 Web 服务进行控制。系统的地理位置,分布在车站、车厢和 RFI CED(数据中心),是一个需要充分考虑的特殊性,因为一个 Web 服务无法应对来自全国各地的所有调用(实际上,这是可能的,但非常昂贵!)。

在这种情况下,我开发了 WS-Discovery 的第一个版本,试图彻底改变现有模型(当然,源代码被完全重写并在许多方面得到改进)。在一个合作良好的团队中工作,我们开发了一个解决方案,使来自全国各地的多个客户端不直接与中央 Web 服务交互,而是运行时发现本地提供商,这些提供商为他们提供了相同的信息。这些本地提供商实际上是缓存管理器,伪装成所需的服务。如果客户端需要直接来自源的信息,它总可以通过使用正确的范围来获取。

Scalar.Png

容错

容错是 WS-Discovery 的另一个神奇方面。当客户端无法调用服务时,它可以尝试调用满足相同作用域标准的另一个服务。这种行为显然可以通过客户端的 AutomaticChangeChannelWhenFaulted 属性进行设置。

do
{
    try
    {
      OnInvoking();
      object ret = method.Invoke(_lastUsedChannel, parameters);
      OnInvoked();
      DiscoveryLogger.Info("Method invoked successfully");
      return ret;
    }
    catch (Exception ex)
    {
      DiscoveryLogger.Error("Errore nell'invocazione del servizio", ex);
      lock (_lock)
      {
        //endpoint error
        ClientContext.Current.RemoveDiscoveredEndpoint(
    Helpers.ContractDescriptionsHelper.GetContractFullName<TChannel>(), mem);
        //Look for another one
        mem = GetBestMemento();
        if (mem == null)
        {
          //Start a new probe process for the future
          StartProbeProcess();
          DiscoveryLogger.Warn(
            @"The DiscoveryClient can't scale on another service");

          //Now I can throw the exception
          if (originalException == null)
            throw ex;
          else
            throw originalException;
        }
        //I try again but I store the original exception before
        if (originalException == null)
          originalException = ex;
      }
    }
} while (AutomaticChangeChannelWhenFaulted);

具有服务质量(QOS)的作用域和系统的动态变化

在与铁路公司合作的另一个项目中,我们研究了一个具有动态作用域的解决方案,这些作用域代表了服务的 QOS(服务质量),允许客户端挂接与其需求兼容的服务,而不会过于苛刻,以避免阻塞所有具有更好 QOS 的服务。通过执行 GetBestMemento 的重载,客户端可以选择具有最合适 QOS 的服务,而服务则通过 MetadataVersion 机制根据并发用户的数量来更新 QOS。

Roman Kiss 的 ESB 扩展

WS-Discovery 完全兼容 Roman Kiss 文章中涉及的 WS-Eventing 和 WS-Transfer 的实现:在服务器端,通过将发现行为包含在服务中;在客户端,例如,通过简单地使用 DiscoveryClient<RKiss.WSEventing.IWSEventing> 类。

动态负载均衡

在一个最近的视频监控和场景分析系统的架构中,我实现了一个可以动态加载场景分析组件的服务,该服务允许通过 SubscriptionManager 订阅分析后的事件。这些组件执行的计算工作量很大。我考虑将它们集成到一个可扩展性足够的服务器中,分布在多个 PC 上,并在作用域中提供已分析场景的标识符。这样,系统就可以在多台机器上优化负载,而无需集群配置。它可以动态分配负载,而无需考虑互连的客户端。在组件从一台机器动态重新分配到另一台机器的情况下,服务器 1 在新机器(服务器 2)上启动主机准备过程,该过程在准备好后会发送一个包含服务组件作用域列表的 hello。客户端收到 hello 消息后,会将 Memento 存入可用列表。服务器 1 发送一个带有新 MetadataVersion 的 Hello,其中不再包含已删除组件的作用域,并自动向已连接的客户端发送 EndSubscription 消息,从而导致传输到新的 Memento 并自动订阅到服务器 2。

EventListener.Png

历史

  • 2008 年 10 月 - 首次发布。
© . All rights reserved.