简介:

简单地说,.NET Remoting是指利用运行库的服务调用运行在不同应用程序域中的对象的方法并共享其数据。也可以这样理解它:一种分布式处理方式,也可将它看作是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。.NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。
      在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。
      在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

应用程序域

这个例子可以非常好地说明应用程序域的概念。在学习分布式程序设计时,首先要理解什么是应用程序域,.NET Remoting 就是利用运行库的服务在调用运行在不同的应用程序域中的对象的方法,并共享其数据。

 

构造两个应用程序域:

1、新建一个控制台应用程序,项目名称为:RemotingTest,vs.net 2003 会在 RemotingTest 解决方案下生成一个 RemotingTest项目。这是我们的一个应用程序域。在这个应用程序内,显示该应用程序域的各种信息,然后,将调用AppDomain.CreateDomain()进入另一个应用程序域。

2、下面,我们来构建另一个应用程序域,该应用程序引用一个简单计算的类库 MathLibrary ,右键单击解决方案,添加——新建项目——类库,数据类库的名字为 MathLibrary,完成。将Class1改名为 SimpelMath,代码如下:

using System;

 

namespace MathLibrary

{

     /// <summary>

     /// Class1 的摘要说明。

     /// </summary>

     public class SimpleMath

     {

         public static int Add(int n1,int n2)

         {

              return n1 + n2;

         }

         public static int Subtract(int n1,int n2)

         {

              return n1 - n2;

         }

     }

}

选择“生成”菜单下面的生成 MathLibrary,完成了对类库的编译。右键单击解决方案,添加——新建项目——控制台应用程序,命名为 MathClient,代码如下:

using System;

 

using MathLibrary;

 

namespace MathClient

{

     /// <summary>

     /// MathClient 的摘要说明。

     /// </summary>

     public class MathClient

     {

         static voidMain(string[] args)

         {

              Console.WriteLine();

 

              AppDomain myDomain = AppDomain.CurrentDomain;

              Console.WriteLine("Info about our current app domain");

              Console.WriteLine("Hash Code = {0}",myDomain.GetHashCode());

              Console.WriteLine("Friendly Name = {0}",myDomain.FriendlyName);

              Console.WriteLine("App Base = {0}",myDomain.BaseDirectory);

              Console.WriteLine();

 

              Console.WriteLine(" 5 + 3 = {0} ",SimpleMath.Add(5,3));

              Console.WriteLine(" 5 - 3 = {0} ",SimpleMath.Subtract(5,3));

 

              Console.ReadLine();

         }

     }

}

注意:一定要添加对 MathLibrary 类库的引用,这样,一个使用类库的应用程序域就建立好了。

 

3、修改RemotingTest项目下的 Class1.cs 为 RemotingTest.cs,修改后代码如下:

 

using System;

 

namespace RemotingTest

{

     /// <summary>

     /// Class1 的摘要说明。

     /// </summary>

     class RemotingTest

     {

         /// <summary>

         /// 应用程序的主入口点。

         /// </summary>

         [STAThread]

         static voidMain(string[] args)

         {

              AppDomain myDomain = AppDomain.CurrentDomain;

              Console.WriteLine("Info about our current app domain");

              Console.WriteLine("Hash Code = {0}",myDomain.GetHashCode());

              Console.WriteLine("Friendly Name = {0}",myDomain.FriendlyName);

              Console.WriteLine("App Base = {0}",myDomain.BaseDirectory);

              Console.WriteLine("Probing paths = {0}",myDomain.RelativeSearchPath);

 

              AppDomain mathDomain = AppDomain.CreateDomain("MathClient");

              mathDomain.ExecuteAssembly(@"E:\Csharp\MathClient\bin\Debug\MathClient.exe");

         }

     }

 

 

}

     注意:一定要注意应用程序域的路径是正确的。

应用程序边界

除了应用程序域外,.NET 提供了另一种类型的应用程序边界——上下文。应用程序可以包含许多上下文,而且至少包含一个默认的上下文。
   上下文提供了:
1、一个由一组属性组成的环境,驻留在同一上下文中的所有对象可以共享该环境。
2、一个拦截边界,由运行库对所有来自上下文外的方法调用进行预处理和事后处理。
3、可以保存具有类似运行库要求的对象,如同步、线程亲缘性或即时激活。

---------------------------------------------------------------------------------------------------
Context Agile对象:当运行库创建对象时,会调查对象的上下文请求,以将它放到合适的上下文中。如果没有合适的上下文,运行时就会创建一个上下文,即默认上下文。大多数对象都会建立在默认上下文中,这些对象称为Context Agile对象,因为可以直接从应用程序域内的任何地方直接访问它们。
Context Bound对象:确实具有上下文要求的对象称之,它们必须要从 ContextBoundObject 类中派生而来。对该类对象的跨上下文访问,由模拟实际对象的由运行库生成的代理来提供。代理允许运行库截取跨上下文调用并可以应用任何预处理和事后处理。(会引起系统开销,慎重使用)

