基本操作(.Net Remoting學習二)

1.應用程序域(.Net Remoting學習一)

2.基本操作(.Net Remoting學習二)

3.分離服務程序實現(.Net Remoting學習三)

4.遠程方法回調(.Net Remoting學習四)

 

 

 

 

Remoting 構架

接下來我們考慮通常的情況,也就是 客戶程序 與 宿主程序 位於不同的進程中的情況。

NOTE:因爲我是在我本地機器做的測試,所以只是位於不同進程,實際上位於不同機器中的操作是完全一樣的,僅僅是Uri不同,下面將會看到。

Remoting 是.Net Framework的一個組成部分,作爲一個框架(Framework),兩個必備的特性是 基本實現可擴展(可定製)。基本實現的意思是說:對於Remoting機制的各個組成部分,.Net 已經提供了一至兩個基本實現,可以直接使用;而可擴展的意思是說:對於每個組成部分,都可以由Framework的用戶自行定製。Remoting 的構架也是如此,它的幾乎每一個部分都是可以由程序員提供實現的,但是.Net也提供了一套默認實現,通常情況下是沒有必要自行定製的。本章主要講述Remoting的各個組成部分。

1.客戶端(客戶應用程序)

客戶端的處理包含三個基本的組成部分,代理(Proxy)、格式器(Formatter) 和 通道(Channel)。

客戶端總是通過一個代理來和服務端對象進行交互。客戶端向代理請求屬性或者方法調用,然後代理將請求發送給服務端的對象。每一個代理綁定一個遠程對象,多個代理也可以綁定同一個對象(Singleton方式,後面會介紹);客戶端的多個對象也可以使用同一個代理。代理分爲兩部分,一個名爲透明代理(Transparent Proxy),一個名爲真實代理(Real Proxy)。透明代理提供了和服務對象完全一致的公共接口,當客戶進行方法調用時,透明代理將棧幀(Stack Frame,在棧中爲參數、返回地址和局部變量保留的一塊內存區,必要時在過程調用中使用)轉換爲消息(Message),然後將消息發送給真實代理。這個消息對象包含了調用的對象的方法信息,包括方法簽名、參數等,同時還包括客戶端的位置(注意這裏,方法回調(Callback)時會再提到)。真實代理知道如何連接遠程對象並將消息發送給它。

真實代理收到消息後,請求Formatter 對象對其進行序列化,同時將客戶程序中斷(block)。.Net 內置了兩種序列化格式,一種是二進制Binary,一種是SOAP。Formatter將消息進行序列化之後,然後將其發送到通道中,由通道將消息發送到遠程對象。當請求返回時,Formatter將返回的消息反序列化,然後再提交給代理,代理將返回值放到發送請求的客戶對象的調用堆棧上,隨後將控制返回給客戶調用程序(解除中斷)。這樣就給了客戶對象一個錯覺:代理即爲遠程對象。

2.服務端(宿主應用程序)

服務端主要由 通道(Channel)、格式器(Formatter)、Stack Builder組成。

在服務端,宿主程序保持着爲Remoting所打開的端口的監聽,一旦通道收到消息,它便將消息發送給Formatter,Formatter將消息進行反序列化,然後將消息發送給Stack Builder,Stack Builder讀取消息,然後依據消息創建對象(可選),調用方法。方法返回時,Stack Builder將返回值封裝爲消息,然後再提交給Formatter,Formatter進行格式化之後,發送到通道傳遞消息。

3.Remoting對象的三種激活方式

上一章 .Net Remoting - Part.1 中,我們提到了傳值封送和傳引用封送,並各給出了一張示意圖,實際上,傳引用封送還分爲了三種不同的方式,下面來一一來介紹。對於傳引用封送,記住各種方式的共同點:服務對象創建且一直保持在宿主程序中。我知道Remoting的概念多得已經讓你厭煩,而且在不結合例子的情況下很難理解,所以這小節我們僅歸納它的特點,到後面例子中,我們再詳細看。

3.1客戶激活(Client activated )

客戶激活方式我們實際上已經瞭解過了,就是在Part.1中我們在單一進程中跨應用程序域傳引用封送時的情況,我們再來回顧一下這張圖:

