所谓.NET Remoting就是跨应用程序域边界调用程序集。如图23-16所示,显示了.NET Remoting应用程序的基本构架。
从图23-16中看到,Remoting服务端承载远程对象,使外界能与之通信,对外的信道可以是HTTP、TCP或者IPC。HTTP方式的信道在跨越防火墙上有优势;TCP方式的信道常用在局域网内通信,速度比HTTP快很多;IPC信道用于同一台机器的进程间通信,通信不占用网络资源,速度又比TCP快很多。因此,这里的服务器是一个广义的概念,对于TCP和HTTP信道,服务器可以是两个独立的物理计算机。
那么,最基本的.NET Remoting应用程序应该由三部分构成:
· 服务端。承载远程对象。
· 远程对象。需要跨应用程序域边界调用的程序集。
· 客户端。用于调用远程对象。
远程对象是根本,服务端只是一个载体,那么我们就先来创建一个简单的远程对象:
1.继续使用前面的一个解决方案。右键单击解决方案,选择“添加”→“新建项目”命令,新建一个TestRemoteObject类库项目。
2.把默认的Class1.cs重命名为RemoteObject.cs,打开cs文件,修改代码为:
using System;
namespace RemoteObject
{
public class MyObject : MarshalByRefObject
{
public int Add(int a, int b)
{
return a + b;
}
}
}
在RemoteObject命名空间下有一个MyObject类,除了继承MarshalByRefObject类使之能跨应用程序域边界被访问之外,和一般的类没有任何区别。
3.右键单击这个类库项目,如图23-17所示。
图23-17 项目属性
我们看到这个项目的程序集名为TestRemoteObject,默认的命名空间为TestRemoteObject。默认的命名空间名字和程序集的名字是一样的,但是在代码中我们的命名空间名字为RemoteObject,和程序集名字不同以便进行区分。
注意:程序集和命名空间是两个不同的概念,一个程序集可以包括几个命名空间,一个命名空间也可以由多个程序集来实现。
在创建了远程对象后就需要创建Remoting服务端来发布这个远程对象了。
4.服务端可以是一个控制台应用程序、Windows应用程序、Windows服务甚至是IIS。为了简单,我们将首先使用控制台应用程序做服务端。在解决方案中新建一个名为TestRemotingConsoleServer的控制台应用程序,然后右键单击项目,选择添加应用,如图23-18所示,添加System.Runtime.Remoting的引用。
5.把Program.cs修改成如下:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
namespace TestRemotingConsoleServer
{
class Program
{
static void Main(string[] args)
{
// 新建一个TCP信道
TcpChannel tc = new TcpChannel(9999);
// 注册TCP信道
ChannelServices.RegisterChannel(tc, false);
// 注册知名对象
RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteObject.
MyObject), "myObject", WellKnownObjectMode.SingleCall);
// 让控制台不会自动关闭
Console.ReadLine();
}
}
}
图23-18 添加System.Runtime.Remoting的引用
我们看到,使用.NET Remoting发布远程对象并不复杂,首先需要告知程序使用哪种信道发布远程对象。在这里我们选择TCP信道,并在9999端口通信。然后要告知程序把对象注册为哪种类型,在这里笔者不想详细阐述远程对象的种类和模式,读者只需要理解在这里我们把Remote- Object.MyObject这个类型使用一个固定的名字myObject来发布(因此叫做知名对象),对象的模式是SingleCall,SingleCall模式的对象是无状态的。
最后我们来完成用客户端应用程序调用远程对象。客户端应用程序可以是ASP.NET应用程序、控制台应用程序或者Windows应用程序。那么,我们就直接使用前一节建立的ASP.NET应用程序作为客户端吧。
6.在TestWeb网站下新建一个RemotingTest.aspx,然后在页面的Page_Load事件处理方法中调用远程对象。
protected void Page_Load(object sender, EventArgs e)
{
RemoteObject.MyObject mo = (RemoteObject.MyObject)Activator.GetObject
(typeof(RemoteObject.MyObject), "tcp://localhost:9999/myObject");
Response.Write(mo.Add(1, 2));
}
在这里,我们从远程地址tcp://localhost:9999/myObject创建远程对象,并调用了对象的Add()方法。myObject就是在服务端中为知名对象起的名字。
7.编译整个解决方案,IDE提示“找不到命名空间RemoteObject”,这是因为我们的客户端和服务端项目没有引用远程对象类库项目。右键单击服务端项目,选择“添加引用”,在项目页中找到类库项目,单击“确定”按钮,如图23-19所示。
对于客户端项目也同样添加类库的引用,然后重新编译解决方案。
8.现在就能进行测试了。解决方案中的项目如图23-20所示。
图23-19 添加项目引用 图23-20 解决方案中的项目
要让远程调用成功运行,先要启动服务端使之监听端口。如图23-20所示,单击控制台应用程序,项目名自动以粗体标识,表示这是当前项目,按Ctrl+F5组合键直接启动程序。然后再单击TestWeb网站,右键单击RemotingTest.aspx,选择设为起始页,按Ctrl+F5组合键启动网站。
如图23-21所示,页面显示3,成功了!
图23-21 调用远程对象
注意图23-21所示,在整个过程中需要确保服务端处于运行状态。至此,我们完成了第一个.NET Remoting应用程序。
23.3.2 Remoting的信道
前面提到过,Remoting有多种信道可以选择,这大大增加了我们分布式系统的灵活性。如果希望在广域网通信,可以使用HTTP信道,如果希望在局域网通信取得更好的性能,可以使用TCP信道,如果希望在本机上的不同进程间通信以获得最好的性能,可以使用IPC信道。
下面我们来修改前面的程序,使之使用三种不同的Remoting信道,并且我们要比较三种信道在效率上差多少:
1.首先在远程对象中新增一个方法,使之返回大量的数据。
using System;
namespace RemoteObject
{
public class MyObject : MarshalByRefObject
{
public int Add(int a, int b)
{
return a + b;
}
public string[] GetData()
{
string[] data = new string[100000];
for (int i = 0; i < data.Length; i++)
data[i] = "很大量的数据"+i;
return data;
}
}
}
2.然后修改服务端,使之在三个不同的信道上发布远程对象。
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Ipc;
namespace TestRemotingConsoleServer
{
class Program
{
static void Main(string[] args)
{
// 新建一个TCP信道
TcpChannel tc = new TcpChannel(9999);
// 新建一个HTTP信道
HttpChannel hc = new HttpChannel(8888);
// 新建一个IPC信道
IpcChannel ic = new IpcChannel("testPipe");
// 注册TCP信道
ChannelServices.RegisterChannel(tc, false);
ChannelServices.RegisterChannel(hc, false);
ChannelServices.RegisterChannel(ic, false);
// 注册知名对象
RemotingConfiguration.RegisterWellKnownServiceType(typeof
(RemoteObject.MyObject), "myObject", WellKnownObjectMode.SingleCall);
// 让控制台不会自动关闭
Console.ReadLine();
}
}
}
注意,由于是在同一个机器上注册多个信道,需要给每个信道使用不同的端口。对于IPC信道来说,使用一个管道名来区分而不是端口号。为了能更好地测试三者的差别,我们把服务端部署到另外一个服务器上(把EXE文件和DLL文件复制过去)。
3.在RemotingTest.aspx页面上新建三个按钮用于使用不同的信道调用远程对象。
按钮的单击事件处理方法如下:
protected void btn_HttpChannel_Click(object sender, EventArgs e)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
RemoteObject.MyObject mo = (RemoteObject.
MyObject)Activator.GetObject(typeof(RemoteObject.MyObject),
"http://srv-devapphost:8888/myObject");
Response.Write("HTTP信道
");
Response.Write(string.Format("记录数:{0}条
", mo.GetData().Length));
Response.Write(string.Format("花费时间:{0}毫秒
", sw.ElapsedMilliseconds));
}
protected void btn_TcpChannel_Click(object sender, EventArgs e)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
RemoteObject.MyObject mo = (RemoteObject.MyObject)Activator.GetObject
(typeof(RemoteObject.MyObject), "tcp://srv-devapphost:9999/myObject");
Response.Write("TCP信道
");
Response.Write(string.Format("记录数:{0}条
", mo.GetData().Length));
Response.Write(string.Format("花费时间:{0}毫秒
", sw.ElapsedMilliseconds));
}
protected void btn_IpcChannel_Click(object sender, EventArgs e)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
RemoteObject.MyObject mo = (RemoteObject.
MyObject)Activator.GetObject(typeof(RemoteObject.MyObject),
"ipc://testPipe/myObject");
Response.Write("IPC信道
");
Response.Write(string.Format("记录数:{0}条
", mo.GetData().Length));
Response.Write(string.Format("花费时间:{0}毫秒
", sw.ElapsedMilliseconds));
}
可以看到,使用三种信道调用的代码仅在URL上有区别。在这里我们不但把服务端部署到了远程服务器上,而且在本地也开了一个服务端用于在IPC上注册远程对象。
4.测试结果如图23-22所示。
图23-22 Remoting的三种信道
可以看到在效率上三者有明显的差别。IPC比TCP快是因为它传递数据不经过网络,不占用网络资源。TCP比HTTP快很多是因为默认情况下TCP信道使用二进制序列化,序列化后的数据量很小,而HTTP默认使用SOAP消息进行格式化,基于XML的SOAP消息非常臃肿,因此在传输上会比TCP花费更多的时间。不过不可否认HTTP信道在跨防火墙上的优势,因此使用哪种信道还需要根据自己的需求来选择。
23.3.3 使用配置文件增加灵活性
虽然我们做的Remoting程序可以正常使用,但是整个程序非常不灵活:
· 服务端有关信道、端口等的配置都直接写死在程序里面。
· 客户端设置的远程对象的地址也是写死在程序里面的。
对于客户端的配置不是大问题,因为其实那个URL就是一个字符串。而服务端的配置文件应该怎么做呢?其实一点也不复杂,添加一个app.config然后写入下面的内容:
"myObject" mode="SingleCall" /> 可以看到配置文件主要由两部分构成: · 定义远程对象类型的service节点。在这里我们定义了一个知名对象,模式是SingleCall,对象名为myObject。 · 定义信道的channels节点。在这里定义了三个信道,和先前程序方式定义的一样。 特别需要注意的是,这里的type="RemoteObject.MyObject,TestRemoteObject",格式是: type="命名空间.类型名,程序集名" 对比图23-17看看,现在你知道为什么当时笔者要把命名空间、类型和程序集三者的名字设置不同了吧。那么,怎么让服务端加载配置文件读取Remoting的配置呢?只需要一行代码就行。 RemotingConfiguration.Configure("TestRemotingConsoleServer.exe.config", false); Console.ReadLine(); 你可能会奇怪,配置文件是app.config,为什么这里写成了应用程序名.config呢?其实在编译的时候IDE会自动把配置文件进行改名,以免发生冲突,如图23-23所示,可以看到Release目录的 文件。 图23-23 服务端程序release文件夹 真正有用的是加亮的三个文件(分别是远程对象、服务端和配置文件),在部署的时候只需要复制这些文件即可。 虽然改了服务端,但是我们并没有改变通道的端口,因此客户端不需要做任何修改就能直接运行。如果你希望把URL从程序中分离的话,可以在配置文件中添加几个节点。 然后在代码中调用配置文件读取URL。 RemoteObject.MyObject mo = (RemoteObject.MyObject) Activator.GetObject(typeof(RemoteObject.MyObject), ConfigurationManager.AppSettings["HTTPChannel"]); 其他两个信道的代码差不多,就不列出来了。现在这样就非常灵活了,修改信道、修改端口甚至转移服务端的位置只需要重新调整配置文件即可。 读者首先要明确一点,客户端调用的远程方法是在服务端执行的。如下,我们在远程对象中增加一个方法。 public void HelloWorld() { Console.WriteLine("编程快乐"); } 重新编译服务端和客户端,运行客户端可以看到服务端控制台程序上输出了“编程快乐”字样,如图23-24所示。 那么问题就来了,既然远程对象是在服务端执行的,客户端为什么要引用远程对象呢?假设我们的报表系统是使用.NET Remoting开发的,难道要把核心DLL也公布给客户吗(要知道.NET应用程序是很容易被反编译得到“源代码”的)?其实,客户端只需要得到远程对象的“描述”,知道远程对象的类型以及成员定义,让客户端代码能编译通过即可。具体方法是什么,怎么实现,客户端并不关心。 那么,怎么构建这个供客户端使用的壳子呢?有两种方法。 · 直接使用工具比如soapsuds.exe来生成。 · 使用基于接口的编程方法。 由于篇幅关系,在这里我们仅仅介绍第二种方法的实现: 1.新建一个类库项目ITestRemoteObject,这个类库是前面TestRemoteObject的接口(Interface),因此以字母I开头。 2.打开TestRemoteObject下的RemoteObject.cs,把鼠标放在MyObject类上单击右键,选择“重构”→“提取接口”,如图23-25所示。 单击“全选”按钮选中所有成员,单击“确定”按钮。可以看到TestRemoteObject类库下面多了一个cs文件,如图23-26所示。 图23-25 提取接口 图23-26 自动生成的接口 IMyObject就是MyObject类对应的接口。打开这个文件可以看到接口其实就是对类成员的定义,没有实际的实现。 using System; namespace TestRemoteObject { interface IMyObject { int Add(int a, int b); string[] GetData(); void HelloWorld(); } } 对这个接口我们要进行一些改动: · 要让接口能被外部调用,需要把接口加上公有访问修饰符。 · 系统自动以程序集的名字作为命名空间的命名,我们还是改回原来的RemoteObject。 using System; namespace RemoteObject { public interface IMyObject { int Add(int a, int b); string[] GetData(); void HelloWorld(); } } 3.现在这个接口在远程对象文件中,我们需要把它移动到ITestRemoteObject中,直接点击文件,Ctrl+X(剪切)、CTRL+V(粘贴)即可。 4.回头看MyObject文件: public class MyObject : MarshalByRefObject, TestRemoteObject.IMyObject 系统自动让它继承了TestRemoteObject.IMyObject,刚才我们把TestRemoteObject修改成了RemoteObject,现在这里也需要同样修改。 既然让类实现接口,那么就需要让TestRemoteObject项目引用ITestRemoteObject项目。右键单击TestRemoteObject项目,选择添加引用,在项目选项卡中找到ITestRemoteObject项目,单击“确定”按钮即可。 现在两个项目的结构应该如图23-27所示。 你可能会问,接口仅仅是对类的一个定义吗?不仅仅是这样,接口还对类有约束力,如果你修改了接口也一定要修改“实现”。如果你在接口中新加入一个Test()的方法,而不修改“实现”,编译程序会得到编译错误,如图23-28所示。 图23-27 基于接口的编程 图23-28 类需要实现接口的成员 5.现在,我们的客户端就可以引用和使用接口,而不是直接引用和使用远程对象了。首先右键单击TestWeb网站,选择属性页。在引用页找到原来的远程对象TestRemoteObject,删除它的引用,并添加ITestRemoteObject的引用,如图23-29所示。 图23-29 修改网站项目的引用 查找替换Remoting.aspx.cs中的所有RemoteObject. MyObject为RemoteObject.IMyObject,比如: RemoteObject.IMyObject mo = (RemoteObject.IMyObject)Activator. GetObject(typeof(RemoteObject.IMyObject), ConfigurationManager. AppSettings["TCPChannel"]); mo.HelloWorld(); 6.重新编译解决方案,先后运行服务端和客户端,效果和原来的没有什么不同。但是,这样的方式更灵活了,或者说耦合更低了。为什么这样说呢?因为,现在如果希望在服务端的实现中做什么改动的话,不需要重新编译和部署客户端程序。 现在的程序看似很完美,但是要想真正应用还有一些问题。我们的服务端是一个控制台应用程序,如果在服务器上需要有10个Remoting的服务端,那么我们服务器重启动后也需要重启动这10个程序吗?读者可能会说可以把它们加入开始菜单的启动中让程序自动启动。但是你有没有想过,在登录到服务器进行维护的时候很容易不小心把控制台程序关闭了,而且关闭之后还不知道。 要想解决这个问题就需要使用一种后台式的程序来作为服务端,Windows服务正好可以满足这个要求,而且还可以设置Windows服务自动启动。使用VS 2005创建.NET的Windows服务非常简单,下面我们一起来实现Windows服务版本的Remoting服务端。 1.创建一个新的Windows服务项目TestRemotingService,如图23-30所示。 图23-30 创建新的Windows服务项目 2.打开Service1代码视图,找到OnStart部分,加入代码。 protected override void OnStart(string[] args) { System.Runtime.Remoting.RemotingConfiguration.Configure(AppDomain.CurrentDomain. BaseDirectory + "TestRemotingService.exe.config", false); } 这句代码实现在Windows服务启动的时候从Windows服务安装目录所在的配置文件加载Remoting配置,然后把先前控制台服务端的配置文件复制过来。 现在这个Windows服务是Remoting的服务端,因此也别忘记添加对TestRemoteObject远程对象的引用。 3.切换到Service1的设计视图,在空白处右键单击,然后选择“添加安装程序”选项。如图23-31所示。 图23-31 添加服务安装程序 4.打开系统自动生成的ProjectInstaller.cs,如图23-32所示,可以看到页面上有两个组件。 图23-32 服务安装程序 单击serviceProcessInstaller1组件,观察属性窗口,如图23-33所示。 在这里我们把Account属性设置为LocalSystem,作为服务的账户类型。然后单击serviceInstaller1组件,观察属性窗口,如图23-34所示。 图23-33 ServiceProcessInstaller组件 图23-34 ServiceInstaller组件 在这里可以设置服务友好名、服务的描述、服务名和启动方式。只需要把StartType设置为Automatic,服务就能在系统重新启动后自动启动。 5.现在就可以安装服务了,单击“开始”菜单→“所有程序”→Microsoft Visual Studio 2005→Visual Studio Tools→“Visual Studio 2005命令行提示”,如图23-35所示,使用installutil程序来安装Windows服务。 图23-35 使用installutil工具安装Windows服务 如果你觉得输入exe所在路径太麻烦,可以直接打开文件夹把exe文件拖入命令行窗口。卸载服务使用–u参数。 installutil -u Windows服务exe所在路径 6.执行“我的电脑右键”→“管理”→“服务和应用程序”→“服务”命令。如图23-36所示,可以在列表中找到我们的服务。 图23-36 服务已经安装成功 查看这个服务的属性,如图23-37所示。 图23-37 Windows服务的属性 7.如果程序写的没有什么问题的话(其实我们只写了一行代码),服务应该能正常启动,然后可以打开网站进行测试。 注意:由于安全问题,必须为Windows服务指定一个有效账户(Account=User)才能使用IPC信道,在这里就不详细叙述了。 除了使用Windows服务承载远程对象外,还可以使用IIS。不过需要注意,使用IIS承载远程对象只能在HTTP信道上通信,好处在于可以使用IIS来进行安全管理。需要说的是,HTTP方式的Remoting效率非常低(甚至不如Web Service),因此不推荐。具体实现IIS部署Remoting的方法在这里就不说明了。 在介绍Web服务的时候,我们介绍了异步调用Web服务的操作,在这里我们将介绍如何异步调用远程对象的方法。 1.首先在远程对象中加一个耗时2秒的方法。 public string LongWork() { System.Threading.Thread.Sleep(2000); return "编程快乐"; } 别忘记同时更新接口。 using System; namespace RemoteObject { public interface IMyObject { int Add(int a, int b); string[] GetData(); void HelloWorld(); string LongWork(); } } 2.在客户端RemotingTest.asp上添加一个按钮,按钮的单击事件处理方法如下: protected void btn_AsyncInvoke_Click(object sender, EventArgs e) { sw = new System.Diagnostics.Stopwatch(); sw.Start(); RemoteObject.IMyObject mo = (RemoteObject.IMyObject) Activator.GetObject(typeof(RemoteObject.IMyObject), ConfigurationManager. AppSettings["TCPChannel"]); MyDelegate md = new MyDelegate(mo.LongWork); AsyncCallback ac = new AsyncCallback(this.CallBack); IAsyncResult Iar = md.BeginInvoke(ac, null); System.Threading.Thread.Sleep(1000); } 在这里使用了两个私有变量,一个是用于Stopwatch,另外一个是方法的代理,在Page_Load上 添加。 private delegate string MyDelegate(); private System.Diagnostics.Stopwatch sw; 在调用了异步方法后线程休息了1秒,异步方法完成之后会调用回调方法。 public void CallBack(IAsyncResult Iar) { if (Iar.IsCompleted) { Response.Write("异步调用 Response.Write(string.Format("花费时间:{0}毫秒 ElapsedMilliseconds)); } } 3.由于方法是异步调用的,方法执行2秒,我们当前的按钮单击处理事件占用1秒,总共占用的时间也是2秒,如图23-38所示。 图23-38 异步调用方法 异步调用远程对象的方法和异步调用本地对象的方法其实差不多,在这里就不详述了。 本章我们介绍了分布式应用程序的基本概念及使用Web服务和.NET Remoting开发分布式应用程序的基本方法。 23.1节介绍了分布式构架的优势以及Web服务和.NET Remoting各自适用的场合。 23.2节介绍如何创建和使用Web服务,以及使用HTTP-GET方式和异步方式调用Web服务。 23.3节首先介绍了如何创建一个基本的.NET Remoting应用程序。然后介绍了使用配置文件和接口方式增加程序灵活性,之后介绍了如何使用Windows服务承载服务端。最后,我们介绍了使用异步方式调用远程方法。 本章只能说是开启了分布式开发的大门,微软.NET框架3.0版本已经引入了全新的WCF(Windows Communication Foundation),使用WCF,我们开发面向服务的分布式应用程序就更简单了,有兴趣的读者可以继续研究WCF的相关内容。23.3.4 使用接口降低耦合
23.3.5 使用Windows服务承载远程对象
23.3.6 异步操作
");
", sw.23.4 回顾与总结