程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WCF技術剖析之十七:消息(Message)詳解(下篇)

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

編輯:關於.NET

《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,命名空間為http://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 = "http://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 = "http://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, "http://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="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
2: <s:Header>
3: <a:Action s:mustUnderstand="1">http://www.artech.com/myaction</a:Action>
4: <ApplicationContext xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://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報頭中,接收到該消息的客戶端將其取出,覆蓋掉現有上下文的值。

圖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所示。

圖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”

注:在我的文章《[原創]WCF後續之旅(6): 通過WCF Extension實現Context信息的傳遞》中,我通過WCF擴展的方式實現上面所示的上下文傳遞。關於讓上下文在客戶端和服務之間進行“隱式”傳遞,從另一方面講就是讓服務調用具有了相應的“狀態”,而SOA崇尚的是無狀態(Stateless)的服務調用,所以從這個意義上講,這是有違SOA的“原則”的。不過在很多項目開發中,實現這樣的功能卻具有很實際的作用。讀者朋友可以根據具體需求,自己去權衡。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved