Remoting中的方法回调

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

1. 远程回调方式说明

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

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

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

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

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

Remoting中的方法回调_第1张图片

其中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中提到过,当对象进行传引用封送时,会包含对象的位置,而有了这个位置,再加上类型的元数据便可以创建代理,代理总是知道远程对象的地址,并将请求发送给远程对象。这种会话模型可以用下面的图来表述:

Remoting中的方法回调_第2张图片

从上面这幅图可以很清楚地看到服务端代理的创建过程:首先在第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运行一个客户端

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

Remoting中的方法回调_第3张图片

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

5.2运行多个客户端

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

Remoting中的方法回调_第4张图片

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

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

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

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

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

你可能感兴趣的:(C#)