例子:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Contexts;

namespace ContextTest
{
 /// <summary>
 /// Class1 的摘要说明。
 /// </summary>
 [Synchronization]
 public class MyContextBoundClass: ContextBoundObject
 {
 }

 public class MyAgilClass
 {
 }

 class ContextTest
 {
  static void Main(string[] args)
  {
   MyContextBoundClass myBound = new MyContextBoundClass();
   MyAgilClass myAgile = new MyAgilClass();

   Console.WriteLine("Is myBound out of context?

{0}",RemotingServices.IsObjectOutOfContext(myBound));
   Console.WriteLine("Is myAgile out of context?

{0}",RemotingServices.IsObjectOutOfContext(myAgile));

   Console.WriteLine("Is myBound a proxy ?

{0}",RemotingServices.IsTransparentProxy(myBound));
   Console.WriteLine("Is myAgile a proxy ?

{0}",RemotingServices.IsTransparentProxy(myAgile));

   Console.ReadLine();
  }
 }

}

运行结果:
Is myBound out of context? true
Is myAgile out of context? false
Is myBound a proxy ?  true
Is myAgile a proxy ?  false

    利用 Thread.CurrentContext 静态方法,您可以获取对当前正在执行的上下文的引用。所返回的 Context 对象可用来查找与上下文相关的各种信息。

将上个例子稍做修改:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Contexts;
using System.Threading;

namespace ContextTest
{
 /// <summary>
 /// Class1 的摘要说明。
 /// </summary>
 [Synchronization]
 public class MyContextBoundClass: ContextBoundObject
 {
  public MyContextBoundClass()
  {
   Console.WriteLine("in MyContextBoundClass Constructor");
   Diagnostics.DisplayContextInfo();
  }
 }

 public class MyAgilClass
 {
 }

 public class Diagnostics
 {
  public static void DisplayContextInfo()
  {
   Context ctx = Thread.CurrentContext;
   Console.WriteLine("Properties for context id:{0}", ctx.ContextID);
   foreach(IContextProperty ctxProp in ctx.ContextProperties)
   {
    Console.WriteLine("{0}",ctxProp.Name);
   }
  }
 }

 class ContextTest
 {
  static void Main(string[] args)
  {
//   MyContextBoundClass myBound = new MyContextBoundClass();
//   MyAgilClass myAgile = new MyAgilClass();
//
//   Console.WriteLine("Is myBound out of context? {0}",RemotingServices.IsObjectOutOfContext(myBound));
//   Console.WriteLine("Is myAgile out of context? {0}",RemotingServices.IsObjectOutOfContext(myAgile));
//
//   Console.WriteLine("Is myBound a proxy ? {0}",RemotingServices.IsTransparentProxy(myBound));
//   Console.WriteLine("Is myAgile a proxy ? {0}",RemotingServices.IsTransparentProxy(myAgile));
//
//   Console.ReadLine();
   Console.WriteLine("In Main");
   Diagnostics.DisplayContextInfo();
   MyContextBoundClass myBound = new MyContextBoundClass();

   Console.ReadLine();

  } 
 }
}

其他上下文中访问 Context Agile 对象

该例中,上下文中执行的 Main() 方法调用了 MyAgileClass 对象的 DisplayContextInfo 方法。还将一个 MyAgileClass 类型的对象 myAgile 传递给 MyContextBoundClass 的构造函数。在上下文1中执行的构造函数,先执行默认构造后,调用 myAgile 的 DisplayContextInfo 方法。通过输出结果可以发现:MyAgileClass 类型的对象可以在上下文间自由地移动。即:上下文 id 分别为 0 和 1。

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Contexts;
using System.Threading;

namespace ContextTest
{
 /// <summary>
 /// MyContextBoundClass 的摘要说明。
 /// </summary>
 [Synchronization]
 public class MyContextBoundClass: ContextBoundObject
 {
  public MyContextBoundClass()
  {
   Console.WriteLine("in MyContextBoundClass Constructor");
   Diagnostics.DisplayContextInfo();  //static method by className
  }

  //:this() means call the default constructor before execute other codes
  public MyContextBoundClass(MyAgileClass myAgile):this()
  {   
   myAgile.DisplayContextInfo();//myAgile should be in the context bound's context for it is an Agile object
  }
 }

 public class MyAgileClass
 {
  public void DisplayContextInfo()
  {
   Console.WriteLine("MyAgileClass.DisplayContextInfo()");
   Diagnostics.DisplayContextInfo();
  }
 }

 public class Diagnostics
 {
  public static void DisplayContextInfo()
  {
   Context ctx = Thread.CurrentContext;
   Console.WriteLine("Properties for context id:{0}", ctx.ContextID);
   foreach(IContextProperty ctxProp in ctx.ContextProperties)
   {
    Console.WriteLine("{0}",ctxProp.Name);
   }
  }
 }

 class ContextTest
 {
  static void Main(string[] args)
  {


   MyAgileClass myAgile = new MyAgileClass();
   myAgile.DisplayContextInfo();

   MyContextBoundClass myBound = new MyContextBoundClass(myAgile);
   Console.ReadLine();
  }  
 }
}

初识用.NET Remoting来开发分布式应用

一..NET Remoting简介:

.NET Remoting从某种意义上讲是DCOM的替代品。ASP.NET Web服务十分有用,但是这项技术在企业内联网的解决方案中,对于某些业务请求来说并不快,也没有足够的灵活性,而且,ASP.NET Web服务需要有运行时的支持。使用.NET Remoting技术后,可以将Web服务提供给世界上的任何地方。而且可以在所有的应用程序类型中运行Web服务。

二..NET Remoting 的基本原理:

体系结构图如下:

 

三.几个重要的概念:

1.远程对象:

远程对象类是从MarshalByRefObject类中派生的。跨越应用程序域调用这个类需要使用代理。.NET Remoting支持两种类型的远程对象:知名的(Well-known)远程对象和客户激活(Client-activated)远程对象。远程对象其实包括两层含义:

操作远程对象:对象运行在远程,客户段向他发送消息;

传递远程对象:将远程对象拿到本地,或者将本地对象发送过去,对副本进行操作。

2.激活:

使用new运算符可以激活远程对象。还有其它一些方式也可以激活远程对象,在以后的随笔里面我会介绍。

3.通道:

一个远程对象使用通道发送和接收消息。服务器选择一个通道来监听请求,客户端选择通道来和服务器通讯。Remoting提供了内置的通道:TCP通道和HTTP通道,我们也可以编写自己的通道。

4.编组:

数组通过应用程序域被传递的过程称为编组。将变量作为远程对象的参数来发送时,这个变量必须被转换,以便能够通过应用程序域发送该变量。

5.监听:

使用监听,能够将某些功能置入到方法调用链中。如果调用某个对象的方法,监听层便能够捕获调用来转换方法调用,或是完成某些日志记录。.NET Remoting调用链的每一部分都是用监听。

四.开发Remoting三步走:

开发.NET Remoting分三步走,在这里以一个简单的例子来说明。

1.创建远程对象:

继承System.MarshalByRefObject

 1 using System;
 2 using System.Collections;
 3 using System.Text;
 4
 5 namespace SimpleRemoting
 6 {
 7     public class HelloServer : MarshalByRefObject
 8      {
 9         public HelloServer()
10          {
11             /**////输出信息,服务器激活
12             Console.WriteLine("服务器激活……");
13         }
14         public String HelloMethod(String name)
15          {
16             Console.WriteLine(
17                 "服务器端 : {0}", name);
18             return "这里是:" + name;
19         }
20     }
21 }


2.创建宿主应用程序:

注册通道

注册服务器激活的远程对象

运行宿主程序

 1 using System;
 2 using System.Net;
 3 using System.Runtime.Remoting;
 4 using System.Runtime.Remoting.Channels;
 5 using System.Runtime.Remoting.Channels.Tcp;
 6 using System.Runtime.Remoting.Channels.Http;
 7
 8 namespace SimpleRemoting 
 9 {
10
11     public class Server
12      {
13         public static int Main(string [] args) 
14          {
15             
16             /**////创建Tcp通道
17             TcpChannel chan1 = new TcpChannel(8085);
18
19             /**////创建Http通道
20             HttpChannel chan2 = new HttpChannel(8086);
21             
22             /**////注册通道
23             ChannelServices.RegisterChannel(chan1);
24             ChannelServices.RegisterChannel(chan2);
25
26             RemotingConfiguration.RegisterWellKnownServiceType
27                 (
28                 typeof(HelloServer),
29                 "SayHello",
30                 WellKnownObjectMode.Singleton
31                 );
32             
33
34             System.Console.WriteLine("按任意键退出!");
35             /**////下面这行不能少
36             System.Console.ReadLine();
37             return 0;
38         }
39
40     }
41 }
42
43


3.建立客户端程序:

注册通道

根据URL得到对象代理

使用代理调用远程对象

 1 using System;
 2 using System.Runtime.Remoting;
 3 using System.Runtime.Remoting.Channels;
 4 using System.Runtime.Remoting.Channels.Tcp;
 5 using System.Runtime.Remoting.Channels.Http;
 6 using System.IO;
 7
 8 namespace SimpleRemoting 
 9 {
10     public class Client
11      {
12         public static void Main(string[] args)
13          {
14             /**////使用TCP通道得到远程对象
15             TcpChannel chan1 = new TcpChannel();
16             ChannelServices.RegisterChannel(chan1);
17
18             HelloServer obj1 = (HelloServer)Activator.GetObject(
19                 typeof(SimpleRemoting.HelloServer),
20                 "tcp://localhost:8085/SayHello");
21
22             if (obj1 == null)
23              {
24                 System.Console.WriteLine(
25                     "连接TCP服务器失败");
26             }
27
28             /**////使用HTTP通道得到远程对象
29             HttpChannel chan2 = new HttpChannel();
30             ChannelServices.RegisterChannel(chan2);
31
32             HelloServer obj2 = (HelloServer)Activator.GetObject(
33                 typeof(SimpleRemoting.HelloServer),
34                 "http://localhost:8086/SayHello");
35
36             if (obj2 == null)
37              {
38                 System.Console.WriteLine(
39                     "连接HTTP服务器失败");
40             }
41             
42             /**////输出信息
43             Console.WriteLine(
44                 "ClientTCP HelloMethod {0}",
45                 obj1.HelloMethod("Caveman1"));
46             Console.WriteLine(
47                 "ClientHTTP HelloMethod {0}",
48                 obj2.HelloMethod("Caveman2"));
49             Console.ReadLine();
50         }
51     }
52 }
53

 

Microsoft .Net Remoting系列专题之一:.Net Remoting基础篇

一、Remoting基础

什么是Remoting,简而言之,我们可以将其看作是一种分布式处理方式。从微软的产品角度来看,可以说Remoting就是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。Microsoft® .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这也正是我们使用Remoting的原因。为什么呢?在Windows操作系统中,是将应用程序分离为单独的进程。这个进程形成了应用程序代码和数据周围的一道边界。如果不采用进程间通信(RPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。

在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。如图所示:

 

首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。

在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

1Remoting的两种通道

Remoting的通道主要有两种:Tcp和Http。在.Net中,System.Runtime.Remoting.Channel中定义了IChannel接口。IChannel接口包括了TcpChannel通道类型和Http通道类型。它们分别对应Remoting通道的这两种类型。

TcpChannel类型放在名字空间System.Runtime.Remoting.Channel.Tcp中。Tcp通道提供了基于Socket的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。TcpChannel类型默认使用二进制格式序列化消息对象,因此它具有更高的传输性能。HttpChannel类型放在名字空间System.Runtime.Remoting.Channel.Http中。它提供了一种使用Http协议,使其能在Internet上穿越防火墙传输序列化消息流。默认情况下,HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。通常在局域网内,我们更多地使用TcpChannel;如果要穿越防火墙,则使用HttpChannel。

2、远程对象的激活方式

在访问远程类型的一个对象实例之前,必须通过一个名为Activation的进程创建它并进行初始化。这种客户端通过通道来创建远程对象,称为对象的激活。在Remoting中,远程对象的激活分为两大类:服务器端激活和客户端激活。

(1) 服务器端激活,又叫做WellKnow方式,很多又翻译为知名对象。为什么称为知名对象激活模式呢?是因为服务器应用程序在激活对象实例之前会在一个众所周知的统一资源标识符(URI)上来发布这个类型。然后该服务器进程会为此类型配置一个WellKnown对象,并根据指定的端口或地址来发布对象。.Net Remoting把服务器端激活又分为SingleTon模式和SingleCall模式两种。

SingleTon模式:此为有状态模式。如果设置为SingleTon激活方式,则Remoting将为所有客户端建立同一个对象实例。当对象处于活动状态时,SingleTon实例会处理所有后来的客户端访问请求,而不管它们是同一个客户端,还是其他客户端。SingleTon实例将在方法调用中一直维持其状态。举例来说,如果一个远程对象有一个累加方法(i=0;++i),被多个客户端(例如两个)调用。如果设置为SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是相同的。如果熟悉Asp.Net的状态管理,我们可以认为它是一种Application状态。

SingleCall模式:SingleCall是一种无状态模式。一旦设置为SingleCall模式,则当客户端调用远程对象的方法时,Remoting会为每一个客户端建立一个远程对象实例,至于对象实例的销毁则是由GC自动管理的。同上一个例子而言,则访问远程对象的两个客户获得的都是1。我们仍然可以借鉴Asp.Net的状态管理,认为它是一种Session状态。

(2) 客户端激活。与WellKnown模式不同,Remoting在激活每个对象实例的时候,会给每个客户端激活的类型指派一个URI。客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。SingleCall模式和客户端激活模式是有区别的:首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时再创建。其次,SingleCall模式激活的对象是无状态的,对象生命期的管理是由GC管理的,而客户端激活的对象则有状态,其生命周期可自定义。其三,两种激活模式在服务器端和客户端实现的方法不一样。尤其是在客户端,SingleCall模式是由GetObject()来激活,它调用对象默认的构造函数。而客户端激活模式,则通过CreateInstance()来激活,它可以传递参数,所以可以调用自定义的构造函数来创建实例。

二、远程对象的定义

前面讲到,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。因此在Remoting中,对于远程对象有一些必须的定义规范要遵循。

由于Remoting传递的对象是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MSDN对MarshalByRefObject的说明是:MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshallByRefObject。

以下是一个远程对象类的定义:
public class ServerObject:MarshalByRefObject
{
        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }
}

这个类只实现了最简单的方法,就是设置一个人的基本信息,并返回一个Person类对象。注意这里返回的Person类。由于这里所传递的Person则是以传值的方式来完成的,而Remoting要求必须是引用的对象,所以必须将Person类序列化。

因此,在Remoting中的远程对象中,如果还要调用或传递某个对象,例如类,或者结构,则该类或结构则必须实现串行化Attribute[SerializableAttribute]:
[Serializable]
 public class Person
 {
        public Person()
        {
           
        }

        private string name;
        private string sex;
        private int age;

        public string Name
        {
            get    {return name;}
            set    {name = value;}
        }

        public string Sex
        {
            get {return sex;}
            set {sex = value;}
        }

        public int Age
        {
            get {return age;}
            set {age = value;}
        }
  }
将该远程对象以类库的方式编译成Dll。这个Dll将分别放在服务器端和客户端,以添加引用。

在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊,我将在本系列之三中介绍。

三、服务器端

根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:

1、注册通道

要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。

注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间:System.Runtime.Remoting.Channel.Tcp。代码如下:
            TcpChannel channel = new TcpChannel(8080);
            ChannelServices.RegisterChannel(channel);

在实例化通道对象时,将端口号作为参数传递。然后再调用静态方法RegisterChannel()来注册该通道对象即可。

2、注册远程对象

注册了通道后,要能激活远程对象,必须在通道中注册该对象。根据激活模式的不同,注册对象的方法也不同。

(1) SingleTon模式

对于WellKnown对象,可以通过静态方法RemotingConfiguration.RegisterWellKnownServiceType()来实现:RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObject),
                "ServiceMessage",WellKnownObjectMode.SingleTon);

(2)SingleCall模式

注册对象的方法基本上和SingleTon模式相同,只需要将枚举参数WellKnownObjectMode改为SingleCall就可以了。RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObject),
                "ServiceMessage",WellKnownObjectMode.SingleCall);

