.Net Remoting(应用程序域) - Part.1

简介: .Net Remoting(应用程序域) - Part.1 引言 在互联网日渐普及,网络传输速度不断提高的情况下,分布式的应用程序是软件开发的一个重要方向。在.Net中,我们可以通过Web Service 或者Remoting 技术构建分布式应用程序(除此还有新一代的WCF,Windows Communication Foundation)。

.Net Remoting(应用程序域) - Part.1

引言

在互联网日渐普及,网络传输速度不断提高的情况下,分布式的应用程序是软件开发的一个重要方向。在.Net中,我们可以通过Web Service 或者Remoting 技术构建分布式应用程序(除此还有新一代的WCF,Windows Communication Foundation)。本文将简单介绍Remoting的一些基本概念,包括 应用程序域、Remoting构架、传值封送(Marshal by value)、传引用封送(Marshal by reference)、远程方法回调(Callback)、分别在Windows Service和IIS中寄宿宿主程序,最后我们介绍一下远程对象的生存期管理。

理解Remoting

1.应用程序域基本概念

.Net中的很多概念都是环环相扣的,如果一个知识点没有掌握(套用一下数据结构中“前驱节点”这个术语,那么这里就是“前驱知识点”),就想要一下子理解自己当前所直接面临问题,常常会遇到一些障碍而无法深入下去,或者是理解的浅显而不透彻(知道可以这样做,不知道为什么会是这样。如果只是应急,需要快速应用,这样也未尝不可)。为了更好地理解Remoting,我们也最好先了解一下Remoting的前驱知识点 -- 应用程序域。

我们知道所有的.Net 应用程序都运行在托管环境(managed environment)中,但操作系统只提供进程(Process)供程序运行,而进程只是提供了基本的内存管理,它不了解什么是托管代码。所以托管代码,也可以说是我们创建的.Net程序,是无法直接运行在操作系统进程中的。为了使托管代码能够运行在非托管的进程之上,就需要有一个中介者,这个中介者可以运行于非托管的进程之上,同时向托管代码提供运行的环境。这个中介者就是 应用程序域(Application Domain,简写为App Domain)。所以我们的.Net程序,不管是Windows窗体、Web窗体、控制台应用程序,又或者是一个程序集,总是运行在一个App Domain中。

如果只有一个类库程序集(.dll文件),是无法启动一个进程的(它并非可执行文件)。所以,创建进程需要加载一个可执行程序集(Windows 窗体、控制台应用程序等.exe文件)。当可执行程序集加载完毕,.Net会在当前进程中创建一个新的应用程序域,称为 默认应用程序域。一个进程中只会创建一个默认应用程序域,这个应用程序域的名称与程序集名称相同。默认应用程序域不能被卸载,并且与其所在的进程同生共灭。

那么应用程序域是如何提供托管环境的呢?简单来说,应用程序域只是允许它所加载的程序集访问由.Net Runtime所提供的服务。这些服务包括托管堆(Managed Heap),垃圾回收器(Garbage collector),JIT 编译器等.Net底层机制,这些服务本身(它们构成了.Net Runtime)是由非托管C++实现的。

在一个进程中可以包含多个应用程序域,一个应用程序域中可以包含多个程序集。比如说,我们的Asp.Net应用程序都运行在aspnet_wp.exe(IIS5.0)或者w3wp.exe(IIS6.0)进程中,而IIS下通常会创建多个站点,那么是为每个站点都创建一个独立的进程么?不是的,而是为每个站点创建其专属的应用程序域,而这些应用程序域运行在同一个进程(w3wp.exe或aspnet_wp.exe)中。这样做起码有两个好处:1、在一个进程中创建多个App Domain要比创建和运行多个进程需要少得多系统开销;2、实现了错误隔离,一个站点如果出现了致命错误导致崩溃,只会影响其所在的应用程序域,而不会影响到其他站点所在的应用程序域。

2.应用程序域的基本操作

在.Net 中,将应用程序域封装为了AppDomain类,这个类提供了应用程序域的各种操作,包含 加载程序集、创建对象、创建应用程序域 等。通常的编程情况下下,我们几乎从不需要对AppDomain进行操作,这里我们仅看几个本文会用到的、有助于理解和调试Remoting的常见操作:

