构建 WCF 路由器 part 1 from MSDN

托管和使用 Windows® Communication Foundation (WCF) 服务通常经历几个基本步骤:实现服务、配置可以访问服务的端点、托管服务、生成 Web 服务描述语言 (WSDL) 文件或启用元数据交换,以便客户端能够生成代理以调用服务、编写代码以使用其相关配置实例化代理、以及启动调用服务操作。您基本不需要研究它的内部原理,但即使是在最简单的情况下,客户端和服务通道也要依赖兼容配置来处理寻址语义和消息筛选,以确保调用了正确的操作。
有时,在客户端和目标服务之间引入中介服务或路由器服务对接收在它们之间传输的消息或执行其他活动(如日志记录、优先级路由、联机/脱机路由、负载平衡)非常有用,引入安全边界也同样有用处。当引入此类中介服务时,需要对一些寻址和消息筛选行为做出相应调整。
因此,让我们深入了解一下如何使用中介服务,为简单起见,我将它们统称为路由器。在本期文章中,我将介绍 WCF 寻址和消息筛选的概念,并重点讲解路由器方案,此外我还将介绍一些适用于路由配置以及相应设置的选项。在本系列文章的第 2 部分中,我将展示如何利用该基本原理实现更高级、更实用的路由功能。

默认寻址语义
在 2007 年 6 月的“服务站”专栏中 (msdn.microsoft.com/msdnmag/issues/07/06/ServiceStation),Aaron Skonnard 介绍了 WCF 如何处理逻辑和物理端点寻址、寻址标头以及消息筛选。在本节中,我将回顾其中的一些基本寻址功能以及它们如何影响路由方案—但您也会发现:Aaron 的专栏对于了解这些 WCF 功能的其他深层次细节非常有用。
通常,客户端使用从服务描述生成的代理将消息直接发送至目标服务。为了使客户端与服务兼容,他们共享等效约定和端点配置。看一下图 1 中所示的服务约定和配置,您可以从中得出几个重要的服务寻址要求。
                                                            Service Contract
[ServiceContract(Namespace =
"http://www.thatindigogirl.com/samples/2008/01")]
public interface IMessageManagerService
{
  [OperationContract]
  string SendMessage(string msg);

  [OperationContract]
  void SendOneWayMessage(string msg);
}
Endpoint Configuration
<system.serviceModel>
  <services>
    <service name="MessageManager.MessageManagerService"
      behaviorConfiguration="serviceBehavior">
      <endpoint
        address="http://localhost:8000/MessageManagerService"
        contract="MessageManager.IMessageManagerService"
        binding="basicHttpBinding" />
    </service>
  </services>
</system.serviceModel>
首先,针对 SendMessage 操作请求预期的 "Action" 寻址标头如下:
http://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessage
由于 OperationContractAttribute 没有指定某个 "Action",因此该值从服务约定命名空间、约定名称(默认为接口名称)和操作名称(默认为方法名称)派生而来。
第二,SendMessage 返回响应的预期 "Action" 标头如下:
http://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessageResponse
由于 OperationContractAttribute 没有指定 ReplyAction,因此该值的派生方法与 Action 属性相同,附加后缀 "Response"。
最后,定位服务端点的消息的预期 "To" 标头如下:
http://localhost:8000/MessageManagerService
该值从端点元素的地址属性派生而来,此元素被视为是端点的逻辑地址。尽管可以另外指定,但端点的物理地址通常都是与逻辑地址相匹配的。这意味着客户端通常将消息发送至与 "To" 标头相匹配的物理地址。
服务元数据描述了这些要求,以便客户端可以生成兼容的代理和配置。为客户端生成的服务约定反映了相同的 Action 和 ReplyAction 服务设置,而客户端绑定配置反映出具有合适逻辑地址和物理地址的端点。例如,下列客户端端点与图 1 中的服务相兼容:
<client>
  <endpoint 
    address="http://localhost:8000/MessageManagerService"
    binding="basicHttpBinding" 
    contract="localhost.IMessageManagerService" 
    name="basicHttp" />
