我所理解的Remoting(1):Marshaling & Activation - Part I

什麼是Marshaling &Activation

對任何一項分佈式技術(Distributed Technology),比如Remoting,XML Web Service,Enterprise Service,Marshaling和Activation(對於Marshaling,我實在是找不到一個比較貼切的中文短語來翻譯,很多書把它翻譯成封送,我總覺得很彆扭,所以在這裏我就直接用英文Marshaling,如果讀者有較好的翻譯,麻煩通知我一下)都是必須要解決的問題。本Blog主要講述的是在Remoting中的Marshaling和Activation。

首先我們來講講到底什麼是Marshaling和Activation。我想對於這個問題每個都心中都有自己的定義。我是這樣理解的:對於一個對象,當他創建的時候被綁定到(Be Bound)到某一個Context上——這個Context可能是一個內部網絡,一台主機,一個進程,一個託管的Application Domain。當另一個Context要調用這個對象,有時候必須 對這個對象作出一些不要的轉變(Transformation),這個轉變的過程被稱為Marshaling。我們一般由兩種方式的Marshaling——By Reference 和By Value。前者向是把對象的一個引用傳遞出去,而後者則是從新創建一個和對象一樣的Copy向外傳遞。

對於任何一個分佈式應用來説,Client和Service都分別出於各自的Context之中,Client 以某種方式調用Server(可能是基於Message, 也可能基於RPC)Server),這個調用請求通過一個已經註冊到Server端註冊的Channel傳遞到Server端,Server從這個調用請求中提取所需的Metadata信息,創建相應的對象——對Client來説這個對象是一個遠程對象(Remote Object)而Activation就是如何創建Remote Object得過程。

Hosting

一個Remote Object能夠被不同的Client調用,首先它必須Host在某一個進程之中,對於Remoting來説,你可以選擇有很多種選擇方式——你可以選擇任何一種Managed Application來Host你所需要的Remote Object——Console Application,Windows From Application,ASP.NET Application,一致於Windows Service 中,我們把這種Host方式Self-Host。你也可以把它Host到IIS (6.0 &7.0)以致WAS(Windows Activation Service)。

這個Host的過程本質上包括下面兩個方面:

Channel Registration:Channel是Client調用Server的通道,Client調用某個Remote Object,這個調用請求首先轉化成一個Message,這個Message通過Client選擇的一個Channel從Client AppDomain傳遞到Server Appdomain,同理執行執行的結果(Result)也以同樣的方式從Server Appdomain傳遞到Client AppDomain。這裏有一個根本的前提,Client選擇的Channel必須先在Host中註冊過。

Object Registration:Object Registration的本質就是把Remote Object相關的原數據(Metadata)註冊到Host環境中,併為它制定一個Objet URI。如果説Channel Registration結果瞭如何Communication的問題,Object Registration可以看成是解決如何創建Remote Object和驗證調用的合法有效性問題——它利用MetaData來創建Remote Object和驗證Client端的調用請求。MSDN把這個過程稱為Object Registration,我實際上不太贊成這種説法,因為這個過程做的僅僅是註冊Remote Object Metadata的信息——實際上就是Type的信息,中間不曾有過對象的創建。所以我覺得叫做Remote Type Registration更加準確點。

當完成Object Registration之後,Remoting Framework根據註冊信息找到Server對應的Assembly,從而提取出所需要的Metadata,結合註冊的Object的Uri 、Assembly Name、Channel相關的信息,創建一個類型為ObjRef的對象,這個對象基本上包含了能夠調用對應Remote Object的所有信息(關於ObjRef,下面的章節會後介紹,如果想查看詳細的信息,你可以參考MSDN)。Remoting Framework內部維護着一個Table用於存儲他所有註冊的類型。

Proxy

在託管的環境(Managed Environment)下,Application Domain把一同的每個Managed Application隔離在它們各自的區域內,在一個Application創建的對象不能被另一個Application所直接調用。他必須通過Marshaling以傳遞引用或者傳遞從一個Application Domain傳遞到另一個Application Domain中。關於Application Domain的隔離性可以參照我的文章(.NET Framework——用Coding證明Application Domain的隔離性 )。

對於Remoting來説,Remote Type繼承的是System.MarshalByRefObject,從名稱就可以看出,它是以傳遞Reference的方式來Marshal 的。為了使我們更加準確地理解MarshalByRefObject,我們需要引入一個新的類System.Runtime.Remoting.ObjRef。ObjRef是一個可序列化的對象,用於擴展MarshalByRefObject對象(MRB Object)。當一個MRB Object被Marshal的時候,實際上Remoting Framework會根據MRB Object創建一個ObjRef,這個ObjRef包含了Client調用此Remote Object的一切信息——Remote Object對應的Type信息;Remote Object實現的Interface;調用這個Remote Object所用的Channels;以及Remote Object所在的Uri。由於ObjRef是可序列化的,所以他以傳值的形式傳遞到Client Application Domain ——Remote Object以Marshal By Reference的形式通過ObjRef實現的Application Domain之間的傳遞,而ObjRef本身則是以Marshal By Value的形式傳遞的。當此ObjRef到達Client Application Domain後,會在Client端創建一個Proxy,通過這個Proxy便可以遠程地調用Remote Object了。

接下來我們結合圖來了解具體的調用過程:
 

 


在Client Application,一個Client Object調用一個Transparent Proxy,這個Transparent Proxy把這個調用轉換成一個IMessage對象,並把這個IMessage對象傳遞給RealProxy 對象,RealProxy調用Invoke方法並把該IMessage對象傳遞給Invoke方法。RealProxy調用CreateObjRef方法得到Remote Object的ObjRef,並通過Client註冊的Channel把這個調用傳遞到Server Application Domain。

Activation

.NET Remoting有兩種不同的Activation方式——Server Activation 和Client Activation。

Server Activation:客户端一般通過Activator的靜態方法GetObject方法在Client端創建Transparent Proxy 和Real Proxy,Transparent Proxy被最終傳道給Client。這裏有非常重要的一點,通過上面的分析,我們知道,Proxy的建立需要Remote Object的ObjRef,而此時這個ObjRef處在Server端的AppDomain中,在創建Proxy的時候,Client是否會為了獲取Remote Object ObjRef 而進行網絡連接呢?答案是否定的,在Client端創建Proxy的時候,是不會使用任何對於Server的網絡訪問的。而創建Proxy所必需的是在Client端獲取的——我們可以用編程和配置信息的方式為他指定Remote Object Metadata的信息(我們可以傳入Remote Object Type 或者Remote Object Type實現的Interface)和Remote Object的Uri。而通過在Client端創建的ObjRef和通過網絡訪問從Server端獲取的具有相同的功效。

當Client的Transparent Proxy創建了以後,這個Transparent Proxy就成了Remote Object 在Client端的代理。我們調用Transparent Proxy的某一個方法,Transparent Proxy會先把這個調用轉化成一個Message(實現了IMessage Interface);然後調用Real Proxy (默認的是一個System.Runtime.Remoting.Proxies.RemotingProxy對象)的Invoke方法,同時把Message傳入該方法。Real Proxy 調用GetObjRef方法獲得Remote Object Metadata的信息來驗證這個請求,驗證失敗,拋出異常。然後Real Proxy判斷調用的對象是存在於和自己相同的AppDomain(MRB 不單單是用在跨AppDomain調用的場景),如果是直接在本地創建一個對象,執行相應的操作,把執行結果通過Transparent Proxy返回給Client。如果判斷結果表明是一個遠程調用,則通過ChannelInfo屬性,獲取Channel的信息,最終把Message通過Channel傳遞到Server端——這中間回經歷序列化和編碼等操作。

前面我們講過,當Remote Obejct被Host的時候,Remoting Framework 會創建一個內部的Table用於記錄他所有被註冊的Remote Object Type。一旦完成了Host,Server端便開始利用所註冊的Channel進行監聽。一旦他監聽到某一格來自Client端的請求,他把Message 截獲下來,獲取Remote  Object(對於Client來説)的ObjRef,並同這個內部表的Items進行比較,從而知道需要激活的對象。如果Mode 是SingleCall創建一個對象,執行相應的操作,返回結構後銷燬該對象。如果是Singleton模式,會判斷相應的對象是否存在(這個説法不太準確,應該説是否有相應的對象存在,並沒有標記為過期——具體的原因,可以留意我的下一篇Blog——關於Remoting的Lifetime  Management),如果存在則直接調用該對象,如果不存在則重新創建,然後執行相應的操作。對於Singleton模的下的對象,其生命週期通過LifetimeManager來控制,這是一個機遇Lease的控制策略。