(3)客户端激活模式

对于客户端激活模式,使用的方法又有不同,但区别不大,看了代码就一目了然。
RemotingConfiguration.ApplicationName = "ServiceMessage";
RemotingConfiguration.RegisterActivatedServiceType(
                typeof(ServerRemoteObject.ServerObject));

为什么要在注册对象方法前设置ApplicationName属性呢?其实这个属性就是该对象的URI。对于WellKnown模式,URI是放在RegisterWellKnownServiceType()方法的参数中,当然也可以拿出来专门对ApplicationName属性赋值。而RegisterActivatedServiceType()方法的重载中,没有ApplicationName的参数,所以必须分开。

3、注销通道

如果要关闭Remoting的服务,则需要注销通道,也可以关闭对通道的监听。在Remoting中当我们注册通道的时候,就自动开启了通道的监听。而如果关闭了对通道的监听,则该通道就无法接受客户端的请求,但通道仍然存在,如果你想再一次注册该通道,会抛出异常。

           //获得当前已注册的通道;
            IChannel[] channels = ChannelServices.RegisteredChannels;

            //关闭指定名为MyTcp的通道;
            foreach (IChannel eachChannel in channels)
            {
                if (eachChannel.ChannelName == "MyTcp")
                {
                    TcpChannel tcpChannel = (TcpChannel)eachChannel;

                    //关闭监听;
                    tcpChannel.StopListening(null);

                    //注销通道;
                    ChannelServices.UnregisterChannel(tcpChannel);
                }
            }
