前面我寫過一個用WCF開發的聊天程序,大家可以翻看前面的博文。
在那個聊天程 序中,我是不引用服務而直接使用WCF。之前沒有跟大家說這一知識點,對於初學者朋友來說 ,可能不知道怎麼回事。
我們之所以說WCF比一般的Web Service要強大得多,是因為 它要比一般的Web服務要靈活得多,而且它不僅僅能在IIS服務器上運行,其實它可以用很多 種方法來運行,哪怕一個控制台應用程序。
現在,大家可以回憶一下前面我寫的《傳 說中的WCF》,我上面的例子絕大多數都是控制台應用程序類型的。我們應當把WCF理解為一 種通信技術,而不只是服務。前面的例子中我是告訴大家,完成服務器端後,就在客戶端項 目中添加服務引用,這樣就生成了客戶端代理類,我們就可以像平時使用一般類型一樣使用 了。
其實按照我們前面所講的方法,也足以完成許多實際任務了。大家是否還想拓展 一下呢? 有朋友肯定會問了:再拓展會不會變得很難? 放心吧,不會很難,相信我,老周從 來不會講大家都看不懂的東西的。
我們現在不妨嘗試一下,在客戶端不添加服務引用 ,而是由我們自己來編寫調用服務的代理類。要做到這一點,首先我們要明確的,其實我們 所編寫的服務協定,在服務器和客戶端都需要用到,如果大家查看過添加服務引用時由工具 生成的代碼,會發現其實它在客戶端也生成了服務協定的代碼。所以,在我們手動編寫調用 服務的代碼時,也需要這樣,因此有兩種方法可以在服務器和客戶端之間共用服務協定,一 是把代碼復制一下粘貼到客戶端中,另一種方法,我們可以新建一個類庫,然後把服務協定 寫到這個類庫中,最後在服務器端和客戶端都引用這個類庫即可。舉個例子,假如有以下定 義的協定:
[ServiceContract] public interface ITest { [OperationContract] int Add(int a, int b); [OperationContract] int GetRandmon(); [OperationContract] int Multiply(int a, int b); }
然後,我們在服務器端實現協定,注意:接口在服務器端實現即可,客戶端不需 要。
// 實現服務 public class MyService : CommonLib.ITest { Random m_rand = null; // 構造函數 public MyService() { m_rand = new Random(); } public int Add(int a, int b) { return a + b; } public int GetRandmon() { return m_rand.Next(); } public int Multiply(int a, int b) { return a * b; } }
接著,和以前一樣,創建服務主機,並偵聽客戶端調用。
static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(MyService)); // HTTP方式 WSHttpBinding httpBinding = new WSHttpBinding(SecurityMode.None); host.AddServiceEndpoint(typeof(CommonLib.ITest), httpBinding, "http://localhost:8900/"); // TCP方式 NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.None); host.AddServiceEndpoint(typeof(CommonLib.ITest), tcpBinding, "net.tcp://localhost:1700/"); // 打開服務 host.Open(); Console.WriteLine("服務已啟動。"); Console.Read(); host.Close(); }
大家可以細心看一下,和以前的代碼有什麼不同? 不妨比較一下,看看。
1、以前,我們在創建ServiceHost時會指定一個HTTP基地址,但這裡不需要了,基址 是便於工具生成代理類的,我們既然要手動來寫了,就不用生成代碼了,也不用基址了。
2、以前,我們會在ServiceHost.Description.Behaviors集合中加一個 ServiceMetadataBehavior對象,以提供WSDL,幫肋工具生成代碼。現在我們都自己手動寫了 ,當然就不用提供WSDL了。
認真想想,看是不是這樣? 如果你有興趣,也可以為 ServiceHost弄一個基址,但不添加ServiceMetadataBehavior,然後在客戶端項目中添加引 用,你會發現……呵呵,你懂的。
那現在在客戶端怎麼調用服務呢? 使用通道,可能 有朋友會看到IChannel接口,又派生出很多接口,但貌似沒有一個是類的,是不是要自己來 寫通道啊? 不用,當然你要擴展通道層是另一回事,通常我們無需擴展通道,因為現有的已 經足夠牛逼了。我們在“對象浏覽器”中是看不到與通道相關的可用的類,因為.NET內部是 有實現的,只是沒有定義為public而已,是internal。
我們根本可以不必理會如何找 通道的問題,就好像我們坐在一輛全自動導航或者有專業司機駕駛的車上,司機知道怎麼走 ,我們不必要擔心不知道怎麼走這段路。同理,我們可以不直接操作通道,為什麼呢?因為我 們定義的每一個服務協定都可以認為是一個通道。
上面我們定義的那麼ITest就是一 個通道,WCF內部已經幫我們把它變成一個通道了,不信的話,你往後看例子。
我們 已經知道,編寫的服務協定可以當成一個通道來操作,所以,在客戶端中,我們要手動寫代 碼來調用服務,要可以遵循以下步驟,有興趣的話你可以背下來,但告訴你,背了沒用。
1、創建與服務器匹配的Binding,這個就不用懷疑的了,你跟別人簽合同,那肯定是 一式兩份,對方持一份,你拿一份,你肯定不會拿一張白紙回家保存吧。
2、創建通 道,使用ChannelFactory<TChannel>類可以創建通道,因為它是“工廠”嘛,工廠當 然是用來生產的,但ChannelFactory工廠不是用來生產老鼠藥也不是生產地雷的,它是專門 生產Channel(通道)的。這個TChannel就可以寫上你定義的服務協定的接口,如上面的 ITest。
3、得到的通道就是ITest,然後就可以調用服務了,比如要兩個數相加,就 調用ITest.Add。
4、關閉通道,把ITest強制轉換為IClientChannel就可以調用Close 方法關閉通道。
可能你對這些步驟還有疑問,沒關系,你不妨先疑一下。我們繼續往 下操作。
前面定義服務主機的時候,我們使用了兩個終結點,一個是HTTP的,另一個 是TCP調用。所以我們這裡也要分別用這兩種方法調用。我可沒說一定要用這兩種方法調用, 我只是多寫了一個作演示。
在客戶端,先聲明這兩個終結點地址,就是我們在服務器 定義的兩個地址。
EndpointAddress edpHttp = new EndpointAddress ("http://localhost:8900/"); EndpointAddress edpTcp = new EndpointAddress ("net.tcp://localhost:1700/");
然後,分別用兩種Binding調有服務。
private void btnHTTP_Click(object sender, EventArgs e) { // 創建Binding WSHttpBinding httpBinding = new WSHttpBinding(SecurityMode.None); // 創建通道 ChannelFactory<CommonLib.ITest> factory = new ChannelFactory<CommonLib.ITest>(httpBinding); CommonLib.ITest channel = factory.CreateChannel(edpHttp); // 調用 int resAdd = channel.Add(int.Parse(txtNum11.Text), int.Parse(txtNum12.Text)); txtResAdd.Text = resAdd.ToString(); int resMult = channel.Multiply(int.Parse(txtNum21.Text), int.Parse(txtNum22.Text)); txtResMulti.Text = resMult.ToString(); int rand = channel.GetRandmon(); txtRand.Text = rand.ToString(); // 關閉通道 ((IClientChannel)channel).Close(); } private void btnTCP_Click(object sender, EventArgs e) { // 創建Binding NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.None); // 創建通道 ChannelFactory<CommonLib.ITest> factory = new ChannelFactory<CommonLib.ITest>(tcpBinding); CommonLib.ITest channel = factory.CreateChannel(edpTcp); // 調用 txtResAdd.Text = channel.Add(int.Parse(txtNum11.Text),int.Parse(txtNum12.Text)).ToString(); txtResMulti.Text = channel.Multiply(int.Parse(txtNum21.Text),int.Parse(txtNum22.Text)).ToString(); txtRand.Text = channel.GetRandmon().ToString(); // 關閉通道 ((IClientChannel)channel).Close(); }
你也許會問,ITest不是接口來的嗎,怎麼可以調用? 別忘了,我們在服務器端已 經實現過了,WCF內部會幫我們找到關聯的類。
現在,你就興奮地看看結果吧。記著 ,運行服務器端需要管理員身份運行,這個我說了三千五百遍了。
嘿嘿,乍一看,好像可以了,已經能調用了,但是,這樣是不是不太簡潔呢? 而且我們不 能將其當成一人類來用,每次調用要通過ChannelFactory來生產,比較麻煩,更重要的是, 如果有服務器回調協定,就不好弄了。
因此,對於上面的客戶端代碼我們是否考慮進 一個封裝呢? 這裡我們完全可以考慮使用ClientBase<TChannel>類,它對於通道和相 關操作作了進一步封裝,當然它是抽象類,不能直接拿來玩,要先派生出一個類。
/// <summary> /// 用於調用服務的類 /// </summary> public class MyClient : ClientBase<CommonLib.ITest>,CommonLib.ITest { public MyClient(System.ServiceModel.Channels.Binding binding, EndpointAddress edpAddr) : base(binding, edpAddr) { } public int Add(int a, int b) { return base.Channel.Add(a, b); } public int GetRandmon() { return base.Channel.GetRandmon(); } public int Multiply(int a, int b) { return base.Channel.Multiply(a, b); } }
有人會問,為什麼從ClientBase<CommonLib.ITest>派生,又要實現一次 CommonLib.ITest接口呢? 當然,你不實現也無所謂,再實現一次CommonLib.ITest接口是為 了讓這個類的公共方法和ITest的方法一樣,這樣方便調用。
通過訪問base.Channel 就可以得到一個對ITest的引用,無需要我們自己創建通道,因為基類中已經帶了默認實現。
現在,把前面的調用代碼改一下,是不是覺得簡潔了?
private void btnHTTP_Click(object sender, EventArgs e) { MyClient client = new MyClient(new WSHttpBinding(SecurityMode.None), edpHttp); txtResAdd.Text = client.Add(int.Parse(txtNum11.Text), int.Parse(txtNum12.Text)).ToString(); txtResMulti.Text = client.Multiply(int.Parse(txtNum21.Text), int.Parse(txtNum22.Text)).ToString(); txtRand.Text = client.GetRandmon().ToString(); } private void btnTCP_Click(object sender, EventArgs e) { MyClient client = new MyClient(new NetTcpBinding(SecurityMode.None), edpTcp); txtResAdd.Text = client.Add(int.Parse(txtNum11.Text), int.Parse(txtNum12.Text)).ToString(); txtResMulti.Text = client.Multiply(int.Parse(txtNum21.Text), int.Parse(txtNum22.Text)).ToString(); txtRand.Text = client.GetRandmon().ToString(); }
現在看看我們自己寫的這段代碼,是不是與VS生成的代碼比較接近了? 而且連配 置文件也省了。
查看本欄目