老老實實學WCF
第十篇 消息通信模式(下) 雙工
在前一篇的學習中,我們瞭解了單向和請求/應答這兩種消息通信模式。我們知道可以通過配置操作協定的IsOneWay屬性來改變模式。在這一篇中我們來研究雙工這種消息通信模式。
在一定程度上說,雙工模式並不是與前面兩種模式相提並論的模式,雙工模式的配置方法同前兩者不同,而且雙工模式也是基於前面兩種模式之上的。
在雙工模式下,服務端和客戶端都可以獨立地調用對方,誰都不用等待誰的答覆,同樣也不期待對方答覆,因爲如果期待答覆,就變成請求/應答模式了。也就是說雙方的調用都是單向調用,即我調你了,你不用回覆我,你什麼時候想回復我的時候呢你再調我,我就知道了,我是不會等着你的回覆的。這樣調用雙方就會有很好的異步體驗,我想調的時候就調,然後我就去幹別的,什麼時候調用完成了,你可以通過回調來通知我,我再決定下一步的動作,誰都不等誰。
因爲在服務模型中,調用總是由客戶端首先發起的,所以一般說的調用,都是客戶端的行爲,在雙工模式下,服務器也可以調用客戶端,我們就把它叫做回調,實際上這個調用和客戶端的調用沒什麼本質區別,在回調的時候,服務端變成了客戶端,客戶端相當於在提供服務了,只不過總是客戶端調用在前,我們就把服務端對客戶端的調用叫做回調了。
1. 建立雙工通信的條件
要建立雙工通信,必須使用支持雙工通信的綁定,比如wsHttpBinding是不支持的,必須採用wsDualHttpBinding才行,他會建立兩條綁定來實現互相調用,因此我們首先要注意的是選擇正確的綁定。
要建立雙工通信,必須使用會話,即將SeviceContract的SessionMode配置爲SessionMode.Required。
2. 如何配置雙工通信
爲了更好地理解這個問題,我們先考慮單工的情形,在單工模式下(單向和請求應答),調用總是由客戶端發起的,服務端可以迴應也可以不迴應。我們是怎麼配置實現這個的呢?我們首先讓兩端共享服務協定接口,這樣客戶端才知道怎樣調用服務端,然後把服務實現類寫在服務端,這樣服務端才能在收到請求的時候實例化這個類並執行服務的操作邏輯。也就是說,客戶端要想調服務端,客戶端必須擁有服務協定接口,而服務端必須擁有服務協定接口以及實現接口的服務類,在運行時服務端還要有服務類的實例。這些是必備條件。
再說回雙工,雙工讓服務端可以調客戶端,那麼道理同單工,必備條件也要有才行,只是他們的地位互換了。也就是說服務端必須擁有協定接口,客戶端必須擁有協定接口以及實現接口的類,在運行時客戶端還要有類的實例。一般情況下我們管服務端的定義的協定接口叫做服務協定,協定接口實現類叫做服務類,這回換到客戶端,我們管它叫回調接口,回調類,其實作用是一樣的。
也就是說在定義上,我們需要在兩邊都定義兩個協定接口,一個服務協定接口,一個回調協定接口,把服務協定接口的實現類寫在服務端,把回調協定接口的實現類寫在客戶端。
通過配置服務協定的ServiceContract的CallBackContract屬性來指定回調的時候使用的回調協定,考慮下面的代碼:
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IHelloWCFCallback))] public interface IHelloWCF { [OperationContract(IsOneWay = true)] void HelloWCF(); } public interface IHelloWCFCallback { [OperationContract(IsOneWay = true)] void Callback(string msg); }
我們定義了一個IHelloWCFCallback的回調協定接口,這個接口的實現是寫在客戶端的,同時指定了IHelloWCF服務協定接口的回調協定爲該回調協定接口,這樣服務協定就知道怎樣進行回調了。
同樣在客戶端要寫出回調協定接口及其實現,如果我們使用了svcutil.exe或添加服務引用,系統會爲我們自動填寫回調協定接口,但是不會爲我們寫實現類,畢竟她不知道我們的客戶端在接受回調的時候執行怎樣的邏輯。我們得自己編寫,一個回調協定接口的實現類看上去是這樣的:
public class HelloWCFCallback : Services.IHelloWCFCallback { public void Callback(string msg) { Console.WriteLine(msg); } }
其實和實現服務協定沒什麼不同。
前面提過綁定需要支持雙工,因此我們需要選擇一個支持雙工的綁定,我們採用wsDualHttpBinding。
<endpoint address="" binding="wsDualHttpBinding" contract="LearnWCF.IHelloWCF"/>
以上雙工通信的配置就完成了,我們還需要添加一些代碼來對雙工的運行時進行實現。
3. 雙工的運行時實現
現在準備工作已經完成,那麼在運行時需要什麼呢?需要通道和實例。回想單工通信,我們通過代理類建立了一個基於服務協定的通道到服務端,然後我們在這個通道上調用服務協定方法,在調用方法的時候,服務端實例上下文會生成並未我們new一個服務類的實例幫我們執行操作,當然這一步是服務端的系統自動完成的,我們不需要做特別的配置,直接調用就行了。
現在反過來服務端要調客戶端了,服務端可沒有代理類,那麼通道何來呢?客戶端這邊也沒有實例上下文和服務類實例,這邊只是一個簡單的控制檯應用程序,我們怎麼能指望客戶端爲我們自動生成這些呢?
所以我們得自己來。
首先在客戶端我們要自己先實例化一個服務類的實例,然後用這個實例作爲參數去創建一個實例上下文實例,這樣運行時的服務實例就有了,然後把這個實例上下文對象作爲參數傳給代理類的構造函數來初始化代理類,在這裏代理類幫了我們大忙,他會檢測到服務端元數據中服務協定使用雙工,他就會爲我們準備好雙工通道,當然前提是他會跟我們要實例上下文對象。這樣我們通過代理類調用服務操作,雙工通道就會建立了。看下面的代碼(這是在客戶端的Program.cs中):
//建立回調服務對象 HelloWCFCallback callbackObject = new HelloWCFCallback(); //建立實例上下文對象 InstanceContext clientContext = new InstanceContext(callbackObject); //用建立好的實例上下文對象初始化代理類對象 Services.HelloWCFClient client = new Services.HelloWCFClient(clientContext);
接下來輪到服務端,服務端既然受到的是一個支持雙工的連接,他就可以在利用操作上下文對象來得到和打開回調的通道,回調通道使用回調協定聲明的(正如通道使用服務協定聲明一樣)。然後再回調通道上調用回調操作就可以了:
string msg = "Hello From Service! Time" + DateTime.Now.ToLongTimeString(); //獲得回調通道 IHelloWCFCallback callbackChannel = OperationContext.Current.GetCallbackChannel<IHelloWCFCallback>(); //調用回調操作 callbackChannel.Callback(msg);
這樣,雙工的運行時就實現了。如果你對上面提到的有些迷糊,趕緊翻回第四篇和第五篇溫習一下有關通信的基礎知識。
4. 雙工通信實例
我們通過一個完整的例子來理解一下雙工通信的過程。
我用IIS作爲服務端宿主,客戶端用一個控制檯應用程序。我們來實現一個比較簡單的雙工通信,客戶端先向服務端發起一個調用,然後去幹別的,服務端等五秒後回調客戶端的回調方法。
你可能對前幾篇講的知識印象模糊了,我們這次從頭做一次。當然,溫習一下前面幾篇的內容是最好的。
(1) 建立SVC文件。
首先我們先建立IIS宿主,建立一個HelloWCFService.svc的文件保存在IIS應用程序的根路徑下。我的IIS應用程序的路徑是
http://localhost/IISService
因此這個文件的地址就變成了:
http://localhost/IISService/HelloWCFService.svc
這個文件的內容只有一行:
<%@ServiceHost language=c# Debug="true" Service="LearnWCF.HelloWCFService"%>
這行指令表示這是個WCF服務,服務的實現類是LearnWCF.HelloWCFService,注意這裏命名空間要寫全,名字你可以隨意起。
(2) 建立服務代碼文件。
代碼文件是名爲HelloWCFService.cs的文件,其實名字可以隨意起,但是要保存在IIS應用程序根目錄下的App_Code目錄下(或者Bin目錄也可以)。
代碼文件內容如下:
using System; using System.ServiceModel; namespace LearnWCF { [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IHelloWCFCallback))] public interface IHelloWCF { [OperationContract(IsOneWay = true)] void HelloWCF(); } public interface IHelloWCFCallback { [OperationContract(IsOneWay = true)] void Callback(string msg); } public class HelloWCFService : IHelloWCF { private int _Counter; public void HelloWCF() { System.Threading.Thread.Sleep(5000); string msg = "Hello From Service! Time" + DateTime.Now.ToLongTimeString(); //獲得回調通道 IHelloWCFCallback callbackChannel = OperationContext.Current.GetCallbackChannel<IHelloWCFCallback>(); //調用回調操作 callbackChannel.Callback(msg); } } }
注意幾個要點。服務協定IHelloWCF的ServiceContract屬性的兩個設置一個是SessionMode爲Required,表示必須使用會話,另一個是指定了回調協定。同時我們能看到,把回調協定接口也定義了進來。服務操作HellWCF在受到調用後先休眠5秒鐘,然後從操作上下文獲得回調通道,然後調用通道上的回調操作,把字符串Hello From Service和調用時間傳遞給了客戶端。
(3) 編寫配置文件。
編寫Web.Config文件並保存在IIS應用程序的根目錄下(跟svc文件放在一起)內容如下:
<configuration> <system.serviceModel> <services> <service name="LearnWCF.HelloWCFService" behaviorConfiguration="metadataExchange"> <endpoint address="" binding="wsDualHttpBinding" contract="LearnWCF.IHelloWCF"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <behaviors> <serviceBehaviors> <behavior name="metadataExchange"> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration>
注意看綁定,配置成了支持雙工的wsDualHttpBinding。
服務端的部分就寫好了。
(4) 建立客戶端應用程序
建立一個控制檯應用程序,命名爲ConsoleClient。
(5) 添加服務引用
在引用上右擊,選擇添加服務引用,並在地址中輸入,並點擊前往
http://localhost/IISService/HelloWCFService.svc
在下面的命名空間中爲代理類指定一個新的命名空間Services。點確定
此時系統爲我們自動添加了App.Config和代理類文件,點擊解決方案瀏覽器上方的查看所有文件,逐層展開服務引用,最後打開reference.cs看看有什麼變化
以下的代碼是系統生成的代理類代碼,不是我們輸入的
//------------------------------------------------------------------------------ // <auto-generated> // 此代碼由工具生成。 // 運行時版本:4.0.30319.261 // // 對此文件的更改可能會導致不正確的行爲,並且如果 // 重新生成代碼,這些更改將會丟失。 // </auto-generated> //------------------------------------------------------------------------------ namespace ConsoleClient.Services { [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] [System.ServiceModel.ServiceContractAttribute(ConfigurationName="Services.IHelloWCF", CallbackContract=typeof(ConsoleClient.Services.IHelloWCFCallback), SessionMode=System.ServiceModel.SessionMode.Required)] public interface IHelloWCF { [System.ServiceModel.OperationContractAttribute(IsOneWay=true, Action="http://tempuri.org/IHelloWCF/HelloWCF")] void HelloWCF(); } [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] public interface IHelloWCFCallback { [System.ServiceModel.OperationContractAttribute(IsOneWay=true, Action="http://tempuri.org/IHelloWCF/Callback")] void Callback(string msg); } [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] public interface IHelloWCFChannel : ConsoleClient.Services.IHelloWCF, System.ServiceModel.IClientChannel { } [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] public partial class HelloWCFClient : System.ServiceModel.DuplexClientBase<ConsoleClient.Services.IHelloWCF>, ConsoleClient.Services.IHelloWCF { public HelloWCFClient(System.ServiceModel.InstanceContext callbackInstance) : base(callbackInstance) { } public HelloWCFClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) : base(callbackInstance, endpointConfigurationName) { } public HelloWCFClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress) { } public HelloWCFClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress) { } public HelloWCFClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, binding, remoteAddress) { } public void HelloWCF() { base.Channel.HelloWCF(); } } }
我們可以看到,代理類爲我們自動生成了回調協定IHelloWCFCallback,而且代理類HelloWCFClient的這些構造函數也不一樣了,需要我們提供InstanceContext實例了,而且繼承的類也不再是ClientBase<>,而是DuplexClientBase<>了,這就是代理髮現我們用雙工通信了,所以改變了代理類所繼承的類爲支持雙工通信的基類。
好,關掉它,我們繼續完善客戶端
(6) 編寫客戶端代碼
我們還沒有在客戶端實現回調協定,首先要實現他,還要爲運行時添加調用代碼,Program.cs的代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; namespace ConsoleClient { class Program { static void Main(string[] args) { //建立回調服務對象 HelloWCFCallback callbackObject = new HelloWCFCallback(); //建立實例上下文對象 InstanceContext clientContext = new InstanceContext(callbackObject); //用建立好的實例上下文對象初始化代理類對象 Services.HelloWCFClient client = new Services.HelloWCFClient(clientContext); Console.WriteLine("Client Call Begin:"+DateTime.Now.ToLongTimeString()); client.HelloWCF(); Console.WriteLine("Client Call End:"+DateTime.Now.ToLongTimeString()); Console.WriteLine("Client can process other things"); Console.ReadLine(); } } public class HelloWCFCallback : Services.IHelloWCFCallback { public void Callback(string msg) { Console.WriteLine(msg); } } }
首先在下面我們實現了回調協定接口,邏輯就是把服務端傳過來的參數輸出。
然後我們建立服務類的對象以及實例上下文對象,接着構造代理類對象。
接下來就是調用服務操作了。我們在這裏記錄了時間便於觀察順序。注意看,這裏客戶端沒有等待服務端的任何返回,也沒有任何的輸出的動作。
完成 F5 運行以下,結果是這樣的:
我們參照源代碼可以看出,客戶端就調用了服務端一次,耗時2秒,然後客戶端就可以去做別的了。在客戶端發起調用5秒以後,服務端向客戶端執行了一次調用,也就是客戶端調用的服務操作休眠了5秒以後執行了回調。最後一條輸出,是服務端主動調用的客戶端的回調服務方法輸出的。
5. 總結
雙工通信有點小複雜,需要反覆琢磨,在思考的時候有的小竅門,就是把單工通信需要的條件和過程仔細想明白,然後依樣反方向複製一份,缺什麼補什麼就是雙工通信了。
一些關鍵點,可以反覆思考:
(1) 通信兩端都有兩個協定接口,接口的實現在兩邊一邊一個。
(2) 支持雙工的綁定。
(3) 指定服務協定的回調協定以建立回調聯繫。
(4) 客戶端自己構造運行時服務類對象和實例上下文對象。
(5) 服務端通過操作上下文獲得回調通道 。
(6) 需要會話的支持。
(7) 兩個協定中的協定操作應爲單向模式。