代码中,RegisterdChannel属性获得的是当前已注册的通道。在Remoting中,是允许同时注册多个通道的,这一点会在后面说明。

四、客户端

客户端主要做两件事,一是注册通道。这一点从图一就可以看出,Remoting中服务器端和客户端都必须通过通道来传递消息,以获得远程对象。第二步则是获得该远程对象。

1、注册通道:
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel);

注意在客户端实例化通道时,是调用的默认构造函数,即没有传递端口号。事实上,这个端口号是缺一不可的,只不过它的指定被放在后面作为了Uri的一部分。

2、获得远程对象。

与服务器端相同,不同的激活模式决定了客户端的实现方式也将不同。不过这个区别仅仅是WellKnown激活模式和客户端激活模式之间的区别,而对于SingleTon和SingleCall模式,客户端的实现完全相同。

(1) WellKnown激活模式

要获得服务器端的知名远程对象,可通过Activator进程的GetObject()方法来获得:
ServerRemoteObject.ServerObject serverObj = (ServerRemoteObject.ServerObject)Activator.GetObject(
              typeof(ServerRemoteObject.ServerObject), "tcp://localhost:8080/ServiceMessage");

首先以WellKnown模式激活,客户端获得对象的方法是使用GetObject()。其中参数第一个是远程对象的类型。第二个参数就是服务器端的uri。如果是http通道,自然是用http://localhost:8080/ServiceMessage了。因为我是用本地机,所以这里是localhost,你可以用具体的服务器IP地址来代替它。端口必须和服务器端的端口一致。后面则是服务器定义的远程对象服务名,即ApplicationName属性的内容。

