.Net Remoting(基本操作) - Part.2
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("\n======= DomoClass Constructor =======");
}
public void ShowCount(string name) {
count++;
Console.WriteLine("{0},the count is {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:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.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 static void RegisterChannel() {
// 建立通道執行個體
// IChannel tcpChnl = 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 static void RegisterChannel(){...} // 略
注意到上面我們通過propertyDic将通道的名稱設為了CustomTcp,而在RegisterChannel()方法中,我們沒有設定(此時,對于TcpChannel,會采用了預設名稱:tcp)。通過顯示指定通道名稱的方式,對于同一種類型的通道,我們進行了多次注冊。現在在指令提示符中輸入 netstat -a ,應該可以看到一共監聽了三個端口。
2.2 注冊對象
注冊通道之後,我們需要告訴.Net允許哪些類型可以被遠端程式通路,這一步驟稱為注冊對象。如同上面所說的,有三種伺服器端的遠端對象類型:客戶激活對象、服務激活Single Call、服務激活Singleton。
客戶激活對象的注冊方式需要使用RemotingConfiguration類型的RegisterActivatedServiceType()靜态方法:
// 注冊 客戶激活對象 Client Activated Object
private static void ClientActivated() {
Console.WriteLine("方式: Client Activated Object");
Type t = typeof(DemoClass);
RemotingConfiguration.RegisterActivatedServiceType(t);
服務激活對象 可以使用RemotingConfiguration類型的 RegisterWellKnownServiceType()靜态方法:
// 注冊 服務激活對象 SingleCall
private static void ServerActivatedSingleCall() {
Console.WriteLine("方式: Server Activated SingleCall");
RemotingConfiguration.RegisterWellKnownServiceType(
t, "ServerActivated", WellKnownObjectMode.SingleCall);
// 注冊 服務端激活對象 Singleton
private static void ServerActivatedSingleton() {
Console.WriteLine("方式: Server Activated Singleton");
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 Activated Object
比如,如果通道采用協定為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 Activated Object
如果RemotingConfiguration類型沒有設定ApplicationName靜态屬性,則用戶端在擷取遠端對象時不需要提供ApplicationName,此時Url變為下面形式:
protocal://hostadrress:port/ObjectUrl // Server Activated Object
protocal://hostadrress:port // Client Activated Object
3.客戶應用程式
我們現在再建立一個空解決方案ClientSide,然後在其下添加一個控制台應用程式ClientConsole,因為用戶端需要ServerAssembly.DemoClass的元資訊來建立代理,是以我們仍要添加對ServerAssembly項目的引用。除此以外,我們依然要添加 System.Runtime.Remoting程式集。
客戶應用程式的任務隻有一個:擷取遠端對象,調用遠端對象服務。記得客戶應用程式實際上獲得的隻是一個代理,隻是感覺上和遠端對象一模一樣。用戶端獲得對象有大緻下面幾種情況:
3.1使用new操作符建立遠端對象
客戶應用程式可以直接使用new獲得一個遠端對象。例如下面語句:
DemoClass obj = new DemoClass();
看到這裡你可能很驚訝,這樣的話不是和通常的建立對象沒有差別,為什麼建立的是遠端對象(這裡用“遠端對象”,隻是為了說明友善,要記得實際上是代理對象)而非本地對象呢(注意本地客戶程式ClientConsole也引用了ServerAssembly項目)?其實.Net和你一樣,它也不知道這裡要建立的是遠端對象,是以,在使用new建立遠端對象之前,我們首先要
注冊對象。注冊對象的目的是告訴.Net,這個類型的對象将在遠端建立,同時還要告訴.Net遠端對象的位置。
我們知道遠端對象有 客戶激活 和 服務激活 兩種可能,是以客戶程式注冊也分為了兩種情況 -- 注冊客戶激活對象,注冊服務激活對象。在用戶端注冊對象也是通過RemotingConfiguration類型來完成:
// 注冊客戶激活對象
// 下面兩個 url 任選一個
string url = "tcp://127.0.0.1:8501";
//string url = "tcp://127.0.0.1:8501/SimpleRemote";
RemotingConfiguration.RegisterActivatedClientType(t, url);
// 注冊服務激活對象
private static void ServerActivated() {
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("服務開啟,可按任意鍵退出...\n");
Console.ReadKey();
用戶端的Main()代碼如下:
// 注冊遠端對象
ClientActivated(); // 客戶激活方式
RunTest("Jimmy", "Zhang");
RunTest("Bruce", "Wang");
Console.WriteLine("用戶端運作結束,按任意鍵退出...");
private static void RunTest(string firstName, string familyName) {
DemoClass obj = new DemoClass();
obj.ShowAppDomain();
obj.ShowCount(firstName);
Console.WriteLine("{0}, the count is {1}.\n",firstName, obj.GetCount());
obj.ShowCount(familyName);
Console.WriteLine("{0}, the count is {1}.",familyName, obj.GetCount());
程式運作的結果如下:
其中第一幅圖是服務端,第二幅圖是用戶端,我們起碼可以得出下面幾個結論:
- 不管是對象的建立,還是對象方法的執行,都在服務端(遠端)執行。
- 服務端為每一個用戶端(兩次RunTest()調用,各建立了一個對象)建立其專屬的對象,為這個客戶提供服務,并且 儲存狀态 (第二次調用ShowCount()的值基于第一次調用ShowCount()之後count的值)。
- 可以從遠端擷取到方法執行的傳回值。(用戶端從GetCount()方法獲得了傳回值)
上面的第3點看起來好像是理所當然的,如果是調用本地對象的方法,那麼确實是顯而易見的。但是對于遠端來說,就存在一個很大的問題:遠端對象如何知道是誰在調用它?方法執行完畢,将傳回值發送給哪個客戶呢?此時可以回顧一下第一篇所提到的,用戶端在建立遠端對象時,已經将自己的位置通過消息發送給了遠端。
最後我們再進行一個深入測試,追蹤對象是在調用new時建立,還是在方法調用時建立。将RunTest()隻保留一行代碼:
DemoClass obj = new DemoClass(); // 建立對象
然後再次運作程式,得到的輸出分别如下:
// 服務端
方式: Client Activated Object
服務端開啟,按任意鍵退出...
======= DomoClass Constructor =======
// 用戶端
用戶端運作結束,按任意鍵退出...
由此可以得出結論:使用客戶激活方式時,遠端對象在調用new操作時建立。
4.2 服務激活方式 -- Singleton
我們再來看一下服務激活的Singleton方式。先看服務端代碼(“按任意鍵退出”等提示語句均以省略,下同):
RegisterChannel(); // 注冊通道
ServerActivatedSingleton(); // Singleton方式
再看下用戶端的Main()方法:
ServerActivated();
程式的運作結果如下:
同上面一樣,第一幅為服務端,第二幅圖為用戶端。從圖中我們可以得出:
當使用Singleton模式時,服務端在第一次請求時建立一個對象(構造函數隻調用了一次)。對于後繼的請求僅使用這個對象進行服務(即使再次調用構造函數也不會建立對象),同時多個用戶端共享同一個對象的狀态(ShowCount()的值累加)。我們和上一小節一樣,再次将用戶端的RunTest()隻保留為“DemoClass obj = new DemoClass(); ”一行語句,然後運作程式,得到的結果為:
方式: Server Activated Singleton
這個結果出乎我們意料,但它又向我們揭示了Singleton的另一個性質:
即使使用new操作符,用戶端也無法建立一個對象,而隻有在對象上第一次調用方法時才會建立。仔細考慮一下這個和上面的結論是類似的,隻是更深入了一步。
4.3 服務激活方式 -- SingleCall
最後我們看一下SingleCall方式,注意到用戶端的代碼不需要做任何修改,是以我們隻需要切換一下服務端的激活方式就可以了:
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 count is {1}.", name, count);
SaveStatus(this, clientId); // 存儲對象狀态
其中LoadStatus()、SaveStatus()方法分别用于加載對象狀态和 存儲對象狀态。注意到ShowCount()方法多了一個clientId參數,這個參數用于标示客戶程式的id,因為服務端需要知道目前是為哪個客戶程式加載狀态。
最後,我們再次進行一下上面兩節将RunTest()隻保留為建立對象的一行代碼,得到的運作結果和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 struct DemoCount {
private readonly int count;
public DemoCount(int count) {
this.count = count;
public int Count {
get { return count; }
在DemoClass中,我們又添加一個方法,它根據count的值建立了DemoCount對象,而DemoCount對象會通過傳值封送傳遞到用戶端。
現在修改用戶端,再重載一個RunTest()方法,用來測試這次的傳值封送:
// 測試傳值封送
private static void RunTest() {
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),作為工廠方法的傳回值。這個例子我就不再示範了,相信看過上面的示例,您已經明白了。