WCF 4 Cookbook 系列(二) 终结点,绑定和行为 [下]

在这篇文章中,将会包括:

  • 配置默认终结点
  • 在MSMQ上设立双向通信
  • 通过双重绑定建立发布-订阅服务
  • 创建一个多终结点服务
  • 实现POX HTTP 服务
  • 定义没有时间戳头的CustomBinding
  • 在未知的SoapHeader禁用mustUnderstand验证
  • 在多个终结点之间共享一个物理地址

创建一个多终结点服务

对于传统的分布式通信服务,我们会通过一个明确的传输终结点暴露服务,比如HTTP终结点。如果需要通过另一个不同的传输层使用服务,我们可能不得不添加辅助的代码来实现新的终结点。WCF编程模型分离了服务实现和底层传输层,这样我们能够通过多个异构终结点(使用不同的传输层和绑定配置)方便的暴露单一服务实现。

怎么做…

  1. 首先,准备好我们用来为不同的终结点绑定暴露的服务契约。一些绑定可能在服务契约上有特殊的要求(比如基于MSMQ的绑定)。下面的示例契约已经为多个内置绑定准备好了:
    [ServiceContract]
    public interface ICounterService
    {
      [OperationContract]
      void Increment();
    
      [OperationContract]
      int GetCurrentCount();
    }
  2. 对于示例服务,为了演示多个终结点分享同样服务的行为,我们把它实现为单一服务。下面的代码是CounterService的实现:
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class CounterService : ICounterService
    {
        object _syncobj = new object();
        int _count = 0;
        public void Increment()
        {
            lock (_syncobj)
            {
                _count++;
            }
        }
        public int GetCurrentCount()
        {
            return _count;
        }
    }
  3. 最终,我们要在服务寄宿代码中加入不同的终结点和绑定,我们将使用三个终结点/绑定暴露服务——BasicHttpBinding, WSHttpBinding和NetTcpBinding。
    Uri baseHttp = new Uri(“http://localhost:8731/CounterService/”);
    Uri baseTcp = new Uri(“net.Tcp://localhost:9731/CounterService/”);
    using (ServiceHost host = new ServiceHost(typeof(CounterService), baseHttp, baseTcp))
    {
        // Add basicHttpBinding endpoint
        var basicHttp = new BasicHttpBinding(BasicHttpSecurityMode.None);
        host.AddServiceEndpoint(typeof(ICounterService), basicHttp,”basicHttp”);
    
        // Add wsHttpBinding endpoint
        var wsHttp = new WSHttpBinding(SecurityMode.None, false);
        host.AddServiceEndpoint(typeof(ICounterService), wsHttp,“wsHttp”);
    
        // Add netTcpBinding endpoint
        var netTcp = new NetTcpBinding(SecurityMode.None, false);
        host.AddServiceEndpoint(typeof(ICounterService), netTcp,“netTcp”);
        host.Open();
    
        Console.WriteLine(“service started .........”);
        Console.ReadLine();
    }

原理…

前面的示例,CounterService通过HTTP和TCP传输层打开了三个终结点。客户端应用程序可以使用任何已经暴露的终结点消费服务。示例客户端使用ChannelFactory来构造消费服务的客户端通道:

string basicHttpAddr = “http://localhost:8731/CounterService/basicHttp”;
string wsHttpAddr = “http://localhost:8731/CounterService/wsHttp”;
string netTcpAddr = “net.Tcp://localhost:9731/CounterService/netTcp”;

// For basicHttpBinding
var basicHttp = new BasicHttpBinding(BasicHttpSecurityMode.None);
_basicHttpFactory = new ChannelFactory<ICounterService>(basicHttp, basicHttpAddr);
_basicHttpClient = _basicHttpFactory.CreateChannel();

// For wsHttpBinding
var wsHttp = new WSHttpBinding(SecurityMode.None, false);
_wsHttpFactory = new ChannelFactory<ICounterService>(wsHttp, wsHttpAddr);
_wsHttpClient = _wsHttpFactory.CreateChannel();

// For netTcpBinding
var netTcp = new NetTcpBinding(SecurityMode.None, false);
_netTcpFactory = new ChannelFactory<ICounterService>(netTcp, netTcpAddr);
_netTcpClient = _netTcpFactory.CreateChannel();

在所有三个终结点上通过调用一个Increment操作,我们可以发现他们消费的是同一个服务实例,因为返回的计数值表明了全部的服务操作调用的次数。

Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

实现POX HTTP 服务

WCF服务默认通过使用SOAP格式化服务操作的消息,这意味着每一个在客户端和服务端传输的消息都要被封装在包含一个SOAP body和一些SoapHeader的SOAP信封(envelope)中。然而,有时WCF服务需要和一些遗留的POX(Plain Old XML)客户端工作或者我们的基于WCF的客户端需要和一个POX风格的服务通信。在这些场景中,有必要让我们的WCF客户端或者服务端生成任意的XML消息而不严格遵守SOAP标准。

怎么做…

我们将使用一个自定义绑定构建一个可用POX的WCF服务,然后使用可用POX的客户端程序消费。让我们看看完整的步骤:

  1. 第一件要做的事是准备我们的POX服务契约。下面的代码展示了为交换POX风格消息的示例操作。
    [OperationContract(Action=”*”,ReplyAction=”*”)]
    Message SayHello(Message reqMsg);

    对比一般的服务操作,一个明显的不同是我们使用了System.ServiceModel.Channels.Message类作为仅有的输入参数和返回参数。

  2. 对于服务终结点,我们需要在它上面应用一个自定义绑定。这个绑定使用HTTP传输层并设置messageVersion为空。下面的截图展示了自定义绑定的完整定义:

    Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

  3. 服务终结点配置完成后,我们可以启动服务和使用可用POX的自定义的客户端。对于服务端,我们可以直接从输入的Message参数中获得XML内容。当返回结果的时候,如果有必要的话,我们还需要构造一个Message实例并分配给合适的HTTP属性。下面的代码演示了一个简单的消息处理方案:

    public Message SayHello(Message reqMsg)
    {
        // Process request message
        Console.WriteLine(reqMsg.GetBody<XElement>());
        // Construct response message
        HttpResponseMessageProperty properties = new
        HttpResponseMessageProperty() { StatusCode = System.Net.HttpStatusCode.OK };
        Message repMsg = Message.CreateMessage(MessageVersion.None, string.Empty, new XElement("HelloResponse","Hello POX Client!"));
        repMsg.Properties[HttpResponseMessageProperty.Name] = properties;
        return repMsg;
    }
  4. 如果我们正在使用的一个WCF客户端需要向另一个服务端发送POX请求,统一的代码逻辑可以应用在这个客户端。下面的代码片段演示了使用简单XML元素作为整个消息体发送一个POX请求:
    ChannelFactory<TestClient.IPOXService> factory = new
    ChannelFactory<IPOXService>("POXEndpoint");
    TestClient.IPOXService client = factory.CreateChannel();
    HttpRequestMessageProperty properties = new
    HttpRequestMessageProperty() { Method = "POST" };
    Message reqMsg = Message.CreateMessage(MessageVersion.None,
    string.Empty, new XElement("HelloRequest","Hello POX Service!"));
    reqMsg.Properties[HttpRequestMessageProperty.Name] = properties;
    Message repMsg = client.SayHello(reqMsg);
    Console.WriteLine(repMsg.GetBody<XElement>());

原理…

正如我们在SayHello POX服务中看到的,使服务交换一个POX风格的消息有两个关键点:

  • 使用messageVersion为None的CustomBinding
  • Message类型的输入参数和返回值

通过设置(<textMessageEncoding>绑定元素的)messageVersion为None,WCF运行时在交换消息时不再强制要求任何SOAP格式化。

另外,System.ServiceModel.Channels.Message格式允许我们为服务请求和相应自由地构造任意的XML风格的消息。

通过捕获请求/相应消息,我们能够确定底层的操作消息使用普通的XML格式代替了基于SOAP信封的格式,正如下面的截图所示:

Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

更多…

这个秘籍使用一个自定义绑定(CustomBinding)构建了一个可用POX的服务;然而在WCF中,这不是构建POX风格服务唯一的方式。WCF REST编程模型是另一个构建处理普通XML消息服务的很好的方式。如果你熟悉Web应用开发,你会发现REST编程模型非常方便和熟悉。

我们会在第10章讨论更多关于WCF REST编程模型,RESTful和AJAX-enabled的WCF服务。

定义没有时间戳头的CustomBinding

对于使用消息层安全的WCF绑定,为确保消息的及时发送,一个时间戳头将被添加进SOAP信封,防止潜在的附加重新消息。然而,一些没有使用WCF的服务平台可能没有暴露这样的头,当和这样的客户端或服务端的服务工作时,我们需要防止WCF消息引擎生成带时间戳的头。

怎么做…

使用WSHttpBinding举例,我们创建一个自定义的绑定沿用内置WSHttpBinding的大多数设置(仅仅阻止时间戳头的生成)。下面的代码片段演示了如何创建CustomBinding并配置指定的绑定元素来禁用时间戳头生成。

private static Binding GetCustomHttpBinding()
{
    WSHttpBinding wshttp = new WSHttpBinding();
    var bec = wshttp.CreateBindingElements();
    SecurityBindingElement secbe = bec.Find<SecurityBindingElement>();
    // Not to include Timestamp header
    secbe.IncludeTimestamp = false;
    // Suppress the message relay detection
    secbe.LocalServiceSettings.DetectReplays = false;
    secbe.LocalClientSettings.DetectReplays = false;
    CustomBinding cb = new CustomBinding(bec);
    return cb;
}

首先位于代码中的SecurityBindingElement实例来自wsHttpBinding的默认元素集合。然后设置IncludeTimestamp属性为false。另外,在LocalServiceSettings和LocalClientSettings成员上,关闭DetectReplays属性是必要的。

最终,我们会应用这个CustomBinding到任何需要禁用时间戳的头的终结点。

原理…

因为时间戳头是一个安全特性,用来执行重复消息检查,WCF编程模型通过SecurityBindingElement类型暴露这个设置。然而,仅仅把SecurityBindingElement.IncludeTimestamp设置成false是不够的,因为这只能帮助移除时间戳头;运行时仍会在获取/输出消息时执行重复检查。因此,我还还需要关闭集合LocalServiceSettings和LocalClientSettings的DetectReplays属性。

通过对比底层的SOAP消息,我们会发现在禁用时间戳头生成之前和之后SoapHeader节的明显差别。下面是捕获一个SOAP消息在移除时间戳头之前的截图:

Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

下面是捕获一个SOAP消息在移除时间戳头之后的截图:

Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

在未知的SoapHeader禁用mustUnderstand验证

对于一个WCF服务或者客户端代理,在达到请求或者返回的相应消息里都要接受SoapHeader。SoapHeader有一个mustUnderstand属性指示目标终结点(或中间消息处理器)SoapHeader是否必须被处理。下面的截屏展示了一个典型的SOAP消息,包含一个mustUnderstand属性设置为1 (true)的SoapHeader。

Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

同样对于一个XML Web Service或者WCF服务,我们可以在运行时动态地插入SoapHeader或者在设计时静态地应用它们(通过WSDL或者服务元数据描述)。

默认情况下,一个WCF终结点在所有获得的消息的SoapHeader进行验证,并且如果有任何未知SoapHeader(没有预先定义)有一个mustUnderstand属性为1(或true),运行时就会抛出一个验证异常。然而,有时候为了能使WCF服务或者客户端代理可以优雅的处理动态添加的未知的SoapHeader,禁用验证也是有用的。

准备工作

如果对SoapHeader,mustUnderstand属性,或者怎么通过WCF编程模型设置这个属性不熟悉,你可以先看一看以下引用:

怎么做…

通过把MustUnderstandBehavior注入WCF终结点的行为集合,我们可以在mustUnderstand=“1”的未知的SoapHeader上禁用验证并跳过异常(raising of exceptions)。步骤如下:

  1. 首先,需要创建一个MustUnderstandBehavior类型的实例并设置它的ValidateMustUnderstand为false(这会在类型构造函数中使用)。
  2. 第二步,我们应该定位想要应用MustUnderstandBehavior的服务端/客户端终结点实例。对于WCF服务,我们可以使用ServiceHost.Description.Endpoints集合寻找想要的终结点。对于客户端代理,可以直接使用[ClientProxy].Endpoint获得终结点实例。
  3. 在获得终结点实例之后,我们可以简单地添加MustUnderstandBehavior实例到终结点实例的行为集合。下面的代码展示了在服务寄宿代码中应用MustUnderstandBehavior完整步骤:
    using (ServiceHost host = new ServiceHost(typeof(HeaderTestService)))
    {
        // Suppress the MustUnderstand header’s validation so that exception won't be raised for unknown MustUnderstand headers
        MustUnderstandBehavior mubehavior = new MustUnderstandBehavior(false);
    
        // Find the target endpoint we want
        ServiceEndpoint endpoint = host.Description.Endpoints.Find(typeof(IHeaderTestService));
    
        // Add the MustUnderstandBehavior to our service endpoint's Behavior collection
        endpoint.Behaviors.Add(mubehavior);
    }

原理…

当你遭遇ProtocolException(如下图所示)时,很可能是因为WCF服务端或客户端接受了一个被标记为mustUnderstand=true的未知的SoapHeader。

Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

在应用了(使用设置为false的ValidateMustUnderstand)MustUnderstandBehavior之后,运行时将无视对任何未知SoapHeader的验证。但是,我们仍能使用代码从OperationContext获取他们,如下:

StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0} headers in message\n", OperationContext.Current.IncomingMessageHeaders.Count);

foreach (var header in OperationContext.Current.IncomingMessageHeaders)
{
    sb.AppendFormat(“HeaderName:{0}, MustUnderstand={1}\n”,
    header.Name, header.MustUnderstand);
}

更多…

在这个秘籍中,我们演示了怎么为一个自寄宿的方案注入MustUnderstandBehavior。然而,在多数场景,WCF服务会通过.svc终结点寄宿在IIS服务中。在这些场景中,我们使用一个自定义的ServiceHostFactory类去添加行为注入代码逻辑。你可以看一下这篇文章:

Extending Hosting Using ServiceHostFactory  http://msdn.microsoft.com/en-us/library/a702697.aspx

在多个终结点之间共享一个物理地址

WCF支持通过多个异构终结点暴露一个单一的服务。另一个很好的特性是让多个终结点监听相同的物理传输地址。例如,你想要寄宿一个暴露了多个终结点的WCF服务,然而,你仅有一个单一的公开监听的HTTP URL,那么你可以使用这个特性使所有终结点(只要他们使用同样的传输协议)监听相同的URL。

怎么做…

我们的示例服务暴露两个终结点(一个是IFoo,另一个是IBar),它们两个监听相同的HTTP URL:

  1. 我们需要做的第一件事是配置服务终结点。我们仍将为两个终结点的address属性设定不同的值。然而,我们将使用一个特殊的命名为listenUri的属性来提供物理地址(两个终结点完全相同)。下面的截图展示了两个属性的使用:
    Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct
  2. 配置客户端终结点以便客户端代理或者ChannelFactory能够正确定位服务终结点,这一步也是非常重要的。WCF为指定客户端终结点的物理地址提供了一个ClientViaBehavior类型,可以在代码中使用ChannelFactory.Endpoint.Behaviors集合注入这个行为,如下所示:
    ChannelFactory<SharedAddressService.IFoo> fooFactory = 
        new ChannelFactory<SharedAddressService.IFoo>(new WSHttpBinding(SecurityMode.None));
    
    SharedAddressService.IFoo foo = fooFactory.CreateChannel(new EndpointAddress(“urn:Foo”), /* logical address */
        new Uri(“http://localhost:8731/FooBarService/Operations”));/*physical address*/
    
    foo.Foo();
    作为一种选择, 还可以在配置文件中使用<clientVia>元素来提供物理地址,如下图所示:
    Packtpub.Microsoft.WCF.4.0.Cookbook.for.Developing.SOA.Applications.Oct

原理…

通常,我们使用address属性来指定终结点监听的URL。然而,这个地址事实上是一个逻辑地址,如果我们没有明确的提供一个物理地址,WCF运行时也把他作为物理地址使用。listenUri,则相反,表示服务终结点监听的物理传输地址。另外,终结点为了共享相同的物理地址,他们将共享相同的服务分配器和信道栈。然后,当有多个终结点监听一个单一的物理地址时,WCF运行时是怎么区分那些操作请求的呢?答案是WCF运行时试着通过组合下面两个部分来解析请求目标:

  • Service/Operation契约
  • 通过address属性指定的逻辑地址

因此,如果我们为逻辑和物理地址提供一个相同的值,分配器/信道栈也能够正确地重定向请求到对应的终结点。

更多…

更多关于WCF地址(Addressing)的深入解释,你可以看一下Aaron Skonnard写的MSDN文章WCF Addressing In Depthhttp://msdn.microsoft.com/en-us/magazine/cc163412.aspx)。

posted @ 2012-02-23 23:39  brycezhang  阅读(1161)  评论(0编辑  收藏  举报