(本文原作于2006.03.15,第一次修正于2006.06.06,修正後适用于ESFramework V0.3+)
分布式系統的建構一般有兩種模式,一是基于消息(如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
/// ResponseTTL 如果一個回複在管理器中存在的時間超過ResponseTTL,則會被删除。如果ResponseTTL為0,則表示不進行生存期管理
/// </summary>
int ResponseTTL{set ;} //s
/// 如果在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() ;
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”(用戶端插件)。如果我們的應用已經釋出投入使用,而此時使用者要求添加一項新的業務,那将是非常簡單的事情,那就是開發一個實作了新業務的功能插件動态加載到功能伺服器中、再開發一個對應的用戶端插件動态加載到用戶端中,這樣就可以了。伺服器不用重編譯、甚至不用停止服務;用戶端也不用重編譯、甚至不用停止使用。一切都是在運作中動态完成的。
這是如何做到的?請關注本系列文章。