我的WCF之旅(3):在WCF中實現(xiàn)雙工通信
雙工(Duplex)模式的消息交換方式體現(xiàn)在消息交換過程中,參與的雙方均可以向?qū)Ψ桨l(fā)送消息。基于雙工MEP消息交換可以看成是多個基本模式下(比如請求-回復(fù)模式和單項模式)消息交換的組合。雙工MEP又具有一些變體,比如典型的訂閱-發(fā)布模式就可以看成是雙工模式的一種表現(xiàn)形式。雙工消息交換模式使服務(wù)端回調(diào)(Callback)客戶端操作成為可能。
一、兩種典型的雙工MEP
1.請求過程中的回調(diào)
這是一種比較典型的雙工消息交換模式的表現(xiàn)形式,客戶端在進(jìn)行服務(wù)調(diào)用的時候,附加上一個回調(diào)對象;服務(wù)在對處理該處理中,通過客戶端附加的回調(diào)對象(實際上是調(diào)用回調(diào)服務(wù)的代理對象)回調(diào)客戶端的操作(該操作在客戶端執(zhí)行)。整個消息交換的過程實際上由兩個基本的消息交換構(gòu)成,其一是客戶端正常的服務(wù)請求,其二則是服務(wù)端對客戶端的回調(diào)。兩者可以采用請求-回復(fù)模式,也可以采用單向(One-way)的MEP進(jìn)行消息交換。圖1描述了這樣的過程,服務(wù)調(diào)用和回調(diào)都采用請求-回復(fù)MEP。
![]()
圖1 請求過程中的回調(diào)
2.訂閱-發(fā)布
訂閱-發(fā)布模式是雙工模式的一個典型的變體。在這個模式下,消息交換的雙方變成了訂閱者和發(fā)布者,若干訂閱者就某個主題向發(fā)布者申請訂閱,發(fā)布者將所有的訂閱者保存在一個訂閱者列表中,在某個時刻將主題發(fā)送給該主題的所有訂閱者。實際上基于訂閱-發(fā)布模式的消息交換也可以看成是兩個基本模式下消息交換的組合,申請訂閱是一個單向模式的消息交換(如果訂閱者行為得到訂閱的回饋,該消息交換也可以采用請求-回復(fù)模式);而主題發(fā)布也是一個基于單向模式的消息交換過程。訂閱-發(fā)布消息交換模式如圖2所示。
圖2 訂閱-發(fā)布
二、實例演示:創(chuàng)建基于雙工通信的WCF應(yīng)用
接下來我們通過一個的案例演示基于雙工通信的WCF應(yīng)用。為簡單起見,我們沿用計算服務(wù)的例子。在這之前,我們都是調(diào)用CalculuateService直接得到計算結(jié)果,并將計算結(jié)果通過控制臺輸出。在本例中我們將采用另外一種截然不同的方式調(diào)用服務(wù)并進(jìn)行結(jié)果的輸出:我們通過單向(One-way)的模式調(diào)用CalculuateService(也就是客戶端不可能通過回復(fù)消息得到計算結(jié)果),服務(wù)端在完成運算結(jié)果后,通過回調(diào)(Callback)的方式在客戶端將計算結(jié)果打印出來。整個應(yīng)用的層次仍然采用我們一貫的4層結(jié)構(gòu):Contracts、Services、Hosting和Clients,如圖3所示。
圖3 雙工通信案例應(yīng)用結(jié)構(gòu)
步驟一:定義服務(wù)契約和回調(diào)契約
首先進(jìn)行服務(wù)契約的定義,我們照例通過接口(ICalculator)的方式定義服務(wù)契約,作用于指定加法運算的Add操作,我們通過OperationContractAttribute特性的IsOneway屬性將操作定義成單向的操作,這意味著客戶端僅僅是向服務(wù)端發(fā)送一個運算的請求,并不會通過回復(fù)消息得到任何運算結(jié)果。
1: using System.ServiceModel;
2: namespace Artech.DuplexServices.Contracts
3: {4: [ServiceContract(Namespace="http://www.artech.com/",
5: CallbackContract=typeof(ICallback))]
6: public interface ICalculator
7: {8: [OperationContract(IsOneWay=true)]
9: void Add(double x, double y);
10: } 11: }我們試圖實現(xiàn)的是通過在服務(wù)端回調(diào)客戶端操作的方式實現(xiàn)運算結(jié)果的輸出。客戶端調(diào)用CalculatorService正常的服務(wù)調(diào)用,那么在服務(wù)執(zhí)行過程中借助于客戶端在服務(wù)調(diào)用時提供的回調(diào)對象對客戶端的操作進(jìn)行回調(diào),從本質(zhì)上講是另外一種形式的服務(wù)調(diào)用。WCF采用基于服務(wù)契約的調(diào)用形式,客戶端正常的服務(wù)調(diào)用需要服務(wù)契約,同理服務(wù)端回調(diào)客戶端依然需要通過描述回調(diào)操作的服務(wù)契約,我們把這種服務(wù)契約稱為回調(diào)契約。回調(diào)契約的類型通過ServiceContractAttribute特性的CallbackContract屬性進(jìn)行指定。
上面代碼中服務(wù)契約ICalculator的回調(diào)契約ICallback定義如下。由于回調(diào)契約本質(zhì)也是一個服務(wù)契約,所以定義方式和一般意義上的服務(wù)契約基本一樣。有一點不同的是,由于定義ICalculator的時候已經(jīng)通過[ServiceContract(CallbackContract=typeof(ICallback))]指明ICallback是一個服務(wù)契約了,所以ICallback不再需要添加ServiceContractAttribute特性。ICallback定義了一個服務(wù)操作DisplayResult用于顯示運算結(jié)果(前兩個參數(shù)為執(zhí)行加法運算的操作數(shù)),由于服務(wù)端不需要回調(diào)的返回值,索性將回調(diào)操作也設(shè)為單向方法。
1: using System.ServiceModel;
2: namespace Artech.DuplexServices.Contracts
3: {4: public interface ICallback
5: {6: [OperationContract(IsOneWay=true)]
7: void DisplayResult(double x, double y, double result);
8: } 9: }步驟二:實現(xiàn)服務(wù)
在實現(xiàn)了上面定義的服務(wù)契約ICalculator的服務(wù)CalculatorService中,實現(xiàn)了Add操作,完成運算和結(jié)果顯示的工作。結(jié)果顯示是通過回調(diào)的方式實現(xiàn)的,所以需要借助于客戶端提供的回調(diào)對象(該對象在客戶端調(diào)用CalculatorService的時候指定,在介紹客戶端代碼的實現(xiàn)的時候會講到)。在WCF中,回調(diào)對象通過當(dāng)前OperationContext的GetCallback<T>方法獲得(T代表回調(diào)契約的類型)。
1: using Artech.DuplexServices.Contracts;
2: using System.ServiceModel;
3: namespace Artech.DuplexServices.Services
4: {5: public class CalculatorService : ICalculator
6: {7: #region ICalculator Members
8: 9: public void Add(double x, double y)
10: {11: double result = x + y;
12: ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>(); 13: callback.DisplayResult(x, y, result); 14: } 15: 16: #endregion
17: } 18: } 注: OperationContext在WCF中是一個非常重要、也是一個十分有用的對象,它代表服務(wù)操作執(zhí)行的上下文。我們可以通過靜態(tài)屬性Current(OperationContext.Current)得到當(dāng)前的OperationContext。借助OperationContext,我們可以在服務(wù)端或者客戶端獲取或設(shè)置一些上下文,比如在客戶端可以通過它為出棧消息(outgoing message)添加SOAP報頭,以及HTTP報頭(比如Cookie)等。在服務(wù)端,則可以通過OperationContex獲取在客戶端設(shè)置的SOAP報頭和HTTP報頭。關(guān)于OperationContext的詳細(xì)信息,可以參閱MSDN在線文檔。
步驟三:服務(wù)寄宿
我們通過一個控制臺應(yīng)用程序完成對CalculatorService的寄宿工作,并將所有的服務(wù)寄宿的參數(shù)定義在配置文件中。由于雙工通信依賴于一個雙工的信道棧,即依賴于一個能夠支持雙工通信的綁定,在此我們選用了NetTcpBinding。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <services>
6: <service name="Artech.DuplexServices.Services.CalculatorService">
7: <endpoint address="net.tcp://127.0.0.1:9999/CalculatorService"
8: binding="netTcpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />
9: </service>
10: </services>
11: </system.serviceModel>
12: </configuration>
注: 在WCF預(yù)定義綁定類型中,WSDualHttpBinding和NetTcpBinding均提供了對雙工通信的支持,但是兩者在對雙工通信的實現(xiàn)機制上卻有本質(zhì)的區(qū)別。WSDualHttpBinding是基于HTTP傳輸協(xié)議的;而HTTP協(xié)議本身是基于請求-回復(fù)的傳輸協(xié)議,基于HTTP的通道本質(zhì)上都是單向的。WSDualHttpBinding實際上創(chuàng)建了兩個通道,一個用于客戶端向服務(wù)端的通信,而另一個則用于服務(wù)端到客戶端的通信,從而間接地提供了雙工通信的實現(xiàn)。而NetTcpBinding完全基于支持雙工通信的TCP協(xié)議。
1: using System;
2: using System.ServiceModel;
3: using Artech.DuplexServices.Services;
4: namespace Artech.DuplexServices.Hosting
5: {6: class Program
7: {8: static void Main(string[] args)
9: {10: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
11: { 12: host.Open(); 13: Console.Read(); 14: } 15: } 16: } 17: }步驟四:實現(xiàn)回調(diào)契約
在客戶端程序為回調(diào)契約提供實現(xiàn),在下面的代碼中CalculateCallback實現(xiàn)了回調(diào)契約ICallback,在DisplayResult方法中對運算結(jié)果進(jìn)行輸出。
1: using System;
2: using Artech.DuplexServices.Contracts;
3: namespace Artech.DuplexServices.Clients
4: {5: class CalculateCallback:ICallback
6: { 7: 8: public void DisplayResult(double x, double y, double result)
9: {10: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", x, y, result);
11: } 12: } 13: }步驟五:服務(wù)調(diào)用
接下來實現(xiàn)對雙工服務(wù)的調(diào)用,下面是相關(guān)的配置和托管程序。在服務(wù)調(diào)用程序中,通過DuplexChannelFactory<TChannel>創(chuàng)建服務(wù)代理對象,DuplexChannelFactory<TChannel>和ChannelFactory<TChannel>的功能都是一個服務(wù)代理對象的創(chuàng)建工廠,不過DuplexChannelFactory<TChannel>專門用于基于雙工通信的服務(wù)代理的創(chuàng)建。在創(chuàng)建DuplexChannelFactory<TChannel>之前,先創(chuàng)建回調(diào)對象,并通過InstanceContext對回調(diào)對象進(jìn)行包裝。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint name="CalculatorService" address="net.tcp://127.0.0.1:9999/CalculatorService" binding="netTcpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />
6: </client>
7: </system.serviceModel>
8: </configuration>
1: using System;
2: using Artech.DuplexServices.Contracts;
3: using System.ServiceModel;
4: namespace Artech.DuplexServices.Clients
5: {6: class Program
7: {8: static void Main(string[] args)
9: {10: InstanceContext instanceContext = new InstanceContext(new CalculateCallback());
11: using(DuplexChannelFactory<ICalculator> channelFactory = new DuplexChannelFactory<ICalculator>(instanceContext,"CalculatorService"))
12: { 13: ICalculator proxy = channelFactory.CreateChannel();14: using (proxy as IDisposable)
15: { 16: proxy.Add(1, 2); 17: Console.Read(); 18: } 19: } 20: } 21: } 22: }在服務(wù)寄宿程序啟用的情況下,運行客戶端程序后,通過服務(wù)端執(zhí)行的運算結(jié)果會通過回調(diào)客戶端的操作顯示出來,下面是最終輸出的結(jié)果。
x + y = 3 when x = 1 and y = 2
三、特別注意
接下來我們將針對上面這個案例,討論一些關(guān)于雙工服務(wù)的細(xì)節(jié)性問題。
問題1:回調(diào)對雙工信道的依賴
在本案例中,由于使用的NetTcpBinding,所以我們底層采用的是TCP協(xié)議。由于TCP協(xié)議是一個基于連接的傳輸協(xié)議,只有當(dāng)通信雙方的連接被成功創(chuàng)建出來后,他們之間才能進(jìn)行正常的消息傳輸。
在上面給出的客戶端代碼中,在調(diào)用了Add方法后添加了這樣的語句“Console.Read();”,這是為了阻止調(diào)用proxy的Dispose方法,因為該方法將會試圖關(guān)閉底層的TCP連接。由于服務(wù)端的回調(diào)操作也會使用該TCP連接,如果在回調(diào)操作尚未執(zhí)行完畢就試圖關(guān)閉網(wǎng)絡(luò)連接,將會導(dǎo)致回調(diào)無法正常執(zhí)行。所以如果我們將該語句去掉,將會拋出如圖4所示的ProtocolException異常。
1: InstanceContext instanceContext = new InstanceContext(new CalculateCallback());
2: using(DuplexChannelFactory<ICalculator> channelFactory = new DuplexChannelFactory<ICalculator>(instanceContext,"CalculatorService"))
3: { 4: ICalculator proxy = channelFactory.CreateChannel();5: using (proxy as IDisposable)
6: { 7: proxy.Add(1, 2);8: //Console.Read();
9: } 10: }
問題2:回調(diào)導(dǎo)致的死鎖
第2個問題是關(guān)于并發(fā)的問題,我們先看表現(xiàn)出來的現(xiàn)象,再分析原因并找出解決方案。現(xiàn)在我們修改一下回調(diào)契約,將OperationContractAttribute的IsOneWay屬性去掉,將Add操作由單向操作改成傳統(tǒng)意義的請求-回復(fù)服務(wù)操作。運行系統(tǒng),將會拋出如圖5所示的InvalidOperationException異常。
1: using System.ServiceModel;
2: namespace Artech.DuplexServices.Contracts
3: {4: public interface ICallback
5: { 6: [OperationContract]7: void DisplayResult(double x, double y, double result);
8: } 9: }圖5 雙工通信的并發(fā)、死鎖
異常的消息已經(jīng)道出了出錯的原因和解決方案,不過可能是由于Visual Studio漢化的原因,顯示的出錯消息顯得有點不知所以。究其本質(zhì),這是一個死鎖導(dǎo)致的異常,由于默認(rèn)的情況是服務(wù)的執(zhí)行按Single并發(fā)模式進(jìn)行,也就是說在服務(wù)執(zhí)行全程,服務(wù)對象只能被一個線程訪問。WCF通過加鎖機制保證服務(wù)對象的獨占性使用,也就是說在服務(wù)執(zhí)行開始會對服務(wù)對象加鎖,該鎖在服務(wù)操作結(jié)束之后釋放。
回到我們的例子,在Add操作執(zhí)行過程中,服務(wù)端回調(diào)客戶端操作進(jìn)行運算結(jié)果的顯示工作。如果回調(diào)是采用單向操作,回調(diào)請求一經(jīng)發(fā)送便會返回,服務(wù)操作可以繼續(xù)得到執(zhí)行直到操作正常結(jié)束。但是服務(wù)采用請求-回復(fù)模式的回調(diào),服務(wù)端會一直等待回調(diào)操作的返回。而另一方面,當(dāng)回調(diào)操作在客戶端正常執(zhí)行后,回到服務(wù)端試圖訪問服務(wù)操作的時候,發(fā)現(xiàn)對象被服務(wù)操作執(zhí)行的線程鎖住,所以它會等待服務(wù)操作的執(zhí)行完成后將鎖釋放。這樣,服務(wù)操作需要等待回調(diào)操作進(jìn)行正常返回以便執(zhí)行后續(xù)操作,而回調(diào)操作只有等待服務(wù)操作執(zhí)行完畢將鎖釋放才能得以返回,從而形成了死鎖。
解決方法就是通過服務(wù)行為改變服務(wù)執(zhí)行的并發(fā)模式,在下面的代碼中我們在服務(wù)類型(CalculatorService)中通過ServiceBehaviorAttribute特性的ConcurrencyMode屬性將并發(fā)模式設(shè)為Reentrant或者M(jìn)ultiple均可以解決這個問題。關(guān)于WCF中的并發(fā)是一個重要而且復(fù)雜的話題,本書的下卷會對其進(jìn)行單獨的介紹。
1: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]2: public class CalculatorService : ICalculator
3: {4: //省略實現(xiàn)
5: } 1: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]2: public class CalculatorService : ICalculator
3: {4: //省略實現(xiàn)
5: }問題3:如果采用WsDualHttpBinding?
接下來我們來看關(guān)于雙工服務(wù)的第3個問題。我們這個案例采用NetTcpBinding作為終結(jié)點的綁定類型。現(xiàn)在我們采用基于HTTP的WSDualHttpBinding看看我們的應(yīng)用能否正常運行。我們需要做的僅僅是改變服務(wù)端和客戶端的配置。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <services>
6: <service name="Artech.DuplexServices.Services.CalculatorService">
7: <endpoint address="http://127.0.0.1:9999/CalculatorService"
8: binding="wsDualHttpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />
9: </service>
10: </services>
11: </system.serviceModel>
12: </configuration>
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint name="CalculatorService" address="
6: http://127.0.0.1:9999/CalculatorService" binding=" wsDualHttpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />
7: </client>
8: </system.serviceModel>
9: </configuration>
如果你的IIS的版本是V6或者V7,你的程序運行將一切正常。但是如果還在使用XP操作系統(tǒng),使用IIS 5.X,會拋出如圖6所示的AddressAlreadyInUseException異常。
圖6 II 5.x + WsDualHttpBinding導(dǎo)致的AddressAlreadyInUseException異常
該異常的出現(xiàn)和不同版本的IIS監(jiān)聽機制有關(guān)。之所以相同的應(yīng)用在使用基于TCP傳輸?shù)?a title="NetTcpBinding Class" target="_blank" rel="noopener nofollow">NetTcpBinding的時候不會出現(xiàn)問題,那是因為HTTP和TCP它們有一個根本的區(qū)別,TCP本身就是一個雙工模式的傳輸協(xié)議,而HTTP協(xié)議本質(zhì)只能提供單向通信方式。WSDualHttpBinding通過創(chuàng)建兩個單項信道的方式提供雙工通信的實現(xiàn)。
對于一個雙工通信的WCF服務(wù)來說,回調(diào)過程本質(zhì)上也是一種服務(wù)調(diào)用,是對寄宿于客戶端的回調(diào)服務(wù)的調(diào)用。為了保證回調(diào)的正常運行,在客戶端創(chuàng)建通道的時候(比如上面的代碼通過DuplexChannelFactory的CreateChannel方法的時候),會進(jìn)行回調(diào)服務(wù)的寄宿,并指定回調(diào)服務(wù)的監(jiān)聽地址。在默認(rèn)的情況下該監(jiān)聽地址采用這樣的格式:http://hostname:80/{臨時監(jiān)聽地址}/guid/。
由于回調(diào)的服務(wù)監(jiān)聽地址采用的默認(rèn)端口是80,在IIS 5.x以及之前的版本中,80端口是IIS獨占的監(jiān)聽端口。所以才會出現(xiàn)AddressAlreadyInUseException異常并提示地址被另外一個應(yīng)用使用,實際上80端口被IIS使用。由于IIS 6和IIS 7采用基于HTTP.SYS驅(qū)動的監(jiān)聽方式實現(xiàn)了端口的共享,故而不會出現(xiàn)上面的問題。關(guān)于不同版本的IIS實現(xiàn)機制,可以參考《WCF技術(shù)剖析(卷1)第7章的有關(guān)IIS服務(wù)寄宿的內(nèi)容。
由于問題的癥結(jié)在于回調(diào)服務(wù)的監(jiān)聽端口和IIS沖突,所以我們只要能夠解決這種沖突,就能從根本上解決這個問題。由于我們不可以為了解決這個問題把IIS卸掉,或者改變IIS默認(rèn)的端口,所以我們只能改變回調(diào)服務(wù)的地址。WsDualHttpBinding定義了一個ClientBaseAddress使你能很容易地改變回調(diào)服務(wù)的基地址。對于我們給出的案例,我們只要通過下面的配置將clientBaseAddress設(shè)為可用的地址(http://127.0.0.1:8888/ CalculatorService),我們的問題就會迎刃而解。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <wsDualHttpBinding>
6: <binding name="MyBinding" clientBaseAddress="http://127.0.0.1:8888/calculatecallback" />
7: </wsDualHttpBinding>
8: </bindings>
9: <client>
10: <endpoint address="http://127.0.0.1:9999/CalculatorService" binding="wsDualHttpBinding"
11: bindingConfiguration="MyBinding" contract="Artech.DuplexServices.Contracts.ICalculator"
12: name="CalculatorService" />
13: </client>
14: </system.serviceModel>
15: </configuration>
出處:http://artech.cnblogs.com/
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。


雙工(Duplex)模式的消息交換方式體現(xiàn)在消息交換過程中,參與的雙方均可以向?qū)Ψ桨l(fā)送消息。基于雙工MEP消息交換可以看成是多個基本模式下(比如請求-回復(fù)模式和單項模式)消息交換的組合。雙工MEP又具有一些變體,比如典型的訂閱-發(fā)布模式就可以看成是雙工模式的一種表現(xiàn)形式。雙工消息交換模式使服務(wù)端回調(diào)(Callback)客戶端操作成為可能。
浙公網(wǎng)安備 33010602011771號