EndpointAddress——不只是一个Uri[上篇]

时间:2022-04-27
本文章向大家介绍EndpointAddress——不只是一个Uri[上篇],主要内容包括一、EndpointAddress的三个功能、二、AddressHeader和AddressHeaderCollection、三、终结点地址报头的指定、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

终结点是整个WCF的核心,由经典的ABC三要素组成。作为表示地址的EndpointAddress,很多人仅仅将其看成是一个表示标识服务并且表示服务所在地址的Uri,其实服务标识和定位服务仅仅是EndpointAddress一个基本的功能,它不仅仅是Uri那么简单。

一、EndpointAddress的三个功能

作为终结点的三要素之一的地址(Address),在基于WCF的通信中不仅仅定位着服务的位置,而且还提供额外的寻址信息。除此之外,终结点地址还和安全有关系,因为它包含着用于进行服务认证的服务身份信息。这三个典型功能(服务标识/定位、辅助寻址和服务身份标识)分别对应着Uri、Headers和Identity三个只读属性:

   1: public class EndpointAddress
   2: {
   3:     //其他成员    
   4:     public Uri                        Uri { get; }
   5:     public AddressHeaderCollection    Headers { get; }
   6:     public EndpointIdentity           Identity { get; }
   7: }

EndpointAddress的属性Uri通过一个统一资源标识符(URI:Uniform Resource Identifier)既作为服务的唯一标识,也作为服务的目标地址。这个地址可以是服务的物理地址,也可以是逻辑地址。

而类型为EndpointIdentity的Identity属性服务的身份,被客户端用于针对服务的认证。具体来说,客户端终结点通过地址的该属性表示自己希望调用服务的真实身份。在调用之前,服务端将自己的凭证(Windows凭证、X.509证书凭证等)提供给客户端。客户端通过整个以EndpointIdentity对象代表的服务身份与凭证进行比较从而验证正在调用服务确实是自己所希望调用的,而不是一个钓鱼服务。关于基于EndpointIdentity的服务认证,在《服务凭证(Service Credential)与服务身份(Service Identity)》中有详细的介绍。

属性Headers是一个类型为AddressHeaderCollection的集合。其元素是一个代表地址报头的AddressHeader对象。EndpointAddress通过以Headers属性代表的地址报头列表存放一些寻址的信息。本篇文章着重讲述地址报头。WCF的通信完全建立在消息交换上,而WCF支持多种不同类型的消息。消息的格式可以使基于XML的,也可以是非XML的(比如采用JSON格式的消息)。而我们使用的最多地XML消息类型是SOAP。一个完整的SOAP消息由一个消息主体(Body)和一组消息报头(Header)组成。主体部分一般是对业务数据的封装,而消息报头用于保存一些控制信息。

对于客户端来说,终结点地址上的AddressHeader列表最终都会被添加到请求消息(这里指SOAP消息)的报头集合中。而针对服务端来说,在根据请求消息进行终结点路由过程中,会提取相应的报头信息和本地终结点的地址报头进行比较以选择出于请求消息相匹配的终结点。

二、AddressHeader和AddressHeaderCollection

我们先来看看表示单个地址报头的AddressHeader对象。如下面的代码片断所示,AddressHeader实际上是个抽象类。实际上WCF并没有定义继承AddressHeader的公有子类(AddressHeader所有具体的子类都是内部类型),我们只能通过定义在AddressHeader中的三个CreateHeader方法来创建AddressHeader对象。

   1: public abstract class AddressHeader
   2: {
   3:     //其他成员
   4:     public static AddressHeader CreateAddressHeader(object value);
   5:     public static AddressHeader CreateAddressHeader(object value, XmlObjectSerializer serializer);
   6:     public static AddressHeader CreateAddressHeader(string name, string ns, object value);
   7:  
   8:     public T GetValue<T>();
   9:     public T GetValue<T>(XmlObjectSerializer serializer);
  10:     public MessageHeader ToMessageHeader(); 
  11:  
  12:     public abstract string Name { get; }
  13:     public abstract string Namespace { get; }
  14: }

CreateAddressHeader方法接受一个可序列化的对象作外参数,它会对指定对象进行序列化并将序列化后的内容作为地址报头的内容。默认采用的序列化器类型是DataContractSerializer,我们也可以调用第二个重载认为地指定序列化器。除了提供可序列化对象作为地址报头的内容之外,我们还可以调用第三个CreateAddressHeader方法重载直接指定一个字符串作为创建的地址报头的值。AddressHeader的值可以通过调用GetValue<T>方法返回。该方法的执行涉及到对报头值得反序列化,所以需要指定相应的序列化器。默认情况下采用DataContractSerializer。

