上期(2008 年 4 月)「服務站」介紹了建立簡單路由器,實現呼叫端與目標服務間訊息透明流動,回顧了 WCF 定址與訊息篩選語義,說明如何設計處理非類型化訊息的路由器約定,以及配置繫結與行為讓訊息繞經路由器傳遞。本期將繼續深入,探討採用更實用路由器方案所涉及的實作細節。
傳遞式路由器插入客戶端與服務之間時,客戶端關係對象是目標服務而非路由器。雖必須使用路由器支援的傳輸協定與訊息編碼器,但訊息完整內容(含安全標頭、可靠會話等)不由路由器處理。適用情境包含負載平衡、基於內容的路由、訊息傳輸。
路由器接收的訊息有兩個定址標頭可用於轉發:
許多情況下 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)) { ... }
設計依賴關係:
最後一點關鍵:若啟用安全會話或可靠會話,實際應用服務訊息前會先發送建立會話的協定訊息,這些訊息使用各自協定的 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");
此法允許路由器將安全會話或可靠會話相關訊息轉發至對應服務端點。
路由器公開服務端點供服務上線/離線時註冊/註銷,而非硬編碼端點。支援此模型需:

圖 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();
使用傳遞式路由器時,客戶端與路由器、路由器與服務需使用相容傳輸協定與編碼。雙向均為 HTTP 時前述功能正常運作。引入 TCP 等傳輸會話時出現挑戰:
解決方案:實作支援會話並依賴雙工輸入/輸出通道的路由器。呼叫端與應用服務無需知曉此實作細節。

圖 7:雙工路由器架構
雙工路由器約定異於傳統請求-回覆約定:
[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 轉發至服務。啟用安全/可靠會話時,即使雙工路由器配置也不足。關鍵點:
結論:混合協定僅適用有限情境,除非深入通道層覆寫預設行為。禁用安全/可靠會話時,客戶端可用 HTTP→路由器,路由器用 TCP→服務。啟用時,客戶端必須用 TCP→路由器,以在不啟用路由器通道會話情況下建立會話。