《WCF服务编程》关于“队列服务”一个值得商榷的地方

时间:2022-04-27
本文章向大家介绍《WCF服务编程》关于“队列服务”一个值得商榷的地方,主要内容包括一、“终结点不能共享相同的消息队列”、二、实践出真知、三、为什么同一个服务的终结点可以共享相同的消息队列、四、为什么不同服务的终结点不能共享相同的终结点、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

今天写《WCF技术剖析(卷2)》关于“队列服务”部分,看了《WCF服务编程》相关的内容。里面介绍一个关于“终结点不能共享相同的消息队列”说法,个人觉得这值得商榷。撰写此文,希望对此征求大家的意见。[源代码从这里下载]

目录 一、“终结点不能共享相同的消息队列” 二、实践出真知 三、为什么同一个服务的终结点可以共享相同的消息队列 四、为什么不同服务的终结点不能共享相同的终结点

一、“终结点不能共享相同的消息队列”

在《WCF服务编程(第三版)》的第9章《Queued Service》,Juval Löwy是这样说的:"WCF requires you to always dedicate a queue per endpoint for each service. This means a service with two contracts needs two queues for the two corresponding endpoints:

   1: <service name = "MyService">
   2:     <endpoint
   3:         address = "net.msmq://localhost/private/MyServiceQueue1"
   4:         binding = "netMsmqBinding"
   5:         contract = "IMyContract"
   6: />
   7:     <endpoint
   8:         address = "net.msmq://localhost/private/MyServiceQueue2"
   9:         binding = "netMsmqBinding"
  10:         contract = "IMyOtherContract"
  11:     />
  12: </service>

The reason is that the client actually interacts with a queue, not a service endpoint. In fact, there may not even be a service at all; there may only be a queue. Two distinct endpoints cannot share queues because they will get each other¡¯s messages. Since the WCF messages in the MSMQ messages will not match, WCF will silently discard those messages it deems invalid, and you will lose the calls. Much the same way, two polymorphic endpoints on two services cannot share a queue, because they will eat each other’s messages.”

简言之,就是消息队列隶属于某个具体的终结点,服务这个终结点从该消息队列中接收的消息与本终结点不一致,就会丢弃这个消息。具体例子,同一个服务具有两个终结点Endpoint1和Endpoint2,它们采用NetMsmqBinding,并且共享相同的地址(意味着采用共享同一个消息队列)。如果客户端试图发送给Endpoint1的消息被Endpoint2截获,就会被丢弃,那么这个服务调用就无缘无故地“丢失”了。

那么,事实果真服如此吗?

二、实践出真知

我看到这段描述,感到挺奇怪,因为就我所了解到的WCF的消息分发机制,对于相同服务小不同终结点的消息队列的共享是没有问题的。但是,Juval Löwy毕竟是Juval Löwy,当初也将我领入WCF领域的启蒙老师,对于他认定的东西不敢贸然的否认。为此我写了一个例子,毕竟不论我了解得底层机制如何,实践是检验真理的唯一标准。

为了模拟一个服务的多个总结点共享相同消息队列的场景,我建立了一个实现了多个服务契约接口的服务GreetingService,它实现了两个服务契约接口:IHello和IGoodbye。这三个类型的定义如下面的代码片断所示。

IHello和IGoodbye:

   1: using System.ServiceModel;
   2: namespace Artech.QueuedService.Service.Interface
   3: {
   4:     [ServiceContract(Namespace = "http://www.artech.com/")]
   5:     public interface IHello
   6:     {
   7:         [OperationContract(IsOneWay = true)]
   8:         void SayHello(string name);
   9:     }
  10:     [ServiceContract(Namespace = "http://www.artech.com/")]
  11:     public interface IGoodbye
  12:     {
  13:         [OperationContract(IsOneWay = true)]
  14:         void SayGoodBye(string name);
  15:     }
  16: }

GreetingService

   1: using System;
   2: using Artech.QueuedService.Service.Interface;
   3: namespace Artech.QueuedService.Service
   4: {
   5:     public class GreetingService: IHello, IGoodbye
   6:     {
   7:         public void SayHello(string name)
   8:         {
   9:             Console.WriteLine("Hello, {0}", name);
  10:         }
  11:         public void SayGoodBye(string name)
  12:         {
  13:             Console.WriteLine("Goodbye, {0}", name);
  14:         }
  15:     }
  16: }