結合這幅圖,我們可以看出對於每個客戶,創建了其專屬的遠程對象爲其服務(由Part.1的代碼可以看出,對象的狀態在兩次方法調用中也是維持着的)。除此以外,一個代理可以爲多個客戶對象服務。

客戶激活模式的缺點就是 如果客戶端過多時,或者服務對象爲“大對象”時,服務器端的壓力過大。另外,客戶程序可能只需要調用服務對象的一個方法,但是卻持有服務對象過長時間,這樣浪費了服務器的資源。

3.2 服務激活 Singleton(Server activated Singleton)

這個模式的最大特色就是所有的客戶共享同一個對象。服務端只在對象第一次被調用時創建服務對象,對於後繼的訪問使用同一個對象提供服務。如下圖所示:

因爲Sinlgton對象是在第一次訪問(比如方法調用)時由.Net自動創建的,後繼的訪問不能重新創建對象,所以它不提供有參數的構造函數。另外,由於Singleton對象的狀態由其它對象所共享,所以使用Singleton對象應該考慮線程同步 的問題。

3.3 服務激活 Single Call(Server activated Single Call)

Single Call方式是對每一次請求(比如方法調用)創建一個對象,而在每次方法返回之後銷燬對象。由此可見Single Call 方式的最大特點就是 不保存狀態。使用Single Call的好處就是不會過久地佔用資源,因爲方法返回後對資源的佔用就隨對象被銷燬而釋放了。最後,Single Call 方式也不允許使用由參數的構造函數。

Remoting程序的基本操作

在這一章我們綜合前面的知識,通過編碼的方式一步步實現一個Remoting的範例程序,以此來熟悉Remoting的一些基本操作和步驟,並對前面的知識加深一下理解。

1.服務程序集

我們首先創建服務程序集,它即爲向客戶程序提供服務的遠程對象的實現代碼。先創建一個類庫項目ServerAssembly,然後創建類型ServerAssembly.DemoClass(爲Part.1中的ClassLib.DemoClass添加了幾個方法)。我們讓它繼承自MarshalByRefObject,使用更爲常用的傳引用封送形式:

public class DemoClass:MarshalByRefObject {
    private int count = 0;

    public DemoClass() {
        Console.WriteLine(" =======DomoClass Constructor =======");
    }

    public void ShowCount(string name) {
        count++;
        Console.WriteLine("{0},the countis {1}.", name, count);
    }
   
    // 打印對象所在的應用程序域
    public void ShowAppDomain() {
        AppDomain currentDomain = AppDomain.CurrentDomain;
        Console.WriteLine(currentDomain.FriendlyName);
    }

    public int GetCount() {
        return count;
    }
}

創建這幾個方法的作用如下:

  • DemoClass()構造函數用於追蹤遠程對象創建的時機。
  • ShowCount()方法用於測試向遠程對象傳遞參數,以及對象狀態的保存。
  • ShowAppDomain()方法用於驗證對象創建的位置(是否真的位於遠程)。
  • GetCount()方法用於測試獲取遠程對象的返回值。

2.宿主應用程序

接下來我們新創建一個空解決方案ServerSide,在其下添加一個新的控制檯項目ServerConsole,然後再將上面創建的項目ServerAssembly添加進來。除此以外,還需要添加System.Runtime.Remoting的引用,它一般位於C:WINDOWSMicrosoft.NETFrameworkv2.0.50727System.Runtime.Remoting.dll(修改系統目錄)。

2.1 註冊通道

實現宿主應用程序的第一步就是註冊通道。通道是實現了System.Runtime.Remoting.Channels.IChannel的類。通道分爲兩種,一種是發送請求的通道,比如說客戶應用程序使用的通道,這種類型的通道還需要實現 System.Runtime.Remoting.Channels.IChannelSender 接口;一種是接收請求的通道,比如說宿主應用程序使用的通道,這種類型的通道還需實現System.Runtime.Remoting.Channels.IChannelReceiver接口。

通常我們不需要實現自己的通道,.Net 提供了三個內置的通道,分別是 System.Runtime.Remoting.Channels.Http.HttpChannel、System.Runtime.Remoting.Channels.Tcp.TcpChannel 以及 System.Runtime.Remoting.Channels.Ipc.IpcChannel。由於 IpcChannel 不能跨機器(只能跨進程),所以我們僅使用最爲常用的 HttpChannel和TcpChannel爲例作爲說明。它們均實現了 System.Runtime.Remoting.Channels 命名空間下的 IChannel、IChannelSender、IChannelReceiver接口,所以它們既可以用於發送請求,也可以用於接收請求。

