Remoting基本原理及其扩展机制(下)
应用程序域
应用程序域(通常简称为AppDomain)可以视为一种轻量级进程。一个Windows进程内可以包含多个AppDomain。AppDomain这个概念的提出是为了实现在一个物理服务器中承载多个应用程序,并且这些应用能够相互独立。ASP.NET中利用AppDomain在同一个进程内承载了多组Web应用程序就是一个例子。实际上微软曾进行过在单一进程内承载多达1000个简单Web应用程序的压力测试。
使用AppDomain所获得的性能优势主要体现在两方面:
- 创建AppDomain所需要的系统资源比创建一个Windows进程更少。
- 同一个Windows进程内所承载的AppDomain之间可以互相共享资源,如CLR、基本.NET类型、地址空间以及线程。
而各个AppDomain之间的独立性体现为以下这些特征:
- 一个AppDomain可以独立于其他的AppDomain而被卸载。
- 一个AppDomain无法访问其他AppDomain的程序集和对象。
- 若没有发生跨边界的异常抛出,一个AppDomain拥有自己独立的异常管理策略。这意味着一个AppDomain内出现问题不会影响到同一个进程内中的其他AppDomain。
- 每个AppDomain可以分别定义独自的程序集代码访问安全策略。
- 每个AppDomain可以分别定义独自的规则以便CLR在加载前定位程序集所在位置。
可以看出应用程序域是进程中的一个子单元,不过在.NET中还存在一个比应用程序域还要细粒度的单元——.NET上下文(Context)。
.NET Context
一个.NET 应用程序域能够包含多个被称为.NET上下文的实体。所有.NET对象都存在于上下文中,每个应用程序域中至少存在一个上下文。这个上下文称为应用程序域的默认上下文,它在应用程序域创建的时候就创建了。下图总结了它们之间的关系:
那么MessageSink与上下文有什么关系呢? 我们知道在通常情况下,如果访问同一个AppDomain中对象的方法时,会采用基于栈的方式(详见本系列上部)。在这种情况下,我们是无法拦截其中的消息的,因为此时根本不存在消息对象。只有当我们通过Transparent Proxy访问另一个对象的方法时,才会采用基于消息的方式。而现在我们只知道当一个对象调用处在另一个AppDomain中的远程对象(该对象为MarshalByRefObject子类)时,Remoting才会为调用方创建那个远程对象的Transparent Proxy。在了解了.NET上下文的概念后,你会发现,即使处在同一个AppDomain中的两个对象,如果它们所处的上下文不同,在访问对方的方法时,也会借由Transparent Proxy实现,即采用基于消息的方法调用方式。此时,我们就可以在上下文中插入MessageSink了。那么在上下文中是否存在类似IClientChannelSinkProvider的接口呢?很幸运,在经过一番探索后,我们发现确实存在类似的接口,而且还不止一个:IContributeEnvoySink、IContributeServerContextSink、IContributeObjectSink、IContributeClientContextSink这四个接口中各自包含了一个GetXXXSink的方法,它们都会返回一个实现了IMessageSink接口的对象。我们知道IClientChannelSinkProvider接口是配合配置文件最终实现向Pipeline中加入ChannelSink的,而以上这四个接口并没有配合配置文件使用,不过与IClientChannelSinkProvider的使用方式倒也有异曲同工之效。读者可以将下面这幅图与本系列上部中的图2比较一番。
在上图中又出现了很多新的概念,下面将对它们一一做出解释:
ContextBoundObject
上下文可以看作应用程序域中一个包含对象和消息接收器的区域。对上下文里的对象的调用会转换成可以被MessageSink(消息接收器)拦截和处理的消息。我们知道要把调用转换成消息,必须通过透明代理这个中介。而且,仅当对象是MarshalByRefObject的子类的实例并被其所在的应用程序域以外的实体调用时,CLR才会为它创建透明代理。这里,我们希望对所有调用使用消息接收器机制,即使那些调用是来自同一个应用程序域中的实体。这个时候我们就需要用到System.ContextBoundObject类了。继承自ContextBoundObject的类的实例同样仅能由透明代理访问。此时,即使在这个类的方法中使用的this引用也是透明代理而不是对这个对象的直接引用。我们会发现ContextBoundObject类继承自MarshalByRefObject,这非常合理,因为它很好地强调了该类的特性——它告诉CLR这个类将会通过透明代理使用。
ContextBoundObject的子类的实例被视为上下文绑定的(context-bound)。没有继承自ContextBoundObject的类的实例则被视为上下文灵活的(context-agile)。上下文绑定的对象永远在其上下文中执行。只要不是远程对象,上下文灵活的对象总是在执行这个调用的上下文中执行。如下图所示:
ContextAttribute
上下文attribute是应用在上下文绑定的类上的.NET attribute。上下文attribute类实现了System.Runtime.Remoting.Contexts.IContextAttribute接口。上下文绑定的类可以应用多个上下文attribute。在这个类的对象创建期间,这个类的每个上下文attribute判断这个对象的创建者所在的上下文是否适用。该操作通过以下方法完成:
public bool IContextAttribute.IsContextOK(Context clientCtx,
IConstructionCallMessage ctorMsg)
只要其中一个上下文attribute返回false,CLR就必须创建一个新的上下文来容纳这个新的对象。这样,每个上下文attribute可以在这个新的上下文中注入一个或多个上下文属性。这些注入通过以下方法完成:
public void IContextAttribute.GetPropertiesForNewContext(
IConstructionCallMessage ctorMsg)
IContextProperty
上下文属性是实现System.Runtime.Remoting.Contexts.IContextProperty接口的类的实例。每个上下文可以包含多个属性。上下文属性在上下文创建的时候通过上下文attribute注入。一旦每个上下文attribute注入了它的属性,就会为每个属性调用下面的方法。此后就无法在这个上下文中注入另外的属性了:
public void IContextProperty.Freeze( Context ctx )
然后,CLR通过调用下面的方法判断新的上下文能否满足每个属性:
public bool IContextProperty.IsNewContextOK( Context ctx )
每个上下文属性都有一个通过Name属性定义的名称:
public string IContextProperty.Name{ get }
上下文中承载的对象的方法可以通过调用下面的方法访问上下文属性:
IContextProperty Context.GetProperty( string sPropertyName )
这一点很有意思,上下文中的对象通过它们所在的上下文的属性可以共享信息并访问服务。不过,上下文属性的主要作用并不在于此。上下文属性的主要作用在于向相关上下文中的消息接收器区域注入消息接收器(MessageSink)。(消息接收器区域的概念将在后面介绍)
以上注入MessageSink的过程可以用下图概括:
MessageSink Region
不知你是否记得之前提到的四个接口:IContributeEnvoySink、IContributeServerContextSink、IContributeObjectSink、IContributeClientContextSink。其实它们分别代表了四个不同的消息接收器区域:服务器(server)区域、对象(object)区域、信使(envoy)区域和客户端(client)区域。要理解区域概念,你必须考虑上下文绑定的对象是否被位于另一个上下文的实体调用。这个实体可以是一个静态方法或者另一个对象。在我们关于区域的讨论中,我们把这个实体所在的上下文称为调用方上下文(calling context),而把被调用对象所在的上下文称为目标上下文(target context)。目标上下文中的每个属性都可以在这些区域中注入消息接收器。
- 注入服务器区域的消息接收器拦截所有从另一个上下文发往目标上下文中所有对象的调用消息。于是,每个目标上下文有一个服务器区域。
- 注入对象区域的消息接收器拦截所有从另一个上下文发往目标对象中特定对象的调用消息。于是,上下文中每个对象会有一个对象区域。
- 注入信使区域的消息接收器拦截所有从另一个上下文发往目标对象中特定对象的调用消息。信使区域和对象区域的不同点是信使区域位于调用方上下文而不是包含对象的目标上下文。我们使用信使区域把调用方上下文的信息传递给目标上下文的消息接收器。
- 注入客户端区域的消息接收器拦截所有从目标上下文发往位于其他上下文的对象的调用消息。于是,每个目标上下文有一个客户端区域。你可能会对这个区域所处的位置有点困惑,似乎当它位于Calling context的信使区域下方时会显得更加对称。之所以会有这样的误解,是因为我们对Server、Client的理解有了偏差。你应该记住除了信使区域是位于Calling context外,另外三个区域都是处在Target Context。而所谓的Server,Client是针对处在Target Context中的对象在某不同时刻所扮演不同角色而言的。当然Calling context中也会有客户端区域,不过其中的MessageSink不是通过Target context的属性注入的,而应该依靠Calling context中的上下文属性注入。
下载清晰版本
上图说明了区域的概念。目标上下文包含名为OBJ1和OBJ2的两个对象。我们选择在目标上下文中放置两个对象而不是一个是为了更好地说明对象区域和信使区域是在对象层面与消息的拦截关联起来的,而服务器区域和客户端区域则是在上下文层面与消息的拦截关联起来的。
我们在每个区域中放置了两个自定义消息接收器是为了更好地说明一个区域能包含零个、一个或多个消息接收器。具体地说,所有自定义消息接收器都通过目标上下文的属性注入区域,即使这个区域不属于目标上下文。因为你可以定义你自己的上下文属性类,你可以选择必须注入哪个消息接收器。
你可能注意到每个区域都包含一个用于通知CLR退出区域的系统终结器接收器(system terminator sink),它是由Remoting框架定义的,并且总是位于每个区域的末尾。
当调用方上下文和目标上下文处在同一个应用程序域中时,CLR会使用mscorlib.dll中CrossContextChannel内部类的实例作为信道。这个实例会使得当前线程的Context属性发生切换。图中也展示了这一实例。