首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。
在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。
1、Remoting的两种通道
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; } } |
[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;} } } |
三、服务器端
根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:
1、注册通道
要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。
注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间:System.Runtime.Remoting.Channel.Tcp。代码如下:
TcpChannel channel = new TcpChannel(8080); ChannelServices.RegisterChannel(channel); |
RemotingConfiguration.RegisterWellKnownServiceType(typeof(ServerRemoteObject.ServerObject), "ServiceMessage",WellKnownObjectMode.SingleTon); |
RemotingConfiguration.RegisterWellKnownServiceType(typeof(ServerRemoteObject.ServerObject), "ServiceMessage",WellKnownObjectMode.SingleCall); |
RemotingConfiguration.ApplicationName = "ServiceMessage"; RemotingConfiguration.RegisterActivatedServiceType(typeof(ServerRemoteObject.ServerObject)); |
//获得当前已注册的通道; IChannel[] channels = ChannelServices.RegisteredChannels; //关闭指定名为MyTcp的通道; foreach (IChannel eachChannel in channels) { if (eachChannel.ChannelName == "MyTcp") { TcpChannel tcpChannel = (TcpChannel)eachChannel; //关闭监听; tcpChannel.StopListening(null); //注销通道; ChannelServices.UnregisterChannel(tcpChannel); } } |
四、客户端
客户端主要做两件事,一是注册通道。这一点从图一就可以看出,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"); |
RemotingConfiguration.RegisterActivatedClientType( typeof(ServerRemoteObject.ServerObject), "tcp://localhost:8080/ServiceMessage"); ServerRemoteObject.ServerObject serverObj = new ServerRemoteObject.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); |
object[] attrs = {new UrlAttribute("tcp://localhost:8080/EchoMessage")}; ObjectHandle handle = Activator.CreateInstance("ServerRemoteObject", "ServerRemoteObject.ServerObject",attrs); ServerRemoteObject.ServerObject obj = (ServerRemoteObject.ServerObject)handle.Unwrap(); |
五、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); |
IDictionary httpProp = new Hashtable(); httpProp["name"] = "http8080"; httpProp["port"] = 8080; IChannel channel = new HttpChannel(httpProp, new SoapClientFormatterSinkProvider(), new SoapServerFormatterSinkProvider()); ChannelServices.RegisterChannel(channel); |
public interface IServerObject { Person GetPersonInfo(string name,string sex,int age); } public class ServerObject:MarshalByRefObject,IServerObject { ......} |
我们在服务器端的远程对象中加上抽象工厂的接口和实现类:
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(); } |
//传递对象; RemotingConfiguration.RegisterWellKnownServiceType( typeof(ServerRemoteObject.ServerObjFactory), "ServiceMessage",WellKnownObjectMode.SingleCall); |
ServerRemoteObject.IServerObjFactory serverFactory = (ServerRemoteObject.IServerObjFactory) Activator.GetObject( typeof(ServerRemoteObject.IServerObjFactory), "tcp://localhost:8080/ServiceMessage"); ServerRemoteObject.IServerObject serverObj = serverFactory.CreateInstance(); |
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(); } } |
3、利用配置文件实现
前面所述的方法,于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。 (1) 服务器端的配置文件:
<configuration> <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> |
TcpChannel channel = new TcpChannel(8080); ChannelServices.RegisterChannel(channel); |
ServerObject obj = new ServerObject(); |
ObjRef objrefWellKnown = RemotingServices.Marshal(obj, "ServiceMessage"); |
RemotingServices.Disconnect(obj); |