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

D.NET Remoting - 事件。事件?事件!

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.05/5 (56投票s)

2003年11月4日

5分钟阅读

viewsIcon

214870

downloadIcon

5626

您有服务器和几个客户端。您希望服务器触发一个事件,并且所有客户端或仅部分特定客户端必须接收它。本文介绍了解决该问题的几种方法。

引言

您有服务器和几个客户端。您希望服务器触发一个事件,并且所有客户端或仅部分特定客户端必须接收它。本文介绍了解决该问题的几种方法。

就事件而言,我特别指满足以下陈述的过程:

  1. 调用者向多个接收者发送相同的消息。
  2. 所有调用都并发执行。
  3. 调用者最终会知道接收者的回复。

我希望第一条陈述不需要任何额外的解释。所有调用都并发执行。如果与特定客户端的连接很慢(或已断开),发送给其他客户端的消息不会延迟,直到该特定客户端回复(或服务器通过超时识别出客户端的不可用性)。第三条陈述源于实际的业务需求。通常,调用者需要知道接收者是否成功收到了消息。如果可能,也最好收集接收者的回复。

使用代码(第一种解决方案)。.NET 原生事件

让我们研究第一个示例。众所周知的层包含委托声明和公开可用的事件。

/// <summary>
/// Is called by the server when a message is sent.
/// </summary>
public delegate void MessageDeliveredEventHandler(string message); 

/// <summary>
/// ChatRoom provides common methods for the chatting.
/// </summary>
public interface IChatRoom
{
   /// <summary>
   /// Sends the message to all clients.
   /// </summary>
   /// <param name="message">Message to send.</param>
   void SendMessage(string message);   

   /// <summary>
   /// Message delivered event.
   /// </summary>
   event MessageDeliveredEventHandler MessageDelivered;
}

在我的实现中,调用者调用 SendMessage 方法,该方法又触发事件。此调用也可以由客户端直接进行。

客户端创建指向 MarshalByRefObject 派生类的委托实例,并将处理程序添加到事件。这里唯一的问题是委托应指向众所周知的类,因此作为一种解决方法,我声明了一个众所周知的类,该类仅调用后期绑定的方法(MessageReceiver 类)。

IChatRoom iChatRoom = (IChatRoom) Activator.GetObject
  (typeof(IChatRoom), "gtcp://127.0.0.1:8737/ChatRoom.rem");
iChatRoom.MessageDelivered += 
  new MessageDeliveredEventHandler(messageReceiver.MessageDelivered); 

// ... ask user to enter the message
// and force the event
iChatRoom.SendMessage(str);

服务器提供实现 IChatRoom 接口的实例。

/// <summary>
/// Sends the message to all clients.
/// </summary>
/// <param name="message">Message to send.</param>
public void SendMessage(string message)
{
   Console.WriteLine("\"{0}\" message will be sent to all clients.", 
                                                          message);    

   if (this.MessageDelivered != null)
      this.MessageDelivered(message);
}   

/// <summary>
/// Message delivered event.
/// </summary>
public event MessageDeliveredEventHandler MessageDelivered;

通过这种方法,我们最终得到了什么?

优点

  • 如果所有业务对象都位于众所周知的层中,则实现起来非常容易。

缺点

  • 对于位于“客户端未知”DLL 中的业务对象,需要后期绑定。
  • 调用是连续进行的。只有当前一个客户端返回结果后,才会调用下一个客户端。
  • 如果客户端无法访问或引发异常,则调用将停止,所有剩余的客户端将不会收到消息。
  • 您应单独管理赞助。

您可以将 MessageReceiver.MessageDelivered 方法标记为 OneWay 属性以解决第二个问题。但您应该明白,在这种情况下无法获得调用结果。断开连接的客户端永远不会从事件的接收者列表中排除。这就像内存泄漏。

[OneWay]
public void MessageDelivered(string message)
{
   if (this.MessageDeliveredHandler != null)
   this.MessageDeliveredHandler(message);
}

摘要

这种方案完全不可接受。它速度慢、不可靠,并且不适合我的条件。您可以将此方案用于生命周期短、客户端数量不多且每个客户端都有可能中断事件过程的事务。

使用代码(第二种解决方案)。基于接口的方法

让我们研究第二个示例。已知的层包含事件提供程序接口和客户端接收者接口。

