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

简介:

消息交换是WCF进行通信的唯一手段,通过方法调用(Method Call)形式体现的服务访问需要转化成具体的消息,并通过相应的编码(Encoding)才能通过传输通道发送到服务端;服务操作执行的结果也只能以消息的形式才能被正常地返回到客户端。所以,消息在整个WCF体系结构中处于一个核心的地位,WCF可以看成是一个消息处理的管道。

尽管消息在整个WCF体系中具有如此重要的意义,可是一般的WCF编程人员,却意识不到消息的存在。原因很简单,WCF设计的目标就是实现消息通信的所有细节,为最终的编程人员提供一个完全面向对象的编程模型。所以对于一般的编程人员来说,他们面对的是接口,却不知道服务契约对于服务的描述;面对的是数据类型,却不知道数据契约对序列化的作用;面对的是方法调用和返回值的获取,却不了解底层消息交换的过程。

鼓励大家深入了解WCF关于消息处理的流程具有两个目的:第一,只有在对整个消息处理流程具有清晰认识的基础上才能写出高质量的WCF程序。第二,WCF是一个极具可扩展性的通信框架,可以灵活地创建一些自定义WCF扩展(WCF Extension)以实现你所需要的功能。如同WCF的插件一样,这些自定义的WCF扩展以即插即用的方式参与到WCF整个消息处理流程之中。了解WCF整个消息处理流程是灵活进行WCF扩展的前提。

在WCF中,定义了一个System.ServiceModel.Channels.Message类,用以表示这些具有不同表现形态的消息。在本篇文章中,我们会着重来讨论这个Message类型。首先来介绍消息的版本。

一、消息版本(Message Version)

由于消息基于不同的格式或者结构,不同的格式决定了对消息不同的处理方式,所以对一个消息进行正确处理的前提是确定消息的格式或结构。在WCF中消息的格式与结构由消息的版本决定,在Message中定义了一个类型为MessageVersion的Version属性来表示消息的版本。

 1: public abstract class Message : IDisposable
 2: { 
 3: //其他成员
 4: public abstract MessageVersion Version { get; }
 5: }

MessageVersion类型定义在System.ServiceModel.Channels命名空间下。由于SOAP规范的版本和WS-Addressing规范的版本是决定消息格式与结构的两个主要因素,所以,MessageVersion由SOAP规范和WS-Addressing规范共同决定。WCF通过System.ServiceModel.EnvelopeVersion和System.ServiceModel.AddressingVersion两个类分别定义SOAP规范的版本和WS-Addressing的版本。

MessageVersion中定义两个静态的方法CreateVersion用以创建相应的MessageVersion对象,两个属性Envelope和Addressing分别表示通过EnvelopeVersion和AddressingVersion体现的SOAP规范版本和WS-Addressing规范版本。

 1: public sealed class MessageVersion
 2: { 
 3: //其他成员
 4: public static MessageVersion CreateVersion(EnvelopeVersion envelopeVersion);
 5: public static MessageVersion CreateVersion(EnvelopeVersion envelopeVersion, AddressingVersion addressingVersion); 
 6:  
 7: public AddressingVersion Addressing { get; }
 8: public EnvelopeVersion Envelope { get; }
 9: }

到目前为止SOAP和WS-Addressing各有两个版本:SOAP 1.1 和SOAP1.2, WS-Addressing 2004和WS-Addressing 1.0。它们分别通过定义在EnvelopeVersion和AddressingVersion中相应的静态只读属性表示。Soap11和Soap12代表SOAP 1.1和SOAP1.2,而WSAddressingAugust2004和WSAddressing10则表示WS-Addressing 2004和WS-Addressing 1.0。EnvelopeVersion.None表示消息并非一个SOAP消息,比如非XML结构的消息(比如基于JSON格式)以及POX(Plain Old XML)消息。AddressingVersion.None则表示消息不遵循WS-Addressing规范,比如通过手工方式解决寻址问题。

 1: public sealed class EnvelopeVersion
 2: { 
 3: //其他成员
 4: public static EnvelopeVersion None { get; }
 5: public static EnvelopeVersion Soap11 { get; }
 6: public static EnvelopeVersion Soap12 { get; }
 7: }
 1: public sealed class AddressingVersion
 2: { 
 3: //其他成员 
 4: public static AddressingVersion None { get; } 
 5: public static AddressingVersion WSAddressing10 { get; }
 6: public static AddressingVersion WSAddressingAugust2004 { get; }
 7: }

