WCF技术剖析(卷1)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1.3 如何指定AddressHeader

WCF是一个基于消息的通信框架,具有一个消息处理的管道。消息流到该管道不同的位置,要对消息进行相应的处理。虽然基于REST/POX的Web Service越来越盛行,但是基于SOAP的Web Service还是主流。SOAP已经成为了Web Service事实上的标准,SOAP同时也是整个WS-*体系结构的基础。得益于可扩展的SOAP消息结构:SOAP报头+消息主体,我们可以把各种各样的控制信息置于SOAP封套(Envelope)的报头集合中,各种WS-* 协议(比如WS-Addressing、WS-Reliable Messaging、WS-Security等)通过定义各自的SOAP报头得以实现。

对于基于消息的通信方式来说,寻址(Addressing)是需要首先解决的问题。一般情况下,WCF通过WS-Addressing规定的标准解决寻址的问题。也就是说通过WS-Addressing标准的SOAP报头内容对目标地址进行解析。典型的寻址相关的报头包含:<wsa:To>、<wsa:Via>、<wsa:ReplyTo>等。

但是在某些情况下,默认的这种寻址方式不能完全解决现实中的具体需求,我们需要一种不同的方式对消息进行路由,在这种情况下,需要实现手工寻址(Manual Addressing)。对于实现手工寻址所需相关的辅助信息,需要定义在SOAP消息的报头中。WCF通过一个特殊的对象,AddressHeader来表示这种寻址相关的SOAP消息报头。

AddressHeaderAddressHeaderCollection

AddressHeader定义在System.ServiceModel.Channels命名空间下,表示用于消息寻址相关的信息的报头,下面是AddressHeader的简单定义:

        public abstract class AddressHeader
        {
            //其他成员
            public static AddressHeader CreateAddressHeader(object value);
            public static AddressHeader CreateAddressHeader(object value,
              XmlObjectSerializer serializer);
            public static AddressHeader CreateAddressHeader(string name, string ns,
              object value);
            public T GetValue<T>();
            public T GetValue<T>(XmlObjectSerializer serializer);
            public MessageHeader ToMessageHeader();
            public void WriteAddressHeader(XmlDictionaryWriter writer);
            public void WriteAddressHeader(XmlWriter writer);
            public void WriteAddressHeaderContents(XmlDictionaryWriter writer);
            public void WriteStartAddressHeader(XmlDictionaryWriter writer);
            public abstract string Name { get; }
            public abstract string Namespace { get; }
        }

通过AddressHeader的静态方法CreateAddressHeader可以很方便地创建AddressHeader对象;GetValue方法用于获取AddressHeader的值;通过ToMessageHeader可以将AddressHeader转化为MessageHeader对象,而MessageHeader对象表示的就是一个消息的报头。4个WriteXXX方法将AddressHeader写入一个流或具体的文件中。Name和Namespace属性代表的是报头的名称和命名空间。

除了AddressHeader类型,在System.ServiceModel.Channels命名空间下还定义了另一个与之相关的对象AddressHeaderCollection,它表示AddressHeader的集合。AddressHeaderCollection继承自ReadOnlyCollection<AddressHeader>,所以该集合是只读的。

        public sealed class AddressHeaderCollection:ReadOnlyCollection<AddressHeader>
        {
            //其他成员
            public AddressHeaderCollection();
            public AddressHeaderCollection(IEnumerable<AddressHeader>
              addressHeaders);
            public void AddHeadersTo(Message message);
            public AddressHeader[] FindAll(string name, string ns);
            public AddressHeader FindHeader(string name, string ns);
        }

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

再回到EndpointAddress的定义,可以看到Header属性的类型就是AddressHeaderCollection,不过这仅仅是一个只读的属性,我们不能通过该属性为EndpointAddress添加AddressHeader。接下来介绍如何添加AddressHeader对象。

        public class EndpointAddress
        {
            //其他成员
            public Uri Uri { get; }
            public AddressHeaderCollection Headers { get; }
            public EndpointIdentity Identity { get; }
        }

如何为服务指定AddressHeader