Client Activation:前面我們花了大量的篇幅來解釋Server Activation,其中一個主要的特點是,Remote Object在第一次調用時激活,而不是在Client創建Proxy的時候激活。正因如此,對於一個Server Activated Object來説,它的對象的創建之只能通過默認的無參構造函數來創建。任何有參構造函數的定義沒有任何意義,並且沒有定義無參構造函數在調用會拋出異常。

相對於Server Activation,Client Activation採用了完全不同的激活方式。在Client Activation方式下,當Client調用New或者Actiovator.CreateInstance方法(再這之前,Client必須在Client端註冊Channel和Remote Object Type),Remoting Framework會在Client段創建一個Activation Proxy,這個Activation Proxy遠程地調用Server端的一個Activator遠程對象,這個Activator對象激活相應的對象,創建相應的ObjRef傳遞到Client端,Client端利用這個ObjRef創建Real Proxy 和Transparent Proxy。至此Client端就可以通過該Transparent Proxy進行遠程調用了。

從這個過程中我們可以看到,Remote Object實在Client創建Proxy的時候同時創建的,所以創建Proxy時指定的信息可以 傳遞到Server端,所以對於Client Activated Object,他們是可以由自定義參數的構造函數的。

你可以通過以下的Link獲得一個全面的Sample([原創]我所理解的Remoting(1):Marshaling & Activation - Part I I ) 

我所理解的Remoting(1):Marshaling & Activation - Part II

 在上面一片文章([原創]我所理解的Remoting(1):Marshaling & Activation - Part I ),我花了大量的文字來來描述了Remote Object如何通過Marshaling的過程從Server端所在的Application Domain經過相關的轉換(Transformation)傳遞到Client所在的Application Domain供Client調用; 以及Client的調用請求如何在Activate處於Server端Application Domain的Remote Object。大體的要點如下:

Host在Server端註冊Client可能會用到的一到多個Channel用於傳輸Client發出的調用請求(這個調用請求最終被序列化成一Message)——Channel Registration。然後把Remote Object的相關Metadata信息和remote Object Uri(Remote Object Type的信息)註冊到Host進程中——Object Registration。完成了Object Registration之後,Remoting Framework分析註冊的信息Load相應的Assembly,利用Reflection的機制為相應的Remote Type生成一個可序列化ObjRef(可序列化以為着ObjRef對象可以穿梭Application Domain),並將它保存到一個Internal Table 之中(這個Internal Table用於Track Remote Object)。

Remoting有兩種Activation 方式——Server Activation 和Client Activation。而Server Activation有具有兩種不同的Mode——SingCall和Singleton(SingleCall和Singleton嚴格地説是關於Instance Management的概念——這個概念在WCF中被引入)。對於Server Activation,Client端根據註冊在Client端的Remote Object的Metadata創建Real Proxy和Transparent Proxy。在創建Proxy的時候,不曾有任何訪問請求發送到Server端,與此同時,也不可能有相應的Remote Object在Server 端被創建。而真正第一次網絡訪問發生在第一次通過Transparent Proxy調用某個方法。當這個方法請求從某個註冊在Server段的某個Channel抵達Server端的時候,Server 端的Remoting Framework提取請求Message 的Remote Object 的ObjRef,同上面提到的Internal Table的相關Entry進行比較,獲得所需的Metadata信息,通過Reflection創建Remote Object。這就是Server Activation的簡單過程。

Client Activation採用了不同的Activation 方式——Client端的Proxy(Both Transparent Proxy和Real Proxy )和Remote Object幾乎在同時創建(當然在不考慮遠程調用時延的因素)。當Client通過New或者Activator.CreateInstance在Client創建Proxy的時候實際上是經歷了以下一個過程:一個Activator Proxy首先在Client端創建,藉助這個Activator Proxy Remoting Framework發送一個Activation請求到Server端,Server端的Remoting Framework根據Activation Request的Metadata信息和已經註冊的Remote Type做一個匹配,提取所需的Metadata通過Reflection的機制創建Remote Object。同時創建這個Remote Object的ObjRef並通過相應的ChannelForward到Client端,隨之生成RealProxy 和Transparent Proxy,Transparent Proxy被Client調用。

上面基本上就是我在上一篇Blog的中心。可能看過我Blog的人都知道,我幾乎在每篇文章中都會有一個Sample,我不太相信流於文字的理論,我喜歡用實踐來證明。所以下面我們將用Sample來證明。這個Sample中將沿用簡單的Calculator的應用。Source Code可以從這裏下載(Artech.MyRemoting.zip)

1. 整個Solution的結構。

 

 

這個結構其實是我比較鄙視的分佈式結構——Client端和Server通過Share一個Type System來共享整個Service(包括Interface和Implementation ,在WCF中我們這兩部分稱為Service Contract和Service Implementation)。但是由於Client Activated Object在創建Proxy的時候需要制定MarshalByRefObject的Type,所以我們不得不用這種結構——Client和Server共享定義在Artech.MyRemoting.RemoteService中定義的MarshalByRefObject Class:CalculatorService。大家可以可以看到Artech.MyRemoting.Hosting和Artech.MyRemoting.Client都有Artech.MyRemoting.RemoteService的Reference。話又説回來,我們可以應用某些策略使只把Service的Interface公開給Client——即使是SAO對象,相關的內容超出了這篇文章範疇,我會相關的討論放到後續的Remoting相關的Blog中,如有興趣可以留意。

2. 在Artech.MyRemoting.RemotingService 定義我們的Service——儘管Remoting算不上是完全基於SOA的Distributed Technology,但我還是會不自覺地使用SOA相關的術語,不當的之處,還往見諒。

使用Spring Initializr_使用Spring Initializr

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;

namespace Artech.MyRemoting.RemoteService
{
    public class CalculatorService:MarshalByRefObject
    {
        private int _callCount;

        public CalculatorService()
        {
            Console.WriteLine("Remote object is activated at {0}\n", DateTime.Now.ToString("hh:mm:ss"));
        }

        public double Add(double x, double y)
        {
            this._callCount++;
            return x + y;
        }

        public int GetCallCount()
        {
            return this._callCount;
        }
    }
}

使用Spring Initializr_使用Spring Initializr

Code 很簡單——一個Constructor用於確定Remote Object到底在什麼時候創建或者説的專業一點,真正的Remote Object什麼時候被Activated。一個GetCallCount用所Add方法調用次數的計數器,這個計數器用來驗證Remoting 的Instance Management。和一個執行加法運算的方法,注意執行一次我們的保存調用次數的變量就是加1。

3. Host CalculatorService

App.Config
 
<?xml versinotallow="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application name="Artech.MyRemoting">
      <service>
        <wellknown type="Artech.MyRemoting.RemoteService.CalculatorService,Artech.MyRemoting.RemoteService"
                   mode ="SingleCall" objectUri="SingleCall.Calculator.rem"></wellknown>
        <wellknown type="Artech.MyRemoting.RemoteService.CalculatorService,Artech.MyRemoting.RemoteService"
                 mode ="Singleton" objectUri="Singleton.Calculator.rem"></wellknown>
        <activated type="Artech.MyRemoting.RemoteService.CalculatorService,Artech.MyRemoting.RemoteService"></activated>
      </service>
      <channels>
        <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel,System.Runtime.Remoting, Versinotallow=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
                 port="1234"></channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>
從這個config文件可以看到,我們為同一個CalculatorService以不同的方式註冊了3此——SingleCall,Singleton和CAO。
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;

