[老老實實學WCF] 第十篇 訊息通訊模式(下) 雙工
老老實實學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) 兩個協定中的協定操作應為單向模式。