这几天看了不少Remoting文章。明白了不少技术细节,但困惑也不少。简单说来,Remoting是一个分布式处理服务。服务器端首先创建通道(Channel),并自动开启监听通道。根据客户端发出的请求,传递远程对象。
因此,编写Remoting程序,主要分为三部分:
1、被传递的远程对象;
2、服务器端监听程序;
3、客户端请求和处理对象程序;
一、被传递的远程对象
在Remoting中,被传递的远程对象类是有诸多限制的。首先,我们必须清楚,这里所谓的传递是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承 MarshallByRefObject。(MSDN)
{
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要求对象类还要调用或传递某个对象,例如类,或者结构,则该类或结构则必须实现串行化Attribute。[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分别添加到服务器端和客户端程序的引用中。也就是说,这个服务器对象Dll要拷贝两份,一份放在服务器端,一份放在客户端。为什么要这样?看了后面的代码就知道原因了。
如此一来,会有个问题存在。那就是代码的安全性。如果客户端必须要保持这个对象的Dll,则该对象的实现方式对于客户而言就近乎透明了。另外,这样对于部署也不好。两份同样的dll,如果传递的对象大,也会影响性能的。对于这个问题,我们可以使用接口来解决。显然,服务器端提供接口和具体类的实现,而客户端则只需要接口就可以了(不过,对于客户端激活模式则必须有实现接口的类)。
{
Person GetPersonInfo(string name,string sex,int age);
}
public class ServerObject:MarshalByRefObject,IServerObject
要注意的是:1、两边生成该对象程序集的名字必须相同,严格地说,是命名空间的名字必须相同。
2、这种方式根据激活方式的不同,实现也不同。如果是服务器端激活(SingleTon和SingCall),那很简单。如上所述的方法即可;
如果是客户端激活,最好是利用抽象工厂,提供创建实例的方法。下面的类图表描述了总体解决方案(MSDN)。
图 1:混合法的结构
这样就必须在代码中还要加上抽象工厂的接口及实现:
{
IServerObject CreateInstance();
}
public class ServerObject:MarshalByRefObject,IServerObject,IServerObjFactory
{
public IServerObject CreateInstance()
{
return new ServerObject();
}
}
但是由于客户端激活的方式,它必须调用类的构造函数来创建实例,因此,在客户端只实现接口似乎是不可能的。
说明:关于对象类继承MarshalByRefObject,我作过测试,使可以间接地继承的。也就是我们可以先通过实现基类来继承它。然后实际所传递的对象在从基类中派生。
最后的代码应该是这样(服务器端,如果是客户端,只需要接口即可。这里我加了抽象工厂,因此该对象应该是以客户端激活模式。如果是服务器端激活模式,应该把抽象工厂接口和实现方法去掉)
namespace ServerRemoteObject
{
[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;}
}
}
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();
}
}
}
要补充说明的是,这里传递的对象显然比Socket更多,它甚至可以传递DataSet对象。其实,我们可以将它理解为WebService。
二、服务器端监听程序
写到这里,我想先介绍一下远程对象的三种激活模式。激活模式分为两大类:服务器端激活和客户端激活。其中服务器端激活又分为:SingleTon和SingleCall两种。
1、服务器端激活,又叫做WellKnow方式。我不想从理论上来讲述,而只是按照自己的理解来解释。简单地说,SingleTon激活方式,是对所有用户都建立一个对象实例,不管这些用户是在同一客户端还是不同客户端。而SingleCall则是对客户端的每个用户都建立一个远程对象实例。至于对象实例的销毁则是由GC自动管理的。举例来说,如果远程对象的一个累加方法(i=0;++i)被多个客户端(例如两个)调用。如果是SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是同样的。而SingleCall方式,则两个客户获得的都是1。原因不言自明。
2、客户端激活。则是对每个客户端都建立一个实例。粗看起来和SingleCall相同。实际上是由区别的。首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时在创建。其次,WellKnow方式对对象的管理是由GC管理的,而客户端则可以自定义生命周期,管理他的销毁。其三,WellKnow对象是无状态的,客户端激活对象则是有状态的。当然具体到使用上来说,实现的方式也就不一样了。
好了,现在我们就开始创建服务器端监听程序。
Remoting传递远程对象实质上来说还是通过Socket来传递,因此必须有一个传递对象的通道(Channel)。一个通道必须提供一个端口。在Remoting中,通道是由IChannel接口提供。它分别有两种类型:TcpChannel和HttpChannel。Tcp是以二进制的方式来传递,Http则是以Soap的方式来传递。两种通道各有优势,从性能上看,Tcp更好。但Http可以更好地通过防火墙。因此用户可以根据自己情况选择使用通道的方式。(本文使用TcpChannel,事实上两种可以混合使用,现创建TcpChannel,如果连接失败,在创建HttpChannel。)通道实现的类为同名类,命名空间则是在System.Runtime.Remoting.Channel下。通过通道可以传递对象,而且可以一次传递多个对象。对象的传递和选择激活方式,是通过RemotingConfiguratin的静态方法RegisterWellKnownServiceType()(针对服务器激活模式)和RegisterActivedServiceType()(针对客户端激活模式)来实现的。代码如下:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
namespace ServerRemoting
{
/// <summary>
/// Class1 的摘要说明。
/// </summary>
class Server
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
//创建通道,使用8080端口;
TcpChannel channel = new TcpChannel(8080);
//注册通道;
ChannelServices.RegisterChannel(channel);
//传递对象;
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(ServerRemoteObject.ServerObject),
"ServiceMessage",WellKnownObjectMode.SingleCall);
Console.WriteLine("Open the server listener");
Console.ReadLine();
}
}
}
代码很简单。我是用控制台来提供服务的。这里重点说一样RegisterWellKnownServiceType()方法的参数。第一个参数就是要传递对象的类型。第二个参数是自己定义的远程对象服务名,其实它是作为客户端的Uri的一部分。第三个参数自然是定义激活的方式。WellKnownObjectMode是一个枚举,有SingleCall和SingleTon两个。
如果是客户端激活模式,稍有不同:
RemotingConfiguration.RegisterActivatedServiceType(
typeof (ServerRemoteObject.ServerObject));
几点说明:
1、Dll的引用。要添加第一步所创建的远程对象Dll;添加System.Runtime.Remoting的引用。
2、关于一个通道传递几个对象。其实没什么复杂的,再接着用RegiseterWellKnownServiceType()方法就是了。只要这个对象符合我前面所讲的要求,同时添加了引用。当然客户端也要增加相应的代码。
3、关于多个通道的使用。在Remoting中,可以同时使用多个通道。但要求通道名必须不同。默认建立的TcpChannel名字为Tcp,HttpChannel名字为Http。如果再要建立一个TcpChannel,则必须自己定义一个名字。Channel本身提供ChannelName字段,但该字段是只读的。所以方法有点变化,要使用HashTable和IDictionary(要添加System.Collection命名空间):
channelProps[ " name " ] = " MyTCP " ;
channelProps[ " priority " ] = " 1 " ;
channelProps[ " Port " ] = " 8081 " ;
TcpChannel channel2 = new TcpChannel(channelProps,
new SoapClientFormatterSinkProvider(),
new SoapServerFormatterSinkProvider());
4、服务的停止。对于通道的使用,主要是用于传递远程对象,并开启对通道的监听。因此我们可以关闭对其的监听。也可以直接注销掉该通道。关闭监听使用通道实例对象的StopListening()方法实现。注销通道则是使用ChannelServices.UnregisterChannel()方法。关闭监听并没有注销掉通道,只是关闭了对客户端请求的监听。还可以通过StartListening()方法来重新打开监听。而通道一旦被注销,则需要重新使用如前所述的注册通道的方法重新注册。
IChannel[] channels = ChannelServices.RegisteredChannels;
// 关闭指定名为MyTcp的通道;
foreach (IChannel eachChannel in channels)
{
if (eachChannel.ChannelName == "MyTcp")
{
TcpChannel tcpChannel = (TcpChannel)eachChannel;
//关闭监听;
tcpChannel.StopListening(null);
//注销通道;
ChannelServices.UnregisterChannel(tcpChannel);
}
}
三、客户端请求和处理对象程序
先看代码:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
namespace ClientRemoting
{
/// <summary>
/// Class1 的摘要说明。
/// </summary>
class Client
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel);
ServerRemoteObject.IServerObject serverObj = (ServerRemoteObject.IServerObject)
Activator.GetObject(
typeof(ServerRemoteObject.IServerObject),
"tcp://localhost:8080/ServiceMessage");
Console.WriteLine("Invoke remoting object:");
ServerRemoteObject.Person person = serverObj.GetPersonInfo("wayfarer","male",28);
Console.WriteLine("name:{0},sex:{1},age:{2}",person.Name,person.Sex,person.Age);
Console.ReadLine();
}
}
}
代码非常简单。注意在客户端实例化通道时,是调用的默认构造函数,即没有传递端口号。事实上,这个端口号是缺一不可的,只不过它的指定被放在后面作为了Uri的一部分。这个程序重点是这一行代码:
Activator.GetObject(
typeof (ServerRemoteObject.IServerObject),
" tcp://localhost:8080/ServiceMessage " );
首先以WellKnown模式激活,客户端获得对象的方法是使用GetObject()。其中参数第一个是远程对象的类型。如前所述,我们在客户端只用了接口,因此返回的对象应该是接口类型。第二个参数就是服务器端的uri。如果是http通道,自然是用http://localhost:8080/ServiceMessage了。因为我是用本地机,所以这里是localhost,你可以用具体的服务器IP地址来代替它。端口必须和服务器端的端口一致。后面则是服务器定义的远程对象服务名。
如果服务器端采用客户端激活模式,调用的方法就不一样了。顺便说一句,上面的实现同样添加了远程对象的Dll引用。我前面已经讲过,这个Dll的命名空间名必须和服务器端的一致。因为我们的实现是在同一机器上完成的,所以大家在建立这个客户端远程对象时,要换个路径来建立这个对象类库。
还是回到刚才的问题,采用工厂,那么代码应稍作修改,因为远程对象不是直接实例化,而是通过工厂来创建的。显然,我们只需要改变上面那一行代码:
typeof (ServerRemoteObject.IServerObjFactory),
" tcp://localhost:8080/ServiceMessage " );
ServerRemoteObject.IServerObjFactory factory = new ServerRemoteObject.ServerObjFactory();
ServerRemoteObject.IServerObject serverObj = factory.CreateInstance();
即使是利用抽象工厂,如果是客户端激活模式,那么在客户端引用的远程对象还是要有类的具体实现,而不是只由接口的实现。从上面的代码就知道了,它获得对象实例的方法不是用Activitor.GetObject()方法来获得对象实例,而是利用RemotingConfiguration.RegisterActivatedClientType()静态方法去注册一个对象,然后再调用类的构造函数。
怎样像WellKnown模式那种,只在客户端实现接口,我还没有想到实现的方法。
通过上面的三个步骤,Remoting的程序也就完成了。注意运行的时候,需要先运行服务器程序,再运行客户端程序。
四、程序的完善
上面写的程序,关于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。
1、服务器端的配置文件:
< system.runtime.remoting >
< application name = " ServerRemoting " >
< service >
< wellknown mode = " Singleton " type = " ServerRemoteObject.ServerObject " objectUri = " ServiceMessage " />
</ service >
< channels >
< channel ref = " tcp " port = " 8080 " />
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
如果是客户端激活模式,则把wellknown改为activated,同时删除mode属性。
把该配置文件放到服务器程序的应用程序文件夹中,命名为ServerRemoting.config。那么前面的服务器端程序直接用这条语句即可:
RemotingConfiguration.Configure("ServerRemoting.config");
2、客户端配置文件
< system.runtime.remoting >
< application name = " ClientRemoting " >
< client url = " tcp://localhost:8080/ServerRemoting " >
< wellknown type = " ServerRemoteObject.ServerObject " objectUri = " tcp://localhost:8080/ServerRemoting/ServiceMessage " />
</ client >
< channels >
< channel ref = " tcp " port = " 8080 " />
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
如果是客户端激活模式,修改和上面一样。调用也是使用RemotingConfiguration.Configure()方法来调用存储在客户端的配置文件。
配置文件还可以放在machine.config中。如果客户端程序是web应用程序,则可以放在web.config中。