资源描述
远程对象和多层架构
一、 远程处理(Remoting)技术概述
做为企业级架构,对象的远程处理是必备的功能,非此不能实现大型分布式体系。
.NET实现分布式体系主要利用两种体系,一种是建立在Windows Service架构下的Remoting技术,另一种是建立在Web Service架构下的技术,这两种技术各有利弊,合理使用,才能兴利除弊,构造高效的分布式体系。
NET平台提供了一套专门用于远程处理的类。在公共语言运行库中,有一个远程处理结构,它提供了一组丰富的类,这些类使开发人员可以不必考虑大部分部署和管理远程对象的复杂工作,对远程对象调用方法也几乎与调用本地方法相同。
这样一来,构造复杂大型的网络系统就成为了可能。
并不说有的对象都能在分布式体系中运行的,可远程处理的对象是能在大范围的分布环境中正常运行的对象。有两种主要的可远程处理的对象:
按值封送对象,它们被复制并传出应用程序域。
按引用封送对象,将为其创建代理,而该代理由客户端用于远程访问对象。
按值封送对象:
按值封送 (MBV) 对象声明它们的序列化规则(通过实现 ISerializable 来实现其自身的序列化,或者通过用 SerializableAttribute 修饰,该属性通知系统自动序列化该对象),但是不扩展 MarshalByRefObject。远程处理系统创建这些对象的完整副本并将副本传递到进行调用的应用程序域。
一旦副本到达调用方的应用程序域内,对它的调用就是对该副本的直接调用。而且,当 MBV 对象作为参数传递时,也是通过值传递的。除声明 SerializableAttribute 或实现 ISerializable 之外,无需做其他任何事情就可以将类的实例跨应用程序或上下文边界通过值传递。
当由于性能或处理原因,将对象的完整状态和任何可执行功能移动到目标应用程序域有意义时,应当使用 MBV 对象。在许多方案中,这减少了跨网络、进程和应用程序域边界的冗长而耗费资源的往返行程。MBV 对象还可以从对象的原始应用程序域内直接使用。在这种情况下,由于不进行任何封送处理,因此不创建任何副本而且访问非常高效。
另一方面,如果发布的对象非常大,那么在繁忙的网络上传递整个副本对于应用程序来说可能不是最佳的选择。此外,永远不会将对复制对象的状态所做的更改传回到起始应用程序域中的原始对象。在抽象级别上,这种方案类似于客户端浏览器所请求的静态 HTML 页的方案。服务器复制文件,将其写入到流中,发送出去,然后忘掉它。所有后续的请求都只是对其他副本的其他请求。
远程处理系统广泛使用可序列化的对象。对其他应用程序域中的对象的引用(由 ObjRef 类在远程处理系统中表示)本身是可序列化的;必须能够将它精确地复制并将副本发送给请求。同样,对于实现 IMessage 的消息对象也是如此,这是因为它们是调用信息和所有其他所需对象引用的一般容器。另外,仅传输数据的对象通常是 MBV 对象。例如,DataSet 扩展实现 ISerializable 的 MarshalByValueComponent。
按引用封送的对象:
按引用封送 (MBR) 的对象是至少扩展 System.MarshalByRefObject 的可远程处理的对象。根据已声明的激活类型,当客户端在自己的应用程序域中创建 MBR 对象的实例时,.NET 远程处理基础结构在调用方的应用程序域中创建表示该 MBR 对象的代理对象,并向调用方返回对此代理的引用。然后客户端将在该代理上进行调用;远程处理封送这些调用,将它们发送回起始应用程序域,并在实际对象上调用该调用。
注意 如果客户端位于与 MBR 对象相同的应用程序域中,基础结构将向客户端返回对该 MBR 对象的直接引用,从而避免封送处理的系统开销。
如果 MarshalByRefObject 作为参数传入,当调用到达时,它变成另一个应用程序域中的代理。MBR 返回值,并且 out 参数以相同的方式工作。
当对象的状态和任何可执行的功能应处在创建它的应用程序域中时,应当使用 MBR 对象。例如,具有内部字段且该内部字段是操作系统句柄的对象应扩展 MarshalByRefObject,这是因为操作系统句柄在其他进程中或其他计算机上的其他应用程序域中是无意义的。有时对象可能大得难以想象;对于功能强大的服务器还行,但通过网络发送到 33.6 kbps 的调制解调器就不行了。
“远程处理”是内置到公共语言运行库中的框架,它可用于生成复杂的分布式应用程序和网络服务。我们首先要了解几个重要的概念:
1) 代理对象。
当客户端创建远程对象的实例时,它接收服务器上该类实例的代理。对代理调用的所有方法都将自动转发到该远程类,并且任何结果都将返回到客户端。从客户端的角度来看,此过程与进行本地调用没有任何区别。远程对象引发的任何异常都将被自动返回到客户端。这使客户端得以在代码段周围使用正常的 Try 块和 catch 块捕获和处理异常。
代理对象
客户程序
服务器程序
服务器
实例
代理实例和客户类的类型相同
2)对象传递。
所有远程创建的对象都由引用返回而且必须从 MarshallByRefObject 派生。作为参数传递给远程方法调用的对象可以由值或引用转发。默认行为是由值传递,前提是使用自定义属性 [serializable] 标记了相关对象。另外,该对象可以实现 ISerializable 接口,这在应如何序列化和反序列化对象方面提供灵活性。不是用引用或值封送的对象不能进行远程处理。
3) 激活模型:
可通过调用 new 轻松地从客户端创建远程对象。框架包含足够的“智能”,能够知道您正在处理远程对象,而且将确保该对象的实例在相关的远程应用程序中创建。创建远程对象的实例不局限于默认构造函数;您甚至可以使用需要一个或多个参数的构造函数执行此操作。Activator 类包含 CreateInstance 和 GetObject 两个方法,它们也可用于创建远程对象的实例。前者可用于替代 new 来创建对象实例,而后者通常用于连接到位于指定 URL 的对象。
4) 无状态和有状态对象:
框架规定将远程对象创建为无状态。如果将某个对象配置为 SingleCall,则当对该对象调用方法时会创建该对象。该对象处理调用,返回可选结果,然后被垃圾回收器回收。这样,对于每个调用客户端都始终连接到新对象。
如果你将对象配置为 Singleton 将确保无论何时调用该对象,所有客户端都连接到同一对象。
ClientActivated 对象允许客户端在远程对象创建后将参数传递给该对象的构造函数。在客户端上对客户端激活的对象的每个激活请求(Activator.CreateInstance 或者 new 与配置文件中的项相结合)都导致在服务器上创建一个新对象。
5) 信道和序列化:
当客户端对远程对象调用方法时,远程处理框架自动序列化与该请求关联的任何数据,然后使用信道(通信协议)将数据传输到远程对象。
一些支持的比较常用的信道包括 HTTP、TCP 和 SMTP。对于 HTTP 而言,框架使用 SOAP 协议以 XML 格式在客户端和服务器之间来回传输数据。
HTTP 的默认序列化格式化程序为 SOAP 格式化程序。因为程序员可以创建用于任何信道的自定义格式化程序,所以远程处理框架可以配置为与其他平台上的任何外部 .NET 框架协同工作。
TCP 信道默认情况下使用普通套接字和“二进制序列化”,而且可用于与远程服务器上的任何对象进行通讯。
6) 基于租约的生存期:
远程对象的生存期由租约机制控制。当第一次创建对象时,赋予它一个租约时间。当对象的租约时间降到零时,该对象将与远程处理结构断开连接,而且当 AppDomain 内对该对象的所有引用都释放后,该对象将由垃圾回收器回收。
框架提供许多机制,允许客户端延长该对象的租约,从而维持其生命。
7) 在 IIS 中承载对象:
远程对象可以轻松地寄宿在 IIS 中。这样使任何客户端都能使用常规 HTTP 通过端口 80 连接到该对象。还可以使用 Internet Explorer 创建允许用户连接到远程对象的 Web 页。
下面,我们进一步讨论远程处理的工作原理。
.NET 远程处理框架为开发人员提供先进的分布式对象模型,该模型允许在网络中的不同公共语言运行库之间或者在同一公共语言运行库中的不同 AppDomain 之间进行远程方法调用。
与远程对象的所有交互都通过代理发生,而且客户端无法直接访问远程对象,因为该对象仅在其自己的 AppDomain 内才有意义。下面,我们简要介绍一下远程对象应用的规则:
1)所有远程对象都必须从 MarshalByRefObject 派生。
这就是说,在构建远程对象的类的时候,它必须继承于MarshalByRefObject,这是一个必要的条件。
2)与远程对象的任何交互都通过代理使用引用发生。客户端必须获取此激活远程对象的代理。此操作可通过调用 CreateInstance、GetObject 或 new 来完成。
3)当进行远程调用时,本地对象可用作参数。因为对远程对象来说,对本地对象的引用没有意义,所以当本地对象用作远程调用中的参数或者作为远程调用的结果返回时,所有本地对象都通过值传递。
4)由值传递的所有对象都必须可序列化。这一点可通过实现 ISerializable 接口或使用 [serializable] 自定义属性标记该对象来完成。
5)必须先激活远程对象,客户端才能使用它们:
框架支持两种激活模型:服务器激活和客户端激活。服务器激活意味着当客户端试图访问对象时,该对象自动在服务器上创建。客户端激活意味着对象将作为来自客户端的蓄意激活请求的结果而创建。
有两种激活模型的原因是因为在分布式对象模型中,对象的生存期经常成为要考虑的问题。在某些情况下,要求远程对象在调用之间维护状态。在其他情况下,要求无状态对象,这样在每次调用时都可以为每个调用方提供一个新的对象实例。
有时客户端要求对对象的生存期有更多的控制(相比服务器激活所提供的控制而言)。通过同时允许服务器激活和客户端激活,开发人员可为手头的任务自由选取最佳的激活模型。
服务器对象在尚未创建时如何能侦听客户端呢?在网络上的某个位置创建并部署服务器对象,该对象在此处侦听要连接的客户端。当客户端出现时,有关该调用和参数的详细信息被传输给该服务器对象,然后方法执行,并将可选结果传输回该客户端。
部署服务器对象的步骤如下所示:
1) 所有远程对象都必须注册到公共语言运行库,这样客户端才能访问它们。
2)注册客户端将用来与该对象进行通讯的所有信道。
每个信道使用特定协议,如 TCP、HTTP 或 SMTP。在合适的地方,可注册多个信道。
该对象的详细信息存储在一个由框架管理的表中。该表还存储了随后为对象创建的对象引用。对象引用将用于表示不同 AppDomain 中的注册对象,并将在需要时被序列化并传输到客户端。
然后,注册的信道开始侦听是否有客户端要连接。
部署远程对象后,客户端可以连接和调用服务器对象上的方法。
3)客户端注册将用来与该远程对象进行通讯的信道。此处唯一的要求是服务器应在类似信道上正在侦听。
4)随后,客户端通过调用 new、GetObject 或 CreateInstance 激活该对象。
随激活请求提供有关于该远程对象的类型和位置的充分信息,以便框架创建表示该远程对象的代理。此过程是本地的,明白这一点很重要,并且根本没有对该远程对象进行调用。代理也存储在本地的一个表中,客户端的任何其他激活请求都接收该代理。
在服务器激活对象的情况下,客户端使用它在激活时接收的代理调用该远程对象上的方法。如果客户端信道尚未连接到远程信道,则客户端信道尝试建立此连接。
建立连接后,消息接收使用指定协议连接到处理通讯详细信息的连接的任一端。
5)方法调用的详细信息加载到 Message 对象中,该对象被序列化并传输到服务器。序列化的类型取决于信道。例如,使用 HTTP 信道时,所有消息序列化为 XML 并通过 SOAP 传输。另一方面,TCP 使用二进制序列化。
6)消息到达服务器端后,框架重新汇编原始调用、激活目标对象(如果它尚未激活)并将调用转发给相关对象。结果返回客户端时路径与消息发出的路径正好相反,它被打包到传输回该客户端的消息中。如果目标对象是 SingleCall 类型,则在调用完成后自动对其进行垃圾回收。
上面这些概念的叙述尽管有些抽象,但这是我们进行远程处理的设计所必需的,我们将通过下面这些专题的研究,使我们对这些概念有一个比较深入的理解,同时,学会编写远程处理程序的方法。
二、 基本远程处理框架
我们通过一个简单的程序,来说明远程处理的一些技术问题。
1)构造需要处理的类
我们类构造一个需要处理的类,首先建立一个类库程序,库的名字是RemoteObject。
把文件名改为:RemoteTest.cs
并且建立一个类:HelloWorld。
using System;
namespace RemoteTest
{
public class HelloWorld:MarshalByRefObject
{
public string Mys;
public string Shout(string S)
{
Mys = S;
return "您好,我收到了,您的信息是:" +S;
}
}
}
这里我们注意到,这个类首先必须继承于MarshalByRefObject,这是作为远程应用的类所必需。
这个类的作用仅仅是收到一个信息,然后把这个信息加几个字返回去。同时,为了进一步讨论问题,把这个信息还保留在一个全局变量中。
现在可以把这个类库编译成DLL文件。
2) 服务器
服务器可以使控制台程序,也可以使我们后面要介绍的Windows服务,但是,这里我们为了研究方便,还是使用普通的Windows Form,因为这样编程更加直截了当,而且很容易移植到其它形式中去。
需要把System.Runting.Remoting引用进来。
还需要把RemoteTest.dll引用进来。
代码:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
private void Form1_Load(object sender, System.EventArgs e)
{
//提供TCP协议发送方-接收方信道的实现
TcpChannel chan;
//定义它的实例,端口号为8086
chan = new TcpChannel(8086);
//注册这个信道
ChannelServices.RegisterChannel(chan);
//远程配置
RemotingConfiguration.RegisterWellKnownServiceType( typeof(RemoteTest.HelloWorld),
"SayHello", WellKnownObjectMode.SingleCall);
}
除了已经注释的内容以外,我们必须注意“远程配置”的内容,它共有三项:第一项定义需处理的类的类型,第二项定义呼叫的名称,可以自己起名字,这里为“SayHello”,第三项为模式,这里为“SingleCall”,也可以根据需要选择“Singleton”,前面已经说明了:
SingleCall :每个传入的消息由新的对象实例提供服务。
Singleton :每个传入的消息由同一个对象实例提供服务。
现在可以编译了。
3) 客户端:
客户端编程同样需要把System.Runting.Remoting引用进来,还需要引入RemoteTest.dll,这是由远程应用的机制决定的。客户端引入的RemoteTest.dll只是提供一个类的结构,具体的数据由服务器的代理通过处理以后传递过来。
所以,你往往感觉自己是在调用本地的类实例,但实际上得到的是服务器过来的数据。
用和服务器同样的方法,引入System.Runting.Remoting和RemoteTest.dll。
代码(客户端不需要注册信道):
using System.Runtime.Remoting;
RemoteTest.HelloWorld obj;
private void button1_Click(object sender, System.EventArgs e)
{
try
{
obj =(RemoteTest.HelloWorld)
(Activator.GetObject(typeof(RemoteTest.HelloWorld), "tcp://localhost:8086/SayHello"));
if (obj==null)
MessageBox.Show("没有连接服务器");
else
label1.Text = obj.Shout("我的连接");
}
catch
{
MessageBox.Show("连接远程对象失败");
}
}
private void button2_Click(object sender, System.EventArgs e)
{
label1.Text = obj.Shout(textBox1.Text);
}
private void button3_Click(object sender, System.EventArgs e)
{
label1.Text = obj.Mys;
}
}
这里十分要注意的是,除了要注册序列化根据和信道以外,需要用下面的语句构造对象obj的实例,这才可以真正和服务器连接上。
obj =(RemoteTest.HelloWorld)
(Activator.GetObject(typeof(RemoteTest.HelloWorld), "tcp://localhost:8086/SayHello"));
其中:
RemoteTest.HelloWorld 提供了服务器代理类的名称;
"tcp://localhost:52000/SayHello"提供了通信协议、端口和呼叫名称的信息;
(RemoteTest.HelloWorld) 提供了本地类的名称以实现类型转换。
如果连接成功,此后调用obj对象,实际上是调用的服务器端相应的对象的数据。
试验一下:
如果在网络上应用,请把localhost改成相应的计算机名。
同样功能的VB程序如下:
服务器:
代码:
Imports System
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Public Class Form1
Inherits System.Windows.Forms.Form
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
'提供TCP协议发送方-接收方信道的实现
Dim chan As TcpChannel
'定义它的实例,端口号为52000
chan = New TcpChannel(52000)
'注册这个信道
ChannelServices.RegisterChannel(chan)
'远程配置
RemotingConfiguration.RegisterWellKnownServiceType( _
Type.GetType("RemoteObject.HelloWorld,RemoteObject"), _
"SayHello", WellKnownObjectMode.SingleCall)
End Sub
End Class
客户端
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Public Class Form1
Inherits System.Windows.Forms.Form
Dim obj As RemoteObject.HelloWorld
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Try
obj = _
CType(Activator.GetObject(Type.GetType("RemoteObject.HelloWorld, " & _
"RemoteObject"), "tcp://localhost:52000/SayHello"), _
RemoteObject.HelloWorld)
If obj Is Nothing Then
MessageBox.Show("没有连接服务器")
Else
Label1.Text = obj.Shout("我的连接")
End If
Catch
MessageBox.Show("连接远程对象失败")
End Try
End Sub
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Label1.Text = obj.Shout(TextBox1.Text)
End Sub
End Class
三、 配置文件及其应用
在上面的例子中,我们已经发现了一个问题,当程序编写好了以后,当需要运行在不同的平台上的时候,往往是很麻烦的事情。比如,平台上的端口已经被占用,需要修改端口,或者把localhost改成相应的计算机名,甚至需要改变传输协议,都需要修改源代码然后重新编译,这在很多情况下甚至是不可能的(比如程序已经发布)。
解决的办法,就是构造一个独立于应用程序的配置文件,.NET规定,这个文件符合XML规范,其相应的节点提供的配置的相应信息。
配置文件的参数是大小写敏感的。比如mode="SingleCall"和mode="Singleton"。
例如:
用TCP协议传输.
server.exe.config
-------------------------------------------------
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="RemoteTest.HelloWorld, RemoteTest"
mode="SingleCall"
objectUri="SayHello"
/>
</service>
<channels>
<channel port="8080" ref="tcp"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
---------------------------------------------------------------
client.exe.config
---------------------------------------------------------------
<configuration>
<system.runtime.remoting>
<application>
<client
url="http://localhost:8080"
>
<wellknown
type="RemoteTest.HelloWorld, RemoteTest"
url="tcp://localhost:8080/SayHello"
/>
</client>
<channels>
<channel
ref="tcp"
port="0"
/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
---------------------------------------------------------------
用HTTP协议传输.
server.exe.config
--------------------------------------------------------------
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="RemoteTest.HelloWorld, RemoteTest"
mode="SingleCall"
objectUri="SayHello"
/>
</service>
<channels>
<channel port="8080" ref="http"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
---------------------------------------------------------------
client.exe.config
---------------------------------------------------------------
<configuration>
<system.runtime.remoting>
<application>
<client
url="http://localhost:8080"
>
<wellknown
type="RemoteTest.HelloWorld, RemoteTest"
url="http://localhost:8080/SayHello"
/>
</client>
<channels>
<channel
ref="http"
port="0"
/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
我们来看看怎么来应用这个配置文件。
服务器:
构造方法和前面基本框架是一样的,但是,你要把server.exe.config拷贝到应用程序的当前目录下(这里为bin子目录)。
代码:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels.Http;
private void Form1_Load(object sender, System.EventArgs e)
{
RemotingConfiguration.Configure("server.exe.config");
}
而客户端,client.exe.config拷贝到应用程序的当前目录下,代码:
using System.Runtime.Remoting;
RemoteTest.HelloWorld obj;
private void button1_Click(object sender, System.EventArgs e)
{
RemotingConfiguration.Configure("client.exe.config");
obj = new RemoteTest.HelloWorld();
label1.Text = obj.Shout("已经连接上啦");
}
private void button2_Click(object sender, System.EventArgs e)
{
label1.Text = obj.Shout(textBox1.Text);
}
效果是一样的。
当你改变配置文件以后,可以很容易的把TCP协议改变为Http协议,所要做的只是重新把相应的配置文件拷贝过去。
配置文件,给我们的应用程序带来了极大的方便。
四、 编译依赖接口的客户端
有时候,客户端并不知道对象的确切类型是什么。它所知道的全部内容只是该对象实现的接口,这就可以编译依赖于借口的客户端。
项目:IRemoteTest
using System;
namespace IRemoteTest
{
public interface IRemoteTest
{
string Shout(string S);
}
}
类的项目:RemoteTest
using System;
namespace RemoteTest
{
public class HelloWorld:MarshalByRefObject,IRemoteTest.IRemoteTest
{
string IRemoteTest.IRemoteTest.Shout(string S)
{
return "您好,我是RemoteTest,你是:" +S;
}
}
}
服务器:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
private void Form1_Load(object sender, System.EventArgs e)
{
//提供TCP协议发送方-接收方信道的实现
TcpChannel chan;
//定义它的实例,端口号为8086
chan = new TcpChannel(8086);
//注册这个信道
ChannelServices.RegisterChannel(chan);
//远程配置
RemotingConfiguration.RegisterWellKnownServiceType(
展开阅读全文