.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的一个特例。但是本文不涉及这部分内容。

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