1.获取当前运行的代码所在的应用程序域,可以使用AppDomain类的静态属性CurrentDoamin,获取当前代码所在的应用程序域;或者使用Thread类的静态方法GetDomain(),得到当前线程所在的应用程序域:

AppDomain currentDomain = AppDomain.CurrentDomain;
AppDomain currentDomain = Thread.GetDomain();

NOTE:一个线程可以访问进程中所包含的所有应用程序域,因为虽然应用程序域是彼此隔离的,但是它们共享一个托管堆(Managed Heap)。

2.获取应用程序域的名称,使用AppDomain的实例只读属性,FriendlyName:

string name = AppDomain.CurrentDomain.FriendlyName;

3.从当前应用程序域中创建新应用程序域,可以使用CreateDomain()静态方法,并传入一个字符串,作为新应用程序域的名称(亦即设置FriendlyName属性):

AppDomain newDomain = AppDomain.CreateDomain("New Domain");

4.在应用程序域中创建对象,可以使用AppDomain的实例方法CreateInstanceAndUnWrap()或者CreateInstance()方法。方法包含两个参数,第一个参数为类型所在的程序集,第二个参数为类型全称(这两个方法后面会详述):

DemoClass obj = (DemoClass)AppDomain.CurrentDomain.CreateInstanceAndUnWrap("ClassLib", "ClassLib.DemoClass");

ObjectHandle objHandle = AppDomain.CurrentDomain.CreateInstance("ClassLib", "ClassLib.DemoClass");
DemoClass obj = (DemoClass)objHandle.UnWrap(); 

5.判断是否为默认应用程序域:

newDomain.IsDefaultAppDomain()

3.在默认应用程序域中创建对象

开始之前我们先澄清一个概念,请看下面一段代码:

class Program{
    static void Main(string[] args) {
        MyClass obj = new MyClass();
        obj.DoSomething();
    }
}

此时我们说obj 是服务对象,Program是客户程序,而不管obj位于什么位置。

接下来我们来看一个简单的范例,我们使用上面提到基于AppDomain的操作,在当前的默认应用程序域中创建一个对象。我们先创建一个类库项目ClassLib,然后在其中创建一个类DemoClass,这个类的实例即为我们将要创建的对象:

namespace ClassLib {
    public class DemoClass {
        private int count = 0;

        public DemoClass() {
            Console.WriteLine("\n======= DomoClass Constructor =======");
        }

        public void ShowCount(string name) {
            count++;
            Console.WriteLine("{0},the count is {1}.", name, count);
        }
       
        // 打印对象所在的应用程序域
        public void ShowAppDomain() {
            AppDomain currentDomain = AppDomain.CurrentDomain;
            Console.WriteLine(currentDomain.FriendlyName);
        }
    }
}

接下来,我们再创建一个控制台应用程序,将项目命名为ConsoleApp,引用上面创建的类库项目ClassLib,然后添加如下代码:

class Program {
    static void Main(string[] args) {
        Test1();
    }

    // 在当前AppDomain中创建一个对象
    static void Test1() {
        AppDomain currentDomain = AppDomain.CurrentDomain;  // 获取当前应用程序域
        Console.WriteLine(currentDomain.FriendlyName);      // 打印名称

        DemoClass  obj;
        // obj = new DemoClass()  // 常规的创建对象的方式

        // 在默认应用程序域中创建对象
        obj = (DemoClass)currentDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");

        obj.ShowAppDomain();
        obj.ShowCount("Jimmy");
        obj.ShowCount("JImmy");
    }
}

运行这段代码,得到的运行结果是:

ConsoleApp.exe

======= DomoClass Constructor =======
ConsoleApp.exe
Jimmy,the count is 1.
Jimmy,the count is 2.

现在运转良好,一切都没有什么问题。你可能想问,使用这种方式创建对象有什么意义呢?通过CreateInstanceAndUnwrap()创建对象和使用new DemoClass()创建对象有什么不同呢?回答这个问题之前,我们再来看下面另一种情况:

4.在新建应用程序域中创建对象

我们看看如何 创建一个新的AppDomain,然后在这个新的AppDomain中创建DemoClass对象。你可能会想,这还不简单,把上面的例子稍微改改不就OK了:

// 在新AppDomain中创建一个对象
static void Test2() {
    AppDomain currentDomain = AppDomain.CurrentDomain;
    Console.WriteLine(currentDomain.FriendlyName);

    // 创建一个新的应用程序域 - NewDomain
    AppDomain newDomain = AppDomain.CreateDomain("NewDomain");

    DemoClass obj;
    // 在新的应用程序域中创建对象
    obj = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
    obj.ShowAppDomain();
    obj.ShowCount("Jimmy");
    obj.ShowCount("Jimmy");
}

然后我们在Main()方法中运行Test2(),结果却是得到了一个异常:类型“ClassLib.DemoClass”未标记为可序列化。在把ClassLib.DemoClass标记为可序列化(Serializable)之前,我们想一想为什么会发生这个异常。我们看看声明obj类型的这行代码:DemoClass obj,这说明了obj是在当前的默认应用程序域,也就是AppConsole.exe中声明的;然后我们在往下看,类型的实例(对象本身)却是通过 newDomain.CreateInstanceAndUnwrap() 在新创建的应用程序域 -- NewDomain中创建的。这样就出现了一种尴尬的情况:对象的引用(类型声明)位于当前应用程序域(AppConsole.exe)中,而对象本身(类型实例)位于新创建的应用程序域(NewDomain)。而上面我们提到默认情况下AppDomain是彼此隔离的,我们不能直接在一个应用程序中引用另一个应用程序域中的对象,所以这里便会引发异常。

那么如何解决这个问题呢?按照异常提示:"ClassLib.DemoClass"未标记为可序列化。那我们将它标记为可序列化是不是就解决了这个问题呢?我们可以试一下,先将ClassLib.DemoClass标记为可序列化:

[Serializable]
public class DemoClass { /*略*/ }

然后再次运行程序,发现程序果然正常运行,并且和上面的输出完全一致:

ConsoleApp.exe

======= DomoClass Constructor =======
ConsoleApp.exe
Jimmy,the count is 1.
Jimmy,the count is 2.

根据输出,我们发现在应用程序域NewDomain中创建的对象位于ConsoleApp.exe,也就是当前应用程序域中了。这就说明了一个问题:当我们将对象标记为可序列化时,然后进行上面的操作时,对象本身已经由另一应用程序域(远程)传递到了本地应用程序域中。因为其要求将对象标记为可序列化,所以不难想到,具体的方法是 先在远程创建对象,接着将对象序列化,然后传递对象,在本地进行反序列化,最后还原对象。

5.代理(Proxy)和封送(Marshaling)

5.1 代理(Proxy)

现在我们在回到第3小节中 在默认应用程序域中创建对象 的例子,通过上面Test2()的例子,很容易理解为什么Test1()没有抛出异常,因为obj对象本身就位于当前应用程序域ConsoleApp.exe,所以不存在跨应用程序域访问的问题,自然不会抛出异常。那么在当前应用程序域中使用下面两种方式创建对象有什么不同呢?

DemoClass obj = new DemoClass();        // 方式一
DemoClass obj = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");                 // 方式二

当我们使用第一种方式时,我们在托管堆中创建了一个对象,并且直接引用了这个对象;采用第二种方式时,我们实际上创建了两个对象:我们在newDomain中创建了这个对象,然后将对象的状态进行拷贝、串行化,然后进行封送,接着在ConsoleApp.exe(客户端应用程序域)重新创建这个对象、还原对象状态,创建对象代理。最后,我们通过这个代理访问这个对象,此时,因为代理访问的是在本地重新创建的对象而非远程对象,所以当我们在对象上调用ShowDomain()时,显示的是ConsoleApp.exe。

