65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 GUI 进行远程处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (68投票s)

2005年5月11日

CPOL

11分钟阅读

viewsIcon

277382

downloadIcon

4285

本文说明了如何在 GUI 环境中使用 .NET 远程处理。它演示了服务器如何反映其所控制的远程对象上发生的情况。文章还探讨了同步和异步调用的使用,以及代码设计的改进。

Client screen shot

Server screen shot

引言

注意:本文使用的是 Remoting,这是一项旧的 .NET 技术,已被 Windows Communication Foundation (WCF) 取代。

大多数关于远程处理的入门文章都侧重于基于控制台的应用程序,但初学者经常会问:“如何在 GUI 中进行远程处理?”几个月前我也有同样的疑问,但在网上找不到任何有用的入门代码示例,能够充分解释如何在 GUI 中进行远程处理,尤其是在服务器端。本文旨在填补这一空白。

有一些关于远程处理的入门文章确实使用了 GUI,并且对您很有帮助,但它们并未涵盖本文讨论的所有主题,而且只有第三篇文章展示了用于服务器的 GUI。

必备组件

本文将让您对如何在 GUI 应用程序中使用 .NET 远程处理有一个很好的了解。在阅读本文之前,您不需要了解很多关于远程处理的知识,但您应该熟悉远程处理的基础知识 - 您应该了解基本的远程控制台应用程序是如何工作的。一个很好的入门介绍可以在 gotdotnet.com 上找到。

使用远程处理创建分布式应用程序时有许多设计考虑因素,我建议您在创建任何完整的应用程序之前,对此主题进行更多研究。一个很好的资源是 Advanced .NET Remoting 2nd Edition (Ingo Rammer 和 Mario Szpuszta,Apress,2005 年 3 月)。Ingo 深入探讨了 GUI 应用程序中常见的远程事件和委托。他还提供了一些关于在 GUI 中使用远程处理非常有用的建议,在他的文章 PRB: GUI application hangs when using non-[OneWay]-Events 中。

高层设计

让我们从高层次的角度看一下我们将要开发的应用程序。一个客户端应用程序将调用一个名为 Greeter 的远程对象,该对象驻留在服务器的应用程序空间中。服务器将向 Greeter 注册自身,以便在 Greeter 上发生调用时接收通知。Greeter 将将调用通知服务器,服务器将在其 GUI 上显示该信息。

Diagram of application

Greeter 远程对象

Greeter 是我们的远程对象,并且像所有远程对象一样,它继承自 MarshalByRefObject。它的租约不会过期,因为它向 InitializeLifetimeService 返回 null。稍后我们将看到 Greeter 由服务器实例化。

// Greeter.cs

using System;
using System.Threading;

namespace guiexample
{
    public class Greeter : MarshalByRefObject
    {
        // Used by GUI to listen for SayHello calls
        public delegate void HelloEventHandler(object sender, HelloEventArgs e);
        public event HelloEventHandler HelloEvent;

        // Time in msec to wait until we respond to the Client
        private int mRespondTime;
        public int RespondTime
        {
            get { return mRespondTime; }
            set { mRespondTime = Math.Max(0, value); }
        }

        public Greeter()
        {
            // Default no argument constructor
        }

        public override Object InitializeLifetimeService()
        {
            // Allow this object to live "forever"
            return null;
        }

        public String SayHello(String name)
        {
            // Inform the GUI that SayHello was called
            if (HelloEvent != null)
                HelloEvent(this, new HelloEventArgs(name));

            // Pretend we're computing something that takes a while
            Thread.Sleep(mRespondTime);

            return "Hello there, " + name + "!";
        }
    }
}

Greeter 只实现一个可以远程调用的方法:SayHello。它还有一个名为 RespondTime 的属性,将用于服务器设置响应 SayHello 调用的延迟。我们将延迟响应以模拟慢返回调用,并观察其对客户端 GUI 的影响。

