《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的“原则”的。不过在很多项目开发中,实现这样的功能却具有很实际的作用。读者朋友可以根据具体需求,自己去权衡。

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
原文链接

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

  1. WCF服务端运行时架构体系详解[下篇]

    作为WCF中一个核心概念,终结点在不同的语境中实际上指代不同的对象.站在服务描述的角度,我们所说的终结点实际上是指ServiceEndpoint对象.如果站在WCF服务端运行时框架来说,终结点实际上指 ...

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

    消息交换是WCF进行通信的唯一手段,通过方法调用(Method Call)形式体现的服务访问需要转化成具体的消息,并通过相应的编码(Encoding)才能通过传输通道发送到服务端:服务操作执行的结果也 ...

  3. WCF技术剖析(卷1)正式出版

    [书     名] WCF技术剖析(卷1) [作     者] 蒋金楠 [出     版] 电子工业出版社 [书     号] 9787121089985 [出版日期] 2009 年7月 [开     ...

  4. WCF技术剖析之十八:消息契约(Message Contract)和基于消息契约的序列化

    在本篇文章中,我们将讨论WCF四大契约(服务契约.数据契约.消息契约和错误契约)之一的消息契约(Message Contract).服务契约关注于对服务操作的描述,数据契约关注于对于数据结构和格式的描 ...

  5. WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模拟程序)...

    WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模拟程序) 原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模 ...

  6. WCF技术剖析之二十五: 元数据(Metadata)架构体系全景展现[WS标准篇]

    元数据实际上是服务终结点的描述,终结点由地址(Address).绑定(Binding)和契约(Contract)经典的ABC三要素组成.认真阅读过<WCF技术剖析(卷1)>的读者相对会对这 ...

  7. WCF技术剖析之三十:一个很有用的WCF调用编程技巧[下篇]

    在<上篇>中,我通过使用Delegate的方式解决了服务调用过程中的异常处理以及对服务代理的关闭.对于<WCF技术剖析(卷1)>的读者,应该会知道在第7章中我通过类似于AOP的 ...

  8. WCF技术剖析之二十一:WCF基本异常处理模式[中篇]

    通过WCF基本的异常处理模式[上篇], 我们知道了:在默认的情况下,服务端在执行某个服务操作时抛出的异常(在这里指非FaultException异常),其相关的错误信息仅仅限于服务端可见,并不会被WC ...

  9. WCF技术剖析之二十八:自己动手获取元数据[附源代码下载]

    WCF技术剖析之二十八:自己动手获取元数据[附源代码下载] 原文:WCF技术剖析之二十八:自己动手获取元数据[附源代码下载] 元数据的发布方式决定了元数据的获取行为,WCF服务元数据架构体系通过Ser ...

最新文章

  1. SSAN 关系抽取 论文笔记
  2. 微信小游戏创业,究竟是红海还是死海?
  3. python 对角化 特征值 特征向量
  4. 开发日记-20190516 关键词 MVVM-代码浏览结束
  5. 比较常用的几个正则表达式
  6. [Lintcode]41. Maximum Subarray/[Leetcode]53. Maximum Subarray
  7. 3分钟把区块链的技术与应用彻底讲清楚
  8. 【CV】10分钟理解Focal loss数学原理与Pytorch代码
  9. 在github上托管Maven存储库(包含源代码和javadoc)
  10. linux etc 服务启动脚本,linux 服务脚本启动问题
  11. Internal Error 2738 - Installing ArcGIS Server 9.3,10 for Java
  12. Vmware VirtualCenter Server服务无法自动启动
  13. 拓端tecdat|MATLAB用Lasso回归拟合高维数据和交叉验证
  14. 计算机视觉 CS231n Course Introduction
  15. 笛卡尔积:(SQL语句中)
  16. arduino做一个表白程序
  17. PSnbsp;08人物抠图
  18. Linux下date和touch用法
  19. 域名解析地址如何查看?为什么要做域名解析?
  20. Excel基础(01)认识excel

热门文章

  1. f函数java_Java流:对N-1个元素执行f(),对N个元素执行g(),即,最后一个元素使用不同的函数...
  2. mysql加锁6_MySQL优化(6):Mysql锁机制
  3. word如何一键全选_【众点学】学了这些Word技巧才知道,原来这么多年的班都白加了...
  4. vscode 多行 行尾_vscode 常用快捷键
  5. 为什么python的命名不能以数字开头_python变量不能以数字打头
  6. Oracle/PLSQL AFTER DELETE Trigger
  7. IDEA本地运行Spark项目[演示自定义分区器]并查看HDFS结果文件
  8. Spark基础学习笔记07:搭建Spark HA集群
  9. Spring Boot基础学习笔记13:路径扫描整合Servlet三大组件
  10. 安卓案例:LayoutCreator演示