在 .NET Remoting 中应用观察者模式






4.74/5 (29投票s)
2002 年 11 月 25 日
6分钟阅读

217713

3548
本文介绍了使用 .NET Remoting 和观察者模式开发简单聊天应用程序的步骤。
引言
.NET Remoting 允许对象或应用程序以面向对象的方式进行通信。这些应用程序可以驻留在同一台计算机上,也可以驻留在不同的计算机上。本文演示了如何使用 .NET Remoting 利用观察者模式创建简单的客户端和服务器聊天应用程序。
聊天应用程序由多个客户端和一个服务器组成。客户端(如图所示)是一个 GUI 应用程序,而服务器是一个控制台应用程序。当一个客户端发送消息时,消息会路由到服务器,然后服务器将消息广播给所有已连接的客户端。
这种客户端和服务器之间的关系可以通过观察者模式进行建模。观察者模式也称为“发布-订阅”设计模式,由“四人组”在 *《设计模式:可复用面向对象软件的基础》* 一书中提出,它定义了主题(可观察对象)和任意数量的观察者之间的一对多依赖关系。当主题的状态发生变化时,所有观察者都会自动收到通知并进行更新。作为响应,每个观察者都会查询主题以同步其状态与主题的状态。
观察者模式
下面的类图显示了主题和观察者之间的关系。主题可以有一个或多个观察者,并且它提供了一个接口,用于在运行时附加和分离观察者对象。观察者提供了一个更新接口,用于接收来自主题的信号。具体主题存储观察者感兴趣的主题状态,并向其观察者发送通知。具体观察者维护与具体主题的引用,并实现一个更新操作。

下面的协作图更详细地描述了上述对象之间的交互。每当发生可能导致其观察者状态与其自身状态不一致的更改时,具体主题都会通知其观察者。在收到具体主题的更改通知后,具体观察者对象可能会查询主题以获取信息。具体观察者使用此信息来协调其状态与主题的状态。