SayHello 方法在被调用时首先引发一个 HelloEvent。这将提醒任何 HelloEvent 侦听器是谁调用了此方法。服务器需要为 HelloEvent 注册一个事件处理程序,以便更新其 GUI。(请注意,我们首先检查了 null,以防服务器“忘记”注册事件。)这使得服务器和 Greeter 可以解耦。这与观察者设计模式相匹配,其中 Greeter 是被观察的主体,而服务器是观察者。我们的 Greeter 可以引发任何观察者感兴趣的事件。

服务器

Server 的构造函数中,首先创建使用端口 50050 的 TCP 通道,然后实例化 Greeter 并使用 RemotingServices.Marshal 注册它,以便我们的客户端可以访问它。Server 对了解 GreeterSayHello 方法何时被调用感兴趣,因此它为 HelloEvent 事件注册了一个侦听器。您会注意到我们没有使用 RemotingConfiguration.RegisterWellKnownServiceTypeGreeter 注册为 Singleton,因为 Server 需要实际使用 Greeter 本身,而不仅仅是将其暴露给客户端。

public Server()
{
    // Required for Windows Form Designer support
    InitializeComponent();

    // Register our tcp channel
    ChannelServices.RegisterChannel(new TcpChannel(50050));

    // Register an object created by the server
    rmGreeter = new Greeter();
    ObjRef refGreeter = RemotingServices.Marshal(rmGreeter, "Greeter");

    rmGreeter.RespondTime = Convert.ToInt32(txbRespond.Text);

    // Register ourself to receive updates that we will display in our GUI
    rmGreeter.HelloEvent += new Greeter.HelloEventHandler(Server_HelloEvent);
}

Greeter 引发 HelloEvent 事件时,将调用 ServerServer_HelloEvent 方法。这是 Server 更新其 GUI 的机会。如果我们可以这样更新我们的 GUI 那将很好

private void Server_HelloEvent(object sender, HelloEventArgs e)
{
    lblHello.Text = "Saying hello to " + e.Name;
}

但这可能会导致死锁。原因是事件正在一个未创建 Label 控件的线程中运行。控件只能由 UI 线程更新。因此,我们改为在控件上调用 BeginInvoke,并为其提供一个委托,该委托将在创建控件的 UI 线程上异步运行。在多线程环境中更新 GUI 控件时,这是必需的。稍后更新客户端 GUI 时,我们将执行相同的操作。

有关使用 Control.BeginInvoke 更新 GUI 控件的更多信息,请参阅 S. Senthil Kumar 的文章 What's up with BeginInvoke? 和 Ingo 的文章 PRB: GUI application hangs when using non-[OneWay]-Events

private delegate void SetLabelTextDelegate(string text);
// For updating label

private void SetLabelText(string text)
{
    lblHello.Text = text;
}

// Called when the Greeter object tells us SayHello has been called
private void Server_HelloEvent(object sender, HelloEventArgs e)
{    
    // Pull out name from the event
    string text = "Saying hello to " + e.Name;

    // Set the label text with the UI thread to avoid possible application hanging
    this.BeginInvoke(new SetLabelTextDelegate(SetLabelText), new object[] {text});
}

HelloEventArgs 提供了更新 GUI 所需的信息。在这种情况下,我们只对调用 SayHello 的人感兴趣,但我们可以增强 HelloEventArgs 以包含错误消息、时间戳等。以下是我们的 HelloEventArgs 类,它有一个名为 Name 的只读属性

public class HelloEventArgs : EventArgs 
{
    private string mName;
    public string Name
    {
        get { return mName; }
    }

    public HelloEventArgs(string name)
    {
        mName = name;
    }
}

我们的 Server 使用一个文本框来允许我们控制远程对象响应客户端的时间量。我们不希望用户输入任意值,因此我们在设置 RespondTime 之前,使用 TextChanged 事件执行一些数据验证。

private void txbRespond_TextChanged(object sender, System.EventArgs e)
{
    try
    {
        // Set delay before we respond to the client

        int delay = Convert.ToInt32(txbRespond.Text);
        if (delay >= 0)
            rmGreeter.RespondTime = delay;
    }
    catch (Exception) {}
}

