zoukankan      html  css  js  c++  java
  • .Net Remoting(基本操作) Part.2

    Remoting 构架

    接下来我们考虑通常的情况,也就是 客户程序 与 宿主程序 位于不同的进程中的情况。

    NOTE:因为我是在我本地机器做的测试,所以只是位于不同进程,实际上位于不同机器中的操作是完全一样的,仅仅是Uri不同,下面将会看到。

    Remoting 是.Net Framework的一个组成部分,作为一个框架(Framework),两个必备的特性是 基本实现 和 可扩展(可定制)。基本实现的意思是说:对于Remoting机制的各个组成部分,.Net 已经提供了一至两个基本实现,可以直接使用;而可扩展的意思是说:对于每个组成部分,都可以由Framework的用户自行定制。Remoting 的构架也是如此,它的几乎每一个部分都是可以由程序员提供实现的,但是.Net也提供了一套默认实现,通常情况下是没有必要自行定制的。本章主要讲述Remoting的各个组成部分。

    1.客户端(客户应用程序)

    客户端的处理包含三个基本的组成部分,代理(Proxy)、格式器(Formatter) 和 通道(Channel)。

    客户端总是通过一个代理来和服务端对象进行交互。客户端向代理请求属性或者方法调用,然后代理将请求发送给服务端的对象。每一个代理绑定一个远程对象,多个代理也可以绑定同一个对象(Singleton方式,后面会介绍);客户端的多个对象也可以使用同一个代理。代理分为两部分,一个名为透明代理(Transparent Proxy),一个名为真实代理(Real Proxy)。透明代理提供了和服务对象完全一致的公共接口,当客户进行方法调用时,透明代理将栈帧(Stack Frame,在栈中为参数、返回地址和局部变量保留的一块内存区,必要时在过程调用中使用)转换为消息(Message),然后将消息发送给真实代理。这个消息对象包含了调用的对象的方法信息,包括方法签名、参数等,同时还包括客户端的位置(注意这里,方法回调(Callback)时会再提到)。真实代理知道如何连接远程对象并将消息发送给它。

    真实代理收到消息后,请求Formatter 对象对其进行序列化,同时将客户程序中断(block)。.Net 内置了两种序列化格式,一种是二进制Binary,一种是SOAP。Formatter将消息进行序列化之后,然后将其发送到通道中,由通道将消息发送到远程对象。当请求返回时,Formatter将返回的消息反序列化,然后再提交给代理,代理将返回值放到发送请求的客户对象的调用堆栈上,随后将控制返回给客户调用程序(解除中断)。这样就给了客户对象一个错觉:代理即为远程对象。

    2.服务端(宿主应用程序)

    服务端主要由 通道(Channel)、格式器(Formatter)、Stack Builder组成。

    在服务端,宿主程序保持着为Remoting所打开的端口的监听,一旦通道收到消息,它便将消息发送给Formatter,Formatter将消息进行反序列化,然后将消息发送给Stack Builder,Stack Builder读取消息,然后依据消息创建对象(可选),调用方法。方法返回时,Stack Builder将返回值封装为消息,然后再提交给Formatter,Formatter进行格式化之后,发送到通道传递消息。

    3.Remoting对象的三种激活方式

    上一章 .Net Remoting - Part.1 中,我们提到了传值封送和传引用封送,并各给出了一张示意图,实际上,传引用封送还分为了三种不同的方式,下面来一一来介绍。对于传引用封送,记住各种方式的共同点:服务对象创建且一直保持在宿主程序中。我知道Remoting的概念多得已经让你厌烦,而且在不结合例子的情况下很难理解,所以这小节我们仅归纳它的特点,到后面例子中,我们再详细看。

    3.1客户激活(Client activated )

    客户激活方式我们实际上已经了解过了,就是在Part.1中我们在单一进程中跨应用程序域传引用封送时的情况,我们再来回顾一下这张图:

    结合这幅图,我们可以看出对于每个客户,创建了其专属的远程对象为其服务(由Part.1的代码可以看出,对象的状态在两次方法调用中也是维持着的)。除此以外,一个代理可以为多个客户对象服务。

    客户激活模式的缺点就是 如果客户端过多时,或者服务对象为“大对象”时,服务器端的压力过大。另外,客户程序可能只需要调用服务对象的一个方法,但是却持有服务对象过长时间,这样浪费了服务器的资源。

    3.2 服务激活 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的一些基本操作和步骤,并对前面的知识加深一下理解。

    1.服务程序集

    我们首先创建服务程序集,它即为向客户程序提供服务的远程对象的实现代码。先创建一个类库项目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()方法用于测试获取远程对象的返回值。

    2.宿主应用程序

    接下来我们新创建一个空解决方案ServerSide,在其下添加一个新的控制台项目ServerConsole,然后再将上面创建的项目ServerAssembly添加进来。除此以外,还需要添加System.Runtime.Remoting的引用,它一般位于C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Runtime.Remoting.dll(修改系统目录)。

    2.1 注册通道

    实现宿主应用程序的第一步就是注册通道。通道是实现了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 ,应该可以看到一共监听了三个端口。

    2.2 注册对象

    注册通道之后,我们需要告诉.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。

    2.3 对象位置

    经过上面两步,我们已经开启了通道,并注册了对象(告诉了.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

    3.客户应用程序

    我们现在再创建一个空解决方案ClientSide,然后在其下添加一个控制台应用程序ClientConsole,因为客户端需要ServerAssembly.DemoClass的元信息来创建代理,所以我们仍要添加对ServerAssembly项目的引用。除此以外,我们依然要添加 System.Runtime.Remoting程序集。

    客户应用程序的任务只有一个:获取远程对象,调用远程对象服务。记得客户应用程序实际上获得的只是一个代理,只是感觉上和远程对象一模一样。客户端获得对象有大致下面几种情况:

    3.1使用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)。

    3.2 其它创建远程对象的方法

    当我们在客户端对远程对象进行注册之后,可以直接使用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。

    4.程序运行测试

    Remoting 最让初学者感到困惑的一个方面就是 客户激活 与 服务激活 有什么不同,什么时候应该使用那种方式。说明它们之间的不同的最好方式就是通过下面几个范例来说明,现在我们来将上面的服务端方法、客户端方法分别进行一下组装,然后进行一下测试(注意在运行客户端之前必须保证服务端已经运行):

    4.1 客户激活方式

    先看下客户激活方式,服务端的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());
    }

    程序运行的结果如下:

    其中第一幅图是服务端,第二幅图是客户端,我们起码可以得出下面几个结论:

    1. 不管是对象的创建,还是对象方法的执行,都在服务端(远程)执行。
    2. 服务端为每一个客户端(两次RunTest()调用,各创建了一个对象)创建其专属的对象,为这个客户提供服务,并且保存状态(第二次调用ShowCount()的值基于第一次调用ShowCount()之后count的值)。
    3. 可以从远程获取到方法执行的返回值。(客户端从GetCount()方法获得了返回值)

    上面的第3点看起来好像是理所当然的,如果是调用本地对象的方法,那么确实是显而易见的。但是对于远程来说,就存在一个很大的问题:远程对象如何知道是谁在调用它?方法执行完毕,将返回值发送给哪个客户呢?此时可以回顾一下第一篇所提到的,客户端在创建远程对象时,已经将自己的位置通过消息发送给了远程。

    最后我们再进行一个深入测试,追踪对象是在调用new时创建,还是在方法调用时创建。将RunTest()只保留一行代码:

    private static void RunTest(string firstName, string familyName) {
        DemoClass obj = new DemoClass();    // 创建对象
    }

    然后再次运行程序,得到的输出分别如下:

    // 服务端
    方式: Client Activated Object
    服务端开启,按任意键退出...
    ======= DomoClass Constructor =======
    ======= DomoClass Constructor =======

    // 客户端
    客户端运行结束,按任意键退出...

    由此可以得出结论:使用客户激活方式时,远程对象在调用new操作时创建。

    4.2 服务激活方式 -- 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操作符,客户端也无法创建一个对象,而只有在对象上第一次调用方法时才会创建。仔细考虑一下这个和上面的结论是类似的,只是更深入了一步。

    4.3 服务激活方式 -- 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),作为工厂方法的返回值。这个例子我就不再演示了,相信看过上面的示例,您已经明白了。

  • 相关阅读:
    LeetCode 83. Remove Duplicates from Sorted List (从有序链表中去除重复项)
    LeetCode 21. Merge Two Sorted Lists (合并两个有序链表)
    LeetCode 720. Longest Word in Dictionary (字典里最长的单词)
    LeetCode 690. Employee Importance (职员的重要值)
    LeetCode 645. Set Mismatch (集合不匹配)
    LeetCode 500. Keyboard Row (键盘行)
    LeetCode 463. Island Perimeter (岛的周长)
    115.Distinct Subsequences
    55.Jump Game
    124.Binary Tree Maximum Path Sum
  • 原文地址:https://www.cnblogs.com/suncms/p/2497376.html
Copyright © 2011-2022 走看看