在对服务寄宿的时候,可以通过代码和配置的方式为EndpointAddress添加相应的AddressHeader。下面的代码片断演示了在对服务进行寄宿的过程中如何设定AddressHeader。

        using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService)))
        {
        string headerValue = "Licensed User";
            string headerName = "UserType";
            string headerNamespace = "http://www.artech.com/";
            AddressHeader addressHeader = AddressHeader.CreateAddressHeader
              (headerName,headerNamespace,headerValue);
            EndpointAddress endpointAddress = new EndpointAddress(new Uri
              ("http://127.0.0.1:9999/CalculatorService"),addressHeader);
            ServiceEndpoint serviceEndpoint = new ServiceEndpoint
              (ContractDescription.GetContract(typeof(ICalculator)),
                new BasicHttpBinding(),endpointAddress);
            serviceHost.Description.Endpoints.Add(serviceEndpoint);
            serviceHost.Open();
            //其他代码
        }

上面的事例代码中,为了确定访问者的类型:购买了服务的用户(Licensed user)和免费试用的用户(Trivial user),添加了一个名称为UserType的AddressHeader,命名空间为http://www.artech.com/。我们希望该终结点只能让第一类用户进行访问,将AddressHeader的值设为Licensed User。先通过AddressHeader的静态方法CreateAddressHeader创建了一个AddressHeader对象,然后传入EndpointAddress的构造函数创建EndpointAddress。最后基于此EndpointAddress创建终结点对象,并添加到服务的终结点列表中。

在一般情况下,我们倾向于通过配置的方式来指定终结点的AddressHeader列表。AddressHeader定义在终结点<headers>配置项中,直接以XML的方式指定名称、命名空间和值。下面的一段配置和上面的代码是等效的。

        <?xml version="1.0" encoding="utf-8" ?>
        <configuration>
            <system.serviceModel>
                <services>
                  <service name="Artech.WcfServices.Services.CalculatorService">
                      <endpoint address="http://127.0.0.1:9999/CalculatorService"
                          binding="basicHttpBinding"
                            contract="Artech.WcfServices.Contracts.ICalculator">
                            <headers>
                                <UserType xmlns="http://www.artech.com/">Licensed
                                  User</UserType>
                            </headers>
                      </endpoint>
                  </service>
                </services>
            </system.serviceModel>
        </configuration>

如何在客户端指定AddressHeader

对于客户端来说,同样具有两种不同的方式对AddressHeader进行设定:代码的方式和配置的方式。和在服务端的配置相似,客户端终结点的AddressHeader列表也定义在终结点的<headers>配置项中。

        <?xml version="1.0" encoding="utf-8" ?>
        <configuration>
            <system.serviceModel>
                <client>
                  <endpoint name="CalculatorService" address=
                      "http://127.0.0.1:8888/CalculatorService" binding=
                      "basicHttpBinding"
                        contract="Artech.WcfServices.Contracts.ICalculator" >
                        <headers>
                            <UserType xmlns="urn:artech.com">Licensed User</UserType>
                        </headers>
                  </endpoint>
                </client>
            </system.serviceModel>
        </configuration>

如果采用配置的方式添加了上述AddressHeader,当我们采用该AddressHeader所在的终结点进行服务调用时,相应的MessageHeader将会自动附加到请求消息中,代码如下所示。

        <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
            <s:Header>
                <UserType xmlns="http://www.artech.com/">Licensed User</UserType>
            </s:Header>
            <s:Body>
                ... ...
            </s:Body>
        </s:Envelope>

如果不希望每次服务访问的消息都是自动附加上这么一段SOAP报头(可能该报头仅仅在某些确定的服务调用下才需要),在这种情况下,我们可以通过当前OperationContext的OutgoingMessageHeaders属性(OperationContext.Current.OutgoingMessageHeaders),直接将AddressHeader添加到出栈消息报头列表中,代码如下所示。

        using (ChannelFactory<ICalculator> channelFactory = new
          ChannelFactory<ICalculator>("CalculatorService"))
        {
            ICalculator calculator = channelFactory.CreateChannel();
            using (calculator as IDisposable)
            {
                using (OperationContextScope contextScope = new
                  OperationContextScope(calculator as IContextChannel))
                {
                    string headerValue = "Licensed User";
                    string headerName = "UserType";
                    string headerNamespace = "http://www.artech.com";
                    AddressHeader addressHeader = AddressHeader.CreateAddressHeader
                      (headerName, headerNamespace, headerValue);
                    OperationContext.Current.OutgoingMessageHeaders.Add(
                      addressHeader.ToMessageHeader());
                    Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1,2,
                      calculator.Add(1,2));
                }
            }
        }