namespace Artech.MyRemoting.Hosting
{
    class Program
    {
        static void Main(string[] args)
        {
            RemotingConfiguration.Configure("Artech.MyRemoting.Hosting.exe.config", false);
            Console.WriteLine("The Calculator services have begun to listen
 
");

            Console.Read();
        }
    }
}
很簡單,無需贅言。
4. 編寫客户端
 
using System;
using System.Collections.Generic;
using System.Text;
using Artech.MyRemoting.RemoteService;
using System.Runtime.Remoting;
using System.Threading;

namespace Artech.MyRemoting.Client
{
    class Program
    {
        static void Main(string[] args)
        {
            string singelCallAddress = "http://localhost:1234/artech.myremoting/singlecall.calculator.rem";
            string singletonAddress = "http://localhost:1234/artech.myremoting/singleton.calculator.rem";
            string caoAddress = "http://localhost:1234/artech.myremoting";

            RemotingConfiguration.RegisterActivatedClientType(typeof(CalculatorService), caoAddress);

            Console.WriteLine("Create server SingleCall proxy at {0}", DateTime.Now.ToString("hh:mm:ss"));
            CalculatorService singleCallCalculator = Activator.GetObject(typeof(CalculatorService), singelCallAddress) as CalculatorService;

            Thread.Sleep(10000);
            Console.WriteLine("Create server Singleton proxy at {0}", DateTime.Now.ToString("hh:mm:ss"));
            CalculatorService singletonCalculator = Activator.GetObject(typeof(CalculatorService), singletonAddress) as CalculatorService;

            Thread.Sleep(10000);
            Console.WriteLine("\nCreate client activated object proxy at {0}", DateTime.Now.ToString("hh:mm:ss"));
            CalculatorService caoCalculator = new CalculatorService();

            Thread.Sleep(10000);
            Console.WriteLine("\nCall the method of SingleCall object at {0}", DateTime.Now.ToString("hh:mm:ss"));
            InvocateCalculator(singleCallCalculator);

            Thread.Sleep(10000);
            Console.WriteLine("\nCall the method of Singleton object at {0}", DateTime.Now.ToString("hh:mm:ss"));
            InvocateCalculator(singletonCalculator);

            Thread.Sleep(10000);
            Console.WriteLine("\nCall the method of CAO object at {0}", DateTime.Now.ToString("hh:mm:ss"));
            InvocateCalculator(caoCalculator);

           
            Console.WriteLine("The times to invovate the current SingleCall remote object is {0}", singleCallCalculator.GetCallCount());
            Console.WriteLine("The times to invovate the current Singleton remote object is {0}", singletonCalculator.GetCallCount());
            Console.WriteLine("The times to invovate the current CAO remote object is {0}", caoCalculator.GetCallCount());

            Console.Read();
        }

        static void InvocateCalculator(CalculatorService calculator)
        { 
            Console.WriteLine("x + y = {2} where x = {0} and y = {1}",1,2,calculator.Add(1,2));
        }
    }
}

使用Spring Initializr_使用Spring Initializr

這裏有必要多説幾句:

我們首先創建基於3種不模式的Proxy,為了弄清楚整個Activation的流程,對於每個操作都為它顯示出具體的執行時間,通過操作的執行會間隔一段時間(我給它指定的是10s)

 

使用Spring Initializr_使用Spring Initializr

string singelCallAddress = "http://localhost:1234/artech.myremoting/singlecall.calculator.rem";

使用Spring Initializr_使用Spring Initializr

            string singletonAddress = "http://localhost:1234/artech.myremoting/singleton.calculator.rem";

使用Spring Initializr_使用Spring Initializr

            string caoAddress = "http://localhost:1234/artech.myremoting";

使用Spring Initializr_使用Spring Initializr


使用Spring Initializr_使用Spring Initializr

            RemotingConfiguration.RegisterActivatedClientType(typeof(CalculatorService), caoAddress);

使用Spring Initializr_使用Spring Initializr


使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("Create server SingleCall proxy at {0}", DateTime.Now.ToString("hh:mm:ss"));

使用Spring Initializr_使用Spring Initializr

            CalculatorService singleCallCalculator = Activator.GetObject(typeof(CalculatorService), singelCallAddress) as CalculatorService;

使用Spring Initializr_使用Spring Initializr


使用Spring Initializr_使用Spring Initializr

            Thread.Sleep(10000);

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("Create server Singleton proxy at {0}", DateTime.Now.ToString("hh:mm:ss"));

使用Spring Initializr_使用Spring Initializr

            CalculatorService singletonCalculator = Activator.GetObject(typeof(CalculatorService), singletonAddress) as CalculatorService;

使用Spring Initializr_使用Spring Initializr


使用Spring Initializr_使用Spring Initializr

            Thread.Sleep(10000);

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("\nCreate client activated object proxy at {0}", DateTime.Now.ToString("hh:mm:ss"));

使用Spring Initializr_使用Spring Initializr

            CalculatorService caoCalculator = new CalculatorService();

使用Spring Initializr_使用Spring Initializr

依次調用三個Proxy的Add方法顯示調用的準確時間

使用Spring Initializr_使用Spring Initializr

Thread.Sleep(10000);

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("\nCall the method of SingleCall object at {0}", DateTime.Now.ToString("hh:mm:ss"));

使用Spring Initializr_使用Spring Initializr

            InvocateCalculator(singleCallCalculator);

使用Spring Initializr_使用Spring Initializr


使用Spring Initializr_使用Spring Initializr

            Thread.Sleep(10000);

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("\nCall the method of Singleton object at {0}", DateTime.Now.ToString("hh:mm:ss"));

使用Spring Initializr_使用Spring Initializr

            InvocateCalculator(singletonCalculator);

使用Spring Initializr_使用Spring Initializr


使用Spring Initializr_使用Spring Initializr

            Thread.Sleep(10000);

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("\nCall the method of CAO object at {0}", DateTime.Now.ToString("hh:mm:ss"));

使用Spring Initializr_使用Spring Initializr

            InvocateCalculator(caoCalculator);

使用Spring Initializr_使用Spring Initializr

獲得他們的計數器,看看調用次數有何不同。
 

 

使用Spring Initializr_使用Spring Initializr

Console.WriteLine("The times to invovate the current SingleCall remote object is {0}", singleCallCalculator.GetCallCount());

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("The times to invovate the current Singleton remote object is {0}", singletonCalculator.GetCallCount());

使用Spring Initializr_使用Spring Initializr

            Console.WriteLine("The times to invovate the current CAO remote object is {0}", caoCalculator.GetCallCount());

使用Spring Initializr_使用Spring Initializr

現在我們首先啓動Hosting,輸出表明Server端正常監聽。



 

 

啓動Client,產生如下的輸出:
 

 



然後我們再看看現在Hosting的輸出:

 

 



我們現在來分析一下為什麼會有如此輸出結果:

  • 我們在04:40:02和04:40:12創建了一個SingleCall Proxy,Server端沒有反應,這就充分驗證了對於Server Activation來説,在Client端創建Proxy的時候,並沒有Remote Object在Server端被Activated。
  • 接着我們在04:40:22創建一個CAO Proxy,Remote Object 對象在一分鐘後便被Activated。表明Proxy和Remote Object 幾乎是同時創建的。
  • 10s後,我們分別調用SingleCall和Singleton Proxy的方法,Server端也在同一時間給出反應,這就説明了,對於Server Activation 來説,第一次調用才會導致Remote Object的Activation。
  • 隨後,我們在04:40:53調用CAO Proxy的方法,Server端沒有任何輸出,因為這個Proxy對應的Remote在Proxy創建的時候就已經被創建了。(Server端的最後一行輸出實際上是調用SingleCall Proxy的GetCallCount方法時輸出的——對於SingleCall來説,對於Proxy的每次調用都會創建一個Remote Object,調用完畢被Garbage Collected。看來這個Sample 還是不夠好,不過相信大家能夠理解了)。
  • 接下來,我們分別獲取3個對象的調用計數 。對於SingleCalll Proxy 來説,每次調用都會創建一個新的Remote Object,所以Add方法的調用次數永遠為零,而對於Singleton來説,所有的Client共享一個Remote Object, 所以它能保持上次來自任意一個Client調用時的State,對於CAO來説,一個Client和一個Remote Object,所以他能夠保持自己的調用的State。所以我們不然想象,當我們在開啓一個Client,會有什麼樣的輸出:
     

 


注: 以上的輸出都應該基於這樣的前提:創建的Remote Object沒有被回收。相關的原理相對比較複雜,它將會出現在的後續的關於Remote  Object Lifetime Management的Blog中,有興趣的網友可以關注。

我所理解的Remoting(2):遠程對象生命週期的管理—Part I

1.CLR的垃圾回收機制

在.NET中提到對象的生命週期,我們會不由自主地想到CLR的垃圾回收。在運行一個.NET程序過程中,我們通過某種方式,比如通過new操作符,通過反序列化,通過反射機制,創建一個對象,CLR在為這個對象在託管堆中開闢一塊內存空間。隨着程序的運行,創建的對象越來越多,託管堆中的可用的內存越來越少,必須有一種機制來判斷被分配在託管堆中的對象那些已經不被使用,以及進行對這些對象佔用的內存進行回收。這種機制被稱為CLR自動內存管理,也就是我們常説的垃圾回收。為了説清楚遠程對象的生命週期管理,我們 得首先了解本地對象的生命週期。

首先我們來説説CLR如何判斷分配在託管堆中的對象那些是可以被垃圾回收的。我想我們應該可以想象得到,在程序運行的某個時刻,以下一些對象是會在後續的運行中會時候使用到的:一個類的靜態字段,一個全局變量,調用方法堆棧中存儲的方法參數和創建的局部變量,CPU 寄存器。我們一般把這些對象稱為根(root),所有的根和被它直接引用或間接引用的對象,我們都認為是不應該被回收的。在CLR的內部維持着一個特殊的數據結構,這個表的每個條目對應一個跟,當CLR加載的時候,該表被創建。隨着程序的不斷運行,新的根會被加進來,已經不是跟的會被刪除掉,所用這個表可以看作根的實時的反應。當垃圾回收器進行工作的時候(一般是第0代對象所對應的託管堆充滿的時候,當然你也可以手動地調用GC.Collect方法啓動垃圾回收。),它首先在託管堆上掃描,如果發現該的內存快所對應的對象是一個根,它會在該對象同步快索引(synchronous block index)字段做一個特殊的標記,表明它當前還不可以被垃圾回收,接着他會一遞歸的方式標記所有 被它直接或者間接引用的對象。所以當掃描完畢之後,那些被標記的對象就是在當前不可以被當成垃圾回收的對象,這些對象一般稱為可達對象(reachable object,因為可以通過根來找到),反之,除此以外的對象則就是垃圾對象了。

標記可達對象只是垃圾回收的第一步,第二步才是對這些未被標記的垃圾對象進行回收。在開始之前我們必須能夠分別兩種不同的對象,一種稱為可終結(Finalizable)對象和非可終結對象。我們知道,在很多情況下,我們的對象會引用一些非託管的資源,比如一個文件的句柄,一個數據庫連結,或者一個Socket連結等等。在我們回收這些對象的時候,如果沒有這些非託管的資源進行相應的終結操作的話,很有可能造成內存的泄漏。在.NET中,我們通常把這些終結操作寫在一個特殊的方法中,叫做Finalize。在C#中我們一般定於在~ClassName()的形式,並且沿用C++ 的説法,稱它為析構函數(我不推薦這麼稱呼,Finalize方法和C++的析構函數是不同的)。如果你有興趣查看C#編譯之後生成的IL代碼,你會發現定義成~ClassName()的方法的名稱就是Finalize。我們把相應的類定義了Finalize方法的對象叫做可終結的對象。説到可終結對象,這裏我們又需要引入一個新的垃圾回收器維護的數據結構,這是的鏈表,用於保存未被回收的可終結對象,一般稱為終結鏈表。

接下來我們看看垃圾回收器如何進行垃圾回收的。垃圾回收器開始再一次掃描託管堆,對於在第一步作了標記的對象,不予理睬。對於未作標記的對象,首先判斷是否是可終結對象(看看在終結鏈表中是否有相應的對象),如果不是,直接回收掉。否則垃圾回收器還要先判斷是否已經進行了終結操作,如果沒有則會把它從終結鏈表中移除,把對象放入另一個稱為終結可達對列(freachable queue——f代表finalizable,注意這裏又引進了一個新的數據結構)。如果已經進行了終結操作,則直接進行回收就好了。

對於放入終結可達對列對象,我們必須在對它進行垃圾回收之前收前進行終結操作。從廣義來講終結可達對列中的對象也是一個根,所以被放入終結可達對列中的對象是不應該被垃圾回收的,由於終結操用涉及到線程同步的問題,所有的終結操作都在一個具有較高優先級的線程上進行。這個線程在終結可達對列為空的時候處於休眠的狀態,一旦垃圾回收器把一個可終結對象放入這個終結可達對列的時候,這個特殊的線程立即被喚醒,調用該對象的Finalize方法並把它從終結可達對列中移除。等再一次進行回收的時候,對於這些經過終結操作對象已經成為垃圾對象——不會有任何的根,包括終結可達對列引用它,這個時候垃圾回收器可以對它進行回收了。

從垃圾的整個過程來看,如果我們重寫了Finalize方法使之成為一個可終結類型,這種對象實際上要經過兩次垃圾回收才會被真正地回收調——其中一次放入終結可達對列,另一次才真正被回收調。所以,我們在定義某個類型的時候,如果沒有必要重寫Finalize方法就千萬不要重寫它,不然會加重內存的壓力,降低應用的性能。
 



2.基於租約(Lease)的生命週期管理機制

在前面我們簡單地講述了CLR垃圾回收機制。按照這種機制,如果我們要阻止一個對象被垃圾回收,我們必須讓它被某個根直接或間接引用,而這個引用它的對象一般應該和該對象處於同一個Application Domain之中。所以這種機制不適合我們分佈式環境。在一個分佈式應用來説,服務器段對象通過Marshaling從Sever端的Application Domain傳到Client所在的Application Domain。無論採用哪種Marshal方式,By Value 或者By Reference,Marshal本身是不會為遠程對象在Server端的Application Domain創建一個引用的。如果這樣的話,對於垃圾回收器來説,遠程對象便成了一個垃圾對象,在進行垃圾回收的時候,必然會被回收掉。如果Client端調用一個已經被回收的遠程對象,對於Client Activated Object,會拋出異常,如果對於Singleton模式的Server Activated Object,Remoting Framework會重新創建一個新的對象,原有的狀態將不復存在。所以我們必須有一種機制來控制遠程對象的生命週期。這就是我們現在講的基於租約(Lease)的生命週期管理。

在正式講述之前,我首先我講一個現實生活中的一個困難不是很恰當的例子,一個租房的例子。

去年9月份,我從蘇州來到上海,通過中介租了一間房子,很巧的是這個中介就是我的房東,並且我是和房東合租的。當時覺得不是太滿意,所以籤合同的時候只簽了半年。在租期還沒有到期的時候,我有權向房東提出續租,租期可以使半年,也可以使一年。如果到期了,我沒有提出續租,房東肯定會主動和我聯繫,詢問我時候有續租的打算,如果我提出續租,考慮到我們房東和和睦相處的關係,我們可以在不需要簽訂新的合同的情況下讓我續租。否則我走人,我的房間被轉租出去。如果房東在找我的時候,可能我出差了,手機又換號了,聯繫不到我,這時候,他肯定會打聯繫我的女朋友,他們也很熟,如果我的女朋友説要續租,房東便會在沒有獲得我答覆的情況下,給我續租。但是現在租期已經到期了,我也沒有提出續租,房東也沒有叫我續租,但是我還是每個月給他交房東,雖然合同已經在法律的意義上失效了,但是我和清楚,我交了下個月的房租,我的房間到下個月底使用權歸我。這就是我租房的故事(呵呵)。大家注意這樣故事中的幾個實體,合同,房東兼中介,房間,我(合同上的承租者),我女朋友。



現在我們再來講Remoting關於Lease的對象生命週期的管理機制。當遠程對象被激活和Marshal的時候,處於Server端Application Domain的Lease Manager會為該遠程對象創建一個Lease。就相當於中介和我簽了一份租房合同,中介相當於Lease Manager,我(承租者)相當於客户端,而房間就是這個遠程對象,Lease則代表我們簽訂的租房合同。就像合同會寫明租期一樣,Lease也會有一個初始的時間(InitialLeaseTime)代表遠程對象的初始生命週期。就像我可以在租期到期之前可以自動提出延長租期一樣,Client可以通過這個Lease來延長對應遠程對象的生命週期。不過和租房的例子不同的是,Server端也可以具有相同的權利。

就像我可以通過交房租來延長一個月的租期一樣,遠程對象可以通過來自Client端的調用來延長這個Lease,這個時間由屬性RenewOnCallTime來表示。不過有一點值得注意的是,就像我在租期到了的那個月之前交房租這個行為不會延長租期(始終是6個月),只有我在第6個月月底交房租才會把實際的租期延長到7月個。在Remoting中,只有在RenewOnCallTime大於Lease剩下的時間的時候,這個RenewOnCallTime才會起作用。

就像我可以讓房東在我不在的時候,找我的女朋友來代表我一樣,在Remoting中,Client可註冊一個Sponsor,這個Sponsor有權代表Client延長租房期限,當然Client有權利取消這個註冊的Sponsor,就像有一天我和女朋友分手了,她就沒有這樣的權利了。隨着時間的推移,當Lease的過期了,Lease Manager會首先通過遠程調用(可以把這種情況看成一種Callback),從Client獲得Client為相應遠程對象註冊的Sponsor,找到了之後,通過這個Sponsor設置的時間來延長遠程對象的生命週期。但是,我們已經説了,Lease Manager獲得Sponsor是一種遠程調用,可能他們處在不同的Application Domain,不同的Process,不同的Machine,甚至處於Internet的兩端。這個調用是否成功和調用的時間是不確定的,所以這裏必須給定一個特定的時間來規定,在某一段限定的時段內,如果不能獲得相應的Sponsor則認為該Sponsor不可得。否則始終這個調用下去也不是個事兒。就像房東在房租到期一個月之內還找不到我和我女朋友,關係再好也必須把房間轉租出去了。對於Lease來説,這個時間通過SponsorshopTimeout屬性表示。

這裏還有一個重要的時間,那就是Lease Manager每個多少時間掃描Lease——LeaseManagerPollTime。

上面的所有的時間都是可以定製的,我們現在看看,如何定製這些時間。

1. 通過編程的方式:通過設置System.Runtime.Remoting.Lifetime. LifetimeServices靜態屬性。

 

LifetimeServices.LeaseManagerPollTime = TimeSpan.FromMinutes(2);
LifetimeServices.LeaseTime = TimeSpan.FromMinutes(2);
LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(2);
LifetimeServices.SponsorshipTimeout = TimeSpan.FromMinutes(2);
2. 通過Configuration的方式
 
<lifetime  leaseTime="7M"  spnotallow="7M"  renewOnCallTime="7M" 
leaseManagerPollTime="7S" />
3. 定製單個MarshalByRefObject 對象的Lease 時間:Override 繼承自MarshalByRefObject 的InitializeLifetimeService方法。
public override object InitializeLifetimeService()
        {
            ILease lease = (ILease)base.InitializeLifetimeService();
            if (lease.CurrentState == LeaseState.Initial)
            {
                lease.InitialLeaseTime = TimeSpan.FromSeconds(1);
                lease.RenewOnCallTime = TimeSpan.FromSeconds(1);
                lease.SponsorshipTimeout = TimeSpan.FromSeconds(1);
            }
            return lease;           
        }

使用Spring Initializr_使用Spring Initializr

通過上面的講述,我的應該對Remoting對象生命基於Lease和Sponsorship的生命週期的管理有一個感性的認識。實際上Sponsorship遠非這麼簡單,對Sponsorship的深入探討,有興趣話可以關注:[原創]我所理解的Remoting (2) :遠程對象的生命週期管理-Part II .

Reference:Jeffery Richiter 《CLR via C#》  

我所理解的Remoting (2) :遠程對象的生命週期管理-Part II

上一篇文章中([原創]我所理解的Remoting(2):遠程對象生命週期的管理—Part I ),我簡要的講述了CLR的垃圾回收機制和Remoting 基於Lease的對象生命週期的管理。在這篇文章中,我們將以此為基礎,繼續我們的話題。在文章的開始,我將以我的理解詳細地講述Remoting中兩個重要的概念——Lease和Sponsorship。然後我通過一個Sample,為大家演示如何以不同的方法延長遠程對象的生命週期。

我們先不談遠程對象、本地對象。 不管是遠程的對象,還是本地對象,都對於程序Application Domain 所在地址空間的一塊連續的託管堆(managed heap)內存區域。在.NET環境下,其對象的生命週期,也就是對應託管堆的回收,都由垃圾回收器(garbage collector)負責。當需要進行垃圾回收時(如果不是強制進行垃圾回收,和程序卸載,垃圾回收操作一般出現在第0代對象充滿的時候),垃圾回收器掃描託管堆,標記垃圾對象(實際上是標記非垃圾對象,未被標記的則是垃圾對象),並最終回收垃圾對象。

通過前面章節的介紹,我們知道了,CLR通過當前程序運行時的一個根(root)的列表來判斷一個對象是否是垃圾對象——沒有被根直接或者間接引用的對象則是垃圾對象。從另一個角度講,如果想讓一個對象存活,或者你試圖讓一個對象具有更長的生命週期,那麼不就必須使它被一個根直接或者間接引用——比如你可以使用一個全局變量引用它,那麼在這個全局變量的生命週期內,這個對象就會一直存活;你甚至可以讓一個靜態變量引用它,那麼這個對象將永遠不會被垃圾回收,直到所在的AppDomain被卸載。而我們現在來討論Remoting 中遠程對象生命週期的管理,説白了,其本質就是在Remoting Framework中,如何創建一些具有根性質的對象引用創建的遠程對象(相對於Client端來講),從而適當地(我們不能讓遠程對象具有太長的生命週期,那樣會見中內存的壓力,同樣我們也不能使遠程對象,那樣會造成頻繁的對象的頻繁創建進而影響系統的性能)阻止垃圾回收器回收該對象。

那麼這個引用遠程對象的對象是誰呢?它就是我們要講的Lease。當Client端的Proxy通過Marshal傳到Host環境的時候,Remoting Framework 激活對應的遠程對象。與此同時,Lease Manager(整個機遇Lease生命週期管理的總負責)會為該遠程對象創建對應的Lease,從垃圾回收的角度講,遠程對象有了Lease對象的引用,則可以在垃圾收器的鍘刀下得以倖存。但是通過前面的分析,遠程對象不能老是存活者,他是具有一定的生命週期的,也就是説,一旦到了該壽終正寢的時候,垃圾回收器就該對他動刀子。而且,這個生命週期應該是可以配置的,系統地設計人員根據具體的程序運作狀況,計算出一個合理的生命週期,在部署的時候,通過配置文件為之設定。

那麼這樣的機制又是如何實現的呢?到現在為止我們知道,遠程對象存在的唯一條件就是它的Lease存在,Lease一旦垃圾回收了,那麼它的死期也不遠了。這樣我們就可以通過Lease對象的生命週期來間接地控制遠程對象的生命週期。而Lease對象的生命週期是可以配置的。那麼現在我們可以把我們的關注點放在如果控制Lease的生命週期上來。在Remoting中,Lease是實現System.Runtime.Remoting.Lifetime. ILease的類的對象。

 

namespace System.Runtime.Remoting.Lifetime
{
    // Summary:
    //     Defines a lifetime lease object that is used by the remoting lifetime service.
    [ComVisible(true)]
    public interface ILease
    {
        LeaseState CurrentState { get; }
        TimeSpan InitialLeaseTime { get; set; }
        TimeSpan RenewOnCallTime { get; set; }
        TimeSpan SponsorshipTimeout { get; set; }
        void Register(ISponsor obj);        
        void Register(ISponsor obj, TimeSpan renewalTime);        
        TimeSpan Renew(TimeSpan renewalTime);        
        void Unregister(ISponsor obj);
    }
}

瞭解在託管環境下Lease是怎樣一個對象之後,我們來看看,在Remoting中Lease的生命週期是如果決定的。在前面一節我們提到過我們有3種方式來設置Lease 的各個屬性(初始的時間:InitialLeaseTime,一個遠程調用所能延續的時間:RenewOnCallTime,Lease Manager聯繫對應的Sponsor的時間:SponsorshipTimeout)——通過Configuration通過設置LifetimeServices的靜態屬性(LeaseTime,RenewOnCallTime,LeaseManagerPollTime,SponsorshipTimeout)同過Override MarshalByRefObj的InitializeLifetimeService。當Lease對象創建之後,Lease Manager會為Lease設置通過上面方式設定的屬性。隨後Lease Manager會每隔一定的時間(由LeaseManagerPollTime設定)輪詢每個Lease,查看Lease是否過期;隨着時間的推移,Lease的租期(InitialLeaseTime - Expired time)越來越少。在這期間,Clien端和Server端獲得該Lease,調用Renew方法來延長Lease的租期;此外,來自Client端的遠程調用也會把Lease的生命週期延長至一個設定的時間(由RenewOnCallTime設定)。

注:只有在Lease的生命週期小於由RenewOnCallTime設定的時間的條件下,遠程調用才會對Lease的租期起作用,或者這樣説:current lease time = MAX(lease time - expired time,renew on call time)

那麼當Lease Manager在獲知某個Lease的已經過期?他會做什麼樣的操作呢?它會馬上結束該Lease嗎?就像我在上一章所舉的租房的例子一樣,房東在房租到期之後,出於人性化的考慮,他會首先通知承租人是否有續租的意願。如果有,可以續約。在Remoting中也存在這樣一種狀況,Client可以在Lease尚未到期的時候,為他註冊一個或多個Sponsor,Lease Manger會首先聯繫註冊到該Lease的Sponsor,如果獲得這樣的Sponsor,則調用Sponsor的Renewal,從而實現續約的目的。由於Sponsor處於Client端所在的Context,Lease Manager調用Sponsor實際上是一種遠程調用,由於遠程調用的不確定性,必須設定Lease Manager聯繫Sponsor的時間範圍(由SponsorshipTimeout屬性設定),如果超出這樣的範圍,則認為Sponsor不可得。

在Remoting中,一個Sponsor是一個是實現了System.Runtime.Remoting.Lifetime. ISponsor Interface的類對象。該接口只有一個成員方法:Renewal。還有一點需要特別説明的是,Spnosor是被設計來被處於Server端的Lease Manager調用的。由於這是一個跨AppDomain的調用,我們知道由於AppDomain的隔離性,在一個AppDomain創建的對象不能在另一個Appdomain中直接調用,需要經過一個Marshal的過程——Marshal By Refence 或者 Marshal By Value。我們一般採用Marshal By Refence的方式,我們經常使用的System.Runtime.Remoting.Lifetime.ClientSponsor就是直接繼承自System. MarshalByRefObject。

 

使用Spring Initializr_使用Spring Initializr

namespace System.Runtime.Remoting.Lifetime
{
    public interface ISponsor
    {        
        TimeSpan Renewal(ILease lease);
    }
}
一旦Lease過期,在既定的時間內,不能獲得對應的Sponsor(或者是Sponsor 的Renewal方法返回TimeSpan.Zero),那麼Lease Manager就把該Lease標記為過期。如果這時有一個遠程調用,Remoting Framework會通過Lease Manager得知Lease已經過期,如果Client Activation模式直接會拋出異常;如果是Singleton 模式的Server Activation,則會創建一個新的對象,原來對象的狀態將不復存在。這裏有一點需要特別注意的是,Lease Manager就把該Lease標記為過期,並不等於該Lease馬上會被垃圾回收掉,同理,這時候雖然遠程對象可能還是存在的,由於這時候我們不能保證調用的安全性——不能確定該對象什麼時候被垃圾回收,對於遠程調用來説,它已經沒有任何意義。
從上面整個流程來看,為了保持遠程對象,我們有Lease對象;為了保持Lease對象,我們有Sponsor對象,那麼什麼對象來保持Sponsor對象呢?那就要依賴於我們的Client代碼了。如果註冊的Sponsor對象如果一直不被回收的話,遠程對象將永遠存在。所以我們應該根據實際的需要,取消Sponsor對Lease的註冊。
下面我們照例來展示一個Sample,在這個Sample中,我們設計一個計數器,獲得某個對象某個方法的調用次數。
Step 1 :整個Solution的構架(Artech.LifetimeManagement.RemoteService被Artech.LifetimeManagement.Client和Artech.LifetimeManagement.Hosting引用)
  

Step 2:遠程對象Artech.LifetimeManagement.RemoteService/CounterService.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting.Lifetime;

namespace Artech.LifetimeManagement.RemoteService
{
    public class CounterService:MarshalByRefObject
    {
        private int _count;