</client>
客户端代理将客户端端点元素的地址属性用作其逻辑地址和物理地址。因此,正如我先前所述,消息发送至与 "To" 标头相匹配的物理地址。代理调用 SendMessage 操作时,将发送带有 "To"(发送方)和 "Action"(操作)标头的消息,如图 2 所示。
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <To s:mustUnderstand="1"
      xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">
      http://localhost:8000/MessageManagerService
    </To>
    <Action s:mustUnderstand="1" 
      xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">
      http://www.thatindigogirl.com/samples/2008/01
        /IMessageManagerService/SendMessage
    </Action>
  </s:Header>
  <s:Body>
    <SendMessage xmlns="http://www.thatindigogirl.com/samples/2008/01">
      <msg>test</msg>
    </SendMessage>
  </s:Body>
</s:Envelope>

"To" 和 "Action" 标头的组合分别指示服务模型:通道调度程序应使用哪个通道处理消息、调用哪个操作。默认情况下,必须有一个端点的逻辑地址与 "To" 标头匹配,一项操作与 "Action" 标头相匹配。图 3 说明了这一流程。
Figure 3 Typical Addressing without a Router (单击该图像获得较大视图)
在接下来的内容中,我将介绍逻辑地址和物理地址、"To" 和 "Action" 标头以及使用路由器时消息筛选规则的含义。

路由体系结构
尽管在路由器的设计方式上有一些变化,但大部分路由器必须要能够接收以任何服务为目标的消息,而且它们还必须能够将原始消息转发给相应的目标服务。设计路由器有以下两种基本方法:透传路由器或处理路由器。
透传路由器对客户端是透明的。客户端的关系属于下游服务,但消息恰好是通过路由器传递的。客户端必须使用兼容的传输协议和消息编码器将消息发送到路由器,但任何服务通道所需的安全性、可靠会话、应用程序会话或其他消息传递协议都满足客户端生成消息的要求。路由器可以查看消息标头甚至插入标头,但原始消息元素仍会以原有方式转发至服务。图 4 阐明了这一关系。
Figure 4 Operation of a Pass-Through Router (单击该图像获得较大视图)
处理路由器在为应用程序处理消息时发挥更为积极的作用。因此,尽管客户端仍必须能够发送与下游服务兼容的消息,但就传输、编码和协议的兼容性而言,客户端是与路由器发生关系。消息通过路由器传送到服务,消息正文及任何服务所需的标头一起完整保留。
安全性、可靠会话以及与其他通信协议相关的标头或消息通常由路由器进行处理,且路由器可通过适当的通信协议为下游服务构建新消息。图 5 说明了处理路由器的兼容性。
Figure 5 Operation of a Processing Router (单击该图像获得较大视图)
每个路由配置都具有实用的执行方法。而且还可以按一定比例建立混合解决方案。

路由器约定
路由器将接收适用于下游服务的消息,并负责将这些消息转发至相应的服务。路由器还负责接收来自服务的响应并将这些响应返回客户端。典型的路由器约定会显示可以处理任何消息请求或响应的单个操作。在下面的示例中,该操作称为 ProcessMessage:
[ServiceContract(Namespace = 
"http://www.thatindigogirl.com/samples/2008/01")]
public interface IRouterService {
  [OperationContract(Action = "*", ReplyAction = "*")]
  Message ProcessMessage(Message requestMessage);
}
正如我们在本专栏前面所述,通常情况下,OperationContractAttribute 的 Action 和 ReplyAction 属性是从服务约定命名空间、约定名称和操作名称派生而来的。默认情况下,当消息到达时,通道调度程序必须找到与 Action 标头完全匹配的操作。但是,如 Action 和 ReplyAction 被设置为 "*",则无论 Action 是何值,通道调度程度都将未映射为具体操作的所有消息发送给全能操作。而且为了避免混淆,只有一个操作可以为 Action 或 ReplyAction 属性指定 "*"。
典型的路由器可提供类似 ProcessMessage 的单个操作,用于处理收到的任何消息。虽然 Action 和 ReplyAction 可确保通道调度程序将消息映射到 ProcessMessage,但操作签名还必须能够处理所有消息。
要解决这一问题,ProcessMessage 以 Message 类型的形式接收并返回非类型化消息。路由器可以通过该类型访问标头集合和消息正文,但除常用寻址标头(通过强类型化属性执行反序列化并使其可用)之外不会进行任何自动序列化。
任何对消息的进一步处理都是通过路由器实现的。基本路由器将只接收非类型化消息并将其按原样转发至下游服务,等待回答。同样,回复将按原始的格式转发至调用的客户端。

