WCF初探-22:WCF中使用Message类(上)
前言
- 从我们学习WCF以来,就一直强调WCF是基于消息的通信机制。但是由于WCF给我们做了高级封装,以至于我们在使用WCF的时候很少了解到消息的内部机制。由于WCF的架构的可扩展性,针对一些特殊情况,WCF为我们提供了Message类来深度定制消息结构,以便我们拓展WCF的通信机制。
- 在之前的文章中,我们针对一些常用的WCF传递数据的方式进行了说明,比如数据协定和消息协定等。他们传递的数据最终都会转化为消息的实例。具体参照:
Message类概述
- Message 类是 WCF的基本类。客户端与服务之间的所有通信最终都会产生要进行发送和接收的 Message 实例。我们通常不会与 Message 类直接进行交互。相反,我们需要使用 WCF 服务模型构造(如数据协定、消息协定和操作协定)来描述传入消息和传出消息。但是,在某些高级方案中,可以直接使用 Message 类进行编程。Message 类提供一种方法,使网络中的发送方和接收方之间可对任意信息进行通信。使用它可以传递信息、建议或要求执行一系列操作或请求数据。
- Message 类用作消息的抽象表示形式,但其设计在很大程度上依赖于 SOAP 消息。Message 包含三个主要信息部分:消息正文、消息头和消息属性。
- 消息正文:消息正文用于表示消息的实际数据负载。消息正文始终表示为 XML Infoset。这并不意味着在 WCF 中创建或接收的所有消息都必须为 XML 格式。这要由通道堆栈来确定如何解释消息正文。通道堆栈可能会将消息正文作为 XML 发出、将转换为某种其他格式(比如Json),甚至可能会完全忽略该消息正文(比如空消息)。当然,对于 WCF 提供的大多数绑定,消息正文在 SOAP 信封的正文部分中都表示为 XML 内容。
- 消息头:消息可以包含标头。标头在逻辑上由与名称、命名空间和几个其他属性相关联的 XML Infoset 组成。在 Message 上使用 Headers 属性可以访问消息头。每个标头由一个 MessageHeader 类表示。消息头通常在使用配置的通道堆栈处理 SOAP 消息时映射到 SOAP 消息头。
- 消息属性:消息可以包含属性。属性是任何与字符串名称关联的 .NET Framework 对象。通过 Message 上的 Properties 属性可以访问这些属性。与消息正文和消息头不同(通常分别映射到 SOAP 正文和 SOAP 标头),消息属性通常不与消息一起发送或接收。消息属性主要作为一种通信机制,用于在通道堆栈中的各个通道之间以及通道堆栈和服务模块之间传递有关消息的数据。
- 总之,Message 是一种通用的数据容器,但其设计严格遵循 SOAP 协议中消息的设计方式。就像 SOAP 中一样,消息同时具有消息正文和标头。消息正文包含实际负载数据,而标头包含其他已命名的数据容器。用于读取和写入消息正文与标头的规则是不同的,例如,标头总是在内存中进行缓冲,并且可以按任意顺序访问任意次,而正文仅能读取一次且可以进行流式处理。通常,使用 SOAP 时,消息正文被映射到 SOAP 正文,而消息头被映射到 SOAP 标头。
Message类的使用场景及限制
- 在以下情况下可能需要使用 Message 类:
- 需要一种替代方式来创建传出的消息内容(例如,从磁盘上的文件直接创建消息),而不是序列化 .NET Framework 对象。
- 需要一种替代方式来使用传入的消息内容(例如,需要将 XSLT 转换应用于原始 XML 内容),而不是反序列化为 .NET Framework 对象。
- 无论消息内容怎样都需要使用常规方式来处理消息(例如,在生成路由器、负载平衡器或发布-订阅系统时对消息进行路由或转发)。
- 可以将 Message 类用作操作的输入参数和/或操作的返回值。只要在操作中的任何位置使用了 Message,就必须遵从以下限制:
- 操作不能具有任何 out 或 ref 参数。
- 不能有一个以上的 input 参数。如果该参数存在,其类型必须为 Message 或消息协定。
- 返回类型必须为 void、Message 或消息协定类型。
使用Message类创建消息
- 创建基本消息
- 所有 CreateMessage 重载都采用一个类型为 MessageVersion 的版本参数,该参数指示要用于消息的 SOAP 和 WS-Addressing 版本。如果要使用与传入消息相同的协议版本,则可以使用 OperationContext 实例(从 Current 属性获取)上的 IncomingMessageVersion 属性。大多数 CreateMessage 重载还具有一个字符串参数,该参数指示要用于消息的 SOAP 操作。可以将版本设置为 None 以禁用 SOAP 信封生成;消息仅包含正文。示例代码如下:
public Message GetDataEmpty() { MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, "http://tempuri.org/IUserInfo/GetDataEmptyResponse"); }
- 从对象创建消息
- 另一种重载采用一个附加的 Object 参数;此重载所创建的消息的正文是给定对象的序列化表示。对象可以是DataContract或者MessageContract。示例代码如下:
public Message GetDataObject() { User user = new User(); user.Name = "JACK"; user.Age = 20; user.ID = 1; user.Nationality = "CHINA"; MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, "http://tempuri.org/IUserInfo/GetDataObjectResponse", user); }
- 从 XML 读取器创建消息
- CreateMessage 重载采用一个 XmlReader 或一个 XmlDictionaryReader 而不是对象作为正文。在这种情况下,消息的正文会包含从传递的 XML 读取器中进行读取而产生的 XML。比如我们有一个名称为test.xml文件存放着User对象。Xml文件格式如下:
示例代码如下:
public Message GetDataXml() { string path = Environment.CurrentDirectory.Substring(0, Environment.CurrentDirectory.IndexOf("bin")) + "test.xml"; FileStream stream = new FileStream(path, FileMode.Open); XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas()); MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, "http://tempuri.org/IUserInfo/GetDataXmlResponse", xdr); }
- 创建错误消息
- 可以使用某些 CreateMessage 重载创建 SOAP 错误消息。其中一个最基本的重载采用一个用于描述错误的 MessageFault 对象作为参数。其他重载是为方便起见而提供的。第一个这样的重载采用一个 FaultCode 和一个原因字符串作为参数,并使用 MessageFault.CreateFault(该方法使用这些信息)创建一个 MessageFault。另一个重载采用一个详细信息对象作为参数,并将该对象与错误代码和原因一起传递给 CreateFault。示例代码如下:
public Message GetDataFault() { FaultException<FaultMessage> fe = new FaultException<FaultMessage> (new FaultMessage("错误时间:" + System.DateTime.Now.ToString(), "输入的字符串大于10个字符")); MessageFault fault = fe.CreateMessageFault(); MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, fault, "http://tempuri.org/IUserInfo/GetDataFaultResponse"); }
WCF中使用Message类示例
- 解决方案如下:
- 工程结构说明如下:
- service:类库程序,WCF服务端程序。IUserInfo.cs定义了四个返回值为Message的操作协定,GetDataEmpty()返回空消息、GetDataObject()返回从对象创建的消息、GetDataXml()返回从XML文件读取创建的消息、GetDataFault()返回创建的错误消息。定了类型为User的消息协定(用于传输消息体数据)和类型为FaultMessage的错误协定(用于传输消息的错误数据)。IUserInfo.cs的代码如下:
using System.ServiceModel; using System.Runtime.Serialization; using System.ServiceModel.Channels; namespace Service { [ServiceContract] public interface IUserInfo { [OperationContract] Message GetDataEmpty(); [OperationContract] Message GetDataObject(); [OperationContract] Message GetDataXml(); [OperationContract] Message GetDataFault(); } [MessageContract] public class User { [MessageBodyMember] public int ID { get; set; } [MessageBodyMember] public string Name { get; set; } [MessageBodyMember] public int Age { get; set; } [MessageBodyMember] public string Nationality { get; set; } } [DataContract] public class FaultMessage { private string _errorTime; private string _errorMessage; [DataMember] public string ErrorTime { get { return this._errorTime; } set { this._errorTime = value; } } [DataMember] public string ErrorMessage { get { return this._errorMessage; } set { this._errorMessage = value; } } public FaultMessage(string time, string message) { this._errorTime = time; this._errorMessage = message; } } }
UserInfo.cs的代码如下:
using System.ServiceModel.Channels; using System.ServiceModel; using System.IO; using System; using System.Xml; namespace Service { public class UserInfo:IUserInfo { public Message GetDataEmpty() { MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, "http://tempuri.org/IUserInfo/GetDataEmptyResponse"); } public Message GetDataObject() { User user = new User(); user.Name = "JACK"; user.Age = 20; user.ID = 1; user.Nationality = "CHINA"; MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, "http://tempuri.org/IUserInfo/GetDataObjectResponse", user); } public Message GetDataXml() { string path = Environment.CurrentDirectory.Substring(0, Environment.CurrentDirectory.IndexOf("bin")) + "test.xml"; FileStream stream = new FileStream(path, FileMode.Open); XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas()); MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, "http://tempuri.org/IUserInfo/GetDataXmlResponse", xdr); } public Message GetDataFault() { FaultException<FaultMessage> fe = new FaultException<FaultMessage> (new FaultMessage("错误时间:" + System.DateTime.Now.ToString(), "输入的字符串大于10个字符")); MessageFault fault = fe.CreateMessageFault(); MessageVersion ver = OperationContext.Current.IncomingMessageVersion; return Message.CreateMessage(ver, fault, "http://tempuri.org/IUserInfo/GetDataFaultResponse"); } } }
2. Host:控制台应用程序,服务承载程序。添加对程序集Service的引用,完成以下代码,寄宿服务。Program.cs代码如下:
using System; using System.ServiceModel; using Service; namespace Host { class Program { static void Main(string[] args) { using (ServiceHost host = new ServiceHost(typeof(UserInfo))) { host.Opened += delegate { Console.WriteLine("服务已经启动,按任意键终止!"); }; host.Open(); Console.Read(); } } } }
由于wsHttpBinding默认启用安全机制,为了简便的观察消息体结构,在本示例中设置wsHttpBinding的security mode="None",App.config的代码如下:
<?xml version="1.0"?> <configuration> <system.serviceModel> <services> <service name="Service.UserInfo" behaviorConfiguration="mexBehavior"> <host> <baseAddresses> <add baseAddress="http://localhost:1234/UserInfo/"/> </baseAddresses> </host> <endpoint address="" binding="wsHttpBinding" contract="Service.IUserInfo" bindingConfiguration="bindConfig"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <behaviors> <serviceBehaviors> <behavior name="mexBehavior"> <serviceMetadata httpGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> </serviceBehaviors> </behaviors> <bindings> <wsHttpBinding> <binding name="bindConfig"> <security mode="None" /> </binding> </wsHttpBinding> </bindings> </system.serviceModel> <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
test.xml文件的内容如下:
<?xml version="1.0" encoding="utf-8" ?> <User> <ID>1</ID> <Name>20</Name> <Age>JACK</Age> <Nationality>CHINA</Nationality> </User>
运行Host.exe程序,成功寄宿服务后,我们通过svcutil.exe工具生成客户端代理类和客户端的配置文件svcutil.exe是一个命令行工具,
位于路径C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin下,我们可以通过命令行运行该工具生成客户端代理类
- 在运行中输入cmd打开命令行,输入 cd C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin
- 输入svcutil.exe /out:f:\UserInfoClient.cs /config:f:\App.config http://localhost:1234/UserInfo
3. Client:控制台应用程序,客户端调用程序。将生成的UserInfoClient.cs和App.config复制到Client的工程目录下,完成客户端调用代码。Program.cs的代码如下:
using System; using System.ServiceModel.Channels; namespace Client { class Program { static void Main(string[] args) { UserInfoClient proxy = new UserInfoClient(); //显示创建基本消息(空消息) //Message message = proxy.GetDataEmpty(); //Console.WriteLine(message.ToString()); //显示从对象创建消息 //Message message = proxy.GetDataObject(); //Console.WriteLine(message.ToString()); //显示从XML读取器创建消息 //Message message = proxy.GetDataXml(); //Console.WriteLine(message.ToString()); //显示创建错误消息 Message message = proxy.GetDataFault(); Console.WriteLine(message.ToString()); Console.Read(); } } }
- 运行空消息代码,显示结果如下:
从运行结果可以看出一个基本的消息结构为<s:Envelope></ s:Envelope >,其中包含消息头<s:Header>< s:Header >和消息正文<s:Body></ s:Body >
- 运行从对象创建的消息,显示结果如下:
从运行结果可以看出类型为User的MessageContract转化为了消息的正文部分
- 运行从XML读取器创建的消息,显示结果如下:
从运行结果可以看出读取文件test.xml的内容转化为了消息正文部分
- 运行创建的错误消息,显示结果如下:
从运行结果可以看出类型为FaultMessage的DataContract转化为了消息的正文部分,并且嵌套在了错误消息结构<s:Fault>下的<s:Detail>中。
总结
- 通过以上示例,我们了解到了消息的基本结构,这个不再是操作WCF测试客户端,而是我们操作Message基类来描述消息结构。
- 尽管上面出现创建消息的几种方式,但我们知道消息都是通过Message的CreateMessage方法的重载来制定和创建消息,并且消息版本(MessageVersion)和消息Action必须指定。Action一般为服务协定的命名空间+服务协定借口名称+操作协定+Response