客户端

Client 在其构造函数中创建一个 TCP 通道,并获取由 Server 提供的 Greeter 对象的引用。

public Client()
{
    // Required for Windows Form Designer support
    InitializeComponent();

    // Register our tcp channel
    ChannelServices.RegisterChannel(new TcpChannel());

    rmGreeter = (Greeter)Activator.GetObject(
        typeof(guiexample.Greeter), "tcp://:50050/Greeter");

    lblResult.Text = "";
}

Client 有两个按钮用于调用远程方法 SayHello。第一个按钮 **Call Synchronously** 使用同步调用 SayHello。这是一个阻塞调用 - 它会锁定当前线程,直到 Greeter 返回响应。这在 GUI 中不是理想情况,因为 GUI 必须继续侦听来自操作系统的消息,例如按钮点击、鼠标移动、绘制更新等。点击 Call Synchronously 将导致 GUI 冻结,直到 SayHello 返回。尝试单击按钮,然后移动窗口,直到窗口更新,以了解我的意思。

我们在调用 SayHello 时使用 try/catch 块,以防我们的 Server 在我们进行调用时宕机,或者 Server 最初就不可用。始终将远程方法调用封装在 try/catch 中,以使您的应用程序更加健壮。

private void btnCallSynch_Click(object sender, System.EventArgs e)
{
    lblResult.Text = "";

    // Force update since the remote method
    // call blocks us from receiving window messages
    lblResult.Refresh();   

    try
    {
        lblResult.Text = rmGreeter.SayHello(txbName.Text);
    }
    catch (Exception ex)
    {
        MessageBox.Show(this, "Unable to call SayHello. " + 
             " Make sure the server is running.\n"
             + ex.Message, "Client", MessageBoxButtons.OK, 
             MessageBoxIcon.Error);
    }
}

第二个按钮 **Call Asynchronously** 使用异步调用 SayHello。实际上,它只是使用了一个委托来模拟同步调用,但最终效果是我们 GUI 在进行远程调用时不再被锁定。当您的 GUI 客户端需要调用远程方法时,这很可能是您应该做的。您会注意到,当我们调用 BeginInvoke 时,我们将用户输入到文本框中的文本传递了进去。

顺便说一下,您可能想知道 OneWay 调用。OneWay 调用允许您在不阻塞调用线程的情况下调用 void 函数。不幸的是,当接收这些调用的服务器宕机时,这类调用可能会导致严重的性能下降。有关为什么应该避免 OneWay 调用的更多信息,请阅读 Ingo 的书籍 Advanced .NET Remoting 中题为 *Why [OneWay] Events Are a Bad Idea* 的部分。

private void btnCallAsynch_Click(object sender, System.EventArgs e)
{
    lblResult.Text = "";

    // Produce an asynchronous call to the remote method SayHello from which
    // we'll immediately return

    AsyncCallback cb = new AsyncCallback(this.SayHelloCallBack);
    SayHelloDelegate d = new SayHelloDelegate(rmGreeter.SayHello);
    IAsyncResult ar = d.BeginInvoke(txbName.Text, cb, null);
}

SayHello 方法完成时,将调用 ClientSayHelloCallBack 方法。我们可以通过调用 EndInvoke 来获取 SayHello 的返回值。就像对 SayHello 的同步调用一样,我们将 EndInvoke 的调用放在 try/catch 块中,以防我们的 Server 未运行。

SayHelloCallBack 方法不直接尝试更新 Label,因为它不在创建 Label 控件的 UI 线程中运行。它改为调用 BeginInvoke,以便异步调用 SetLabelText 来更改 Label 的文本。我们对 Server 做了类似的事情。

private delegate void SetLabelTextDelegate(string text);
// For updating label

private void SetLabelText(string text)
{
    lblResult.Text = text;
}