上面的说明中出现了两个新名称,代理和封送。现在先来解释一下代理,代理(Proxy) 提供了和远程对象(本例中是在NewDomain中创建的DemoClass对象)完全相同的接口(属性和方法)。.Net需要在客户端(本例中是ConsoleApp.exe)基于远程对象的元信息(metadata)创建代理。因此客户端必须包含远程对象的元信息(简单来说就是只包含名称及接口定义,但可以不包含实际的代码实现)。因为代理有着和远程对象完全一样的接口和名称,所以对于客户程序来说,代理就好像是远程对象一样;而代理实际上又并不包含向客户程序提供服务的实际代码(比如说方法体),所以代理仅仅是将自己与某一对象相绑定,然后把客户程序对自己的服务请求发送给对象。对于客户程序来说,远程对象(服务端对象)就好像是在本地;而对远程对象来说,也好像是为其本地程序提供服务。

NOTE:有的书本讲到这里,会提到透明代理、真实代理,以及Message Sink等概念,这些我们留待后面再说。

5.2 传值封送、传引用封送

在上面的例子中,当位于ConsoleApp.exe的obj引用NewDomain中创建的对象时,.Net将NewDomain中对象的状态进行复制、序列化,然后在ConsoleApp.exe中重新创建对象,还原状态,并通过代理来对对象进行访问。这种跨应用程序域的访问方式叫做 传值封送(Marshal by value),有点类似于C#中参数的按值传递:

NOTE:上面这种通过调用CreateInstanceAndUnWrap()方法这种方式进行传值封送是一种特例,仅仅作为示范用。在Remoting通常的情况下,传值封送发生在远程对象的方法向客户端返回数值,或者客户端向远程对象传递方法参数的情况下。后面会详细解释。

由图上可以看出,传值封送时,因为要将整个对象传递到本地,对于大对象来说很显然是低效的。所以还有一种方式就是让对象依然保留在远程(本例为NewDomain中),而在客户端仅创建代理,上面已经说了代理的接口和远程对象完全相同,所以客户端以为仍然访问的是远程对象,当客户端调用代理上的方法时,由代理将对方法的请求发送给远程对象,远程对象执行方法请求,最后再将结果传回。这种方式叫做 传引用封送(Marshal by reference)

对象或者对象引用在传递的过程中,是以一种包装过的状态(warpper state)进行传递(所以才会称为封送吧,仅为个人猜测)。所以在创建对象时,要解包装,因此在CreateInstanceAndUnWrap()方法后多了一个AndUnWrap后缀,实际上UnWrap还包含一个创建代理的过程。

6.传引用封送范例

上面的例子中我们已经使用了传值封送,那么如何实现传引用封送呢?我们只要让对象继承自MarshalByRefObject基类就可以了,所以修改DemoClass,去掉Serializable标记,然后让它继承自MarshalByRefObject:

public class DemoClass:MarshalByRefObject {/*略*/}

接下来我们再次运行程序:

ConsoleApp.exe

======= DomoClass Constructor =======
NewDomain
Jimmy,the count is 1.
Jimmy,the count is 2.

发现obj.ShowDomain()输出为NewDomain,说明DemoClass的类型实例obj没有传值封送到ConsoleApp.exe中,而是依然保留在了NewDomain中。有的人可能想那我既标记上Serializable,又继承自MarshalByRefObject程序怎么处理呢?当我们让一个类型继承自MarshalByRefObject后,它就一定不会离开自己的应用程序域,所以仍会以传引用封送的方式进行。声明为Serialzable只是说明它可以被串行化。

继续进行之前,我们看看上面的结果还能说明什么问题:对象的状态是保留着的。这句话是什么意思呢?当我们两次调用ShowCount()方法时,第二次运行的值(count的值)是基于第一次的运行结果的。

我们再对上面Test2()的进行一下修改,多创建一个DemoClass的实例,看看会发生什么:

