基于SOAP消息格式的WCF之所以強大原因之一是因為SOAP消息頭的高度擴展性。相應的WS-*協議很多都體現在消息頭封裝的信息上,包括諸如尋址,需要調用方法名,維護Session的信息等等……
SOAP示例
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
下面就是很長很有內涵的消息頭
<s:Header>
<a:Action s:mustUnderstand="1" u:Id="_2" xmlns:u="http://docs.oasis-
open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
http://www.thatindigogirl.com/samples/2006/06/PhotoUploadContract/UploadPhoto
</a:Action>
<a:MessageID u:Id="_3" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-
200401-wss-wssecurity-utility-1.0.xsd">
urn:uuid:940d5687-fcb2-44b5-a696-cc7eba22524b</a:MessageID>
<a:ReplyTo u:Id="_4" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-
200401-wss-wssecurity-utility-1.0.xsd">
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1" u:Id="_5" xmlns:u="http://docs.oasis-
open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
http://localhost:3045/PhotoApplication/PhotoManagerService.svc/Soap12</a:To>
<o:Security .../>
</s:Header>
Body部分省略
<s:Body>........</s:Body>
</s:Envelope>
我們要清楚的是,SOAP協議規定的只是一種帶有固定格式的XML。規定了由消息頭和消息體兩部分組成,并且規定了相應的節點。
WCF中的REST是一種POX(plain old xml),換句話說就是沒有任何規定的xml,沒有強行規定消息頭,沒有消息體。甚至因為沒有規定,所以傳輸的格式用不用xml都不是大問題,Json也可以做為傳輸數據的封裝格式。
沒有消息頭,所以我們需要另外一種方式定位所需調用的方法,傳遞我們需要傳送的參數信息。而我們能依靠的,就是Http協議給我們提供的信息。
總的來說,基于SOAP消息的WCF是對于WS-*規范的一種實現。而REST方式的WCF和采用basicHttpBinding差不多,也就是什么都沒有,相應的規范則由我們自己定義和實現。
我們都知道(額,不知道的話以后再說)如果調用基于SOAP消息的WCF服務的客戶端也是WCF的話,那么客戶端就可以使用透明代理,在通過反射把對方法棧的調用攔截并轉換為相應的消息,發送給服務提供者。其中相應的調用方法信息則封裝在action屬性里。服務端通過消息過濾器查找到相應的信息,并實例化service對象(服務對象也有可能已經實例化好了,具體看你的InstanceContextMode),調用相應的方法。
為了方便演示,我直接通過VS自帶的模板新建了一個WCF Liberary項目用來說明上面描述的步驟。
圖一:Contract的定義
[ServiceContract] public interface IService1 { [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); }
圖2:根據Contract信息生成的元數據,包含相應的Action節點,用來告知客戶端如何序列化相應的SOAP消息頭:
<wsdl:portType name="IService1">
<wsdl:operation name="GetDataUsingDataContract">
<wsdl:input wsaw:Action="http://tempuri.org/IService1/GetDataUsingDataContract" message="tns:IService1_GetDataUsingDataContract_InputMessage"/>
<wsdl:output wsaw:Action="http://tempuri.org/IService1/GetDataUsingDataContractResponse" message="tns:IService1_GetDataUsingDataContract_OutputMessage"/>
</wsdl:operation>
</wsdl:portType>
圖3:在客戶端調用方法時,透明代理把對方法的調用攔截并轉換SOAP消息發送給服務端,下圖就是轉換后發出的SOAP:
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
注意下面這一行
<a:Action s:mustUnderstand="1">http://tempuri.org/IService1/GetDataUsingDataContract</a:Action>
<a:MessageID>urn:uuid:2b959c1a-a159-4d25-bbb0-9f644b92fa68</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
</s:Header>
<s:Body>
<GetDataUsingDataContract xmlns="http://tempuri.org/">
<composite xmlns:d4p1="http://schemas.datacontract.org/2004/07/WcfServiceLibrary1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:BoolValue>true</d4p1:BoolValue>
<d4p1:StringValue>df</d4p1:StringValue>
</composite>
</GetDataUsingDataContract>
</s:Body>
</s:Envelope>
現在問題出來了,如果我們不是通過相應的序列化器,而是選擇自己發送沒有相應消息頭的XML,比如:
<GetDataUsingDataContract xmlns="http://tempuri.org/">
<composite>
<d4p1:BoolValue>true</d4p1:BoolValue>
<d4p1:StringValue>df</d4p1:StringValue>
</composite>
</GetDataUsingDataContract>
或者干脆就是在瀏覽器輸入一個地址,當然也就沒有action,會出現什么問題?
我們最直接的反應應該是,服務端找不到相應的信息,報錯:
<Fault><Code><Value>Sender</Value><Subcode><Value>a:ActionNotSupported</Value></Subcode></Code><Reason><Text xml:lang="zh-CN">由于 ContractFilter 在 EndpointDispatcher 不匹配,因此 Action 為“”的消息無法在接收方處理。這可能是由于協定不匹配(發送方和接收方 Action 不匹配)或發送方和接收方綁定/安全不匹配。請檢查發送方和接收方是否具有相同的協定和綁定(包括安全要求,如 Message、Transport、None)。</Text></Reason></Fault>
因此我們的OperationContract特性需要修改一下,處理那些找不到調用信息的請求:
為了方便說明,因此我新寫了一個方法。
//把[OperationContract] 注釋掉,改成下面的寫法
[OperationContract(Action = "*", ReplyAction = "*")]
Message Resource(Message input);
這樣所有由客戶端發送過來,沒有被處理的消息都會被送到這里進行處理,因此我們就可以處理那些不帶消息頭的信息了。以Message類作為參數是為了方便使用Http中所包含的信息,因為可以方便的轉換為HttpRequestMessageProperty 類型。下面這個方法實現的簡單功能就是根據參數信息獲取相應的文件。比如訪問http://localhost:9527/Service?job ,獲取job.xml的信息,此時沒有消息頭并且也不再需要消息頭篩選:
public Message Resource(Message input) { HttpRequestMessageProperty httpRequestProperty = (HttpRequestMessageProperty)input.Properties[HttpRequestMessageProperty.Name]; string query = httpRequestProperty.QueryString; string fileName = string.Format("{0}.xml", query); switch (httpRequestProperty.Method) { case "GET": Message message = null; if (File.Exists(fileName)) { XmlDocument document = new XmlDocument(); document.Load(fileName); message = Message.CreateMessage( MessageVersion.None, "*", new XmlNodeReader(document.DocumentElement)); } else { message = Message.CreateMessage( MessageVersion.None, "*"); } return message; default: return Message.CreateMessage( MessageVersion.None, "*"); } }
相應的配置文件,不用多說了吧,既然采用的是REST,那么編碼方式肯定就是文本,傳輸方式肯定就是Http。注意MessageVersion = None意味著咱們不需要對輸入的數據的格式有要求
<services>
<service name="Service.SimpleServer">
<endpoint address="Service" binding="customBinding" bindingConfiguration="RESTPOXBinding"
contract="IService.ISimpleResource" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:9527/" />
</baseAddresses>
</host>
</service>
</services>
<bindings>
<customBinding>
<binding name="RESTPOXBinding">
<textMessageEncoding messageVersion="None"/>
<httpTransport/>
</binding>
</customBinding>
</bindings>
當然,這么寫是有點小麻煩,WCF的設計者肯定會考慮到這點,幫我們采用URI模板方式封裝好了,對比:
原始方式:
[OperationContract(Action = "*", ReplyAction = "*")]
Message Resource(Message input);
新的方式:
[OperationContract]
[WebGet(UriTemplate = “/{query}”, BodyStyle = WebMessageBodyStyle.Bare)]
Message Resource(string query);
這樣我們在實現方法的時候就不用再自己手動調用 HttpRequestMessageProperty 類提取信息,也不再判斷Method類型,因此重寫后的Resource方法看起來也清爽了很多。
public Message Resource(String query) { string fileName = string.Format("{0}.xml", query); Message message = null; if (File.Exists(fileName)) { XmlDocument document = new XmlDocument(); document.Load(fileName); message = Message.CreateMessage( MessageVersion.None, "*", new XmlNodeReader(document.DocumentElement)); } else { message = Message.CreateMessage( MessageVersion.None, "*"); } return message; }
同樣的,系統給了我們一個webHttpBinding套餐,也是對上面傳輸協議和編碼的封裝。
關于OperationContract 和 WebGet 特性的說明
這兩個特性主要是給Operation的描述添加了一些元數據信息,給一個Operation添加了這兩個特性以后,該Operation仍然可以通過SOAP消息的方式來訪問。
文章列表