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

[爱心链接:拯救一个25岁身患急性白血病的女孩[内有苏州电视台经济频道《天天山海经》为此录制的节目视频(苏州话)]]《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报头中,接收到该消息的客户端将其取出,覆盖掉现有上下文的值。

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”

注:在我的文章《[原创]WCF后续之旅(6): 通过WCF Extension实现Context信息的传递》中,我通过WCF扩展的方式实现上面所示的上下文传递。关于让上下文在客户端和服务之间进行“隐式”传递,从另一方面讲就是让服务调用具有了相应的“状态”,而SOA崇尚的是无状态(Stateless)的服务调用,所以从这个意义上讲,这是有违SOA的“原则”的。不过在很多项目开发中,实现这样的功能却具有很实际的作用。读者朋友可以根据具体需求,自己去权衡。

作者:Artech
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2009-08-01 10:44 Artech 阅读(...) 评论(...) 编辑 收藏