首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

服務站:打造 WCF 路由器進階實作(第 2 部分)

     編輯:關於.NET

上期(2008 年 4 月)「服務站」介紹了建立簡單路由器,實現呼叫端與目標服務間訊息透明流動,回顧了 WCF 定址與訊息篩選語義,說明如何設計處理非類型化訊息的路由器約定,以及配置繫結與行為讓訊息繞經路由器傳遞。本期將繼續深入,探討採用更實用路由器方案所涉及的實作細節。

傳遞式路由器適用場景

傳遞式路由器插入客戶端與服務之間時,客戶端關係對象是目標服務而非路由器。雖必須使用路由器支援的傳輸協定與訊息編碼器,但訊息完整內容(含安全標頭、可靠會話等)不由路由器處理。適用情境包含負載平衡、基於內容的路由、訊息傳輸。

  • 負載平衡:雖硬體負載平衡器較佳,但 WCF 路由器在無昂貴設備、基礎設施不受控、需基於領域邏輯路由、或需輕量易配置方案時極具價值。可將訊息分發至同機多進程或跨機器分散的服務。
  • 核心功能需求:服務需向路由器註冊納入負載分配;路由器需識別服務類型與端點以正確轉發;需具備分配演算法(輪詢、優先權基礎路由)。
  • 基於內容的路由:依訊息標頭或本文內容分發。如持有效授權金鑰的客戶端訊息高優先級轉發至高效能伺服器池,試用授權則轉發至較小池。路由器需檢查每條訊息(標頭或本文)再決定轉發目標。

透過 Action 標頭轉發

路由器接收的訊息有兩個定址標頭可用於轉發:

  • To 標頭:指示端點名稱。若與目標服務匹配而非路由器,即指示服務端點 URL。
  • Action 標頭:指示訊息期望觸發的服務操作,本身可能非有效 URL。

許多情況下 To 標頭匹配路由器位址而非服務,此時 Action 標頭成為更可靠資訊來源。Action 標頭源自服務約定命名空間、約定名稱、操作名稱。假設不同服務類型不共享約定,此資訊足以唯一識別目標服務。路由器可依賴約定命名空間與服務端點的對應映射。

圖 1:將約定命名空間映射至服務端點

static public IDictionary<string, string> RegistrationList = new Dictionary<string, string>();
RegistrationList.Add("https://www.thatindigogirl.com/samples/2008/01/IServiceA", "ServiceA");
RegistrationList.Add("https://www.thatindigogirl.com/samples/2008/01/IServiceB", "ServiceB");

// 初始化通道
string contractNamespace = requestMessage.Headers.Action.Substring(0, requestMessage.Headers.Action.LastIndexOf("/"));
string configurationName = RouterService.RegistrationList[contractNamespace];
using (ChannelFactory<IRouterService> factory = new ChannelFactory<IRouterService>(configurationName)) { ... }

設計依賴關係:

  1. 約定映射至服務簡化配置、支援多路由器實例(類似資料庫)。
  2. 服務約定不可在多服務類型實作,除非訊息可由任一實作處理。
  3. 伺服器陣列中多服務實例,各端點配置應映射至虛擬位址,由物理負載平衡器分發。
  4. 除應用程式服務訊息外,不支援含 Action 標頭的訊息。

最後一點關鍵:若啟用安全會話或可靠會話,實際應用服務訊息前會先發送建立會話的協定訊息,這些訊息使用各自協定的 Action 標頭,獨立於應用服務。因此必須使用其他標頭代替 Action 標頭進行轉發。

使用自訂標頭轉發:支援安全/可靠會話

在應用程式服務端點配置指定自訂標頭,確保每條訊息(含建立會話訊息)皆包含路由資訊:

<endpoint address="https://localhost:8010/RouterService" binding="wsHttpBinding" ... listenUri="ServiceA">
  <headers>
    <Route xmlns="https://www.thatindigogirl.com/samples/2008/01">
      https://www.thatindigogirl.com/samples/2008/01/IServiceA
    </Route>
  </headers>
</endpoint>

標頭包含名稱、命名空間、值。此標頭指定為端點配置一部分,故包含於服務中繼資料,客戶端產生 Proxy 時同步產生含標頭的配置。路由器可依名稱與命名空間擷取標頭值:

string contractNamespace = requestMessage.Headers.GetHeader<string>("Route", "https://www.thatindigogirl.com/samples/2008/01");

此法允許路由器將安全會話或可靠會話相關訊息轉發至對應服務端點。

動態服務註冊:減少配置負擔

路由器公開服務端點供服務上線/離線時註冊/註銷,而非硬編碼端點。支援此模型需:

  1. 實作路由器註冊約定,向防火牆後應用服務公開端點。
  2. 維護路由器註冊清單。
  3. ServiceHost 初始化後向路由器註冊端點。
  4. ServiceHost 錯誤或關閉時向路由器註銷端點。

圖 2:向路由器註冊服務流程

圖 5:透過負載平衡路由器使用動態連接埠註冊服務

註冊約定與資料約定:

[ServiceContract(Namespace = "...")]
public interface IRegistrationService {
    [OperationContract] void Register(RegistrationInfo regInfo);
    [OperationContract] void Unregister(RegistrationInfo regInfo);
}
[DataContract(Namespace = "...")]
public class RegistrationInfo {
    [DataMember(IsRequired=true, Order=1)] public string Address { get; set; }
    [DataMember(IsRequired=true, Order=2)] public string ContractName { get; set; }
    [DataMember(IsRequired=true, Order=3)] public string ContractNamespace { get; set; }
    public override int GetHashCode() { ... }
}

為支援同一約定多服務實例分發,路由器使用唯一鍵值(RegistrationInfo 雜湊碼)儲存多條目。接收訊息時,收集約定命名空間查找字典匹配項,若存在多個,使用選擇條件(如輪詢)決定轉發目標。

同機多服務實例需動態配置連接埠。TCP 服務可在端點配置設 listenUriMode="Unique";命名管道與 HTTP 需在程式碼中顯式指定基底位址或端點位址確保唯一連接埠。

檢查訊息內容:基於內容的路由與驗證

路由器通常轉發原始訊息,但可能依內容執行:檢查標頭/本文進行基於內容路由,或依有效性拒絕訊息。

檢查標頭簡單,Message 類型公開 Headers 屬性可直接擷取。若訊息使用訊息約定在標頭傳遞 LicenseKey:

string licenseKey = requestMessage.Headers.GetHeader<string>("LicenseKey", "...");

若 LicenseKey 在訊息本文,需讀取本文。GetReaderAtBodyContents() 回傳 XmlDictionaryReader。注意:讀取本文會將訊息狀態從 Created 變為 Read,讀取後無法轉發(訊息只能讀/寫/複製一次)。

基於內容路由器實作應先複製訊息至緩衝區:

MessageBuffer messageBuffer = requestMessage.CreateBufferedCopy(int.MaxValue);
Message messageCopy = messageBuffer.CreateMessage();
XmlDictionaryReader bodyReader = messageCopy.GetReaderAtBodyContents();
XmlDocument doc = new XmlDocument(); doc.Load(bodyReader);
XmlNodeList elements = doc.GetElementsByTagName("LicenseKey");
string licenseKey = elements[0].InnerText;
// 再次使用同一緩衝區建立轉發用訊息
Message forwardMessage = messageBuffer.CreateMessage();

路由器與傳輸會話:TCP 雙工路由器實作

使用傳遞式路由器時,客戶端與路由器、路由器與服務需使用相容傳輸協定與編碼。雙向均為 HTTP 時前述功能正常運作。引入 TCP 等傳輸會話時出現挑戰:

  • 啟用安全性:路由器必須提供已簽署的 To 標頭。預設路由器轉發時修改 To 標頭匹配服務位址,除非啟用手動定址。但 TCP 基於請求-回覆約定時不允許手動定址。
  • 啟用可靠會話:透過路由器發回非同步確認,要求路由器維持與服務會話接收確認,客戶端須維持與路由器雙工會話接收相同確認。

解決方案:實作支援會話並依賴雙工輸入/輸出通道的路由器。呼叫端與應用服務無需知曉此實作細節。

雙工路由器約定與架構

圖 7:雙工路由器架構