        public int GetCount()
        {
            this._count++;
            return this._count;
        }
        public CounterService()
        {
            Console.WriteLine("Counter Object has been activated!");
        }
        ~CounterService()
        {
            Console.WriteLine("Counter Object has been destroied!");
        }
        public override object InitializeLifetimeService()
        {
            ILease lease = (ILease)base.InitializeLifetimeService();
            if (lease.CurrentState == LeaseState.Initial)
            {
                lease.InitialLeaseTime = TimeSpan.FromSeconds(1);
                lease.RenewOnCallTime = TimeSpan.FromSeconds(1);
                lease.SponsorshipTimeout = TimeSpan.FromSeconds(1);
            }
            return lease;
        }
    }
}
為了確定對象的創建和回收,我們定義了Constructor和重寫了 Finalize方法。GetCouter是 遠程調用的方法,每次調用,技術器一次遞增並返回該計數。為了更容易地演示對象的生命週期,我們重寫了InitializeLifetimeService,設置了一些列短時間的Lease屬性(都為1s)。
Step 3 Host : Artech.LifetimeManagement.Hosting
App.config
 
<?xml versinotallow="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application name="Artech.MyRemoting">
      <service>
        <wellknown type="Artech.LifetimeManagement.RemoteService.CounterService,Artech.
LifetimeManagement.RemoteService"
                  mode="Singleton" objectUri="Counter.rem"></wellknown>
        <activated type="Artech.LifetimeManagement.RemoteService.CounterService,Artech.
LifetimeManagement.RemoteService"></activated>
      </service>
      <channels>
        <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel,System.
Runtime.Remoting, Versinotallow=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
                 port ="1234">
          <serverProviders>
            <provider ref="wsdl" />
            <formatter ref="binary" typeFilterLevel="Full" />
          </serverProviders>
          <clientProviders>
            <formatter ref="binary" />
          </clientProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>
我定義了兩個Service,一個WellKnown Service,一個CAO Service。因為我們將同時比較兩種不同激活方式的生命週期的管理。在前面我們不止一次地説,調用Sponsor是一種遠程調用,説得更確切地,只一種遠程回調(Remote Callback),所以我們要把Type Filter Level設為Full,其原因可以參考我們文章([原創].NET Remoting: 如何通過Remoting實現雙向通信(Bidirectional Communication)),在這裏就不再説明。
Program.cs
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.Remoting;

namespace Artech.LifetimeManagement.Hosting
{
    class Program
    {
        static void Main(string[] args)
        {
            RemotingConfiguration.Configure("Artech.LifetimeManagement.Hosting.exe.config", false);
            Console.WriteLine("Calculator has begun to listen 
 
");
            GarbageCollect();
            Console.Read();
        }

        static void GarbageCollect()
        {
            while (true)
            {
                Thread.Sleep(10000);                
                GC.Collect();
            }
        }
    }
}
通過上面的Code,我們先註冊App.config的配置,為了更加清楚地看清對象的回收時間,我們每隔10s作一次垃圾回收。
Step 4 Client:Artech.LifetimeManagement.Client
App.config
 
<configuration>
  <system.runtime.remoting>
    <application>
      <channels>
        <channel ref="http" port="0">
          <clientProviders>
            <formatter ref="binary" />
          </clientProviders>
          <serverProviders>
            <formatter ref="binary" typeFilterLevel="Full" />
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>
由於處於Server端的Lease Manager要CallbackClient端的Sponsor,Client端必須註冊一個Channel用於回調。同樣把Type Filter Level設為Full。
Program.cs
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;
using Artech.LifetimeManagement.RemoteService;
using System.Threading;
using System.Runtime.Remoting.Lifetime;

namespace Artech.LifetimeManagement.Client
{
    class Program
    {
        const int _invocationFrequency = 4;