注: MessageVersion的静态方法CreateVersion(EnvelopeVersion envelopeVersion)默认采用的AddressingVersion为WSAddressing10。

由于EnvelopeVersion和AddressingVersion共同决定了MessageVesion。所以EnvelopeVersion和AddressingVersion的两两组合就得到相应的MessageVersion。这些通过两者组合得到的MessageVersion通过静态只读属性定义在MessageVersion类中。Soap11WSAddressing10、Soap11WSAddressingAugust2004、Soap12WSAddressing10和Soap12WSAddressingAugust2004的含义都是一目了然的,而None、Soap11和Soap12表示的EnvelopeVersion和Addressing组合分别是:

  • None:EnvelopeVersion.None + AddressingVersion.None;
  • Soap11:EnvelopeVersion.Soap11+ AddressingVersion.None;
  • Soap12:EnvelopeVersion.Soap12 + AddressingVersion.None
 1: public sealed class MessageVersion
 2: { 
 3: //其他成员 
 4: public static MessageVersion Default { get; }
 5:  
 6: public static MessageVersion None { get; }
 7: public static MessageVersion Soap11 { get; }
 8: public static MessageVersion Soap11WSAddressing10 { get; }
 9: public static MessageVersion Soap11WSAddressingAugust2004 { get; }
 10: public static MessageVersion Soap12 { get; }
 11: public static MessageVersion Soap12WSAddressing10 { get; }
 12: public static MessageVersion Soap12WSAddressingAugust2004 { get; }
 13: }

WS-Addressing是建立在SOAP之上的,所以EnvelopeVersion.None和AddressingVersion.WSAddressingAugust2004与AddressingVersion.WSAddressing10的组合是无效的。此外在MessageVersion中还定义了一个静态只读属性Default,表示默认的MessageVersion,目前该值为MessageVersion.Soap12WSAddressing10。

二、如何创建消息

由于Message是一个抽象类型,不能直接实例化。Message类中定义了一系列静态CreateMessage方法,使我们能够方便快捷地以不同的方式进行消息的创建。对于如此众多的CreateMessage方法,按照具体的消息创建方式的不同,大体上可以分为5类:

  • 创建空消息;
  • 将对象序列化成消息的主体(Body);
  • 通过XMLWriter将内容“写”到消息中;
  • 通过XMLReader将内容“读”到消息中;
  • 创建Fault消息。

1、创建空消息

下面是所有CreateMessage静态方法中最简单的一个,包含两个输入参数:消息的版本和Action。通过该方法可以创建一个只包含Action报头的SOAP消息。

 1: public abstract class Message : IDisposable
 2: {
 3: //其他成员
 4: public static Message CreateMessage(MessageVersion version, string action); 
 5: }

为演示消息的创建以及创建后的消息的结构,我写了下面一个辅助方法WriteMessage。该方法将一个Message对象写入一个文件中,并通过开启进程的方式将文件打开。

 1: static void WriteMessage(Message message, string fileName)
 2: {
 3: using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
 4: {
 5: message.WriteMessage(writer);
 6: }
 7: Process.Start(fileName);
 8: }

通过下面的代码,调用Message的CreateMessage方法,并设置消息版本为MessageVersion.Soap12WSAddressing10,Action设置为http://www.artech.com/myaction。最终将会生成如后面XML片断所示的SOAP消息。

 1: string fileName = @"E:\message.xml";
 2: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction");
 3: WriteMessage(message, fileName); 
 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: </s:Header>
 5: <s:Body />
 6: </s:Envelope> 

由于消息报头(Header)仅仅限于SOAP消息,所以如果将消息的版本改成MessageVersion.None,制定的Action不会被包含在消息中。实际上创建的Message对象不包含任何内容,最终生成的XML文件也不会包含任何文本信息。

 1: string fileName = @"E:\message.xml";
 2: Message message = Message.CreateMessage(MessageVersion.None, "http://www.artech.com/myaction");
 3: WriteMessage(message, fileName); 

2、将对象序列化成消息的主体

现在我们来关注Message的第2个重载的CreateMessage静态方法。如下面代码所示,该方法在上面一个重载方法的基础上加了一个object类型的body参数,表示消息的主体(Body)。在执行该方法的时候,相应的序列化器会被调用,将对象序列化成XML并将其置于消息的主体部分。默认的序列化器就是我们在前面介绍的DataContractSerializer。

 1: public abstract class Message : IDisposable
 2: {
 3: //其他成员
 4: public static Message CreateMessage(MessageVersion version, string action, object body);
 5: }

为了演示对象的序列化,我定义了下面一个数据契约Order,并定义了4个数据成员:OrderNo、OrderDate、Customer和ShipAddress。

 1: [DataContract(Namespace = "http://www.artech.com")]
 2: public class Order
 3: {
 4: [DataMember(Name = "OrderNo", Order = 1)]
 5: public Guid ID
 6: { get; set; }
 7:  
 8: [DataMember(Name = "OrderDate", Order = 2)]
 9: public DateTime Date
 10: { get; set; }
 11:  
 12: [DataMember(Order = 3)]
 13: public string Customer
 14: { get; set; }
 15:  
 16: [DataMember(Order = 4)]
 17: public string ShipAddress
 18: { get; set; }
 19: }

通过下面的代码,创建Order对象,并将其传入CreateMessage方法,作为body参数。最终将会生成如后面所示的SOAP消息。

 1: string fileName = @"E:\message.xml";
 2: Order order = new Order
 3: {
 4: ID = Guid.NewGuid(),
 5: Date = DateTime.Today,
 6: Customer = "Foo",
 7: ShipAddress = "#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province"
 8: };
 9: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction", order);
 10: WriteMessage(message, fileName);
 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: </s:Header>
 5: <s:Body>
 6: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
 7: <OrderNo>104a0213-1a0b-4d0b-b084-e912a991f908</OrderNo>
 8: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
 9: <Customer>Foo</Customer>
 10: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
 11: </Order>
 12: </s:Body>
 13: </s:Envelope>

从上面生成的XML,我们可以看出SOAP的主体部分就是Order对象通过DataContractSerializer序列化生成的XML。如果我们的消息不是一个SOAP消息呢?为了演示非SOAP消息的创建,我们将消息的版本替换成MessageVersion.None。从最终产生的XML结构来看,消息的整个部分就是Order对象序列化后的XML。

 1: //其他代码
 2: Message message = Message.CreateMessage(MessageVersion.None, "http://www.artech.com/myaction", order);
 3: WriteMessage(message, fileName);
 1: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
 2: <OrderNo>ae6047b2-6154-4b77-9153-6ffae03ac7c6</OrderNo>
 3: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
 4: <Customer>Foo</Customer>
 5: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
 6: </Order>

3、通过BodyWriter将内容写入消息

接下来,我们来介绍另一个包含BodyWriter参数的CreateMessage方法重载。

 1: public abstract class Message : IDisposable
 2: {
 3: //其他成员
 4: public static Message CreateMessage(MessageVersion version, string action, BodyWriter body);
 5: }

BodyWriter,顾名思义,就是消息主体的写入器。BodyWriter是一个抽象类,定义在System.ServiceModel.Channels命名空间下,下面的代码简单地描述了BodyWriter的定义。构造函数参数(isBuffered)和只读属性IsBuffered表示消息是否被缓存。消息主体内容的写入实现在OnWriteBodyContents方法中。

 1: public abstract class BodyWriter
 2: { 
 3: //其他成员
 4: protected BodyWriter(bool isBuffered);
 5: protected abstract void OnWriteBodyContents(XmlDictionaryWriter writer); 
 6:  
 7: public bool IsBuffered { get; } 
 8: }

为了演示基于BodyWriter的Message的创建过程,我自定义了一个简单的BodyWriter:XmlReaderBodyWriter。实现的功能很简单,就是从一个XML文件中读取内容作为消息主体的内容。XmlReaderBodyWriter的定义如下:

 1: public class XmlReaderBodyWriter : BodyWriter
 2: {
 3: private String _fileName; 
 4: internal XmlReaderBodyWriter(String fileName)
 5: : base(true)
 6: {
 7: this._fileName = fileName;
 8: } 
 9: protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
 10: {
 11: using (XmlReader reader = new XmlTextReader(this._fileName))
 12: {
 13: while (!reader.EOF)
 14: {
 15: writer.WriteNode(reader, false);
 16: }
 17: } 
 18: }
 19: }

