1. 程式人生 > >WCF學習心得------(七)訊息協定

WCF學習心得------(七)訊息協定

第七章 訊息協定

7.1 訊息協定概述

通常情況下,在定義訊息的架構時只使用資料協定就足夠,但是有時需要精確控制如何將型別對映到通過網路傳輸的SOAP訊息。對於這種情況,通常解決方案是插入自定義的SOAP標頭。

此外還可以定義訊息頭和正文的安全屬性,通過確定是否對這些元素進行數字簽名和加密,訊息樣式的操作可提供這種控制。訊息樣式的操作最多具有一個引數和一個返回值,其中引數和返回值的型別都是訊息型別,即這兩種型別可直接序列化為指定的SOAP訊息結構。

訊息協定可以是用MessageContractAttribute標記的任何型別或Message型別。

如下所示:

[OperationContract]
BankingTransactionResponse Process(BankingTransaction bt);
[OperationContract]
Void Store(BankingTransaction  bt);

以上示例的兩個訊息樣式的操作均為有效的操作。

那麼該如何定義訊息協定呢?請看以下示例:

[MessageContract]
Public class BankingTranscation
{
[MessageHeader]
Public Operation operation;
[MessageHeader]
Public DateTime transactionDate;
[MessageBodyMember]
Private Account sourceAccount;
}

通過上邊的示例我們可以看出,若要為某一型別定義訊息協定即定義該型別和SOAP信封之間的對映,請對該型別的應用MessageContractAttribute屬性,然後對型別中藥成為SOAP標頭的成員使用MessageHeaderAttribute屬性,對成為訊息的SOAP正文部分成員使用MessageBodyMemberAttribute屬性即可。

此外還需要注意的是,可以對所有的欄位/屬性/事件應用MessageHeaerAttribute和MessageBodyMemeberAttribute,且不受欄位/屬性/事件的訪問限制。

演示示例:

  1. 新建一個WCF應用服務程式。修改後的目錄結構如下:

                       

  1. 在ICalculatorService.cs檔案中,定義訪問介面,訊息協定類MyMessage

具體程式碼如下:

[ServiceContract(Namespace="Http://Microsoft.WCF.Message")]
    public interface ICalculatorService
    {
        [OperationContract(Action = "Http://Test/MyMessage action", ReplyAction = "Http://Test/MyMessage action")]
        MyMessage Calculator(MyMessage request);
    }
    [MessageContract]
    public class MyMessage
    {
        private string operation;
        private double n1;
        private double n2;
        private double result;
        public MyMessage(){}
        public MyMessage(string operation ,double n1,double n2,double result)
        {
            this.n1=n1;
            this.n2=n2;
            this.result=result;
            this.operation=operation;
        }
        public MyMessage(MyMessage message)
        {
            this.n1=message.n1;
            this.n2=message.n2;
            this.operation=message.operation;
            this.result=message.result;
        }
        [MessageHeader]
        public string Operation
        {
            get{return operation;}
            set{operation=value;}
        }
        [MessageBodyMember]
        public double N1
        {
            get{return n1;}
            set{n1=value;}
        }
        [MessageBodyMember]
        public double N2
        {
            get{return n2;}
            set{n2=value;}
        }
        [MessageBodyMember]
        public double Result
        {
          get{return result;}
          set{result=value;}
        }   
        [MessageHeader(MustUnderstand=true)]
        public string str;

2. 在CalculatorService.svc檔案中,定義CalculatorService類,該類實現上一步中定義的ICalculatorService介面。具體程式碼如下:

  public class CalculatorService : ICalculatorService
    {
        public MyMessage Calculator(MyMessage request)
        {
            MyMessage myMessage = new MyMessage(request);
            switch (myMessage.Operation)
            {
                case "+":
                    myMessage.Result = myMessage.N1 + myMessage.N2;
                    break;
                case "-":
                    myMessage.Result = myMessage.N1 - myMessage.N2;
                    break;
                case "*":
                    myMessage.Result = myMessage.N1 * myMessage.N2;
                    break;
                case "/":
                   myMessage.Result = myMessage.N1 / myMessage.N2;
                    break;
                default:
                    myMessage.Result = 0.0D;
                    break;
            }
            return myMessage;

        }
}

3. 對配置檔案進行一些配置設定,程式碼如下:

在<System.serviceModel>節點下新增如下內容:

<services>
      <service name="Message.CalculatorService"  behaviorConfiguration="Message.CalculatorServiceBehaviors">
             <endpoint binding="wsHttpBinding"
                       contract="Message.ICalculatorService"/>
             <endpoint binding="mexHttpBinding"
                       contract="IMetadataExchange"
                       address="mex"/>
</service>
    </services>

4. 將上一步生成檔案進行釋出,部署,然後利用C盤Windows SDKs資料夾下的Svcutil.exe工具生成客戶端呼叫類和App.Config配置檔案。(關於Svcutil.exe工具的使用請參考上一篇文章)

5. 新建控制檯應用程式,作為呼叫服務的客戶端,並新增上一步生產的客戶端呼叫類檔案和配置檔案。專案目錄如下:

 

6. 在Client專案的Program.cs檔案中,呼叫服務。程式碼如下:

static void Main(string[] args)
        {
            CalculatorServiceClient calculator = new CalculatorServiceClient();
            MyMessage request = new MyMessage("+","Olive", 1.2, 3.4, 0);
            MyMessage respons = ((ICalculatorService)calculator).Calculator(request);
            Console.WriteLine("N1={0},N2={1},Operation is:{2},Result={3}", respons.N1, respons.N2, respons.Operation,respons.Result);
            Console.WriteLine("Calculator is Over!");
            Console.Read();
        }

7. 演示結果如下:

 

總結:該例主要用來演示如何建立訊息協定,並呼叫訊息服務。

7.2 在訊息協定內部使用自定義型別

每個單獨的訊息頭和訊息正文部分均使用為訊息所使用的服務協定選擇的序列化引擎進行序列化,預設的序列化引擎XmlFormatter可以顯示的處理或隱式出來具有訊息協定的任何型別。

此外,還可以在訊息協定內部使用陣列,可以通過直接在陣列上使用MessageHeaderAttribute或MessageBodyMemberAttribute來修飾陣列型別的欄位或屬性等,當然也可以通過MessageHeaderArrayAttribute來修飾。

示例如下:

[MessageContract]
Public class MyMessage
{
[MessageHeader]
Public int numRecords;
[MessageHeader]
Public int[] Counts;
MessageHeaderArray]
Public int [] Records;
}

這兩種不同的修飾方法,產生的結果如下:

<MyMessage>
<numRecords>116</numRecords>
<Counts>
<int>1</int>
<int>2</int>
</Counts>
<Records>3</Records>
<Record>4</Records>
</MyMessage>

 通過生成的結果可知,使用MessageHeaderAttribute屬性修飾的欄位比通過MessageHeaderArrayAttribte屬性修飾的欄位多了一個訊息信封<Counts>但是這兩種修飾方式產生的效果是一樣的。

演示示例:

這個演示示例我們直接在上一個示例中新增一些東西就可以了。

  1. 在上一個示例中的ICalculatorService.cs檔案中新增如下資料協定
