一個WCF服務可以實現多個服務協定(服務協定實為接口),不過,每個終結點只能與一個服務協定關聯,並指定調用的唯一地址。那么,binding是干嗎的?binding是負責描述通信的協議,以及消息是否加密等內容。
好,不扯F話,說說今天的主題——OperationContextScope,這是一個類,而且是實現了 IDisposable 接口,說明這個類在實例化后,可能會持有某些特定的狀態信息,在釋放實例時需要進行清理。
這個猜測很對,OperationContextScope類的作用其實就是這樣。說具體一點,就是在這個類實例化后,到它被釋放之間形成一個代碼范圍,在這個特定的范圍內,可以對正在調用的服務操作進行訪問,最典型的做法是在這個范圍內修改消息頭,通常是添加自定義的消息頭。
那為什么要用OperationContextScope來圈定一個范圍來修改消息頭呢?因為這樣做可以保證只有在這一次調用服務操作才會添加自定義的消息頭,在其他地方調用則不會添加自定義頭。
一般來說,自定義消息頭是用來附加一些額外的信息,這些數據不屬於服務操作正文部分,並且是可有可無的。有點像發電子郵件時的附件,附件是可有可無的,可以與郵件正文有關,也可以與正文無關。
好,理論說完了。WCF相關的文章,我之所以寫得少,就是因為它難寫,WCF本身就涉及到很多Web服務相關的標准和概念,理解起來也不容易,而理論部分總是讓人越看越不懂。經過老周K年時間的摸爬滾打,總結出來最有用的編程學習辦法就是——寫代碼。雖然聽起來是句F話,但是,真的找不到比這一招更好的辦法。就拿老周學習WCF的過程來說吧,盡管許多概念可以網上查,可是看了之后呢,懂嗎?還是不懂,哪些讀了與Web服務相關的專著,還是不懂;哪怕再看一遍MSDN,似乎還是不懂。那怎么辦,無他,硬着頭皮敲代碼。理論方面的東西弄不懂,難道連代碼也不會寫了不成?嘿,這果然是個好招兒,本來不懂的東東,把代碼一寫,果然就有感覺了。
道理一樣,要搞懂OperationContextScope類是個什么貨色,光用嘴說太抽象,但是,把代碼往VS里面一寫,我相信你會馬上懂了。不信?咱們試試。
按照正常人類思維方式,我們應當先做服務器端。
先弄個服務協定,當然,它是一個接口,這個基礎相信大家是有的。
[ServiceContract(ConfigurationName ="ct",Namespace ="http://dog.org", Name ="哈吧dog")] interface ITest { [OperationContract(Name = "wang")] string SaySomthing(string name); }
服務操作很簡單,傳入一個字符串,返回一個字符串。哦,對了,這里有個玩意兒可能大家比較陌生,ConfigurationName ="ct" 是個什么鬼?首先,我聲明一下,它不是鬼;再者,它呢,你可以隨便指定一個名字,最好是簡短的,方便你記住的,因為等會兒在寫配置文件時有用,這里我取了個名字叫ct。
然后,理所當然,就是實現服務協定。
[ServiceBehavior(ConfigurationName = "sv")] public class Service : ITest { public string SaySomthing(string name) { Console.WriteLine("\n========= 操作被調用 ==========="); OperationContext context = OperationContext.Current; var hds = context.IncomingMessageHeaders; StringBuilder strb = new StringBuilder(); foreach (var h in hds) { strb.AppendLine($"【{h.Namespace} - {h.Name}】: {hds.GetHeader<string>(h.Name, h.Namespace)}"); } Console.WriteLine(strb); return $"旺旺!{name}。"; } }
在實現 SaySomething 方法過程中,我做了些手腳,通過OperationContext的InComingMessageHeaders屬性得到了客戶端發送過來的消息的Header列表,然后在屏幕上輸出每個Header的信息,包括命名空間,名稱,以及內容。Header的內容可以通過GetHeader<T>方法來獲取,T是返回內容的類型,如果希望把header的內容以字符串形式返回,就指定string。
和剛才服務協定定義相似,可能大家又看到了,我在類上應用了ServiceBehavior特性,又搞了個ConfigurationName,它的名字叫 sv, 同樣,也是在配置文件上使用的。
最后,創建ServiceHost,並打開服務。
using (ServiceHost host=new ServiceHost(typeof(Service))) { try { host.Open(); Console.WriteLine("服務已啟動。"); } catch(Exception ex) { Console.WriteLine(ex.Message); } Console.Read(); }
一般在演示示例時,我不用IIS來承載,麻煩,直接弄個控制台應用程序來啟動服務,方便。
下面,開始配置一個服務的配置文件。
<system.serviceModel> <services> <service name="sv"> <endpoint address="http://127.0.0.1:1000/mt" contract="ct" binding="basicHttpBinding" /> </service> </services> </system.serviceModel>
看出來了沒,現在知道我剛才搞的兩個ConfigurationName的作用了吧。沒看出來?我給你講講。
先看 service 元素,通常,name應該指定服務類的全名,包括命名空間,比如我剛剛的類名為Service,這里應寫上 name = "MyNamespace.Service",不過,因為我剛才用ServiceBehaviorAttribute指定了ConfigurationName叫 sv,所以在配置文件中,我不用再寫一大串的類型名稱了,只寫上 sv 就完事了。
一樣的道理,endpoint是必須與一個服務協定關聯的,剛才在定義服務協定時,我也用ConfigurationName給了一個名字叫 ct,故這里的 contract = "ct" 就是指向ITest協定接口了,如果不指定ConfigurationName,那么在配置endpoint元素時,你只好把接口的路徑寫全了,即contract = "MyNamespace.ITest",但這里我可以輕松寫成ct就可以了。
Good,服務端完成,現在做客戶端。先給客戶端的配置文件寫一下。
<system.serviceModel> <client> <endpoint name="ep" address="http://127.0.0.1:1000/mt" contract="ct" binding="basicHttpBinding"/> </client> </system.serviceModel>
然后,在客戶端代碼中重定義服務協定,你可以讓服務器和客戶端共享服務協定,當然也可以重新定義,接口和成員方法的名字可以不同,只要參數類型,個數和順序對上就行。當然了,協定的命名空間和名稱要與服務端一致。
[ServiceContract(Namespace = "http://dog.org", Name = "哈吧dog", ConfigurationName = "ct")] interface IDemo : IClientChannel { [OperationContract(Name = "wang")] string SpeakTo(string name); }
這里我讓它繼承了IClientChannel接口,因為待會兒要用。在客戶端,只定義接口就行了,不用實現,運行時庫會自動完成。你也不用實現IClientChannel接口,因為WCF內部已經有實現類了。
接着,用ChannelFactory<IDemo>來創建通道,通道類型就是協定類型接口。
ChannelFactory<IDemo> fac = null; IDemo channel = null; ………… fac = new ChannelFactory<IDemo>("ep"); channel = fac.CreateChannel(); // 調用完后,記得X掉它們 channel?.Close(); fac?.Close();
調用CreateChannel方法就能得到實現了IDemo接口的實例,這個內部會自動完成,你可以不管。在調用Close方法時,變量名后面多了個?,這是C# 6的新玩法,意思就是如果變量引用的是null,那代碼就不執行了。
現在,我們用同一個通道實例,對服務進行兩次調用。
using (OperationContextScope scope = new OperationContextScope(channel)) { // 獲取當前操作上下文 OperationContext context = OperationContext.Current; // 添加新的消息頭 MessageHeader hd = MessageHeader.CreateHeader("add_msg", "http://www.dog.net", "這是一條德國進口犬"); context.OutgoingMessageHeaders.Add(hd); // 調用服務操作 lblMsg.Text += channel.SpeakTo("傑克") + "\n"; } // 再次調用 lblMsg.Text += channel.SpeakTo("肯肯");
第一次調用,是在OperationContextScope所划定的范圍內進行的,並且向消息添加了個自定義Header;而第二次調用是在Scope范圍之外的。
兩次調用后,看看服務器的輸出內容,你就能發現什么新事情了。
看出來了吧,第一次調用多了個Header,而第二次調用沒有。看看兩次發出的消息。
回憶一下,我們剛才兩次調用服務,用的是同一個通道實例——channel,它在第一次調用時有自定義hd,而第二次調用時,自定義hd不見了。
從這個例子的比較中,你就知道OperationContextScope的用途了。Scope所圈定的范圍內所做的修改只在該范圍內有效,當跳出這個Scope,你再調用服務,Operation的狀態會自動被還原。
在Scope中調用,給消息加了自定義的頭,但只在這個范圍內有效,等代碼走出Scope后,對Operation的調用就會自動變回原來的樣子,所以,自定義的頭部不復存在。正因為如此,在服務器上的輸出中,第一次調用會有自定義頭,而第二次沒有自定義頭。第一次調用時,客戶端在Scope中添加了自定義頭,而第二次調用是在Scope之外,消息狀態被還原,自定義頭就不見了。
老周只能講到這兒了,能不能懂,真的看你的理解了,前面都說了,WCF的東西真的很難講解。如果對OperationContextScope還不理解,可以把示例反復研究一下,示例中我調用了兩次服務作對比,如果你不理解,可以改代碼,讓客戶端調用三次、四次。