假设现在有一个XML文件,具有下面列出的内容(即上面演示过程中Order对象序列化的结果),文件名为E:\order.xml。

 1: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
 2: <OrderNo>ae6047b2-6154-4b77-9153-6ffae03ac7c6</OrderNo>
 3: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
 4: <Customer>FOO</Customer>
 5: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
 6: </Order>

那么我们就可以通过我们定义的XmlReaderBodyWriter进行消息的创建,具体代码实现如下所示。最终生成后面所示的SOAP消息。

 1: string fileName1 = @"E:\order.xml";
 2: string fileName2 = @"E:\message.xml";
 3: XmlReaderBodyWriter writer = new XmlReaderBodyWriter(fileName1);
 4: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction", writer);
 5: WriteMessage(message, fileName2);
 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: </s:Header>
 5: <s:Body>
 6: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
 7: <OrderNo>104a0213-1a0b-4d0b-b084-e912a991f908</OrderNo>
 8: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
 9: <Customer>FOO</Customer>
 10: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
 11: </Order>
 12: </s:Body>
 13: </s:Envelope>

4、通过XMLReader将内容读到消息中

如果说基于BodyWriter进行消息的创建是采用一种“推”的模式将内容写入消息,那么基于XMLReader的方式就是采用一种“拉”的模式。Message中定义了4个基于XmlReader的CreateMessage重载,其中两个是直接利用XmlReader的,其余两个则是通过XmlReader的子类XmlDictionaryReader进行消息内容的写入。关于XmlDictionaryReader,在《WCF技术剖析(卷1)》中有详细的介绍,对此不十分了解的读者只需要将其理解为一个特殊的XmlReader就可以了。

 1: public abstract class Message : IDisposable
 2: { 
 3: //其他成员 
 4: public static Message CreateMessage(MessageVersion version, string action, XmlDictionaryReader body);
 5: public static Message CreateMessage(MessageVersion version, string action, XmlReader body);
 6: public static Message CreateMessage(XmlDictionaryReader envelopeReader, int maxSizeOfHeaders, MessageVersion version);
 7: public static Message CreateMessage(XmlReader envelopeReader, int maxSizeOfHeaders, MessageVersion version);
 8: }

在下面的程序演示中,创建一个XmlReader对象,用于读取一个XML文件。将该XmlReader对象传入CreateMessage方法中,该方法将会利用该XmlReader读取相应的XML,并将其作为消息的主体部分。

 1: string fileName1 = @"E:\order.xml";
 2: string fileName2 = @"E:\message.xml"; 
 3:  
 4: using (XmlReader reader = new XmlTextReader(fileName1))
 5: {
 6: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction", reader);
 7: WriteMessage(message, fileName2);
 8: }

5、创建Fault消息

接下来我们着重介绍如何创建一个Fault消息。在Message类中,定义了以下3个CreateMessage方法重载用以创建Fault消息。

 1: public abstract class Message : IDisposable
 2: {
 3: //其他成员
 4: public static Message CreateMessage(MessageVersion version, MessageFault fault, string action);
 5: public static Message CreateMessage(MessageVersion version, FaultCode faultCode, string reason, string action);
 6: public static Message CreateMessage(MessageVersion version, FaultCode faultCode, string reason, object detail, string action);
 7: }

对于一个Fault消息来说,SOAP Code和SOAP Reason是必须的元素。SOAP Reason描述出错的基本原因,通过字符串的形式表示。SOAP Code具体通过一个特殊的类System.ServiceModel.FaultCode表示,定义如下。

 1: public class FaultCode
 2: {
 3: public FaultCode(string name);
 4: public FaultCode(string name, FaultCode subCode);
 5: public FaultCode(string name, string ns);
 6: public FaultCode(string name, string ns, FaultCode subCode);
 7:  
 8: public static FaultCode CreateReceiverFaultCode(FaultCode subCode);
 9: public static FaultCode CreateReceiverFaultCode(string name, string ns);
 10: public static FaultCode CreateSenderFaultCode(FaultCode subCode);
 11: public static FaultCode CreateSenderFaultCode(string name, string ns);
 12:  
 13: public bool IsPredefinedFault { get; }
 14: public bool IsReceiverFault { get; }
 15: public bool IsSenderFault { get; }
 16: public string Name { get; }
 17: public string Namespace { get; }
 18: public FaultCode SubCode { get; }
 19: } 