/// <summary>
/// Describes a callback called when a message is received.
/// </summary>
public interface IChatClient
{
   /// <summary>
   /// Is called by the server when a message is accepted.
   /// </summary>
   /// <param name="message">A message.</param>
   object ReceiveMessage(string message);
}  

/// <summary>
/// ChatRoom provides common methods for chatting.
/// </summary>
public interface IChatRoom
{
   /// <summary>
   /// Sends the message to all clients.
   /// </summary>
   /// <param name="message">Message to send.</param>
   void SendMessage(string message);   

   /// <summary>
   /// Attaches a client.
   /// </summary>
   /// <param name="iChatClient">Receiver that 
   /// will receive chat messages.</param>
   void AttachClient(IChatClient iChatClient);
}

IChatClient 接口必须由任何想要接收聊天消息的对象实现。客户端类实现 IChatClient 接口。

namespace Client {
   class ChatClient : MarshalByRefObject, IChatClient {
      static void Main(string[] args) {
         // client attaches to the event
         IChatRoom iChatRoom = (IChatRoom) Activator.GetObject(typeof
            (IChatRoom), "gtcp://127.0.0.1:8737/ChatRoom.rem");
         iChatRoom.AttachClient(new ChatClient());

         //... and asks user to enter a message.

         // Then fires the event
         iChatRoom.SendMessage(str);

         //...
      }
   }
}

服务器实现 IChatRoom 接口并允许附加客户端。我只将客户端保存在哈希表中,因为我想快速删除失败的接收者。我在下面的代码片段中添加了其他注释。

class ChatServer : MarshalByRefObject, IChatRoom
{
   /// <summary>
   /// Contains entries of MBR uri =>
   /// client MBR implementing IChatClient interface.
   /// </summary>
   static Hashtable _clients = new Hashtable();   

   /// <summary>
   /// Attaches the client.
   /// </summary>
   /// <param name="iChatClient">Client to be attached.</param>
   public void AttachClient(IChatClient iChatClient)
   {
      if (iChatClient == null)
         return ;    

      lock(_clients)
      {
         //****************
         // I just register this receiver under MBR uri. So I can 
         // find and perform an 
         // operation or remove it quickly at any time I will need it.
         _clients[RemotingServices.GetObjectUri((MarshalByRefObject)
            iChatClient)] = iChatClient;
      }
   }   

   /// <summary>
   /// To kick off the async call.
   /// </summary>
   public delegate object ReceiveMessageEventHandler(string message);

   /// <summary>
   /// Sends the message to all clients.
   /// </summary>
   /// <param name="message">Message to send.</param>
   /// <returns>Number of clients having received this 
   /// message.</returns>
   public void SendMessage(string message)
   {
      lock(_clients)
      {
         Console.WriteLine("\"{0}\" message will be sent to all clients.", 
                                                                message);
         AsyncCallback asyncCallback = new AsyncCallback
            (OurAsyncCallbackHandler);

         foreach (DictionaryEntry entry in _clients)
         {
            // get the next receiver
            IChatClient iChatClient = (IChatClient) entry.Value;
            ReceiveMessageEventHandler remoteAsyncDelegate = new
               ReceiveMessageEventHandler(iChatClient.ReceiveMessage);

            // make up the cookies for the async callback
            AsyncCallBackData asyncCallBackData = 
               new AsyncCallBackData();
            asyncCallBackData.RemoteAsyncDelegate = 
               remoteAsyncDelegate;
            asyncCallBackData.MbrBeingCalled = 
               (MarshalByRefObject) iChatClient; 

            // and initiate the call
            IAsyncResult RemAr = remoteAsyncDelegate.BeginInvoke
               (message, asyncCallback, asyncCallBackData);
         }
      }  
   }

   // Called by .NET Remoting when async call is finished.
   public static void OurAsyncCallbackHandler(IAsyncResult ar)
   {
      AsyncCallBackData asyncCallBackData = (AsyncCallBackData)
            ar.AsyncState;    

      try
      {
         object result = 
             asyncCallBackData.RemoteAsyncDelegate.EndInvoke(ar);

         // the call is successfully finished and 
         // we have call results here    
      }
      catch(Exception ex)
      {
         // The call has failed.
         // You can analyze an exception 
         // to understand the reason.
         // I just exclude the failed receiver here.
         Console.WriteLine("Client call failed: {0}.", ex.Message);
         lock(_clients)
         {
            _clients.Remove( RemotingServices.GetObjectUri
               (asyncCallBackData.MbrBeingCalled) );
         }
      }
   }
}