(2) 客户端激活模式

如前所述,WellKnown模式在客户端创建对象时,只能调用默认的构造函数,上面的代码就说明了这一点,因为GetObject()方法不能传递构造函数的参数。而客户端激活模式则可以通过自定义的构造函数来创建远程对象。

客户端激活模式有两种方法:
1) 调用RemotingConfiguration的静态方法RegisterActivatedClientType()。这个方法返回值为Void,它只是将远程对象注册在客户端而已。具体的实例化还需要调用对象类的构造函数。
 RemotingConfiguration.RegisterActivatedClientType(               
                typeof(ServerRemoteObject.ServerObject),
                "tcp://localhost:8080/ServiceMessage");
 ServerRemoteObject.ServerObject serverObj = new ServerRemoteObject.ServerObject();

2) 调用进程Activator的CreateInstance()方法。这个方法将创建方法参数指定类型的类对象。它与前面的GetObject()不同的是,它要在客户端调用构造函数,而GetObject()只是获得对象,而创建实例是在服务器端完成的。CreateInstance()方法有很多个重载,我着重说一下其中常用的两个。
a、 public static object CreateInstance(Type type, object[] args, object[] activationAttributes);

参数说明:
type:要创建的对象的类型。
args :与要调用构造函数的参数数量、顺序和类型匹配的参数数组。如果 args 为空数组或空引用(Visual Basic 中为 Nothing),则调用不带任何参数的构造函数(默认构造函数)。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

