前言
- 從我們學習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