How to trace the client - GenuineChannels/GenuineChannels GitHub Wiki

You can intercept connection established or connection closed events. But how to interlink them with your application?

By Dmitry Belikov

In this article I will show how you can manage client-related data with Genuine Channels. Using Client Session is not always convenient. It holds on references and sometimes you certainly should know how to mark some data with a client unique identifier and be able to unmark it when that client gets disconnected. In this article I will try to show how you can mark any data with a client unique identifier, how to fetch this identifier in different cases. That is you will be able to use broad means and patterns such as weak referencing.

Suppose you have intercepted the GlobalEventContainer.GenuineChannelsGlobalEvent event and clients call methods on your server’s objects. You want to trace the lifetime of a specific client. You want to mark the client while the connection is being established, retrieve and update this data when the client calls the method on servers' objects, retrieve and release this data during the disconnection of the client.

You can download an example for Visual Studio 2002 from here. Open the Server\Server.sln solution file and let us go through it.

The known layer is very simple. I declared interfaces I worked with.

/// <summary>
/// Describes a callback called when a message is received.
/// </summary>
public interface IMessageReceiver
{
   /// <summary>
   /// Is called by the server when a message is accepted.
   /// </summary>
   /// <param name="message">A message.</param>
   /// <param name="nickname">Nickname of the client who sent the message.</param>
   object ReceiveMessage(string message, string nickname);
}

/// <summary>
/// Server chat room factory.
/// </summary>
public interface IChatServer
{
   /// <summary>
   /// Logs into the chat room.
   /// </summary>
   /// <param name="iMessageReceiver">Chat message receiver.</param>
   /// <param name="nickname">Nickname.</param>
   /// <returns>Chat room interface.</returns>
   IChatRoom EnterToChatRoom(IMessageReceiver iMessageReceiver,
       string nickname);
}

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

The client class implements IChatClient and outputs received messages. It’s also simple.

The server keeps track of the clients. Please note that IChatRoom.SendMessage does not have the client nickname parameter, therefore the server will have to determine the client nickname on its own at run time.

Firstly, how the server processes GenuineChannels events:

namespace Server
{
   /// <summary>
   /// Chat server implements server that configures Genuine Server
   /// TCP Channel and implements
   /// chat server behavior.
   /// </summary>
   class ChatServer : MarshalByRefObject, IChatServer
   {
      // Contains collection of { client uri => IDictionary }
      // entries.
      private static Hashtable _clients = new Hashtable();

      // Catches Genuine Channels events and removes client session
      // when user disconnects.
      public static void GenuineChannelsEventHandler(object sender,
                   GenuineEventArgs e)
      {
        if (e.SourceException == null)
          Console.WriteLine("Global event: {0}\r\nUrl: {1}", 
                 e.EventType, 
                 e.HostInformation == null ? "<not specified>" : 
                 e.HostInformation.ToString());
        else
          Console.WriteLine("Global event: {0}\r\nUrl: {1}
                                \r\nException: {2}", e.EventType, 
                 e.HostInformation == null ? "<not specified>" : 
                 e.HostInformation.ToString(), 
                 e.SourceException);

         // remember connection DateTime
        if ( e.EventType == 
                  GenuineEventType.GeneralConnectionEstablished )
            ChatServer.FetchClientSession(e.HostInformation.Uri);

         if (e.EventType == 
                  GenuineEventType.GeneralConnectionClosed )
         {
            // remove client session
            lock(_clients)
            {
               IDictionary iDictionary =
                 ChatServer.FetchClientSession(e.HostInformation.Uri);
               string nickname = iDictionary["Nickname"] as string;
               if (nickname != null)
                  Console.WriteLine(...);
              _clients.Remove(e.HostInformation.Uri);
            }
         }
      }

      // Returns or creates thread-safe session (IDictionary) object
      //  corresponding to the current client.
      public static IDictionary FetchClientSession(string clientUri)
      {
         lock(_clients)
         {
            if (clientUri == null)
               clientUri = GenuineUtility.FetchCurrentRemoteUri();
            if (! _clients.ContainsKey(clientUri))
            {
               Console.WriteLine(...);
               IDictionary iDictionary = 
                         Hashtable.Synchronized(new Hashtable());
               iDictionary["Connected"] = DateTime.Now;
               _clients[clientUri] = iDictionary;
            }
            return _clients[clientUri] as IDictionary;
         }
      }
   }
}

When a client connects, the server creates (or makes sure that it’s been already created) a client session. Genuine Channels have absolutely asynchronous design, thus the connection established event can come before or after the first call from that client is made. And you should not think that the session has not been created during the first call. It depends on threading and synchronization details, so you should be ready for this. Actually this is the only problem here, everything else is pretty simple.

You use GenuineUtility.FetchCurrentRemoteUri() to retrieve the client URI during the processing of the invocation. And you receive the client URI in the e.Url member while processing the event.

In my implementation the server stores the client nickname in the session when the client logs on:

/// <summary>
/// Attaches the client.
/// </summary>
/// <param name="iMessageReceiver">Message receiver.</param>
/// <param name="nickname">Nickname.</param>
public void AttachClient(IMessageReceiver iMessageReceiver,
                         string nickname)
{
   if (iMessageReceiver == null)
      throw new ArgumentNullException("iMessageReceiver");

   this._dispatcher.Add((MarshalByRefObject) iMessageReceiver);

   ChatServer.FetchClientSession(null)["Nickname"] = nickname;
}

Each time the server needs to get the client nickname, it gets a client cookie:

/// <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)
{
   // fetch the nickname
   string nickname = ChatServer.FetchClientSession(null)
                                           ["Nickname"] as string;
   Console.WriteLine(...);

   IMessageReceiver iMessageReceiver = (IMessageReceiver) 
                       this._dispatcher.TransparentProxy;
   iMessageReceiver.ReceiveMessage(message, nickname);
}

When a client disconnects, the server waits for TimeoutToRememberClient, then considers the client to be disconnected and releases the client session from its session collection.

// Catches Genuine Channels events and removes client session
// when user disconnects.
public static void GenuineChannelsEventHandler(object sender,
                   GenuineEventArgs e)
{
   Console.WriteLine("Global event: {0}, Url: {1}", 
      e.EventType, e.HostInformation.Uri);

   // remember connection DateTime
   if (e.EventType ==
          GenuineEventType.GeneralConnectionEstablished )
      ChatServer.FetchClientSession(e.HostInformation.Uri);

   if (e.EventType == GenuineEventType.GeneralConnectionClosed)
   {
      // remove client session
      lock(_clients)
      {
         IDictionary iDictionary =
                ChatServer.FetchClientSession(e.HostInformation.Uri);
         string nickname = iDictionary["Nickname"] as string;
         if (nickname != null)
            Console.WriteLine(...);
         _clients.Remove(e.HostInformation.Uri);
      }
   }
}

It is worth mentioning that the returned session is a synchronized Hashtable object. If you only get or set values, you should not bother about synchronization. But you should lock the Hashtable.SyncRoot value if you want to enumerate all entries in it.

Summary

Using GenuineUtility.FetchCurrentRemoteUri() for retrieving client URI during any call and the e.Url member while processing Genuine Channels events allow you to trace a specific client and mark any data with the client URI. The client URI is an absolutely valid URI, you can construct MBR addresses using it, no matter whether the remote host is a client or a server.

In addition, you should definitely consider Client Session possibilities. It's very easy to use it and it is also available during events and calls from the remote host.

⚠️ **GitHub.com Fallback** ⚠️