(本文原作於2006.03.15,第一次修正於2006.06.06,修正後適用於ESFramework V0.3+)
(本文是ESFramework對用戶端開發的支援特性之一 ,如果要按順序閱讀,請轉到ESFramework介紹(序))
分布式系統的構建一般有兩種模式,一是基於訊息(如Tcp,http等),一是基於方法調用(如RPC、WebService、Remoting)。深入想一想,它們其實是一回事。如果你瞭解過.NET的Proxy,那麼你會發現,方法調用和訊息請求/回複實際上是可以相互轉換的,.NET的Proxy的實現,就是在堆疊框架和訊息之間相互轉換的過程。關於這方面的詳細論述可以參見《.Net本質論》一書。
我覺得IServerAgent是我在開發ESFramework期間非常滿意的一個想法,相信大家也會對它感興趣的。因為它
使得使用基於訊息請求/回複的互動就像方法調用一樣簡單。
用戶端與伺服器之間的所有通訊都可經過IServerAgent,包括要轉寄的P2P訊息。它的主要目的是:
(1)屏蔽用戶端與服務端之間的通訊協定(Tcp/Udp),ITcpServerAgent、IUdpServerAgent
(2)可將非同步訊息請求/回複轉化為同步的方法調用。
ESFramework主要支援基於Tcp或Udp的C/S系統,所以用戶端和服務端之間是通過訊息進行互動的。如果僅僅是用戶端發出請求、伺服器給出服務這種情況很容易處理,但是如果服務端有主動發訊息給用戶端的情況,事情就會變得稍微複雜。通常,用戶端會有一個專門的接收線程來負責從網路接收資料,然後把接收的訊息交給對應的處理器處理,或者,這個接收到的訊息是個服務端給出的回複,那麼這個回複就應該交給發出請求的要求者,但是對應的要求者在哪裡了?這種回複訊息與請求訊息的匹配是比較繁瑣的,特別是在上述服務端可以主動給用戶端發送訊息的情況下。為了簡化這個過程,IServerAgent出現了,它用於用戶端,像它的名字一樣,可以把它當作伺服器。IServerAgent的主要目的就是將訊息請求/回複轉換成方法調用,就像該介面定義的一樣:
public interface IServerAgent
{
/// <summary>
/// 如果逾時仍然沒有回複,則拋出逾時異常
/// 如果dataPriority != DataPriority.CanBeDiscarded ,則checkRespond只能為false
/// </summary>
NetMessage CommitRequest(NetMessage requestMsg ,DataPriority dataPriority , bool checkRespond);
}
public enum DataPriority
{
High ,//緊急命令
Common ,//如普通訊息,如聊天訊息
Low ,//如檔案傳輸
CanBeDiscarded //如視頻資料、音頻資料
}
首先解釋一下參數dataPriority的意義,dataPriority參數僅僅對Tcp協議起作用,當有多個請求要同時發送時,它決定了發送的優先順序。CanBeDiscarded表明這個訊息在網路繁忙時可以被拋棄,比如即時通訊的音頻資料、視頻資料等。關於這個資料發送的優先順序機制的實現是ITcpAutoSender,這個組件會在後文中介紹。
CommitRequest方法提交一個請求訊息該給伺服器,並返回一個回複訊息給要求者。這就是一個方法調用!!!其間隱藏了通過網路將訊息發送給伺服器並從伺服器擷取結果的中間細節。這是怎麼做到的?思路其實很簡單,只是描述起來有些複雜。主要要解決兩個問題,一是如何將請求訊息與對應的回複匹配起來,二是CommitRequest從哪裡找到匹配的回複。
對於第一個問題,相信大家還記得IMessageHeader定義中有個CorrelationID屬性,正如其名,這是一個隨機數,每產生一個新的請求訊息,就會產生一個隨機數賦值給CorrelationID屬性,由於隨機數重複的可能性很小,所以可以把它當作是唯一的。這樣一個隨機數就唯一的標誌了一個請求,當服務端收到這個請求後,就處理這個請求,並把回複訊息的訊息頭中的CorrelationID屬性設為與對應的請求訊息的CorrelationID一樣的值,這樣,用戶端收到回複訊息後,就可以和對應的請求訊息一一對應起來了。
對於第二個問題的解釋,就需要涉及到ESFramework中支援用戶端開發的其它兩個組件:EsbPassiveDataDealer和IResponseManager。EsbPassiveDataDealer是用戶端使用者處理所有接收到的訊息的處理器,而IResponseManager組件用於暫存所有的來自服務端的回複。對於每個接收到的訊息,EsbPassiveDataDealer判斷其是否為回複,如果是,則將其交給IResponseManager暫存。IResponseManager為暫存的每個回複都設定的生存期TTL,如果回複在IResponseManager中的時間超過了這個TTL,則會被刪除。
你也許已經想到第二個問題的解決方案了。是的,CommitRequest方法將請求發送到網路之後,就定時從IResponseManager中尋找CorrelationID為請求訊息頭的CorrelationID值的回複訊息,如果找到,就返回它,否則就等待迴圈,直至逾時拋出TimeoutException異常。下面給出IResponseManager的介面定義:
public interface IResponseManager
{
void Initialize() ;
void PushResponse(NetMessage response) ;
NetMessage PopRespose(int correlationID ,int serviceKey) ; //立即返回
NetMessage PickupResponse(int serviceKey ,int corelationID) ;//在TimeoutSec時間內不斷的PopRespose
/// <summary>
/// ResponseTTL 如果一個回複在管理器中存在的時間超過ResponseTTL,則會被刪除。如果ResponseTTL為0,則表示不進行生存期管理
/// </summary>
int ResponseTTL{set ;} //s
/// <summary>
/// 如果在TimeoutSec內,仍然接收不到期望的回複,則拋出異常。取0時,表示不設定逾時
/// </summary>
int TimeoutSec{set ; }
}
IServerAgent的具體實現包括TcpServerAgent和UdpServerAgent,分別支援Tcp協議和Udp協議的用戶端開發。從它們的介面定義中可以看到它們都藉助於IServerAgentHelper實現自己。
public interface IServerAgentHelper
{
IEsbLogger EsbLogger{set ; get ;}
IContractHelper ContractHelper{set ; get ;}
INetMessageHook NetMessageHook {set ; get ;}
IPassiveHelper PassiveHelper {set ; get ;}
IResponseManager ResponseManager{set ;get ;}
ISingleMessageDealer SingleMessageDealer{set ; get ;}
IMessageDispatcher ConstructDispatcher() ;
}
我們要特別注意其ConstructDispatcher方法,該方法構建了一個用戶端比較常用的訊息分配器執行個體。在介紹IMessageDispatcher時,我們講過,用戶端通常不需要對訊息Spy,僅僅需要Hook就可以了,所以IServerAgentHelper正是通過對各組件的組裝做到了這一點:
public IMessageDispatcher ConstructDispatcher()
{
//NakeDispatcher
EsbPassiveDataDealer dealer = new EsbPassiveDataDealer(this.responseManager ,this.passiveHelper ,this.singleMessageDealer) ;
EsbPassiveDealerFactory factory = new EsbPassiveDealerFactory(dealer) ;
NakeDispatcher nakeDispatcher = new NakeDispatcher() ;
nakeDispatcher.ContractHelper = this.contractHelper ;
nakeDispatcher.DataDealerFactory= factory ;
//MessageDispatcher
IMessageDispatcher messageDispatcher = new MessageDispatcher() ;
messageDispatcher.ContractHelper = this.contractHelper ;
messageDispatcher.NetMessageHook = this.netMessageHook ;
messageDispatcher.NakeDispatcher = nakeDispatcher ;
return messageDispatcher ;
}
在IServerAgent的基礎之上,我們就可以從一個新的角度來設計用戶端的結構的,那就是採用和功能伺服器一樣的外掛程式方式。在ESFramework的支援下,我們的應用開發變得非常簡潔和簡單,所要做的主要內容就是開發服務端的“業務功能外掛程式”和對應的用戶端的“PassiveAddin”(用戶端外掛程式)。如果我們的應用已經發布投入使用,而此時使用者要求添加一項新的業務,那將是非常簡單的事情,那就是開發一個實現了新業務的功能外掛程式動態載入到功能伺服器中、再開發一個對應的用戶端外掛程式動態載入到用戶端中,這樣就可以了。伺服器不用重編譯、甚至不用停止服務;用戶端也不用重編譯、甚至不用停止使用。一切都是在運行中動態完成的。
這是如何做到的?請關注本系列文章。
轉到 :ESFramework 可複用的通訊架構(序)