.NET Remoting 使用总结
Remoting
什么是Remoting,简而言之,我们可以将其看作是一种分布式处理方式。从微软的产品角度来看,可以说Remoting就是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。Microsoft? .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这也正是我们使用Remoting的原因。为什么呢?在Windows操作系统中,是将应用程序分离为单独的进程。这个进程形成了应用程序代码和数据周围的一道边界。如果不采用进程间通信(RPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。
在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。
在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。
Remoting的两种通道
Remoting的通道主要有两种:Tcp和Http。在.Net中,System.Runtime.Remoting.Channel中定义了 IChannel接口。IChannel接口包括了TcpChannel通道类型和Http通道类型。它们分别对应Remoting通道的这两种类型。
TcpChannel类型放在名字空间System.Runtime.Remoting.Channel.Tcp中。Tcp通道提供了基于Socket 的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。TcpChannel类型默认使用二进制格式序列化消息对象,因此它具有更高的传输性能。HttpChannel类型放在名字空间System.Runtime.Remoting.Channel.Http中。它提供了一种使用 Http协议,使其能在Internet上穿越防火墙传输序列化消息流。默认情况下,HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。通常在局域网内,我们更多地使用TcpChannel;如果要穿越防火墙,则使用HttpChannel。
远程对象的激活方式
在访问远程类型的一个对象实例之前,必须通过一个名为Activation的进程创建它并进行初始化。这种客户端通过通道来创建远程对象,称为对象的激活。在Remoting中,远程对象的激活分为两大类:服务器端激活和客户端激活。
服务器端激活,又叫做WellKnow方式,很多又翻译为知名对象。为什么称为知名对象激活模式呢?是因为服务器应用程序在激活对象实例之前会在一个众所周知的统一资源标识符(URI)上来发布这个类型。然后该服务器进程会为此类型配置一个WellKnown对象,并根据指定的端口或地址来发布对象。. Net Remoting把服务器端激活又分为SingleTon模式和SingleCall模式两种。
SingleTon模式:此为有状态模式。如果设置为SingleTon激活方式,则Remoting将为所有客户端建立同一个对象实例。当对象处于活动状态时, SingleTon实例会处理所有后来的客户端访问请求,而不管它们是同一个客户端,还是其他客户端。SingleTon实例将在方法调用中一直维持其状态。举例来说,如果一个远程对象有一个累加方法(i=0;++i),被多个客户端(例如两个)调用。如果设置为SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是相同的。如果熟悉Asp.Net的状态管理,我们可以认为它是一种Application状态。
SingleCall模式:SingleCall是一种无状态模式。一旦设置为SingleCall模式,则当客户端调用远程对象的方法时, Remoting会为每一个客户端建立一个远程对象实例,至于对象实例的销毁则是由GC自动管理的。同上一个例子而言,则访问远程对象的两个客户获得的都是1。我们仍然可以借鉴Asp.Net的状态管理,认为它是一种Session状态。
客户端激活。与WellKnown模式不同, Remoting在激活每个对象实例的时候,会给每个客户端激活的类型指派一个URI。客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。SingleCall模式和客户端激活模式是有区别的:首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时再创建。其次,SingleCall模式激活的对象是无状态的,对象生命期的管理是由GC管理的,而客户端激活的对象则有状态,其生命周期可自定义。其三,两种激活模式在服务器端和客户端实现的方法不一样。尤其是在客户端,SingleCall模式是由 GetObject()来激活,它调用对象默认的构造函数。而客户端激活模式,则通过CreateInstance()来激活,它可以传递参数,所以可以调用自定义的构造函数来创建实例。
远程对象的定义
前面讲到,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。因此在Remoting中,对于远程对象有一些必须的定义规范要遵循。
由于Remoting传递的对象是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MSDN对 MarshalByRefObject的说明是:MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshallByRefObject。
在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊,我将在本系列之三中介绍。
服务器端
根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:
1、注册通道
要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和 HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。
注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间: System.Runtime.Remoting.Channel.Tcp。在实例化通道对象时,将端口号作为参数传递。然后再调用静态方法 RegisterChannel()来注册该通道对象即可。
2、注册远程对象
注册了通道后,要能激活远程对象,必须在通道中注册该对象。根据激活模式的不同,注册对象的方法也不同。
对于WellKnown对象,可以通过静态方法 RemotingConfiguration.RegisterWellKnownServiceType()来实现,注册对象的方法基本上和 SingleTon模式相同,只需要将枚举参数WellKnownObjectMode改为SingleCall就可以了。
3、注销通道
如果要关闭Remoting的服务,则需要注销通道,也可以关闭对通道的监听。在Remoting中当我们注册通道的时候,就自动开启了通道的监听。而如果关闭了对通道的监听,则该通道就无法接受客户端的请求,但通道仍然存在,如果你想再一次注册该通道,会抛出异常。
Remoting优点: 1、能让我们进行分布式开发 2、Tcp通道的Remoting速度非常快 3、虽然是远程的,但是非常接近于本地调用对象 4、可以做到保持对象的状态 5、没有应用程序限制,可以是控制台,winform,iis,windows服务承载远程对象 缺点: 1、非标准的应用因此有平台限制 2、脱离iis的话需要有自己的安全机制
根据需求,我们的系统必须以C/S方式构建,而且是三层架构,这样一来,就出现了服务器端和客户端通信的问题。
为了解决双方的通信问题,还要考虑效率、性能等方面,经过分析、试验,我们根据效率、移植、开发难易等几个因素,舍弃了一开始提出的WebService、消息队列机制,以及有人建议的基于流I/O自己解析数据的通信方式,在分析了目前主流的RPC方式(DCOM、CORBA、.NET Remoting)及我们的开发平台后,最终选择了微软新推出的.NET Remoting机制。我们的原因如下:
1、.NET Remoting是目前分布式对象实现RPC的一种主要方式。
2、.NET Remtoing在性能上可以达到DCOM,或者与之相差不多。
3、.NET Remoting建立在.NET定义的公共数据类型CTS及运行环境CLR之上,和.NET框架有着很好的互操作性,因此功能强大切易于使用。
4、扩展性和安全性方面都比较好。
从试验结果来看,该机制可以实现C/S模式下的双方通信,而且在性能上具有很好的保障。根据我们开发完毕的系统性能来看,Remoting机制很好的实现了我们赋予它的任务,或者说,我们采用Remoting机制达到了我们预期的目标。
下面,对我们采用Remoting机制进行开发这一从无到有过程中的一些资料、感悟进行整理。
.NET Remoting是微软随.NET推出的一种分布式应用解决方案,被誉为管理应用程序域之间的 RPC 的首选技,它允许不同应用程序域之间进行通信(这里的通信可以是在同一个进程中进行、一个系统的不同进程间进行、不同系统的进程间进行)。
更具体的说,Microsoft .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。也就是说,使用.NET Remoting,一个程序域可以访问另外一个程序域中的对象,就好像这个对象位于自身内部,只不过,对这个远程对象的调用,其代码是在远程应用程序域中进行的,例如在本地应用程序域中调用远程对象上一个会弹出对话框的方法,那么,这个对话框,则会在远程应用程序域中弹出。
.NET Remoting框架提供了多种服务,包括激活和生存期支持,以及负责与远程应用程序进行消息传输的通讯通道。格式化程序用于在消息通过通道传输之前,对其进行编码和解码。应用程序可以在注重性能的场合使用二进制编码,在需要与其他远程处理框架进行交互的场合使用 XML 编码。在从一个应用程序域向另一个应用程序域传输消息时,所有的 XML 编码都使用 SOAP 协议。出于安全性方面的考虑,远程处理提供了大量挂钩,使得在消息流通过通道进行传输之前,安全接收器能够访问消息和序列化流。
.NET Remoting协同工作能力
下图是.NET Remoting的体系结构图
.NET Remoting通信体系结构
一般来说,.NET Remoting包括如下几点主要元素:
Ø 远程对象:运行在Remoting服务器上的对象。客户端通过代理对象来间接调用该对象的服务,如上图的“通信体系结构”所示。在.NET Remoting体系中,要想成为远程对象提供服务,该对象的类必须是MarshByRefObject的派生对象。另外,要说明的是,需要在网络上传递的对象,例如“参数”,则必须是可序列化的。
Ø 信道:信道是服务器和客户机进行通信用的(这里的服务器和客户机并不一定都是计算机,也可能是进程)。在.NET Remoting中,提供了三种信道类型:TCP、HTTP、IPC,另外,也可以定制不同的信道以适应不同的通信协议(至于如何定制,我尚未涉及到,因此,不好说)。
Ø 消息:客户机和服务器通过消息进行信息交换,消息在信道中传递。这里的消息包括,远程对象的信息,调用方法名称,参数,返回值等。
Ø 格式标识符:该标识符标明了消息是按照什么样的格式被发送到信道上的,目前.NET 2.0提供了两种格式标识符:SOAP格式和二进制格式。SOAP格式标识符符合SOAP标准,比较通用,可以和非.NET 框架的Web服务通信。二进制格式标识符,则在速度、效率上面更生一筹,但通用性较SOAP差。另外,Remoting还支持自定义的格式标识符。(顺便说一下:TCP信道,默认使用二进制格式传输,因为这个效率更高;Http信道则默认使用SOAP格式;不过在系统中,哪种信道具体使用哪种格式,则是可以根据需要设置的。)。
Ø 格式标识符提供程序:它用于把格式标识符和信道联系起来。在创建信道时,可以指定所要使用的标识符提供程序,一旦指定了提供程序,那么消息被发送到信道上的格式也就确定了下来。
为序列化消息, .NET Remoting 提供了两类格式程序接收器: BinaryFormatter和SoapFormatter。选择的类型很大程度上取决于连接分布式对象的网络环境的类型。由于. NET Remoting体系结构的可插入特性,可以创建自己的格式程序接收器,并插入到.NET Remoting基础设施中。这种灵活性使基础设施能够支持可能的各种线路格式。
对于可以发送并接收二进制数据(例如TCP/IP)的网络传输协议,可以使用System.Runtime.Serialization.Formatters.Binary名字空间中定义的BinaryFormatter类型。顾名思义,BinaryFormatter将消息对象序列化为一个二进制格式的流。这是消息对象在线缆间进行传输的最有效而简洁的表示方式。一些网络传输系统不允许发送和接收二进制数据。这类传输迫使应用程序在发送之前将所有的二进制数据转换成ASCII文本表示形式。在这种情况下(或者要得到最佳协作能力的时候),.NET Remoting在System.Runtime.Serialization.Formatters.Soap名字空间中提供SoapFormatter类型。SoapFormatter使用消息的SOAP表示形式将消息序列化为流。
下面为创建信道的一个示例过程。
//标识符提供程序 BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
//信道名称及端口 IDictionary dict = new Hashtable(); dict["name"] = "MacSystem.Server.Channel"; dict["port"] = 8087;
//注册TCP通信信道 TcpServerChannel serverChannel = new TcpServerChannel(dict, serverProvider); ChannelServices.RegisterChannel(serverChannel, false); |
Ø 代理对象:前面也说过,客户端不能直接调用远程对象,客户机只能通过代理对象来操作远程对象。代理对象,又分为透明代理和真实代理。在客户机看来,代理对象和远程对象是一样的。客户机调用透明代理对象上的方法,透明代理再调用真实代理上的Invoke方法,Invoke方法再使用消息接受器把消息传递到信道上。
下图是客户机的方法调用导致消息在信道间传递的一个体系结构图:
消息在信道上的传递过程
Ø 消息接受器:如上图所示,消息接受器在服务器端和客户端都有,接受真实代理的调用,把序列化的消息发布到信道上。
Ø 激活器:这涉及到对象生命期管理,客户机使用激活器在服务器上创建远程对象,或者说是申请一个远程对象的引用。
Ø RemotingConfiguration类:该类用于配置远程服务器和客户机的一个实用类,它可以用于读取配置文件或者动态地配置远程对象。说明一点的是:RemotingConfiguration类中的大部分属性、方法都是静态的,这就意味着很多属性,如应用程序名称,只能通过当前属性或配置文件设置一次。如果应用程序运行在宿主环境中,例如 Internet 信息服务 (IIS),则可能已经设置了该值(通常将其设置为虚拟目录)。如果未设置应用程序名称,则当前属性将返回空引用
Ø ChannelServices类:该类用于注册信道,并把消息分派到信道上。
//服务器端:演示如何使用 ApplicationName 属性指示远程处理应用程序的名称 ChannelServices.RegisterChannel(new TcpChannel(8082));
RemotingConfiguration.ApplicationName = "HelloServiceApplication"; RemotingConfiguration.RegisterWellKnownServiceType( typeof(HelloService), "MyUri", WellKnownObjectMode.SingleCall );
//客户端:演示如何从指定的应用程序访问远程对象 ChannelServices.RegisterChannel(new TcpChannel()); RemotingConfiguration.RegisterWellKnownClientType(typeof(HelloService), "tcp://localhost:8082/HelloServiceApplication/MyUri" );
HelloService service = new HelloService(); |
第一步:确定使用的信道。
前面已经说过,.NET Remoting提供了三种预定义的信道:TCP、HTTP、IPC,他们各有自己的特点,根据自己需要进行选择。对于每个信道,都有一些可以配置的信息,如:
Ø 信道名称:若不指定,则使用默认名称,每种信道,都有默认的信道名称;不过为了区分计,开发者最好给自己创建的信道命名。
Ø 信道使用的格式化提供程序:如果不指定,则使用默认形式。
Ø 信道优先级:优先级越高,则被选择进行连接的机会则越大。
Ø 信道的端口:服务器端信道,则必须具备一个所有客户都知道的端口即固定端口,客户端则可以由系统自动分配端口。
Ø ……其它属性参见MSDN
下面是一些创建信道的示例代码:
// 1. 使用默认构造函数创建一个TCP侦听信道 TcpServerChannel serverChannel = new TcpServerChannel(9090); ChannelServices.RegisterChannel(serverChannel);
// 2. 使用带信道配置属性的形式创建TCP侦听信道 IDictionary dict = new Hashtable(); dict["port"] = 9090; dict["authenticationMode"] = "IdentifyCallers";
TcpServerChannel serverChannel = new TcpServerChannel(dict, null); ChannelServices.RegisterChannel(serverChannel);
// 3. 指定信道名称、端口及格式标识符提供程序的形式创建信道 BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; TcpServerChannel channel = new TcpServerChannel( "Server Channel", 9090, serverProvider); ChannelServices.RegisterChannel(serverChannel,false); |
第二步:注册信道。
注册信道就是把已经创建的信道,使用ChannelServices类的RegisterChannel方法来“向信道服务注册信道”。
示例代码如上所示。
第三步:注册远程对象。
服务器端打算发布一个可以被客户端调用的远程对象,那它必须以某种方式“告诉”系统:我这里有一个这样的服务可供你使用。这里的“告诉”的过程,就是注册远程对象。
在.NET Remoting中,注册远程对象,使用RemotingConfiguration类提供的静态方法RegisterWellKnownServiceType或RegisterActivatedServiceType来进行注册。至于选择哪种注册方式,则根据远程对象的激活方式而定。关于这两个方法,做如下说明:
服务器激活分为:singleton和singlecall两种
singleton模式是第一次的方法请求会产生一个新的实例,之后一直存在,生命周期一直到宿主程序的生命周期的结束。
singlecall模式是每一次的方法请求都会产生一个新的实例,生命周期到方法调用结束。
客户端激活
客户端激活的生命周期是客户端控制的。可以通过租约来控制。
服务器激活只有在客户端在对象上进行方法调用时才创建这些对象,而不会在(客户端激活)客户端调用 new 或 Activator.GetObject 时创建这些对象;这节省了仅为创建实例而进行的一次网络往返过程
RegisterWellKnownServiceType 功能:将服务端上的对象 Type 注册为已知类型(“单个调用”(singlecall) 或 singleton)。 激活方式:服务器端激活 服务器端激活模式又有两种Singleton 和 SingleCall,说明如下
|
||
RegisterActivatedServiceType 功能:将服务端上的对象 Type 注册为可根据请求从客户端激活的类型 激活方式:客户端激活 配置:“客户端激活的对象”是当客户端调用 new 或 Activator.CreateInstance() 时在服务器上创建的。客户端本身使用生存期租用系统,可以参与到这些实例的生存期中。这种激活机制能够提供最广泛的设计灵活性。如果使用客户端激活,当客户端试图激活对象时,激活请求将发送到服务器。这种机制允许使用参数化的构造函数和针对每个客户端的连接状态管理。使用客户端激活,每个客户端接受其特定的服务器实例提供的服务,从而简化了多个调用时对象状态的保存过程。但使用这些对象时一定要谨慎,因为很容易忘记会话是分布式的,对象实际上不仅在进程之外,而且在多层应用程序的情况下,还有可能在计算机之外(在 Internet 上设置一个属性并不过分)。实用而不花哨的接口应该成为这里的准则:为了提高性能,我们可能需要在高度结合与松散耦合之间进行权衡。要创建客户端激活类型的实例,可以通过编程的方法配置应用程序,也可以进行静态配置。在服务器上进行客户端激活的配置相当简单。 |
第四步:注销信道。
在程序关闭时,或者清理资源时,要关闭调已经注册的信道,这样好让出服务所使用的计算机端口,方法就是调用ChannelServices.UnregisterChannel即可实现,示例代码如下:
//卸载名称为"MacSystem.Server.Channel的通信信道 IChannel[] regChannels = ChannelServices.RegisteredChannels;
foreach (IChannel channel in regChannels) { if (channel.ChannelName == "MacSystem.Server.Channel") { (IchannelReceiver).StopListenning(null); ChannelServices.UnregisterChannel(channel); break; } }//end foreach() |
第一步:创建信道
客户端的信道注册,跟服务器端的注册基本相同,差别在于它不必指定端口,和服务器端对应的,则使用TcpClientChannel、HttpClientChannel类等,当然也可以使用TcpChannel、HttpChannel类来注册。
在创建了信道后,也是使用ChannelServices类的RegisterChannel方法来完成信道的注册。
第二步:发现URL
客户端要激活服务器上的远程对象,也就是说要获得一个远程对象的本地代理,则必须首先获得远程对象的URL。该URL和Web浏览器的URL具有一样的含义,具有如下的格式:
Protocol://server:port/URI
这里的协议,也就是信道的格式,如tcp、http、ipc。不过,由于IPC机制只能使用在单个机器上,因此,不需要使用服务器地址。
这里的URI则包括远程对象的应用程序名成,对象的服务名称,如下面的示例:
http://localhost:8085/helloapp/hello
tcp://localhost:8087/helloapp/hello
ipc://8088/helloapp/hello
第三步:创建对象
创建对象,也就是在客户端激活服务器上的对象,并获得这个远程对象的一个本地代理(透明代理)。
在客户端创建远程对象,有两种方法,一种是使用new方法来创建,另外一种方法则是使用激活类的Activator的创建方法。针对每一种方法,在不同的激活模式下,又稍有区别,解释如下:
服务器端激活: 方法一:使用new激活远程知名对象 RemotingConfiguration.RegisterWellKonwnClientType(typeof(Hello), “tcp://localhost:8085/helloapp/hi”); Hello hello = new Hello();
方法二:使用激活器创建服务器激活的已知对象 Hello hello = (Hello)(Activator.GetObject((typeof(Hello), “tcp://localhost:8085/helloapp/hi”); |
客户端激活: 方法一:使用new激活远程对象 RemotingConfiguration.RegisterActivatedClientType(typeof(Hello), “tcp://localhost:8085/helloapp/hi”); Hello hello = new Hello();
方法二:使用激活器 Hello hello = (Hello)(Activator.CreateInstance((typeof(Hello), “tcp://localhost:8085/helloapp/hi”); 说明:CreateInstance用于创建客户激活的远程对象。 |
说明一下,这里的方法一,new操作符并没有创建新的远程对象,而是泛湖一个与远程对象相思的代理对象。
第四步:注销信道
客户端的信道注销和服务器端是一样的,这里就不再做过多说明。
这里的事件调用,指的是服务器对客户端的事件做出反应,以及客户端对服务器上的事件做出反应。 例如,在客户端调用了某个操作时,服务器进行日志记录;或者,在服务器向所有客户端发送一条消息时,客户端截获此消息并进行处理等。
如此一来,就包括两个方面,一个是服务器注册客户端事件,以拦截客户端的某些事件;一个是客户端注册服务器上的事件。
不过,总的说来,Remoting的核心是远程对象,因此,对事件的注册、触发等功能,还是和远程对象有关。还可以这样说,远程对象是Remoting事件调用的核心。
其实,服务器注册客户端事件,相对客户端注册服务器事件来说,是比较简单的。因为,客户端在调用服务器上的远程对象时,代码都在服务器上执行,服务器很容易截获这一事件,从而进行自己的处理。
具体的实现思路是,在远程对象中定义一个事件,然后在某方法内部,调用该事件的处理函数,这样,在客户端调用该方法时,就触发了事件。
下面给出一个远程对象的示例代码:
public delegate void CallFooDelegate(string message);
public class RemoteObject:MarshByRefObject { public static event CallFooDelegate CallFooEvent; public void Foo(String message) { //……Do Something
//触发事件 if (CallFooEvent != null) { CallFooEvent(message); } }
…… } |
说明一下,上面的示例代码中,RemoteObject直接从MarshByRefObject对象集成,没有实现其它任何接口,这样在程序发布的时候,客户端和服务器端都必须包含RemoteObject组件,如此一来,最常见的发布模型如下图所示:
这样,在客户端就具有了远程对象的代码,这在某种程度上来说,是不安全的。因此,一个推荐的方案是:创建一个接口,让远程对象实现该接口,然后在客户端,只包含该接口,在服务器端则包含两个部分,这样一来,组件示意图如下:
客户端注册服务器上的事件,是个毕竟复杂的过程,自己在刚开始的时候,以外和服务器注册客户端事件是一样的,结果稍一思考,就冷汗一头,原来根本不是那么回事儿。下面是我在此险阻前的开发历程。
问题的提出:
在服务器注册客户端事件时,只要客户端激活了远程对象,就可以发送消息,而只要服务器订阅了此事件,就可以处理客户端的消息了。然而,就客户端注册服务器事件这一问题来说,如果把思路置反,由远程对象发送消息,问题能否解决呢?
问题分析
然而,第一个事实是:在激活远程对象前,服务所做必须做的是“注册”该远程对象类以给客户端提供服务,然后由客户端决定何时来创建一个远程对象,也就是说,服务器端没有显式的创建过远程对象,这样,既然服务器端没有显式的对象,那么又如何来显式的操作之以传递消息呢?
第二个事实是:按照服务器注册客户端激活的方式使用远程对象,客户端得到的远程对象是根据需要创建的,在事件上来说,这些远程对象并不保证具有完全一致的状态。因此,在服务器端注册对象后显式创建一个对象的思路也并不可行。
第三个事实是:客户端的使用的远程对象只是一个代理,和服务器上的远程对象处于完全不同的应用程序域,或者说,服务器上的对象和客户端的对象是具有相似性但完全不同的两个“物体”,因此,在服务器端,或者是在客户端对各自对象所做的操作,是不会互相影响的。这同样也说明了在服务器端注册对象后显式创建一个对象的思路也并不可行。
第四个事实:客户端要订阅服务器事件,那么事件处理程序向对象的注册动作,也就应该在客户端完成,而服务器端还需逐个调用各个客户端注册的处理程序,这就要求客户端和服务器端所操作的对象是“同一个”。
这样,为了实现客户端注册客户端事件,必须让远程对象具有的特征也就很明显了:客户端得到的远程对象实际上是服务器上对象的代理。
为了实现这一特征,最好的方法是在服务器上创建一个远程对象,然后将此对象公布出来(注意,这个Sington方式有着本质的区别,Sington方式只是保证在任何一时间上只有一个对象存在于服务器上),本文第一部分所述的服务器端注册方式都不能满足这点,那么还有其它的注册方式吗?
经过查阅,我发现了RemotingServices.Marshal方法,该方法接受 MarshalByRefObject,并将其转换为具有指定 URI 和提供的Type 的 ObjRef 类的实例。该方法注册远程对象的方式如下:
RemoteObject Obj = new RemoteObject ();
ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");
Marshal方法与前述的几种注册方式不一样。前面的方式,远程对象是根据客户端调用的方式,来自动创建的。而该Marshal方法则显式地创建了远程对象实例,然后将其Marshal到通道中,形成ObjRef指向对象的代理。只要生命周期没有结束,这个对象就一直存在。而此时客户端获得的对象,正是创建的Obj实例的代理。
如此,使用这种注册方式,就可以保证每次客户端所获得的远程对象都是“相同”的,这样就满足了客户端注册远程服务器事件的要求。那么,使用这种方式来构建真正的系统,还需要什么呢?
实践过程:
有了新的方法及思路,那么剩下的就是编写远程对象的功能代码,之后在服务器上发布,然后客户端进行连接并注册事件。
写出的远程对象代码如下:
public class RemoteObject : MarshalByRefObject , IRemoteObject { #region IRemoteObject 成员
public event GetClientMessageDelegate GetClientMessageEvent = null;
public event BroadcastMessageDelegate BroadcastMessageEvent = null;
public void SendMessageToServer(string user, string message) { //调用本地事件处理函数 if (GetClientMessageEvent != null) { GetClientMessageEvent( user, message); }
//将此客户端消息作为广播转发 BroadcastMessage(user, message); }
#endregion
/// <summary> /// 发送广播消息 /// </summary> /// <param name="caption">消息标识</param> /// <param name="message">消息内容</param> public void BroadcastMessage(string caption, string message) { if (BroadcastMessageEvent != null) { BroadcastMessageDelegate tmpEvent = null; foreach (Delegate del in BroadcastMessageEvent.GetInvocationList()) { try { tmpEvent = (BroadcastMessageDelegate)del; tmpEvent( caption, message); } catch (Exception e) { Console.WriteLine(e.Message); Console.WriteLine(e.StackTrace);
//注册的事件处理程序出错,删除 BroadcastMessageEvent -= tmpEvent; }//end try-catch }//end foreach() }//end:if (BroadcastMessageEvent != null) }
public override object InitializeLifetimeService() { //return base.InitializeLifetimeService(); return null; } } |
服务上发布远程对象的代码如下:
private void PublishRemoteObject() { //tcp channel BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full; TcpServerChannel serverChannel = new TcpServerChannel( _ServerChannelName, _Port, serverProvider );
ChannelServices.RegisterChannel(serverChannel, false);
//RemotingConfiguration RemotingConfiguration.ApplicationName = "CoolQ";
//Marsh _MyRemoteObject.GetClientMessageEvent += new GetClientMessageDelegate(obj_GetClientMessageEvent); RemotingServices.Marshal(_MyRemoteObject, "RemoteObj");
} |
在客户端也写出了获得远程对象及注册事件的方法如下:
//tcp channel BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary dict = new Hashtable(); dict["port"] = 0; dict["name"] = _ClientChannelName; TcpChannel channel = new TcpChannel(dict, new BinaryClientFormatterSinkProvider(),serverProvider); ChannelServices.RegisterChannel(channel,false);
_MyRemoteObject = (IRemoteObject)(Activator.GetObject(typeof(IRemoteObject), "tcp://192.168.13.2:9527/CoolQ/RemoteObj")); _MyRemoteObject.BroadcastMessageEvent += new BroadcastMessageDelegate (ProcessBroadcastMessage); |
这样,就“完成”了客户端注册服务器端事件的功能,立即编译,满怀希望的等着出现运行结果,然后人生不如意事十有八九啊,迎接我的确是一个
附带的异常信息是:System.Reflection.TargetInvocationException: 调用的目标发生了异常。 ---> System.IO.FileNotFoundException: 未能加载文件或程序集“CollogueClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”或它的某一个依赖项。系统找不到指定的文件。
上面的信息出现在客户端,却提示找不到客户端程序集呢,这是怎么回事呢?别急,一步一步来。跟踪代码,发现异常出现在_MyRemoteObject.BroadcastMessageEvent += new BroadcastMessageDelegate (ProcessBroadcastMessage)这里,也就是在向服务器注册事件处理程序的时候出错的。由于客户端的远程对象代理是服务器端对象通过序列化后得到的,因此,对远程对象进行的事件注册,实际上发生在服务器上,而在注册时,.NET要加载ProcessBroadcastMessage所在的程序集,而这个程序集就是客户端程序集,在服务器端当然没有这个程序集,于是就出现这个异常了。
客户端注册服务器事件的处理代码,必定要位于客户端,而服务器事件又是由位于服务器上的远程对象捕获的,这样,事件在服务器上,处理代码处在客户端上,这中间如何来架起桥梁呢?
看到桥梁这个“词汇”,联想起2.3.1部分中的远程对象接口类的功能,我们能否也建立起一个中间类来做这个桥梁呢,而这个中间类就像接口类一样,位于服务器端和客户端,在服务器端,它接收远程对象的调用,在客户端,它复杂调用用户注册的处理代码,也就是说,把服务器直接对客户端代码的调用,转换成对中间类的调用,然后再由中间累代替服务器去调用客户端代码。
如此,中间类的代码如下:
public class RemoteObjectWrapper : MarshalByRefObject { public event BroadcastMessageDelegate WrapperBroadcastMessageEvent = null;
/// <summary> /// 发送广播消息的包装函数 /// </summary> /// <param name="caption">标题</param> /// <param name="message">消息</param> public void WrapperBroadMessage( string caption, string message) { if (WrapperBroadcastMessageEvent != null) { WrapperBroadcastMessageEvent( caption, message); }//end if }
//重载生命周期函数,使之无限长 public override object InitializeLifetimeService() { //return base.InitializeLifetimeService(); return null; } } |
这个包装类,放在一个公共程序集中,在发布的时候,服务器端和客户端都有一份,这样就保证了服务器可以加载到所需要的代码段。
这样,在客户端注册服务器事件,就使用这个中间类做桥梁,避免服务器直接加载客户端代码。客户端注册服务器事件调整后的代码如下,
RemoteObjectWrapper _WrapperRemoteObject = new RemoteObjectWrapper(); _WrapperRemoteObject.WrapperBroadcastMessageEvent += new BroadcastMessageDelegate(_WrapperRemoteObject_WrapperBroadcastMessageEvent); _MyRemoteObject.BroadcastMessageEvent += new BroadcastMessageDelegate(_WrapperRemoteObject.WrapperBroadMessage); |
服务器端不需做调整。
到此,服务器事件注册完毕,运行,调试,一路顺畅,呵呵,问题“基本”解决。
也许,有人要问,为什么这样就可以了呢,其实,我对这个地方也不甚明了,尝试着再做点解释吧。前面说过,使用中间类之前,服务器端的委托要装载client程序集,于是出现了上面的异常。现在我们把远程对象委托装载的权利移交给RemoteObjectWrapper,而这个类的程序集在服务器上有,所以这个加载不存在问题。在实际运行过程中,这个类对象是放在客户端的,所以它要装载client程序集丝毫没有问题。语句:
RemoteObjectWrapper _WrapperRemoteObject = new RemoteObjectWrapper();
_WrapperRemoteObject.WrapperBroadcastMessageEvent += new BroadcastMessageDelegate(_WrapperRemoteObject_WrapperBroadcastMessageEvent);
实现了这个功能。
不过此时虽然订阅了事件,但事件还是客户端的,没有与服务端联系起来。而服务端的事件是放到远程对象中的,所以,还要订阅事件,这个任务由远程对象_MyRemoteObject来完成。但此时它订阅的不再是客户端的事件处理器了,而是RemoteObjectWrapper的触发事件方法WrapperBroadMessage。那么此时委托同样要装载程序集,但此时装载的就是WrapperBroadMessage所在的程序集了。由于装载发生的地点是在服务端,而WrapperBroadMessage所在的程序集正是公共程序集(前面已说过,RemoteObjectWrapper 应放到公共程序集Common.dll中),而公共程序集在服务端和客户端都已经部署了。自然就不会出现找不到程序集的问题了。
1、安全验证问题
在ChannelServices类提供的RegisterChannel方法里面,一共有两个版本,其中一个没有下述的第二个参数,但微软已经把其标记为“过时”的方法,建议使用者使用下面的方法:
ChannelServices.RegisterChannel(Ichannel channel, bool security)
针对这个方法中的第二个参数,刚开始没有太在意,在使用时,直接使用true来进行调用,然而程序出现异常,把true改为false之后,程序即变得正常起来,这下才意识到这个参数原来不是摆设,赶快搜索,MSDN给出的解释是:
esureSecurity参数,如果启用了安全,则为 true;否则为 false。将该值设置为 false 将不会使在 TCP 或 IPC 信道上所做的安全设置无效。对于 TcpServerChannel,将 esureSecurity 设置为 true 将在 Win98 上引发异常(因为 Wi9x 上不支持安全 tcp 信道);对于 Http 服务器信道,这样会在所有平台上引发异常(如果想要安全的 http 信道,用户需要在 IIS 中承载服务)。
至于这个解释,本人看的云里雾里,不得要领,也许功力不够,详细的解释乃至让人明白的解释,以后再说吧。
2、通道过滤类型: 没有正在侦听的已注册服务器信道异常
从.NET 1.1开始,序列化的安全级别得到提高了。所以,在注册通道时,应该将TypeFilterLevel设置为Full;
下面是一个简单的演示代码。
服务器端:
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable(); props["port"] = 8085;
TcpChannel chan = new TcpChannel(props,clientProvider,serverProvider); ChannelServices.RegisterChannel(chan); |
注意,除了服务器段要做这个修改外,客户端也要做对应的修改。如果只修改服务器段不修改客户端,就会有下面的异常产生:
An unhandled exception of type 'System.Runtime.Remoting.RemotingException' occurred in mscorlib.dll
Additional information: 此远程处理代理没有信道接收,这意味着服务器没有正在侦听的已注册服务器信道,或者此应用程序没有用来与服务器对话的适当客户端信道。
客户端的代码跟服务器的代码几乎一样:
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable(); props["port"] = 8084;
TcpChannel chan = new TcpChannel(props,clientProvider,serverProvider); ChannelServices.RegisterChannel(chan); |
说明一下:注意如果是在同一台机子上作测试,客户端、服务器端的端口号不要一样。
关于这个问题,微软在MSDN中给出来了一些正式解释:
依赖于运行时类型验证的远程处理系统必须反序列化一个远程流,然后才能开始使用它,未经授权的客户端可能会试图利用反序列化这一时机。为了免受这种攻击,.NET 远程处理提供了两个自动反序列化级别:Low 和 Full。Low(默认值)防止反序列化攻击的方式是,在反序列化时,只处理与最基本的远程处理功能关联的类型,如自动反序列化远程处理基础结构类型、有限的系统实现类型集和基本的自定义类型集。Full 反序列化级别支持远程处理在所有情况下支持的所有自动反序列化类型。
警告 不要以为控制反序列化是应用程序需要的唯一安全机制。在分布式应用程序中,即使严格控制序列化也不能防止这种危险的发生:即未经授权的客户端截获通信内容,然后以某种方式利用该通信内容,即使只是向其他用户显示数据,也会造成损害。因此,虽然 Low 反序列化级别对某些基于自动反序列化的攻击类型提供了一定的保护,但您仍然必须考虑是否使用身份验证和加密来为您的数据提供完全的保护。
如果应用程序需要使用仅在 Full 反序列化级别才可用的远程处理功能,您必须提供身份验证的类型和必要的加密级别,以保护任何在使用远程方案中的这些高级功能时可能遭受风险的资源。
解决之道:
您可以通过编程方式或使用应用程序配置文件设置反序列化级别。
以编程方式设置反序列化级别,示例代码见上面的服务器端代码。
使用应用程序配置文件设置反序列化级别。
若要使用配置文件设置反序列化级别,必须显式指定 <formatter> 元素的 typeFilterLevel 属性。虽然这通常是在服务器端指定的,但您还必须为注册来侦听回调的客户端上的任何信道指定这一属性,以控制其反序列化级别。以下示例为应用程序域中的 SoapFormatter 和 BinaryFormatter 显式地将反序列化级别设置为 Low。
<configuration> <system.runtime.remoting> <application> <service> <wellknown type="ServiceType, common" objectUri="ServiceType.soap" mode="Singleton" /> </service> <channels> <channel ref="http"> <serverProviders> <provider ref="wsdl" /> <formatter ref="soap" typeFilterLevel="Low" /> <formatter ref="binary" typeFilterLevel="Low" /> </serverProviders> </channel> </channels> </application> </configuration>
|
这一问题影响到的APIs有:
System.Runtime.Serialization.ISerializable
System.Runtime.Remoting.ObjRef
System.Runtime.Remoting.Lifetime.ILease
System.Runtime.Remoting.Lifetime.ISponsor
System.Runtime.Remoting.Contexts.IContributeEnvoySink
System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider
System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider
至于Remoting,真乃一博大之学问,小可我才用不久,这里只是自己使用过程中遇到的一些问题归结,至于Remoting中的很多方面,例如异步调用、配置文件等等,都没涉及,以后有机会,再来细细研究。