托管和使用 Windows® Communication Foundation (WCF) 服務通常經歷幾個基本步驟:實現服務 、配置可以訪問服務的端點、托管服務、生成 Web 服務描述語言 (WSDL) 文件或啟用元數據交換,以便 客戶端能夠生成代理以調用服務、編寫代碼以使用其相關配置實例化代理、以及啟動調用服務操作。您基 本不需要研究它的內部原理,但即使是在最簡單的情況下,客戶端和服務通道也要依賴兼容配置來處理尋 址語義和消息篩選,以確保調用了正確的操作。
有時,在客戶端和目標服務之間引入中介服務或路由器服務對接收在它們之間傳輸的消息或執行其他 活動(如日志記錄、優先級路由、聯機/脫機路由、負載平衡)非常有用,引入安全邊界也同樣有用處。 當引入此類中介服務時,需要對一些尋址和消息篩選行為做出相應調整。
因此,讓我們深入了解 一下如何使用中介服務,為簡單起見,我將它們統稱為路由器。在本期文章中,我將介紹 WCF 尋址和消 息篩選的概念,並重點講解路由器方案,此外我還將介紹一些適用於路由配置以及相應設置的選項。在本 系列文章的第 2 部分中,我將展示如何利用該基本原理實現更高級、更實用的路由功能。
默認尋 址語義
在 2007 年 6 月的“服務站”專欄中 (msdn.microsoft.com/msdnmag/issues/07/06/ServiceStation),Aaron Skonnard 介紹了 WCF 如何處理 邏輯和物理端點尋址、尋址標頭以及消息篩選。在本節中,我將回顧其中的一些基本尋址功能以及它們如 何影響路由方案—但您也會發現:Aaron 的專欄對於了解這些 WCF 功能的其他深層次細節非常有用 。
通常,客戶端使用從服務描述生成的代理將消息直接發送至目標服務。為了使客戶端與服務兼 容,他們共享等效約定和端點配置。看一下圖 1 中所示的服務約定和配置,您可以從中得出幾個重要的 服務尋址要求。
Figure 1 服務約定和端點配置
Service Contract [ServiceContract(Namespace = "http://www.thatindigogirl.com/samples/2008/01")] public interface IMessageManagerService { [OperationContract] string SendMessage(string msg); [OperationContract] void SendOneWayMessage(string msg); } Endpoint Configuration <system.serviceModel> <services> <service name="MessageManager.MessageManagerService" behaviorConfiguration="serviceBehavior"> <endpoint address="http://localhost:8000/MessageManagerService" contract="MessageManager.IMessageManagerService" binding="basicHttpBinding" /> </service> </services> </system.serviceModel>
首先,針對 SendMessage 操作請求預期的 "Action" 尋址標頭如下:
http://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessage
由於 OperationContractAttribute 沒有指定某個 "Action",因此該值從服務約定命 名空間、約定名稱(默認為接口名稱)和操作名稱(默認為方法名稱)派生而來。
第二, SendMessage 返回響應的預期 "Action" 標頭如下:
http://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessageRes ponse
由於 OperationContractAttribute 沒有指定 ReplyAction,因此該值的派生方法與 Action 屬性相同,附加後綴 "Response"。
最後,定位服務端點的消息的預期 "To" 標頭如下:
http://localhost:8000/MessageManagerService
該值從 端點元素的地址屬性派生而來,此元素被視為是端點的邏輯地址。盡管可以另外指定,但端點的物理地址 通常都是與邏輯地址相匹配的。這意味著客戶端通常將消息發送至與 "To" 標頭相匹配的物理地址。
服務元數據描述了這些要求,以便客戶端可以生成兼容的代理和配置。為客戶端生成的服 務約定反映了相同的 Action 和 ReplyAction 服務設置,而客戶端綁定配置反映出具有合適邏輯地址和物理地址的端點。例如,下列客戶端端點與圖 1 中的服務相兼容:
<client> <endpoint address="http://localhost:8000/MessageManagerService" binding="basicHttpBinding" contract="localhost.IMessageManagerService" name="basicHttp" /> </client>
客戶端代理將客戶端端點元素的地址屬性用作其邏輯地址和物理地址。因此, 正如我先前所述,消息發送至與 "To" 標頭相匹配的物理地址。代理調用 SendMessage 操作 時,將發送帶有 "To"(發送方)和 "Action"(操作)標頭的消息,如圖 2 所示。
Figure 2 從代理發出的消息
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none"> http://localhost:8000/MessageManagerService </To> <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none"> http://www.thatindigogirl.com/samples/2008/01 /IMessageManagerService/SendMessage </Action> </s:Header> <s:Body> <SendMessage xmlns="http://www.thatindigogirl.com/samples/2008/01"> <msg>test</msg> </SendMessage> </s:Body> </s:Envelope>
"To" 和 "Action" 標頭的組合分別指示服務模型 :通道調度程序應使用哪個通道處理消息、調用哪個操作。默認情況下,必須有一個端點的邏輯地址與 "To" 標頭匹配,一項操作與 "Action" 標頭相匹配。圖 3 說明了這一流程。
Figure 3 Typical Addressing without a Router
在接下來的內容中,我將介紹邏輯地址和物理地址、"To" 和 "Action" 標頭以及使用路由器時消息篩選規則的含義。
路由體系結構
盡管在路由器的設計方式上有一些變 化,但大部分路由器必須要能夠接收以任何服務為目標的消息,而且它們還必須能夠將原始消息轉發給相應的目標服務。設計路由器有以下兩種基本方法:透傳路由器或處理路由器。
透傳路由器對客戶 端是透明的。客戶端的關系屬於下游服務,但消息恰好是通過路由器傳遞的。客戶端必須使用兼容的傳輸 協議和消息編碼器將消息發送到路由器,但任何服務通道所需的安全性、可靠會話、應用程序會話或其他 消息傳遞協議都滿足客戶端生成消息的要求。路由器可以查看消息標頭甚至插入標頭,但原始消息元素仍會以原有方式轉發至服務。圖 4 闡明了這一關系。
Figure 4 Operation of a Pass-Through Router
處理路由器在為應用程序處理消息時發揮更為積極的作用。因此,盡管客戶端仍必須能夠發送與下游 服務兼容的消息,但就傳輸、編碼和協議的兼容性而言,客戶端是與路由器發生關系。消息通過路由器傳送到服務,消息正文及任何服務所需的標頭一起完整保留。
安全性、可靠會話以及與其他通信協 議相關的標頭或消息通常由路由器進行處理,且路由器可通過適當的通信協議為下游服務構建新消息。圖 5 說明了處理路由器的兼容性。
Figure 5 Operation of a Processing Router
每個路由配置都具有實用的執行方法。而且還可以按一定比例建立混合解決方案。
路由器約定
路由器將接收適用於下游服務的消息,並負責將這些消息轉發至相應的服務。路由器還負責接收 來自服務的響應並將這些響應返回客戶端。典型的路由器約定會顯示可以處理任何消息請求或響應的單個操作。在下面的示例中,該操作稱為 ProcessMessage:
[ServiceContract(Namespace = "http://www.thatindigogirl.com/samples/2008/01")] public interface IRouterService { [OperationContract(Action = "*", ReplyAction = "*")] Message ProcessMessage(Message requestMessage); }
正如我們在本專欄前面所述,通常情況下,OperationContractAttribute 的 Action 和 ReplyAction 屬性是從服務約定命名空間、約定名稱和操作名稱派生而來的。默認情況下,當消息到達時 ,通道調度程序必須找到與 Action 標頭完全匹配的操作。但是,如 Action 和 ReplyAction 被設置為 "*",則無論 Action 是何值,通道調度程度都將未映射為具體操作的所有消息發送給全能操作。而且為了避免混淆,只有一個操作可以為 Action 或 ReplyAction 屬性指定 "*"。
典型的路由器可提供類似 ProcessMessage 的單個操作,用於處理收到的任何消息。雖然 Action 和 ReplyAction 可確保通道調度程序將消息映射到 ProcessMessage,但操作簽名還必須能夠處理所有消息。
要解決這一問題,ProcessMessage 以 Message 類型的形式接收並返回非類型化消息。路由器 可以通過該類型訪問標頭集合和消息正文,但除常用尋址標頭(通過強類型化屬性執行反序列化並使其可用)之外不會進行任何自動序列化。
任何對消息的進一步處理都是通過路由器實現的。基本路由 器將只接收非類型化消息並將其按原樣轉發至下游服務,等待回答。同樣,回復將按原始的格式轉發至調用的客戶端。
轉發消息
路由器將接收消息並根據其自己的需求對該消息進行處理後,將消 息轉發至合適的下游服務以供進一步處理。圖 6 顯示了前文所述約定的一個簡單路由器執行方法。 ProcessMessage 通過 ChannelFactory<T> 構建客戶端通道(或代理)並使用該代理將消息轉發至特殊的服務端點,從而返回所有響應。
Figure 6 簡單路由器執行方法
[ServiceBehavior( InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] public class RouterService : IRouterService { public Message ProcessMessage(Message requestMessage) { using (ChannelFactory<IRouterService> factory = new ChannelFactory<IRouterService>("serviceEndpoint")) { IRouterService proxy = factory.CreateChannel(); using (proxy as IDisposable) { return proxy.ProcessMessage(requestMessage); } } } }
代理通常被強類型化為目標服務約定,但在此種情況下,代理應能夠轉發任何消息並接收全部 回復—一些便於路由器約定利用的內容。在這個簡單的示例中,路由器只將原始消息轉發至目標服務並返回全部響應。如果目標服務端的操作為單向操作,則將不會發送任何回復。
由於約定使用 非類型化消息,因此同樣的消息也將轉發至服務,如圖 7 所示。但您必須認識到,有一個對消息所做的更改可能是您沒有預料到的:在消息發送至服務之前,"To" 標頭發生了改變。
Figure 7 Addressing Semantics through a Simple Router
請注意,默認情況下,代理將使用其端點配置的邏輯地址來設置傳出消息的 "To" 標頭 —即使傳遞的原始 Message 實例已具有 "To" 標頭。雖然這看起來是一件好事— 因為所有服務都要求 "To" 標頭匹配它們其中一個服務端點的邏輯地址—但這會導致其他負面影響。例如,若更新的 "To" 標頭未簽名且服務已啟用安全性,則消息將被拒絕。
理想的情況是,客戶端應發送一條包含匹配目標服務的 "To" 標頭的消息,無論是否 匹配,路由器均應接收該消息,且路由器應在不改變 "To" 標頭的情況下將該消息轉發給服務。這可以通過綁定配置進行處理,我將在稍後討論這一配置。
邏輯尋址和物理尋址
將路由 器引入應用程序體系結構時,如果客戶端可以使用正確的服務 "To" 標頭發送消息,又能將此 消息發送至路由器,這應該是最理想的狀況。達此目的一種方法是將客戶端配置為使用 ClientViaBehavior,如圖 8 所示。這會令客戶端代理根據端點的邏輯地址生成包含 "To" 標頭的消息,但要通過路由器的物理地址發送該消息。問題是這會將客戶端與路由器的存在聯系在一起。
Figure 8 使用 ClientViaBehavior
<client> <endpoint address="http://localhost:8000/MessageManagerService" binding="wsHttpBinding" bindingConfiguration="wsHttpNoSecurity" contract="localhost.IMessageManagerService" name="basicHttp" behaviorConfiguration="viaBehavior"/> </client> <behaviors> <endpointBehaviors> <behavior name="viaBehavior"> <clientVia viaUri="http://localhost:8010/RouterService"/> </behavior> </endpointBehaviors> </behaviors>
解決此問題的另一個方法是讓服務為其端點配置 listenUri 屬性,以便服務與路由器使用同一邏輯地址,而將物理地址專用於服務。請看以下服務配置:
<endpoint address="http://localhost:8010/RouterService"
contract="MessageManager.IMessageManagerService"
binding="wsHttpBinding"
bindingConfiguration="wsHttpNoSecurity"
listenUri="http://localhost:8000/MessageManagerService"/>
所產生的服務元數據將路由器地址發布給客戶端,這樣客戶端端點就能反映出路由器地址。實際上, 我不太喜歡這一解決方案,因為它將服務與路由器聯系到了一起,而理想狀況是服務不需要了解這一內容。
備選方案是讓服務使用未綁定到路由器或服務的 URI 類型的邏輯地址,然後手動通知客戶端要接收消息的物理地址(因為它不是元數據的組成部分)。以下是這種端點配置的示例:
<endpoint address="urn:MessageManagerService"
contract="MessageManager.IMessageManagerService"
binding="wsHttpBinding"
bindingConfiguration="wsHttpNoSecurity"
listenUri="http://localhost:8000/MessageManagerService"/>
在任一種情況下,服務都會收到與其端點配置匹配的 "To" 標頭且路由器會先收到消息。
路由器著實應挑起配置的重任,讓客戶端和服務不受其存在狀態的約束。因此,"To" 標頭 絕對不能與路由器的邏輯地址匹配。默認情況下,服務使用 EndpointAddressMessageFilter 確定消息的 "To" 標頭是否與任何其配置的端點相匹配。由於路由器無法實現同樣的操作,因此應安裝 MatchAllMessageFilter。
ServiceBehaviorAttribute 通過 AddressFilterMode 屬性支持此操作,該 屬性可以設置為 AddressFilterMode 枚舉之一:Exact(默認)、Prefix 或 Any。由於無法保證路由器前綴匹配所有接收消息的服務,因此允許所有 "To" 標頭通過會很有幫助,如下所示:
[ServiceBehavior(InstanceContextMode =
InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
AddressFilterMode=AddressFilterMode.Any)]
public class RouterService : IRouterService
默認情況下,"To" 標頭將始終根據其端點配置更新,以匹配代理的邏輯地址,而不管 "To" 地址是否已設置為正確的值。為抑制這種行為以便路由器可以使用原始的 "To" 標頭將消息轉發至服務,路由器必須將綁定配置與手動尋址搭配使用。在任何標准綁定上均無法設置該屬性,因此您必須使用自定義綁定實現這一目的。
下列代碼段顯示了為 HTTP 傳輸通道設置這一功能的 customBinding 段落:
<customBinding>
<binding name="manualAddressing">
<textMessageEncoding />
<httpTransport manualAddressing="true"/>
</binding>
</customBinding>
這大大簡化了圖 9 中所示的尋址流程(標頭不改變)。
Figure 9 Addressing Semantics through a Router with Manual Addressing
MustUnderstand 標頭
到現在為止,我已重點介紹了簡單的路由實現以說明核心路由器的設計注意事項,它們還會對尋址、 篩選和綁定配置產生影響。這個簡單的路由解決方案僅在服務沒有為其綁定啟用安全性、可靠會話或任何其他豐富協議時起作用。圖 10 顯示了我為上述討論假設的綁定協議的簡化視圖。
Figure 10 Service Contract and Endpoint Configuration
圖 11 顯示了服務要求安全性和可靠會話時透傳路由器的相同視圖。啟用這些協議即意味著客戶端和 服務通道將交換其他消息以建立會話、請求安全令牌以及其他相關消息傳送。由於使用路由器可以透傳所有消息,因此這些特定於協議的消息也將會透傳到服務—這不失為一件好事。
Figure 11 Pass-Through Configuration with Security and Reliable Sessions
但是,如果服務發送至/接收自的消息包含接收方必須理解的標頭,問題就會隨之出現。由於透傳路由器並未特意啟用安全性或可靠會話,因此處理相關協議標頭時不會有那些的通道。
通過將 ServiceBehaviorAttribute 的 ValidateMustUnderstand 屬性設置為 false,您可以指示路由器服務忽略 MustUnderstand 標頭,如下所示:
[ServiceBehavior(InstanceContextMode =
InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
AddressFilterMode=AddressFilterMode.Any,
ValidateMustUnderstand=false)]
public class RouterService : IRouterService
這將確保來自客戶端的傳入消息不會出現問題,但從下游服務返回的消息不在其保證范圍之內。
要解決這一問題,為調用下游服務初始化通道工廠時,您還必須修改路由器執行方法以指定其行為,如下所示:
using (ChannelFactory<IRouterService> factory =
new ChannelFactory<IRouterService>("serviceEndpoint"))
{
factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
IRouterService proxy = factory.CreateChannel();
// remaining code
}
現在,協議和服務消息可通過路由器在客戶端與服務之間自由傳送—假設使用的協議是 HTTP。
如 使用了 TCP 這類雙向協議或命名管道,就會出現另一種復雜情況。這意味著服務可以向客戶端啟動消息 ,例如,當啟用可靠會話時。有一個高級路由器配置可以用於處理這一特例,我將在本系列的第 2 部分中介紹該情形及其實用性。
請將您想詢問的問題和提出的意見發送至 [email protected].