2.3.2服务器激活Singleton(Server activated Singleton)
2.3.3 服务器激活 Single Call(Server activated Single Call)
4.1.3使用Soapsuds.exe分离程序集(本系统中使用的方法)
基于DotNetRemoting分布式安全部署
1、Remoting基本原理
.NET Remoting是.NET平台上允许存在于不同应用程序域中的对象相互知晓对方并进行通讯的基础设施。调用对象被称为客户端,而被调用对象则被称为服务器或者服务器对象。简而言之,它就是.NET平台上实现分布式对象系统的框架。只要是跨越AppDomain的访问,都属于Remoting。
Remoting编程基本原理:当客户端创建远程RemotableClass的一个实例,.NET框架在客户端应用程序域中产生一个代理。该代理看起来就像实际对象。代理收到调用后,通过通道连接到远程的对象。
2、Remoting基本知识
2.1 通道
Remoting通过通道(channel)来传输消息。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.2传值封送、传引用封送
.Net将远程域中的远程对象的状态进行复制、序列化,然后在客户端中重新创建对象,还原状态,并通过代理来对对象进行访问。这种跨应用程序域的访问方式叫做 传值封送(Marshal by value),有点类似于C#中参数的按值传递。
当客户端调用代理上的方法时,由代理将对方法的请求发送给远程对象,远程对象执行方法请求,最后再将结果传回。这种方式叫做 传引用封送(Marshal by reference)。实现方式 :让对象继承自MarshalByRefObject基类。
2.3 Remoting对象的三种激活方式
2.3.1客户激活(Client activated )
(图二)
结合图二,我们可以看出对于每个客户,创建了其专属的远程对象为其服务除此以外,一个代理可以为多个客户对象服务。
客户激活模式的缺点就是 如果客户端过多时,或者服务对象为“大对象”时,服务器端的压力过大。另外,客户程序可能只需要调用服务对象的一个方法,但是却持有服务对象过长时间,这样浪费了服务器的资源。
2.3.2服务器激活Singleton(Server activated Singleton)
这个模式的最大特色就是所有的客户共享同一个对象。服务端只在对象第一次被调用时创建服务对象,对于后继的访问使用同一个对象提供服务。如下图(图三)所示:
(图三)
因为Sinlgton对象是在第一次访问(比如方法调用)时由.Net自动创建的,后继的访问不能重新创建对象,所以它不提供有参数的构造函数。另外,由于Singleton对象的状态由其它对象所共享,所以使用Singleton对象应该考虑线程同步 的问题。
2.3.3 服务器激活 Single Call(Server activated Single Call)
Single Call方式是对每一次请求(比如方法调用)创建一个对象,而在每次方法返回之后销毁对象。由此可见Single Call 方式的最大特点就是不保存状态。使用Single Call的好处就是不会过久地占用资源,因为方法返回后对资源的占用就随对象被销毁而释放了。最后,Single Call 方式也不允许使用由参数的构造函数。
2.4 激活远程对象
Activator.GetObject和Activator.CreateInstance方法 New操作符并不是激活远程对象的唯一方法。.NET框架提供了其他的激活方法:GetObject和CreateInstance。它们都是System.Activator类的成员。GetObject被用来激活在服务器端激活的对象,而CreateInstance被用来激活在客户端激活的对象。
当使用GetObject或者CreateInstance来激活远程对象时,不再需要调用RegisterActivatedClientType或者RegisterWellKnownClientType来注册服务器上可远程化的类。例如:激活在服务器端激活的对象时:
RemotingConfiguration.RegisterWellKnownClientType(typeof(Clock),"tcp://localhost:1234/Clock");
Clock clock = new Clock();
可以使用下面的方法代
Clock clock =(Clock) Activator.GetObject(typeof(Clock,"tcp://localhost:1234/Clock");
激活客户端对象时:
RemotingConfiguration.RegisterActivatedClientType(typeof(Stopwatch),"tcp://localhost:1234");
Stopwatch sw = new StopWatch();
可以这样的方式:
object[] url ={new UrlAttribute("tcp://localhost:1234")};
Stopwatch sw =(Stopwatch) Activator.CreateInstance(typeof(Stopwatch),null,url);
为什么要使用它们来代替new呢?因为在你仅知道URL和接口时,GetObject和CreateInstance可以仍使用。假设改变Clock类,它实现一个IClock接口。
使用GetObject时:
Iclock ic = (Iclock)Activator.GetObject(typeof(Iclock),"tcp://localhost:1234/Clock");
如果使用new,则会出现编译错误,因为new不能接受一个接口名称:
RemotingConfiguration.RegisterWellKnownClientType(typeof(IClock),"tcp://localhost:1234/Clock");
IClock ic = new IClock ();
3、基本实现
(1)、建一个类库(RemoteObject)以供远程调用。其中只有一个类(RemoteObj),里面的方法也很简单,传如一个参数并返回。
RemoteObj.cs类文件代码
namespace RemoteObject
{
public class RemoteObj : MarshalByRefObject //传引用封送
{
public RemoteObj()
{
Console.WriteLine("================开始调用=====================");
}
public string MyName(string name)
{
return "My Name is " + name + "";
}
}
}
(2)、建一个控制台应用程序(Server),作用是注册通道和注册对象,要告诉.Net允许哪些类型可以被远程程序访问。通过它来调用远程的类(RemoteObj)。对比三种激活方式的优缺点选择服务器激活Singleton模式激活
当使用Singleton模式时,服务端在第一次请求时创建一个对象(构造函数只调用了一次)。对于后继的请求仅使用这个对象进行服务(即使再次调用构造函数也不会创建对象),同时多个客户端共享同一个对象的状态。在客户端即使使用new操作符,客户端也无法创建一个对象,而只有在对象上第一次调用方法时才会创建。
Server中的Program类的代码如下
namespace Server
{
class Program
{
static void Main(string[] args)
{
RegisterChannel(); // 注册2个通道
ServerActivatedSingleton();//服务器端激活对象 Singleton
Console.WriteLine("服务端程序开启,按任意键退出...");
Console.ReadKey();
}
/// <summary>
/// 注册通道
/// </summary>
private static void RegisterChannel()
{
// 创建通道实例
IChannelReceiver tcpChnl = new TcpChannel(8501);
//在CS系统中 注册tcp通道
ChannelServices.RegisterChannel(tcpChnl, false);
}
//服务器端激活对象 Singleton
private static void ServerActivatedSingleton()
{
Console.WriteLine("方式: Server Activated Singleton");
Type t = typeof(RemoteObject.RemoteObj);
RemotingConfiguration.RegisterWellKnownServiceType(t, "ServerActivated", WellKnownObjectMode.Singleton);
}
}
}
(3)、建一个控制台应用程序(Client)它是调用者。客户端(Client)需要RemoteObject.RemoteObj的元信息来创建代理,所以我们仍要添加对RemoteObect项目的引用。客户应用程序的任务只有一个:获取远程对象,调用远程对象服务。记得客户应用程序实际上获得的只是一个代理,只是感觉上和远程对象一模一样。客户应用程序可以直接使用new获得一个远程对象。例如下面语句:
RemoteObject.RemoteObj obj = new RemoteObject.RemoteObj();
这样的话和通常的创建对象有什么区别呢?为什么创建的是远程对象(实际上是代理对象)而非本地对象呢(注意本地客户程序Client也引用了RemoteObjecgt项目)?其实.Net它也不知道这里要创建的是远程对象,所以,在使用new创建远程对象之前,我们首先要注册对象。注册对象的目的是告诉.Net,这个类型的对象将在远程创建,同时还要告诉.Net远程对象的位置。
Program.cs代码如下。
namespace Client
{
class Program
{
static void Main(string[] args)
{
ServerActivated();
string temp=RunTest("sunqiang");
Console.WriteLine(temp);
Console.WriteLine("调用远程对象结束");
Console.ReadKey();
}
// 注册服务激活对象
private static void ServerActivated()
{
Type t = typeof(RemoteObject .RemoteObj );
string url = "tcp://127.0.0.1:8501/ServerActivated";
RemotingConfiguration.RegisterWellKnownClientType(t, url); // 注册对象
}
private static string RunTest(string name)
{
RemoteObject.RemoteObj obj = new RemoteObject.RemoteObj();
return obj.MyName(name);
}
}
}
(4)、运行程序
程序编译通过后,找到Server端(\Server\bin\Debug\Server.exe)的Server.exe文件,双击运行注册通道,注册对象。运行结果如下图(图四)所示
图四
然后找到客户端(\Client\bin\DebugClient.exe)的Client.exe文件双击运行看能否返回正确的结果。我们调用的代码为:string temp=RunTest("sunqiang");
运行结果如下图(图五)所示:
(图五)
看到返回结果为My Name is sunqiang 结果正确。
(5)模拟局域网内的调用
把编译后的Server端的exe文件和RemoteObject类库生成的Dll文件部署到到另外一台电脑(陈炜)
重新添加引用,更改Client.Program中注册对象的URLstring url = "tcp://192.9.110.106:8501/ServerActivated";运行,返回结果和图二相同,测试正确。
4、Romoting通信机制的安全
4.1分离服务程序实现
在上面Remoting基本操作的范例中,我们发现了这样一个情况:即是 客户应用程序(Client)仍然需要引用远程对象 (RemoteObject),因为它需要远程对象的元信息来创建代理。使用这种共享服务程序集的方式构建Remoting程序,其运行时的示意图如下(图六)所示:
(图六)
可以看到 宿主应用程序域(Host App Domain,位于服务端) 和 客户应用程序域(Client App Domain,位于客户端)均引用了RemoteObject服务程序集。尽管直接将服务程序集交给客户端是最简单直接的方法,但是很多情况下,出于安全性或者其他方面考虑,我们并不愿意将服务程序集交给客户端,此时通常有下面几种做法:
4.1.1使用接口分离服务程序集
使用接口分离程序集是最简单的一个方法。大家知道接口的作用之一就是将类型的定义和类型的实现分离,放到这里换个说法,就是将类型的元信息和类型的实现分离。具体的步骤如下:
再新建一个ShareAssembly项目,这个项目将由服务端和客户端共享。
将RemoteObject中的服务对象,比如RemoteObj.cs的接口抽象出来,创建为IRemoteObj.cs文件(可以使用VS2005的“重构”,“抽象接口”功能)。
将IRemoteObj.cs文件添加到ShareAssembly项目中,并从RemoteObject中删除。
让RemoteObject项目引用ShareAssembly项目,并让RemoteObj实现IRemoteObj接口。
服务端 ServerConsole项目 引用 RemoteObject项目 和 ShareAssembly项目。
客户端 ClientConsole项目 只引用 ShareAssembly项目。
除此以外,需要传值封送到客户端的对象,也要放到ShareAssembly中。原因很简单,在传值封送之后,它便运行于客户端,所以客户端需要它的实现。(具体实现略)
4.1.2使用“空类”分离服务程序集
使用空类分离服务程序集还是利用了客户端只需要类型信息创建代理,而不需要实际的实现代码这个特性。既然如此,我们何不创建两份RemoteObject,一份包含服务对象的具体实现,供服务端使用;一份不包含服务对象实现,供客户端使用。我们把不包含实现的RemoteObject版本交给客户端就可以了。(具体实现略)
4.1.3使用Soapsuds.exe分离程序集(本系统中使用的方法)
Soapsuds.exe是.Net所提供的一个工具,它允许客户端通过输入一个远程对象的Url,然后生成一个dll程序集文件,这个文件包含了在客户端创建代理的全部信息但是不包含具体的实现。客户端引用此生成的文件,就如同引用服务程序集一样(你可以将这个文件视为上一小节的“空类”程序集)。因为这个程序集不包含服务对象的实现代码,于是也就达到了向客户端隐藏服务程序实现的目的。需要注意的是,使用Soapsuds.exe获取dll文件时,服务端必须注册为服务激活对象(SingleCall或者Singleton)。在获得dll文件以后,不管服务器使用那种协议或者格式,客户都可以访问远程对象。
简单地说,就是产生程序集的元数据,而这主要使用在Remoting架构中。在Remoting中,为了达到客户端和服务端对远程对象元数据的分离,使用SoapSuds工具产生远程对象的元数据,这样在客户端就不用引用远程对象的程序集了。本系统中使用Soapsuds 工具方法执行.\MES\BussinessRule\bin\debug\ buildServer1.bat文件
其中buildServer1.bat内容soapsuds -ia:BussinessRule -oa:BussinessRuleProxy.dll
参数说明如下:-ia: assemblyfile 指定输入程序集文件。该工具导入程序集中的所有类型。当您指定输入程序集时,不要包括 .exe 或 .dll 扩展名。
-oa: assemblyfile 将输出保存到指定的程序集文件。Soapsuds.exe 在生成程序集时始终生成源代码。
具体实现:
(1)利用先前我们的程序,我们已经创建了Server两个控制台程序和一个RemoteObject类库程序。其中Server和RemoteObject部署在另一台计算机(IP:192.9.110.106)现在我们用Soapsuds 工具来生成客户端要使用的dll文件。打开VS2008 命令提示符,然后在其下输入命令:D:\>soapsuds -url:http://192.9.110.106:8502/ServerActivated?wsdl -oa:ClientProxy.dll 运行命令成功后进入D盘根目录可以看到生成的代理Dll文件ClientProxy.dll。
(2)在客户端添加刚才生成的dll文件。重新编译运行。双击Client.exe客户端输出正确的结果。
4.2 数据传输安全
4.2.1 密文传输
在未进行任何安全防范措施的情况下,能不能截获从客户端发送到客户端(或客户端发送到服务器端)的数据呢?我们用抓包工具(WireShark)查看一下。
测试用例:客户端IP: 192.9.111.73 调用方法:RunTest("sunqiang") 数据:sunqiang
服务端IP:192.9.110.106 使用抓包工具截获的数据如下图(图七)所示:
(图七)
实际的数据如下图(图八)所示:
(图八)
由上图(图四)可以很明显的看到服务器向客户端传递的数据为:“My Name is sunqiang”。这样数据可以很容易的被人截获。
解决方法:把客户端发送数据时加密,调用远程对象端调用方法时解密。然后再加密传回客户端,在客户端解密(加密,解密代码略)。
再次使用抓包工具(WireShark)查看如下图(图九,图十)所示,截获的数据的加密后的。
(图九)
(图十)
(图十一)
由图十一可以看出在客户端解密后数据显示正常。