优点

  • 所有调用都并发进行。
  • 失败的接收者不会影响其他接收者。
  • 您可以对失败的接收者制定任何策略。
  • 您知道调用的结果,并且可以收集 refout 参数。

缺点

  • 比第一种情况复杂得多。

摘要

如果您需要实现事件并且正在使用原生通道,那么这正是您应该使用的模式。我在这里没有实现附加和分离赞助商,但如果您的客户端不持有接收者,您绝对应该考虑。

使用代码(第三种解决方案)。广播引擎

现在让我们使用 Genuine Channels 来完成同样的事情。这种方法看起来像前一种。但它在服务器端更简单,并且内部实现完全不同。

已知的层和客户端具有完全相同的实现。我们只会在服务器端发现区别。

服务器构造一个 Dispatcher 实例,该实例将包含接收者列表。

private static Dispatcher _dispatcher = new Dispatcher(typeof
   (IChatClient));
private static IChatClient _caller;

为了执行完全异步处理,服务器附加一个处理程序并打开异步模式。

static void Main(string[] args)
{
   //...
   _dispatcher.BroadcastCallFinishedHandler += new
      BroadcastCallFinishedHandler
         ( ChatServer.BroadcastCallFinishedHandler );
   _dispatcher.CallIsAsync = true;
   _caller = (IChatClient) _dispatcher.TransparentProxy;

   //...
}

每次客户端想要接收消息时,服务器将其放入 dispatcher 实例。

/// <summary>
/// Attaches the client.
/// </summary>
/// <param name="iChatClient">Client to attach.</param>
public void AttachClient(IChatClient iChatClient)
{
   if (iChatClient == null)
      return ;    

   _dispatcher.Add((MarshalByRefObject) iChatClient);
}

当服务器想要触发事件时,它只需调用提供的代理上的方法。此调用将自动发送给所有注册的接收者。

/// <summary>
/// Sends message to all clients.
/// </summary>
/// <param name="message">Message to send.</param>
/// <returns>Number of clients having received this message.</returns>
public void SendMessage(string message)
{
   Console.WriteLine("\"{0}\" message will be sent to all clients.",
      message);    
   _caller.ReceiveMessage(message);
}

在我的示例中,我忽略了调用结果。无论如何,Dispatcher 默认会在第四次失败后自动排除失败的接收者。但如果我想这样做,我会写如下内容:

public void BroadcastCallFinishedHandler(Dispatcher dispatcher, 
               IMessage message, ResultCollector resultCollector)
{
   lock(resultCollector)
   {
      foreach(DictionaryEntry entry in resultCollector.Successful)
      {
         IMethodReturnMessage iMethodReturnMessage = 
            (IMethodReturnMessage) entry.Value;

         // here you get client responses
         // including out and ref parameters
         Console.WriteLine("Returned object = {0}", 
            iMethodReturnMessage.ReturnValue.ToString());
      }

      foreach(DictionaryEntry entry in resultCollector.Failed)
      {
         string mbrUri = (string) entry.Key;
         Exception ex = null;
         if (entry.Value is Exception)
            ex = (Exception) entry.Value;
         else
            ex = ((IMethodReturnMessage) entry.Value).Exception;
         MarshalByRefObject failedObject = 
            dispatcher.FindObjectByUri(mbrUri);

         Console.WriteLine("Receiver {0} has failed. Error: {1}", 
                                            mbrUri, ex.Message);    
         // here you have failed MBR object (failedObject)
         // and Exception (ex)
      }
   }
}

您在此处收集了所有结果,因此您可以做出任何决定。

广播引擎具有同步模式。在此模式下,所有调用都会并发进行,但它会等待所有客户端回复或超时到期。有时这非常有用,但此模式会占用一个线程直到调用完成。请查看 Programming Guide,其中包含更多详细信息。

优点

  • 比第二种方法更容易使用。
  • 所有调用都并发进行。
  • 失败的接收者不会影响其他接收者。
  • 广播引擎会自动识别接收者可以通过真正的广播通道接收消息的情况。如果接收者未能通过真正的广播通道接收消息,广播引擎会通过普通通道重复发送消息。因此,您可以轻松地利用 IP 组播。
  • 广播引擎负责处理已附加 MBR 接收者的赞助。

缺点

  • 您仍然需要声明一个众所周知的接口才能实现事件。
© . All rights reserved.