这里的参数args是一个object[]数组类型。它可以传递要创建对象的构造函数中的参数。从这里其实可以得到一个结论:WellKnown激活模式所传递的远程对象类,只能使用默认的构造函数;而Activated模式则可以用户自定义构造函数。activationAttributes参数在这个方法中通常用来传递服务器的url。
假设我们的远程对象类ServerObject有个构造函数:
            ServerObject(string pName,string pSex,int pAge)
            {
                name = pName;
                sex = pSex;
                age = pAge;
            }

那么实现的代码是:
            object[] attrs = {new UrlAttribute("tcp://localhost:8080/ServiceMessage")};
            object[] objs = new object[3];
            objs[0] = "wayfarer";
            objs[1] = "male";
            objs[2] = 28;
            ServerRemoteObject.ServerObject = Activator.CreateInstance(
                typeof(ServerRemoteObject.ServerObject),objs,attrs);
可以看到,objs[]数组传递的就是构造函数的参数。

b、public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttribute);

参数说明:
assemblyName :将在其中查找名为 typeName 的类型的程序集的名称。如果 assemblyName 为空引用(Visual Basic 中为 Nothing),则搜索正在执行的程序集。
typeName:首选类型的名称。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

参数说明一目了然。注意这个方法返回值为ObjectHandle类型,因此代码与前不同:
            object[] attrs = {new UrlAttribute("tcp://localhost:8080/EchoMessage")};           
            ObjectHandle handle = Activator.CreateInstance("ServerRemoteObject",
                                   "ServerRemoteObject.ServerObject",attrs);
            ServerRemoteObject.ServerObject obj = (ServerRemoteObject.ServerObject)handle.Unwrap();

这个方法实际上是调用的默认构造函数。ObjectHandle.Unwrap()方法是返回被包装的对象。

说明:要使用UrlAttribute,还需要在命名空间中添加:using System.Runtime.Remoting.Activation;

五、Remoting基础的补充

通过上面的描述,基本上已经完成了一个最简单的Remoting程序。这是一个标准的创建Remoting程序的方法,但在实际开发过程中,我们遇到的情况也许千奇百怪,如果只掌握一种所谓的“标准”,就妄想可以“一招鲜、吃遍天”,是不可能的。

1、注册多个通道

