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

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的一個特例。但是本文不涉及這部分內容。

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