在 C# 中,主题和观察者接口可以定义如下:
namespace Patterns.Observer
{
public interface ISubject
{
void Attach( IObserver observer );
void Detach( IObserver observer );
bool Notify( string objType, short objState );
}
public interface IObserver
{
bool Update( ISubject sender, string objType,
short objState );
}
}
.NET 远程处理
.NET Remoting 定义了两种对象激活方式:服务器激活和客户端激活。服务器激活对象是在需要时由服务器创建的。有两种类型的服务器激活对象:单例(Singleton)和单调用(Single Call)。Singleton
对象是指无论有多少客户端需要该对象,都始终只有一个实例的对象,并且它们具有默认的生存期。SingleCall
对象是在每次客户端方法调用时创建的。因此,SingleCall
对象的生存期仅限于客户端调用的持续时间。*客户端激活* 对象是指其生存期由调用应用程序域控制的对象,就像该对象位于客户端本地一样。
单调用对象是无状态的,而单例对象则与所有客户端共享状态。客户端激活对象按每个客户端维护状态。本文中的客户端和服务器聊天应用程序使用单例服务器激活模式。
任何对象都可以通过继承 MarshalByRefObject
来变成远程对象。当客户端激活远程对象时,它会获得该远程对象的代理。对该代理的所有操作都会被相应地间接化,以便远程基础结构能够适当地拦截和转发调用。
下面的类 ChatServerObject
和 ChatClientObject
继承自 MarshalByRefObject
,并分别实现了 ISubject
和 IObserver
。这些类的创建遵循*观察者模式* 部分中解释的 ConcreteSubject
和 ConcreteObserver
类。ChatClientObject
实现了 IObserver
,因此它可以调用 ChatServerObject
的 Attach 函数来向服务器 ChatServerObject
注册自己。ChatServerObject
的成员变量 clients 包含已注册到 ChatServerObject
的观察者或 ChatClientObject
对象的列表。
namespace ChatApplication
{
public class ChatServerObject : MarshalByRefObject, ISubject
{
private ArrayList clients = new ArrayList();
public void SetValue( string clientData )
{
Notify( clientData, 0 );
}
public void Attach( IObserver client )
{
Console.WriteLine( "observer attached" );
clients.Add( client );
}
public void Detach( IObserver client )
{
clients.Remove( client );
}
public bool Notify( string clientData, short objState )
{
for ( int i = 0; i < clients.Count; i++ )
((IObserver) clients[i]).Update( this, clientData, objState );
return true;
}
}
public class ChatClientObject : MarshalByRefObject, IObserver
{
private ArrayList newData = new ArrayList();
public bool Update( ISubject sender, string data, short objState )
{
newData.Add( data );
return true;
}
public int GetData( out string[] arrData )
{
int len = 0;
arrData = new String[newData.Count];
newData.CopyTo( arrData );
newData.Clear();
len = arrData.Length;
return len;
}
}
}
现在,我们将创建一个托管 ChatServerObject 对象的服务器。如上所述,服务器是一个控制台应用程序。如果将 /config
作为参数传递,服务器将从“server.config”文件加载配置。否则,它将在运行时配置远程参数。
在运行时,它必须首先创建一个用于主题和观察者之间,或服务器和客户端之间通信的通道。通道用于在远程对象之间传输消息。当客户端调用远程对象上的方法时,参数以及与调用相关的其他详细信息会通过通道传输到远程对象。调用结果会以相同的方式返回给客户端。客户端可以选择注册在“服务器”上的任何通道来与远程对象通信。通道是一种数据类型,它接收数据流,根据特定的网络协议创建数据包,然后将数据包发送到另一台计算机。通道可以是单向的,也可以是双向的。.NET 框架提供了两种类型的通道:TcpChannel 和 HttpChannel。在此应用程序中,使用了 TCP 通道,它监听端口 9000。
创建 TCP 通道后,必须通过调用 ChannelServices.RegisterChannel( chan )
进行注册。然后,通过调用 RemotingConfiguration.RegisterWellKnownServiceType
函数将 ChatApplication.ChatServerObject
对象类型注册为服务端的对象类型,该类型可以在客户端请求时激活。此调用的最后一个参数指定激活类型为 Singleton。
class ChatServer
{
[STAThread]
static void Main(string[] args)
{
if ( args.Length == 1 )
{
if ( args[0].CompareTo("/config") == 0 )
{
RemotingConfiguration.Configure( "server.config" );
}
}
else
{
TcpChannel chan = new TcpChannel( 9000 );
ChannelServices.RegisterChannel( chan );
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(ChatApplication.ChatServerObject),
"ChatServer", WellKnownObjectMode.Singleton );
}
System.Console.WriteLine( "Hit <enter> to exit..." );
System.Console.ReadLine();
}
}
配置文件采用 XML 格式。“server.config”服务器配置文件将应用程序名称指定为“ChatServer”。服务是已知的服务类型“Singleton”,其 URI 为“Chatserver”。客户端在连接到此对象时可以指定此 URI。在“channel”元素中,定义了端口 9000 和“TCP”通道类型。
该对象的默认生存期使用
<configuration> <system.runtime.remoting> <application name="ChatServer"> <service> <wellknown mode="Singleton" type="ChatServerObject, ChatObjects" objectUri="Chatserver" /> </service> <channels> <channel port="9000" ref="tcp" /> </channels> <lifetime leaseTime="1M" renewOnCallTime="2M" /> </application> </system.runtime.remoting> </configuration>
客户端应用程序的初始化过程非常相似。客户端应用程序可以加载配置文件中包含的远程参数,或者在运行时配置它们。要在运行时配置远程参数,用户可以按下“Connect”按钮,然后将执行以下代码。
它首先检查“use configuration”复选框是否被选中。如果选中,则从文件加载配置。否则,注册 TCP 通道以与服务器通信。将零传递给 TcpChannel 以指示全双工或双向通信。与上面的服务器应用程序一样,必须注册通道。Activator.GetObject
函数调用创建远程对象的代理。在这种情况下,它会创建一个 ChatApplication.ChatServerObject
类型的代理。
private void buttonConnect_Click(object sender, System.EventArgs e)
{
if ( useConfigCheckBox.Checked )
{
RemotingConfiguration.Configure( "client.config" );
chatServer = new ChatApplication.ChatServerObject();
}
else
{
if ( textServer.Text.Length <= 0 )
{
MessageBox.Show( "Please enter the target server name or address" );
return;
}
chan = new TcpChannel(0);
ChannelServices.RegisterChannel( chan );
string url = String.Format( "tcp://{0}:9000/ChatServer",
textServer.Text );
chatServer = (ChatApplication.ChatServerObject)
Activator.GetObject( typeof(
ChatApplication.ChatServerObject ), url );
}
if ( chatServer == null )
MessageBox.Show( "Could not locate server" );
else
{
chatClient = new ChatApplication.ChatClientObject();
try
{
chatServer.Attach( chatClient );
buttonConnect.Enabled = false;
}
catch( SocketException sockExp )
{
MessageBox.Show( sockExp.Message );
}
}
}
}
客户端应用程序有一个计时器,每 500 毫秒执行一次 TimerOnTick 函数。TimerOnTick 函数只是调用 chatClient.GetData 函数来检索 chatClient 中的所有数据,然后将它们显示在列表中。
void TimerOnTick( object obj, EventArgs ea )
{
if ( chatClient != null )
{
string[] arrData;
chatClient.GetData( out arrData );
for ( int i = 0; i < arrData.Length; i++ )
listData.Items.Add( arrData[i] );
listData.SelectedIndex = listData.Items.Count-1;
}
}
摘要
本文是关于 .NET Remoting 的概述。它展示了如何使用 .NET Remoting 开发分布式应用程序,并在过程中应用观察者模式。希望您像我一样喜欢这篇文章!