接下來需要對通道進行註冊,然後對這個通道進行監聽。對於同一個應用程序域,同一類型(實際上是同一名稱,因爲同一類型的通道默認名稱相同)的通道只能註冊一次。對同一機器來說,同一端口也只能使用一次。同一應用程序域可以註冊多個不同類型的通道。註冊的方式是調用ChannelServices類型的靜態方法RegisterChannel():

using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels.Http;

namespace ServerConsole {
    class Program {
        static void Main(string[] args) {
            RegisterChannel();      // 註冊2個通道
            RemotingConfiguration.ApplicationName = "SimpleRemote";
            Console.WriteLine("服務開啓,按任意鍵退出...");
            Console.ReadKey();
        }
       
        private staticvoid RegisterChannel() {
            // 創建通道實例
            // IChanneltcpChnl = new TcpChannel(8501);
            IChannelReceiver tcpChnl = new TcpChannel(8501);

            // 註冊tcp通道
            ChannelServices.RegisterChannel(tcpChnl, false);

            // 註冊http通道
            IChannel httpChnl = new HttpChannel(8502);
            ChannelServices.RegisterChannel(httpChnl, false);
        }
    }
}

上面的程序便成功註冊了兩個端口用於Remoting程序的監聽。運行程序,然後在Windows的命令提示符中輸入 netstat -a,查看端口狀況,可以看到這兩個端口位於監聽狀態(最後兩行):

當通道從端口監聽到新請求時,它會從線程池中抓取一個線程執行請求,從而可以不間斷地對端口進行監聽(不會因爲處理請求而中斷)。當關閉宿主程序時,.Net會自動釋放端口,以便其他程序可以使用該端口。

在上面我們已經提到消息(Message)以某種特定格式通過通道傳遞。當我們使用上面的構造函數創建通道時,消息會以通道所默認的消息格式傳遞。對於TcpChannel來說,使用二進制,也就是Binary 格式;對於HttpChannel來說,使用SOAP消息格式。我們也可以使用重載的構造函數創建通道,指定通道所採用的消息格式,以TcpChannel爲例:

public TcpChannel(IDictionary properties,
    IClientChannelSinkProvider clientSinkProvider,
    IServerChannelSinkProvider serverSinkProvider);

IDictionary是key已經預先定義好了的 屬性/值 集合,屬性有通道名稱、端口號等。IClientChannelSinkProvider可以用於提供客戶端通道消息所採用的格式;IServerChannelSinkProvider 用於提供服務端通道消息所採用的格式。我們知道一個通道實例不會同時用於位於客戶端和服務端,所以,當創建服務端通道時,將IClientChannelSinkProvider設爲null就可以了;同理,創建客戶端通道時,將IServerChannelSinkProvider設爲null。現在我們將上面的例子改一下,顯示地設置通道所採用的消息格式:

class Program {
    static void Main(string[] args) {
        RegisterChannel();      // 先以第一種方式註冊2個通道
        RegisterChannel2();     // 以自定義模式註冊1個通道

        RemotingConfiguration.ApplicationName = "SimpleRemote";
        Console.WriteLine("服務開啓,可按任意鍵退出...");
        Console.ReadKey();
    }

    // 自定義Formatter和通道名稱的註冊方式
    private static void RegisterChannel2() {

        IServerChannelSinkProvider formatter;
        formatter = new BinaryServerFormatterSinkProvider();

        IDictionary propertyDic = new Hashtable();
        propertyDic["name"] = "CustomTcp";
        propertyDic["port"] = 8503;

        IChannel tcpChnl = new TcpChannel(propertyDic, null, formatter);
        ChannelServices.RegisterChannel(tcpChnl, false);
    }

    private staticvoid RegisterChannel(){...} // 略
}

注意到上面我們通過propertyDic將通道的名稱設爲了CustomTcp,而在RegisterChannel()方法中,我們沒有設置(此時,對於TcpChannel,會採用了默認名稱:tcp)。通過顯示指定通道名稱的方式,對於同一種類型的通道,我們進行了多次註冊。現在在命令提示符中輸入 netstat -a ,應該可以看到一共監聽了三個端口。

