概述
WCF是一个通信框架,同时也可以将它看成是一个消息处理或者传递的基础框架,它可以接收消息、对消息做处理,或者根据客户端给定的数据构造消息并将消息发送到目标端点,在这个过程中,一切都是围绕“消息”而展开的。WCF在消息处理体系结构提供统一编程模型的同时,还允许灵活的表示数据和传递消息,本文将介绍如何配置消息支持各个SOAP和WS-Addressing版本或者不用任何SOAP和WS-Addressing,以及如何控制消息状态等。
消息契约
在大多数情况下,开发者只关心数据契约而不必考虑携带这些数据的消息,然而某些特殊情况下,需要完全控制SOAP消息的结构,如提供户操作性,或者控制消息的某一部分的安全性,此时可以使用WCF中提供的编程模型消息契约,它使用一种可直接序列化为所需精确SOAP消息的类型。如果为某一个数据类型定义了消息契约,我们可以完全控制该类型和SOAP消息之间的映射,如下面的代码:
[MessageContract] public class CustomerMessage { [MessageHeader] public Guid Id { get; set; } [MessageBodyMember] public String Name { get; set; } [MessageBodyMember] public String Email { get; set; } }
此处使用MessageContract特性指定CustomerMessage类型为消息契约,并用MessageHeader指定Id属性在SOAP消息的标头,用MessageBodyMember指定Name、Email作为SOAP消息的正文,如果拦截到SOAP消息,可以看到如下所示:
<s:Envelope xmlns:s="[url]http://schemas.xmlsoap.org/soap/envelope/[/url]"> <s:Header> <Action s:mustUnderstand="1" xmlns="[url]http://schemas.microsoft.com/ws/2005/05/addressing/none[/url]"> [url]http://tempuri.org/ICustomerContract/GetCustomerResponse[/url] </Action> <h:Id xmlns:h="[url]http://tempuri.org/[/url]"> 38097c1d-366d-4c58-84a5-93525766630c </h:Id> </s:Header> <s:Body> <CustomerMessage xmlns="[url]http://tempuri.org/[/url]"> <Email>lhj_cauc[@@AT@@]163.com</Email> <Name>TerryLee</Name> </CustomerMessage> </s:Body> </s:Envelope>
当然,还可以在消息契约中使用数组,对消息的部分进行签名和加密等操作以及指定标头和正文部分的命名空间,这些不是本文的重点,将不再阐述。可以看到,消息契约为开发者完全控制SOAP消息和自定义类型之间的映射,提供了一种非常方便的途径。
认识Message类型
绝大多数情况下,我们都不会直接去使用Message类,而是仅仅使用WCF服务编程模型中的数据契约、消息契约来描述输入或者输出消息。但在某些高级应用中,我们需要对Message类进行编程,如需要从别处创建输出消息的内容,而不是序列化.NET Framework类型,如可能从磁盘上的某个文件来创建输出消息,在这种情况下,简单的使用WCF中服务编程模型已经不能满足需要,而需要针对Message类进行编程。
简单来说,Message类是一个通用的数据容器,在本质上它完全模拟SOAP消息正文以及消息标头和属性的集合,另外Message类中提供了一系列的方法用来创建消息、读写消息正文以及标头和属性的集合。它的定义如下所示:
public abstract class Message : IDisposable { // 标头集合 public abstract MessageHeaders Headers { get; } protected bool IsDisposed { get; } public virtual bool IsEmpty { get; } public virtual bool IsFault { get; } // 属性集合 public abstract MessageProperties Properties { get; } public MessageState State { get; } // 消息版本 public abstract MessageVersion Version { get; } public void Close(); public MessageBuffer CreateBufferedCopy(int maxBufferSize); public static Message CreateMessage(MessageVersion version, string action); // 获取正文 public T GetBody<T>(); public void WriteBody(XmlWriter writer); public void WriteMessage(XmlWriter writer); public void WriteStartBody(XmlWriter writer); public void WriteStartEnvelope(XmlDictionaryWriter writer); // 更多成员 }
消息版本
在创建消息时,一个很重要的部分就是指定消息版本,消息版本标识了消息在传输中使用SOAP和WS-Addressing的版本,即每个消息版本都会由两部分组成,SOAP版本和WS-Addressing版本。在WCF中,支持的SOAP信封版本用EnvelopeVersion类来表示,如下代码:
public sealed class EnvelopeVersion { public static EnvelopeVersion None { get; } public static EnvelopeVersion Soap11 { get; } public static EnvelopeVersion Soap12 { get; } }
None表示在传输中不适用SOAP,即使用传统的XML消息传递方案;Soap11和Soap12分别表示在传输中使用SOAP 1.1版本和SOAP 1.2版本。
同样,在WCF中支持的WS-Addressing版本用AddressingVersion类来表示,如下代码:
public sealed class AddressingVersion { public static AddressingVersion None { get; } public static AddressingVersion WSAddressing10 { get; } public static AddressingVersion WSAddressingAugust2004 { get; } }
WSAddressingAugust2004表示2004年8月开始提交的WS-Addressing W3C提案,目前得到广泛支持。而WSAddressing10表示最终的WS-Addressing 1.0 W3C推荐标准。
在创建消息时,我们需要指定消息版本,这个消息版本包括所使用的SOAP和WS-Addressing规范的版本,如下代码所示:
MessageVersion version = MessageVersion.CreateVersion (EnvelopeVersion.Soap11, AddressingVersion.WSAddressing10);
此外,在MessageVersion中,已经提供了对这两者之间的常见组合,这样我们可以使用而不用考虑两者之间的兼容性等问题,如下代码所示:
public sealed class MessageVersion { public static MessageVersion Default { get; } public static MessageVersion None { get; } public static MessageVersion Soap11 { get; } public static MessageVersion Soap11WSAddressing10 { get; } public static MessageVersion Soap11WSAddressingAugust2004 { get; } public static MessageVersion Soap12 { get; } public static MessageVersion Soap12WSAddressing10 { get; } public static MessageVersion Soap12WSAddressingAugust2004 { get; } }
我们看看在SOAP消息中,SOAP版本和WS-Addressing版本是如何体现的,创建一个简单的消息,使用的消息版本为Soap11WSAddressing10:
public Message GetCustomer() { Customer customer = new Customer { Id = Guid.NewGuid(), Name = "TerryLee", Email = "lhj_cauc[@@AT@@]163.com" }; Message message = Message.CreateMessage( MessageVersion.Soap11WSAddressing10, "http://localhost/CustomerService/GetCustomer", customer); return message; }
可以看到SOAP消息包为如下所示,从命名空间上可以看到使用的SOAP版本为1.1而WS-Addressing为1.0。
<s:Envelope xmlns:a="[url]http://www.w3.org/2005/08/addressing[/url]" xmlns:s="[url]http://schemas.xmlsoap.org/soap/envelope/[/url]"> <s:Header> <a:Action s:mustUnderstand="1"> [url]http://localhost/CustomerService/GetCustomer[/url] </a:Action> </s:Header> <s:Body> <Customer xmlns:i="[url]http://www.w3.org/2001/XMLSchema-instance[/url]" xmlns="[url]http://schemas.datacontract.org/2004/07/[/url] Data"> <Email>lhj_cauc[@@AT@@]163.com</Email> <Id>d297aa45-2d9e-4f89-aa41-491507db2a21</Id> <Name>TerryLee</Name> </Customer> </s:Body> </s:Envelope>
如果修改消息的版本为Soap12WSAddressingAugust2004,可以看到它们命名空间发生的变化:
<s:Envelope xmlns:a="[url]http://schemas.xmlsoap.org/ws/2004/08/addressing[/url]" xmlns:s="[url]http://www.w3.org/2003/05/soap-envelope[/url]"> <s:Header> <a:Action s:mustUnderstand="1"> [url]http://localhost/CustomerService/GetCustomer[/url] </a:Action> </s:Header> <s:Body> <Customer xmlns:i="[url]http://www.w3.org/2001/XMLSchema-instance[/url]" xmlns="[url]http://schemas.datacontract.org/2004/07/[/url] Data"> <Email>lhj_cauc[@@AT@@]163.com</Email> <Id>e13bef92-bba2-47c2-954c-ba7bfe472cc2</Id> <Name>TerryLee</Name> </Customer> </s:Body> </s:Envelope>
终结点配置与消息
大家都知道终结点的配置由契约、地址和绑定组成,其中契约定义了消息和方法之间的映射,而地址则指定了服务在何处,在绑定中描述了所要使用的传输,消息采用的编码方法以及支持WS-*系列协议,同时还有消息版本,对于每个绑定来说,它所使用的消息编码器和消息版本不尽相同,关于消息编码器将会在后面详细讲述,先来看一下消息版本,如在服务端有如下配置:
<endpoint address="" binding ="basicHttpBinding" contract="TerryLee.MessageHandling.Contract.ICustomerContract" name="defaultBinding"> </endpoint> <endpoint address="Other" binding ="wsHttpBinding" contract="TerryLee.MessageHandling.Contract.ICustomerContract" name="otherBinding"> </endpoint>
我们在宿主端分别输出一下它们所采用的消息版本:
foreach (ServiceEndpoint endpoint in host.Description.Endpoints) { Console.WriteLine("Binding:{0}", endpoint.Binding.Name); Console.WriteLine("AddressingVersion:{0}", endpoint.Binding.MessageVersion.Addressing.ToString()); Console.WriteLine("EnvelopeVersion:{0}", endpoint.Binding.MessageVersion.Envelope.ToString()); Console.WriteLine("----------------------------\n"); }
最后结果如下:
Binding:BasicHttpBinding AddressingVersion:AddressingNone ([url]http://schemas.microsoft.com/ws/2005/05/addres[/url] sing/none) EnvelopeVersion:Soap11 ([url]http://schemas.xmlsoap.org/soap/envelope/[/url]) ---------------------------- Binding:WSHttpBinding AddressingVersion:Addressing10 ([url]http://www.w3.org/2005/08/addressing[/url]) EnvelopeVersion:Soap12 ([url]http://www.w3.org/2003/05/soap-envelope[/url]) ----------------------------
可以看到,对于BasicHttpBinding来说,它的Addressing版本为None,SOAP版本为Soap11,即MessageVersion为Soap11;而对于WSHttpBinding来说,它的Addressing版本为Addressing10,SOAP版本为Soap12,即MessageVersion为Soap12WSAddressing10。
操作消息
在创建消息时,可以从其它文件写入消息正文,或者把自定义类型序列化到消息正文中。同样我们还可以控制消息的标头和属性,标头将会在SOAP消息中进行传输,所以当中介在检查标头时,必须支持标头使用的协议的基础版本;而属性提供一种与版本更加无关的方式来批注消息。如下面的代码:
public Message GetCustomer() { Customer customer = new Customer { Name = "TerryLee", Email = "lhj_cauc[@@AT@@]163.com" }; Message message = Message.CreateMessage( MessageVersion.Soap12WSAddressingAugust2004, "http://localhost/CustomerService/GetCustomer", customer); message.Headers.Add(MessageHeader.CreateHeader( "CustomerID", "http://www.cnblogs.com/terrylee", Guid.NewGuid() )); return message; }
SOAP消息如下所示,可以看到CustomerID放在SOAP消息标头中:
<s:Envelope xmlns:a="[url]http://schemas.xmlsoap.org/ws/2004/08/addressing[/url]" xmlns:s="[url]http://www.w3.org/2003/05/soap-envelope[/url]"> <s:Header> <a:Action s:mustUnderstand="1"> [url]http://localhost/CustomerService/GetCustomer[/url] </a:Action> <CustomerID xmlns="[url]http://www.cnblogs.com/terrylee[/url]"> c2f34dd3-d71a-42fa-b3f2-6f58c553c8ee </CustomerID> </s:Header> <s:Body> <Customer xmlns:i="[url]http://www.w3.org/2001/XMLSchema-instance[/url]" xmlns="[url]http://schemas.datacontract.org/2004/07/Data[/url]"> <Email>lhj_cauc[@@AT@@]163.com</Email> <Id>00000000-0000-0000-0000-000000000000</Id> <Name>TerryLee</Name> </Customer> </s:Body> </s:Envelope>
消息状态控制
在WCF中,Message类的正文对象已经设计为支持流处理,这意味着在Message的生命周期内只能被处理一次。这是通过保持Message对象的当前状态来强制实施的。当Message对象处于Created状态时,可读取/写入/复制该对象,其他状态为 Read、Written 和 Copied,这意味着相应的操作已经执行过一次。Message对象的五种状态定义如下:
public enum MessageState { Created, Read, Written, Copied, Closed, }
Message对象在开始时处于Created状态,该状态是处理正文的唯一有效状态。处理正文有以下几种不同的方式:可以对其进行读取、写入或复制。调用GetReaderAtBodyContents或 GetBody<T> 可将状态更改为Read。调用WriteMessage或WriteBody可将状态更改为Written。调用 CreateBufferedCopy可将状态更改为Copied,如下图所示:
如下面的代码:
Customer c = new Customer { Name = "TerryLee", Email = "lhj_cauc[@@AT@@]163.com" }; Message message = Message.CreateMessage( MessageVersion.Soap12WSAddressingAugust2004, "http://localhost/CustomerService/GetCustomer", c); Console.WriteLine(message.State); Customer c = message.GetBody<Customer>(); Console.WriteLine(message.State); message.Close(); Console.WriteLine(message.State);