使用 GUI 进行远程处理






4.85/5 (68投票s)
本文说明了如何在 GUI 环境中使用 .NET 远程处理。它演示了服务器如何反映其所控制的远程对象上发生的情况。文章还探讨了同步和异步调用的使用,以及代码设计的改进。
引言
注意:本文使用的是 Remoting,这是一项旧的 .NET 技术,已被 Windows Communication Foundation (WCF) 取代。
大多数关于远程处理的入门文章都侧重于基于控制台的应用程序,但初学者经常会问:“如何在 GUI 中进行远程处理?”几个月前我也有同样的疑问,但在网上找不到任何有用的入门代码示例,能够充分解释如何在 GUI 中进行远程处理,尤其是在服务器端。本文旨在填补这一空白。
有一些关于远程处理的入门文章确实使用了 GUI,并且对您很有帮助,但它们并未涵盖本文讨论的所有主题,而且只有第三篇文章展示了用于服务器的 GUI。
- 在 .NET 远程处理中应用观察者模式,作者:Liong。
- 用简单的英语解释 .NET 远程处理,作者:Daniel Ang Chee Meng。
- .NET 远程处理示例,作者:Helmut Güldenagel。
- 使用 .NET 远程处理设计一个用于进程密集型分析的分布式系统,作者:Nate D'Anna。
必备组件
本文将让您对如何在 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 上显示该信息。
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
对了解 Greeter
的 SayHello
方法何时被调用感兴趣,因此它为 HelloEvent
事件注册了一个侦听器。您会注意到我们没有使用 RemotingConfiguration.RegisterWellKnownServiceType
将 Greeter
注册为 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
事件时,将调用 Server
的 Server_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
方法完成时,将调用 Client
的 SayHelloCallBack
方法。我们可以通过调用 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
实例化一次,使用接口来强制 Server
和 Client
正确使用 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
。我们可能希望 Server
和 Client
对 Greeter
有单独的视图,以强制正确实现。通过为 Client
和 Server
创建单独的 Greeting 接口,我们可以防止 Client
访问 RespondTime
和 HelloEvent
。
首先,让我们为 Client
创建一个接口,允许 Client
只调用 SayHello
。
public interface IClientGreeter
{
String SayHello(String name);
}
现在,让我们为 Server
创建一个接口,允许它访问 RespondTime
和 HelloEvent
。
public interface IServerGreeter
{
int RespondTime
{
get;
set;
}
event Greeter.HelloEventHandler HelloEvent;
}
Greeter
需要实现这两个接口,并修改 ResponseTime
和 SayHello
的声明方式
public class Greeter : MarshalByRefObject, IServerGreeter, IClientGreeter
...
int IServerGreeter.RespondTime
...
String IClientGreeter.SayHello(String name)
...
现在,我们需要更改 Client
和 Server
访问 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.dll 和 server.exe,而客户端需要 IClientGreeter.dll 和 client.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
- 原始文章。