雙工路由器約定異於傳統請求-回覆約定:

  1. ProcessMessage 為單向操作。
  2. 服務約定需求會話並包含回呼約定(不要求客戶端實作回呼,在路由器內部執行)。
  3. 回呼約定有一單向方法接收從路由器呼叫服務的回應。服務不知其回應將發送至回呼通道,它們可為請求-回覆訊息。
[ServiceContract(Namespace = "...", SessionMode = SessionMode.Required, CallbackContract = typeof(IDuplexRouterCallback))]
public interface IDuplexRouterService {
    [OperationContract(IsOneWay=true, Action = "*")] void ProcessMessage(Message requestMessage);
}
[ServiceContract(Namespace = "...", SessionMode = SessionMode.Allowed)]
public interface IDuplexRouterCallback {
    [OperationContract(IsOneWay=true, Action = "*")] void ProcessMessage(Message requestMessage);
}

架構流程:客戶端發送請求等待同步回應 → 路由器接收單向請求並保存客戶端回呼通道 → 路由器用雙工通道轉發訊息並提供回呼通道接收服務回應 → 服務接收請求發送同步回應 → 路由器回呼通道接收回應 → 使用客戶端回呼通道發回客戶端。全程操作同步,路由器在基本收發通道中用雙工通訊關聯訊息。

雙工路由器實作核心碼

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple, AddressFilterMode=AddressFilterMode.Any, ValidateMustUnderstand=false)]
public class DuplexRouterService : IDuplexRouterService, IDisposable {
    object m_duplexSessionLock = new object();
    IDuplexRouterService m_duplexSession;
    public void ProcessMessage(Message requestMessage) {
        lock (this.m_duplexSessionLock) {
            if (this.m_duplexSession == null) {
                IDuplexRouterCallback callback = OperationContext.Current.GetCallbackChannel<IDuplexRouterCallback>();
                DuplexChannelFactory<IDuplexRouterService> factory = new DuplexChannelFactory<IDuplexRouterService>(
                    new InstanceContext(null, new DuplexRouterCallback(callback)), "serviceEndpoint");
                factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
                this.m_duplexSession = factory.CreateChannel();
            }
        }
        this.m_duplexSession.ProcessMessage(requestMessage);
    }
    public void Dispose() { /* 關閉通道 */ }
}
public class DuplexRouterCallback : IDuplexRouterCallback {
    private IDuplexRouterCallback m_clientCallback;
    public DuplexRouterCallback(IDuplexRouterCallback clientCallback) { m_clientCallback = clientCallback; }
    public void ProcessMessage(Message requestMessage) { this.m_clientCallback.ProcessMessage(requestMessage); }
}

回呼物件實作回呼約定,接收服務回應並用客戶端回呼通道發回。路由器服務實例、客戶端/路由器回呼通道引用在會話期間持續存在。故路由器必須公開支援會話的端點,下游服務必須支援會話。

混合傳輸協定的限制

客戶端透過 HTTP 發送至路由器,路由器透過 TCP 轉發至服務。啟用安全/可靠會話時,即使雙工路由器配置也不足。關鍵點:

  • 僅請求-回覆通道支援手動定址。TCP 無原生請求-回覆支援,故手動定址不可行(除非約定為單向)。發送通道必須基於單向約定(如 IDuplexRouterService)建立,提供回呼通道接收回應。
  • 路由器回呼通道發送回應前必須保持活躍,客戶端回呼通道亦同。客戶端必須與路由器建立會話,路由器必須與服務建立會話。
  • 若應用服務需安全,轉發未經處理訊息可能需手動定址。路由器用 TCP 呼叫服務需雙工路由器實作(輸出為單向通道),強制客戶端用會話感知繫結(啟用 HTTP 安全/可靠會話)。
  • 傳遞式路由器重點讓應用服務處理安全/可靠會話標頭。若路由器需與客戶端建立 HTTP 會話,則路由器處理這些標頭,不與應用服務建立會話。

結論:混合協定僅適用有限情境,除非深入通道層覆寫預設行為。禁用安全/可靠會話時,客戶端可用 HTTP→路由器,路由器用 TCP→服務。啟用時,客戶端必須用 TCP→路由器,以在不啟用路由器通道會話情況下建立會話。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved