遠端方法回撥(.Net Remoting學習四)
Remoting中的方法回撥
1. 遠端回撥方式說明
遠端方法回撥通常有兩種方式:
- 客戶端也存在繼承自MarshalByValueObject的型別,並將該型別的例項作為引數傳遞給了遠端物件的方法,然後遠端物件在其方法中通過該型別例項的引用對它進行呼叫(訪問其屬性或者方法)。記得繼承自MarshalByValueObject的型別例項永遠不會離開自己的應用程式域,所以相當於服務端物件呼叫了客戶端物件。
- 客戶端物件註冊了遠端物件釋出的事件,遠端物件通過委託呼叫客戶端註冊了的方法。
當服務端呼叫客戶端的方法時,它們的角色就互換了。此時,需要注意這樣幾個問題:
- 因為不能通過物件引用訪問靜態方法(屬性),所以無法對靜態方法(屬性)進行回撥。
- 由於服務端在執行時需要訪問客戶端物件,此時它們的角色互換,需要在服務端建立對客戶端物件的代理,所以服務端也需要客戶端物件的型別元資料。因此,最好將客戶端需要回調的方法,抽象在一個物件中,服務端只需引用含有這個物件的程式集就可以了。而如果直接寫在Program中,服務端還需要引用整個客戶端。
- 由於將客戶端進行回撥的邏輯抽象成為了一個獨立的物件,此時客戶端的構成就類似於前面所講述的服務端。它包含兩部分:(1)客戶端物件,用於支援服務端的方法回撥,以及其它的業務邏輯;(2)客戶端控制檯應用程式(也可以是其它型別程式),它僅僅是註冊通道、註冊埠、註冊遠端物件,提供一個客戶端物件的執行環境。
根據這三點的變化,我們可以看出:客戶端含有客戶端物件,但它還需要遠端服務物件的元資料來構建代理;服務端含有服務物件,但它還需要客戶端物件的元資料來構建代理。因此,客戶端服務端均需要服務物件、客戶物件的型別元資料
其中ShareAssembly.dll包含服務物件和客戶端物件的程式碼。接下來一節我們來看一下它們的程式碼。
2.客戶端和服務端物件
2.1服務端物件
由於本文討論的主要是回撥,所以我們建立新的服務物件和客戶物件來進行演示。下面是ShareAssembly程式集包含的程式碼,我們先看一下服務端物件和委託的定義:
public
delegate void NumberChangedEventHandler(string name,int count);
public
private int count = 0;
private string serverName ="SimpleServer";
public eventNumberChangedEventHandler NumberChanged;
// 觸發事件,呼叫客戶端方法
[MethodImpl(MethodImplOptions.Synchronized)]
public void DoSomething() {
// 做某些額外方法
count++;
if (NumberChanged != null) {
Delegate[] delArray = NumberChanged.GetInvocationList();
foreach (Delegate del in delArray) {
NumberChangedEventHandler method = (NumberChangedEventHandler)del;
try {
method(serverName, count);
} catch {
Delegate.Remove(NumberChanged, del);//取消某一客戶端的訂閱
}
}
}
}
// 直接呼叫客戶端方法
public void InvokeClient(Client remoteClient, int x,int y) {
int total = remoteClient.Add(x, y);//方法回撥
Console.WriteLine(
"Invoke client method: x={0}, y={1}, total={2}",x, y, total);
}
// 呼叫客戶端屬性
public void GetCount(Client remoteClient) {
Console.WriteLine("Count value from client: {0}", remoteClient.Count);
}
}
在這段程式碼中首先定義了一個委託,並在服務物件Server中聲明瞭一個該委託型別的事件,它可以用於客戶物件註冊。它主要包含三個方法:DoSomething()、InvokeClient()和GetCount()。需要注意的是DoSomething()方法,因為我後面將服務端實現為了Singleton模式,所以需要處理併發訪問,我使用了一種簡便的方法,向方法新增MethodImp特性,它會自動實施方法的執行緒安全。其次就是在方法中觸發事件時,我採用了遍歷委託連結串列的方式,並放在了try/catch塊中,因為觸發事件時客戶端有可能已經不存在了。另外,如果發生異常,我將它從訂閱的委託列表中刪除掉,這樣下次觸發時就不會再次呼叫它了。這裡也可以採用BeginInvoke()進行非同步呼叫,具體可以參見C#中的委託和事件(續)一文。
InvokeClient()方法呼叫了客戶端的Add()方法,並向控制檯輸出了提示性的說明;GetCount()方法獲取了客戶端Count的值,併產生了輸出。注意這三個方法均由客戶端呼叫,但是方法內部又回調了呼叫它們的客戶物件。
2.2客戶端物件
接下來我們看下客戶端的程式碼,它沒有什麼特別,OnNumberChanged()方法在事件觸發時自動呼叫,而其餘兩個方法由服務物件進行回撥,並在呼叫它時,在客戶端控制檯輸出相應的提示:
public
class Client :
MarshalByRefObject {
private int count = 0;
// 方式1:供遠端物件呼叫
public
int Add(int x,
int y) {
// 當有服務端呼叫時,列印下面一行
Console.WriteLine("Add callback: x={0}, y={1}.", x, y);
return x + y;
}
// 方式1:供遠端物件呼叫
public
int Count {
get {
count++;
return count;
}
}
// 方式2:訂閱事件,供遠端物件呼叫
public
void OnNumberChanged(string serverName,int count){
Console.WriteLine("OnNumberChanged callback:");
Console.WriteLine("ServerName={0}, Server.Count={1}", serverName, count);
}
}
注意一下Count屬性,它在輸出前進行了一次自增,等下執行時我們會重新看這裡。
3.服務端、客戶端會話模型
當客戶物件呼叫服務物件方法時,服務端已經註冊了通道、開放了埠,對請求進行監聽。同理,當服務端回撥客戶端物件時,客戶端也需要註冊通道、開啟埠。但現在問題是:服務端如何知道客戶端使用了哪個埠?我們在Part.1中提到過,當物件進行傳引用封送時,會包含物件的位置,而有了這個位置,再加上型別的元資料便可以建立代理,代理總是知道遠端物件的地址,並將請求傳送給遠端物件。這種會話模型可以用下面的圖來表述:
從上面這幅圖可以很清楚地看到服務端代理的建立過程:首先在第1階段,客戶端服務端誰也不知道誰在哪兒;因此,在第2階段,我們首先要為客戶端提供服務端物件的地址和型別元資料,有了這兩樣東西,客戶端便可以建立服務端的代理,然後通過代理就訪問到服務端物件;第3階段是最關鍵的一步,在客戶端通過代理呼叫InvokeClient()時,將client物件以傳引用封送的方式傳遞了過去,我們前面說過,在傳引用封送時,它還包括了這個物件的位置,也就是client物件的位置和埠號;第4步時,服務端根據客戶端位置和型別元資料建立了客戶端物件的代理,並通過代理呼叫了客戶端的Add()方法。
NOTE:圖中的代理實際應該分別指向client或者server,由於繪圖的空間問題,我就直接指在框框上了。
因此,客戶端應用程式與之前相比一個最大的區別就是需要註冊通道,除此以外,它並不需要明確地指定一個埠號,可以由.NET自動選擇一個埠號,而服務端則會通過客戶端代理知道其使用的是哪個埠號。
4.宿主應用程式
4.1服務端宿主應用程式
現在我們來看一下服務端宿主應用程式的實現。簡單起見,我們依然建立一個控制檯應用程式ServerConsole,然後在解決方案下新增前面建立的ShareAssembly專案,然後在ServerConsole中引用ShareAssembly。
NOTE:在這裡我喜歡將解決方案和專案起不同的名稱,比如解決方案我起名為ServerSide(服務端),服務端控制檯應用程式則叫ServerConsole。這樣感覺更清晰一些。
服務端控制檯應用程式的程式碼和前面的類似,還是老一套的註冊通道,註冊物件,需要注意的是這裡採用了自定義formatter的方式,並設定了它的TypeFilterLevel屬性為TypeFilterLevel.Full,它預設為Low,但是當設為Low時一些複雜的型別將無法進行Remoting(主要是出於安全性的考慮)。
// using... 略
class Program {
static void Main(string[] args) {
// 設定Remoting應用程式名
RemotingConfiguration.ApplicationName =
"CallbackRemoting";
// 設定formatter
BinaryServerFormatterSinkProvider formatter;
formatter = new
BinaryServerFormatterSinkProvider();
formatter.TypeFilterLevel = TypeFilterLevel.Full;
// 設定通道名稱和埠
IDictionary propertyDic =
new Hashtable();
propertyDic["name"] = "CustomTcpChannel";
propertyDic["port"] = 8502;
// 註冊通道
IChannel tcpChnl =
new TcpChannel(propertyDic,
null, formatter);
ChannelServices.RegisterChannel(tcpChnl, false);
// 註冊型別
Type t = typeof(Server);
RemotingConfiguration.RegisterWellKnownServiceType(
t, "ServerActivated", WellKnownObjectMode.Singleton);
Console.WriteLine("Server running, model: Singleton ");
Console.ReadKey();
}
}
4.2客戶端宿主應用程式
與服務端類似,我們建立解決方案ClientSide,在其下新增ClientConsole控制檯專案,新增現有的ShareAssembly專案,並在ClientConsole專案下新增對ShareAssembly的引用。
//using... 略
class Program {
static void Main(string[] args) {
// 註冊通道
IChannel chnl =
new TcpChannel(0);
ChannelServices.RegisterChannel(chnl, false);
// 註冊型別
Type t = typeof(Server);
string url ="tcp://127.0.0.1:8502/CallbackRemoting/ServerActivated";
RemotingConfiguration.RegisterWellKnownClientType(t, url);
Server remoteServer =
new Server();
// 建立遠端物件
Client localClient =
new Client();
// 建立本地物件
// 註冊遠端物件事件
remoteServer.NumberChanged +=
new NumberChangedEventHandler(localClient.OnNumberChanged);
remoteServer.DoSomething(); // 觸發事件
remoteServer.GetCount(localClient);
// 呼叫GetCount()
remoteServer.InvokeClient(localClient, 2, 5);// 呼叫InvokeClient()
Console.ReadKey();
// 暫停客戶端
}
}
我們看一下上面的程式碼,它僅僅是多了一個通道註冊,注意我們將埠號設定為0,意思是由.NET選擇一個可用埠。由於註冊了遠端型別,所以我們直接使用new操作建立了一個Server物件。然後,我們建立了一個本地的Client物件,註冊了NumberChanged事件、觸發事件、呼叫了GetCount()方法和InvokeClient()方法。最後,我們暫停了客戶端,為什麼這裡暫停,而不是直接結束,我們下面執行時再解釋。
5.程式執行測試
5.1執行一個客戶端
我們執行先服務端,接著執行一個客戶端,此時產生的輸出如下:
上面是服務端,下面是客戶端。我們在呼叫server.DoSomething()方法時,觸發了事件,所以呼叫了客戶端的OnNumberChanged,產生了客戶端的前兩行輸出;呼叫GetCount()時,客戶端沒有產生輸出,服務端輸出了“Count value from client:1”;呼叫InvokeClient()時,客戶端和服務端分別產生了相應的輸出。
5.2執行多個客戶端
接下來,我們不要關閉上面的視窗,再次開啟一個客戶端。此時程式的執行結果如下所示,其中第1幅圖是服務端、第2幅圖是第一個客戶端、第3幅圖是新開啟的客戶端:
這裡可以發現兩點:由於第二個客戶端再次呼叫了DoSomething()方法,所以它再次觸發了事件,因此在第一個客戶端再次產生了輸出“OnNumberChanged Callback...”;再次呼叫GetCount()方法時,對於服務端來說,是一個新建的客戶端localClient物件,所以count值繼續輸出為1,也就是說兩個客戶端物件是獨立的,對伺服器來說,可以將客戶端視為客戶啟用方式(Client-Actived Model)。
5.3 關閉第一個客戶端,再新建一個客戶端
這種情況主要用來測試當服務端觸發事件時,之前訂閱了事件的客戶端已經不存在了的情況。由於我們已經在服務端物件中進行了異常處理,可以看到不會出現任何錯誤,程式會按照預期的執行。
這裡還有另外一種方式,就是將客戶端的回撥方法使用OneWay特性進行標記,然後服務端物件觸發事件時直接使用NumberChanged委託變數。當客戶端方法用OneWay標記後,.NET會自動實施非同步呼叫,並且在客戶端產生異常時也不會影響到服務端的執行。