导航

转载 WCF 深度寻址

Posted on 2011-09-02 17:48  Xiaotao.Li  阅读(158)  评论(0)    收藏  举报
服务站
WCF 深度寻址
Aaron Skonnard

Windows Communication Foundation 用多种不同的通信协议为公开服务端点和与其通信提供了灵活的模式。本月,我将围绕端点通信的各种寻址细节展开介绍,其中的很多内容都使更先进的消息传递方案成为可能。

端点和地址
Windows® Communication Foundation 体系结构对物理通信细节和底层服务实现进行了清楚地划分。开发人员花费大多数时间在他们熟悉的 Microsoft® .NET Framework 代码上,以实现服务约定和定义服务行为。当实现完成后,他们可以决定怎样承载服务并将其公开给使用者。
为了承载特定服务,您需要创建一个 ServiceHost 实例并定义一个服务端点集合。服务端点为通信指定了一个地址、一个绑定和一个约定。Windows Communication Foundation 需要此信息以建立必要的信息传递运行库,也称为信道堆栈,应要求向外部使用者提供 Web 服务描述语言 (WSDL) 形式的元数据。
您可以通过代码(参见 ServiceEndpoint 类),或者通过 <system.serviceModel> 配置部分的条目(参见 <endpoint> 元素)向 ServiceHost 实例提供服务端点。您可以通过代码实现的任何内容,也同样可以通过配置来实现,反之亦然。但是,通过配置作为提供方的端点通常为在开发后进行更改提供了更大的灵活性。
在您试图打开 ServiceHost 之前,它必须包含至少一个服务端点,否则运行时会引发异常;连一个端点都没有的服务没有太大用处,因为您将无法与之交互。但是,服务可以公开多个端点,以适用具有多种不同能力和约束的多个约定或不同类型的使用者。
当讨论端点时,大多数人倾向于关注绑定和约定,而忽视端点地址以及与之相关的各种问题。本月,我的目标是改变这种倾向,带您发现可以使用的各种寻址技术。
有关 Windows Communication Foundation 编程模型的进一步讨论,以及如何在代码或各种配置元素中使用端点,请参见我在 2006 年 2 月出版的《MSDN® 杂志》上撰写的“Windows Communication Foundation 编程基础”一文。

寻址基本原理
定义服务端点时,您有很多种指定地址的选择。您可以指定一个 IP 地址或主机名称,您也可以指定端口号或直接采用传输的默认端口。当然,您也可以在地址(主机/端口之后的部分)中包含特定的路径。
在 Windows Communication Foundation 中指定地址时,有两种基本技术可以使用。您可以为每个端点指定绝对地址,或者可以为 ServiceHost 提供一个基址,然后为每个端点指定相对路径。指定绝对地址相对较容易理解,但是基址技术通常能使事情更容易管理。
指定绝对地址的方法是在端点定义中提供完全限定的地址,如图 1 所示。在此示例中,我已经指定了三个绝对地址,但请注意有两个 HTTP 地址共享基址 http://localhost:8080/calcservice。在这种情况下,您可以选择向主机提供基址,然后在定义单独的端点时使用相对地址。
Figure 1 配置端点
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService”>
  5.         <endpoint address=”http://localhost:8080/calcservice”
  6.                   binding=”basicHttpBinding”
  7.                   contract=”ISimpleMath”/>
  8.         <endpoint address=”http://localhost:8080/calcservice/secure”
  9.                   binding=”wsHttpBinding”
  10.                   contract=”ISimpleMath”/>
  11.         <endpoint address=”net.tcp://localhost:8081/calcservice”
  12.                   binding=”netTcpBinding”
  13.                   contract=”ISimpleMath”/>
  14.       </service>
  15.     </services>
  16.   </system.serviceModel>
  17. </configuration>
构建 ServiceHost 对象时,您可以为每个传输提供一个基址,如下所示:
  1. ServiceHost host = new ServiceHost(typeof(CalculatorService),
  2.     // base HTTP address
  3.     new Uri(“http://localhost:8080/calcservice”),
  4.     // base TCP address
  5.     new Uri(“net.tcp://localhost:8081/calcservice”));
您也可以在配置文件中与端点本身一起指定基址。为此,您需要针对每个 <service> 将基址列于 <host> 元素内,如下所示:
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService”>
  5.         <host>
  6.           <baseAddresses>
  7.             <add baseAddress=”http://localhost:8080/calcservice”/>
  8.             <add baseAddress=”net.tcp://localhost:8081/calcservice”/>
  9.           </baseAddresses>
  10.         </host>
  11.         <!-- endpoint definitions follow -->
  12.         ...
无论是通过代码还是配置,这两种技术都实现同样的目标 — 它们为每个使用中的传输给 ServiceHost 对象配置一个基址。通过这种方式配置 ServiceHost 之后,您可以在定义端点地址时利用相对 URI,如图 2 中所示。当打开 ServiceHost 时,它在相应基址(与绑定的传输相匹配)的基础上解析任何相对地址,从而确定为每个端点使用的绝对地址。
Figure 2 使用相对 URI
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService”>
  5.         <endpoint binding=”basicHttpBinding”
  6.                   contract=”ISimpleMath”/>
  7.         <endpoint address=”secure”
  8.                   binding=”wsHttpBinding”
  9.                   contract=”ISimpleMath”/>
  10.         <endpoint binding=”netTcpBinding”
  11.                   contract=”ISimpleMath”/>
  12.       </service>
  13.     </services>
  14.   </system.serviceModel>
  15. </configuration>
请注意,第一个和最后一个端点都没有“地址”属性,这等同于将它们留空。因此,这些端点将各自使用对应的基址作为端点地址;第一个端点使用基本 HTTP 地址,而最后一个端点使用基本 TCP 地址。第二个端点使用“secure”的相对地址,后者将根据基本 HTTP 地址解析(成为 http://localhost:8080/calcservice/secure)。
即使是在使用基址时,您仍可以在需要时为端点指定绝对地址。并且该绝对地址不需要以任何方式与对应的基址(如果有的话)相匹配。如果需要,每个端点可以指定一个完全不同的位置。
基址技术主要是为了提供方便,因此在修改端点的位置时不需要改动太多地方。当 GET 检索启用(通过 <serviceMetadata> 行为)后,默认情况下 Windows Communication Foundation 也使用基本 HTTP 地址以公开元数据。但是,您可以使用行为的 httpGetUrl 属性改变检索位置。
客户端并不知道服务的基址,也不需要在他们那里为类似的情况提供支持。因此,您不会在客户端对象模型中,或在 <client> 配置部分找到任何与基址相关的内容。客户端只需选择一个特定的端点(通常都配置了绝对地址),该绝对地址会确定它在传输中使用的地址。
这些寻址基本原理适用于自承载的 Windows Communication Foundation 服务(在此您自己创建和管理 ServiceHost 实例),但当您在 IIS 内承载服务时,需要有一些特殊的考虑。

IIS 寻址注意事项
在 IIS 中承载时,您无需担心创建或管理 ServiceHost 实例。IIS 在后台负责处理。您只需映射一个 .svc 端点到服务类,在 web.config 中配置服务端点和行为,让 Windows Communication Foundation 管理运行时的 ServiceHost 实例创建和配置过程即可。
在此承载方案中,基本 HTTP 地址是由保存服务的 IIS 虚拟目录以及 .svc 文件的名称确定的。作为开发人员,您的确不需要考虑有关基址的任何问题,因为它完全由 IIS 寻址方案控制。因此在此方案中,Windows Communication Foundation 恰当地忽略了您可能在 web.config 中指定的任何基址。如果您想为端点更改基址,您需要将服务移至不同的 IIS 虚拟目录。
IIS 不仅控制基址,事实上它还强制您的所有端点使用同一基址(与自承载不同)。这意味着,如果您确实为特定的端点指定了绝对地址,它就必须以与虚拟目录对应的基址开始,否则将出现异常。因此,在 IIS 内承载时,确实只有使用相对地址才有意义。当为特定的 .svc 服务公开多个端点时,您将需要使用相对地址(我们很快将讨论这样做的原因)。
让我们看一个示例。假定您有一个文件,名为 calc.svc,您把它放在一个虚拟目录中,对应于 http://localhost:8080/calcservice。此服务的基址将是 http://localhost:8080/calcservice/calc.svc。假定您已经启用了 HTTP GET 元数据检索,帮助页面将公开在同一地址,这样您可以简单地在 Internet Explorer 中浏览到该处。
现在,考虑在虚拟目录的 web.config 文件中找到的端点配置(见图 3)。在这种情况下,由于我将端点地址留空,因此第一个端点的地址变得与基址相同 (http://localhost:8080/calcservice/calc.svc)。第二个端点地址成为基址加上“secure”的组合,如下所示:http://localhost:8080/calcservice/calc.svc/secure。“mex”端点地址是 http://localhost:8080/calcservice/calc.svc/mex。由于地址的相对部分加入了文件名的右边,这对一些人来说可能有点奇怪,但是您需要记住 calc.svc 是基址的一部分,因此只能这样做。
Figure 3 用于 IIS 承载服务的配置示例
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.         <service name=”CalculatorService”>
  5.           <!-- base address determined by IIS virtual directory -->
  6.           <endpoint binding=”basicHttpBinding”
  7.                     contract=”ISimpleMath”/>
  8.           <endpoint address=”secure”
  9.                     binding=”wsHttpBinding”
  10.                     contract=”ISimpleMath”/>
  11.           <endpoint address=”mex”
  12.                     binding=”mexHttpBinding”
  13.                     contract=”IMetadataExchange”/>
  14.         </service>
  15.         ...
由于我在讨论 IIS 承载主题,因此我也要指出,在 IIS 5.0 和 IIS 6.0 中,您在端点中只能使用不同的 HTTP 绑定。换言之,在一个 IIS 5.0 或 6.0 承载的服务中,您不能承载 TCP 端点,只有 HTTP 端点得到支持。这在 Windows Process Activation Services (WAS) 中有所改变,WAS 附带了代号为“Longhorn”的 Windows Server。WAS 使得在 IIS 内使用与上述相同的 .svc 技术承载非 HTTP 端点成为可能。

多端点和唯一地址
您可能希望公开特定服务上的多个端点有几个原因。原因之一是用一些不同的绑定公开同一约定。例如,您可能有一些使用者,他们只能处理与 WS-I Basic Profile 1.1 兼容的服务(一个绑定),而其他使用者则可以处理一整套标准(另一个绑定)。或者您可能有一些内部的公司使用者,出于性能的原因需要进行二进制 TCP 传输(又一个绑定)。使用不同绑定公开同一约定的能力使您可以同时为所有这些使用者服务。
当使用不同绑定公开多个端点时,每个端点地址都必须唯一。这是因为每个端点都要求不同的传输侦听器和信道堆栈。以图 4 中服务配置为例。在此示例中,所有的端点公开同一约定 (ISimpleMath),但各自都使用不同的绑定,所以每个地址都必须唯一。如果您将端点修改为使用与其他端点相同的地址,Windows Communication Foundation 将在打开 ServiceHost 时引发异常。
Figure 4 使用唯一地址的配置示例
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService”>
  5.         <endpoint address=”http://localhost:8080/calcservice”
  6.                   binding=”basicHttpBinding”
  7.                   contract=”ISimpleMath”/>
  8.         <endpoint address=”http://localhost:8080/calcservice/secure”
  9.                   binding=”wsHttpBinding”
  10.                   contract=”ISimpleMath”/>
  11.         <endpoint address=”net.tcp://localhost:8081/calcservice”
  12.                   binding=”netTcpBinding”
  13.                   contract=”ISimpleMath”/>
  14.       </service>
  15.  ...
但是,如果多个端点共享同一绑定,您可以对所有这些端点使用相同的地址。当您需要使用同一绑定公开多个约定时(这是另外一种需要在单一服务上提供多个端点的主要情形),这个方法尤其有用。在下面的示例中,两个被定义的端点共享同一地址,但他们公开不同的约定:
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService”>
  5.         <endpoint address=”http://localhost:8080/calcservice”
  6.                   binding=”wsHttpBinding”
  7.                   contract=”ISimpleMath”/>
  8.         <endpoint address=”http://localhost:8080/calcservice”
  9.                   binding=”wsHttpBinding”
  10.                   contract=”IScientific”/>
  11. </service>
  12.  ...
在这种情况下,两个端点最终共享同一传输侦听器和信道堆栈(他们只需调度到一组不同的方法)。Windows Communication Foundation 非常智能化,知道它可以创建和使用单一的绑定实例以供两个端点共享,因为两者都为绑定指定了“wsHttpBinding”。
如果您手动在代码中添加端点,而它们又将共享同一地址,您将需要保证在调用 AddServiceEndpoint 时使用同一绑定实例。以下代码示例阐明如何避免这样做:
  1. ServiceHost host = new ServiceHost(typeof(CalculatorService));
  2. host.AddServiceEndpoint(
  3.     typeof(ISimpleMath), new WSHttpBinding(), 
  4.     “http://localhost:8080/calc”);
  5. host.AddServiceEndpoint(
  6.     typeof(IScientific), new WSHttpBinding(), 
  7.     “http://localhost:8080/calc”);
  8. host.Open();
  9. ...
即使两个端点使用相同类型的绑定,我在调用 AddServiceEndpoint 时已经提供了两个完全不同的实例。如果您试图运行此代码,Windows Communication Foundation 在第二次调用 AddServiceEndpoint 时引发异常,指出已有一个绑定实例与该特定端点地址相关联。实际上,您应该创建一个单一的 WSHttpBinding 实例,并将其提供给如下所示的两个 AddServiceEndpoint 调用:
  1. ServiceHost host = new ServiceHost(typeof(CalculatorService));
  2. WSHttpBinding wsbinding = new WSHttpBinding();
  3. host.AddServiceEndpoint(
  4.     typeof(ISimpleMath), wsbinding, “http://localhost:8080/calc”);
  5. host.AddServiceEndpoint(
  6.     typeof(IScientific), wsbinding, “http://localhost:8080/calc”);
  7. host.Open();
  8. ...
客户端不需要关心这两个端点在共享同一地址这一事实。客户端仍将看到两个完全不同的端点,并需要选择其中之一。在这个特定的示例中,svcutil.exe 实际上产生了两个代理类,分别给两个端点,原因是它们公开了不同的约定。两个代理类不过是实际指向了同一地址。

逻辑地址与物理地址
在 Windows Communication Foundation 中,每个服务端点实际上有两个地址与之相关联——一个逻辑地址,一个物理地址。这两个地址之间的区别与 WS-Addressing 中“To”和“Via”的区别相同。逻辑地址(“To”)是 SOAP 消息的目标地址。不同的是,物理地址(“Via”)是 Windows Communication Foundation 侦听抵达消息的实际传输特定网络地址。在整个对象模型中,Windows Communication Foundation 将逻辑地址称之为“Address”(地址)或“Endpoint Address”(端点地址),而将物理地址称之为“ListenUri”。
Windows Communication Foundation 通道基础结构针对物理地址,因为物理地址负责使用特定的传输协议在特定的位置接收传入的消息。另一方面,Windows Communication Foundation 调度程序避开了这种联网细节,而是关注将传入消息映射到一个端点,并最终到达方法调用。
如果您象我到目前为止所做的那样指定端点地址,您实际上是在提供逻辑地址。但是,除非另外指定,否则逻辑地址也被用来作为物理地址。下面的示例阐明如何输出不同的端点详细信息,包括逻辑地址和物理地址。
  1. ...
  2. foreach (ServiceEndpoint se in host.Description.Endpoints)
  3. {
  4.     Console.WriteLine(“Endpoint details:”);
  5.     Console.WriteLine(“Logical address: \t{0}”, se.Address);
  6.     Console.WriteLine(“Physical address: \t{0}”, se.ListenUri);
  7.     Console.WriteLine(“Binding: \t{0}”, se.Binding.Name);
  8.     Console.WriteLine(“Contract: \t{0}”, se.Contract.Name);
  9.     Console.WriteLine();
  10. }
  11. ...
如果您针对前面的示例运行它,它将为两个地址打印同样的值。在大多数情形下,逻辑地址和物理地址没有理由不相同。但是,有些情况下您可能需要指定不同的物理地址。不论是在代码中还是在配置中,您在定义端点时都可以指定 ListenUri。以下示例阐明如何在代码中操作:
  1. host.AddServiceEndpoint(
  2.     typeof(ISimpleMath), wsbinding, 
  3.     “urn:calcservice:simplemath”, // logical 
  4.     new Uri(“http://localhost:8080/calc”)); // physical (listenUri)
  5. host.AddServiceEndpoint(
  6.     typeof(IScientific), wsbinding, 
  7.     “urn:calcservice:scientific”, // logical 
  8.     new Uri(“http://localhost:8080/calc”)); // physical (listenUri)
在配置中,您可以使用 listenUri 属性完成同样的事情,如图 5 所示。现在,当您运行代码打印端点详细信息时,您应该看到逻辑地址和物理地址的不同值。在此示例中,两个端点共享同一个物理地址(在此情况下是 http://localhost:8080/calcservice),但各自有一个以 URN 形式存在的唯一逻辑地址。
Figure 5 指定 ListenUri
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService”>
  5.         <endpoint address=”urn:calcservice:simplemath”
  6.                   listenUri=”http://localhost:8080/calcservice” 
  7.                   binding=”wsHttpBinding”
  8.                   contract=”ISimpleMath”/>
  9.         <endpoint address=”urn:calcservice:scientific”
  10.                   listenUri=”http://localhost:8080/calcservice” 
  11.                   binding=”wsHttpBinding”
  12.                   contract=”IScientific”/>
  13.       </service>
  14.       ...
如果您需要保证某个 ListenUri 在机器上是唯一的,可以使用 ListenUriMode 属性,将其值设为 ListenUriMode.Unique。操作时,通道基础结构将选择一个唯一的端口号(在 TCP 的情况下不启用端口共享),或者在使用 ListenUri 地址之前附加一个 GUID 到它的最后。
需要指出的是,您在指定 ListenUri 时也可以使用相对地址,Windows Communication Foundation 将根据对应的基址进行解析,与处理逻辑端点地址的方式相同。
现在,您可能在琢磨客户端如何与一个配置了不同 ListenUri 地址的服务进行交互。客户端意识不到 ListenUri。客户端所知道的只是每个端点有一个单独的地址。由于 WSDL 定义包含了每个端点的逻辑地址,因此 svcutil.exe 将逻辑地址嵌入客户端配置中,并默认使用它进行传输。
当某个服务已经配置了不同的物理地址后,您将需要一个带外机制,通知客户端要使用的物理地址。然后客户端可以指示 Windows Communication Foundation to 通过物理地址传送外发消息,就如同它是路由器或者某种类型的中介一样。ClientViaBehavior 使这一点变得简单,如下所示:
  1. SimpleMathClient client = new SimpleMathClient(
  2.     “WSHttpBinding_ISimpleMath”);
  3. client.Endpoint.Behaviors.Add(new ClientViaBehavior(
  4.         new Uri(“http://localhost:8080/calcservice”)));
  5. double sum = client.Add(3, 4);
在客户端的配置文件内应用 ClientViaBehavior 也是可能的,如图 6 所示。现在,客户端通过服务端点使用的同一物理地址传送消息,而不是试图将消息传送到“urn:calcservice:simplemath”。当消息到达 ListenUri 时,调度程序可以通过检查逻辑地址 (URN) 确定使用哪个端点。
Figure 6 在配置中应用 ClientViaBehavior
  1. <configuration>
  2.   <system.serviceModel>
  3.     <client>
  4.       <endpoint address=”urn:calcservice:simplemath” 
  5.                 binding=”wsHttpBinding”
  6.                 behaviorConfiguration=”Via”
  7.                 contract=”Client.localhost.ISimpleMath”
  8.                 name=”WSHttpBinding_ISimpleMath”/>
  9.       ...
  10.     </client>
  11.     <behaviors>
  12.       <endpointBehaviors>
  13.         <behavior name=”Via”>
  14.           <clientVia viaUri=”http://localhost:8080/calcservice”/>
  15.         </behavior>
  16.       </endpointBehaviors>
  17.     </behaviors>
  18.     ...
为什么会有人希望使用这些技术?答案很简单:路由。任何时候您需要通过中介将消息传输到最终目的地,您需要了解 Address 和 ListenUri 之间的区别并正确地使用它们。这在某些企业中很常见,其中一个中央“网关”负责为所有已部署服务处理常见任务,如安全性、记录和统计。
TcpTrace 正是此类型的中介,唯一的区别在于,它是供开发人员用于诊断目的。假定您想要使用 TcpTrace 截取所有传送至服务的消息,但又不想更改服务代码。您很容易就可以做到这一点,方法是为客户端配置 ClientViaBehavior,指向 TcpTrace 侦听地址,瞬间就完成了。
如果不要求客户端使用 ClientViaBehavior,还有另外一种方式可以达到同样的目的。在定义服务端点时,您可以使用 TcpTrace 侦听的同一地址作为逻辑地址(因为此地址出现在 WSDL 中,因此会导致客户端发送消息到 TcpTrace 侦听地址)。然后,您为端点 (ListenUri) 指定不同的物理地址,并将 TcpTrace 配置为向该地址转发。
在此示例中,TcpTrace 就象其他完成简单路由的中介一样。延用到更加复杂的自定义路由方案也并不困难。

寻址标头
为了适应更加复杂的路由和调度逻辑,您可能希望以自定义寻址标头注释 SOAP 消息。自定义寻址标头与传统的 WS-Addressing 标头一起包含在 SOAP 消息中。例如,如果传入消息包含 <premium> 会员标头,您将其调度到功能丰富的端点,如果它包含 <basic> 标头,您将其发送到只有基本功能的端点。
您可能在想,为什么不把此信息指定为服务约定的一部分,使之出现在消息正文中呢?问题在于,使用者可能不知道或没有所需的信息。实际上,该信息需要由传送到端点的中介提供。例如,中介查看所提供的会员 id、确定会员级别,并将适当的标头添加到外发的消息中。
您可以将自定义寻址标头与端点相关联以定义更多的调度标准。这样操作时,只有包含必需寻址标头的消息才将被调度到给定的端点。
如果客户端需要了解任何必需的标头,他们同样应该被包括在服务的 WSDL 定义中;寻址标头通常作为引用参数在 <EndpointReference> 元素中进行描述(<EndpointReference> 放在 WSDL <port> 元素内)。
这或许听起来有点复杂,但 Windows Communication Foundation 使寻址标头的使用变得简单。以图 7 所示的服务配置为例。在此示例中,我定义了共享同一逻辑/物理地址的两个端点,但他们各自公开不同的约定。每个端点还要求特定的寻址标头。第一个要求 <basic> 标头,而第二个要求 <premium> 标头。只有包含这些寻址标头之一的传入消息才被调度到这些端点之一。
Figure 7 有自定义寻址标头的端点
  1. <configuration>
  2.   <system.serviceModel>
  3.     <services>
  4.       <service name=”CalculatorService” 
  5.                behaviorConfiguration=”metadata”>
  6.         <host>
  7.           <baseAddresses>
  8.             <add baseAddress=”http://localhost:8080/calcservice”/>
  9.           </baseAddresses>
  10.         </host>
  11.         <endpoint binding=”wsHttpBinding” contract=”ISimpleMath”>
  12.           <headers>
  13.             <basic xmlns=”http://example.org/level”/>
  14.           </headers>
  15.         </endpoint>
  16.         <endpoint binding=”wsHttpBinding” contract=”IScientific”>
  17.           <headers>
  18.             <premium xmlns=”http://example.org/level”/>
  19.           </headers>
  20.         </endpoint>
  21.       </service>
  22.  ...
在代码中指定寻址标头也是可能的,只需手动创建 AddressHeader 对象,并将其提供给一个 EndpointAddress 对象。然后,您可以创建新的 ServiceEndpoint(基于 EndpointAddress),并将其添加到主机的 Endpoints 集合,如下所示:
  1. ServiceHost host = new ServiceHost(typeof(CalculatorService));
  2. AddressHeader header = 
  3.     AddressHeader.CreateAddressHeader(“basic”, 
  4.        “http://example.org/level”, null);
  5. EndpointAddress ea = new EndpointAddress(
  6.     new Uri(“http://localhost:8080/calcservice/foobar”), header);
  7.  
  8. host.Description.Endpoints.Add(
  9.     new ServiceEndpoint(
  10.         ContractDescription.GetContract(typeof(ISimpleMath)),
  11.         new WSHttpBinding(), ea));
  12. ...
如果您检查此特定端点配置的 WSDL,您将找到 <service> 元素中的详细信息(请参见图 8)。<port> 元素包含一个 <wsa:EndpointReference> 元素,而后者又包含必需的标头作为引用参数。当您通过 svcutil.exe 运行 WSDL 时,您将得到一个包括同样 <headers> 元素的类似客户端配置。这样,当您使用该特定端点时,Windows Communication Foundation 将自动在外发消息中包括必需的 <basic> 标头,如图 9 所示。
Figure 9 SOAP 中发送的自定义寻址标头
  1. <s:Envelope xmlns:s=”http://www.w3.org/2003/05/soap-envelope” 
  2.             xmlns:a=”http://www.w3.org/2005/08/addressing”>
  3.   <s:Header>
  4.     <a:Action s:mustUnderstand=”1”
  5.      >http://example.org/calc/Add</a:Action>
  6.     <a:MessageID
  7.      >urn:uuid:db1ba7a6-ca2e-494b-b390-9f621e8b90f4</a:MessageID>
  8.     <a:ReplyTo>
  9.       <a:Address
  10.        >http://www.w3.org/2005/08/addressing/anonymous</a:Address>
  11.     </a:ReplyTo>
  12.     <a:To s:mustUnderstand=”1”>http://localhost:8080/calcservice</a:To>
  13.     <basic a:IsReferenceParameter=”true” 
  14.       xmlns=”http://example.org/level”/>
  15.   </s:Header>
  16.   <s:Body>
  17.   ...
Figure 8 WSDL 中的自定义寻址标头
  1. ...
  2. <wsdl:service name=”CalculatorService”>
  3.   <wsdl:port name=”WSHttpBinding_ISimpleMath” 
  4.              binding=”tns:WSHttpBinding_ISimpleMath”>
  5.     <soap12:address location=”http://localhost:8080/calcservice”/>
  6.     <wsa10:EndpointReference>
  7.       <wsa10:Address>http://localhost:8080/calcservice</wsa10:Address>
  8.       <wsa10:ReferenceParameters>
  9.         <basic xmlns=”http://example.org/level”/>
  10.       </wsa10:ReferenceParameters>
  11.       ...
  12.     </wsa10:EndpointReference>
  13.   </wsdl:port>
  14.   ...
  15. </wsdl:service>
  16. ...
您可以确认调度程序正在寻找 <basic> 标头,方法是从生成的客户端配置中删除 <headers> 元素并重新调用服务,此时调度程序将失败。

消息筛选器
您甚至可以更进一步地通过利用所谓的消息筛选器来自定义调度进程。当 Windows Communication Foundation 调度传入消息时,它使用消息筛选器确定匹配端点(如果有)。您可以选择要使用的消息筛选器,也可以自己提供一个。这种灵活性允许您在使用 Windows Communication Foundation 时摆脱传统调度模型,实现传统 SOAP 以外的方式 — 例如,在此描述的技术使您可以在 Windows Communication Foundation 消息传递基础之上实现 REST/POX 样式的服务。
每个端点实际上关联着两个筛选器:一个地址筛选器和一个约定筛选器。地址筛选器确定传入消息是否匹配端点的“To”地址和任何必需的地址标头,而约定筛选器则确定它是否匹配端点的约定。两个筛选器都被调度程序用来确定目标端点。
筛选器只是 MessageFilter(定义了一些抽象的 Match 方法)派生而来的类。MessageFilter 有几个内置实现,包括 EndpointAddressMessageFilter 和 ActionMessageFilter,它们分别作为默认地址和约定筛选器。EndpointAddressMessageFilter 仅仅将“To”地址与端点地址进行比较,预期它们完全匹配。它也将传入消息中获得的寻址标头和端点要求的一组寻址标头进行比较。ActionMessageFilter 将传入的“Action”值和约定上的操作进行比较,再次预期完全匹配。
还有其它寻址筛选器提供不同的行为。例如,只要传入的“To”地址与端点地址有相同的地址前缀(一种松散匹配),PrefixEndpointAddressMessageFilter 将导致两者匹配。还有一个 MatchAllMessageFilter,它导致所有消息匹配给定端点。
改变服务所使用的消息筛选器的一个简单办法是通过 [ServiceBehavior] 的 AddressFilterMode 属性。AddressFilterMode 带有三个值:Any、Exact 和 Prefix。每个这些值都映射对相应的 MessageFilter 实现。以下示例阐明了如何配置我的服务,以使用 PrefixEndpointAddressMessageFilter:
  1. [ServiceBehavior(AddressFilterMode=AddressFilterMode.Prefix)]
  2. public class CalculatorService : ISimpleMath, IScientific
  3. {
  4.    ...
通过编写一些代码也可以改变每个端点的筛选器。您甚至可以编写自己的 MessageFilter 实现以包含一些自定义的匹配逻辑。Windows Communication Foundation 确实附带了一个 XPathMessageFilter,允许您在匹配过程中针对传入消息评估任意 XPath 表达式,独立涵盖内容驱动的方案。

主机名称比较
关于地址和调度,最后要注意的一点是:定义端点时,将特定主机名称硬编码入物理地址很常见。但如果您希望配置端点以侦听绑定在特定机器(localhost、contoso、www.contoso.com 等)上的所有主机名称?或如果您希望限制端点只匹配一个特定主机名称,该怎么办?
您可以通过 HostNameComparisonMode(在大多数绑定上都作为属性提供)进行进一步控制。HostNameComparisonMode 附带三个设置:StrongWildcard(默认)、Exact 和 WeakWildcard。
StrongWildcard 和 WeakWildcard 表示任何主机名称加上端口号的组合将匹配端口地址(包括 IP 地址)。StrongWildcard 匹配比 WeakWildcard 匹配具有更高的优先级。Exact 表示主机名称加上端口号必须完全匹配端点地址(至少是不需要区分大小写的匹配)。
由于默认模式是 StrongWildcard,因此您的端点应自动处理 localhost 与您计算机名称的差异,因此只需要使用此特性就可以进一步约束匹配过程。
注意,本文讨论的许多寻址技术只能配合使用为使用 SOAP 1.2 和 WS-Addressing 而配置的绑定。如果在使用中出现问题,请确认您使用的类似于 WSHttpBinding 的新绑定。有关如何启用绑定以使用 WS-Addressing 的更多信息,请参见上月名为“WCF 消息传递基础”的专栏。

结束语
Windows Communication Foundation 消息传递框架为开发人员提供了许多寻址功能,这些功能旨在简化常见的承载和通信方案,并在应对复杂的路由和调度逻辑时增加灵活性。将这些寻址技术加入您的工具箱会提高将来的工作效率,更不用说按照您的意愿来塑造 Windows Communication Foundation 的技能了。

将您想向 Aaron 询问的问题和提出的意见发送至 sstation@microsoft.com.


Aaron Skonnard 是 Microsoft .NET 培训提供商 Pluralsight 的创始人之一。Aaron 是 Pluralsight 推出的《Web Services 2.0 应用》、《BizTalk Server 2006 应用》和《Windows Communication Foundation 应用》等众多课程的作者。多年以来,Aaron 一直致力于编写课程、会议演讲以及面向专业开发人员的教学。您可以通过 pluralsight.com/aaron 与他联系。