2.2 註冊對象

註冊通道之後,我們需要告訴.Net允許哪些類型可以被遠程程序訪問,這一步驟稱爲註冊對象。如同上面所說的,有三種服務器端的遠程對象類型:客戶激活對象、服務激活Single Call、服務激活Singleton。

客戶激活對象的註冊方式需要使用RemotingConfiguration類型的RegisterActivatedServiceType()靜態方法:

// 註冊 客戶激活對象 Client Activated Object
private staticvoid ClientActivated() {
    Console.WriteLine("方式: Client Activated Object");
    Type t = typeof(DemoClass);
    RemotingConfiguration.RegisterActivatedServiceType(t);
}

服務激活對象 可以使用RemotingConfiguration類型的 RegisterWellKnownServiceType()靜態方法:

// 註冊 服務激活對象 SingleCall
private staticvoid ServerActivatedSingleCall() {
    Console.WriteLine("方式: Server Activated SingleCall");
    Type t = typeof(DemoClass);
    RemotingConfiguration.RegisterWellKnownServiceType(
        t, "ServerActivated", WellKnownObjectMode.SingleCall);
}

// 註冊 服務端激活對象 Singleton
private staticvoid ServerActivatedSingleton() {
    Console.WriteLine("方式: Server Activated Singleton");
    Type t = typeof(DemoClass);
    RemotingConfiguration.RegisterWellKnownServiceType(
        t, "ServerActivated", WellKnownObjectMode.Singleton);
}

同一類型對象只可以用一種方式註冊(客戶激活 或者 服務激活)。即是說如果使用上面的方法註冊對象,那麼要麼調用 ClientActivated(),要麼調用ServerActivatedSingleCall()或者ServerActivatedSingleton(),而不能都調用。上面的RegisterWellKnownServiceType()方法接受三個參數:1.允許進行遠程訪問的對象類型信息;2.遠程對象的名稱,用於定位遠程對象;3.服務激活對象的方式,Singleton或者Single Call。

2.3 對象位置

經過上面兩步,我們已經開啓了通道,並註冊了對象(告訴了.Net哪個類型允許遠程訪問)。那麼客戶端如何知道遠程對象位置呢?如同Web頁面有一個Url一樣,遠程對象也有一個Url,這個Url提供了遠程對象的位置。客戶程序通過這個Url來獲得遠程對象。

RemotingConfiguration類型還有一個ApplicationName靜態屬性,當設置了這個屬性之後,對於客戶激活對象,可以提供此ApplicationName作爲Url參數,也可以不提供。如果提供ApplicationName,則必須與服務端設置的ApplicationName相匹配;對於服務激活對象,訪問時必須提供ApplicationName,此時兩種方式的Uri爲下面的形式:

protocal://hostadrress:port/ApplicationName/ObjectUrl       // Server Activated
protocal://hostadrress:port                     // Client Activated Object
protocal:// hostadrress:port/ApplicationName    // Client ActivatedObject

比如,如果通道採用協議爲tcp,服務器地址爲127.0.0.1,端口號爲8051,ApplicationName設爲DemoApp,ObjectUrl設爲RemoteObject(ObjUrl爲使用RegisterWellKnownServiceType()方法註冊服務激活對象時第2個參數所提供的字符串;注意客戶激活對象不使用它),則客戶端在訪問時需要提供的地址爲:

tcp://127.0.0.1:8051/DemoApp/RemoteObject   // Server Activated Object
tcp://127.0.0.1:8051/DemoApp                // Client Activated Object
tcp://127.0.0.1:8051                        // Client ActivatedObject

如果RemotingConfiguration類型沒有設置ApplicationName靜態屬性,則客戶端在獲取遠程對象時不需要提供ApplicationName,此時Url變爲下面形式:

protocal://hostadrress:port/ObjectUrl       // Server Activated Object
protocal://hostadrress:port                 // Client ActivatedObject

3.客戶應用程序

我們現在再創建一個空解決方案ClientSide,然後在其下添加一個控制檯應用程序ClientConsole,因爲客戶端需要ServerAssembly.DemoClass的元信息來創建代理,所以我們仍要添加對ServerAssembly項目的引用。除此以外,我們依然要添加 System.Runtime.Remoting程序集。

