随风而行

^o^ 格言:相信没有做不到的事情,只有想不到的事情.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.NET Remoting2(转)

Posted on 2009-04-06 11:30  随风而行  阅读(125)  评论(0编辑  收藏  举报

前面提到过,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页面上新建三个按钮用于使用不同的信道调用远程对象。

<asp:Button ID="btn_HttpChannel" runat="server" Text="http通道"/>

<asp:Button ID="btn_TcpChannel" runat="server" Text="tcp通道"/>

<asp:Button ID="btn_IpcChannel" runat="server" Text="ipc通道"/>

按钮的单击事件处理方法如下:

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信道<br/>");

    Response.Write(string.Format("记录数:{0}条<br/>", mo.GetData().Length));

    Response.Write(string.Format("花费时间:{0}毫秒<br/>", 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信道<br/>");

   Response.Write(string.Format("记录数:{0}条<br/>", mo.GetData().Length));

    Response.Write(string.Format("花费时间:{0}毫秒<br/>", 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信道<br/>");

    Response.Write(string.Format("记录数:{0}条<br/>", mo.GetData().Length));

    Response.Write(string.Format("花费时间:{0}毫秒<br/>", 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然后写入下面的内容:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<system.runtime.remoting>

    <application name="RemoteHostService">

      <service>

        <wellknown type="RemoteObject.MyObject, TestRemoteObject" objectUri=

        "myObject" mode="SingleCall" />

      </service>

      <channels>

        <channel ref="tcp" port="9999" />

        <channel ref="http" port="8888" />

        <channel ref="ipc" portName="testPipe" />

      </channels>

    </application>

</system.runtime.remoting>

</configuration>

可以看到配置文件主要由两部分构成:

·      定义远程对象类型的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从程序中分离的话,可以在配置文件中添加几个节点。

<appSettings>

    <add key="HTTPChannel" value="http://localhost:8888/myObject"/>

    <add key="TCPChannel" value="tcp://localhost:9999/myObject"/>

    <add key="IPCChannel" value="ipc://testPipe/myObject"/>

</appSettings>

然后在代码中调用配置文件读取URL。

RemoteObject.MyObject mo = (RemoteObject.MyObject)

Activator.GetObject(typeof(RemoteObject.MyObject),

ConfigurationManager.AppSettings["HTTPChannel"]);

其他两个信道的代码差不多,就不列出来了。现在这样就非常灵活了,修改信道、修改端口甚至转移服务端的位置只需要重新调整配置文件即可。

23.3.4 使用接口降低耦合

读者首先要明确一点,客户端调用的远程方法是在服务端执行的。如下,我们在远程对象中增加一个方法。

public void HelloWorld()

{

    Console.WriteLine("编程快乐");

}

重新编译服务端和客户端,运行客户端可以看到服务端控制台程序上输出了“编程快乐”字样,如图23-24所示。

文本框:图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.重新编译解决方案,先后运行服务端和客户端,效果和原来的没有什么不同。但是,这样的方式更灵活了,或者说耦合更低了。为什么这样说呢?因为,现在如果希望在服务端的实现中做什么改动的话,不需要重新编译和部署客户端程序。

23.3.5 使用Windows服务承载远程对象

现在的程序看似很完美,但是要想真正应用还有一些问题。我们的服务端是一个控制台应用程序,如果在服务器上需要有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的方法在这里就不说明了。

23.3.6 异步操作

在介绍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("异步调用<br/>");

        Response.Write(string.Format("花费时间:{0}毫秒<br/>", sw.

        ElapsedMilliseconds));

    }

}

3.由于方法是异步调用的,方法执行2秒,我们当前的按钮单击处理事件占用1秒,总共占用的时间也是2秒,如图23-38所示。

图23-38 异步调用方法

异步调用远程对象的方法和异步调用本地对象的方法其实差不多,在这里就不详述了。