.Net Remoting(基本操作) - Part.2
Remoting 构架
接下来我们考虑通常的情况,也就是 客户程序 与 宿主程序 位于不同的进程中的情况。
因为我是在我本地机器做的测试,所以只是位于不同进程,实际上位于不同机器中的操作是完全一样的,仅仅是Uri不同,下面将会看到。
Remoting 是.Net Framework的一个组成部分,作为一个框架(Framework),两个必备的特性是 基本实现 和 可扩展(可定制)。基本实现的意思是说:对于Remoting机制的各个组成部分,.Net 已经提供了一至两个基本实现,可以直接使用;而可扩展的意思是说:对于每个组成部分,都可以由Framework的用户自行定制。Remoting 的构架也是如此,它的几乎每一个部分都是可以由程序员提供实现的,但是.Net也提供了一套默认实现,通常情况下是没有必要自行定制的。本章主要讲述Remoting的各个组成部分。
客户端(客户应用程序)
客户端的处理包含三个基本的组成部分,代理(Proxy)、格式器(Formatter) 和 通道(Channel)。
客户端总是通过一个代理来和服务端对象进行交互。客户端向代理请求属性或者方法调用,然后代理将请求发送给服务端的对象。每一个代理绑定一个远程对象,多个代理也可以绑定同一个对象(Singleton方式,后面会介绍);客户端的多个对象也可以使用同一个代理。代理分为两部分,一个名为透明代理(Transparent Proxy),一个名为真实代理(Real Proxy)。透明代理提供了和服务对象完全一致的公共接口,当客户进行方法调用时,透明代理将栈帧(Stack Frame,在栈中为参数、返回地址和局部变量保留的一块内存区,必要时在过程调用中使用)转换为消息(Message),然后将消息发送给真实代理。这个消息对象包含了调用的对象的方法信息,包括方法签名、参数等,同时还包括客户端的位置(注意这里,方法回调(Callback)时会再提到)。真实代理知道如何连接远程对象并将消息发送给它。
真实代理收到消息后,请求Formatter 对象对其进行序列化,同时将客户程序中断(block)。.Net 内置了两种序列化格式,一种是二进制Binary,一种是SOAP。Formatter将消息进行序列化之后,然后将其发送到通道中,由通道将消息发送到远程对象。当请求返回时,Formatter将返回的消息反序列化,然后再提交给代理,代理将返回值放到发送请求的客户对象的调用堆栈上,随后将控制返回给客户调用程序(解除中断)。这样就给了客户对象一个错觉:代理即为远程对象。
服务端(宿主应用程序)
服务端主要由 通道(Channel)、格式器(Formatter)、Stack Builder组成。
在服务端,宿主程序保持着为Remoting所打开的端口的监听,一旦通道收到消息,它便将消息发送给Formatter,Formatter将消息进行反序列化,然后将消息发送给Stack Builder,Stack Builder读取消息,然后依据消息创建对象(可选),调用方法。方法返回时,Stack Builder将返回值封装为消息,然后再提交给Formatter,Formatter进行格式化之后,发送到通道传递消息。
Remoting对象的三种激活方式
上一章 .Net Remoting - Part.1 中,我们提到了传值封送和传引用封送,并各给出了一张示意图,实际上,传引用封送还分为了三种不同的方式,下面来一一来介绍。对于传引用封送,记住各种方式的共同点:服务对象创建且一直保持在宿主程序中。我知道Remoting的概念多得已经让你厌烦,而且在不结合例子的情况下很难理解,所以这小节我们仅归纳它的特点,到后面例子中,我们再详细看。
客户激活(Client activated )
客户激活方式我们实际上已经了解过了,就是在Part.1中我们在单一进程中跨应用程序域传引用封送时的情况,我们再来回顾一下这张图:
结合这幅图,我们可以看出对于每个客户,创建了其专属的远程对象为其服务(由Part.1的代码可以看出,对象的状态在两次方法调用中也是维持着的)。除此以外,一个代理可以为多个客户对象服务。
客户激活模式的缺点就是 如果客户端过多时,或者服务对象为“大对象”时,服务器端的压力过大。另外,客户程序可能只需要调用服务对象的一个方法,但是却持有服务对象过长时间,这样浪费了服务器的资源。
服务激活 Singleton(Server activated Singleton)
这个模式的最大特色就是所有的客户共享同一个对象。服务端只在对象第一次被调用时创建服务对象,对于后继的访问使用同一个对象提供服务。如下图所示:
因为Sinlgton对象是在第一次访问(比如方法调用)时由.Net自动创建的,后继的访问不能重新创建对象,所以它不提供有参数的构造函数。另外,由于Singleton对象的状态由其它对象所共享,所以使用Singleton对象应该考虑线程同步 的问题。
3.3 服务激活 Single Call(Server activated Single Call)
Single Call方式是对每一次请求(比如方法调用)创建一个对象,而在每次方法返回之后销毁对象。由此可见Single Call 方式的最大特点就是 不保存状态。使用Single Call的好处就是不会过久地占用资源,因为方法返回后对资源的占用就随对象被销毁而释放了。最后,Single Call 方式也不允许使用由参数的构造函数。
Remoting程序的基本操作
在这一章我们综合前面的知识,通过编码的方式一步步实现一个Remoting的范例程序,以此来熟悉Remoting的一些基本操作和步骤,并对前面的知识加深一下理解。
服务程序集
我们首先创建服务程序集,它即为向客户程序提供服务的远程对象的实现代码。先创建一个类库项目ServerAssembly,然后创建类型ServerAssembly.DemoClass(为Part.1中的ClassLib.DemoClass添加了几个方法)。我们让它继承自MarshalByRefObject,使用更为常用的传引用封送形式:
public class DemoClass:MarshalByRefObject { 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); } public int GetCount() { return count; } }
创建这几个方法的作用如下:
- DemoClass()构造函数用于追踪远程对象创建的时机。
- ShowCount()方法用于测试向远程对象传递参数,以及对象状态的保存。
- ShowAppDomain()方法用于验证对象创建的位置(是否真的位于远程)。
- GetCount()方法用于测试获取远程对象的返回值。
宿主应用程序
接下来我们新创建一个空解决方案ServerSide,在其下添加一个新的控制台项目ServerConsole,然后再将上面创建的项目ServerAssembly添加进来。除此以外,还需要添加System.Runtime.Remoting的引用,它一般位于C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Runtime.Remoting.dll(修改系统目录)。
注册通道
实现宿主应用程序的第一步就是注册通道。通道是实现了System.Runtime.Remoting.Channels.IChannel的类。通道分为两种,一种是发送请求的通道,比如说客户应用程序使用的通道,这种类型的通道还需要实现 System.Runtime.Remoting.Channels.IChannelSender 接口;一种是接收请求的通道,比如说宿主应用程序使用的通道,这种类型的通道还需实现System.Runtime.Remoting.Channels.IChannelReceiver接口。
通常我们不需要实现自己的通道,.Net 提供了三个内置的通道,分别是 System.Runtime.Remoting.Channels.Http.HttpChannel、System.Runtime.Remoting.Channels.Tcp.TcpChannel 以及 System.Runtime.Remoting.Channels.Ipc.IpcChannel。由于 IpcChannel 不能跨机器(只能跨进程),所以我们仅使用最为常用的 HttpChannel和TcpChannel为例作为说明。它们均实现了 System.Runtime.Remoting.Channels 命名空间下的 IChannel、IChannelSender、IChannelReceiver接口,所以它们既可以用于发送请求,也可以用于接收请求。
接下来需要对通道进行注册,然后对这个通道进行监听。对于同一个应用程序域,同一类型(实际上是同一名称,因为同一类型的通道默认名称相同)的通道只能注册一次。对同一机器来说,同一端口也只能使用一次。同一应用程序域可以注册多个不同类型的通道。注册的方式是调用ChannelServices类型的静态方法RegisterChannel():
using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; using System.Runtime.Remoting.Channels.Http; namespace ServerConsole { class Program { static void Main(string[] args) { RegisterChannel(); // 注册2个通道 RemotingConfiguration.ApplicationName = "SimpleRemote"; Console.WriteLine("服务开启,按任意键退出..."); Console.ReadKey(); } private static void RegisterChannel() { // 创建通道实例 // IChannel tcpChnl = new TcpChannel(8501); IChannelReceiver tcpChnl = new TcpChannel(8501); // 注册tcp通道 ChannelServices.RegisterChannel(tcpChnl, false); // 注册http通道 IChannel httpChnl = new HttpChannel(8502); ChannelServices.RegisterChannel(httpChnl, false); } } }
上面的程序便成功注册了两个端口用于Remoting程序的监听。运行程序,然后在Windows的命令提示符中输入 netstat -a,查看端口状况,可以看到这两个端口位于监听状态(最后两行):
当通道从端口监听到新请求时,它会从线程池中抓取一个线程执行请求,从而可以不间断地对端口进行监听(不会因为处理请求而中断)。当关闭宿主程序时,.Net会自动释放端口,以便其他程序可以使用该端口。
在上面我们已经提到消息(Message)以某种特定格式通过通道传递。当我们使用上面的构造函数创建通道时,消息会以通道所默认的消息格式传递。对于TcpChannel来说,使用二进制,也就是Binary 格式;对于HttpChannel来说,使用SOAP消息格式。我们也可以使用重载的构造函数创建通道,指定通道所采用的消息格式,以TcpChannel为例:
public TcpChannel(IDictionary properties, IClientChannelSinkProvider clientSinkProvider, IServerChannelSinkProvider serverSinkProvider);
IDictionary是key已经预先定义好了的 属性/值 集合,属性有通道名称、端口号等。IClientChannelSinkProvider可以用于提供客户端通道消息所采用的格式;IServerChannelSinkProvider 用于提供服务端通道消息所采用的格式。我们知道一个通道实例不会同时用于位于客户端和服务端,所以,当创建服务端通道时,将IClientChannelSinkProvider设为null就可以了;同理,创建客户端通道时,将IServerChannelSinkProvider设为null。现在我们将上面的例子改一下,显示地设置通道所采用的消息格式:
class Program { static void Main(string[] args) { RegisterChannel(); // 先以第一种方式注册2个通道 RegisterChannel2(); // 以自定义模式注册1个通道 RemotingConfiguration.ApplicationName = "SimpleRemote"; Console.WriteLine("服务开启,可按任意键退出..."); Console.ReadKey(); } // 自定义Formatter和通道名称的注册方式 private static void RegisterChannel2() { IServerChannelSinkProvider formatter; formatter = new BinaryServerFormatterSinkProvider(); IDictionary propertyDic = new Hashtable(); propertyDic["name"] = "CustomTcp"; propertyDic["port"] = 8503; IChannel tcpChnl = new TcpChannel(propertyDic, null, formatter); ChannelServices.RegisterChannel(tcpChnl, false); } private static void RegisterChannel(){...} // 略 }
注意到上面我们通过propertyDic将通道的名称设为了CustomTcp,而在RegisterChannel()方法中,我们没有设置(此时,对于TcpChannel,会采用了默认名称:tcp)。通过显示指定通道名称的方式,对于同一种类型的通道,我们进行了多次注册。现在在命令提示符中输入 netstat -a ,应该可以看到一共监听了三个端口。
注册对象
注册通道之后,我们需要告诉.Net允许哪些类型可以被远程程序访问,这一步骤称为注册对象。如同上面所说的,有三种服务器端的远程对象类型:客户激活对象、服务激活Single Call、服务激活Singleton。
客户激活对象的注册方式需要使用RemotingConfiguration类型的RegisterActivatedServiceType()静态方法:
// 注册 客户激活对象 Client Activated Object private static void ClientActivated() { Console.WriteLine("方式: Client Activated Object"); Type t = typeof(DemoClass); RemotingConfiguration.RegisterActivatedServiceType(t); }
服务激活对象 可以使用RemotingConfiguration类型的 RegisterWellKnownServiceType()静态方法:
// 注册 服务激活对象 SingleCall private static void ServerActivatedSingleCall() { Console.WriteLine("方式: Server Activated SingleCall"); Type t = typeof(DemoClass); RemotingConfiguration.RegisterWellKnownServiceType( t, "ServerActivated", WellKnownObjectMode.SingleCall); } // 注册 服务端激活对象 Singleton private static void ServerActivatedSingleton() { Console.WriteLine("方式: Server Activated Singleton"); Type t = typeof(DemoClass); RemotingConfiguration.RegisterWellKnownServiceType( t, "ServerActivated", WellKnownObjectMode.Singleton); }
同一类型对象只可以用一种方式注册(客户激活 或者 服务激活)。即是说如果使用上面的方法注册对象,那么要么调用 ClientActivated(),要么调用ServerActivatedSingleCall()或者ServerActivatedSingleton(),而不能都调用。上面的RegisterWellKnownServiceType()方法接受三个参数:1.允许进行远程访问的对象类型信息;2.远程对象的名称,用于定位远程对象;3.服务激活对象的方式,Singleton或者Single Call。
对象位置
经过上面两步,我们已经开启了通道,并注册了对象(告诉了.Net哪个类型允许远程访问)。那么客户端如何知道远程对象位置呢?如同Web页面有一个Url一样,远程对象也有一个Url,这个Url提供了远程对象的位置。客户程序通过这个Url来获得远程对象。
RemotingConfiguration类型还有一个ApplicationName静态属性,当设置了这个属性之后,对于客户激活对象,可以提供此ApplicationName作为Url参数,也可以不提供。如果提供ApplicationName,则必须与服务端设置的ApplicationName相匹配;对于服务激活对象,访问时必须提供ApplicationName,此时两种方式的Uri为下面的形式:
protocal://hostadrress:port/ApplicationName/ObjectUrl // Server Activated protocal://hostadrress:port // Client Activated Object protocal:// hostadrress:port/ApplicationName // Client Activated Object
比如,如果通道采用协议为tcp,服务器地址为127.0.0.1,端口号为8051,ApplicationName设为DemoApp,ObjectUrl设为RemoteObject(ObjUrl为使用RegisterWellKnownServiceType()方法注册服务激活对象时第2个参数所提供的字符串;注意客户激活对象不使用它),则客户端在访问时需要提供的地址为:
tcp://127.0.0.1:8051/DemoApp/RemoteObject // Server Activated Object tcp://127.0.0.1:8051/DemoApp // Client Activated Object tcp://127.0.0.1:8051 // Client Activated Object
如果RemotingConfiguration类型没有设置ApplicationName静态属性,则客户端在获取远程对象时不需要提供ApplicationName,此时Url变为下面形式:
protocal://hostadrress:port/ObjectUrl // Server Activated Object protocal://hostadrress:port // Client Activated Object
客户应用程序
我们现在再创建一个空解决方案ClientSide,然后在其下添加一个控制台应用程序ClientConsole,因为客户端需要ServerAssembly.DemoClass的元信息来创建代理,所以我们仍要添加对ServerAssembly项目的引用。除此以外,我们依然要添加 System.Runtime.Remoting程序集。
客户应用程序的任务只有一个:获取远程对象,调用远程对象服务。记得客户应用程序实际上获得的只是一个代理,只是感觉上和远程对象一模一样。客户端获得对象有大致下面几种情况:
使用new操作符创建远程对象
客户应用程序可以直接使用new获得一个远程对象。例如下面语句:
DemoClass obj = new DemoClass();
看到这里你可能很惊讶,这样的话不是和通常的创建对象没有区别,为什么创建的是远程对象(这里用“远程对象”,只是为了说明方便,要记得实际上是代理对象)而非本地对象呢(注意本地客户程序ClientConsole也引用了ServerAssembly项目)?其实.Net和你一样,它也不知道这里要创建的是远程对象,所以,在使用new创建远程对象之前,我们首先要注册对象。注册对象的目的是告诉.Net,这个类型的对象将在远程创建,同时还要告诉.Net远程对象的位置。
我们知道远程对象有 客户激活 和 服务激活 两种可能,因此客户程序注册也分为了两种情况 -- 注册客户激活对象,注册服务激活对象。在客户端注册对象也是通过RemotingConfiguration类型来完成:
// 注册客户激活对象 private static void ClientActivated() { Type t = typeof(DemoClass); // 下面两个 url 任选一个 string url = "tcp://127.0.0.1:8501"; //string url = "tcp://127.0.0.1:8501/SimpleRemote"; RemotingConfiguration.RegisterActivatedClientType(t, url); } // 注册服务激活对象 private static void ServerActivated() { Type t = typeof(DemoClass); string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated"; RemotingConfiguration.RegisterWellKnownClientType(t, url); }
我们看到,尽管在服务端,服务激活有两种可能的方式,Singleton和SingleCall,但是在客户端,服务激活的两种方式采用同一个方法RegisterWellKnownClientType()方法进行注册。所以我们可以说 服务端决定服务激活对象的运行方式(Singleton或SingleCall)。
其它创建远程对象的方法
当我们在客户端对远程对象进行注册之后,可以直接使用new操作符创建对象。如果不进行注册来创建远程对象,可以通过 RemotingServices.Connect()、Activator.GetObject()、Activator.CreateInstance()方法来完成:
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated"; // 方式1 DemoClass obj = (DemoClass)RemotingServices.Connect(typeof(DemoClass), url); // 方式2 DemoClass obj = (DemoClass)Activator.GetObject(typeof(DemoClass), url); // 方式3 object[] activationAtt = { new UrlAttribute(url) }; DemoClass obj = (DemoClass)Activator.CreateInstance(typeof(DemoClass), null, activationAtt);
这几种方法,RemotingServices.Connect()和Activator.GetObject()是最简单也较为常用的,它们都是只能创建服务激活对象,且创建的对象只能有无参数的构造函数,并且在获得对象的同时创建代理。Activator.CreateInstance()提供了多达13个重载方法,允许创建客户激活对象,也允许使用有参数的构造函数创建对象,并且可以先返回一个Wrapper(包装)状态的对象,然后在以后需要的时候通过UnWrap()方法创建代理。CreateInstance()方法更详细的内容可以参考MSDN。
程序运行测试
Remoting 最让初学者感到困惑的一个方面就是 客户激活 与 服务激活 有什么不同,什么时候应该使用那种方式。说明它们之间的不同的最好方式就是通过下面几个范例来说明,现在我们来将上面的服务端方法、客户端方法分别进行一下组装,然后进行一下测试(注意在运行客户端之前必须保证服务端已经运行):
客户激活方式
先看下客户激活方式,服务端的Main()代码如下:
static void Main(string[] args) { RegisterChannel(); // 注册通道 ClientActivated(); // 客户激活方式 Console.WriteLine("服务开启,可按任意键退出...\n"); Console.ReadKey(); }
客户端的Main()代码如下:
static void Main(string[] args) { // 注册远程对象 ClientActivated(); // 客户激活方式 RunTest("Jimmy", "Zhang"); RunTest("Bruce", "Wang"); Console.WriteLine("客户端运行结束,按任意键退出..."); Console.ReadKey(); } private static void RunTest(string firstName, string familyName) { DemoClass obj = new DemoClass(); obj.ShowAppDomain(); obj.ShowCount(firstName); Console.WriteLine("{0}, the count is {1}.\n",firstName, obj.GetCount()); obj.ShowCount(familyName); Console.WriteLine("{0}, the count is {1}.",familyName, obj.GetCount()); }
程序运行的结果如下:
其中第一幅图是服务端,第二幅图是客户端,我们起码可以得出下面几个结论:
- 不管是对象的创建,还是对象方法的执行,都在服务端(远程)执行。
- 服务端为每一个客户端(两次RunTest()调用,各创建了一个对象)创建其专属的对象,为这个客户提供服务,并且保存状态(第二次调用ShowCount()的值基于第一次调用ShowCount()之后count的值)。
- 可以从远程获取到方法执行的返回值。(客户端从GetCount()方法获得了返回值)
上面的第3点看起来好像是理所当然的,如果是调用本地对象的方法,那么确实是显而易见的。但是对于远程来说,就存在一个很大的问题:远程对象如何知道是谁在调用它?方法执行完毕,将返回值发送给哪个客户呢?此时可以回顾一下第一篇所提到的,客户端在创建远程对象时,已经将自己的位置通过消息发送给了远程。
最后我们再进行一个深入测试,追踪对象是在调用new时创建,还是在方法调用时创建。将RunTest()只保留一行代码:
private static void RunTest(string firstName, string familyName) { DemoClass obj = new DemoClass(); // 创建对象 }
然后再次运行程序,得到的输出分别如下:
// 服务端 方式: Client Activated Object 服务端开启,按任意键退出... ======= DomoClass Constructor ======= ======= DomoClass Constructor ======= // 客户端 客户端运行结束,按任意键退出...
由此可以得出结论:使用客户激活方式时,远程对象在调用new操作时创建。
服务激活方式 -- Singleton
我们再来看一下服务激活的Singleton方式。先看服务端代码(“按任意键退出”等提示语句均以省略,下同):
static void Main(string[] args) { RegisterChannel(); // 注册通道 ServerActivatedSingleton(); // Singleton方式 }
再看下客户端的Main()方法:
static void Main(string[] args) { // 注册远程对象 ServerActivated(); RunTest("Jimmy", "Zhang"); RunTest("Bruce", "Wang"); }
程序的运行结果如下:
同上面一样,第一幅为服务端,第二幅图为客户端。从图中我们可以得出:当使用Singleton模式时,服务端在第一次请求时创建一个对象(构造函数只调用了一次)。对于后继的请求仅使用这个对象进行服务(即使再次调用构造函数也不会创建对象),同时多个客户端共享同一个对象的状态(ShowCount()的值累加)。
我们和上一小节一样,再次将客户端的RunTest()只保留为“DemoClass obj = new DemoClass(); ”一行语句,然后运行程序,得到的结果为:
// 服务端 方式: Server Activated Singleton 服务端开启,按任意键退出... // 客户端 客户端运行结束,按任意键退出...
这个结果出乎我们意料,但它又向我们揭示了Singleton的另一个性质:即使使用new操作符,客户端也无法创建一个对象,而只有在对象上第一次调用方法时才会创建。仔细考虑一下这个和上面的结论是类似的,只是更深入了一步。
服务激活方式 -- SingleCall
最后我们看一下SingleCall方式,注意到客户端的代码不需要做任何修改,所以我们只需要切换一下服务端的激活方式就可以了:
static void Main(string[] args) { RegisterChannel(); // 注册通道 ServerActivatedSingleCall(); }
我们再次看一下运行结果:
我们可能首先惊讶构造函数居然调用了有10次之多,在每次RunTest()方法中各调用了5次。如同前面所说,对于SingleCall方式来说,对象对每一次方法调用提供服务,换言之,对于每一次方法调用,创建一个全新的对象为其服务,在方法执行完毕后销毁对象。我们再看下客户端的输出:GetCount()方法全部返回0,现在也很明确了,因为每次方法调用都会创建新对象(在创建对象时,int类型的count被赋默认值0),所以SingleCall方式是不会保存对象状态的。如果想要为对象保存状态,那么需要另外的机制,比如将状态存储到对象之外:
public void ShowCount(string name, object clientId) { LoadStatus(this, clientId); // 加载对象状态 count++; Console.WriteLine("{0},the count is {1}.", name, count); SaveStatus(this, clientId); // 存储对象状态 }
其中LoadStatus()、SaveStatus()方法分别用于加载对象状态和 存储对象状态。注意到ShowCount()方法多了一个clientId参数,这个参数用于标示客户程序的id,因为服务端需要知道当前是为哪个客户程序加载状态。
最后,我们再次进行一下上面两节将RunTest()只保留为创建对象的一行代码,得到的运行结果和Singleton是一样的:
// 服务端 方式: Server Activated Singleton 服务端开启,按任意键退出... // 客户端 客户端运行结束,按任意键退出...
这说明使用SingleCall时,即使使用了new 来创建对象,也不会调用构造函数,只有在调用方法时才会创建对象(调用了构造函数)。
Remoting中的传值封送
很多朋友可能此刻会感到些许困惑,在Part.1的范例中,我们讲述AppDomain时,使用了传值封送和传引用封送两种方式,但是上面的三种激活方式都属于传引用封送。那么如何进行对象的传值封送呢(将DemoClass直接传到本地)?实际上,在上面的例子中,我们已经进行了传值封送,这个过程发生在我们在客户端调用 GetCount() 时。为什么呢?想一想,count值本来是位于服务端的,且int为可序列化对象(Serializable),在向客户端返回方法结果时,count值被包装为了消息,然后由服务端发送回了客户端,最后在客户端进行了解包装及还原状态。
为了看得更清楚一些,我们在ServerAssembly中再创建一个DemoCount类型,然后对这个类型进行传值封送,因为DemoCount仅仅是为了传送数据,不包含任何行为,所以我们将它声明为结构:
public class DemoClass : MarshalByRefObject { // 其余方法略... // 示范传值封送 public DemoCount GetNewCount() { return new DemoCount(count); } } [Serializable] public struct DemoCount { private readonly int count; public DemoCount(int count) { this.count = count; } public int Count { get { return count; } } public void ShowAppDomain() { AppDomain currentDomain = AppDomain.CurrentDomain; Console.WriteLine(currentDomain.FriendlyName); } }
在DemoClass中,我们又添加一个方法,它根据count的值创建了DemoCount对象,而DemoCount对象会通过传值封送传递到客户端。
现在修改客户端,再重载一个RunTest()方法,用来测试这次的传值封送:
// 测试传值封送 private static void RunTest() { DemoClass obj = new DemoClass(); obj.ShowAppDomain(); // 显示远程对象所在应用程序域 obj.ShowCount("张子阳"); // Count = 1 DemoCount myCount = obj.GetNewCount(); // 传值封送DemoCount myCount.ShowAppDomain(); // 显示DemoCount所在应用程序域 // 在客户端显示count值 Console.WriteLine("张子阳, count: {0}.", myCount.Count); }
此时我们再次进行测试,得到的结果如下:
可以看到,我们在客户端DemoCount上调用ShowAppDomain()方法时,返回了ClientApp.exe,可见DemoCount已经通过传值封送传递到了客户端。那么我们继续上面的问题,如何将DemoClass整个传值封送过来呢?首先,我认为没有这个必要,如果将服务对象整个封送到客户端来执行,那么Remoting还有什么意义呢?其次,我们来看如何实现它。方法很简单,我们创建一个工厂类作为远程服务对象,然后将我们实际要传值封送到客户端的对象(比如DemoClass),作为工厂方法的返回值。这个例子我就不再演示了,相信看过上面的示例,您已经明白了。
感谢阅读,希望这篇文章能给你带来帮助!