在一個基於面向服務的分散式環境中,藉助一個標準的、平臺無關的通信協議,使各個服務通過SOAP Message實現相互之間的交互。這個交互的過程實際上就是信息交換的過程。WCF支持不同形式的信息交換,我們把這稱之為信息交換模式(Message Exchange Pattern(簡稱MEP),下同), ... ...
五、TCP雙工模式
上一篇文章中我們學習了HTTP的雙工模式,我們今天就學習一下TCP的雙工模式。
在一個基於面向服務的分散式環境中,藉助一個標準的、平臺無關的通信協議,使各個服務通過SOAP Message實現相互之間的交互。這個交互的過程實際上就是信息交換的過程。WCF支持不同形式的信息交換,我們把這稱之為信息交換模式(Message Exchange Pattern(簡稱MEP),下同), 常見的MEP包括: 請求/答覆,單向模式和雙工模式。通過採用雙工的MEP,我們可以實現在服務端調用客戶端的操作。雖然WCF為我們實現底層的通信細節,使得我們把精力轉移到業務邏輯的實現,進行與通信協議無關的編程,但是對通信協議的理解有利於我們根據所處的具體環境選擇一個合適的通信協議。說到通信協議, WCF 經常使用的是以下4個:Http,TCP,Named Pipe,MSMQ。
我們用上一文章( WCF學習之旅—HTTP雙工模式(二十))中的示例,進行一下修改,變成一個TCP雙向通信的WCF服務應用程式。下麵直接上代碼。
1.Contract
using Contracts; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace Contracts { // 註意: 使用“重構”菜單上的“重命名”命令,可以同時更改代碼和配置文件中的介面名“IBookService”。 [ServiceContract(CallbackContract = typeof(ICallback))] public interface IBookService { /// <summary> /// 請求與答覆模式,預設模式 /// </summary> /// <param name="Id">書籍ID</param> /// <returns></returns> [OperationContract] string GetBook(string Id); /// <summary> /// 單工模式,顯示名稱 /// </summary> /// <param name="name">書籍名稱</param> [OperationContract(IsOneWay = true)] void ShowName(string name); /// <summary> /// 雙工模式,顯示名稱 /// </summary> /// <param name="name">書籍名稱</param> [OperationContract(IsOneWay = true)] void DisplayName(string name); } } using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace Contracts { public interface ICallback { [OperationContract(IsOneWay = true)] void DisplayResult(string result); } }
2.WcfServiceLib
using Contracts; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace WcfServiceLib { // 註意: 使用“重構”菜單上的“重命名”命令,可以同時更改代碼、svc 和配置文件中的類名“BookService”。 // 註意: 為了啟動 WCF 測試客戶端以測試此服務,請在解決方案資源管理器中選擇 BookService.svc 或 BookService.svc.cs,然後開始調試。 public class BookService : IBookService { /// <summary> /// 請求與答覆模式,預設模式 /// </summary> /// <param name="Id">書籍ID</param> /// <returns></returns> public string GetBook(string Id) { System.Threading.Thread.Sleep(20000); int bookId = Convert.ToInt32(Id); Books book = SetBook(bookId); string xml = XMLHelper.ToXML<Books>(book); return xml; } public Books SetBook(int Id) { Books book = new Books(); book.BookID = Id; book.AuthorID = 1; book.Category = "IBM"; book.Price = 39.99M; book.Numberofcopies = 25; book.Name = "DB2資料庫性能調整和優"; book.PublishDate = new DateTime(2015, 2, 23); return book; } /// <summary> /// 單工模式,顯示名稱 /// </summary> /// <param name="name">名稱</param> public void ShowName(string name) { string result = string.Format("書籍名稱:{0},日期時間{1}", name, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); Console.WriteLine("\r\n" + result); } /// <summary> /// 雙工模式,回調顯示結果 /// </summary> /// <param name="name">名稱</param> public void DisplayName(string name) { string result=string.Format("書籍名稱:{0},日期時間{1}", name, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); Console.WriteLine("\r\n" + result); ICallback call = OperationContext.Current.GetCallbackChannel<ICallback>(); call.DisplayResult("回調客戶端---"+result); } } }
在服務端,通過OperationContext.Current.GetCallbackChannel來獲得客戶端指定的CallbackContext 實例,進而調用客戶端的操作。
3.Hosting:
宿主配置文件:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /> </startup> <system.serviceModel> <diagnostics> <messageLogging logEntireMessage="true" logKnownPii="false" logMalformedMessages="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" /> <endToEndTracing propagateActivity="true" activityTracing="true" messageFlowTracing="true" /> </diagnostics> <services> <service name="WcfServiceLib.BookService"> <endpoint address="net.tcp://127.0.0.1:9999/BookService" binding="netTcpBinding" contract="Contracts.IBookService" /> </service> </services> </system.serviceModel> </configuration>
我們通過netTcpBinding來模擬基於TCP的雙向通信。代碼如下:
using Contracts; using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Description; using System.Text; using System.Threading.Tasks; using WcfServiceLib; namespace ConsoleHosting { class Program { static void Main(string[] args) { Console.WriteLine("輸入啟動方式,C--Code A -- App.config方式!"); string key = Console.ReadLine(); switch (key) { case "C": StartByCode(); break; case "A": StartByConfig(); break; default: Console.WriteLine("沒有選擇啟動方式,使用預設方式"); StartByCode(); break; } } private static void StartByCode() { //創建宿主的基地址 Uri baseAddress = new Uri("http://localhost:8080/BookService"); //創建宿主 using (ServiceHost host = new ServiceHost(typeof(BookService), baseAddress)) { //向宿主中添加終結點 host.AddServiceEndpoint(typeof(IBookService), new WSDualHttpBinding(), baseAddress); if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) { //將HttpGetEnabled屬性設置為true ServiceMetadataBehavior behavior = new ServiceMetadataBehavior(); behavior.HttpGetEnabled = true; behavior.HttpGetUrl = baseAddress; //將行為添加到Behaviors中 host.Description.Behaviors.Add(behavior); //打開宿主 host.Opened += delegate { Console.WriteLine("BookService控制台程式寄宿已經啟動,HTTP監聽已啟動....,按任意鍵終止服務!"); }; host.Open(); //print endpoint information Console.ForegroundColor = ConsoleColor.Yellow; foreach (ServiceEndpoint se in host.Description.Endpoints) { Console.WriteLine("[終結點]: {0}\r\n\t[A-地址]: {1} \r\n\t [B-綁定]: {2} \r\n\t [C-協定]: {3}", se.Name, se.Address, se.Binding.Name, se.Contract.Name); } Console.Read(); } } } private static void StartByConfig() { using (ServiceHost host = new ServiceHost(typeof(BookService))) { host.Opened += delegate { Console.WriteLine("BookService控制台程式寄宿已經啟動,TCP監聽已啟動....,按任意鍵終止服務!"); }; host.Open(); //print endpoint information Console.ForegroundColor = ConsoleColor.Yellow; foreach (ServiceEndpoint se in host.Description.Endpoints) { Console.WriteLine("[終結點]: {0}\r\n\t[A-地址]: {1} \r\n\t [B-綁定]: {2} \r\n\t [C-協定]: {3}", se.Name, se.Address, se.Binding.Name, se.Contract.Name); } Console.Read(); } } } }
4.客戶端:
配置文件中的信息進行修改:
<system.serviceModel> <client> <endpoint address="net.tcp://localhost:9999/BookService" binding="netTcpBinding" bindingConfiguration="" contract="Contracts.IBookService" name="BookServiceEndpoint" /> </client> </system.serviceModel>
接下來實現對雙工服務的調用,下麵是相關的配置和托管程式。在服務調用程式中,通過 DuplexChannelFactory<TChannel>創建服務代理對 象,DuplexChannelFactory<TChannel>和ChannelFactory<TChannel>的功能 都是一個服務代理對象的創建工廠,不過DuplexChannelFactory<TChannel>專門用於基於雙工通信的服務代理的創 建。在創建DuplexChannelFactory<TChannel>之前,先創建回調對象,並通過InstanceContext對回 調對象進行包裝。代碼如下:
private void btnTcpDuplex_Click(object sender, EventArgs e) { DuplexChannelFactory<IBookService> channelFactory = new DuplexChannelFactory<IBookService>(instanceContext, "BookServiceEndpoint"); IBookService client = channelFactory.CreateChannel(); //在BookCallBack對象的mainThread(委托)對象上搭載兩個方法,線上程中調用mainThread對象時相當於調用了這兩個方法。 textBox1.Text += string.Format("開始調用wcf服務:{0}\r\n\r\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); client.DisplayName("TCP---科學可以這樣看叢書"); textBox1.Text += string.Format("\r\n\r\n調用結束:{0}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); }
在創建 DuplexChannelFactory< IBookService >中,指定了Callback Context Instance: 一個實現了Callback Contract的BookCallBack對象。該對象在Service中通過 OperationContext.Current.GetCallbackChannel<ICallback>()獲得。
通過運行程式之後的結果如下圖:
2. 基於Http的雙向通訊V.S.基於TCP的雙向通訊
由於Http和TCP在各自協議上的差異,他們實現雙向通信的髮式是不同的。
Http是一個應用層的協議,它的主要特征就是無連接和無狀態。它採用傳統的“請求/回覆”的方式進行通信,客戶端發送Http Request請求服務端的某個資源,服務端接收到該Http Request, 回發對應的Http Response。當客戶端接收到對應的Response,該連接就會關閉。也就是說客戶端和服務端的 連接僅僅維持在發送Request到接收到Response這一段時間內。同時,每次基於Http的 連接是相互獨立,互不相干的,當前連接無法獲得上一次連接的狀態。為了保存調用的的狀態信 息,ASP.NET通過把狀態信息保存在服務端的Session之中,具體的做法是:ASP.NET為每個Session創建一個 Unique ID,與之關聯一個HttpSessionState對象,並把狀態信息保存在記憶體中或者持久的存儲介質(比如SQL Server)中。而WCF則採用另外的方式實現對Session的支持:每個Session關聯到某個Service Instance上。
我們來講一下HTTP雙向通信的過程,當客戶端通過HTTP請求調用WCF服務之前,會有一個終結點在客戶端被創建,用於監聽服務端對它的Request。客戶端對 WCF服務的調用會建立一個客戶端到服務端的連接,當WCF服務在執行操作過程中需要回調對應的客戶端,實際上會建立另一個服務端到客戶端的Http 連接。雖然我們時候說WCF為支持雙向通信提供一個雙工通道,實際上這個雙工通道是由兩個HTTP連接組成的。
再來看一下TCP的雙向通信的過程,對於TCP傳輸層協議,它則是一個基於連接的協議,在正式進行數據傳輸的之前,必須要在客戶端和服務端之間建立一個連接,連接的建立通過經典的“3次握手”來實現。TCP天生就具有雙工的特性,也就是說當連接 被創建之後,從客戶端到服務端,和從服務端到客戶端的數據傳遞都可以利用同一個連接來實現。對於WCF中的雙向通信,客戶端調用服務端,服務端回調客戶端的操作使用的都是同一個連接、同一個通道。所以基於TCP的雙工通信模式才是真正意義上的雙工通信模式。