WCF後傳系列(10):訊息處理功能核心

weixin_34321977發表於2008-11-17

概述

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="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <Action s:mustUnderstand="1"
      xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">
      http://tempuri.org/ICustomerContract/GetCustomerResponse
    </Action>
    <h:Id xmlns:h="http://tempuri.org/">
      38097c1d-366d-4c58-84a5-93525766630c
    </h:Id>
  </s:Header>
  <s:Body>
    <CustomerMessage xmlns="http://tempuri.org/">
      <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="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <a:Action s:mustUnderstand="1">
      http://localhost/CustomerService/GetCustomer
    </a:Action>
  </s:Header>
  <s:Body>
    <Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
              xmlns="http://schemas.datacontract.org/2004/07/ 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="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">
      http://localhost/CustomerService/GetCustomer
    </a:Action>
  </s:Header>
  <s:Body>
    <Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
              xmlns="http://schemas.datacontract.org/2004/07/ 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 (http://schemas.microsoft.com/ws/2005/05/addres
sing/none)
EnvelopeVersion:Soap11 (http://schemas.xmlsoap.org/soap/envelope/)
----------------------------

Binding:WSHttpBinding
AddressingVersion:Addressing10 (http://www.w3.org/2005/08/addressing)
EnvelopeVersion:Soap12 (http://www.w3.org/2003/05/soap-envelope)
----------------------------

可以看到,對於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="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">
      http://localhost/CustomerService/GetCustomer
    </a:Action>
    <CustomerID xmlns="http://www.cnblogs.com/terrylee">
      c2f34dd3-d71a-42fa-b3f2-6f58c553c8ee
    </CustomerID>
  </s:Header>
  <s:Body>
    <Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
              xmlns="http://schemas.datacontract.org/2004/07/Data">
      <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,如下圖所示:

TerryLee_WCF_34

如下面的程式碼:

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);

輸出的Message狀態分別為:

Created
Read
Closed

小結

WCF在訊息處理體系結構提供統一程式設計模型的同時,還允許靈活的表示資料和傳遞訊息。從本文可以看出,它可以配置訊息支援各個SOAP和WS-Addressing版本或者不適用任何SOAP和WS-Addressing,這將提供極大的靈活性。

相關文章