一个完整的Fault Code由一个必需的Value元素和一个可选的SubCode元素构成(如下面的XML片段所示)。而Subcode的规范和Fualt Code一样,也就是说Subcode是一个FaultCode,这实际上这是一个嵌套的结构。对应到FaultCode类中,属性Name和Namepace对应Value结点的内容,而SubCode则自然对应着Fault Code的Subcode结点。如果你使用基于SOAP 1.1和SOAP 1.2的命名空间(SOAP 1.1为http://schemas. xmlsoap.org/soap/envelope/ ;SOAP 1.2为http://www.w3.org/2003/05/soap-envelope)或者是http://schemas.microsoft.com/ws/2005/05/envelope/none(相当于EnvelopeVersion.None),那么将被视为预定义错误(Fault)。对于一个FaultCode,可以通过IsPredefinedFault属性判断是否为预定义错误。SOAP 1.1和SOAP 1.2定义了一些预定义的Fault Code,比如VersionMismatch、MustUnderstand、DataEncodingUnknown、Sender、Reveiver等等,其中Sender和Reveiver表示发送端和接收端导致的错误。FaultCode甚至定义了4个静态的方法(CreateSenderFaultCode和CreateReceiverFaultCode)方便开发者创建这两种特殊的FaultCode。

 1: <env:Code>
 2: <env:Value>env:Sender</env:Value>
 3: <env:Subcode>
 4: <env:Value>m:MessageTimeout</env:Value>
 5: </env:Subcode>
 6: </env:Code>

下面的代码是一个典型的创建Fault消息的例子,后面给出的XML是最终生成的SOAP消息。

 1: string fileName = @"E:\message.xml";
 2: FaultCode subCode = new FaultCode("E0001", "http://www.artech.com/faults/");
 3: FaultCode faultCode = FaultCode.CreateSenderFaultCode(subCode);
 4: Message message = Message.CreateMessage(MessageVersion.Default, faultCode, "Access is denied.", "http://www.artech.com/myaction");
 5: WriteMessage(message, fileName);
 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: </s:Header>
 5: <s:Body>
 6: <s:Fault>
 7: <s:Code>
 8: <s:Value>s:Sender</s:Value>
 9: <s:Subcode>
 10: <s:Value xmlns:a="http://www.artech.com/faults/">a:E0001</s:Value>
 11: </s:Subcode>
 12: </s:Code>
 13: <s:Reason>
 14: <s:Text xml:lang="en-US">Access is denied.</s:Text>
 15: </s:Reason>
 16: </s:Fault>
 17: </s:Body>
 18: </s:Envelope>

除了直接通过指定Fault Code和Fault Reason创建Fault消息之外,还可以利用System.ServiceModel.Channels.MessageFault对象的方式创建。实际上,MessageFault就是Fault消息托管类型的表示。由于篇幅所限,在这里就不做详细介绍了,有兴趣的读者可以参阅MSDN在线文档。


作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
安全 网络协议 网络安全
WCF安全3-Transport与Message安全模式
WCF安全3-Transport与Message安全模式
WCF安全3-Transport与Message安全模式
|
安全 C#
WCF技术我们应该如何以正确的方式去学习掌握
一、WCF技术我该如何学习?       阿笨的回答是:作为初学者的我们,那么请跟着阿笨一起玩WCF吧,阿笨将带领大家如何以正确的姿势去掌握WCF技术。由于WCF技术知识点太多了,就纯基础概念性知识都可以单独出一本书来讲解,本次分享课程《C#面向服务编程技术WCF从入门到实战演练》开课之前,阿笨还是希望从没了解过WCF技术的童鞋们提前先了解一下WCF技术,至少要明白WCF技术的ABC三要素分别指的是什么。
1171 0