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

2008-8-22 分类: .Net 框架

Remoting中的方法回调

远程回调方式说明

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

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

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

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

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

客户端和服务端对象

服务端对象

由于本文讨论的主要是回调,所以我们创建新的服务对象和客户对象来进行演示。下面是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#中的委托和事件 - Part.2一文。

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

客户端对象

接下来我们看下客户端的代码,它没有什么特别,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属性,它在输出前进行了一次自增,等下运行时我们会重新看这里。

服务端、客户端会话模型

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

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

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

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

宿主应用程序

服务端宿主应用程序

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

在这里我喜欢将解决方案和项目起不同的名称,比如解决方案我起名为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(); } }

客户端宿主应用程序

与服务端类似,我们创建解决方案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()方法。最后,我们暂停了客户端,为什么这里暂停,而不是直接结束,我们下面运行时再解释。

程序运行测试

运行一个客户端

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

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

运行多个客户端

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

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

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

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

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

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

感谢阅读,希望这篇文章能给你带来帮助!