Remoting 构架
1.客户端(客户应用程序)
客户端的处理包含三个基本的组成部分,代理(Proxy)、格式器(Formatter) 和 通道(Channel)。
2.服务端(宿主应用程序)
服务端主要由 通道(Channel)、格式器(Formatter)、Stack Builder组成。
在服务端,宿主程序保持着为Remoting所打开的端口的监听,一旦通道收到消息,它便将消息发送给Formatter,Formatter将消息进行反序列化,然后将消息发送给Stack Builder,Stack Builder读取消息,然后依据消息创建对象(可选),调用方法。方法返回时,Stack Builder将返回值封装为消息,然后再提交给Formatter,Formatter进行格式化之后,发送到通道传递消息。
3.Remoting对象的三种激活方式
上一章 .Net Remoting - Part.1 中,我们提到了传值封送和传引用封送,并各给出了一张示意图,实际上,传引用封送还分为了三种不同的方式,下面来一一来介绍。对于传引用封送,记住各种方式的共同点:服务对象创建且一直保持在宿主程序中。
3.1客户激活(Client activated )
对于每个客户,创建了其专属的远程对象为其服务(由Part.1的代码可以看出,对象的状态在两次方法调用中也是维持着的)。除此以外,一个代理可以为多个客户对象服务。
客户激活模式的缺点就是 如果客户端过多时,或者服务对象为“大对象”时,服务器端的压力过大。另外,客户程序可能只需要调用服务对象的一个方法,但是却持有服务对象过长时间,这样浪费了服务器的资源。
3.2 服务激活 Singleton(Server activated Singleton)
这个模式的最大特色就是所有的客户共享同一个对象。服务端只在对象第一次被调用时创建服务对象,对于后继的访问使用同一个对象提供服务。
因为Sinlgton对象是在第一次访问(比如方法调用)时由.Net自动创建的,后继的访问不能重新创建对象,所以它不提供有参数的构造函数。另外,由于Singleton对象的状态由其它对象所共享,所以使用Singleton对象应该考虑线程同步 的问题。
3.3 服务激活 Single Call(Server activated Single Call)
Single Call方式是对每一次请求(比如方法调用)创建一个对象,而在每次方法返回之后销毁对象。由此可见Single Call 方式的最大特点就是 不保存状态。使用Single Call的好处就是不会过久地占用资源,因为方法返回后对资源的占用就随对象被销毁而释放了。最后,Single Call 方式也不允许使用由参数的构造函数。
Remoting程序的基本操作
1.服务程序集
我们首先创建服务程序集,它即为向客户程序提供服务的远程对象的实现代码。先创建一个类库项目ServerAssembly,然后创建类型ServerAssembly.DemoClass(为Part.1中的ClassLib.DemoClass添加了几个方法)。我们让它继承自MarshalByRefObject。
2.宿主应用程序
接下来我们新创建一个空解决方案ServerSide,在其下添加一个新的控制台项目ServerConsole,然后再将上面创建的项目ServerAssembly添加进来。除此以外,还需要添加System.Runtime.Remoting的引用。
2.1 注册通道
实现宿主应用程序的第一步就是注册通道。通道是实现了System.Runtime.Remoting.Channels.IChannel的类。通道分为两种,一种是发送请求的通道,比如说客户应用程序使用的通道,这种类型的通道还需要实现 System.Runtime.Remoting.Channels.IChannelSender 接口;一种是接收请求的通道,比如说宿主应用程序使用的通道,这种类型的通道还需实现System.Runtime.Remoting.Channels.IChannelReceiver接口。
通常我们不需要实现自己的通道,.Net 提供了三个内置的通道,分别是 System.Runtime.Remoting.Channels.Http.HttpChannel、
System.Runtime.Remoting.Channels.Tcp.TcpChannel
以及
System.Runtime.Remoting.Channels.Ipc.IpcChannel。
由于 IpcChannel 不能跨机器(只能跨进程),所以我们仅使用最为常用的 HttpChannel和TcpChannel为例作为说明。它们均实现了 System.Runtime.Remoting.Channels 命名空间下的 IChannel、IChannelSender、IChannelReceiver接口,所以它们既可以用于发送请求,也可以用于接收请求。
接下来需要对通道进行注册,然后对这个通道进行监听。对于同一个应用程序域,同一类型(实际上是同一名称,因为同一类型的通道默认名称相同)的通道只能注册一次。对同一机器来说,同一端口也只能使用一次。同一应用程序域可以注册多个不同类型的通道。注册的方式是调用ChannelServices类型的静态方法RegisterChannel();
当通道从端口监听到新请求时,它会从线程池中抓取一个线程执行请求,从而可以不间断地对端口进行监听(不会因为处理请求而中断)。当关闭宿主程序时,.Net会自动释放端口,以便其他程序可以使用该端口。
2.2 注册对象
注册通道之后,我们需要告诉.Net允许哪些类型可以被远程程序访问,这一步骤称为注册对象。如同上面所说的,有三种服务器端的远程对象类型:客户激活对象、服务激活Single Call、服务激活Singleton。
客户激活对象的注册方式需要使用RemotingConfiguration类型的RegisterActivatedServiceType()静态方法:
同一类型对象只可以用一种方式注册(客户激活 或者 服务激活)。即是说如果使用上面的方法注册对象,那么要么调用 ClientActivated(),要么调用ServerActivatedSingleCall()或者ServerActivatedSingleton(),而不能都调用。上面的RegisterWellKnownServiceType()方法接受三个参数:1.允许进行远程访问的对象类型信息;2.远程对象的名称,用于定位远程对象;3.服务激活对象的方式,Singleton或者Single Call。
2.3 对象位置
经过上面两步,我们已经开启了通道,并注册了对象(告诉了.Net哪个类型允许远程访问)。那么客户端如何知道远程对象位置呢?如同Web页面有一个Url一样,远程对象也有一个Url,这个Url提供了远程对象的位置。客户程序通过这个Url来获得远程对象。
RemotingConfiguration类型还有一个ApplicationName静态属性,当设置了这个属性之后,对于客户激活对象,可以提供此ApplicationName作为Url参数,也可以不提供。如果提供ApplicationName,则必须与服务端设置的ApplicationName相匹配;对于服务激活对象,访问时必须提供ApplicationName,此时两种方式的Uri为下面的形式:
protocal://hostadrress:port/ApplicationName/ObjectUrl // Server Activated
protocal://hostadrress:port // Client Activated Object
protocal:// hostadrress:port/ApplicationName // Client Activated Object
比如,如果通道采用协议为tcp,服务器地址为127.0.0.1,端口号为8051,ApplicationName设为DemoApp,ObjectUrl设为RemoteObject(ObjUrl为使用RegisterWellKnownServiceType()方法注册服务激活对象时第2个参数所提供的字符串;注意客户激活对象不使用它),则客户端在访问时需要提供的地址为:
tcp://127.0.0.1:8051/DemoApp/RemoteObject // Server Activated Object
tcp://127.0.0.1:8051/DemoApp // Client Activated Object
tcp://127.0.0.1:8051 // Client Activated Object
如果RemotingConfiguration类型没有设置ApplicationName静态属性,则客户端在获取远程对象时不需要提供ApplicationName,此时Url变为下面形式:
protocal://hostadrress:port/ObjectUrl // Server Activated Object
protocal://hostadrress:port // Client Activated Object
3.客户应用程序
我们现在再创建一个空解决方案ClientSide,然后在其下添加一个控制台应用程序ClientConsole,因为客户端需要ServerAssembly.DemoClass的元信息来创建代理,所以我们仍要添加对ServerAssembly项目的引用。除此以外,我们依然要添加 System.Runtime.Remoting程序集。
客户应用程序的任务只有一个:获取远程对象,调用远程对象服务。记得客户应用程序实际上获得的只是一个代理,只是感觉上和远程对象一模一样。客户端获得对象有大致下面几种情况:
3.1使用new操作符创建远程对象
客户应用程序可以直接使用new获得一个远程对象。例如下面语句:
DemoClass obj = new DemoClass();
看到这里你可能很惊讶,这样的话不是和通常的创建对象没有区别,为什么创建的是远程对象(这里用“远程对象”,只是为了说明方便,要记得实际上是代理对象)而非本地对象呢(注意本地客户程序ClientConsole也引用了ServerAssembly项目)?其实.Net和你一样,它也不知道这里要创建的是远程对象,所以,在使用new创建远程对象之前,我们首先要注册对象。注册对象的目的是告诉.Net,这个类型的对象将在远程创建,同时还要告诉.Net远程对象的位置。
我们知道远程对象有 客户激活 和 服务激活 两种可能,因此客户程序注册也分为了两种情况 -- 注册客户激活对象,注册服务激活对象。在客户端注册对象也是通过RemotingConfiguration类型来完成:
// 注册客户激活对象
private static void ClientActivated() {
Type t = typeof(DemoClass);
// 下面两个 url 任选一个
string url = "tcp://127.0.0.1:8501";
//string url = "tcp://127.0.0.1:8501/SimpleRemote";
RemotingConfiguration.RegisterActivatedClientType(t, url);
}
// 注册服务激活对象
private static void ServerActivated() {
Type t = typeof(DemoClass);
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
RemotingConfiguration.RegisterWellKnownClientType(t, url);
}
我们看到,尽管在服务端,服务激活有两种可能的方式,Singleton和SingleCall,但是在客户端,服务激活的两种方式采用同一个方法RegisterWellKnownClientType()方法进行注册。所以我们可以说 服务端决定服务激活对象的运行方式(Singleton或SingleCall)。
3.2 其它创建远程对象的方法
当我们在客户端对远程对象进行注册之后,可以直接使用new操作符创建对象。如果不进行注册来创建远程对象,可以通过 RemotingServices.Connect()、Activator.GetObject()、Activator.CreateInstance()方法来完成:
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
// 方式1
DemoClass obj = (DemoClass)RemotingServices.Connect(typeof(DemoClass), url);
// 方式2
DemoClass obj = (DemoClass)Activator.GetObject(typeof(DemoClass), url);
// 方式3
object[] activationAtt = { new UrlAttribute(url) };
DemoClass obj = (DemoClass)Activator.CreateInstance(typeof(DemoClass), null, activationAtt);
这几种方法,RemotingServices.Connect()和Activator.GetObject()是最简单也较为常用的,它们都是只能创建服务激活对象,且创建的对象只能有无参数的构造函数,并且在获得对象的同时创建代理。Activator.CreateInstance()提供了多达13个重载方法,允许创建客户激活对象,也允许使用有参数的构造函数创建对象,并且可以先返回一个Wrapper(包装)状态的对象,然后在以后需要的时候通过UnWrap()方法创建代理。CreateInstance()方法更详细的内容可以参考MSDN。
4.程序运行测试
Remoting 最让初学者感到困惑的一个方面就是 客户激活 与 服务激活 有什么不同,什么时候应该使用那种方式。说明它们之间的不同的最好方式就是通过下面几个范例来说明,现在我们来将上面的服务端方法、客户端方法分别进行一下组装,然后进行一下测试(注意在运行客户端之前必须保证服务端已经运行):
4.1 客户激活方式
先看下客户激活方式,服务端的Main()代码如下:
static void Main(string[] args) {
RegisterChannel(); // 注册通道
ClientActivated(); // 客户激活方式
Console.WriteLine("服务开启,可按任意键退出... ");
Console.ReadKey();
}
客户端的Main()代码如下:
static void Main(string[] args) {
// 注册远程对象
ClientActivated(); // 客户激活方式
RunTest("Jimmy", "Zhang");
RunTest("Bruce", "Wang");
Console.WriteLine("客户端运行结束,按任意键退出...");
Console.ReadKey();
}
private static void RunTest(string firstName, string familyName) {
DemoClass obj = new DemoClass();
obj.ShowAppDomain();
obj.ShowCount(firstName);
Console.WriteLine("{0}, the count is {1}. ",firstName, obj.GetCount());
obj.ShowCount(familyName);
Console.WriteLine("{0}, the count is {1}.",familyName, obj.GetCount());
}
程序运行的结果如下:
程序运行的结果如下:
其中第一幅图是服务端,第二幅图是客户端,我们起码可以得出下面几个结论:
- 不管是对象的创建,还是对象方法的执行,都在服务端(远程)执行。
- 服务端为每一个客户端(两次RunTest()调用,各创建了一个对象)创建其专属的对象,为这个客户提供服务,并且保存状态(第二次调用ShowCount()的值基于第一次调用ShowCount()之后count的值)。
- 可以从远程获取到方法执行的返回值。(客户端从GetCount()方法获得了返回值)
上面的第3点看起来好像是理所当然的,如果是调用本地对象的方法,那么确实是显而易见的。但是对于远程来说,就存在一个很大的问题:远程对象如何知道是谁在调用它?方法执行完毕,将返回值发送给哪个客户呢?此时可以回顾一下第一篇所提到的,客户端在创建远程对象时,已经将自己的位置通过消息发送给了远程。
最后我们再进行一个深入测试,追踪对象是在调用new时创建,还是在方法调用时创建。将RunTest()只保留一行代码:
private static void RunTest(string firstName, string familyName) {
DemoClass obj = new DemoClass(); // 创建对象
}
然后再次运行程序,得到的输出分别如下:
// 服务端
方式: Client Activated Object
服务端开启,按任意键退出...
======= DomoClass Constructor =======
======= DomoClass Constructor =======
// 客户端
客户端运行结束,按任意键退出...
由此可以得出结论:使用客户激活方式时,远程对象在调用new操作时创建。
4.2 服务激活方式 -- Singleton
我们再来看一下服务激活的Singleton方式。先看服务端代码(“按任意键退出”等提示语句均以省略,下同):
static void Main(string[] args) {
RegisterChannel(); // 注册通道
ServerActivatedSingleton(); // Singleton方式
}
再看下客户端的Main()方法:
static void Main(string[] args) {
// 注册远程对象
ServerActivated();
RunTest("Jimmy", "Zhang");
RunTest("Bruce", "Wang");
}
程序的运行结果如下:
同上面一样,第一幅为服务端,第二幅图为客户端。从图中我们可以得出:当使用Singleton模式时,服务端在第一次请求时创建一个对象(构造函数只调用了一次)。对于后继的请求仅使用这个对象进行服务(即使再次调用构造函数也不会创建对象),同时多个客户端共享同一个对象的状态(ShowCount()的值累加)。
我们和上一小节一样,再次将客户端的RunTest()只保留为“DemoClass obj = new DemoClass(); ”一行语句,然后运行程序,得到的结果为:
// 服务端
方式: Server Activated Singleton
服务端开启,按任意键退出...
// 客户端
客户端运行结束,按任意键退出...
这个结果出乎我们意料,但它又向我们揭示了Singleton的另一个性质:即使使用new操作符,客户端也无法创建一个对象,而只有在对象上第一次调用方法时才会创建。仔细考虑一下这个和上面的结论是类似的,只是更深入了一步。
4.3 服务激活方式 -- SingleCall
最后我们看一下SingleCall方式,注意到客户端的代码不需要做任何修改,所以我们只需要切换一下服务端的激活方式就可以了:
static void Main(string[] args) {
RegisterChannel(); // 注册通道
ServerActivatedSingleCall();
}
我们再次看一下运行结果:
我们可能首先惊讶构造函数居然调用了有10次之多,在每次RunTest()方法中各调用了5次。如同前面所说,对于SingleCall方式来说,对象对每一次方法调用提供服务,换言之,对于每一次方法调用,创建一个全新的对象为其服务,在方法执行完毕后销毁对象。我们再看下客户端的输出:GetCount()方法全部返回0,现在也很明确了,因为每次方法调用都会创建新对象(在创建对象时,int类型的count被赋默认值0),所以SingleCall方式是不会保存对象状态的。如果想要为对象保存状态,那么需要另外的机制,比如将状态存储到对象之外:
public void ShowCount(string name, object clientId) {
LoadStatus(this, clientId); // 加载对象状态
count++;
Console.WriteLine("{0},the count is {1}.", name, count);
SaveStatus(this, clientId); // 存储对象状态
}
其中LoadStatus()、SaveStatus()方法分别用于加载对象状态和 存储对象状态。注意到ShowCount()方法多了一个clientId参数,这个参数用于标示客户程序的id,因为服务端需要知道当前是为哪个客户程序加载状态。
最后,我们再次进行一下上面两节将RunTest()只保留为创建对象的一行代码,得到的运行结果和Singleton是一样的:
// 服务端
方式: Server Activated Singleton
服务端开启,按任意键退出...
// 客户端
客户端运行结束,按任意键退出...
这说明使用SingleCall时,即使使用了new 来创建对象,也不会调用构造函数,只有在调用方法时才会创建对象(调用了构造函数)。
Remoting中的传值封送
很多朋友可能此刻会感到些许困惑,在Part.1的范例中,我们讲述AppDomain时,使用了传值封送和传引用封送两种方式,但是上面的三种激活方式都属于传引用封送。那么如何进行对象的传值封送呢(将DemoClass直接传到本地)?实际上,在上面的例子中,我们已经进行了传值封送,这个过程发生在我们在客户端调用 GetCount() 时。为什么呢?想一想,count值本来是位于服务端的,且int为可序列化对象(Serializable),在向客户端返回方法结果时,count值被包装为了消息,然后由服务端发送回了客户端,最后在客户端进行了解包装及还原状态。
为了看得更清楚一些,我们在ServerAssembly中再创建一个DemoCount类型,然后对这个类型进行传值封送,因为DemoCount仅仅是为了传送数据,不包含任何行为,所以我们将它声明为结构:
public class DemoClass : MarshalByRefObject {
// 其余方法略...
// 示范传值封送
public DemoCount GetNewCount() {
return new DemoCount(count);
}
}
[Serializable]
public struct DemoCount {
private readonly int count;
public DemoCount(int count) {
this.count = count;
}
public int Count {
get { return count; }
}
public void ShowAppDomain() {
AppDomain currentDomain = AppDomain.CurrentDomain;
Console.WriteLine(currentDomain.FriendlyName);
}
}
在DemoClass中,我们又添加一个方法,它根据count的值创建了DemoCount对象,而DemoCount对象会通过传值封送传递到客户端。
现在修改客户端,再重载一个RunTest()方法,用来测试这次的传值封送:
// 测试传值封送
private static void RunTest() {
DemoClass obj = new DemoClass();
obj.ShowAppDomain(); // 显示远程对象所在应用程序域
obj.ShowCount("张子阳"); // Count = 1
DemoCount myCount = obj.GetNewCount(); // 传值封送DemoCount
myCount.ShowAppDomain(); // 显示DemoCount所在应用程序域
// 在客户端显示count值
Console.WriteLine("张子阳, count: {0}.", myCount.Count);
}
此时我们再次进行测试,得到的结果如下:
可以看到,我们在客户端DemoCount上调用ShowAppDomain()方法时,返回了ClientApp.exe,可见DemoCount已经通过传值封送传递到了客户端。那么我们继续上面的问题,如何将DemoClass整个传值封送过来呢?首先,我认为没有这个必要,如果将服务对象整个封送到客户端来执行,那么Remoting还有什么意义呢?其次,我们来看如何实现它。方法很简单,我们创建一个工厂类作为远程服务对象,然后将我们实际要传值封送到客户端的对象(比如DemoClass),作为工厂方法的返回值。这个例子我就不再演示了,相信看过上面的示例,您已经明白了。