在Remoting中,允许同时创建多个通道,即根据不同的端口创建不同的通道。但是,Remoting要求通道的名字必须不同,因为它要用来作为通道的唯一标识符。虽然IChannel有ChannelName属性,但这个属性是只读的。因此前面所述的创建通道的方法无法实现同时注册多个通道的要求。

这个时候,我们必须用到System.Collection中的IDictionary接口:

注册Tcp通道:
IDictionary tcpProp = new Hashtable();
tcpProp["name"] = "tcp9090";
tcpProp["port"] = 9090;
IChannel channel = new TcpChannel(tcpProp,
 new BinaryClientFormatterSinkProvider(),
 new BinaryServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);

注册Http通道:
IDictionary httpProp = new Hashtable();
httpProp["name"] = "http8080";
httpProp["port"] = 8080;
IChannel channel = new HttpChannel(httpProp,
 new SoapClientFormatterSinkProvider(),
 new SoapServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);

在name属性中,定义不同的通道名称就可以了。

2、远程对象元数据相关性

由于服务器端和客户端都要用到远程对象,通常的方式是生成两份完全相同的对象Dll,分别添加引用。不过为了代码的安全性,且降低客户端对远程对象元数据的相关性,我们有必要对这种方式进行改动。即在服务器端实现远程对象,而在客户端则删除这些实现的元数据。

由于激活模式的不同,在客户端创建对象的方法也不同,所以要分离元数据的相关性,也应分为两种情况。

(1) WellKnown激活模式:

通过接口来实现。在服务器端,提供接口和具体类的实现,而在客户端仅提供接口:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

public class ServerObject:MarshalByRefObject,IServerObject
{ ......}
注意:两边生成该对象程序集的名字必须相同,严格地说,是命名空间的名字必须相同。
           
(2) 客户端激活模式:

如前所述,对于客户端激活模式,不管是使用静态方法,还是使用CreateInstance()方法,都必须在客户端调用构造函数实例化对象。所以,在客户端我们提供的远程对象,就不能只提供接口,而没有类的实现。实际上,要做到与远程对象元数据的分离,可以由两种方法供选择:

a、利用WellKnown激活模式模拟客户端激活模式:

方法是利用设计模式中的“抽象工厂”,下面的类图表描述了总体解决方案:

 

我们在服务器端的远程对象中加上抽象工厂的接口和实现类:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

    public interface IServerObjFactory
    {
        IServerObject CreateInstance();       
    }

    public class ServerObject:MarshalByRefObject,IServerObject
    {
        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }       
    }

    public class ServerObjFactory:MarshalByRefObject,IServerObjFactory
    {
        public IServerObject CreateInstance()
        {
            return new ServerObject();
        }
    }

然后再客户端的远程对象中只提供工厂接口和原来的对象接口:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

    public interface IServerObjFactory
    {
        IServerObject CreateInstance();       
    }
我们用WellKnown激活模式注册远程对象,在服务器端:
           //传递对象;
            RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObjFactory),
                "ServiceMessage",WellKnownObjectMode.SingleCall);

注意这里注册的不是ServerObject类对象,而是ServerObjFactory类对象。

客户端:
ServerRemoteObject.IServerObjFactory serverFactory =               
                (ServerRemoteObject.IServerObjFactory) Activator.GetObject(
                typeof(ServerRemoteObject.IServerObjFactory),
                "tcp://localhost:8080/ServiceMessage");

ServerRemoteObject.IServerObject serverObj = serverFactory.CreateInstance();

为什么说这是一种客户端激活模式的模拟呢?从激活的方法来看,我们是使用了SingleCall模式来激活对象,但此时激活的并非我们要传递的远程对象,而是工厂对象。如果客户端要创建远程对象,还应该通过工厂对象的CreateInstance()方法来获得。而这个方法正是在客户端调用的。因此它的实现方式就等同于客户端激活模式。

b、利用替代类来取代远程对象的元数据

实际上,我们可以用一个trick,来欺骗Remoting。这里所说的替代类就是这个trick了。既然是提供服务,Remoting传递的远程对象其实现的细节当然是放在服务器端。而要在客户端放对象的副本,不过是因为客户端必须调用构造函数,而采取的无奈之举。既然具体的实现是在服务器端,又为了能在客户端实例化,那么在客户端就实现这些好了。至于实现的细节,就不用管了。

如果远程对象有方法,服务器端则提供方法实现,而客户端就提供这个方法就OK了,至于里面的实现,你可以是抛出一个异常,或者return 一个null值;如果方法返回void,那么里面可以是空。关键是这个客户端类对象要有这个方法。这个方法的实现,其实和方法的声明差不多,所以我说是一个trick。方法如是,构造函数也如此。

还是用代码来说明这种“阴谋”,更直观:

服务器端:
    public class ServerObject:MarshalByRefObject
    {
        public ServerObject()
        {
           
        }

        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }       
    }

