閱讀212 返回首頁    go 汽車大全


WCF技術剖析之十七:消息(Message)詳解(下篇)

《WCF技術剖析(卷1)》自出版近20天以來,得到了園子裏的朋友和廣大WCF愛好者的一致好評,並被卓越網計算機書店作為首頁推薦,在這裏對大家的支持表示感謝。同時我將一直堅持這個博文係列,與大家分享我對WCF一些感悟和學習經驗。在《消息(Message)詳解》係列的上篇中篇,先後對消息版本、詳細創建、狀態機和基於消息的基本操作(讀取、寫入、拷貝、關閉)進行了深入剖析,接下來我們來談談消息的另一個重要組成部分:消息報頭(Message Header)。

按照SOAP1.1或者SOAP1.2規範,一個SOAP消息由若幹SOAP報頭和一個SOAP主體構成,SOAP主體是SOAP消息的有效負載,一個SOAP消息必須包含一個唯一的消息主體。SOAP報頭是可選的,一個SOAP消息可以包含一個或者多個SOAP報頭,SOAP報頭一般用於承載一些控製信息。消息一經創建,其主體內容不能改變,而SOAP報頭則可以自由地添加、修改和刪除。正是因為SOAP的這種具有高度可擴展的設計,使得SOAP成為實現SOA的首選(有這麼一種說法SOAP= SOA Protocol)。

按照SOAP 1.2規範,一個SOAP報頭集合由一係列XML元素組成,每一個報頭元素的名稱為Header,命名空間為https://www.w3.org/2003/05/soap-envelope。每一個報頭元素可以包含任意的屬性(Attribute)和子元素。在WCF中,定義了一係列類型用於表示SOAP報頭。

一、MessageHeaders、MessageHeaderInfo、MessageHeader和MessageHeader<T>

在Message類中,消息報頭集合通過隻讀屬性Headers表示,類型為System.ServiceModel.Channels.MessageHeaders。MessageHeaders本質上就是一個System.ServiceModel.Channels.MessageHeaderInfo集合。

   1: public abstract class Message : IDisposable
   2: {   
   3:     //其他成員
   4:     public abstract MessageHeaders Headers { get; }
   5: }
   1: public sealed class MessageHeaders : IEnumerable<MessageHeaderInfo>, IEnumerable
   2: {
   3:     //省略成員
   4: }

MessageHeaderInfo是一個抽象類型,是所有消息報頭的基類,定義了一係列消息SOAP報頭的基本屬性。其中Name和Namespace分別表示報頭的名稱和命名空間,Actor、MustUnderstand、Reply與SOAP 1.1或者SOAP 1.2規定SOAP報頭同名屬性對應。需要對SOAP規範進行深入了解的讀者可以從W3C官方網站下載相關文檔。

   1: public abstract class MessageHeaderInfo
   2: {
   3:     protected MessageHeaderInfo();
   4:     
   5:     public abstract string Actor { get; }
   6:     public abstract bool     IsReferenceParameter { get; }
   7:     public abstract bool     MustUnderstand { get; }
   8:     public abstract string Name { get; }
   9:     public abstract string Namespace { get; }
  10:     public abstract bool     Relay { get; }
  11: }

當我們針對消息報頭編程的時候,使用到的是另一個繼承自MessageHeaderInfo的抽象類:System.ServiceModel.Channels.MessageHeader。除了實現MessageHeaderInfo定義的抽象隻讀屬性外,MessageHeader中定義了一係列工廠方法(CreateHeader)方便開發人員創建MessageHeader對象。這些CreateHeader方法接受一個可序列化的對象,並以此作為消息報頭的內容,WCF內部會負責從對象到XML InfoSet的序列化工作。此外,可以通過相應的WriteHeader方法對MessageHeader對象執行寫操作。MessageHeader定義如下:

   1: public abstract class MessageHeader : MessageHeaderInfo
   2: {
   3:     public static MessageHeader CreateHeader(string name, string ns, object value);
   4:     public static MessageHeader CreateHeader(string name, string ns, object value, bool mustUnderstand);
   5:     //其他CreateHeader方法
   6:     
   7:     public void WriteHeader(XmlDictionaryWriter writer, MessageVersion messageVersion);
   8:     public void WriteHeader(XmlWriter writer, MessageVersion messageVersion);
   9:     //其他WriteHeader方法
  10:  
  11:     public override string Actor { get; }
  12:     public override bool IsReferenceParameter { get; }
  13:     public override bool MustUnderstand { get; }
  14:     public override bool Relay { get; }
  15: }

