zoukankan      html  css  js  c++  java
  • .Net Remoting(远程方法回调) Part.4

    .Net Remoting(远程方法回调) - Part.4

    Remoting中的方法回调

    1. 远程回调方式说明

    远程方法回调通常有两种方式:

    • 客户端也存在继承自MarshalByValueObject的类型,并将该类型的实例作为参数传递给了远程对象的方法,然后远程对象在其方法中通过该类型实例的引用对它进行调用(访问其属性或者方法)。记得继承自MarshalByValueObject的类型实例永远不会离开自己的应用程序域,所以相当于服务端对象调用了客户端对象。
    • 客户端对象注册了远程对象发布的事件,远程对象通过委托调用客户端注册了的方法。

    当服务端调用客户端的方法时,它们的角色就互换了。此时,需要注意这样几个问题:

    1. 因为不能通过对象引用访问静态方法(属性),所以无法对静态方法(属性)进行回调。
    2. 由于服务端在运行时需要访问客户端对象,此时它们的角色互换,需要在服务端创建对客户端对象的代理,所以服务端也需要客户端对象的类型元数据。因此,最好将客户端需要回调的方法,抽象在一个对象中,服务端只需引用含有这个对象的程序集就可以了。而如果直接写在Program中,服务端还需要引用整个客户端。
    3. 由于将客户端进行回调的逻辑抽象成为了一个独立的对象,此时客户端的构成就类似于前面所讲述的服务端。它包含两部分:(1)客户端对象,用于支持服务端的方法回调,以及其它的业务逻辑;(2)客户端控制台应用程序(也可以是其它类型程序),它仅仅是注册通道、注册端口、注册远程对象,提供一个客户端对象的运行环境。

    根据这三点的变化,我们可以看出:客户端含有客户端对象,但它还需要远程服务对象的元数据来构建代理;服务端含有服务对象,但它还需要客户端对象的元数据来构建代理。因此,客户端服务端均需要服务对象、客户对象的类型元数据,简单起见,我们将它们写在同一个程序集中,命名为ShareAssembly,供客户端、服务端引用。此时,运行时的状态图如下所示:

    其中ShareAssembly.dll包含服务对象和客户端对象的代码。接下来一节我们来看一下它们的代码。

    2.客户端和服务端对象

    2.1服务端对象

    由于本文讨论的主要是回调,所以我们创建新的服务对象和客户对象来进行演示。下面是ShareAssembly程序集包含的代码,我们先看一下服务端对象和委托的定义:

    public delegate void NumberChangedEventHandler(string name, int count);

    public class Server :MarshalByRefObject {
        private int count = 0;
        private string serverName = "SimpleServer";

        public event NumberChangedEventHandler NumberChanged;

        // 触发事件,调用客户端方法
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void DoSomething() {
            // 做某些额外方法
            count++;
            if (NumberChanged != null) {
                Delegate[] delArray = NumberChanged.GetInvocationList();
                foreach (Delegate del in delArray) {
                    NumberChangedEventHandler method = (NumberChangedEventHandler)del;
                    try {
                        method(serverName, count);
                    } catch {
                        Delegate.Remove(NumberChanged, del);//取消某一客户端的订阅
                    }
                }              
            }
        }

        // 直接调用客户端方法
        public void InvokeClient(Client remoteClient, int x, int y) {
            int total = remoteClient.Add(x, y); //方法回调
            Console.WriteLine(
                "Invoke client method: x={0}, y={1}, total={2}",x, y, total);
        }

        // 调用客户端属性
        public void GetCount(Client remoteClient) {
            Console.WriteLine("Count value from client: {0}", remoteClient.Count);
        }
    }

    在这段代码中首先定义了一个委托,并在服务对象Server中声明了一个该委托类型的事件,它可以用于客户对象注册。它主要包含三个方法:DoSomething()、InvokeClient()和GetCount()。需要注意的是DoSomething()方法,因为我后面将服务端实现为了Singleton模式,所以需要处理并发访问,我使用了一种简便的方法,向方法添加MethodImp特性,它会自动实施方法的线程安全。其次就是在方法中触发事件时,我采用了遍历委托链表的方式,并放在了try/catch块中,因为触发事件时客户端有可能已经不存在了。另外,如果发生异常,我将它从订阅的委托列表中删除掉,这样下次触发时就不会再次调用它了。这里也可以采用BeginInvoke()进行异步调用,具体可以参见C#中的委托和事件(续)一文。

    InvokeClient()方法调用了客户端的Add()方法,并向控制台输出了提示性的说明;GetCount()方法获取了客户端Count的值,并产生了输出。注意这三个方法均由客户端调用,但是方法内部又回调了调用它们的客户对象。

    2.2客户端对象

    接下来我们看下客户端的代码,它没有什么特别,OnNumberChanged()方法在事件触发时自动调用,而其余两个方法由服务对象进行回调,并在调用它时,在客户端控制台输出相应的提示:

    public class Client : MarshalByRefObject {
        private int count = 0;
       
        // 方式1:供远程对象调用
        public int Add(int x, int y) {
            // 当有服务端调用时,打印下面一行
            Console.WriteLine("Add callback: x={0}, y={1}.", x, y);
            return x + y;
        }

        // 方式1:供远程对象调用
        public int Count {
            get {
                count++;
                return count;
            }
        }

        // 方式2:订阅事件,供远程对象调用
        public void OnNumberChanged(string serverName, int count){
            Console.WriteLine("OnNumberChanged callback:");
            Console.WriteLine("ServerName={0}, Server.Count={1}", serverName, count);
        }
    }

    注意一下Count属性,它在输出前进行了一次自增,等下运行时我们会重新看这里。

    3.服务端、客户端会话模型

    当客户对象调用服务对象方法时,服务端已经注册了通道、开放了端口,对请求进行监听。同理,当服务端回调客户端对象时,客户端也需要注册通道、打开端口。但现在问题是:服务端如何知道客户端使用了哪个端口?我们在Part.1中提到过,当对象进行传引用封送时,会包含对象的位置,而有了这个位置,再加上类型的元数据便可以创建代理,代理总是知道远程对象的地址,并将请求发送给远程对象。这种会话模型可以用下面的图来表述:

    从上面这幅图可以很清楚地看到服务端代理的创建过程:首先在第1阶段,客户端服务端谁也不知道谁在哪儿;因此,在第2阶段,我们首先要为客户端提供服务端对象的地址和类型元数据,有了这两样东西,客户端便可以创建服务端的代理,然后通过代理就访问到服务端对象;第3阶段是最关键的一步,在客户端通过代理调用InvokeClient()时,将client对象以传引用封送的方式传递了过去,我们前面说过,在传引用封送时,它还包括了这个对象的位置,也就是client对象的位置和端口号;第4步时,服务端根据客户端位置和类型元数据创建了客户端对象的代理,并通过代理调用了客户端的Add()方法。

    NOTE:图中的代理实际应该分别指向client或者server,由于绘图的空间问题,我就直接指在框框上了。

    因此,客户端应用程序与之前相比一个最大的区别就是需要注册通道,除此以外,它并不需要明确地指定一个端口号,可以由.NET自动选择一个端口号,而服务端则会通过客户端代理知道其使用的是哪个端口号。

    4.宿主应用程序

    4.1服务端宿主应用程序

    现在我们来看一下服务端宿主应用程序的实现。简单起见,我们依然创建一个控制台应用程序ServerConsole,然后在解决方案下添加前面创建的ShareAssembly项目,然后在ServerConsole中引用ShareAssembly。

    NOTE:在这里我喜欢将解决方案和项目起不同的名称,比如解决方案我起名为ServerSide(服务端),服务端控制台应用程序则叫ServerConsole。这样感觉更清晰一些。

    服务端控制台应用程序的代码和前面的类似,还是老一套的注册通道,注册对象,需要注意的是这里采用了自定义formatter的方式,并设置了它的TypeFilterLevel属性为TypeFilterLevel.Full,它默认为Low,但是当设为Low时一些复杂的类型将无法进行Remoting(主要是出于安全性的考虑)。

    // using... 略
    class Program {
        static void Main(string[] args) {

            // 设置Remoting应用程序名
            RemotingConfiguration.ApplicationName = "CallbackRemoting";

            // 设置formatter
            BinaryServerFormatterSinkProvider formatter;
            formatter = new BinaryServerFormatterSinkProvider();
            formatter.TypeFilterLevel = TypeFilterLevel.Full;

            // 设置通道名称和端口
            IDictionary propertyDic = new Hashtable();
            propertyDic["name"] = "CustomTcpChannel";
            propertyDic["port"] = 8502;

            // 注册通道
            IChannel tcpChnl = new TcpChannel(propertyDic, null, formatter);
            ChannelServices.RegisterChannel(tcpChnl, false);

            // 注册类型
            Type t = typeof(Server);
            RemotingConfiguration.RegisterWellKnownServiceType(
                t, "ServerActivated", WellKnownObjectMode.Singleton);

            Console.WriteLine("Server running, model: Singleton\n");
            Console.ReadKey();
        }
    }

    4.2客户端宿主应用程序

    与服务端类似,我们创建解决方案ClientSide,在其下添加ClientConsole控制台项目,添加现有的ShareAssembly项目,并在ClientConsole项目下添加对ShareAssembly的引用。

    //using... 略
    class Program {
        static void Main(string[] args) {

            // 注册通道
            IChannel chnl = new TcpChannel(0);
            ChannelServices.RegisterChannel(chnl, false);

            // 注册类型
            Type t = typeof(Server);
            string url = "tcp://127.0.0.1:8502/CallbackRemoting/ServerActivated";
            RemotingConfiguration.RegisterWellKnownClientType(t, url);

            Server remoteServer = new Server(); // 创建远程对象
            Client localClient = new Client();  // 创建本地对象

            // 注册远程对象事件
            remoteServer.NumberChanged +=
                new NumberChangedEventHandler(localClient.OnNumberChanged);

            remoteServer.DoSomething();             // 触发事件
            remoteServer.GetCount(localClient);     // 调用GetCount()
            remoteServer.InvokeClient(localClient, 2, 5);// 调用InvokeClient()

            Console.ReadKey();  // 暂停客户端
        }
    }

    我们看一下上面的代码,它仅仅是多了一个通道注册,注意我们将端口号设置为0,意思是由.NET选择一个可用端口。由于注册了远程类型,所以我们直接使用new操作创建了一个Server对象。然后,我们创建了一个本地的Client对象,注册了NumberChanged事件、触发事件、调用了GetCount()方法和InvokeClient()方法。最后,我们暂停了客户端,为什么这里暂停,而不是直接结束,我们下面运行时再解释。

    5.程序运行测试

    5.1运行一个客户端

    我们运行先服务端,接着运行一个客户端,此时产生的输出如下:

    上面是服务端,下面是客户端。我们在调用server.DoSomething()方法时,触发了事件,所以调用了客户端的OnNumberChanged,产生了客户端的前两行输出;调用GetCount()时,客户端没有产生输出,服务端输出了“Count value from client:1”;调用InvokeClient()时,客户端和服务端分别产生了相应的输出。

    5.2运行多个客户端

    接下来,我们不要关闭上面的窗口,再次打开一个客户端。此时程序的运行结果如下所示,其中第1幅图是服务端、第2幅图是第一个客户端、第3幅图是新开启的客户端:

    这里可以发现两点:由于第二个客户端再次调用了DoSomething()方法,所以它再次触发了事件,因此在第一个客户端再次产生了输出“OnNumberChanged Callback...”;再次调用GetCount()方法时,对于服务端来说,是一个新建的客户端localClient对象,所以count值继续输出为1,也就是说两个客户端对象是独立的,对服务器来说,可以将客户端视为客户激活方式(Client-Actived Model)。

    5.3 关闭第一个客户端,再新建一个客户端

    这种情况主要用来测试当服务端触发事件时,之前订阅了事件的客户端已经不存在了的情况。由于我们已经在服务端对象中进行了异常处理,可以看到不会出现任何错误,程序会按照预期的执行。

    这里还有另外一种方式,就是将客户端的回调方法使用OneWay特性进行标记,然后服务端对象触发事件时直接使用NumberChanged委托变量。当客户端方法用OneWay标记后,.NET会自动实施异步调用,并且在客户端产生异常时也不会影响到服务端的运行。

    5.3的例子就不演示了,感兴趣可以自己试一下。

  • 相关阅读:
    解释机器学习模型的一些方法(一)——数据可视化
    机器学习模型解释工具-Lime
    Hive SQL 语法学习与实践
    LeetCode 198. 打家劫舍(House Robber)LeetCode 213. 打家劫舍 II(House Robber II)
    LeetCode 148. 排序链表(Sort List)
    LeetCode 18. 四数之和(4Sum)
    LeetCode 12. 整数转罗马数字(Integer to Roman)
    LeetCode 31. 下一个排列(Next Permutation)
    LeetCode 168. Excel表列名称(Excel Sheet Column Title)
    论FPGA建模,与面向对象编程的相似性
  • 原文地址:https://www.cnblogs.com/JimmyZhang/p/1280146.html
Copyright © 2011-2022 走看看