我创建一个控制台应用对上面定义的GreetingService进行寄宿,下面是相关的配置和程序。从这可以看出寄宿服务具有两个基于NetMsmqBinding的终结点,它们的契约分别为IHello和IGoodBye,并且具有相同的地址。这意味着这两个终结点共享一个名称为mq4demo的本机私有队列。由于mq4demo为非事务性队列,我将ExactlyOnce设置为false,并且将安全模式设置为None以适应WorkGroup Installation模式。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:       <bindings>
   5:         <netMsmqBinding>
   6:           <binding exactlyOnce="false">
   7:             <security mode="None"/>
   8:           </binding>
   9:         </netMsmqBinding>
  10:       </bindings>
  11:         <services>
  12:             <service name="Artech.QueuedService.Service.GreetingService">
  13:                 <endpoint 
  14:                   address="net.msmq://localhost/private/mq4demo" 
  15:                   binding="netMsmqBinding" 
  16:                   contract="Artech.QueuedService.Service.Interface.IHello" />
  17:                 <endpoint 
  18:                   address="net.msmq://localhost/private/mq4demo" 
  19:                   binding="netMsmqBinding" 
  20:                   contract="Artech.QueuedService.Service.Interface.IGoodbye" />
  21:             </service>
  22:         </services>
  23:     </system.serviceModel>
  24: </configuration>

服务寄宿程序:

   1: string path = @".Private$mq4demo";
   2: if (!MessageQueue.Exists(path))
   3: {
   4:     MessageQueue.Create(path,true);
   5: }
   6: using (ServiceHost host = new ServiceHost(typeof(GreetingService)))
   7: {
   8:     host.Open();
   9:     Console.Read();
  10: } 

现在我们编写代码分别针对这两个终结点发起服务调用,看看它们是否能够成功。下面的配置和代码的定义:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <bindings>
   5:       <netMsmqBinding>
   6:         <binding exactlyOnce="false">
   7:           <security mode="None"/>
   8:         </binding>
   9:       </netMsmqBinding>
  10:     </bindings>
  11:     <client>
  12:       <endpoint name="helloService" 
  13:                 address="net.msmq://localhost/private/mq4demo" 
  14:                 binding="netMsmqBinding" 
  15:                 contract="Artech.QueuedService.Service.Interface.IHello" />
  16:       <endpoint name="goodbyeService" 
  17:                 address="net.msmq://localhost/private/mq4demo" 
  18:                 binding="netMsmqBinding" 
  19:                 contract="Artech.QueuedService.Service.Interface.IGoodbye" />
  20:     </client>
  21:   </system.serviceModel>
  22: </configuration>

服务调用程序:

   1: using(ChannelFactory<IHello> channelFactoryHello = new ChannelFactory<IHello>("helloService"))
   2: using (ChannelFactory<IGoodbye> channelFactoryGoodbye = new ChannelFactory<IGoodbye>("goodbyeService"))
   3: {
   4:     IHello helloProxy = channelFactoryHello.CreateChannel();
   5:     IGoodbye goodbyeProxy = channelFactoryGoodbye.CreateChannel();
   6:     helloProxy.SayHello("Foo");
   7:     goodbyeProxy.SayGoodBye("Bar");
   8: }

先后开启服务端和客户端(实际上那个先那个对于队列服务来说都可以),你会发现服务端控制台具有如下的输出,表明服务调用时没有问题的。

   1: Hello, Foo
   2: Goodbye, Bar

三、为什么同一个服务的终结点可以共享相同的消息队列

从上面的例子我们可以看到,同一个服务的终结点是可以共享相同的消息队列的。这也可以从WCF的消息分别机制来解释。就以我们上面的例子来说,服务GreetingService虽然具有两个不同的终结点,但是它们的监听地址是相同的,所以当服务开启的时候,只会创建一个唯一的ChannelDispatcher,它具有自己的ChannelListener。而该ChannelListener用于监听指定的消息队列中抵达的消息,一旦检测到消息队列中具有消息传来,或者开启时队列中已经有了消息,就会按照优先级去接收这些消息。然后按照“消息筛选机制”去选择用于处理该消息的EndpointDispatcher(对应于具体的终结点)。所以每个消息都能准确地抵达对应的终结点,并不会出现“一个终结点会吃掉另一个终结点消息”的说法。WCF服务端具体采用怎么的消息筛选机制进行终结点的选择,请参阅我的文章《WCF服务端运行时架构体系详解[上篇]》。

四、为什么不同服务的终结点不能共享相同的终结点

在上面的内容中,我说“多个终结点可以共享相同的消息队列”,都不忘提及一个前提:同一个服务的多个终结点。那么隶属于不同服务的终结点能否共享相同的消息的队列呢?答案是:“不能”。我想这才是Juval Löwy想表达的意思。

在上面我们说了,当服务开启之后就会试图是从绑定的消息队列中去“接收”消息。如果基于多个服务的终结点使用相同的消息队列,那么Service1开启的时候就有可能接收到发送给Service2的消息,在这种情况下,Service1采用消息筛选机制根本就不能选择出能够处理该消息的终结点,最终它会丢弃该消息。我我们之所以要强调“接收”二字,是因为它代表的事针对消息队列的操作Receive(而不是Peek),意味着被接收的消息会从消息队列中移除。为了证明这一点,我们对上面的例子作一下简单的更改。现将定义在服务端的终结点注视掉一个(保留契约IHello的终结点)。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:       ...
   5:         <services>
   6:             <service name="Artech.QueuedService.Service.GreetingService">
   7:                 <endpoint 
   8:                   address="net.msmq://localhost/private/mq4demo" 
   9:                   binding="netMsmqBinding" 
  10:                   contract="Artech.QueuedService.Service.Interface.IHello" />
  11:                 <!--<endpoint 
  12:                   address="net.msmq://localhost/private/mq4demo" 
  13:                   binding="netMsmqBinding" 
  14:                   contract="Artech.QueuedService.Service.Interface.IGoodbye" />-->
  15:             </service>
  16:         </services>
  17:     </system.serviceModel>
  18: </configuration>

然后在服务寄宿的时候,确认服务开启之前和关闭之后消息队列中具有的消息数量,相关的代码如下所示:

   1: static void Main(string[] args)
   2: {
   3:     string path = @".Private$mq4demo";
   4:     if (!MessageQueue.Exists(path))
   5:     {
   6:         MessageQueue.Create(path,true);
   7:     }
   8:  
   9:     var queue = new MessageQueue(path);
  10:     Console.WriteLine("Message Count: {0}", GetMessageNumber(queue));
  11:     using (ServiceHost host = new ServiceHost(typeof(GreetingService)))
  12:     {
  13:         host.Open();
  14:         Console.WriteLine("Press Enter to exit.");
  15:         Console.ReadLine();
  16:     }
  17:     Console.WriteLine("Message Count: {0}", GetMessageNumber(queue));
  18:     Console.Read();
  19: }
  20: static int GetMessageNumber(MessageQueue queue)
  21: {
  22:     int count = 0;
  23:     var enumerator = queue.GetMessageEnumerator2();
  24:     while (enumerator.MoveNext())
  25:     {
  26:         count++;
  27:     }
  28:     return count;
  29: }

现在我们先运行客户端,让客户端将服务调用封装成队列消息发送的消息队列中。然后开启服务端,在开启之前由于客户端进行两次服务调用,所以消息队列中具有两个消息。由于服务只有一个终结点,所以它只能处理针对IHello契约的调用的消息。我们现在需要确定的是:“客户端针对IGoodbye契约发送的请求消息还会在消息队列里面吗?”。从输出结果来看,消息队列中已经不存在消息。

   1: Message Count: 2
   2: Press Enter to exit.
   3: Hello, Foo
   4:  
   5: Message Count: 0

你可以将针对IGoodbye契约的请求看成是针对另一个服务的终结点发出的。由此可见,“只有同一个服务的多个终结点可以共享同一个消息队列,而基于不同服务的终结点则不行”。