AddressHeader对象最终需要转换成SOAP消息的报头,而SOAP报头具有自己的名称和命名空间。当我们调用第三个CreateAddressHeader方法重载的时候,除了传入作为报头值得字符串之外,还需要传输名称和命名空间。而传输的名称和命名空间可以通过只读属性Name和Namespace返回。针对可序列化对象创建的AddressHeader对象,其属性Name和Namespace返回的是对象序列化后生成的XML的根节点的名称和命名空间。

消息报头通过System.ServiceModel.Channels.MessageHeader类型表示。AddressHeader向MessageHeader的转换可以直接通过调用ToMessageHeader方法实现。AddressHeaderCollection表示AddressHeader的集合。如下面的代码片断所示,AddressHeaderCollection继承自ReadOnlyCollection<AddressHeader>,所以该集合是只读的。

   1: public sealed class AddressHeaderCollection:ReadOnlyCollection<AddressHeader>
   2: {
   3:     //其他成员
   4:     public AddressHeaderCollection();
   5:     public AddressHeaderCollection(IEnumerable<AddressHeader> addressHeaders);
   6:  
   7:     public void AddHeadersTo(Message message);
   8:     public AddressHeader[] FindAll(string name, string ns);
   9:     public AddressHeader FindHeader(string name, string ns);
  10: }

通过AddHeadersTo方法可以很容易地将一个AddressHeaderCollection对象添加到一个代表消息的Message对象的报头列表中。FindAll和FindHeader根据报头的名称和命名空间找到对应的AddressHeader。FindAll得到所有相关的AddressHeader,而FindHeader只获得满足条件的第一个AddressHeader。

由于EndpointAddress的Headers属性代表的是一个只读的集合,我们不能直接将创建的AddressHeader添加到该集合中。所以地址报头只能在创建EndpointAddress的时候通过构造函数参数的方式指定。如下面的代码片断所示,EndpointAddress的四个构造函数中,既提供了作为可选参数的addressHeaders,又提供类型为AddressHeaderCollection的headers参数。它们都表示为EndpointAddress添加的AddressHeader列表。

   1: public class EndpointAddress
   2: {
   3:     //其他成员
   4:     public EndpointAddress(Uri uri, params AddressHeader[] addressHeaders);
   5:     public EndpointAddress(Uri uri, EndpointIdentity identity, params AddressHeader[] addressHeaders);
   6:     public EndpointAddress(Uri uri, EndpointIdentity identity, AddressHeaderCollection headers);
   7:     public EndpointAddress(Uri uri, EndpointIdentity identity, AddressHeaderCollection headers, XmlDictionaryReader metadataReader, 
   8:         XmlDictionaryReader extensionReader);   
   9: }

三、终结点地址报头的指定

在进行服务寄宿的时候,我们可以为添加的终结点地址指定一个或者多个AddressHeader。而客户端在通过指定EndpointAddress对象创建ChannelFactory<TChannel>或者ClientBase<TChannel>对象的时候,都可以为终结点地址指定相应的地址报头。在下面的代码中,我们为寄宿的CalculatorService添加了一个基于WSHttpBinding的终结点。而终结点地址具有一个值为“Licensed User”的地址报头,表示许可用户才能调用该终结点。

   1: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
   2: {
   3:     Uri uri = new Uri("http://127.0.0.1:3721/calculatorservice");
   4:     AddressHeader header = AddressHeader.CreateAddressHeader("Licensed User", "http://www.artech.com", "UserType");
   5:     EndpointAddress address = new EndpointAddress(uri, header);
   6:     Binding binding = new WS2007HttpBinding();
   7:     ContractDescription contract = ContractDescription.GetContract(typeof(ICalculator));
   8:     ServiceEndpoint endpoint = new ServiceEndpoint(contract, binding, address);
   9:     host.AddServiceEndpoint(endpoint);
  10:     host.Open();
  11:     //...            
  12: }

终结点地址报头同样可以通过配置的方式来定义。一个通过AddressHeader对象最终体现为一个XML元素。不论是服务端终结点配置节(<services>/<service>/<endpoint>),还是客户端终结点配置节(<client>/<endpoint>)都具有一个<headers>子节点。你可以在该节点中定义任意的XML作为该终结点的地址报头列表。

   1: <configuration>
   2:   <system.serviceModel>
   3:     <services>
   4:       <service ...>
   5:         <endpoint ...>
   6:           <headers>
   7:             <服务终结点地址报头>
   8:           </headers>
   9:         </endpoint>
  10:       </service>
  11: </services>
  12: <client>
  13:       <endpoint ...>
  14:           <headers>
  15:             <客户端终结点地址报头>
  16:           </headers>
  17:         </endpoint>
  18: </client>
  19:   </system.serviceModel>
  20: </configuration>

