.Net Remoting 实践
.Net Remoting 实践
对于C/S架构的软件来说,Client端界面主要负责数据采集和呈现。而Server端则负责业务逻辑实现和数据处理。Client和Server的通信工作是一个联系两端的桥梁。
通常,我们的C/S应用环境都是类似如下图所示的情况:
1)一个数据库服务器(DB Server)
2)一个应用服务器(APP Server)
3)多个客户端(Client。可能是PC,NoteBook,PDA…)
在Client和APP Server上,是两个独立运行的程序。在.Net的环境下,存在“App Domain”的概念,也就是每个运行的程序有自己独立的应用程序域,“不能”直接从A应用程序域访问B应用程序域中的对象。这就是我门今天要说的如何建立这个桥梁的问题。
在处理通信的问题上,我们有Socket,MQ,Remoting,WebService等技术可以选择。至于选择哪一种,要具体问题具体分析。在项目中,我门采用了Remoting技术。
查看微软的资料和网上同仁的Blog,可以很容易地明白以下几方面的基础知识:
1. Remoting的两种通道:TCP Channel和HTTP Channel
2. 远程对象的激活方式:服务端激活(WellKnown和Singleton)和客户端激活
还有就是要注意:要远程调用的对象都必须继承自MarshalByRefObject。
实现服务端的步骤一般如下:(这些都是从Microsoft .Net Remoting系列专题之一:.Net Remoting基础篇 张逸:晴窗笔记摘录的)
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;
在我们的实际应用中,使用的是Singleton +客户端激活的方式。首先创建了一个Singleton方式的远程对象ServiceFactory(继承MarshalByRefObject)。ServiceFactory是个工厂,有许多的方法CreateA(),CreateB(),CreateC()…,可以创建其他的远程业务对象。其次,在客户端通过Singleton的方式拿到这个Factory后,调用这些方法,再获取其他的远程业务对象。简单地说,就是我们要刀要枪要炮,不用自己造,先把兵工厂搞到手了,再让工厂造。
在服务端工厂对象的处理上,我们也是先注册通道,然后调用RemotingServices.Marshal方法,就实现了远程对象的注册。
……注册信道
ServiceFactory ServiceObj = new ServiceFactory();
ServiceRef = RemotingServices.Marshal(ServiceObj, serviceName, typeof(ServiceFactory));
而在远程对象的销毁上,直接调用RemotingServices.Disconnect方法,阻止ServiceObj接收任何消息。
RemotingServices.Disconnect(ServiceObj);
……注销信道
当然,这里面还有一些问题需要注意。例如远程对象的生命周期要设置为无限期,则需要重载如下方法,并返回null。
public override object InitializeLifetimeService()
{
return null;
}
在客户端,要获取远程对象ServiceFacory的实例引用,需要使用Activator.GetObject方法,
IServiceFactory boFactory =
(IServiceFactory)Activator.GetObject( typeof(IServiceFactory), serviceUrl);
这里IServiceFacotry 是ServiceFacotry的接口。
获得IServiceFacotry类型 的boFactory后,我们就可以调用boFactory的CreateXXX方法来造枪造炮了。
虽然远程对象能够提供服务,而且客户端也能获取远程对象,并使用该服务了,但是总觉得不够完整:服务什么时候启动?什么时候关闭?如何灵活管理该服务呢?要是能像SQL Server的服务管理器一样就好了。
首先将Remoting服务包装一下,做成Windows服务。这个比较简单,网上也有很多。大致是建立一个Windows 服务项目,在Service1的代码中重载两个方法:
protected override void OnStart(string[] args)
protected override void OnStop()
然后在Service1的属性窗口点击“添加安装程序”会添加一个“ProjectInstaller1”的文件。它包含两个组件,“serviceInstaller1”设置一些服务的基本信息,如名称,说明等,“serviceProcessInstaller1”的Accout属性比较有意思,选择的是运行服务的账户类型,这里选择的是“Local System”。
编译后生成WindowsService1.exe,然后用命令行执行:C:"WINDOWS"Microsoft.NET"Framework"v2.0.50727"InstallUtil.exe WindowsService1.exe
net start ServiceName
就可以安装并启动服务了。
顺便说一下,要卸载服务,使用
net stop ServiceName
C:"WINDOWS"Microsoft.NET"Framework"v2.0.50727"InstallUtil.exe -u WindowsService1.exe
最后,我们还要做一个服务管理器,当然,这是一个很简单的界面。
其中要注意的就是对Windows服务的管理(包括启动,停止等操作),我门用到了ServiceController(要引用System.ServiceProcess名空间)
ServiceController sc = new ServiceController(serviceName);//建立服务对象
//服务停止则启动服务
if ((sc.Status.Equals(ServiceControllerStatus.Stopped)) || (sc.Status.Equals(ServiceControllerStatus.StopPending)))
{
sc.Start();
sc.Refresh();
}
//服务运行则停止服务
if (sc.Status.Equals(ServiceControllerStatus.Running))
{
sc.Stop();
sc.Refresh();
}
另外,我还遇到了一个小问题,在停止服务的时候,管理界面反应还挺快,但是在启动服务的时候,服务的状态不能立即刷新:(。我也没有办法了,只好做了个Timer来检查Service的状态。
好了,这个环节终于快马加鞭地回顾了一遍,嗯~