除了MessageHeader,WCF還提供一個非常有價值的泛型類:System.ServiceModel. MessageHeader<T>,泛型參數T表示報頭內容對應的類型,MessageHeader<T>為我們提供了強類型的報頭創建方式。由於Message的Headers屬性是一個MessageHeaderInfo的集合,MessageHeader<T>並不能直接作為Message對象的消息報頭。GetUntypedHeader方法提供了從MessageHeader<T>對象到MessageHeader對象的轉換。MessageHeader<T>定義如下:

   1: public class MessageHeader<T>
   2: {
   3:     public MessageHeader();
   4:     public MessageHeader(T content);
   5:     public MessageHeader(T content, bool mustUnderstand, string actor, bool relay);
   6:     public MessageHeader GetUntypedHeader(string name, string ns);
   7:  
   8:     public string Actor { get; set; }
   9:     public T Content { get; set; }
  10:     public bool MustUnderstand { get; set; }
  11:     public bool Relay { get; set; }
  12: }

接下來,我們通過一個簡單的例子演示如何為一個Message對象添加報頭。假設在一個WCF應用中,我們需要在客戶端和服務端之間傳遞一些上下文(Context)的信息,比如當前用戶的相關信息。為此我定義一個ApplicationContext類,這是一個集合數據契約(關於集合數據契約,可以參考我的文章:泛型數據契約和集合數據契約)。ApplicationContext是一個字典,為了簡單起見,key和value均使用字符串。ApplicationContext不能被創建(構造函數被私有化),隻能通過靜態隻讀屬性Current得到。當前ApplicationContext存入CallContext從而實現了在線程範圍內共享的目的。在ApplicationContext中定義了兩個屬性UserName和Department,表示用戶名稱和所在部門。3個常量分別表示ApplicationContext存儲於CallContext的Key,以及置於MessageHeader後對應的名稱和命名空間。

   1: [CollectionDataContract(Namespace = "https://www.artech.com/", ItemName = "Context", KeyName = "Key", ValueName = "Value")]
   2: public class ApplicationContext : Dictionary<string, string>
   3: {
   4:     private const string callContextKey = "__applicationContext";
   5:     public const string HeaderLocalName = "ApplicationContext";
   6:     public const string HeaderNamespace = "https://www.artech.com/";
   7:  
   8:     private ApplicationContext()
   9:     { }
  10:  
  11:     public static ApplicationContext Current
  12:     {
  13:         get
  14:         {
  15:             if (CallContext.GetData(callContextKey) == null)
  16:             {
  17:                 CallContext.SetData(callContextKey, new ApplicationContext());
  18:             }
  19:             return (ApplicationContext)CallContext.GetData(callContextKey);
  20:         }
  21:     }
  22:  
  23:     public string UserName
  24:     {
  25:         get
  26:         {
  27:             if (!this.ContainsKey("__username"))
  28:             {
  29:                 return string.Empty;
  30:             }
  31:             return this["__username"];
  32:         }
  33:         set
  34:         {
  35:             this["__username"] = value;
  36:         }
  37:     }
  38:  
  39:     public string Department
  40:     {
  41:         get
  42:         {
  43:             if (!this.ContainsKey("__department"))
  44:             {
  45:                 return string.Empty;
  46:             }
  47:             return this["__department"];
  48:         }
  49:         set
  50:         {
  51:             this["__department"] = value;
  52:         }
  53:     }
  54: }

在下麵代碼中,首先對當前ApplicationContext進行相應的設置,然後創建MessageHeader<ApplicationContext>對象。通過調用GetUntypedHeader轉換成MessageHeader對象之後,將其添加到Message的Headers屬性集合中。後麵是生成的SOAP消息。

   1: Message message = Message.CreateMessage(MessageVersion.Default, "https://www.artech.com/myaction");
   2: ApplicationContext.Current.UserName = "Foo";
   3: ApplicationContext.Current.Department = "IT";
   4: MessageHeader<ApplicationContext> header = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
   5: message.Headers.Add(header.GetUntypedHeader(ApplicationContext.HeaderLocalName, ApplicationContext.HeaderNamespace));
   6: WriteMessage(message, @"e:\message.xml");
   1: <s:Envelope xmlns:a="https://www.w3.org/2005/08/addressing" xmlns:s="https://www.w3.org/2003/05/soap-envelope">
   2:     <s:Header>
   3:         <a:Action s:mustUnderstand="1">https://www.artech.com/myaction</a:Action>
   4:         <ApplicationContext xmlns:i="https://www.w3.org/2001/XMLSchema-instance" xmlns="https://www.artech.com/">
   5:             <Context>
   6:                 <Key>__username</Key>
   7:                 <Value>Foo</Value>
   8:             </Context>
   9:             <Context>
  10:                 <Key>__department</Key>
  11:                 <Value>IT</Value>
  12:             </Context>
  13:         </ApplicationContext>
  14:     </s:Header>
  15:     <s:Body />
  16: </s:Envelope> 

