1.應用程式域(.Net Remoting學習一)
2.基本操作(.Net Remoting學習二)
3.分離服務程式實作(.Net Remoting學習三)
4.遠端方法回調(.Net Remoting學習四)
引言
在網際網路日漸普及,網絡傳輸速度不斷提高的情況下,分布式的應用程式是軟體開發的一個重要方向。在.Net中,我們可以通過Web Service 或者Remoting 技術建構分布式應用程式(除此還有新一代的WCF,Windows Communication Foundation)。本文将簡單介紹Remoting的一些基本概念,包括 應用程式域、Remoting構架、傳值封送(Marshal by value)、傳引用封送(Marshal by reference)、遠端方法回調(Callback)、分别在Windows Service和IIS中寄宿宿主程式,最後我們介紹一下遠端對象的生存期管理。
了解Remoting
1.應用程式域基本概念
.Net中的很多概念都是環環相扣的,如果一個知識點沒有掌握(套用一下資料結構中“前驅節點”這個術語,那麼這裡就是“前驅知識點”),就想要一下子了解自己目前所直接面臨問題,常常會遇到一些障礙而無法深入下去,或者是了解的淺顯而不透徹(知道可以這樣做,不知道為什麼會是這樣。如果隻是應急,需要快速應用,這樣也未嘗不可)。為了更好地了解Remoting,我們也最好先了解一下Remoting的前驅知識點 -- 應用程式域。
我們知道所有的.Net 應用程式都運作在托管環境(managed environment)中,但作業系統隻提供程序(Process)供程式運作,而程序隻是提供了基本的記憶體管理,它不了解什麼是托管代碼。是以托管代碼,也可以說是我們建立的.Net程式,是無法直接運作在作業系統程序中的。為了使托管代碼能夠運作在非托管的程序之上,就需要有一個中介者,這個中介者可以運作于非托管的程序之上,同時向托管代碼提供運作的環境。這個中介者就是應用程式域(Application Domain,簡寫為App Domain)。是以我們的.Net程式,不管是Windows窗體、Web窗體、控制台應用程式,又或者是一個程式集,總是運作在一個App Domain中。
如果隻有一個類庫程式集(.dll檔案),是無法啟動一個程序的(它并非可執行檔案)。是以,建立程序需要加載一個可執行程式集(Windows 窗體、控制台應用程式等.exe檔案)。當可執行程式集加載完畢,.Net會在目前程序中建立一個新的應用程式域,稱為預設應用程式域。一個程序中隻會建立一個預設應用程式域,這個應用程式域的名稱與程式集名稱相同。預設應用程式域不能被解除安裝,并且與其所在的程序同生共滅。
那麼應用程式域是如何提供托管環境的呢?簡單來說,應用程式域隻是允許它所加載的程式集通路由.Net Runtime所提供的服務。這些服務包括托管堆(Managed Heap),垃圾回收器(Garbage collector),JIT 編譯器等.Net底層機制,這些服務本身(它們構成了.Net Runtime)是由非托管C++實作的。
在一個程序中可以包含多個應用程式域,一個應用程式域中可以包含多個程式集。比如說,我們的Asp.Net應用程式都運作在aspnet_wp.exe(IIS5.0)或者w3wp.exe(IIS6.0)程序中,而IIS下通常會建立多個站點,那麼是為每個站點都建立一個獨立的程序麼?不是的,而是為每個站點建立其專屬的應用程式域,而這些應用程式域運作在同一個程序(w3wp.exe或aspnet_wp.exe)中。這樣做起碼有兩個好處:1、在一個程序中建立多個App Domain要比建立和運作多個程序需要少得多系統開銷;2、實作了錯誤隔離,一個站點如果出現了緻命錯誤導緻崩潰,隻會影響其所在的應用程式域,而不會影響到其他站點所在的應用程式域。
2.應用程式域的基本操作
在.Net 中,将應用程式域封裝為了AppDomain類,這個類提供了應用程式域的各種操作,包含 加載程式集、建立對象、建立應用程式域 等。通常的程式設計情況下下,我們幾乎從不需要對AppDomain進行操作,這裡我們僅看幾個本文會用到的、有助于了解和調試Remoting的常見操作:
1.擷取目前運作的代碼所在的應用程式域,可以使用AppDomain類的靜态屬性CurrentDoamin,擷取目前代碼所在的應用程式域;或者使用Thread類的靜态方法GetDomain(),得到目前線程所在的應用程式域:
AppDomain currentDomain = AppDomain.CurrentDomain;
AppDomain currentDomain = Thread.GetDomain();
NOTE:一個線程可以通路程序中所包含的所有應用程式域,因為雖然應用程式域是彼此隔離的,但是它們共享一個托管堆(Managed Heap)。
2.擷取應用程式域的名稱,使用AppDomain的執行個體隻讀屬性,FriendlyName:
string name = AppDomain.CurrentDomain.FriendlyName;
3.從目前應用程式域中建立新應用程式域,可以使用CreateDomain()靜态方法,并傳入一個字元串,作為新應用程式域的名稱(亦即設定FriendlyName屬性):
AppDomain newDomain = AppDomain.CreateDomain("New Domain");
4.在應用程式域中建立對象,可以使用AppDomain的執行個體方法CreateInstanceAndUnWrap()或者CreateInstance()方法。方法包含兩個參數,第一個參數為類型所在的程式集,第二個參數為類型全稱(這兩個方法後面會詳述):
DemoClass obj = (DemoClass)AppDomain.CurrentDomain.CreateInstanceAndUnWrap("ClassLib", "ClassLib.DemoClass");
ObjectHandle objHandle = AppDomain.CurrentDomain.CreateInstance("ClassLib", "ClassLib.DemoClass");
DemoClass obj = (DemoClass)objHandle.UnWrap();
5.判斷是否為預設應用程式域:
newDomain.IsDefaultAppDomain()
3.在預設應用程式域中建立對象
開始之前我們先澄清一個概念,請看下面一段代碼:
class Program{
static void Main(string[] args) {
MyClass obj = new MyClass();
obj.DoSomething();
}
}
此時我們說obj 是服務對象,Program是客戶程式,而不管obj位于什麼位置。
接下來我們來看一個簡單的範例,我們使用上面提到基于AppDomain的操作,在目前的預設應用程式域中建立一個對象。我們先建立一個類庫項目ClassLib,然後在其中建立一個類DemoClass,這個類的執行個體即為我們将要建立的對象:
namespace ClassLib {
public classDemoClass {
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);
}
}
}
接下來,我們再建立一個控制台應用程式,将項目命名為ConsoleApp,引用上面建立的類庫項目ClassLib,然後添加如下代碼:
class Program {
static void Main(string[] args) {
Test1();
}
// 在目前AppDomain中建立一個對象
static void Test1() {
AppDomain currentDomain = AppDomain.CurrentDomain;// 擷取目前應用程式域
Console.WriteLine(currentDomain.FriendlyName); // 列印名稱
DemoClass obj;
// obj = newDemoClass() // 正常的建立對象的方式
// 在預設應用程式域中建立對象
obj = (DemoClass)currentDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
obj.ShowAppDomain();
obj.ShowCount("Jimmy");
obj.ShowCount("JImmy");
}
}
運作這段代碼,得到的運作結果是:
ConsoleApp.exe
======= DomoClass Constructor =======
ConsoleApp.exe
Jimmy,the count is 1.
Jimmy,the count is 2.
現在運轉良好,一切都沒有什麼問題。你可能想問,使用這種方式建立對象有什麼意義呢?通過CreateInstanceAndUnwrap()建立對象和使用new DemoClass()建立對象有什麼不同呢?回答這個問題之前,我們再來看下面另一種情況:
4.在建立應用程式域中建立對象
我們看看如何 建立一個新的AppDomain,然後在這個新的AppDomain中建立DemoClass對象。你可能會想,這還不簡單,把上面的例子稍微改改不就OK了:
// 在新AppDomain中建立一個對象
static void Test2() {
AppDomain currentDomain = AppDomain.CurrentDomain;
Console.WriteLine(currentDomain.FriendlyName);
// 建立一個新的應用程式域 - NewDomain
AppDomain newDomain = AppDomain.CreateDomain("NewDomain");
DemoClass obj;
// 在新的應用程式域中建立對象
obj = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
obj.ShowAppDomain();
obj.ShowCount("Jimmy");
obj.ShowCount("Jimmy");
}
然後我們在Main()方法中運作Test2(),結果卻是得到了一個異常:類型“ClassLib.DemoClass”未标記為可序列化。在把ClassLib.DemoClass标記為可序列化(Serializable)之前,我們想一想為什麼會發生這個異常。我們看看聲明obj類型的這行代碼:DemoClass obj,這說明了obj是在目前的預設應用程式域,也就是AppConsole.exe中聲明的;然後我們在往下看,類型的執行個體(對象本身)卻是通過 newDomain.CreateInstanceAndUnwrap() 在新建立的應用程式域 -- NewDomain中建立的。這樣就出現了一種尴尬的情況:對象的引用(類型聲明)位于目前應用程式域(AppConsole.exe)中,而對象本身(類型執行個體)位于新建立的應用程式域(NewDomain)。而上面我們提到預設情況下AppDomain是彼此隔離的,我們不能直接在一個應用程式中引用另一個應用程式域中的對象,是以這裡便會引發異常。
那麼如何解決這個問題呢?按照異常提示:"ClassLib.DemoClass"未标記為可序列化。那我們将它标記為可序列化是不是就解決了這個問題呢?我們可以試一下,先将ClassLib.DemoClass标記為可序列化:
[Serializable]
public classDemoClass { }
然後再次運作程式,發現程式果然正常運作,并且和上面的輸出完全一緻:
ConsoleApp.exe
======= DomoClass Constructor =======
ConsoleApp.exe
Jimmy,the count is 1.
Jimmy,the count is 2.
根據輸出,我們發現在應用程式域NewDomain中建立的對象位于ConsoleApp.exe,也就是目前應用程式域中了。這就說明了一個問題:當我們将對象标記為可序列化時,然後進行上面的操作時,對象本身已經由另一應用程式域(遠端)傳遞到了本地應用程式域中。因為其要求将對象标記為可序列化,是以不難想到,具體的方法是 先在遠端建立對象,接着将對象序列化,然後傳遞對象,在本地進行反序列化,最後還原對象。
5.代理(Proxy)和封送(Marshaling)
5.1 代理(Proxy)
現在我們在回到第3小節中 在預設應用程式域中建立對象 的例子,通過上面Test2()的例子,很容易了解為什麼Test1()沒有抛出異常,因為obj對象本身就位于目前應用程式域ConsoleApp.exe,是以不存在跨應用程式域通路的問題,自然不會抛出異常。那麼在目前應用程式域中使用下面兩種方式建立對象有什麼不同呢?
DemoClass obj = new DemoClass(); // 方式一
DemoClass obj = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass"); // 方式二
當我們使用第一種方式時,我們在托管堆中建立了一個對象,并且直接引用了這個對象;采用第二種方式時,我們實際上建立了兩個對象:我們在newDomain中建立了這個對象,然後将對象的狀态進行拷貝、串行化,然後進行封送,接着在ConsoleApp.exe(用戶端應用程式域)重新建立這個對象、還原對象狀态,建立對象代理。最後,我們通過這個代理通路這個對象,此時,因為代理通路的是在本地重新建立的對象而非遠端對象,是以當我們在對象上調用ShowDomain()時,顯示的是ConsoleApp.exe。
上面的說明中出現了兩個新名稱,代理和封送。現在先來解釋一下代理,代理(Proxy) 提供了和遠端對象(本例中是在NewDomain中建立的DemoClass對象)完全相同的接口(屬性和方法)。.Net需要在用戶端(本例中是ConsoleApp.exe)基于遠端對象的元資訊(metadata)建立代理。是以用戶端必須包含遠端對象的元資訊(簡單來說就是隻包含名稱及接口定義,但可以不包含實際的代碼實作)。因為代理有着和遠端對象完全一樣的接口和名稱,是以對于客戶程式來說,代理就好像是遠端對象一樣;而代理實際上又并不包含向客戶程式提供服務的實際代碼(比如說方法體),是以代理僅僅是将自己與某一對象相綁定,然後把客戶程式對自己的服務請求發送給對象。對于客戶程式來說,遠端對象(服務端對象)就好像是在本地;而對遠端對象來說,也好像是為其本地程式提供服務。
NOTE:有的書本講到這裡,會提到透明代理、真實代理,以及Message Sink等概念,這些我們留待後面再說。
5.2 傳值封送、傳引用封送
在上面的例子中,當位于ConsoleApp.exe的obj引用NewDomain中建立的對象時,.Net将NewDomain中對象的狀态進行複制、序列化,然後在ConsoleApp.exe中重新建立對象,還原狀态,并通過代理來對對象進行通路。這種跨應用程式域的通路方式叫做傳值封送(Marshal by value),有點類似于C#中參數的按值傳遞:

NOTE:上面這種通過調用CreateInstanceAndUnWrap()方法這種方式進行傳值封送是一種特例,僅僅作為示範用。在Remoting通常的情況下,傳值封送發生在遠端對象的方法向用戶端傳回數值,或者用戶端向遠端對象傳遞方法參數的情況下。後面會詳細解釋。
由圖上可以看出,傳值封送時,因為要将整個對象傳遞到本地,對于大對象來說很顯然是低效的。是以還有一種方式就是讓對象依然保留在遠端(本例為NewDomain中),而在用戶端僅建立代理,上面已經說了代理的接口和遠端對象完全相同,是以用戶端以為仍然通路的是遠端對象,當用戶端調用代理上的方法時,由代理将對方法的請求發送給遠端對象,遠端對象執行方法請求,最後再将結果傳回。這種方式叫做傳引用封送(Marshal by reference)。
對象或者對象引用在傳遞的過程中,是以一種包裝過的狀态(warpper state)進行傳遞(是以才會稱為封送吧,僅為個人猜測)。是以在建立對象時,要解包裝,是以在CreateInstanceAndUnWrap()方法後多了一個AndUnWrap字尾,實際上UnWrap還包含一個建立代理的過程。
6.傳引用封送範例
上面的例子中我們已經使用了傳值封送,那麼如何實作傳引用封送呢?我們隻要讓對象繼承自MarshalByRefObject基類就可以了,是以修改DemoClass,去掉Serializable标記,然後讓它繼承自MarshalByRefObject:
public class DemoClass:MarshalByRefObject {}
接下來我們再次運作程式:
ConsoleApp.exe
======= DomoClass Constructor =======
NewDomain
Jimmy,the count is 1.
Jimmy,the count is 2.
發現obj.ShowDomain()輸出為NewDomain,說明DemoClass的類型執行個體obj沒有傳值封送到ConsoleApp.exe中,而是依然保留在了NewDomain中。有的人可能想那我既标記上Serializable,又繼承自MarshalByRefObject程式怎麼處理呢?當我們讓一個類型繼承自MarshalByRefObject後,它就一定不會離開自己的應用程式域,是以仍會以傳引用封送的方式進行。聲明為Serialzable隻是說明它可以被串行化。
繼續進行之前,我們看看上面的結果還能說明什麼問題:對象的狀态是保留着的。這句話是什麼意思呢?當我們兩次調用ShowCount()方法時,第二次運作的值(count的值)是基于第一次的運作結果的。
我們再對上面Test2()的進行一下修改,多建立一個DemoClass的執行個體,看看會發生什麼:
static void Test2() {
AppDomain currentDomain = AppDomain.CurrentDomain;
Console.WriteLine(currentDomain.FriendlyName);
// 建立一個新的應用程式域 - NewDomain
AppDomain newDomain = AppDomain.CreateDomain("NewDomain");
DemoClass obj, obj2;
// 在新的應用程式域中建立對象
obj = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
obj.ShowAppDomain();
obj.ShowCount("Jimmy");
obj.ShowCount("Jimmy");
// 再建立一個obj2
obj2 = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
obj2.ShowAppDomain();
obj2.ShowCount("Zhang");
obj2.ShowCount("Zhang");
}
運作Test2(),可以得到下面的輸出:
ConsoleApp.exe
======= DomoClass Constructor =======
NewDomain
Jimmy,the count is 1.
Jimmy,the count is 2.
======= DomoClass Constructor =======
NewDomain
Zhang,the count is 1.
Zhang,the count is 2.
這次我們又發現什麼了呢?對于obj和obj2,在NewDomain中分别建立了兩個對象為其服務,且這兩個對象僅建立了一次(注意到隻調用了一次構造函數)。這種方式稱為用戶端激活對象(Client Activated Object,簡稱為 CAO)。請大家再次看看上面第二張傳引用封送的示意圖,是不是可以推出這裡的結果?關于客戶激活對象,後面我們會再看到,這裡大家先留個印象。
7.客戶應用程式(域)、服務端程式集、宿主應用程式(域)
看到Remoting這個詞,我們通常所了解的可能隻是本地客戶機與遠端伺服器之間的互動。而實際上,隻要是跨越AppDomain的通路,都屬于Remoting。不管這兩個AppDomain位于同一程序中,不同程序中,還是不同機器上。對于Remoting,可能大家了解它就包含兩個部分,一個Server(伺服器端)、一個Client(用戶端)。但是如果從AppDomain的角度來看,服務端的AppDomain僅僅是提供了一個實際提供服務的遠端對象的運作環境。是以提起Remoting,我們應該将其視為三個部分,這樣在以後操作,以及我下面的講述中,概念都會更加清晰:
- 宿主應用程式(域),服務程式運作的環境(服務對象所在的AppDomain),它可以是控制台應用程式,Windows窗體程式,Windows 服務,或者是IIS的工作者程序等。上例中為 NewDomain。
- 服務程式(對象),響應客戶請求的程式(或對象),通常為繼承自MarshalByRefObject的類型,表現為一個程式集。上例中為 DemoClass。
- 客戶應用程式(域),向宿主應用程式發送請求的程式(或對象)。上例中為 ConsoleApp.exe。
在文中,有時我可能也會用到 用戶端(Client Side) 和 服務端(Server Side)這樣的詞,當提到用戶端時,僅指客戶應用程式;當提到服務端的時候,指服務程式 和 宿主應用程式。
可以看出,在我們上面的例子中,用戶端 與 宿主應用程式 位于同一個程序的不同應用程式域當中,盡管大多數情況下,它們位于不同的程序中。
而我們本章第三節,在目前應用程式域的執行個體上調用CreateInstanceAndUnwrap()方法建立DemoClass對象時,則是一個極端情況:即 客戶程式域、宿主應用程式域 為同一個應用程式域 ConsoleApp.exe 。
NOTE:在應用程式域中底部,還有一個代碼執行領域,稱為環境(Context)。一個AppDomain中可以包含多個環境,跨越環境的通路也可以了解成Remoting的一個特例。但是本文不涉及這部分内容。