EndpointAddress——不只是一个Uri[下篇]
《上篇》对AddressHeader在服务端和客户端的作用,以及如何通过配置和编成的方式设置AddressHeader进行了详细介绍。现在我们通过一个实例来演示终结点的地址报头如何影响实现终结点选择的消息筛选机制。这个实例通过为服务端终结点指定地址报头实现针对客户端的授权,让经过许可的客户端才能访问这个服务。具体来说,我们将一个代码序列号的GUID作为终结点的地址报头。对于客户端发送的消息,只有具有相应的报头才能访问服务。[三个实例源代码下载地址:实例1、实例2和实例3]
一、无地址报头下服务调用(实例1)
我们采用计算服务的例子,整个实例的解决方案具有右图所示的3个项目。其中类库项目Service.Interface用于定义契约接口。Service项目是一个控制台应用程序,用于定义服务类型和作为服务的宿主。控制台应用程序Client代码进行服务调用的客户端。在本书后续部分的绝大部分实例都会采用这个结构。
实例演示的目的旨在旨在指导读者编程,或者说明某个方面的原理,所以我会将服务承载的业务功能尽量地简化。所以我们分别在Service.Interface和Service项目中定义了如下所示的契约接口ICalculator和服务类型CalculatorService。ICalculator仅仅具有唯一的表示加法运算的Add操作。
ICalculator:
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Service.Interface
3: {
4: [ServiceContract(Name = "CalculatorService", Namespace ="http://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract]
8: double Add(double x, double y);
9: }
10: }
CalculatorService:
1: using Artech.WcfServices.Service.Interface;
2: namespace Artech.WcfServices.Service
3: {
4: public class CalculatorService : ICalculator
5: {
6: public double Add(double x, double y)
7: {
8: return x + y;
9: }
10: }
11: }
服务CalculatorService通过控制台程序Service进行寄宿。下面是服务寄宿代码和相应的配置。从配置可以看到,服务唯一的终结点具有一个作为地址报头的<sn>元素,它的值代表服务的序列号。
服务寄宿程序:
1: using System;
2: using System.ServiceModel;
3: namespace Artech.WcfServices.Service
4: {
5: class Program
6: {
7: static void Main(string[] args)
8: {
9: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
10: {
11: host.Open();
12: Console.Read();
13: }
14: }
15: }
16: }
配置:
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Service.CalculatorService">
5: <endpoint address="http://127.0.0.1:3721/calculatorservice"
6: binding="ws2007HttpBinding"
7: contract="Artech.WcfServices.Service.Interface.ICalculator">
8: <headers>
9: <sn xmlns="http://www.artech.com/">
10: {DDA095DA-93CA-49EF-BE01-EF5B47179FD0}
11: </sn>
12: </headers>
13: </endpoint>
14: </service>
15: </services>
16: </system.serviceModel>
17: </configuration>
客户端通过ChannelFactory<TChannel>创建的服务代理进行服务调用。下面是进行服务调用的程序和客户端配置。
服务调用程序:
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Service.Interface;
4: namespace Artech.WcfServices.Client
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
11: {
12: ICalculator calculator = channelFactory.CreateChannel();
13: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2,calculator.Add(1,2));
14: }
15: Console.Read();
16: }
17: }
18: }
配置:
1: <configuration>
2: <system.serviceModel>
3: <client>
4: <endpoint name="calculatorservice"
5: address="http://127.0.0.1:3721/calculatorservice"
6: binding="ws2007HttpBinding"
7: contract="Artech.WcfServices.Service.Interface.ICalculator"/>
8: </client>
9: </system.serviceModel>
10: </configuration>
由于进行服务调用的客户端终结点并没有一个相应的表示序列号的<sn>地址报头,在进行服务调用的时候没有显式地将序列号作为报头添加到请求消息中,所以针对服务端来说,这是一个不被许可的客户端。客户端运行后将会抛出如下图所示的EndpointNotFoundException异常。(S201)
二、为请求消息添加地址报头(实例2)
假设服务端将作为序列化的GUID分发给经过许可的客户端,那么它就可以将其作为客户端终结点的地址报头定义到配置文件中,也可以在消息发送之前将序列化作为报头添加到请求消息中。第一种方式比较简单,我们来演示第二种方式。我们采用如下的代码进行服务调用,在调用之前将序列号作为报头添加到请求消息的报头列表中。在这种情况下,服务嗲用将会顺利进行。(S202)
1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
2: {
3: ICalculator calculator = channelFactory.CreateChannel();
4: using (OperationContextScope contextScope = new OperationContextScope(calculator as IClientChannel))
5: {
6: string sn = "{DDA095DA-93CA-49EF-BE01-EF5B47179FD0}";
7: string ns = "http://www.artech.com/";
8: AddressHeader addressHeader = AddressHeader.CreateAddressHeader("sn", ns, sn);
9: MessageHeader messageHeader = addressHeader.ToMessageHeader();
10: OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader);
11: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2));
12: }
13: }
输出结果:
1: x + y = 3 when x = 1 and y = 2
之所以在请求消息中不存在于终结点地址报头相匹配的报头会导致抛出EndpointNotFoundException异常,原因在于按照默认的消息筛选机制找不到匹配的终结点。为了解决这个问题,对于客户端来说,可以通过在消息中添加相应的报头满足服务端筛选的条件;而对于服务端来说,则可以改变为了实现终结点的选择而采用消息筛选机制。总之一句话,只要服务端能够根据匹配的终结点就可以抑制EndpointNotFoundException异常的抛出。
三、改变地址筛选策略(实例3)
我们可以在服务类型上应用ServiceBehaviorAttribute特性并为AddressFilterMode属性进行相应的设置来改变针对终结点地址的筛选机制。如下面的代码所示,AddressFilterMode属性是一个类型为AddressFilterMode的枚举。三个枚举项(Exact、Prefix和Any)分别代表三种地址匹配的策略,即精确匹配,基于前缀匹配和匹配任意地址。
1: [AttributeUsage(AttributeTargets.Class)]
2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
3: {
4: [DefaultValue(0)]
5: public AddressFilterMode AddressFilterMode { get; set; }
6: }
7: public enum AddressFilterMode
8: {
9: Exact,
10: Prefix,
11: Any
12: }
其中Exact和Prefix都需要进行地址报头的匹配,而Any则不需要。从应用在AddressFilterMode的DefaultValueAttribute特性可以看出,该属性的默认值是Exact,所以在默认的情况下采用的是针对地址的精确匹配。那么如果我们在CalculatorService上应用ServiceBehaviorAttribute特性并将AddressFilterMode设置为Any,即使请求消息中不具有相关的报头,服务调用也会成功。(S203)
1: [ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
2: public class CalculatorService : ICalculator
3: {
4: //省略成员
5: }
本例虽然名为通过“通过地址报头实现对客户端的授权”,其实在真正的应用中我们不会通过这样的方式对服务授权。因为终结点的地址报头是元数据的一部分,客户端在获取服务发布的元数据时会将地址报头一并获取。