        static void Main(string[] args)
        {
           RemotingConfiguration.Configure("Artech.LifetimeManagement.Client.exe.config", false);
            RemotingConfiguration.RegisterActivatedClientType(typeof(CounterService), "http://localhost:1234/Artech.MyRemoting");

            CounterService singletonCounter = Activator.GetObject(typeof(CounterService),"http://localhost:1234/Artech.MyRemoting/Counter.rem")
                 as CounterService;
            CounterService caoCounter = new CounterService();

            Thread singletonThread = new Thread(new ParameterizedThreadStart(InvocateCounterService));
            Thread caoThread = new Thread(new ParameterizedThreadStart(InvocateCounterService));

            singletonThread.Name = "Singleton";
            caoThread.Name = "CAO";
            
            singletonThread.Start(singletonCounter);
            caoThread.Start(caoCounter);
           
        }

        static void InvocateCounterService(object counter)
        {
            CounterService counterService = counter as CounterService;
            while (true)
            {
                try
                {
                    Console.WriteLine("{1}: The count is {0}", counterService.GetCount(), Thread.CurrentThread.Name.PadRight(10));
                    Thread.Sleep(_invocationFrequency * 1000);
                }
                catch (Exception ex)
                {
                    if (Thread.CurrentThread.Name == "Singleton")
                    {
                        Console.WriteLine("Fail to invocate Singleton counter because \"{0}\"", ex.Message);
                        break;
                    }

                    if (Thread.CurrentThread.Name == "CAO")
                    {
                        Console.WriteLine("Fail to invocate CAO counter because \"{0}\"", ex.Message);
                        break;
                    }

                }                         
            }
        }        
    }
}

使用Spring Initializr_使用Spring Initializr

通過上上面的Code,我首先創建了連個Proxy,一個Singleton和一個CAO。然後創建連個線程,並在這兩個線程中以固定的時間間隔(4s:_ invocationFrequency = 4)通過這兩個Proxy進行遠程調用。

現在我們來看看運行的結果:

啓動Hosting:
 

 


啓動Client,等待一段時間:

 

 



再來看Host現在的顯示:

 


我們可以看到,對於Singleton Proxy,調用沒有出現異常,但是調用的計數器不沒有維持一個持續的增長——從1到3,然後又從1-3,這樣周而復始,這就證明了,沒3次調用的遠程對象不是同一個,當Lease過期之後,一個新的遠程對象被創建。從Host 的輸出也驗證了這點,遠程對象在不斷地被創建。還有一個有意思的是,調用了3次Constructor之後才開始調用Finalizer方法,這説明了什麼呢?這説明了,Lease過期後的調用,會導致新的遠程對象的創建,但實際上這是,該遠程對象還沒有被回收。它在連續創建了3個新的對象後,才真正被垃圾回收。

而對於CAO Proxy,則不同,在第4次調用時,出現Exception,這Lease過期,再調用某個遠程方法,會直接拋出Exception。

1.通過遠程調用來延長遠程對象的生命週期

通過我們開始的分析,在Lease的租約小於renew on call time,遠程調用會使租約延長。按照這樣理論,如果我們提高遠程調用的頻率,我們可以延長遠程對象的生命週期。基於這樣的想法,我們把效用的時間間隔從原來的4縮短為1s(_ invocationFrequency = 1)。再次運行。

Client端:



Host:

 

正如我們想得一樣,無論是對於Singleton Proxy還是CAO Proxy,計數器一直維持一個持續增加的趨勢,並且沒有Exception拋出。從Client端就可以看出,Singleton Proxy和CAO Proxy調用的始終是一個遠程對象,而Host的輸出更是確鑿地證明了,從始到終,只有連個對象被創建,一個對於Singleton,另一個對於CAO。

2.通過Lease來延長生命週期

上面我們通過遠程調用來延長遠程對象的生命週期,現在我們採用另一種方法,直接利用Lease對象來延長遠程對象的生命週期。我們改動Client的代碼:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;
using Artech.LifetimeManagement.RemoteService;
using System.Threading;
using System.Runtime.Remoting.Lifetime;

namespace Artech.LifetimeManagement.Client
{
    class Program
    {
        const int _invocationFrequency = 4;
        const int _leaseRenewalFrequency = 1;