客户端:
    public class ServerObject:MarshalByRefObject
    {
        public ServerObj()
        {
            throw new System.NotImplementedException();
        }

        public Person GetPersonInfo(string name,string sex,int age)
        {
            throw new System.NotImplementedException();
        }       
    }

比较客户端和服务器端,客户端的方法GetPersonInfo(),没有具体的实现细节,只是抛出了一个异常。或者直接写上语句return null,照样OK。我们称客户端的这个类为远程对象的替代类。

3、利用配置文件实现

前面所述的方法,于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。

(1) 服务器端的配置文件:
&lt;configuration&gt;
  &lt;system.runtime.remoting&gt;
    &lt;application name="ServerRemoting"&gt;
      &lt;service&gt;
        &lt;wellknown mode="Singleton" type="ServerRemoteObject.ServerObject" objectUri="ServiceMessage"/&gt;
      &lt;/service&gt;
      &lt;channels&gt;
         &lt;channel ref="tcp" port="8080"/&gt;
      &lt;/channels&gt;
    &lt;/application&gt;
  &lt;/system.runtime.remoting&gt;
&lt;/configuration&gt;

如果是客户端激活模式,则把wellknown改为activated,同时删除mode属性。

把该配置文件放到服务器程序的应用程序文件夹中,命名为ServerRemoting.config。那么前面的服务器端程序直接用这条语句即可:
RemotingConfiguration.Configure("ServerRemoting.config");

(2) 客户端配置文件

如果是客户端激活模式,修改和上面一样。调用也是使用RemotingConfiguration.Configure()方法来调用存储在客户端的配置文件。

配置文件还可以放在machine.config中。如果客户端程序是web应用程序,则可以放在web.config中。

4、启动/关闭指定远程对象

Remoting中没有提供类似UnregisterWellKnownServiceType()的方法,也即是说,一旦通过注册了远程对象,如果没有关闭通道的话,该对象就一直存在于通道中。只要客户端激活该对象,就会创建对象实例。如果Remoting传送的只有一个远程对象,这不存在问题,关闭通道就可以了。如果传送多个远程对象呢?要关闭指定的远程对象应该怎么做?关闭之后又需要启动又该如何?

我们注意到在Remoting中提供了Marshal()和Disconnect()方法,答案就在这里。Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个对象是存储生成代理以与远程对象通讯所需的所有相关信息。这样就可以将该实例序列化以便在应用程序域之间以及通过网络进行传输,客户端就可以调用了。而Disconnect()方法则将具体的实例对象从通道中断开。

方法如下:
首先注册通道:
TcpChannel channel = new TcpChannel(8080);
ChannelServices.RegisterChannel(channel);

接着启动服务:
先在服务器端实例化远程对象。
ServerObject obj = new ServerObject();

然后,注册该对象。注意这里不用RemotingConfiguration.RegisterWellKnownServiceType(),而是使用RemotingServices.Marshal():

ObjRef objrefWellKnown = RemotingServices.Marshal(obj, "ServiceMessage");

如果要注销对象,则:
RemotingServices.Disconnect(obj);

要注意,这里Disconnect的类对象必须是前面实例化的对象。正因为此,我们可以根据需要创建指定的远程对象,而关闭时,则Disconnect之前实例化的对象。

至于客户端的调用,和前面WellKnown模式的方法相同,仍然是通过Activator.GetObject()来获得。但从实现代码来看,我们会注意到一个问题,由于服务器端是显式的实例化了远程对象,因此不管客户端有多少,是否相同,它们调用的都是同一个远程对象。因此我们将这个方法称为模拟的SingleTon模式。

客户端激活模式

我们也可以通过Marshal()和Disconnect()来模拟客户端激活模式。首先我们来回顾“远程对象元数据相关性”一节,在这一节中,我说到采用设计模式的“抽象工厂”来创建对象实例,以此用SingleCall模式来模拟客户端激活模式。在仔细想想前面的模拟的SingleTon模式。是不是答案就将呼之欲出呢?

在“模拟的SingleTon”模式中,我们是将具体的远程对象实例进行Marshal,以此让客户端获得该对象的引用信息。那么我们换一种思路,当我们用抽象工厂提供接口,工厂类实现创建远程对象的方法。然后我们在服务器端创建工厂类实例。再将这个工厂类实例进行Marshal。而客户端获取对象时,不是获取具体的远程对象,而是获取具体的工厂类对象。然后再调用CreateInstance()方法来创建具体的远程对象实例。此时,对于多个客户端而言,调用的是同一个工厂类对象;然而远程对象是在各个客户端自己创建的,因此对于远程对象而言,则是由客户端激活,创建的是不同对象了。

当我们要启动/关闭指定对象时,只需要用Disconnet()方法来注销工厂类对象就可以了。