static void Test2() {
    AppDomain currentDomain = AppDomain.CurrentDomain;
    Console.WriteLine(currentDomain.FriendlyName);

    // 创建一个新的应用程序域 - NewDomain
    AppDomain newDomain = AppDomain.CreateDomain("NewDomain");
   
    DemoClass obj, obj2;

    // 在新的应用程序域中创建对象
    obj = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
    obj.ShowAppDomain();
    obj.ShowCount("Jimmy");
    obj.ShowCount("Jimmy");
   
    // 再创建一个obj2
    obj2 = (DemoClass)newDomain.CreateInstanceAndUnwrap("ClassLib", "ClassLib.DemoClass");
    obj2.ShowAppDomain();
    obj2.ShowCount("Zhang");
    obj2.ShowCount("Zhang");
}

运行Test2(),可以得到下面的输出:

ConsoleApp.exe

======= DomoClass Constructor =======
NewDomain
Jimmy,the count is 1.
Jimmy,the count is 2.

======= DomoClass Constructor =======
NewDomain
Zhang,the count is 1.
Zhang,the count is 2.

这次我们又发现什么了呢?对于obj和obj2,在NewDomain中分别创建了两个对象为其服务,且这两个对象仅创建了一次(注意到只调用了一次构造函数)。这种方式称为客户端激活对象(Client Activated Object,简称为 CAO)。请大家再次看看上面第二张传引用封送的示意图,是不是可以推出这里的结果?关于客户激活对象,后面我们会再看到,这里大家先留个印象。

7.客户应用程序(域)、服务端程序集、宿主应用程序(域)

看到Remoting这个词,我们通常所理解的可能只是本地客户机与远程服务器之间的交互。而实际上,只要是跨越AppDomain的访问,都属于Remoting。不管这两个AppDomain位于同一进程中,不同进程中,还是不同机器上。对于Remoting,可能大家理解它就包含两个部分,一个Server(服务器端)、一个Client(客户端)。但是如果从AppDomain的角度来看,服务端的AppDomain仅仅是提供了一个实际提供服务的远程对象的运行环境。所以提起Remoting,我们应该将其视为三个部分,这样在以后操作,以及我下面的讲述中,概念都会更加清晰:

  • 宿主应用程序(域),服务程序运行的环境(服务对象所在的AppDomain),它可以是控制台应用程序,Windows窗体程序,Windows 服务,或者是IIS的工作者进程等。上例中为 NewDomain。
  • 服务程序(对象),响应客户请求的程序(或对象),通常为继承自MarshalByRefObject的类型,表现为一个程序集。上例中为 DemoClass。
  • 客户应用程序(域),向宿主应用程序发送请求的程序(或对象)。上例中为 ConsoleApp.exe。

在文中,有时我可能也会用到 客户端(Client Side) 和 服务端(Server Side)这样的词,当提到客户端时,仅指客户应用程序;当提到服务端的时候,指服务程序 和 宿主应用程序。

可以看出,在我们上面的例子中,客户端 与 宿主应用程序 位于同一个进程的不同应用程序域当中,尽管大多数情况下,它们位于不同的进程中。

而我们本章第三节,在当前应用程序域的实例上调用CreateInstanceAndUnwrap()方法创建DemoClass对象时,则是一个极端情况:即 客户程序域、宿主应用程序域 为同一个应用程序域 ConsoleApp.exe 。

NOTE:在应用程序域中底部,还有一个代码执行领域,称为环境(Context)。一个AppDomain中可以包含多个环境,跨越环境的访问也可以理解成Remoting的一个特例。但是本文不涉及这部分内容。

