一步一步学Remoting之一:从简单开始
一、Remoting的优缺点?
优点:
1、能让我们进行分布式开发
2、Tcp通道的Remoting速度非常快
3、虽然是远程的,但是非常接近于本地调用对象
4、可以做到保持对象的状态
5、没有应用程序限制,可以是控制台,winform,iis,windows服务承载远程对象
缺点:
1、非标准的应用因此有平台限制
2、脱离iis的话需要有自己的安全机制
二、Remoting和Web服务的区别?
ASP.NET Web 服务基础结构通过将 SOAP 消息映射到方法调用,为 Web 服务提供了简单的 API。通过提供一种非常简单的编程模型(基于将 SOAP 消息交换映射到方法调用),它实现了此机制。ASP.NET Web 服务的客户端不需要了解用于创建它们的平台、对象模型或编程语言。而服务也不需要了解向它们发送消息的客户端。唯一的要求是:双方都要认可正在创建和使用的 SOAP 消息的格式,该格式是由使用 WSDL 和 XML 架构 (XSD) 表示的 Web 服务合约定义来定义的。
. NET Remoting 为分布式对象提供了一个基础结构。它使用既灵活又可扩展的管线向远程进程提供 .NET 的完全对象语义。ASP.NET Web 服务基于消息传递提供非常简单的编程模型,而 .NET Remoting 提供较为复杂的功能,包括支持通过值或引用传递对象、回调,以及多对象激活和生命周期管理策略等。要使用 .NET Remoting,客户端需要了解所有这些详细信息,简而言之,需要使用 .NET 建立客户端。.NET Remoting 管线还支持 SOAP 消息,但必须注意这并没有改变其对客户端的要求。如果 Remoting 端点提供 .NET 专用的对象语义,不管是否通过 SOAP,客户端必须理解它们。
三、最简单的Remoting的例子
1、远程对象:
建立类库项目:RemoteObject
namespace RemoteObject
{
public class MyObject:MarshalByRefObject
{
public int Add( int a, int b)
{
return a + b;
}
}
}
2、服务端
建立控制台项目:RemoteServer
using System.Runtime.Remoting;
namespace RemoteServer
{
class MyServer
{
[STAThread]
static void Main( string [] args)
{
RemotingConfiguration.Configure( " RemoteServer.exe.config " );
Console.ReadLine();
}
}
}
建立配置文件:app.config
< system .runtime.remoting >
< application name ="RemoteServer" >
< service >
< wellknown type ="RemoteObject.MyObject,RemoteObject" objectUri ="RemoteObject.MyObject"
mode ="Singleton" />
</ service >
< channels >
< channel ref ="tcp" port ="9999" />
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
3、客户端:
建立控制台项目:RemoteClient
namespace RemoteClient
{
class MyClient
{
[STAThread]
static void Main( string [] args)
{
RemoteObject.MyObject app = (RemoteObject.MyObject)Activator.GetObject( typeof (RemoteObject.MyObject),System.Configuration.ConfigurationSettings.AppSettings[ " ServiceURL " ]);
Console.WriteLine(app.Add( 1 , 2 ));
Console.ReadLine();
}
}
}
建立配置文件:app.config
一步一步学Remoting之二:激活模式
远程对象的激活模式分服务端激活和客户端激活两种,(也就是对象分服务端激活对象或者说是知名对象和客户端激活对象两种)先看看msdn怎么描述服务端激活的:
服务器激活的对象是其生存期由服务器直接控制的对象。服务器应用程序域只有在客户端在对象上进行方法调用时才创建这些对象,而不会在客户端调用 new 或 Activator.GetObject 时创建这些对象;这节省了仅为创建实例而进行的一次网络往返过程。客户端请求服务器激活的类型实例时,只在客户端应用程序域中创建一个代理。然而,这也意味着当您使用默认实现时,只允许对服务器激活的类型使用默认构造函数。若要发布其实例将使用带参数的特定构造函数创建的类型,可以使用客户端激活或者动态地发布您的特定实例。
一步一步学Remoting之四:承载方式(1)
在实际的应用中我们通常只会选择用windows服务和iis来承载远程对象。选择windows服务的原因是能自启动服务,服务器重启后不需要再去考虑启动service。选择iis的理由是我们能使用集成验证等一些iis的特性。
在msdn中可以找到相关文章:
http://www.microsoft.com/china/msdn/library/architecture/architecture/architecturetopic/BuildSucApp/BSAAsecmodsecmod29.mspx
http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/cpguide/html/cpconRemotingExampleHostingInIIS.asp
可能大家会觉得这个过程将是一个复杂的过程,其实不然,下面说一下实现方法,步骤非常少。
先来建立远程对象
一步一步学Remoting之四:承载方式(2)
这里来说一下iis承载方式,顺便简单说一下remoting的通道和【复杂对象】中的遗留问题。
首先明确一点:iis来承载的话只能是http通道方式的。
我们来建立一个web项目,比如叫remoting,删除项目中的所有webform,把远程对象dll-RemoteObject.dll复制到项目的dll文件夹下面,然后打开web.config进行服务端设置:
< appSettings >
< add key ="ServiceURL" value ="tcp://localhost:9999/RemoteObject.MyObject" />
</ appSettings >
</ configuration >
4、测试
在最后编译的时候会发现编译报错:
1、找不到app.Add()
2、找不到RemoteObject
这是因为客户端RemoteClient没有添加RemoteObject的引用,编译器并不知道远程对象存在哪些成员所以报错,添加引用以后vs.net会在客户端也保存一个dll,可能大家会问这样如果对远程对象的修改是不是会很麻烦?其实不麻烦,对项目编译一次vs.net会重新复制dll。
然后直接运行客户端会出现“目标主机拒绝”的异常,也说明了通道没有打开
运行服务端再运行客户端出现“找不到程序集RemoteObject”!回头想想可以发现我们并在服务端对RemoteObject添加引用,编译的时候通过是因为这个时候并没有用到远程对象,大家可能不理解运行服务端的时候也通过?这是因为没有这个时候还没有激活远程对象。理所当然,对服务端要添加引用远程对象,毕竟我们的对象是要靠远程承载的。
现在再先后运行服务端程序和客户端程序,客户端程序显示3,测试成功。
四、结束语
我们通过一个简单的例子实现了最简单的remoting,对其实质没有做任何介绍,我想通过例子入门才是最简单的。
服务器激活的对象有两种激活模式(或 WellKnownObjectMode 值):Singleton 和 SingleCall。
Singleton 类型任何时候都不会同时具有多个实例。如果存在实例,所有客户端请求都由该实例提供服务。如果不存在实例,服务器将创建一个实例,而所有后继的客户端请求都将由该实例来提供服务。由于 Singleton 类型具有关联的默认生存期,即使任何时候都不会有一个以上的可用实例,客户端也不会总接收到对可远程处理的类的同一实例的引用。
SingleCall 远程服务器类型总是为每个客户端请求设置一个实例。下一个方法调用将改由其他实例进行服务。从设计角度看,SingleCall 类型提供的功能非常简单。这种机制不提供状态管理,如果您需要状态管理,这将是一个不利之处;如果您不需要,这种机制将非常理想。也许您只关心负载平衡和可伸缩性而不关心状态,那么在这种情况下,这种模式将是您理想的选择,因为对于每个请求都只有一个实例。如果愿意,开发人员可以向 SingleCall 对象提供自己的状态管理,但这种状态数据不会驻留在对象中,因为每次调用新的方法时都将实例化一个新的对象标识。
首先对于服务端激活的两种模式来做一个试验,我们把远程对象做如下的修改:
namespace RemoteObject
{
public class MyObject:MarshalByRefObject
{
private int i = 0 ;
public int Add( int a, int b)
{
return a + b;
}
public int Count()
{
return ++ i;
}
}
}
对客户端做以下修改:
Console.WriteLine(app.Count());
Console.ReadLine();
第一次打开客户端的时候显示1,第二次打开的时候显示2,类推……由此验证了Singleton 类型任何时候都不会同时具有多个实例。如果存在实例,所有客户端请求都由该实例提供服务。如果不存在实例,服务器将创建一个实例,而所有后继的客户端请求都将由该实例来提供服务。
把服务器端的config修改一下:
mode ="SingleCall" />
(这里注意大小写,大写的C)
再重新运行服务端和客户端,打开多个客户端发现始终显示1。由此验证了SingleCall 类型对于每个客户端请求都会重新创建实例。下一个方法调用将由另一个服务器实例提供服务。
下面再说一下客户端的激活模式,msdn中这么写:
客户端激活的对象是其生存期由调用应用程序域控制的对象,正如对象对于客户端是本地对象时对象的生存期由调用应用程序域控制一样。对于客户端激活,当客户端试图创建服务器对象的实例时发生一个到服务器的往返过程,而客户端代理是使用对象引用 (ObjRef) 创建的,该对象引用是从在服务器上创建远程对象返回时获取的。每当客户端创建客户端激活的类型的实例时,该实例都将只服务于该特定客户端中的特定引用,直到其租约到期并回收其内存为止。如果调用应用程序域创建两个远程类型的新实例,每个客户端引用都将只调用从其中返回引用的服务器应用程序域中的特定实例。
理解一下,可以归纳出
1、客户端激活的时间是在客户端请求的时候,而服务端激活远程对象的时间是在调用对象方法的时候
远程对象修改如下:
namespace RemoteObject
{
public class MyObject:MarshalByRefObject
{
private int i = 0 ;
public MyObject()
{
Console.WriteLine( " 激活 " );
}
public int Add( int a, int b)
{
return a + b;
}
public int Count()
{
return ++ i;
}
}
}
服务端配置文件:
< system .runtime.remoting >
< application name ="RemoteServer" >
< service >
< activated type ="RemoteObject.MyObject,RemoteObject" />
</ service >
< channels >
< channel ref ="tcp" port ="9999" />
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
客户端程序:
namespace RemoteClient
{
class MyClient
{
[STAThread]
static void Main( string [] args)
{
// RemoteObject.MyObject app = (RemoteObject.MyObject)Activator.GetObject(typeof(RemoteObject.MyObject),System.Configuration.ConfigurationSettings.AppSettings["ServiceURL"]);
RemoteObject.MyObject app = (RemoteObject.MyObject)Activator.CreateInstance( typeof (RemoteObject.MyObject), null , new object []{ new System.Runtime.Remoting.Activation.UrlAttribute(System.Configuration.ConfigurationSettings.AppSettings[ " ServiceURL " ])});
// Console.WriteLine(app.Count());
Console.ReadLine();
}
}
}
客户端配置文件:
< appSettings >
< add key ="ServiceURL" value ="tcp://localhost:9999/RemoteServer" />
</ appSettings >
</ configuration >
(这里的uri按照服务端配置文件中application元素定义的RemoteServer来写)
运行程序可以看到,在客户端启动的时候服务端就输出了“激活”,我们再转回知名模式进行测试发现只有运行了方法才会在服务端输出“激活”。
2、客户端激活可以调用自定义的构造方法,而不像服务端激活只能使用默认的构造方法
把客户端代码修改如下:
Console.WriteLine(app.Count());
这里看到我们在CreateInstance方法的第二个参数中提供了10作为构造方法的参数。在服务端激活模式我们不能这么做。
远程对象构造方法修改如下:







毫无疑问,我们运行客户端发现输出的是11而不是1了。
3、通过上面的例子,我们运行多个客户端发现出现的永远是11,因此,客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。
总结:
1、Remoting支持两种远程对象:知名的和客户激活的。知名的远程对象使用了uri作为标识,客户程序使用这个uri来访问那些远程对象,也正式为什么称作知名的原因。对知名的对象来说2种使用模式:SingleCall和Singleton,对于前者每次调用都会新建对象,因此对象是无状态的。对于后者,对象只被创建一次,所有客户共享对象状态,因此对象是有状态的。另外一种客户端激活对象使用类的类型来激活,uri再后台被动态创建,并且返回给客户程序。客户激活对象是有状态的。
2、对于Singleton对象来说需要考虑伸缩性,Singleton对象不能在多个服务器上被部署,如果要跨服务器就不能使用Singleton了。
备注:个人习惯原因,在我的例子中服务端的配置都是用config文件的,客户端的配置都是基本用程序方式的
使用配置文件的优点:无需重新编译就可以配置通道和远程对象,编写的代码量比较少
使用程序定制的优点:可以获得运行期间的信息,对程序调试有利。
一步一步学Remoting之三:复杂对象
这里说的复杂对象是比较复杂的类的实例,比如说我们在应用中经常使用的DataSet,我们自己的类等,通常我们会给远程的对象传递一些自己的类,或者要求对象返回处理的结果,这个时候通常也就是需要远程对象有状态,上次我们说了几种激活模式提到说只有客户端激活和Singleton是有状态的,而客户端激活和Singleton区别在于Singleton是共享对象的。因此我们可以选择符合自己条件的激活方式:
状态 拥有各自实例
Singleton 有 无
SingleCall 无 有
客户端激活 有 有
在这里,我们先演示自定义类的传入传出:
先说一个概念:MBV就是按值编码,对象存储在数据流中,用于在网络另外一端创建对象副本。MBR就是按引用编组,在客户机上创建代理,远程对象创建ObjRef实例,实例被串行化传递。
我们先来修改一下远程对象:
namespace RemoteObject
{
public class MyObject:MarshalByRefObject
{
private MBV _mbv;
private MBR _mbr;
public int Add( int a, int b)
{
return a + b;
}
public MBV GetMBV()
{
return new MBV( 100 );
}
public MBR GetMBR()
{
return new MBR( 200 );
}
public void SetMBV(MBV mbv)
{
this ._mbv = mbv;
}
public int UseMBV()
{
return this ._mbv.Data;
}
public void SetMBR(MBR mbr)
{
this ._mbr = mbr;
}
public int UseMBR()
{
return this ._mbr.Data;
}
}
[Serializable]
public class MBV
{
private int _data;
public MBV( int data)
{
this ._data = data;
}
public int Data
{
get
{
return this ._data;
}
set
{
this ._data = value;
}
}
}
public class MBR:MarshalByRefObject
{
private int _data;
public MBR( int data)
{
this ._data = data;
}
public int Data
{
get
{
return this ._data;
}
set
{
this ._data = value;
}
}
}
}
using System.Data;
using System.Data.SqlClient;
namespace RemoteObject
{
public class MyObject:MarshalByRefObject
{
public DataSet GetData()
{
SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings[ " strconn " ]);
SqlDataAdapter da = new SqlDataAdapter( " select * from UBI_ProvinceMaster " ,conn);
DataSet ds = new DataSet();
da.Fill(ds);
return ds;
}
}
}
客户端仍然是一个控制台来进行测试:
DataTable dt = app.GetData().Tables[ 0 ];
foreach (DataRow dr in dt.Rows)
{
Console.WriteLine(dr[ " iPrMId " ] + " " + dr[ " vPrMName " ]);
}
Console.ReadLine();
服务端配置文件:
< appSettings >
< add key ="strconn" value ="server=(local);uid=sa;pwd=;database=UBISOFT" />
</ appSettings >
< system .runtime.remoting >
< application name ="RemoteServer" >
< service >
< wellknown type ="RemoteObject.MyObject,RemoteObject" objectUri ="RemoteObject.MyObject"
mode ="SingleCall" />
</ service >
< channels >
< channel ref ="tcp" port ="9999" />
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
运行程序,我们得到的是一个省市的列表:
一、windows服务承载
用vs.net制作一个windows服务的过程基本不超过10个步骤,所以我们不需要害怕。
1、建立一个新的windows服务项目RemoteServer1
2、打开Service1代码视图,找到OnStart部分,加入代码
(不要遗漏AppDomain.CurrentDomain.BaseDirectory + )
config和控制台方式的config是一样的,我们让这个windows服务做的仅仅是从config文件读出配置信息进行配置通道。别忘记添加配置文件。
3、切换到设计视图,右键-添加安装程序
4、切换到新生成的ProjectInstaller.cs设计视图,找到serviceProcessInstaller1对Account属性设置为LocalSystem,对serviceInstaller1的ServiceName属性设置为RemoteServer1(服务的名字),StartType属性设置为Automatic(系统启动的时候自动启动服务)
5、别忘记对添加RemoteObject的引用
6、建立一个新的安装项目RemoteServerSetup(我们为刚才那个服务建立一个安装项目)
7、右键-添加-项目输出-主输出-选择RemoteService1-确定
8、右键-视图-自定义操作-自定义操作上右键-添加自定义操作-打开应用程序文件夹-选择刚才那个主输出-确定
9、重新生成这个安装项目-右键-安装
10、在服务管理器中(我的电脑-右键-管理-服务和应用程序-服务)找到RemoteServer1服务,启动服务
现在就可以打开客户端测试了!
一些FAQ:
1、启动服务的时候系统说了类似“服务什么都没有做,服务已经被停止”表示什么?
表示windows服务出错了,一般是服务的程序有问题,检查服务做了什么?在我们这个程序中仅仅添加了一行代码,一般不会出现这个错误。
2、运行客户端出现“服务器无响应”?
先检查windows服务配置文件是不是正确设置了激活方式和激活对象,客户端服务端端口号是否统一?
3、运行客户端出现“无法找到程序集”?
检查windows服务配置文件是否正确配置了激活对象的类型和uri?服务是否添加了远程对象引用?
4、远程对象类中有用到System.Configuration.ConfigurationSettings.AppSettings["strconn"],但是远程对象并没有配置文件,它从哪里读取这个config的?
因为远程对象不是独立存在的,它是被windows服务承载的,因此它从windows服务的配置文件中读取一些配置信息,远程对象本生不需要配置文件。
5、安装的时候是不是要卸载服务?
不需要,安装程序会 停止服务端-》卸载服务-》安装服务
6、在正式使用的时候怎么部署我们的系统?
如果客户端是程序仅仅只要把安装项目下面3个文件传到服务器进行安装,配置好config文件(比如连接字符串),开启服务即可。如果客户端是网站,同样把服务在服务器安装,配置好config文件(比如连接字符串),开启服务,最后把网站传到web服务器(可能和service不是同一个服务器)。
7、部署的时候需要传远程对象dll吗?
不需要,可以看到安装项目中已经自动存在了这个dll。
8、这样的系统有什么特点?
一个web服务器,多个service服务器,多个sqlservice服务器,web服务器负担比较小,所有的逻辑代码都分布到不同的service服务器上面。
最后说一个测试的tip:
如果我们远程调用对象进行测试程序非常麻烦,我们需要这么做
修改了远程对象-》重新编译安装程序-》在自己机器重新安装服务-》启动服务-》查看结果
其实可以这么做:
1、修改远程对象中的连接数据库字符串,由于不是远程对象了,我们必须从本地读取连接字符串,比如上列我们直接修改为:
SqlConnection conn=new SqlConnection("server=(local);uid=sa;pwd=;database=UBISOFT");
2、修改客户端代码,直接实例化远程对象
RemoteObject.MyObject app = new RemoteObject.MyObject();
等到正式部署的时候我们还原数据库连接字符串从config文件中读取,还原远程对象从远程读取即可。
如果对windows服务还不是很清楚,请看以下文章:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbwlkwalkthroughcreatingwindowsserviceapplication.asp
http://www.cnblogs.com/lovecherry/archive/2005/03/25/125527.html
< appSettings >
< add key ="strconn" value ="server=(local);uid=sa;pwd=;database=UBISOFT" />
</ appSettings >
< system .runtime.remoting >
< application >
< service >
< wellknown type ="RemoteObject.MyObject,RemoteObject" objectUri ="MyObject.soap"
mode ="SingleCall" />
</ service >
< channels >
< channel ref ="http" />
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
来分析一下这个config:
1、可能大家还不是很理解type属性,其实type属性分两部分<命名空间.类名>,<程序集>
2、objectURi是用来表示对象的uri的,到时候我们用这个uri来连接到服务端
3、我们需要为uri指定soap(soap格式化)或者rem(二进制格式化)后缀
要进行测试其实很简单,我们在浏览器输入:http://localhost/remoting/MyObject.soap?wsdl
进行测试,如果发生问题基本就是配置文件的问题或者对象dll没有正确复制到dll目录
接下来修改一下客户端的配置文件就可以了,主要是修改地址。
< appSettings >
< add key ="ServiceURL" value ="http://localhost/remoting/MyObject. soap"/>
</ appSettings >
</ configuration >
iis承载方式默认是80端口,我们不需要在端口上做任何设置。还需要注意到的是iis方式,我们使用这样的格式作为地址:
http://ip地址/虚拟目录/远程对象.soap
运行了客户端以后如果我们的数据量比较大的话,就算是本机我们也能感受到延迟,比tcp方式延迟厉害很多很多,其实http方式的remoting效率比webservice还要差,具体选择http方式的remoting还是webservice还是要看我们是不是对对象的状态有需求。
iis的部署也是自动启动服务的,还有一个优点就是可以结合iis的windows身份认证,这个参照一些iis的配置文章,这里就不说了。
下面还是要来看一下两种【通道】:
默认情况下,HTTP 通道使用 SOAP 格式化程序,因此,如果客户端需要通过 Internet 访问对象,则可以使用 HTTP 通道。由于这种方法使用 HTTP,所以允许客户端通过防火墙远程访问 .NET 对象。将这些对象集成在 IIS 中,即可将其配置为 Web 服务对象。随后,客户端就可以读取这些对象的 WSDL 文件,以便使用 SOAP 与 Remoting 对象通信。
默认情况下,TCP 通道使用二进制格式化程序。此格式化程序以二进制格式进行数据的序列化,并使用原始套接字在网络中传送数据。如果对象部署在受防火墙保护的封闭环境中,则此方法是理想的选择。该方法使用套接字在对象之间传递二进制数据,因此性能更好。由于它使用 TCP 通道来提供对象,因此具有在封闭环境中开销较小的优点。由于防火墙和配置问题,此方法不能在 Internet 上使用。
因此我们也需要更根据自己的需求来选择通道!看看remoting有这么多可以选择的方式:选择激活模式,选择通道,选择承载方式,如此多的选择给了我们灵活的同时也增加了理解remoting的难度。
msdn相关章节:http://msdn.microsoft.com/library/CHS/cpguide/html/cpconChannels.asp
最后说一下前面的遗留问题,为什么会发生这个安全异常?
http://www.cnblogs.com/lovecherry/archive/2005/05/20/159335.html
msdn说:
依赖于运行时类型验证的远程处理系统必须反序列化一个远程流,然后才能开始使用它,未经授权的客户端可能会试图利用反序列化这一时机。为了免受这种攻击,.NET 远程处理提供了两个自动反序列化级别:Low 和 Full。Low(默认值)防止反序列化攻击的方式是,在反序列化时,只处理与最基本的远程处理功能关联的类型,如自动反序列化远程处理基础结构类型、有限的系统实现类型集和基本的自定义类型集。Full 反序列化级别支持远程处理在所有情况下支持的所有自动反序列化类型。
我们首先来修改服务端的配置文件:
< system .runtime.remoting >
< application name ="RemoteServer" >
< service >
< wellknown type ="RemoteObject.MyObject,RemoteObject" objectUri ="RemoteObject.MyObject"
mode ="Singleton" />
</ service >
< channels >
< channel ref ="tcp" port ="9999" />
< serverProviders >
< provider ref ="wsdl" />
< formatter ref ="soap" typeFilterLevel ="Full" />
< formatter ref ="binary" typeFilterLevel ="Full" />
</ serverProviders >
</ channels >
</ application >
</ system.runtime.remoting >
</ configuration >
当然也可以用程序进行设置:






BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props[ " port " ] = 9999 ;
TcpChannel channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelServices.RegisterChannel(channel);
Console.ReadLine();
客户端还要用程序进行调整:
若要使用配置文件设置反序列化级别,必须显式指定 <formatter> 元素的 typeFilterLevel 属性。虽然这通常是在服务器端指定的,但您还必须为注册来侦听回调的客户端上的任何信道指定这一属性,以控制其反序列化级别
在程序前面加上和服务端基本相同的代码:
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props[ " port " ] = 0 ;
TcpChannel channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelServices.RegisterChannel(channel);
这样就可以了,注意:如果在同一个机器上面测试端口号应设为不同于服务器端设置的端口号,推荐设置为0(远程处理系统自动选择可用端口)
.NET Remoting 自身不提供安全模型。然而,通过将远程对象驻留在 ASP.NET 中并使用 HTTP 通道进行通信,远程对象可以使用 IIS 和 ASP.NET 提供的基本安全服务。比较而言,TCP 通道和自定义的主机可执行文件能够提供更高的性能,但这种组合不提供内置的安全功能。
• 若要对客户端进行身份验证,请使用 HTTP 通道,在 ASP.NET 中驻留对象,以及在 IIS 中禁用匿名访问。
• 如果您不担心客户端身份验证问题,请使用 TCP 通道,它可以提供更高的性能。
• 如果您使用 TCP 通道,请使用 IPSec 保护客户端和服务器之间的通信通道。使用 SSL 来保护 HTTP 通道。
• 如果您需要对远程资源进行受信任的调用,请将组件驻留在 Windows 服务中,而不是驻留在控制台应用程序中。
• 始终不要向 Internet 公开远程对象。在这种情况下,请使用 Web 服务。
应该仅在 Intranet 中使用 .NET Remoting。应该使用内部方式从 Web 应用程序访问对象。即使对象驻留在 ASP.NET 中,也不要向 Internet 客户端公开它们,因为客户端必须是 .NET 客户端。
最后,让我们来看一篇msdn有关remoting安全的文章:
http://www.microsoft.com/china/msdn/library/architecture/architecture/architecturetopic/BuildSucApp/BSAAsecmod11.mspx
一步一步学Remoting之五:异步操作
如果你还不知道什么是异步也不要紧,我们还是来看实例,通过实例来理解才是最深刻的。
在Remoting中,我们可以使用以下几种异步的方式:
1、普通异步
2、回调异步
3、单向异步
一个一个来说,首先我们这么修改我们的远程对象:
{
Console.WriteLine( " 异步方法开始 " );
System.Threading.Thread.Sleep(time);
Console.WriteLine( " 异步方法结束 " );
return a + b;
}
这个方法传入2个参数,返回2个参数和表示方法执行成功,方法需要time毫秒的执行时间,这是一个长时间的方法。
如果方法我们通过异步远程调用,这里需要注意到这个方法输出的行是在服务器端输出的而不是客户端。因此,为了测试简单,我们还是在采用本地对象,在实现异步前我们先来看看同步的调用方法,为什么说这是一种阻塞?因为我们调用了方法主线程就在等待了,看看测试:
RemoteObject.MyObject app = new RemoteObject.MyObject();
Console.WriteLine(app.ALongTimeMethod( 1 , 2 , 1000 ));
Method();
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
Console.ReadLine();
假设method方法是主线程的方法,需要3秒的时间:
{
Console.WriteLine( " 主线程方法开始 " );
System.Threading.Thread.Sleep( 3000 );
Console.WriteLine( " 主线程方法结束 " );
}
好了,现在开始运行程序:
用了4秒,说明在我们的方法开始以后本地就一直在等待了,总共用去的时间=本地方法+远程方法,对于长时间方法调用这显然不科学!我们需要改进:
1、普通异步:
首先在main方法前面加上委托,签名和返回类型和异步方法一致。
main方法里面这么写:
RemoteObject.MyObject app = new RemoteObject.MyObject();
MyDelegate md = new MyDelegate(app.ALongTimeMethod);
IAsyncResult Iar = md.BeginInvoke( 1 , 2 , 1000 , null , null );
Method();
if ( ! Iar.IsCompleted)
{
Iar.AsyncWaitHandle.WaitOne();
}
else
{
Console.WriteLine( " 结果是 " + md.EndInvoke(Iar));
}
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
Console.ReadLine();
来看一下执行结果:
现在总共执行时间接近于主线程的执行时间了,等于是调用方法基本不占用时间。
分析一下代码:Iar.AsyncWaitHandle.WaitOne(); 是阻塞等待异步方法完成,在这里这段代码是不会被执行的,因为主方法完成的时候,异步方法早就IsCompleted了,如果我们修改一下代码:IAsyncResult Iar=md.BeginInvoke(1,2,5000,null,null);
可以看到,主线程方法结束后就在等待异步方法完成了,总共用去了接近于异步方法的时间:5秒。
在实际的运用中,主线程往往需要得到异步方法的结果,也就是接近于上述的情况,我们在主线程做了少量时间的工作以后最终要是要WaitOne去等待异步操作返回的结果,才能继续主线程操作。看第二个图可以发现,异步操作仅仅用了1秒,但是要等待3秒的主线程方法完成后再返回结果,这还是不科学啊。因此,我们要使用委托回调的异步技术。
2、回调异步:
{
private delegate int MyDelegate( int a, int b, int time);
private static MyDelegate md;
[STAThread]
static void Main( string [] args)
{
DateTime dt = DateTime.Now;
// RemoteObject.MyObject app=(RemoteObject.MyObject)Activator.GetObject(typeof(RemoteObject.MyObject),System.Configuration.ConfigurationSettings.AppSettings["ServiceURL"]);
RemoteObject.MyObject app = new RemoteObject.MyObject();
md = new MyDelegate(app.ALongTimeMethod);
AsyncCallback ac = new AsyncCallback(MyClient.CallBack);
IAsyncResult Iar = md.BeginInvoke( 1 , 2 , 1000 ,ac, null );
Method();
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
Console.ReadLine();
}
public static void CallBack(IAsyncResult Iar)
{
if (Iar.IsCompleted)
{
Console.WriteLine( " 结果是 " + md.EndInvoke(Iar));
}
}
private static void Method()
{
Console.WriteLine( " 主线程方法开始 " );
System.Threading.Thread.Sleep( 3000 );
Console.WriteLine( " 主线程方法结束 " );
}
}
可以看到我上面的注释行,去掉远程调用的注释,对下面的本地调用注释,编译后启动服务端,再启动客户端就是远程调用了。
异步调用结束,立即就能显示出结果,如果开启远程方法的话,可以看的更加清晰:
客户端:主线程方法开始-》服务端:异步方法开始-》服务端:异步方法结束-》客户端:结果是3-》客户端:主线程方法结束-》客户端:用了3.03125秒。
3、单向异步就是像同步调用方法那样调用方法,方法却是异步完成的,但是不能获得方法的返回值而且不能像同步方法那样取得所调用方法的异常信息!对于不需要返回信息的长时间方法,我们可以放手让它去干就行了:
远程对象:
using System.Runtime.Remoting.Messaging;
namespace RemoteObject
{
public class MyObject:MarshalByRefObject
{
[OneWay]
public void ALongTimeMethodOneWay( int time)
{
Console.WriteLine( " 异步方法开始 " );
System.Threading.Thread.Sleep(time);
Console.WriteLine( " 异步方法结束 " );
}
}
}
[OneWay]属性是Remoting.Messaging的一部分,别忘记using,下面看看客户端代码:
namespace RemoteClient
{
class MyClient
{
[STAThread]
static void Main( string [] args)
{
DateTime dt = DateTime.Now;
RemoteObject.MyObject app = (RemoteObject.MyObject)Activator.GetObject( typeof (RemoteObject.MyObject),System.Configuration.ConfigurationSettings.AppSettings[ " ServiceURL " ]);
// RemoteObject.MyObject app=new RemoteObject.MyObject();
app.ALongTimeMethodOneWay( 1000 );
Method();
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
Console.ReadLine();
}
private static void Method()
{
Console.WriteLine( " 主线程方法开始 " );
System.Threading.Thread.Sleep( 3000 );
Console.WriteLine( " 主线程方法结束 " );
}
}
}
这次我们仅仅只能在远程调试,我们先让异步方法去做,然后就放心的做主线程的事情,其他不管了。
运行结果我描述一下:
客户端:主线程方法开始-》服务端:异步方法开始-》服务端:异步方法结束-》客户端:主线程方法结束-》客户端:用了3.8秒。
上面说的三种方法,只是异步编程的一部分,具体怎么异步调用远程方法要结合实际的例子,看是否需要用到方法的返回和主线程方法的运行时间与远程方法运行时间等结合起来考虑,比如上述的WaitHandle也可以用轮询来实现:
while(Iar.IsCompleted==false) System.Threading.Thread.Sleep(10);总的来说远程对象的异步操作和本地对象的异步操作是非常接近。
还可以参考msdn相关文章:
http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/cpguide/html/cpconasynchronousprogrammingdesignpattern2.asp
一步一步学Remoting之六:事件(1)
周末又过去了要上班了,一大早起来继续写。
概念就不说了,具体参见msdn相关章节:
http://msdn.microsoft.com/library/CHS/cpguide/html/cpconEvents.asp
我们先来改造一下上次的程序,为上次的主线程方法添加事件,能不断的引发事件来汇报处理的进度:
{
private int _rate;
public int Rate
{
get
{
return _rate;
}
}
public MyEventArgs( int rate)
{
this ._rate = rate;
}
}
public class MyObject
{
public delegate void MyEventHandler( object sender,MyEventArgs e);
public event MyEventHandler MyEvent;
public void ALongTimeMethod( int time)
{
Console.WriteLine( " 主线程方法开始 " );
for ( int i = 0 ;i < 100 ;i ++ )
{
System.Threading.Thread.Sleep(time);
OnMyEvent( new MyEventArgs(i));
}
Console.WriteLine( " 主线程方法结束 " );
}
protected void OnMyEvent(MyEventArgs e)
{
if (MyEvent != null )
{
MyEvent( this ,e);
}
}
}
再来为事件添加处理程序:
{
[STAThread]
static void Main( string [] args)
{
DateTime dt = DateTime.Now;
MyObject obj = new MyObject();
obj.MyEvent += new MyObject.MyEventHandler(obj_MyEvent);
obj.ALongTimeMethod( 50 );
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
Console.ReadLine();
}
public static void obj_MyEvent( object sender,MyEventArgs e)
{
Console.WriteLine( " 主线程方法完成了 " + e.Rate + " % " );
}
}
运行程序可以看到:
这个是本地的,远程对象的事件也这么简单吗?其实没有想象的简单,因为对象是在远程的,服务端的事件客户端怎么捕捉?应该说远程对象的事件可以分成客户端触发-》服务器应答,服务端触发-》客户端应答和客户端触发-》客户端应答,第一种就很简单了,后面2种都需要有一个中间件。下面我们来要为程对象同样来添加一个进度机制,首先来建立我们的远程对象:
public class MyEventArgs:EventArgs
{
private int _rate;
private string _ip;
public int Rate
{
get
{
return _rate;
}
}
public string IP
{
get
{
return _ip;
}
}
public MyEventArgs( int rate, string ip)
{
this ._rate = rate;
this ._ip = ip;
}
}
public class MyObject:MarshalByRefObject
{
public delegate void MyEventHandler( object sender,MyEventArgs e);
public event MyEventHandler MyEvent;
public int ALongTimeMethod( int a, int b, int time, string ip)
{
Console.WriteLine( " 异步方法开始 " );
for ( int i = 0 ;i < 10 ;i ++ )
{
System.Threading.Thread.Sleep(time);
OnMyEvent( new MyEventArgs(i,ip));
}
Console.WriteLine( " 异步方法结束 " );
return a + b;
}
protected void OnMyEvent(MyEventArgs e)
{
if (MyEvent != null )
{
MyEvent( this ,e);
}
}
}
为了调试方便,服务器端和客户端这次都用程序实现,下面是服务器端:
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
namespace RemoteServer
{
class MyServer
{
[STAThread]
static void Main( string [] args)
{
RemotingConfiguration.RegisterWellKnownServiceType( typeof (RemoteObject.MyObject), " RemoteObject.MyObject " ,WellKnownObjectMode.Singleton);
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props[ " port " ] = 9999 ;
TcpChannel channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelServices.RegisterChannel(channel);
Console.ReadLine();
}
}
}
客户端为了简单一点,我去除了前面做测试的本地事件:
using System.Net;
using System.Collections;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
class MyClient
{
private delegate int MyDelegate( int a, int b, int time, string ip);
private static MyDelegate md;
[STAThread]
static void Main( string [] args)
{
DateTime dt = DateTime.Now;
RemotingConfiguration.RegisterWellKnownClientType( typeof (RemoteObject.MyObject), " tcp://localhost:9999/RemoteObject.MyObject " );
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props[ " port " ] = 0 ;
TcpChannel channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelServices.RegisterChannel(channel);
RemoteObject.MyObject app = new RemoteObject.MyObject();
app.MyEvent += new RemoteObject.MyObject.MyEventHandler(MyEvent);
md = new MyDelegate(app.ALongTimeMethod);
AsyncCallback ac = new AsyncCallback(MyClient.CallBack);
IPHostEntry ipHE = Dns.GetHostByName(Dns.GetHostName());
IAsyncResult Iar = md.BeginInvoke( 1 , 2 , 300 ,ipHE.AddressList[ 0 ].ToString(),ac, null );
Method();
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
ChannelServices.UnregisterChannel(channel);
Console.ReadLine();
}
public static void CallBack(IAsyncResult Iar)
{
if (Iar.IsCompleted)
{
Console.WriteLine( " 结果是 " + md.EndInvoke(Iar));
}
}
public static void MyEvent( object sender,RemoteObject.MyEventArgs e)
{
Console.WriteLine( " 来自 " + e.IP + " 的异步方法完成了 " + e.Rate * 10 + " % " );
}
public static void Method()
{
Console.WriteLine( " 主线程方法开始 " );
System.Threading.Thread.Sleep( 5000 );
Console.WriteLine( " 主线程方法结束 " );
}
}
代码看上去不错,可是debug启动后报错:
这就是我前面提到的问题,远程不可能有本地的程序集,也无法触发本地事件。解决办法就是加一个事件中间件,继承MarshalByRefObject:
{
public void MyEvent( object sender,MyEventArgs e)
{
Console.WriteLine( " 来自 " + e.IP + " 的异步方法完成了 " + e.Rate * 10 + " % " );
}
}
然后来修改一下客户端:
把app.MyEvent+=new RemoteObject.MyObject.MyEventHandler(MyEvent);修改为
app.MyEvent += new RemoteObject.MyObject.MyEventHandler(ec.MyEvent);
删除客户端的MyEvent静态方法。
运行一下程序:
前后两个窗口本别是服务端和客户端的,貌似达到了我们的要求,其实不然,程序有2个漏洞:
1、客户端关闭以后打开新的程序就出错,因为以前的委托链丢失,服务端程序企图触发事件出错。
2、同时打开几个客户端,客户端收到的是所有的进度信息,而不仅仅是自己的,广播性质的消息。
(原创)一步一步学Remoting之六:事件(2)
到了午休的时间,抓紧时间继续写,上次说有2个遗留问题:
(1)关闭一个客户端以后会影响其他的客户端事件
原因:客户端没有取消事件订阅就关闭了,触发事件的时候找不到事件订阅者
解决:遍历委托链,找到异常的对象,从委托链中卸下
(2)服务器端对客户端广播,客户端能收到其他客户端的事件处理信息
原因:使用了Singleton模式,共享远程对象
解决:因为需要远程对象有状态且不共享实例,所以只有客户端激活可以选择
修改后的服务端:
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
namespace RemoteServer
{
class MyServer
{
[STAThread]
static void Main( string [] args)
{
RemotingConfiguration.ApplicationName = " RemoteObject.MyObject " ;
RemotingConfiguration.RegisterActivatedServiceType( typeof (RemoteObject.MyObject));
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props[ " port " ] = 8888 ;
TcpChannel channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelServices.RegisterChannel(channel);
Console.ReadLine();
}
}
}
修改后的远程对象:
namespace RemoteObject
{
[Serializable]
public class MyEventArgs:EventArgs
{
private int _rate;
private string _ip;
public int Rate
{
get
{
return _rate;
}
}
public string IP
{
get
{
return _ip;
}
}
public MyEventArgs( int rate, string ip)
{
this ._rate = rate;
this ._ip = ip;
}
}
public class MyObject:MarshalByRefObject
{
public delegate void MyEventHandler( object sender,MyEventArgs e);
public event MyEventHandler MyEvent;
public string tmp;
public int ALongTimeMethod( int a, int b, int time, string ip)
{
Console.WriteLine( " 来自 " + ip + " 的异步方法开始 " );
for ( int i = 1 ;i <= 10 ;i ++ )
{
System.Threading.Thread.Sleep(time);
Console.WriteLine( " 来自 " + ip + " 的异步方法完成了 " + i * 10 + " % " );
OnMyEvent( new MyEventArgs(i,ip));
}
Console.WriteLine( " 来自 " + ip + " 的异步方法结束 " );
return a + b;
}
protected void OnMyEvent(MyEventArgs e)
{
if (MyEvent != null )
{
foreach (Delegate d in MyEvent.GetInvocationList())
{
try
{
((MyEventHandler)d)( this ,e);
}
catch
{
MyEvent -= (MyEventHandler)d;
}
}
}
}
}
public class EventClass:MarshalByRefObject
{
public void MyEvent( object sender,MyEventArgs e)
{
if (((MyObject)sender).tmp == e.IP)
Console.WriteLine( " 异步方法完成了 " + e.Rate * 10 + " % " );
}
}
}
修改后的客户端:
using System.Net;
using System.Collections;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
class MyClient
{
private delegate int MyDelegate( int a, int b, int time, string ip);
private static MyDelegate md;
static RemoteObject.MyObject app;
static RemoteObject.EventClass ec;
static DateTime dt;
[STAThread]
static void Main( string [] args)
{
dt = DateTime.Now;
RemotingConfiguration.RegisterActivatedClientType( typeof (RemoteObject.MyObject), " tcp://localhost:8888/RemoteObject.MyObject " );
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props[ " port " ] = 0 ;
TcpChannel channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelServices.RegisterChannel(channel);
app = new RemoteObject.MyObject();
ec = new RemoteObject.EventClass();
app.MyEvent += new RemoteObject.MyObject.MyEventHandler(ec.MyEvent);
md = new MyDelegate(app.ALongTimeMethod);
AsyncCallback ac = new AsyncCallback(MyClient.CallBack);
IPHostEntry ipHE = Dns.GetHostByName(Dns.GetHostName());
Random rnd = new Random(System.Environment.TickCount);
string ip = ipHE.AddressList[ 0 ].ToString() + " ( " + rnd.Next( 100000000 ).ToString() + " ) " ;
app.tmp = ip;
IAsyncResult Iar = md.BeginInvoke( 1 , 2 , 500 ,ip,ac, null );
Method();
Console.WriteLine( " 用了 " + ((TimeSpan)(DateTime.Now - dt)).TotalSeconds + " 秒 " );
ChannelServices.UnregisterChannel(channel);
Console.ReadLine();
}
public static void CallBack(IAsyncResult Iar)
{
if (Iar.IsCompleted)
{
Console.WriteLine( " 结果是 " + md.EndInvoke(Iar));
app.MyEvent -= new RemoteObject.MyObject.MyEventHandler(ec.MyEvent);
}
}
public static void Method()
{
Console.WriteLine( " 主线程方法开始 " );
System.Threading.Thread.Sleep( 5000 );
Console.WriteLine( " 主线程方法结束 " );
}
}
之所以要在ip地址后面跟上随机数,是因为可能在一个机器上会打开多个客户端,需要在这个时候能在服务器端区分多个客户端。
备注:我的所有例子都是在客户端和服务器端部署远程对象的,其实这个做法不是很好,我们应该仅仅把接口部署在两地,远程对象仅仅部署在服务器端即可。