分享

.NET Remoting监听端口的Windows Service

 ThinkTank_引擎 2014-11-20
   

所谓.NET Remoting就是跨应用程序域边界调用程序集。如图23-16所示,显示了.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页面上新建三个按钮用于使用不同的信道调用远程对象。

<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  异步调用方法

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多