分离服务程序集元信息和实现
在上面Remoting基本操作的范例中,我们发现了这样一个情况:即是 客户应用程序 仍然需要引用 服务程序集(ServerAssembly),因为它需要DemoClass的元信息来创建代理。使用这种共享服务程序集的方式构建Remoting程序,其运行时的示意图如下所示:
可以看到 宿主应用程序域(Host App Domain,位于服务端) 和 客户应用程序域(Client App Domain,位于客户端)均引用了ServerAssembly服务程序集。尽管直接将服务程序集交给客户端是最简单直接的方法,但是很多情况下,出于安全性或者其他方面考虑,我们并不愿意将服务程序集交给客户端,此时通常有下面几种做法:
1.使用接口分离服务程序集
使用接口分离程序集是最简单的一个方法。大家知道接口的作用之一就是将类型的定义和类型的实现分离,放到这里换个说法,就是将类型的元信息和类型的实现分离。具体的步骤如下:
- 再新建一个ShareAssembly项目,这个项目将由服务端和客户端共享。
- 将ServerAssembly中的服务对象,比如DemoClass的接口抽象出来,创建为IDemoClass.cs文件(可以使用VS2005的“重构”,“抽象接口”功能)。
- 将IDemoClass.cs 文件添加到ShareAssembly项目中,并从ServerAssembly中删除。
- 让ServerAssembly项目引用ShareAssembly项目,并让DemoClass实现IDemoClass接口。
- 服务端 ServerConsole项目 引用 ServerAssembly项目 和 ShareAssembly项目。
- 客户端 ClientConsole项目 只引用 ShareAssembly项目。
除此以外,需要传值封送到客户端的对象,比如DemoCount,也要放到ShareAssembly中。原因很简单,在传值封送之后,它便运行于客户端,所以客户端需要它的实现。
NOTE:注意这里如果不仔细容易看错,因为Server和Share都是S开头,不仔细看两个单词容易搞混。
我们先来看下ShareAssembly的实现:
// 此程序集(ShareAssembly)由客户端和服务端共享
namespace ShareAssembly {
public interface IDemoClass {
int GetCount();
DemoCount GetNewCount();
void ShowAppDomain();
void ShowCount(string name);
}
[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);
}
}
}
接着让ServerAssembly项目引用ShareAssembly项目,并让DemoClass去实现ShareAssembly.IDemoClass接口:
using ShareAssembly; // ShareAssembly 命名空间位于 ShareAssembly.dll程序集中
// DemoClass位于ServerAssembly.dll程序集中
namespace ServerAssembly {
public class DemoClass : MarshalByRefObject, IDemoClass {...} // 实现略
}
然后,取消客户端ClientConsole项目对ServerAssembly的引用,添加它对ShareAssembly项目的引用。然后让服务端ServerConsole项目添加对ShareAssembly项目的引用,通过上面这种方式,现在服务端和客户端运行时的示意图如下所示:
最后,我们还需要修改客户应用程序的代码,因为它没有引用ServerAssembly,所以不包含DemoClass的元信息,进而我们无法通过 new 来创建DemoClass对象:
IDemoClass obj = new DemoClass(); // 无法编译,没有引用ServerAssembly程序集
我们也无法直接new一个接口类型IDemoClass,所以我们只能使用上一节提到的Activator类型的GetObject()来创建对象(实为代理):
// 获取服务激活对象
// 服务端使用服务激活方式时使用此方法获取对象
private static IDemoClass GetServerActivatedObject() {
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
IDemoClass obj = (IDemoClass)Activator.GetObject(typeof(IDemoClass), url);
// 也可以用下面这种方式
//IDemoClass obj = (IDemoClass)RemotingServices.Connect(typeof(IDemoClass), url);
return obj;
}
然后我们再进行一下测试。我们先运行服务端,它现在可以使用Singleton或者Single Call方式为客户端提供远程服务。再运行一下客户端程序,应该可以获得预期的输出:
class Program {
static void
RunTest("Jimmy", "Zhang");
RunTest("Bruce", "Wang");
Console.WriteLine("客户端运行结束,按任意键退出...");
Console.ReadKey();
}
private static void RunTest(string firstName, string familyName) {
IDemoClass obj = GetServerActivatedObject(); // 创建对象
obj.ShowAppDomain(); // 显示程序所在应用程序域
obj.ShowCount(firstName);
Console.WriteLine("{0}, count: {1}.", firstName, obj.GetCount());
obj.ShowCount(familyName);
Console.WriteLine("{0}, count: {1}.\n", familyName, obj.GetCount());
}
}
但是使用接口分离程序集的方式无法创建 客户激活对象。因为Activator.GetObject()方法只适用于服务激活方式。而使用Activator.CreateInstance()方法时,作为参数的Type对象需要满足两个条件:1.类型必须继承自MarshalByRefObject;2.服务端必须对此类型进行注册。而使用接口分离服务程序集时,我们在客户端只有一个IDemoClass接口。它的类型信息(type对象,使用typeof(IDemoClass)获得)无法满足CreateInstance()方法中参数的要求。
如果服务端采用客户激活方式,我们可以采用其他方法获得远程对象,比如下面的 使用“空类”分离服务程序集。
2.使用“空类”分离服务程序集
使用空类分离服务程序集还是利用了 客户端只需要类型信息创建代理,而不需要实际的实现代码 这个特性。既然如此,我们何不创建两份ServerAssembly,一份包含服务对象的具体实现,供服务端使用;一份不包含服务对象实现,供客户端使用。我们把不包含实现的ServerAssembly版本交给客户端就可以了。接下来我们来一步步看如何实现:
1.首先,我们在ServerAssembly中再添加一个DemoClass类型,这个类与DemoClass的接口完全一致,区别是它的所有方法、属性都不包含实现,只是抛出异常:
public class DemoClass : MarshalByRefObject {
public void ShowCount(string name) {
throw new Exception("只运行于服务端");
}
public void ShowAppDomain() {
throw new Exception("只运行于服务端");
}
public int GetCount() {
throw new Exception("只运行于服务端");
}
public DemoCount GetNewCount() {
throw new Exception("只运行于服务端");
}
}
然后我们注释掉之前包含实现的DemoClass(包含两个DemoClass无法编译),重新成生,得到ServerAssembly.dll文件,将这个文件交给客户端使用。
2.接着注释掉DemoClass空类,然后取消对之前DemoClass的注释,服务端ServerConsole应用这个ServerAssembly项目,此时它包含完整实现的ServerAssembly。
3.客户端取消对ServerAssembly项目的引用,将第1步生成的包含DemoClass“空类”的ServerAssembly.dll程序集复制到Bin目录中,然后添加对它的引用。此时客户端的解决方案窗口如下图所示,它不引用其他任何项目,只引用第1步生成的“空类”ServerAssembly程序集:
这样就完成了所有的步骤,现在我们在客户端再添加一个方法GetClientActivatedObject()方法,获取服务端采用客户激活方式时的远程对象(获取服务激活的方法GetServerActivatedObject()与上一小节中相同):
// 获取客户激活对象
// 服务端使用客户激活方式时使用此方法获取对象
private static DemoClass GetClientActivatedObject() {
string url = "tcp://127.0.0.1:8501";
object[] activationAtt = { new UrlAttribute(url) };
DemoClass obj = (DemoClass)Activator.CreateInstance(
typeof(DemoClass), null, activationAtt);
return obj;
}
然后我们让服务端采用客户激活激活方式,同时在客户端运行测试程序:
static void
// 同上,略...
}
private static void RunTest(string firstName, string familyName) {
DemoClass obj = GetClientActivatedObject(); // 创建客户对象
// 以下同,略
}
注意到在RunTest()方法中,创建对象时调用了GetClientActivatedObject()方法获取了客户激活对象。运行后应该看到一切运转正常,此时的运行方式可以用下图来表示:
对于服务激活方式,使用方法类同,这里就不再做示范了。
使用“空类”的方式还有一个好处就是可以向Part.2中一样,可以先注册对象,然后直接使用new创建远程对象:
// 获取服务激活对象
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
RemotingConfiguration.RegisterWellKnownClientType(typeof(DemoClass), url);
DemoClass obj = new DemoClass();
// 获取客户激活对象
string url = "tcp://127.0.0.1:8501";
RemotingConfiguration.RegisterActivatedClientType(typeof(DemoClass), url);
DemoClass obj = new DemoClass();
如果 服务程序集 不大、代码不多的话,使用这种方式或许是最好的办法。但有时服务程序集可能包含大量代码,通过这种方式就显得低效了,我们期望如果能够自动创建一个“空类”程序集,事情就好办的多了。此时,我们可以应用.Net提供的Soapsuds.exe工具。
3.使用Soapsuds.exe分离程序集
Soapsuds.exe是.Net所提供的一个工具,它允许客户端通过输入一个远程对象的Url,然后生成一个dll程序集文件,这个文件包含了在客户端创建代理的全部信息但是不包含具体的实现。客户端引用此生成的文件,就如同引用服务程序集一样(你可以将这个文件视为上一小节的“空类”程序集)。因为这个程序集不包含服务对象的实现代码,于是也就达到了向客户端隐藏服务程序实现的目的。需要注意的是,使用Soapsuds.exe获取dll文件时,服务端必须注册为服务激活对象(SingleCall或者Singleton),且使用Http作为传输协议,SOAP作为传输格式。在获得dll文件以后,不管服务器使用那种协议或者格式,客户都可以访问远程对象。
NOTE:我发现有的书中说当使用Soapsuds.exe这种方式分离程序集,则服务端只能使用Http通道和SOAP格式提供远程服务。我认为这是一个误导,这个限制条件应该是针对于Soapsuds.exe这个工具,而不是你的Remoting程序。你完全可以在服务器端同时开启Http通道和Tcp通道,然后让客户端使用Soapsuds.exe 和 Http通道获得生成文件。而在获得以后使用Tcp通道和Binary格式与服务端进行交互。下面的例子就将使用这种做法。
接下来我们来一步步看如何实现:
1. 首先看服务端,我们要开启Http通道并且以SOAP作为传输格式,应该记得Http通道的默认格式就为SOAP,所以我们只需要开启Http通道就可以了。而我们从Part.2开始就一直使用的RegisterChannel()方法本身就包含了两种通道的注册,这里我再把它复制下来:
// 一般注册通道的方式
private static void RegisterChannel() {
// 创建通道实例
// IChannel tcpChnl = new TcpChannel(8501);
IChannelReceiver tcpChnl = new TcpChannel(8501);
// 注册通道
ChannelServices.RegisterChannel(tcpChnl, false);
IChannel httpChnl = new HttpChannel(8502);
ChannelServices.RegisterChannel(httpChnl, false);
}
2.我们需要注册服务激活对象,因为服务端只会为服务激活对象产生WSDL文件,所以Soapsuds.exe只能基于服务激活对象生成dll文件。为了后面客户端的测试方便,我们同时在服务端注册服务激活对象和客户激活对象:
static void
RegisterChannel(); // 注册通道
RemotingConfiguration.ApplicationName = "SimpleRemote";
ClientActivated(); // 客户激活方式
ServerActivatedSingleton(); // 服务激活方式 - Singleton
//ServerActivatedSingleCall(); // 服务激活方式 - Single Call
Console.WriteLine("服务端开启,按任意键退出...\n");
Console.ReadKey();
}
3.现在服务端的设置已经完成,我们来生成客户端要使用的dll文件。打开VS2008 命令提示符,然后在其下输入:
E:\>soapsuds -url:http://127.0.0.1:8502/SimpleRemote/ServerActivated?wsdl -oa:ClientProxy.dll
这将为我们在E盘的根目录下生成ClientProxy.dll文件,这个文件将用于客户端成生代理。
4.接下来我们看客户端,我们让ClientConsole项目引用ClientProxy.dll文件,如下图所示:
5.这个ClientProxy就好像ServerAssembly一样,包含ServerAssembly命名空间和DemoClass类。与DemoClass相关的类型,比如DemoCount,也会包含进来:
接着我们就可以运行一下测试了,下面是客户端的代码:
using ServerAssembly; // 来自于 ClientProxy
// ...
static void
RunTest("Jimmy", "Zhang");
RunTest("Bruce", "Wang");
Console.WriteLine("客户端运行结束,按任意键退出...");
Console.ReadKey();
}
private static void RunTest(string firstName, string familyName) {
DemoClass obj = GetClientActivatedObject(); // 创建客户对象
//DemoClass obj = GetServerActivatedObject(); // 创建服务激活
obj.ShowAppDomain(); // 显示程序所在应用程序域
obj.ShowCount(firstName);
Console.WriteLine("{0}, count: {1}.", firstName, obj.GetCount());
obj.ShowCount(familyName);
Console.WriteLine("{0}, count: {1}.\n", familyName, obj.GetCount());
}
// 获取服务激活对象
// 服务端使用服务激活方式时使用此方法获取对象
private static DemoClass GetServerActivatedObject() {
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
DemoClass obj = (DemoClass)Activator.GetObject(typeof(DemoClass), url);
// 也可以用下面这种方式
//DemoClass obj = (DemoClass)RemotingServices.Connect(typeof(DemoClass), url);
return obj;
}
// 获取客户激活对象
// 服务端使用客户激活方式时使用此方法获取对象
private static DemoClass GetClientActivatedObject() {
string url = "tcp://127.0.0.1:8501";
object[] activationAtt = { new UrlAttribute(url) };
DemoClass obj = (DemoClass)Activator.CreateInstance(
typeof(DemoClass), null, activationAtt);
return obj;
}
应该可以得到期望的结果,与前面的输出类似,这里就不演示了。
注意到我们已经在服务端同时开启了 服务激活 和 客户激活,所以我们只需要在上面的RunTest()中切换获取对象的方式就可以对服务激活方式和客户激活方式进行测试。另外看到在GetServerActivatedObject()和GetClientActivatedObject()方法中,我们均使用了tcp协议和8501端口来获取远程对象。而获得ClientProxy.dll生成文件时,使用的是 http协议和8502端口,可见没有对我们的Remoting程序产生影响。
接下来我们看最后一种实现分离的方式,使用工厂方法实现分离,它属于使用接口分离的一个变种:
4.使用工厂方法实现分离
使用工厂方法有一定的局限性,因为它只适用于客户激活方式,但是在表现形式上又像服务激活一样。现在我来详细解释一下,它的做法是将服务对象作为工厂类以服务激活方式(因为是工厂类,所以服务激活方式几乎总是采用Singleton方式)向客户端提供。因为它是服务激活方式,所以我们可以使用本章第一小节分离接口的方式来完成,并使用Activator.GetObject()方法获得。而工厂方法的返回值,为实际向客户端提供服务的对象(DemoClass)的接口,在本例中也就是IDemoClass。通过将实际服务对象作为方法的返回值以接口的方式返回到客户端,这样做等于是绕过了本章第一种方法中使用CreateInstance()创建客户激活对象的参数限制问题。实现工厂方法返回值接口的对象当然就是DemoClass了,我们让它继承自MarshalByRefObject,这样它将永远驻留在服务端,而不会传递到客户端去。接下来我们来一步步实现:
1.我们先创建一个ShareAssembly项目,这个项目将由服务端和客户端共享,它包含工厂类和实际提供服务的对象(DemoClass)的接口定义,以及进行传值封送的DemoCount结构:
namespace ShareAssembly {
public interface IDemoClass {
int GetCount();
DemoCount GetNewCount();
void ShowAppDomain();
void ShowCount(string name);
}
public interface IServerFactory {
IDemoClass GetDemoClass();
}
[Serializable]
public struct DemoCount {
// 实现略
}
}
2.我们创建ServerAssembly项目,这个项目引用ShareAssembly项目,它只位于服务端。它只包含两个类型 -- ServerFactory和DemoClass,分别用于实现IServerFactory和IDemoClass接口:
namespace ServerAssembly {
public class DemoClass : MarshalByRefObject, IDemoClass {
// 实现略
}
public class ServerFactory : MarshalByRefObject, IServerFactory {
public IDemoClass GetDemoClass() {
return new DemoClass();
}
}
}
注意GetDemoClass()方法会为每一个调用此方法的客户端在服务端创建一个DemoClass对象,就相当于直接使用客户激活方式一样。而实际我们直接提供给客户端的ServerFactory是服务激活Singleton方式的。通过这种方式间接的为每个客户端创建了一个DemoClass对象。
3.在服务端,我们只需要引用ServerAssembly项目和ShareAssembly项目,然后注册ServerFactory对象,并开启服务激活Singleton方式就可以了:
class Program {
static void
RegisterChannel();
RemotingConfiguration.ApplicationName = "SimpleRemote";
ServerActivatedSingleton();
Console.WriteLine("服务端开启,按任意键退出...\n");
Console.ReadKey();
}
private static void RegisterChannel() {...} // 实现略
// 注册 服务端激活对象 Singleton
private static void ServerActivatedSingleton() {
Console.WriteLine("方式: Server Activated Singleton");
Type t = typeof(ServerFactory); // 注意此处注册为ServerFactory
RemotingConfiguration.RegisterWellKnownServiceType(
t, "ServerActivated", WellKnownObjectMode.Singleton);
}
}
4.在客户端,我们只引用ShareAssembly项目。先使用GetObject()方法获得IServerFactory,然后在其上调用GetDemoClass(),进而获得IDemoClass接口,通过IDemoClass接口调用远程DemoClass对象的方法以获得服务:
class Program {
static void
RunTest("Jimmy", "Zhang");
RunTest("Bruce", "Wang");
Console.WriteLine("客户端运行结束,按任意键退出...");
Console.ReadKey();
}
private static void RunTest(string firstName, string familyName) {
IDemoClass obj = GetServerActivatedObject(); // 创建IDemoClass对象
obj.ShowAppDomain(); // 显示程序所在应用程序域
obj.ShowCount(firstName);
Console.WriteLine("{0}, count: {1}.", firstName, obj.GetCount());
obj.ShowCount(familyName);
Console.WriteLine("{0}, count: {1}.\n", familyName, obj.GetCount());
}
// 获取服务激活对象
// 服务端使用服务激活方式时使用此方法获取对象
private static IDemoClass GetServerActivatedObject() {
string url = "tcp://127.0.0.1:8501/SimpleRemote/ServerActivated";
IServerFactory obj = (IServerFactory)Activator.GetObject(typeof(IServerFactory), url);
// 也可以用下面这种方式
//IServerFactory obj =
// (IServerFactory)RemotingServices.Connect(typeof(IServerFactory), url);
IDemoClass rtn = obj.GetDemoClass(); // 在远程创建DemoClass,返回接口
return rtn;
}
}
运行上面的程序,应该可以得到和Part.2中共享服务程序集同样的输出。