[DataContract]
    public class ExtType
    {
        public ExtType() { }
        public ExtType(string _name1,string _name2)
        {
            this.name1=_name1;
            this.name2=_name2;
        }
        private string name1;
        private string name2;
        [DataMember]
      public string Name1
        {
            get{ return name1;}
            set{name1=value;}
        }
        [DataMember]
        public string Name2
        {
            get{return name2;}
            set{name2=value;}
        }

2. 然後在該檔案的訊息協定類MyMessage中新增如下屬性:

[MessageBodyMember]
        public ExtType ExtType
        {
            get { return et; }
            set { et = value; }
        }

3. 重新生成釋出,然後又Svcutil.exe工具重新生成客戶端類和配置檔案,並在客戶端專案中將原有的Client.cs檔案和App.config檔案刪除,新增剛剛生成的新的檔案。

4. 在客戶端專案中的Program.cs新增如下程式碼:

static void Main(string[] args)
        {
            CalculatorServiceClient calculator = new CalculatorServiceClient();
            MyMessage request = new MyMessage();
            request.N1 = 1.2;
            request.N2 = 3.4;
            request.Result = 0;
            request.Operation = "+";
            request.NewString = "Spartacus";
            Message1.ExtType et = new Message1.ExtType();
            et.Name2 = "Only";
            et.Name1 = "For";
            request.ExtType = et;
            MyMessage respons = ((ICalculatorService)calculator).Calculate(request);
            Console.WriteLine("N1={0},N2={1},Operation is:{2},Result={3}", respons.N1, respons.N2, respons.Operation, respons.Result);
            Console.WriteLine("The ExtType's Name1 is : {0} ,and Name2 is : {1}", respons.ExtType.Name1, respons.ExtType.Name2);
            Console.WriteLine("Calculator is Over!");
            Console.Read();
        }

 5. 編譯執行,結果如下:

7.3           對訊息部分進行簽名和加密

訊息協定可以通過在MessageHeaderAttribute和MessageBodyMemberAttribute屬性上設定MessageContractMemberAttribute.ProtectionLevel屬性的不同的值,來指示訊息頭或正文是否應該進行數字簽名和加密。

System.Net.Security.ProtectionLevel有三個列舉值,分別為:

None:不加密或簽名

Sign:僅數字簽名

EncryptAndSign:加密並數字簽名

除了通過設定ProtectionLevel屬性值來進行簽名和加密以外,還有以下幾點需要注意:

  1. 必須正確配置繫結和行為,如果沒有正確配置就使用這些安全功能,則驗證時會引發異常。
  2. 對於訊息頭會分別為每個訊息頭確定其保護級別。
  3. 物件訊息正文部分,保護級別為“最低保護級別”,正文的保護級別由所有正文的最高ProtectionLevel屬性值確定,在設定時應設定為最低保護級別。

演示示例:

[MessageContract]
Public class PatientRecord
{
[MessageHeader(ProtectionLevel=None)]
Public int recordID;
[MessageHeader(ProtectionLevel=Sign)]
Public string patientName;
[MessageBodyMember(ProtectionLevel=None)]
Public string comments;
[MessageBodyMember(ProtectionLevel=EncryptAndSign)]
Public string SSN;
}

註釋:

上邊的示例程式碼中,我們可以得知,recordID標頭未受保護,patientName標頭未signed,因為訊息正文部分SSN已經應用EncryptAndSign,所以所有的訊息正文部分都進行了加密和簽名。

7.4           控制標頭和正文部分的名稱和名稱空間

在訊息協定的SOAP表示形式中,每個標頭和正文部分都對映為一個具有名稱和名稱空間的Xml元素,如果想修改預設的名稱可以通過System.ServiceModel.MessageContractMemberAttribute.Name和NameSpace屬性實現。

示例如下:

[MessageContract]
Public class BankingTransaction
{
[MessageHeader(NameSpace=”http://www.cnblogs.com/Olive116”)]
Public bool IsGoodMan;
[MessageBodyMember(Name=”Author”)]
Public string writer;
}

註釋:IsGoodMan 標頭位於程式碼中指定的名稱空間中,正文部分writer由名為Author的Xml元素表示。

這裡的Name和NameSpace屬性和資料協定中的Name和Namespace屬性有些相似的地方。

7.5 控制是否包裝SOAP正文部分

預設的情況下,SOAP的正文部分會在包裝元素內部進行序列化,如果想要取消包裝,可以通過將IsWrapped屬性設定為false來實現,若要控制包裝元素的名稱和名稱空間,可以使用WrapperName和WrapperNamespace屬性來實現。

7.6           SOAP標頭屬性

SOAP標準定義了下列可能存在於標頭上的屬性:

l  Actor/Role(SOAP 1.1中為Actor,SOAP 1.2中為Role)

--指定使用給定標頭節點的統一資源識別符號(URL)

l  MustUnderstand

--指定處理標頭的節點是否必須理解該標頭

l  Relay

--指定要將標頭中繼到下游節點

除了MustUnderstand節點以外,WCF不會對傳入訊息的這些屬性作任何的處理。

SOAP標頭屬性的設定有以下三種方式:

  1. 通過靜態方式設定標頭屬性

示例如下:

[MessageContract]
Public class BankingTransaction
{
[MessageHeader(Actor=”Http://www.cnblogs.com/Olive116”,MustUnderstand=true)]
Public bool IsAudited;
}

 2. 通過動態方式設定標頭屬性

[MessageContract]
Public class BankingTransaction
{
[MessageHeader]
Public bool IsAudited;
}

 ……

……

BankingTransaction bt=new BankingTransaction();

bt.IsAudited=true;

bt.IsAudited.Actor=”Http://www.cnblogs.com/Olive116”;

bt.IsAudited.MustUnderstand=true;

3. 通過動靜態結合的方式設定標頭屬性,此時,標頭屬性預設為靜態設定的值,但是可以在以後使用動態機制進行重寫.

示例如下:

[MessageContract]
Public class BankingTransaction
{
[MessageHeader(MustUnderstand=false)]
Public bool IsAudited;
}
……

……
BankingTransaction bt=new BankingTransaction();
bt.IsAudited=true;
bt.IsAudited.Actor=”Http://www.cnblogs.com/Olive116”;
bt.IsAudited.MustUnderstand=true;

7.7           SOAP正文部分順序

預設情況下SOAP正文采用字母順序,但是可以通過System.ServiceModel.MessageBodyMemberAttribute.Order屬性進行設定,

這裡需要特別注意的是以資料協定不同,在訊息協定中,基型別正文成員不排列在派生類正文成員之前。

示例如下:

[MessageContract]
Public class BankingTransaction
{
[MessageHeader]
Public bool IsAudited;
[MessageBodyMember(Order=1)]
Public string MemberOrder1;
[MessageBodyMember(Order=2)]
Public string MemberOrder2;
[MessageBodyMember(Order=3)]
Public string MemberOrder3;
}

生成訊息正文的順序分別為MemberOrder1,MemberOrder2,MemberOrder3.

7.8           訊息協定版本管理

應用程式的新版本可能會向訊息中新增額外的標頭,在新版本應用程式向舊版本應用程式傳送訊息時,系統必須處理額外的標頭,同樣反向操作時系統必須處理缺少的標頭。

標頭版本管理的一些規則:

l  WCF不反對缺少標頭,相應的成員將保留其預設值

l  WCF會忽略意外的額外標頭,但是額外標頭中如果有MustUnderstand屬性且值為true,在這種情況下,由於存在無法處理但必須理解的標頭,因此會引發異常。

訊息正文的版本管理規則跟標頭的版本管理規則大體上是類似的,即忽略缺少和附加的訊息正文部分。

此外還有一些訊息的效能注意事項,一般來講每個訊息頭和正文部分相互獨立的進行序列化,因此可以為每個標頭和正文重新宣告相同的名稱空間,為提高效能,可以將多個標頭和正文部分合併成一個標頭或正文部分。

示例如下:

[MessageContract]
Public class BankingTransaction
{
[MessageHeader]
Public Operation operation;
[MessageBodyMember]
Public string BodyMember1;
[MessageBodyMember]
Public string BodyMember2;
[MessageBodyMember]
Public string BodyMember3;
}

 這樣的效能是很差的,我們可以對此進行稍加修飾對訊息正文進行合併,如下:

[MessageContract]
Public class BankingTransaction
{
[MessageHeader]
Public Operation operation;
[MessageBodyMember]
Public BodyMemberDetails details;
}
[DataContract]
Public class BodyMemberDetails
{
[DataMember]
Public string BodyMember1;
[DataMember]
Public string BodyMember2;
[DataMember]
Public string BodyMember3;
}