目录
相关文章
|
3月前
|
存储 Shell Linux
快速上手基于 BaGet 的脚本自动化构建 .net 应用打包
本文介绍了如何使用脚本自动化构建 `.net` 应用的 `nuget` 包并推送到指定服务仓库。首先概述了 `BaGet`——一个开源、轻量级且高性能的 `NuGet` 服务器,支持多种存储后端及配置选项。接着详细描述了 `BaGet` 的安装、配置及使用方法,并提供了 `PowerShell` 和 `Bash` 脚本实例,用于自动化推送 `.nupkg` 文件。最后总结了 `BaGet` 的优势及其在实际部署中的便捷性。
154 10
|
28天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
1月前
|
JSON 算法 安全
JWT Bearer 认证在 .NET Core 中的应用
【10月更文挑战第30天】JWT(JSON Web Token)是一种开放标准,用于在各方之间安全传输信息。它由头部、载荷和签名三部分组成,用于在用户和服务器之间传递声明。JWT Bearer 认证是一种基于令牌的认证方式,客户端在请求头中包含 JWT 令牌,服务器验证令牌的有效性后授权用户访问资源。在 .NET Core 中,通过安装 `Microsoft.AspNetCore.Authentication.JwtBearer` 包并配置认证服务,可以实现 JWT Bearer 认证。具体步骤包括安装 NuGet 包、配置认证服务、启用认证中间件、生成 JWT 令牌以及在控制器中使用认证信息
|
2月前
|
SQL XML 关系型数据库
入门指南:利用NHibernate简化.NET应用程序的数据访问
【10月更文挑战第13天】NHibernate是一个面向.NET的开源对象关系映射(ORM)工具,它提供了从数据库表到应用程序中的对象之间的映射。通过使用NHibernate,开发者可以专注于业务逻辑和领域模型的设计,而无需直接编写复杂的SQL语句来处理数据持久化问题。NHibernate支持多种数据库,并且具有高度的灵活性和可扩展性。
43 2
|
2月前
|
开发框架 .NET API
Windows Forms应用程序中集成一个ASP.NET API服务
Windows Forms应用程序中集成一个ASP.NET API服务
98 9
|
3月前
|
数据采集 JSON API
.NET 3.5 中 HttpWebRequest 的核心用法及应用
【9月更文挑战第7天】在.NET 3.5环境下,HttpWebRequest 类是处理HTTP请求的一个核心组件,它封装了HTTP协议的细节,使得开发者可以方便地发送HTTP请求并接收响应。本文将详细介绍HttpWebRequest的核心用法及其实战应用。
142 6
|
4月前
|
Linux iOS开发 开发者
跨平台开发不再难:.NET Core如何让你的应用在Windows、Linux、macOS上自如游走?
【8月更文挑战第28天】本文提供了一份详尽的.NET跨平台开发指南,涵盖.NET Core简介、环境配置、项目结构、代码编写、依赖管理、构建与测试、部署及容器化等多个方面,帮助开发者掌握关键技术与最佳实践,充分利用.NET Core实现高效、便捷的跨平台应用开发与部署。
293 3
|
4月前
|
开发框架 监控 安全
.NET 应用程序安全背后究竟隐藏着多少秘密?从编码到部署全揭秘!
【8月更文挑战第28天】在数字化时代,.NET 应用程序的安全至关重要。从编码阶段到部署,需全面防护以保障系统稳定与用户数据安全。开发者应遵循安全编码规范,实施输入验证、权限管理和加密敏感信息等措施,并利用安全测试发现潜在漏洞。此外,部署时还需选择安全的服务器环境,配置 HTTPS 并实时监控应用状态,确保全方位防护。
59 3
|
4月前
|
缓存 Java API
【揭秘】.NET高手不愿透露的秘密:如何让应用瞬间提速?
【8月更文挑战第28天】本文通过对比的方式,介绍了针对 .NET 应用性能瓶颈的优化方法。以一个存在响应延迟和并发处理不足的 Web API 项目为例,从性能分析入手,探讨了使用结构体减少内存分配、异步编程提高吞吐量、EF Core 惰性加载减少数据库访问以及垃圾回收机制优化等多个方面,帮助开发者全面提升 .NET 应用的性能和稳定性。通过具体示例,展示了如何在不同场景下选择最佳实践,以实现更高效的应用体验。
51 3
|
4月前
|
前端开发 JavaScript 开发工具
跨域联姻:React.NET——.NET应用与React的完美融合,解锁前后端高效协作新姿势。
【8月更文挑战第28天】探索React.NET,这是将热门前端框架React与强大的.NET后端无缝集成的创新方案。React以其组件化和虚拟DOM技术著称,能构建高性能、可维护的用户界面;.NET则擅长企业级应用开发。React.NET作为桥梁,使.NET应用轻松采用React构建前端,并优化开发流程与性能。通过直接托管React组件,.NET应用简化了部署流程,同时支持服务器端渲染(SSR),提升首屏加载速度与SEO优化。
93 1