在演示添加消息報頭的例子中,創建了一個ApplicationContext,這個類型將繼續為本案例服務。上麵僅僅是演示如果為一個現成的Message對象添加相應的報頭,在本例中,我們將演示在一個具體的WCF應用中如何通過添加消息報頭的方式從客戶端向服務端傳遞一些上下文信息。

上麵我們定義的ApplicationContext借助於CallContext實現了同一線程內數據的上下文消息的共享。由於CallContext的實現方式是將數據存儲於當前線程的TLS(Thread Local Storage)中,所以它僅僅在客戶端或者服務端執行的線程中有效。現在我們希望相同的上下文信息能夠在客戶端和服務端之間傳遞,毫無疑問,我們隻有唯一的辦法:就是將信息存放在請求消息和回複消息中。圖1大體上演示了具體的實現機製。

客戶端的每次服務調用,會將當前ApplicationContext封裝成MessageHeader,存放到出棧消息(Outbound Message)的SOAP報頭中;服務端在接收到入棧消息(InBound message)後,將其取出,作為服務端的當前ApplicationContext。由此實現了客戶端向服務端的上下文傳遞。從服務端向客戶端上下文傳遞的實現與此類似:服務端將當前ApplicationContext植入出棧消息(Outbound Message)的SOAP報頭中,接收到該消息的客戶端將其取出,覆蓋掉現有上下文的值。

clip_image002

圖1 上下文信息傳遞在消息交換中的實現

我們知道了如何實現消息報頭的創建,現在需要解決的是如何將創建的消息報頭植入到出棧和入棧消息報頭集合中。我們可以借助System.ServiceModel.OperationContext實現這樣的功能。OperationContext代表當前操作執行的上下文,定義了一係列與當前操作執行有關的上下文屬性,其中就包含出棧和入棧消息報頭集合。對於一個請求-回複模式服務調用來講,IncomingMessageHeaders和OutgoingMessageHeaders對於客戶端分別代表回複和請求消息的SOAP報頭,對於服務端則與此相反。

注: OperationContext代表服務操作執行的上下文。通過OperationContext可以得到出棧和入棧消息的SOAP報頭列表、消息屬性或者HTTP報頭。對於Duplex服務,在服務端可以通過OperationContext得到回調對象。此外通過OperationContext還可以得到基於當前執行的安全方麵的屬性一起的其他相關信息。

   1: public sealed class OperationContext : IExtensibleObject<OperationContext>
   2: {   
   3:     //其他成員
   4:     public MessageHeaders IncomingMessageHeaders { get; }   
   5:     public MessageHeaders OutgoingMessageHeaders { get; }
   6: }

有了上麵這些鋪墊,對於我們即將演示的案例就很好理解了。我們照例創建一個簡單的計算器的例子,同樣按照我們經典的4層結構,如圖2所示。

clip_image004

圖2 上下文傳遞案例解決方案結構

先看看服務契約(ICalculator)和服務實現(CalculatorService)。在Add操作的具體實現中,先通過OperationContext.Current.IncomingMessageHeaders,根據預先定義在ApplicationContext中的報頭名稱和命名空間得到從客戶端傳入的ApplicationContext,並將其輸出。待運算結束後,修改服務端當前ApplicationContext的值,並將其封裝成MessageHeader,通過OperationContext.Current.OutgoingMessageHeaders植入到回複消息的SOAP報頭中。

   1: using System.ServiceModel;
   2: namespace Artech.ContextPropagation.Contracts
   3: {
   4:     [ServiceContract]
   5:    public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:        double Add(double x, double y);
   9:     }
  10: }
   1: using System;
   2: using Artech.ContextPropagation.Contracts;
   3: using System.ServiceModel;
   4: namespace Artech.ContextPropagation.Services
   5: {
   6:     public class CalculatorService : ICalculator
   7:     {
   8:         public double Add(double x, double y)
   9:         {
  10:             //從請求消息報頭中獲取ApplicationContext
  11:             ApplicationContext context = OperationContext.Current.IncomingMessageHeaders.GetHeader<ApplicationContext>(ApplicationContext.HeaderLocalName, ApplicationContext.HeaderNamespace);
  12:             ApplicationContext.Current.UserName = context.UserName;
  13:             ApplicationContext.Current.Department = context.Department;
  14:             Console.WriteLine("ApplicationContext.Current.UserName = \"{0}\"", ApplicationContext.Current.UserName);
  15:             Console.WriteLine("ApplicationContext.Current.Department = \"{0}\"", ApplicationContext.Current.Department);
  16:  
  17:             double result = x + y;
  18:              
  19:             // 將服務端當前ApplicationContext添加到回複消息報頭集合
  20:             ApplicationContext.Current.UserName = "Bar";
  21:             ApplicationContext.Current.Department = "HR/Admin";
  22:             MessageHeader<ApplicationContext> header = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
  23:             OperationContext.Current.OutgoingMessageHeaders.Add(header. GetUntypedHeader(ApplicationContext.HeaderLocalName, ApplicationContext.HeaderNamespace));
  24:  
  25:             return result;
  26:         }
  27:     }
  28: }