转发消息
路由器将接收消息并根据其自己的需求对该消息进行处理后,将消息转发至合适的下游服务以供进一步处理。图 6 显示了前文所述约定的一个简单路由器执行方法。ProcessMessage 通过 ChannelFactory<T> 构建客户端通道(或代理)并使用该代理将消息转发至特殊的服务端点,从而返回所有响应。
[ServiceBehavior(
  InstanceContextMode = InstanceContextMode.Single, 
  ConcurrencyMode = ConcurrencyMode.Multiple)]
public class RouterService : IRouterService {
  public Message ProcessMessage(Message requestMessage) {
    using (ChannelFactory<IRouterService> factory = 
      new ChannelFactory<IRouterService>("serviceEndpoint")) {

      IRouterService proxy = factory.CreateChannel();

      using (proxy as IDisposable) {
        return proxy.ProcessMessage(requestMessage);
      }
    }
  }
}

代理通常被强类型化为目标服务约定,但在此种情况下,代理应能够转发任何消息并接收全部回复—一些便于路由器约定利用的内容。在这个简单的示例中,路由器只将原始消息转发至目标服务并返回全部响应。如果目标服务端的操作为单向操作,则将不会发送任何回复。
由于约定使用非类型化消息,因此同样的消息也将转发至服务,如图 7 所示。但您必须认识到,有一个对消息所做的更改可能是您没有预料到的:在消息发送至服务之前,"To" 标头发生了改变。
Figure 7 Addressing Semantics through a Simple Router (单击该图像获得较大视图)
请注意,默认情况下,代理将使用其端点配置的逻辑地址来设置传出消息的 "To" 标头—即使传递的原始 Message 实例已具有 "To" 标头。虽然这看起来是一件好事—因为所有服务都要求 "To" 标头匹配它们其中一个服务端点的逻辑地址—但这会导致其他负面影响。例如,若更新的 "To" 标头未签名且服务已启用安全性,则消息将被拒绝。
理想的情况是,客户端应发送一条包含匹配目标服务的 "To" 标头的消息,无论是否匹配,路由器均应接收该消息,且路由器应在不改变 "To" 标头的情况下将该消息转发给服务。这可以通过绑定配置进行处理,我将在稍后讨论这一配置。

逻辑寻址和物理寻址
将路由器引入应用程序体系结构时,如果客户端可以使用正确的服务 "To" 标头发送消息,又能将此消息发送至路由器,这应该是最理想的状况。达此目的一种方法是将客户端配置为使用 ClientViaBehavior,如图 8 所示。这会令客户端代理根据端点的逻辑地址生成包含 "To" 标头的消息,但要通过路由器的物理地址发送该消息。问题是这会将客户端与路由器的存在联系在一起。
<client>
  <endpoint address="http://localhost:8000/MessageManagerService"
    binding="wsHttpBinding"  
    bindingConfiguration="wsHttpNoSecurity"
    contract="localhost.IMessageManagerService" 
    name="basicHttp" 
    behaviorConfiguration="viaBehavior"/>
</client>
<behaviors>
  <endpointBehaviors>
    <behavior name="viaBehavior">
      <clientVia viaUri="http://localhost:8010/RouterService"/>
    </behavior>
  </endpointBehaviors>
</behaviors>

解决此问题的另一个方法是让服务为其端点配置 listenUri 属性,以便服务与路由器使用同一逻辑地址,而将物理地址专用于服务。请看以下服务配置:
<endpoint address="http://localhost:8010/RouterService" 
  contract="MessageManager.IMessageManagerService" 
  binding="wsHttpBinding"
  bindingConfiguration="wsHttpNoSecurity" 
  listenUri="http://localhost:8000/MessageManagerService"/>
所产生的服务元数据将路由器地址发布给客户端,这样客户端端点就能反映出路由器地址。实际上,我不太喜欢这一解决方案,因为它将服务与路由器联系到了一起,而理想状况是服务不需要了解这一内容。
备选方案是让服务使用未绑定到路由器或服务的 URI 类型的逻辑地址,然后手动通知客户端要接收消息的物理地址(因为它不是元数据的组成部分)。以下是这种端点配置的示例:
<endpoint address="urn:MessageManagerService" 
  contract="MessageManager.IMessageManagerService" 
  binding="wsHttpBinding"  
  bindingConfiguration="wsHttpNoSecurity" 
  listenUri="http://localhost:8000/MessageManagerService"/>