由于最终的实现是将MessageHeader添加到OutgoingMessageHeaders中,所以也可以绕开AddressHeader,直接添加一个与之等效的MessageHeader。

        using (ChannelFactory<ICalculator> channelFactory = new
        ChannelFactory<ICalculator>("CalculatorService"))
        {
            ICalculator calculator = channelFactory.CreateChannel();
            using (calculator as IDisposable)
            {
                using (OperationContextScope contextScope = new
                  OperationContextScope(calculator as IContextChannel))
                {
                    string headerValue = "Licensed User";
                    string headerName = "UserType";
                    string headerNamespace = "http://www.artech.com";
                    MessageHeader<string> messageHeader = new
                      MessageHeader<string>(headerValue);
                    OperationContext.Current.OutgoingMessageHeaders.Add
                      (messageHeader.GetUntypedHeader(headerName,headerNamespace));
                    Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1,2,
                      calculator.Add(1,2));
                }
            }
        }

上面采用ChannelFactory<T>的方式编写了客户端的代码并进行了服务调用。当我们使用继承自ClientBase<T>的服务代理进行服务调用时,AddressHeader的指定方式具有小小的差异。

        using (CalculatorClient CalculatorClient = new
          CalculatorClient("CalculatorService"))
        {
            using (OperationContextScope contextScope = new OperationContextScope
              (CalculatorClient.InnerChannel as IContextChannel))
            {
                string headerValue = "Licensed User";
                string headerName = "UserType";
                string headerNamespace = "http://www.artech.com ";
                AddressHeader addressHeader = AddressHeader.CreateAddressHeader
                  (headerName, headerNamespace, headerValue);
                OperationContext.Current.OutgoingMessageHeaders.Add
                  (addressHeader.ToMessageHeader());
                Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2,
                  CalculatorClient.Add(1, 2));
            }
        }

由于在服务端我们为服务的终结点指定了AddressHeader,就意味着该终结点只接受消息的报头和与此AddressHeader相匹配的消息请求。我们可以通过实验证实这一点:通过下面的配置,我们将客户端AddressHeader的值从服务端希望的Licensed User变成Trivial User。由于服务端找不到相匹配的终结点导致如图2-3所示的EndpointNotFoundException异常被抛出。

图2-3 基于AddressHeader的消息筛选导致EndpointNotFoundException异常

        <?xml version="1.0" encoding="utf-8" ?>
        <configuration>
            <system.serviceModel>
                <client>
                  <endpoint name="CalculatorService" address=
                      "http://127.0.0.1:8888/CalculatorService" binding=
                      "basicHttpBinding"
                        contract="Artech.WcfServices.Contracts.ICalculator" >
                        <headers>
                            <UserType xmlns="urn:artech.com">Trivial User </UserType>
                        </headers>
                  </endpoint>
                </client>
            </system.serviceModel>
        </configuration>

本例实际上涉及的是消息筛选(Message Filter)问题,消息筛选解决的是如何通过接收的消息选择相应终结点的问题。其中地址筛选(Address Filter)是消息筛选的一种类型,WCF默认采用完全地址匹配的模式:地址的URI和AddressHeader均完全匹配。为了让服务端的终结点在AddressHeader和消息的报头不匹配的情况下也能处理消息请求,我们可以通过ServiceBeahviorAttribute改变地址筛选模式(AddressFilterMode)。比如在下面的代码中,通过ServiceBehaviorAttribute将AddressFilterMode设为AddressFilterMode.Any,从而避免了如图2-3所示的EndpointNotFoundException异常的抛出。关于消息筛选,在本章2.4节中还会详细介绍。

        namespace Artech.WcfServices.Services
        {
            [ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
            public class CalculatorService:ICalculator
            {
                #region ICalculator Members
                public double Add(double x, double y)
                {
                    return x + y;
                }
                #endregion
            }
        }