        static void Main(string[] args)
        {
           RemotingConfiguration.Configure("Artech.LifetimeManagement.Client.exe.config", false);
            RemotingConfiguration.RegisterActivatedClientType(typeof(CounterService), "http://localhost:1234/Artech.MyRemoting");

            CounterService singletonCounter = Activator.GetObject(typeof(CounterService),"http://localhost:1234/Artech.MyRemoting/Counter.rem")
                 as CounterService;
            CounterService caoCounter = new CounterService();

            Thread singletonThread = new Thread(new ParameterizedThreadStart(InvocateCounterService));
            Thread caoThread = new Thread(new ParameterizedThreadStart(InvocateCounterService));

Thread singletonLeaseRennewal = new Thread(new ParameterizedThreadStart(ExtendLifetimeViaLease));
            Thread caoLeaseRenewal = new Thread(new ParameterizedThreadStart(ExtendLifetimeViaLease));

            singletonThread.Name = "Singleton";
            caoThread.Name = "CAO";
            
            singletonThread.Start(singletonCounter);
            caoThread.Start(caoCounter);

  singletonLeaseRennewal.Start(singletonCounter);
            caoLeaseRenewal.Start(caoCounter);
           
        }

static void ExtendLifetimeViaLease(object counter)
        { 
            CounterService counterService = counter as CounterService;
            ILease lease = (ILease)RemotingServices.GetLifetimeService(counterService);
            while (true)
            {
                if (lease == null)
                {
                    Console.WriteLine("Can not retrieve the lease!");
                    break;
                }

                lease.Renew(TimeSpan.FromSeconds(_leaseRenewalFrequency));
                Thread.Sleep(_leaseRenewalFrequency);
            }
        }

        static void InvocateCounterService(object counter)
        {
            CounterService counterService = counter as CounterService;
            while (true)
            {
                try
                {
                    Console.WriteLine("{1}: The count is {0}", counterService.GetCount(), Thread.CurrentThread.Name.PadRight(10));
                    Thread.Sleep(_invocationFrequency * 1000);
                }
                catch (Exception ex)
                {
                    if (Thread.CurrentThread.Name == "Singleton")
                    {
                        Console.WriteLine("Fail to invocate Singleton counter because \"{0}\"", ex.Message);
                        break;
                    }

                    if (Thread.CurrentThread.Name == "CAO")
                    {
                        Console.WriteLine("Fail to invocate CAO counter because \"{0}\"", ex.Message);
                        break;
                    }

                }                         
            }
        }        
    }
}
在上面的代碼中,我通過ExtendLifetimeViaLease方法每個一定的時間(1s:_leaseRenewalFrequency = 1)對獲得的Lease Renew 一次。  
static void ExtendLifetimeViaLease(object counter)
        { 
            CounterService counterService = counter as CounterService;
            ILease lease = (ILease)RemotingServices.GetLifetimeService(counterService);
            while (true)
            {
                if (lease == null)
                {
                    Console.WriteLine("Can not retrieve the lease!");
                    break;
                }

                lease.Renew(TimeSpan.FromSeconds(_leaseRenewalFrequency));
                Thread.Sleep(_leaseRenewalFrequency);
            }
        }
象原來的例子一樣,分別在連個線程中以一定時間間隔(4s)調用遠程對象,不過這次我們創建兩個新的線程不同對Lease進行Renew,這樣確保Lease用不過期。實驗證明,輸出結果和上面完全一樣。
3.通過Sponsor來延長生命週期 
不説廢話直接來看代碼:
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;
using Artech.LifetimeManagement.RemoteService;
using System.Threading;
using System.Runtime.Remoting.Lifetime;

namespace Artech.LifetimeManagement.Client
{
    class Program
    {
        const int _invocationFrequency = 4; 
        static ISponsor _singletonSponsor;
        static ISponsor _caoSponsor;

        static void Main(string[] args)
        {
            RemotingConfiguration.Configure("Artech.LifetimeManagement.Client.exe.config", false);
            RemotingConfiguration.RegisterActivatedClientType(typeof(CounterService), "http://localhost:1234/Artech.MyRemoting");
            CounterService singletonCounter = Activator.GetObject(typeof(CounterService),"http://localhost:1234/Artech.MyRemoting/Counter.rem")
                 as CounterService;
            CounterService caoCounter = new CounterService();

            Thread singletonThread = new Thread(new ParameterizedThreadStart(InvocateCounterService));
            Thread caoThread = new Thread(new ParameterizedThreadStart(InvocateCounterService));            

            singletonThread.Name = "Singleton";
            caoThread.Name = "CAO";
            
            singletonThread.Start(singletonCounter);
            caoThread.Start(caoCounter);

            _singletonSponsor = ExtendLifetimeViaSponsor(singletonCounter);
            _caoSponsor = ExtendLifetimeViaSponsor(caoCounter);
           
        }

        static void InvocateCounterService(object counter)
        {
            CounterService counterService = counter as CounterService;
            while (true)
            {
                try
                {
                    Console.WriteLine("{1}: The count is {0}", counterService.GetCount(), Thread.CurrentThread.Name.PadRight(10));
                    Thread.Sleep(_invocationFrequency * 1000);
                }
                catch (Exception ex)
                {
                    if (Thread.CurrentThread.Name == "Singleton")
                    {
                        Console.WriteLine("Fail to invocate Singleton counter because \"{0}\"", ex.Message);
                        break;
                    }

                    if (Thread.CurrentThread.Name == "CAO")
                    {
                        Console.WriteLine("Fail to invocate CAO counter because \"{0}\"", ex.Message);
                        break;
                    }

                }                         
            }
        }