在任一种情况下,服务都会收到与其端点配置匹配的 "To" 标头且路由器会先收到消息。
路由器着实应挑起配置的重任,让客户端和服务不受其存在状态的约束。因此,"To" 标头绝对不能与路由器的逻辑地址匹配。默认情况下,服务使用 EndpointAddressMessageFilter 确定消息的 "To" 标头是否与任何其配置的端点相匹配。由于路由器无法实现同样的操作,因此应安装 MatchAllMessageFilter。
ServiceBehaviorAttribute 通过 AddressFilterMode 属性支持此操作,该属性可以设置为 AddressFilterMode 枚举之一:Exact(默认)、Prefix 或 Any。由于无法保证路由器前缀匹配所有接收消息的服务,因此允许所有 "To" 标头通过会很有帮助,如下所示:
[ServiceBehavior(InstanceContextMode = 
  InstanceContextMode.Single, 
  ConcurrencyMode = ConcurrencyMode.Multiple, 
  AddressFilterMode=AddressFilterMode.Any)]
public class RouterService : IRouterService
默认情况下,"To" 标头将始终根据其端点配置更新,以匹配代理的逻辑地址,而不管 "To" 地址是否已设置为正确的值。为抑制这种行为以便路由器可以使用原始的 "To" 标头将消息转发至服务,路由器必须将绑定配置与手动寻址搭配使用。在任何标准绑定上均无法设置该属性,因此您必须使用自定义绑定实现这一目的。
下列代码段显示了为 HTTP 传输通道设置这一功能的 customBinding 段落:
<customBinding>
  <binding name="manualAddressing">
    <textMessageEncoding />
    <httpTransport manualAddressing="true"/>
  </binding>
 </customBinding>
这大大简化了图 9 中所示的寻址流程(标头不改变)。
Figure 9 Addressing Semantics through a Router with Manual Addressing (单击该图像获得较大视图)

MustUnderstand 标头
到现在为止,我已重点介绍了简单的路由实现以说明核心路由器的设计注意事项,它们还会对寻址、筛选和绑定配置产生影响。这个简单的路由解决方案仅在服务没有为其绑定启用安全性、可靠会话或任何其他丰富协议时起作用。图 10 显示了我为上述讨论假设的绑定协议的简化视图。
Figure 10 Service Contract and Endpoint Configuration (单击该图像获得较大视图)
图 11 显示了服务要求安全性和可靠会话时透传路由器的相同视图。启用这些协议即意味着客户端和服务通道将交换其他消息以建立会话、请求安全令牌以及其他相关消息传送。由于使用路由器可以透传所有消息,因此这些特定于协议的消息也将会透传到服务—这不失为一件好事。
Figure 11 Pass-Through Configuration with Security and Reliable Sessions (单击该图像获得较大视图)
但是,如果服务发送至/接收自的消息包含接收方必须理解的标头,问题就会随之出现。由于透传路由器并未特意启用安全性或可靠会话,因此处理相关协议标头时不会有那些的通道。
通过将 ServiceBehaviorAttribute 的 ValidateMustUnderstand 属性设置为 false,您可以指示路由器服务忽略 MustUnderstand 标头,如下所示:
[ServiceBehavior(InstanceContextMode = 
  InstanceContextMode.Single, 
  ConcurrencyMode = ConcurrencyMode.Multiple, 
  AddressFilterMode=AddressFilterMode.Any, 
  ValidateMustUnderstand=false)]
public class RouterService : IRouterService
这将确保来自客户端的传入消息不会出现问题,但从下游服务返回的消息不在其保证范围之内。
要解决这一问题,为调用下游服务初始化通道工厂时,您还必须修改路由器执行方法以指定其行为,如下所示:
using (ChannelFactory<IRouterService> factory = 
  new ChannelFactory<IRouterService>("serviceEndpoint"))
{
  factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
  IRouterService proxy = factory.CreateChannel();
  
  // remaining code
}
现在,协议和服务消息可通过路由器在客户端与服务之间自由传送—假设使用的协议是 HTTP。
如使用了 TCP 这类双向协议或命名管道,就会出现另一种复杂情况。这意味着服务可以向客户端启动消息,例如,当启用可靠会话时。有一个高级路由器配置可以用于处理这一特例,我将在本系列的第 2 部分中介绍该情形及其实用性。
posted @ 2009-10-26 20:32  KidYang  阅读(640)  评论(0编辑  收藏  举报