客戶應用程序的任務只有一個:獲取遠程對象,調用遠程對象服務。記得客戶應用程序實際上獲得的只是一個代理,只是感覺上和遠程對象一模一樣。客戶端獲得對象有大致下面幾種情況:

3.1使用new操作符創建遠程對象

客戶應用程序可以直接使用new獲得一個遠程對象。例如下面語句:

DemoClass obj = new DemoClass();

看到這裏你可能很驚訝,這樣的話不是和通常的創建對象沒有區別,爲什麼創建的是遠程對象(這裏用“遠程對象”,只是爲了說明方便,要記得實際上是代理對象)而非本地對象呢(注意本地客戶程序ClientConsole也引用了ServerAssembly項目)?其實.Net和你一樣,它也不知道這裏要創建的是遠程對象,所以,在使用new創建遠程對象之前,我們首先要註冊對象。註冊對象的目的是告訴.Net,這個類型的對象將在遠程創建,同時還要告訴.Net遠程對象的位置。

我們知道遠程對象有 客戶激活 和 服務激活 兩種可能,因此客戶程序註冊也分爲了兩種情況 -- 註冊客戶激活對象,註冊服務激活對象。在客戶端註冊對象也是通過RemotingConfiguration類型來完成:

// 註冊客戶激活對象
private staticvoid ClientActivated() {
    Type t = typeof(DemoClass);

    // 下面兩個 url 任選一個
    string url ="tcp://127.0.0.1:8501";   
    //string url ="tcp://127.0.0.1:8501/SimpleRemote";
    RemotingConfiguration.RegisterActivatedClientType(t, url);
}

// 註冊服務激活對象
private staticvoid ServerActivated() {
    Type t = typeof(DemoClass);
    string url ="tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
    RemotingConfiguration.RegisterWellKnownClientType(t, url);
}

我們看到,儘管在服務端,服務激活有兩種可能的方式,Singleton和SingleCall,但是在客戶端,服務激活的兩種方式採用同一個方法RegisterWellKnownClientType()方法進行註冊。所以我們可以說服務端決定服務激活對象的運行方式(Singleton或SingleCall)。

3.2 其它創建遠程對象的方法

當我們在客戶端對遠程對象進行註冊之後,可以直接使用new操作符創建對象。如果不進行註冊來創建遠程對象,可以通過 RemotingServices.Connect()、Activator.GetObject()、Activator.CreateInstance()方法來完成:

string url ="tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
// 方式1
DemoClass obj = (DemoClass)RemotingServices.Connect(typeof(DemoClass), url);
// 方式2
DemoClass obj = (DemoClass)Activator.GetObject(typeof(DemoClass), url);
// 方式3
object[] activationAtt = { new UrlAttribute(url) };
DemoClass obj = (DemoClass)Activator.CreateInstance(typeof(DemoClass),null, activationAtt);

這幾種方法,RemotingServices.Connect()和Activator.GetObject()是最簡單也較爲常用的,它們都是隻能創建服務激活對象,且創建的對象只能有無參數的構造函數,並且在獲得對象的同時創建代理。Activator.CreateInstance()提供了多達13個重載方法,允許創建客戶激活對象,也允許使用有參數的構造函數創建對象,並且可以先返回一個Wrapper(包裝)狀態的對象,然後在以後需要的時候通過UnWrap()方法創建代理。CreateInstance()方法更詳細的內容可以參考MSDN。

4.程序運行測試

Remoting 最讓初學者感到困惑的一個方面就是 客戶激活 與 服務激活 有什麼不同,什麼時候應該使用那種方式。說明它們之間的不同的最好方式就是通過下面幾個範例來說明,現在我們來將上面的服務端方法、客戶端方法分別進行一下組裝,然後進行一下測試(注意在運行客戶端之前必須保證服務端已經運行):

4.1 客戶激活方式

先看下客戶激活方式,服務端的Main()代碼如下:

static void Main(string[] args) {
    RegisterChannel();          // 註冊通道
    ClientActivated();          // 客戶激活方式

    Console.WriteLine("服務開啓,可按任意鍵退出... ");
    Console.ReadKey();
}

客戶端的Main()代碼如下:

static void Main(string[] args) {
    // 註冊遠程對象
    ClientActivated();      // 客戶激活方式

    RunTest("Jimmy", "Zhang");
    RunTest("Bruce", "Wang");

    Console.WriteLine("客戶端運行結束,按任意鍵退出...");
    Console.ReadKey();
}