        static ISponsor ExtendLifetimeViaSponsor(CounterService counter)
        {
            CounterService counterService = counter as CounterService;
            ILease lease = (ILease)RemotingServices.GetLifetimeService(counterService);
            ClientSponsor sponsor = new ClientSponsor(TimeSpan.FromSeconds(4));
            sponsor.Register(counterService);
            return sponsor;
        }
    }
}

使用Spring Initializr_使用Spring Initializr

上面的代碼中,我通過ExtendLifetimeViaSponsor方法為Lease註冊一個Sposnor,並把Renew時間設為4s,最後把該Sposnor負值給一個靜態變量,這樣他不會被垃圾回收。那麼每次Lease Manager獲得該Sponsor時候,會自動把Lease 的租期變為4s。這樣遠程對象將會永久存活。可以想象,輸出結果將會和上面一樣。  

我所理解的Remoting(3):CAO Service Factory使接口和實現相分離

我們知道對於Remoting,有兩種不同的Activation模式:Server Activation和Client Activation。他我在前面的系列文章中分析、比較了這兩種不同激活方式的區別:Marshaling方式,遠程對象創建的時機,狀態的保持,生命週期的管理。 在編程模式方面Server Activation和Client Activation也具有一定的差異:為一個SAO(server activated object)和一個CAO(client activated object)註冊一個遠程對象類型的方式是不同的(Wellknown Service Type Re V.S. Activated Type Registration);為為一個SAO(server activated object)和一個CAO(client activated object)創建Proxy的方式也不一樣:對於SAO,一般通過Activator的靜態方法GetObject(傳入一個遠程對象的地址);而我們一般通過new 關鍵字或者Activator的靜態方法CreateInstance。

 

使用Spring Initializr_使用Spring Initializr

String remoteAddress = "http://localhost/Artech.CAOFactory/CounterFactory.rem";        

使用Spring Initializr_使用Spring Initializr

ICounter counter = ICounterFactory counterFactory = (ICounterFactory)Activator.GetObject(typeof(ICounterFactory), remoteAddress);

使用Spring Initializr_使用Spring Initializr

對於Client Activation,由於我們在創建Proxy對象的時候,必須利用遠程對象對應的原數據,所以在Client端,需要引用遠程的對象所對應的dll。比如我們現在做一個簡單的計數器的例子(Client遠程調用獲得計數器當前的計數)我們把業務邏輯封裝在Counter Service的實體中。下圖反映了這樣一種架構的依賴關係。 
 


經驗豐富的開發人員很快會意識到這是一種很不好的分佈式構架。從SOA的角度來講也是不值得推薦的構架方式。SOA崇尚的是Contract層面的共享,而拒絕Type層面的共享。Common Type增加了交互雙方的依賴性,造成的緊耦合。所以我們一般從Service中把相對靜態的Contract(可以簡單地把 Contract看成是Service提供的所有操作的列表和調用的接口)提取出來,作為雙方交互的契約:Client只要滿足這個Contract,它就能夠調用相應的Service,而Service 真正實現的改變對Client沒有任何的影響,實際上Service的實現對於Client來説是完全透明的。我們可以説基於Contract的共享成就了SOA的鬆耦合。通過提取Contract之後,各個實體成為了下面一種依賴關係。

 

 



但是對於Client Activation,要直接實現這樣的構架是不可能的。我們已經説過,Client創建一個CAO Proxy,需要和Host端註冊的遠程類型對應的原數據,換句話説,如果遠程類型實現在CounterService的dll中,Host和Client雙方都需要引用這個dll——雖然實現部分的代碼對Client毫無意義。但是現在我們的目的的吧這個dll僅僅駐留在Host中,Client只需引用存儲Contract的dll。

在一個分佈式環境中,一個Application要跨AppDomain調用一個駐留在另一個AppDomain的的方法,他不需要獲得這個真正的遠程對象(而實事上它也不可能獲得在另一個AppDomain中創建的對象),它只需要獲得該對象的一個引用(説得具體點,它只需要獲得該對象的ObjRef),並根據這個引用創建相應的Proxy來進行遠程調用。或者説,我們只要通過某種方法把Server端創建的對象通過Marshaling傳遞到Client端,Client就可以進行遠程調用了。

那麼如何為一個遠程調用從另一個AppDomain中獲取一個遠程對象的引用並創建Proxy呢?而這個獲取的方式本身也是一個遠程調用。我們的做法是:通過一個基於SAO的遠程調用獲取一個遠程對象的引用並同時創建Proxy。而這個Proxy對應的遠程對象就像當於一個CAO.

下面是我們的解決方案簡要的類圖。我們整個基於計數器的Service封裝在CounterService中,它實現了ICounter接口,CounterFactoryService用於創建一個CounterService對象,它實現的接口是ICounterFactory。
 

 



現在我們就來實現它:

Step 1: 建立這個Solution的整體結構

整個Solution包含3個Project:Artech.CAOFactory.Contract;Artech.CAOFactory.Service;Artech.CAOFactory.Client。Artech.CAOFactory.Contract被Artech.CAOFactory.Service和Artech.CAOFactory.Client引用。我們使用IIS的Host方式,從而省略了Host Application。
 

Step 2 創建Contract
ICounter
 
using System;
using System.Collections.Generic;
using System.Text;

namespace Artech.CAOFactory.Contract
{
  public   interface ICounter
    {
      int GetCount();
    }
}
ICounterFactory
 
using System;
using System.Collections.Generic;
using System.Text;

namespace Artech.CAOFactory.Contract
{
    public interface ICounterFactory
    {
        ICounter CreateService();
    }
}
Step 3 實現Contract:Artech.CAOFactory.Service
 
using System;
using System.Collections.Generic;
using System.Text;
using Artech.CAOFactory.Contract;

namespace Artech.CAOFactory.Service
{
    public class CounterService : MarshalByRefObject,ICounter
    {
        private int _count;

        ICounter Members
    }
}
CounterFactoryService
using System;
using System.Collections.Generic;
using System.Text;
using Artech.CAOFactory.Contract;

namespace Artech.CAOFactory.Service
{
    public class CounterFactoryService :MarshalByRefObject, ICounterFactory
    {
        
        ICounterFactory Members
    }
}
Step 3 通過IIS Host CounterFactoryService
修改編譯配置把Dll生成在Project根目錄的bin目錄下,基於這個根目錄創建虛擬目錄(假設Alias就是Artech.CAOFactory),並添加Web.config。
<?xml versinotallow="1.0"?>
<configuration>
    <configSections>
        <section name="WorkflowRuntime" type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Versinotallow=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </configSections>
    <WorkflowRuntime Name="WorkflowServiceContainer">
        <Services>
            <add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Versinotallow=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
            <add type="System.Workflow.Runtime.Hosting.DefaultWorkflowCommitWorkBatchService, System.Workflow.Runtime, Versinotallow=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        </Services>
    </WorkflowRuntime>
    <appSettings/>
    <connectionStrings/>
    <system.web>        
        <compilation debug="false"/>        
        <authentication mode="Windows"/>        
    </system.web>
    <system.runtime.remoting>
        <application>
            <service>
                <wellknown type="Artech.CAOFactory.Service.CounterFactoryService, Artech.CAOFactory.Service"
                           mode ="SingleCall" objectUri="CounterFactory.rem"></wellknown>
            </service>
        </application>
    </system.runtime.remoting>
</configuration>
Step 4 創建客户端Artech.CAOFactory.Client
Program
 
using System;
using System.Collections.Generic;
using System.Text;
using Artech.CAOFactory.Contract;
using System.Threading;
using System.Runtime.Remoting;

namespace Artech.CAOFactory.Client
{
    class Program
    {
        const string REMOTE_ADDRESS = "http://localhost/Artech.CAOFactory/CounterFactory.rem";
        static void Main(string[] args)
        {
            ICounterFactory counterFactory = (ICounterFactory)Activator.GetObject(typeof(ICounterFactory), REMOTE_ADDRESS);
            ICounter counter = counterFactory.CreateService();

            while (true)
            {
                Console.WriteLine("The current value of the counter is {0}", counter.GetCount());
                Thread.Sleep(5000);
            }
        }
    }
}

使用Spring Initializr_使用Spring Initializr

從上面的代碼我們可以看到,我們希望使用的遠程對象的Proxy(counter),是通過另一個SongleCall Proxy(counterFactory)獲得的。

下面來運行,從輸出結果來看,和我們平常使用的SAO方式的結果沒有什麼兩樣——同一個Proxy之間的調用狀態被保留。

好文太多了,拿來主義!!!以後能夠厚積薄發嗎?