上面通过编程方式指定的地址报头就可以通过如下一段配置来指定。而<headers>结点下的<UserType>元素就是通过编程方式指定的AddressHeader的值序列化后的XML。

   1: <configuration>
   2:   <system.serviceModel>
   3:     <services>
   4:       <service ...>
   5:         <endpoint ...>
   6:           <headers>
   7:             <UserType xmlns=" http://www.artech.com ">Licensed User</UserType>
   8:           </headers>
   9:         </endpoint>
  10:       </service>
  11:     </services>
  12:   </system.serviceModel>
  13: </configuration>

服务端和客户端终结点的地址报头具有不同的作用,服务端终结点的地址报头主要用于辅助实现对终结点的选择。由于一个服务可以具有多个终结点,服务端在接收到请求消息后需要将其分发给匹配的终结点。WCF通过消息筛选机制实现基于请求消息对匹配终结点的选择。在默认情况下,WCF采用基于地址匹配的消息筛选策略。由于消息(SOAP)具有一个<To>报头表示调用服务的地址,被选择的终结点的地址必须具有相匹配的Uri。其次,如果终结点地址具有相应的地址报头,要求请求消息具有相应的报头。只有满足这两个条件的终结点才会最终被选择用于处理请求消息。

如果客户端终结点地址指定了相应的地址报头,最终发送的消息将包含一个相应的报头。比如说针对下面一段进行服务调用的代码,创建ChannelFactory<TChannel>针对的终结点具有一个“Licensed User”地址报头。最终生成的SOAP消息将具有一个<UserType>报头。

服务调用代码:

   1: Uri uri = new Uri("http://127.0.0.1:3721/calculatorservice");
   2: AddressHeader header = AddressHeader.CreateAddressHeader("Licensed User","http://www.artech.com", "UserType");
   3: EndpointAddress address = new EndpointAddress(uri, header);
   4: Binding binding = new WS2007HttpBinding();
   5: ContractDescription contract = ContractDescription.GetContract(typeof(ICalculator));
   6: ServiceEndpoint endpoint = new ServiceEndpoint(contract, binding, address);
   7:  
   8: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(endpoint))
   9: {
  10:     ICalculator calculator = channelFactory.CreateChannel();
  11:     double result = calculator.Divide(1, 2);
  12:     //...
  13: }
  14:  

SOAP消息:

   1: <s:Envelope ...>
   2:     <s:Header>
   3:         <UserType xmlns="http://www.artech.com/">Licensed User</UserType>
   4:         ...
   5:     </s:Header>
   6:     <s:Body>
   7:         ...
   8:     </s:Body>
   9: </s:Envelope>

由于客户端终结点地址报头的最终目的是为请求消息添加添加相应的报头,所以如果我直接在消息上以手工的方式添加相应的报头也能得到相同的效果。如果要实现消息报头的手工添加,首选需要解决的是如何获得请求消息。当前的请求消息可以通过表示操作指定上下文的OperationContext对象获取。如下面的代码所示,OperationContext具有IncomingMessageHeaders和OutgoingMessageHeaders两个类型为System.ServiceModel.Channels.MessageHeaders的属性,分别表示如栈消息和出栈消息的报头列表。对于客户端来说,所谓入栈消息和出栈消息就是指的回复消息和请求消息,而对于服务端则正好相反。OperationContext中的静态Current属性表示当前的操作调用/执行上下文。

   1: public sealed class OperationContext : ...
   2: {
   3:     //其他成员
   4:     public MessageHeaders IncomingMessageHeaders { get; }   
   5:     public MessageHeaders OutgoingMessageHeaders { get; }
   6:     public static OperationContext Current { get; set; }
   7: }

倘若客户端终结点不曾定义<UserType>地址报头,但是服务端却要求请求消息必须具有这么一个消息报头,那么可以可以通过如下的编程方式将创建的AddressHeader手工地添加到请求消息的报头集合中。

   1: Uri uri = new Uri("http://127.0.0.1:3721/calculatorservice");
   2: AddressHeader header = AddressHeader.CreateAddressHeader("Licensed User", "http://www.artech.com", "UserType");
   3: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
   4: {            
   5:     ICalculator calculator = channelFactory.CreateChannel();
   6:     using (OperationContextScope operationContextScope = new OperationContextScope(calculator as IContextChannel))
   7:     {
   8:         OperationContext.Current.OutgoingMessageHeaders.Add(header.ToMessageHeader());
   9:         double result = calculator.Divide(1, 2);
  10:         //...
  11:     }
  12: }

现在我们通过一个实例来演示终结点的地址报头如何影响实现终结点选择的消息筛选机制。这个实例通过为服务端终结点指定地址报头实现针对客户端的授权,让经过许可的客户端才能访问这个服务。具体来说,我们将一个代码序列号的GUID作为终结点的地址报头。对于客户端发送的消息,只有具有相应的报头才能访问服务。至于如何实现,请听下回分解。

EndpointAddress——不只是一个Uri[上篇] EndpointAddress——不只是一个Uri[下篇]