我所理解的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相关的术语,不当的之处,还往见谅。
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;
}
}
}
Code 很简单——一个Constructor用于确定Remote Object到底在什么时候创建或者说的专业一点,真正的Remote Object什么时候被Activated。一个GetCallCount用所Add方法调用次数的计数器,这个计数器用来验证Remoting 的Instance Management。和一个执行加法运算的方法,注意执行一次我们的保存调用次数的变量就是加1。
3. Host CalculatorService
App.Config
<?
xml version="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, Version=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));
}
}
}
这里有必要多说几句:
我们首先创建基于3种不模式的Proxy,为了弄清楚整个Activation的流程,对于每个操作都为它显示出具体的执行时间,通过操作的执行会间隔一段时间(我给它指定的是10s)
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();
依次调用三个Proxy的Add方法显示调用的准确时间
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());
现在我们首先启动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"
sponsorshipTimeout
="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;
}
通过上面的讲述,我的应该对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。
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 version="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, Version=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;
}
}
}
}
}
}
通过上上面的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;
}
}
}
上面的代码中,我通过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。
String remoteAddress
=
"
http://localhost/Artech.CAOFactory/CounterFactory.rem
"
;
ICounter counter
=
ICounterFactory counterFactory
=
(ICounterFactory)Activator.GetObject(
typeof
(ICounterFactory), remoteAddress);
对于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 version="1.0"
?>
<
configuration
>
<
configSections
>
<
section
name
="WorkflowRuntime"
type
="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/>
</
configSections
>
<
WorkflowRuntime
Name
="WorkflowServiceContainer"
>
<
Services
>
<
add
type
="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/>
<
add
type
="System.Workflow.Runtime.Hosting.DefaultWorkflowCommitWorkBatchService, System.Workflow.Runtime, Version=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);
}
}
}
}
从上面的代码我们可以看到,我们希望使用的远程对象的Proxy(counter),是通过另一个SongleCall Proxy(counterFactory)获得的。
下面来运行,从输出结果来看,和我们平常使用的SAO方式的结果没有什么两样——同一个Proxy之间的调用状态被保留。
我所理解的Remoting(1):Marshaling & Activation - Part I
对任何一项分布式技术(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,他们是可以由自定义参数的构造函数的。
我所理解的Remoting(1):Marshaling & Activation - Part II
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相关的术语,不当的之处,还往见谅。
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;
}
}
}
Code 很简单——一个Constructor用于确定Remote Object到底在什么时候创建或者说的专业一点,真正的Remote Object什么时候被Activated。一个GetCallCount用所Add方法调用次数的计数器,这个计数器用来验证Remoting 的Instance Management。和一个执行加法运算的方法,注意执行一次我们的保存调用次数的变量就是加1。
3. Host CalculatorService
App.Config
< 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, Version=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.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.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));
}
}
}
这里有必要多说几句:
我们首先创建基于3种不模式的Proxy,为了弄清楚整个Activation的流程,对于每个操作都为它显示出具体的执行时间,通过操作的执行会间隔一段时间(我给它指定的是10s)
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();
依次调用三个Proxy的Add方法显示调用的准确时间
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 Singleton remote object is {0} " , singletonCalculator.GetCallCount());
Console.WriteLine( " The times to invovate the current CAO remote object is {0} " , caoCalculator.GetCallCount());
现在我们首先启动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.LeaseTime = TimeSpan.FromMinutes( 2 );
LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes( 2 );
LifetimeServices.SponsorshipTimeout = TimeSpan.FromMinutes( 2 );
2. 通过Configuration的方式
leaseManagerPollTime ="7S" />
3. 定制单个MarshalByRefObject 对象的Lease 时间:Override 继承自MarshalByRefObject 的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;
}
通过上面的讲述,我的应该对Remoting对象生命基于Lease和Sponsorship的生命周期的管理有一个感性的认识。实际上Sponsorship远非这么简单,对Sponsorship的深入探讨,有兴趣话可以关注:[原创]我所理解的Remoting (2) :远程对象的生命周期管理-Part II .
我所理解的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的类的对象。
{
// 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。
{
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.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
< 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, Version=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.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
< 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.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;
}
}
}
}
}
}
通过上上面的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.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 一次。
{
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.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;
}
}
}
我所理解的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。
ICounter counter = ICounterFactory counterFactory = (ICounterFactory)Activator.GetObject( typeof (ICounterFactory), remoteAddress);
对于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.Collections.Generic;
using System.Text;
namespace Artech.CAOFactory.Contract
{
public interface ICounter
{
int GetCount();
}
}
ICounterFactory
using System.Collections.Generic;
using System.Text;
namespace Artech.CAOFactory.Contract
{
public interface ICounterFactory
{
ICounter CreateService();
}
}
Step 3 实现Contract:Artech.CAOFactory.Service
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.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。
< configuration >
< configSections >
< section name ="WorkflowRuntime" type ="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</ configSections >
< WorkflowRuntime Name ="WorkflowServiceContainer" >
< Services >
< add type ="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
< add type ="System.Workflow.Runtime.Hosting.DefaultWorkflowCommitWorkBatchService, System.Workflow.Runtime, Version=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.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);
}
}
}
}
从上面的代码我们可以看到,我们希望使用的远程对象的Proxy(counter),是通过另一个SongleCall Proxy(counterFactory)获得的。
下面来运行,从输出结果来看,和我们平常使用的SAO方式的结果没有什么两样——同一个Proxy之间的调用状态被保留。