public void SayHelloCallBack(IAsyncResult ar)
{
    SayHelloDelegate d = (SayHelloDelegate)((AsyncResult)ar).AsyncDelegate;

    try
    {
        // Pull out the return value
        string text = (string)d.EndInvoke(ar);

        // Set the label text with the UI thread
        // to avoid possible application hanging
        this.BeginInvoke(new SetLabelTextDelegate(SetLabelText), 
                                           new object[] {text});
    }
    catch (Exception ex)
    {
        MessageBox.Show(this, "Unable to call SayHello." + 
            " Make sure the server is running.\n" + 
            ex.Message, "Client", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

运行应用程序

下载应用程序并运行服务器(server.exe)和客户端(client.exe)。如果您想在不同的计算机上运行该应用程序,则需要将“localhost”更改为运行客户端和服务器的计算机名称,并重新编译该应用程序。

我提供了一个 make.bat 文件,该文件使用 C# 命令行编译器 csc 来重新编译应用程序

csc /t:library /r:System.Runtime.Remoting.dll Greeter.cs HelloEventArgs.cs
csc /r:Greeter.dll /r:System.Runtime.Remoting.dll 
                   /target:winexe /out:server.exe Server.cs
csc /r:Greeter.dll /r:System.Runtime.Remoting.dll 
                   /target:winexe /out:client.exe Client.cs

改进

我们可以对应用程序进行一些改进,这些改进不会提高其执行速度,但会改进其设计:使用 Singleton 设计模式确保 Greeter 只被 Server 实例化一次,使用接口来强制 ServerClient 正确使用 Greeter,并使用配置文件以便于部署中的更改。

Singleton 设计模式

我们的 Greeter 应该只被 Server 实例化一次。我们可以通过使用 Singleton 设计模式来控制这一点。Jon Skeet 讨论了 在 C# 中实现 Singleton 设计模式

我们需要将 Greeter 的构造函数设为 private,以便我们能够强制控制 Greeter 的实例化次数。我们还将提供一个 Instance 属性,Server 将使用该属性来实例化 Greeter

static Greeter mInstance = null;

private Greeter()
{
    // Private constructor prevents instantiation with "new"
}

public static Greeter Instance
{
    get
    {
        if (mInstance == null)
            mInstance = new Greeter();
        
        return mInstance;
    }
}

Server 现在将这样实例化 Greeter

Greeter g = Greeter.Instance;

现在我们可以确保 Server 不会意外地创建多个 Greeter

使用接口

当前的实现允许 Client 更改 RespondTime 并侦听 HelloEvent。我们可能希望 ServerClientGreeter 有单独的视图,以强制正确实现。通过为 ClientServer 创建单独的 Greeting 接口,我们可以防止 Client 访问 RespondTimeHelloEvent

首先,让我们为 Client 创建一个接口,允许 Client 只调用 SayHello

public interface IClientGreeter
{
    String SayHello(String name);
}

现在,让我们为 Server 创建一个接口,允许它访问 RespondTimeHelloEvent

public interface IServerGreeter
{
    int RespondTime
    {
        get;
        set;
    }

    event Greeter.HelloEventHandler HelloEvent;
}

Greeter 需要实现这两个接口,并修改 ResponseTimeSayHello 的声明方式

public class Greeter : MarshalByRefObject, IServerGreeter, IClientGreeter
...

int IServerGreeter.RespondTime
...

String IClientGreeter.SayHello(String name)
...

现在,我们需要更改 ClientServer 访问 Greeter 的方式。在 Client 中,我们将 private 引用从 Greeter 更改为 IClientGreeter,并更改我们激活 Greeter 的方式。

private IClientGreeter rmGreeter;

...

rmGreeter = (guiexample.IClientGreeter)Activator.GetObject(
    typeof(guiexample.IClientGreeter), "tcp://:50050/Greeter");

我们将在 Server 上进行类似的 code 更改

private IServerGreeter rmGreeter;

...

Greeter g = new Greeter();
ObjRef refGreeter = RemotingServices.Marshal(g, "Greeter");
rmGreeter = (IServerGreeter)g;

现在,如果您尝试在 Client 上设置 RespondTime,您的代码将无法编译。

使用接口的一个好处是,您可以完全解耦远程对象的实现与客户端。如果我们为 IClientGreeter 接口创建一个 DLL,我们可以只将 IClientGreeter.dll 部署到客户端。我们在服务器上对 Greeter 所做的任何更改(保持接口不变)都不需要对客户端进行任何代码或文件的重新部署。

我们将这样编译我们的系统

csc /t:library /r:System.Runtime.Remoting.dll 
         Greeter.cs HelloEventArgs.cs IServerGreeter.cs IClientGreeter.cs
csc /t:library /r:System.Runtime.Remoting.dll IClientGreeter.cs
csc /r:Greeter.dll /r:System.Runtime.Remoting.dll 
         /target:winexe /out:server.exe Server.cs
csc /r:IClientGreeter.dll /target:winexe /out:client.exe Client.cs

服务器需要 Greeter.dllserver.exe,而客户端需要 IClientGreeter.dllclient.exe 来执行。

配置文件

当您刚开始学习远程处理时,直接在源代码中显式创建所有通道和对象会更容易理解。但后来当您想在不同的计算机上运行程序或使用不同的端口时,您会发现必须更改代码并重新编译是一件非常麻烦的事情。这时就需要配置文件了。

让我们为 Server 创建一个 server.exe.config 文件

<configuration>
    <system.runtime.remoting>
        <application>
            <channels>
                <channel ref="tcp" port="50050" />
            </channels>
        </application>
    </system.runtime.remoting>
</configuration>

让我们在 Server 的构造函数中用以下代码替换 RegisterChannel 调用

RemotingConfiguration.Configure("server.exe.config");

让我们为 Client 创建一个 client.exe.config 文件

<configuration>
    <appSettings>
        <add key="GreeterUrl" value="tcp://:50050/Greeter" />
    </appSettings>
    <system.runtime.remoting>
        <application>
            <channels>
                <channel ref="tcp" port="0" />
            </channels>
        </application>
    </system.runtime.remoting>
</configuration>

这使我们可以从 Client 的构造函数中删除 RegisterChannel 调用,并删除 Greeter 远程对象的硬编码 URL

// Create a TCP channel
RemotingConfiguration.Configure("client.exe.config");

Greeter g = 
    (Greeter)Activator.GetObject(typeof(guiexample.Greeter), 
    System.Configuration.ConfigurationSettings.AppSettings["GreeterUrl"]);

现在我们可以部署我们的应用程序,并且只需更改配置文件,即可在服务器的 URL 或端口号发生更改时进行调整。

结论

我介绍了一些在使用 GUI 环境进行远程处理时非常基础的概念,这些概念我在网络上找不到任何其他地方。

以下是在 GUI 中使用远程处理时需要记住的重要几点

  • 服务器应实例化远程对象,并使用 RemotingServices.Marshal 使其可供客户端访问。
  • 远程对象应使用事件来更新服务器的 GUI。
  • 在 GUI 环境中,异步调用比同步调用更好,因为异步调用在远程方法调用完成时不会锁定 GUI。
  • 在完成异步调用或处理来自远程对象的事件时,对客户端 GUI 和服务器 GUI 的所有更新都应使用 UI 线程,并使用 Control.BeginInvoke(或类似方法)来完成。
  • 使用接口来清晰地区分客户端对远程对象的视图和服务器对远程对象的视图。

希望这篇文章对您有所帮助。欢迎随时给我提出建设性的批评意见。

修订历史

  • 06-03-2005
    • 在列表中添加了更多入门远程处理的文章。
    • 展示了如何使用 make.bat 编译系统。
    • 在调用 HelloEvent 之前检查了 null
    • 改进了接口部分。
  • 05-17-2005
    • make.bat 中添加了编译器开关,以便服务器和客户端应用程序不会打开控制台窗口。
    • 在结论部分添加了“重要提示”。
  • 05-11-2005
    • 将代码更改为使用 BeginInvoke 显示 Label 更新。
  • 05-07-2005
    • 原始文章。
© . All rights reserved.