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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (29投票s)

2002 年 11 月 25 日

6分钟阅读

viewsIcon

217713

downloadIcon

3548

本文介绍了使用 .NET Remoting 和观察者模式开发简单聊天应用程序的步骤。

Sample Image - ChatClients.gif

引言

.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 来变成远程对象。当客户端激活远程对象时,它会获得该远程对象的代理。对该代理的所有操作都会被相应地间接化,以便远程基础结构能够适当地拦截和转发调用。

下面的类 ChatServerObjectChatClientObject 继承自 MarshalByRefObject,并分别实现了 ISubjectIObserver。这些类的创建遵循*观察者模式* 部分中解释的 ConcreteSubjectConcreteObserver 类。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”通道类型。

该对象的默认生存期使用元素指定。“leaseTime”属性指定对象在租约管理器开始删除对象之前保留在内存中的初始时间跨度。默认值为 5 分钟。

<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 开发分布式应用程序,并在过程中应用观察者模式。希望您像我一样喜欢这篇文章!

© . All rights reserved.