客戶端的代碼與服務端在消息報頭的設置和獲取正好相反。在服務調用代碼中,先初始化當前ApplicationContext,通過ChannelFactory<ICalculator>創建服務代理對象。根據創建的服務代理對象創建OperationContextScope對象。在該OperationContextScope對象的作用範圍內(using塊中),將當前的ApplicationContext封裝成MessageHeader並植入出棧消息的報頭列表中,待正確返回執行結果後,獲取服務端植入回複消息中返回的AppicationContext,並覆蓋掉現有的Context相應的值。

注: 同Transaction和TransactionScope一樣,OperationContextScope定義了當前OperationContext存活的範圍。對於客戶端來說,當前的OperationContext生命周期和OperationContextScope一樣,一旦成功創建OperationContextScope,就會創建當前的OperationContext,當OperationContextScope的Dispose方法被執行,當前的OperationContext對象也相應被回收。

   1: using System;
   2: using Artech.ContextPropagation.Contracts;
   3: using System.ServiceModel;
   4: using System.ServiceModel.Channels;
   5: namespace Artech.ContextPropagation
   6: {
   7:     class Program
   8:     {
   9:         static void Main(string[] args)
  10:         {
  11:             ApplicationContext.Current.UserName = "Foo";
  12:             ApplicationContext.Current.Department = "IT";
  13:             using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("CalculatorService"))
  14:             {
  15:                 ICalculator calculator = channelFactory.CreateChannel();
  16:                 using (calculator as IDisposable)
  17:                 {
  18:                     using (OperationContextScope contextScope = new OperationContextScope(calculator as IContextChannel))
  19:                     {
  20:                 //將客戶端當前ApplicationContext添加到請求消息報頭集合
  21:                         MessageHeader<ApplicationContext> header = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
  22:                         OperationContext.Current.OutgoingMessageHeaders.Add(header.GetUntypedHeader(ApplicationContext.HeaderLocalName, ApplicationContext.HeaderNamespace));
  23:                         Console.WriteLine("x + y = {2} when x = {0} and y = {1}",1,2,calculator.Add(1,2));
  24:                  //從回複消息報頭中獲取ApplicationContext
  25:                         ApplicationContext context = OperationContext.Current.IncomingMessageHeaders.GetHeader<ApplicationContext>(ApplicationContext.HeaderLocalName, ApplicationContext.HeaderNamespace);
  26:                         ApplicationContext.Current.UserName = context.UserName;
  27:                         ApplicationContext.Current.Department = context.Department;                        
  28:                     }
  29:                 }
  30:             }
  31:             Console.WriteLine("ApplicationContext.Current.UserName = \"{0}\"", ApplicationContext.Current.UserName);
  32:             Console.WriteLine("ApplicationContext.Current.Department = \"{0}\"", ApplicationContext.Current.Department);
  33:  
  34:             Console.Read();
  35:         }
  36:     }
  37: }

下麵的兩段文字分別代表服務端(Hosting)和客戶端的輸出結果,從中可以很清晰地看出,AppContext實現了在客戶端和服端之間的雙向傳遞。

   1: ApplicationContext.Current.UserName = “Foo”
   2: ApplicationContext.Current.Department = “IT”
   1: x + y = 3 when x = 1 and y = 2
   2: ApplicationContext.Current.UserName = “Bar”
   3: ApplicationContext.Current.Department = “HR/Admiin”

注:在我的文章《


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:35:13

  上一篇:go  學界 | ICLR 2018截稿:取消公開評審改為雙盲引人關注
  下一篇:go  WCF技術剖析之十八:消息契約(Message Contract)和基於消息契約的序列化