private static void RunTest(string firstName, string familyName) {
    DemoClass obj = new DemoClass();
    obj.ShowAppDomain();
    obj.ShowCount(firstName);
    Console.WriteLine("{0}, the count is {1}. ",firstName, obj.GetCount());
               
    obj.ShowCount(familyName);
    Console.WriteLine("{0}, the count is {1}.",familyName, obj.GetCount());
}

程序運行的結果如下:

其中第一幅圖是服務端,第二幅圖是客戶端,我們起碼可以得出下面幾個結論:

  1. 不管是對象的創建,還是對象方法的執行,都在服務端(遠程)執行。
  2. 服務端爲每一個客戶端(兩次RunTest()調用,各創建了一個對象)創建其專屬的對象,爲這個客戶提供服務,並且保存狀態(第二次調用ShowCount()的值基於第一次調用ShowCount()之後count的值)。
  3. 可以從遠程獲取到方法執行的返回值。(客戶端從GetCount()方法獲得了返回值)

上面的第3點看起來好像是理所當然的,如果是調用本地對象的方法,那麼確實是顯而易見的。但是對於遠程來說,就存在一個很大的問題:遠程對象如何知道是誰在調用它?方法執行完畢,將返回值發送給哪個客戶呢?此時可以回顧一下第一篇所提到的,客戶端在創建遠程對象時,已經將自己的位置通過消息發送給了遠程。

最後我們再進行一個深入測試,追蹤對象是在調用new時創建,還是在方法調用時創建。將RunTest()只保留一行代碼:

private static void RunTest(string firstName, string familyName) {
    DemoClass obj = new DemoClass();    // 創建對象
}

然後再次運行程序,得到的輸出分別如下:

// 服務端
方式: Client Activated Object
服務端開啓,按任意鍵退出...
======= DomoClass Constructor =======
======= DomoClass Constructor =======

// 客戶端
客戶端運行結束,按任意鍵退出...

由此可以得出結論:使用客戶激活方式時,遠程對象在調用new操作時創建。

4.2 服務激活方式 -- Singleton

我們再來看一下服務激活的Singleton方式。先看服務端代碼(“按任意鍵退出”等提示語句均以省略,下同):

static void Main(string[] args) {
    RegisterChannel();              // 註冊通道
    ServerActivatedSingleton();     // Singleton方式
}

再看下客戶端的Main()方法:

static void Main(string[] args) {
    // 註冊遠程對象
    ServerActivated();     

    RunTest("Jimmy", "Zhang");
    RunTest("Bruce", "Wang");
}

程序的運行結果如下:

同上面一樣,第一幅爲服務端,第二幅圖爲客戶端。從圖中我們可以得出:當使用Singleton模式時,服務端在第一次請求時創建一個對象(構造函數只調用了一次)。對於後繼的請求僅使用這個對象進行服務(即使再次調用構造函數也不會創建對象),同時多個客戶端共享同一個對象的狀態(ShowCount()的值累加)。

我們和上一小節一樣,再次將客戶端的RunTest()只保留爲“DemoClass obj =new DemoClass(); ”一行語句,然後運行程序,得到的結果爲:

// 服務端
方式: Server Activated Singleton
服務端開啓,按任意鍵退出...
// 客戶端
客戶端運行結束,按任意鍵退出...

這個結果出乎我們意料,但它又向我們揭示了Singleton的另一個性質:即使使用new操作符,客戶端也無法創建一個對象,而只有在對象上第一次調用方法時纔會創建。仔細考慮一下這個和上面的結論是類似的,只是更深入了一步。

4.3 服務激活方式 -- SingleCall

最後我們看一下SingleCall方式,注意到客戶端的代碼不需要做任何修改,所以我們只需要切換一下服務端的激活方式就可以了:

static void Main(string[] args) {
    RegisterChannel();          // 註冊通道
    ServerActivatedSingleCall();
}

我們再次看一下運行結果:

我們可能首先驚訝構造函數居然調用了有10次之多,在每次RunTest()方法中各調用了5次。如同前面所說,對於SingleCall方式來說,對象對每一次方法調用提供服務,換言之,對於每一次方法調用,創建一個全新的對象爲其服務,在方法執行完畢後銷燬對象。我們再看下客戶端的輸出:GetCount()方法全部返回0,現在也很明確了,因爲每次方法調用都會創建新對象(在創建對象時,int類型的count被賦默認值0),所以SingleCall方式是不會保存對象狀態的。如果想要爲對象保存狀態,那麼需要另外的機制,比如將狀態存儲到對象之外:

public void ShowCount(string name, object clientId) {
    LoadStatus(this, clientId);         // 加載對象狀態
    count++;
    Console.WriteLine("{0},the countis {1}.", name, count);
    SaveStatus(this, clientId);         // 存儲對象狀態
}

其中LoadStatus()、SaveStatus()方法分別用於加載對象狀態和 存儲對象狀態。注意到ShowCount()方法多了一個clientId參數,這個參數用於標示客戶程序的id,因爲服務端需要知道當前是爲哪個客戶程序加載狀態。

最後,我們再次進行一下上面兩節將RunTest()只保留爲創建對象的一行代碼,得到的運行結果和Singleton是一樣的:

// 服務端
方式: Server Activated Singleton
服務端開啓,按任意鍵退出...
// 客戶端
客戶端運行結束,按任意鍵退出...

這說明使用SingleCall時,即使使用了new 來創建對象,也不會調用構造函數,只有在調用方法時纔會創建對象(調用了構造函數)。

Remoting中的傳值封送

很多朋友可能此刻會感到些許困惑,在Part.1的範例中,我們講述AppDomain時,使用了傳值封送和傳引用封送兩種方式,但是上面的三種激活方式都屬於傳引用封送。那麼如何進行對象的傳值封送呢(將DemoClass直接傳到本地)?實際上,在上面的例子中,我們已經進行了傳值封送,這個過程發生在我們在客戶端調用 GetCount() 時。爲什麼呢?想一想,count值本來是位於服務端的,且int爲可序列化對象(Serializable),在向客戶端返回方法結果時,count值被包裝爲了消息,然後由服務端發送回了客戶端,最後在客戶端進行了解包裝及還原狀態。

爲了看得更清楚一些,我們在ServerAssembly中再創建一個DemoCount類型,然後對這個類型進行傳值封送,因爲DemoCount僅僅是爲了傳送數據,不包含任何行爲,所以我們將它聲明爲結構:

public class DemoClass : MarshalByRefObject {
    // 其餘方法略...

    // 示範傳值封送
    public DemoCount GetNewCount() {
        return new DemoCount(count);
    }
}

[Serializable]
public structDemoCount {
    private readonlyint count;
    public DemoCount(int count) {
        this.count = count;
    }
    public int Count {
        get { return count; }
    }
    public void ShowAppDomain() {
        AppDomain currentDomain = AppDomain.CurrentDomain;
        Console.WriteLine(currentDomain.FriendlyName);
    }
}

在DemoClass中,我們又添加一個方法,它根據count的值創建了DemoCount對象,而DemoCount對象會通過傳值封送傳遞到客戶端。

現在修改客戶端,再重載一個RunTest()方法,用來測試這次的傳值封送:

// 測試傳值封送
private static void RunTest() {
    DemoClass obj = new DemoClass();
    obj.ShowAppDomain();                // 顯示遠程對象所在應用程序域
    obj.ShowCount("張子陽");     // Count = 1

    DemoCount myCount = obj.GetNewCount();    // 傳值封送DemoCount
    myCount.ShowAppDomain();        // 顯示DemoCount所在應用程序域

    // 在客戶端顯示count值
    Console.WriteLine("張子陽, count: {0}.", myCount.Count);
}

此時我們再次進行測試,得到的結果如下:

可以看到,我們在客戶端DemoCount上調用ShowAppDomain()方法時,返回了ClientApp.exe,可見DemoCount已經通過傳值封送傳遞到了客戶端。那麼我們繼續上面的問題,如何將DemoClass整個傳值封送過來呢?首先,我認爲沒有這個必要,如果將服務對象整個封送到客戶端來執行,那麼Remoting還有什麼意義呢?其次,我們來看如何實現它。方法很簡單,我們創建一個工廠類作爲遠程服務對象,然後將我們實際要傳值封送到客戶端的對象(比如DemoClass),作爲工廠方法的返回值。這個例子我就不再演示了,相信看過上面的示例,您已經明白了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章