1. 程式人生 > >遠端方法回撥(.Net Remoting學習四)

遠端方法回撥(.Net Remoting學習四)

Remoting中的方法回撥

1. 遠端回撥方式說明

遠端方法回撥通常有兩種方式:

  • 客戶端也存在繼承自MarshalByValueObject的型別,並將該型別的例項作為引數傳遞給了遠端物件的方法,然後遠端物件在其方法中通過該型別例項的引用對它進行呼叫(訪問其屬性或者方法)。記得繼承自MarshalByValueObject的型別例項永遠不會離開自己的應用程式域,所以相當於服務端物件呼叫了客戶端物件。
  • 客戶端物件註冊了遠端物件釋出的事件,遠端物件通過委託呼叫客戶端註冊了的方法。

當服務端呼叫客戶端的方法時,它們的角色就互換了。此時,需要注意這樣幾個問題:

  1. 因為不能通過物件引用訪問靜態方法(屬性),所以無法對靜態方法(屬性)進行回撥。
  2. 由於服務端在執行時需要訪問客戶端物件,此時它們的角色互換,需要在服務端建立對客戶端物件的代理,所以服務端也需要客戶端物件的型別元資料。因此,最好將客戶端需要回調的方法,抽象在一個物件中,服務端只需引用含有這個物件的程式集就可以了。而如果直接寫在Program中,服務端還需要引用整個客戶端。
  3. 由於將客戶端進行回撥的邏輯抽象成為了一個獨立的物件,此時客戶端的構成就類似於前面所講述的服務端。它包含兩部分:(1)客戶端物件,用於支援服務端的方法回撥,以及其它的業務邏輯;(2)客戶端控制檯應用程式(也可以是其它型別程式),它僅僅是註冊通道、註冊埠、註冊遠端物件,提供一個客戶端物件的執行環境。

根據這三點的變化,我們可以看出:客戶端含有客戶端物件,但它還需要遠端服務物件的元資料來構建代理;服務端含有服務物件,但它還需要客戶端物件的元資料來構建代理。因此,客戶端服務端均需要服務物件、客戶物件的型別元資料

,簡單起見,我們將它們寫在同一個程式集中,命名為ShareAssembly,供客戶端、服務端引用。此時,執行時的狀態圖如下所示:

其中ShareAssembly.dll包含服務物件和客戶端物件的程式碼。接下來一節我們來看一下它們的程式碼。

2.客戶端和服務端物件

2.1服務端物件

由於本文討論的主要是回撥,所以我們建立新的服務物件和客戶物件來進行演示。下面是ShareAssembly程式集包含的程式碼,我們先看一下服務端物件和委託的定義:

public delegate void NumberChangedEventHandler(string name,int count);

public

classServer :MarshalByRefObject {
    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會自動實施非同步呼叫,並且在客戶端產生異常時也不會影響到服務端的執行。