閱讀410 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《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 :

   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 = "https://www.artech.com/")]
   5:     public interface IHello
   6:     {
   7:         [OperationContract(IsOneWay = true)]
   8:         void SayHello(string name);
   9:     }
  10:     [ServiceContract(Namespace = "https://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契約的請求看成是針對另一個服務的終結點發出的。由此可見,“”。

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-26 15:03:36

  上一篇:go  來源於WCF的設計模式:可擴展對象模式[下篇]
  下一篇:go  《魔獸》遭黑客入侵 主城橫屍遍野