Click here to Skip to main content
15,884,388 members
Articles / Programming Languages / C#

Generic Client-Server DotNet Remoting with Server CallBack

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
2 Mar 2021CPOL6 min read 6.2K   486   17   1
Full generic Client-Server remoting Framework with Server Callback notification based on EventArgs
This article explains how to implement the remoting Framework with a simple Test example.

Image 1

Introduction

During my professional activity as a Senior Software Engineer for Computer Numerical Control (CNC), I had the opportunity to develop a Framework covering a wide spectrum of applications. This article specifically covers generic Client-Server communication Framework with server callback notifications.

Further articles will focus on diagnostics with log4net and DbgView, common WinForm oriented Framework and a LogViewer application for viewing log files and implementing the Framework as an example.

As simple is beautiful, I've removed everything from the Framework that isn't necessary for this article.

This article describes only what must be understood for the implementation of the generic remoting Framework in a Client-Server communication. The details of the classes are reported to the appreciation of each one.

DotNet Remoting Framework

In a Client-Server communication, what is important for the client is that it is able to connect to the server at any time, whether the server is started before or after the client or that communication is cut for some reason and re-established later. For the server, what is important is that it is able to recognize if a client connects, if it disconnects, or that the communication is cut for some reason or another and re-established later.

The most important thing in Client-Server communication is that the server should be able to return notifications to the client.

Restriction, Limitation

The generic remoting Framework assumes that multiple clients can connect to the server, but the server has only one Sink for all clients. It also assumes that the connection between the client and the server is permanent, no lease.

Client-Server Definitions

Before implementing a client-server communication, it is necessary to define two things.

  1. The interface that the server presents to the client.
  2. The type of notification argument that the server uses to notify the client.

As this information is used by the server and the client, it is necessary to define them in a common project for the client and the server. For the test example in this article, the common project is TestLibrary. The client project is TestClient and the server is TestServer.

The Interface

For this test example, the interface looks like this:

C#
namespace TestLibrary
{
    /// Interface provided by the server for clients.
    public interface ITestInterface : IConnect<EventArgs>
    {
        /// The server will send this message back to the clients asynchronously
        void Execute(string message);

        /// The server will send this message back to the clients asynchronously
        void Execute(Guid clientID, string message);

        /// The server will send this message back to the clients synchronously.
        void ExecuteSync(string message);

        /// The server will generate an exception.
        void ExecuteException();

        /// The server will notify an exception.
        void ExecuteNotifyException();
    }
}

The interface must always derive from IConnect<EventArgs>! The IConnect<EventArgs> interface is implemented in the RemoteServer class server-side, and in the RemoteConnector class client-side. Only your interface is to be implemented using these two base classes.

It is possible to define any method in this interface, but care must be taken that the method parameters used must be serializable. This example defines fife methods but the number is not limited. These methods simply send a message to the server or ask the server to generate exceptions. The server implementation will send the message back to the client via a notification asynchronously or synchronously or generate the exceptions as example.

The Server Notification Argument

The server notification argument looks like this:

C#
namespace TestLibrary
{
    [Serializable]
    public class TestMessageArgs : EventArgs
    {
        /// The message.
        public string Message { get; }

        /// Constructs a TestMessageArgs object.
        public TestMessageArgs(string message)
        {
            Message = message;
        }
    }
}

It should be derived from EventArgs but more importantly it should be serializable.

The Executes(..) message on the server will send back the message as notification to the client using this argument.

Server Exceptions

There are two exception types. Exception by notification or exception thrown by the server. You may catch a server exception on the server and notify the clients that an exception occurs via a NotifyExceptionArgs notification or you may catch a generic server exception on the client. It is possible to define your one exception in the common library, but the exception should be serializable.

Server Implementation

The server implementation looks like this:

C#
namespace TestServer
{
	/// Class to manage a TestServer.
	public class TestServer : RemoteServer<TestSink>
	{
		/// Constructs a TestServer object, the base class has no default constructor.
		public TestServer(IChannel channel)
			: base(channel) { }

		/// Notify all clients
		public void SendNotificationToAllClients(string message)
		{
			Sink?.PerformNotifyClients(new TestMessageArgs(message));
		}
	}
}

The TestServer class derives from the generic RemoteServer<T> and is only used to define the Sink as T parameter.

The application has no access to the sink created by the DotNet remoting but the RemoteServer<T> class provides a T Sink property which is created when and only when the first client connects. Once created, the Sink is permanent.

The RemoteServer<T> class provides events for:

  • ClientConnect: a client connects
  • ClientDisconnect: a client disconnects
  • Starting: the server is starting
  • Waiting: the server is waiting, it is possible to cancel the server from the event handler
  • Stopping: the server is stopping
  • Stopped: the server is stopped

The most important piece of server-side code is the Sink. There is only one Sink in the server by definition. It implements the server Interface.

C#
namespace TestServer
{
    /// Class to manage the Server Sink defined by its Interface.
    public class TestSink : NotifyClientSink, ITestInterface
    {
        /// The server will send the message back to the clients asynchronously.
        public void Execute(string message)
        {
            PerformNotifyClients(new TestMessageArgs(message));
        }

        /// The server will send the message back to the client asynchronously.
        public void Execute(Guid clientID, string message)
        {
            PerformNotifyClient(clientID, new TestMessageArgs(message));
        }

        /// The server will send the message back to the clients synchronously.
        public void ExecuteSync(string message)
        {
            PerformNotifyClientsSync(new TestMessageArgs(message));
        }

        /// The server will generate an exception.
        public void ExecuteException()
        {
            throw new NotImplementedException("ExecuteException");
        }

        /// The server will notify an exception.
        public void ExecuteNotifyException()
        {
            try
            {
                // server code that generates an exception
                throw new ApplicationException("ExecuteNotifyException");
            }
            catch (Exception ex)
            {
                PerformNotifyClients(new NotifyExceptionArgs(ex));
            }
        }
    }
}

The Sink simply notify the clients or one client asynchronously or synchronously that a message is received. It also generates notification exception and server exception.

Attention: If the notification is synchronous, the server is blocked until the client gives the control back.

Client Implementation

The client implementation looks like this:

C#
namespace TestClient
{
    /// <summary>
    /// Class to manage a TestClient
    /// </summary>
    public class TestClient : IDisposable
    {
        private Delay _delay;
        private RemoteConnector<ITestInterface> _remoteConnector;
        private Guid ClientID => _remoteConnector.ClientID;
        private ITestInterface TestProvider => _remoteConnector.RemoteObject;

        public RemoteState RemoteState => _remoteConnector.RemoteState;

        /// Dispose this object
        public void Dispose()
        {
            _remoteConnector?.Dispose();
        }

        /// <summary>
        /// Initialize a remote connector to the server giving the channel.
        /// </summary>
        /// <param name="channel">The channel.</param>
        public void Initialize(IChannel channel)
        {
            // instantiate the client
            _remoteConnector = new RemoteConnector<ITestInterface>(new CHANNEL());
            _remoteConnector.Callback += OnRemoteCallback;
            _remoteConnector.RemoteStateChanged += OnRemoteStateChanged;
        }

        /// <summary>
        /// Notification from the server to the client.
        /// </summary>
        /// <param name="sender">The sender is the client ID.</param>
        /// <param name="e">The event argument.</param>
        private void OnRemoteCallback(object sender, EventArgs e)
        {
            // is notification for me ?
            if ((Guid)sender != ClientID)
                return;

            // the server sends me a message
            if (e is TestMessageArgs args1)
                Console.WriteLine("Message from Server: " + args1.Message);

            // the server sends me an exception
            else if (e is NotifyExceptionArgs args2)
                Console.WriteLine("Exception from Server: " + args2.Exception.Message);
        }

        /// <summary>
        /// Notification when the remote connection is changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="remoteState">The new remote state.</param>
        private void OnRemoteStateChanged(object sender, RemoteState remoteState)
        {
            switch (remoteState)
            {
                case RemoteState.Connected:
                    Console.WriteLine("RemoteState: [" + RemoteState +
                    "] Client [" + ClientID +
                    "] Delay [" + _delay.TotalMilliseconds + "] ms");
                    // execute commands on the server
                    ExecuteCommandsOnServer();
                    break;
                default:
                    Console.WriteLine("RemoteState: [" + RemoteState + "]");
                    break;
            }
        }

        /// Starts the connection with the server.
        public void Start()
        {
            if (_remoteConnector == null)
                throw new ApplicationException("Please call Initialize before Start");
            _delay = new Delay();
            _remoteConnector.Start(10); // wait 10s to connect to the server else exception
        }

        /// Stops the connection with the server.
        public void Stop()
        {
            _remoteConnector?.Stop();
        }

        /// Execute commands on the server.
        private void ExecuteCommandsOnServer()
        {
            // the server will send back notification asynchronously to all clients
            for (int i = 1; i <= 3; i++)
            {
                TestProvider.Execute("Hello TestServer all #" + i);
                Thread.Sleep(500);
            }

            // the server will send back notification asynchronously to me
            for (int i = 1; i <= 3; i++)
            {
                TestProvider.Execute(ClientID, "Hello TestServer me #" + i);
                Thread.Sleep(500);
            }

            // the server will send back notification synchronously to all client
            for (int i = 1; i <= 3; i++)
            {
                TestProvider.ExecuteSync("Hello TestServer Sync all #" + i);
                Thread.Sleep(500);
            }

            // the server will generate an exception
            try
            {
                TestProvider.ExecuteException();
            }
            catch (Exception ex)
            {
                Console.WriteLine("** Server exception: " + ex.Message);
            }

            // execute method on the server, the server will notify an exception
            TestProvider.ExecuteNotifyException();
        }
    }
}

The TestClient class is like a controller. It implements a RemoteConnector<ITestInterface> to connect to the server. The remote connector provides the ITestInterface from the server as TestProvider, the server Callback event, the RemoteStateChanged events from the connector and the client ID. It must implement the IDisposable interface to release the remote connector as the remote connector has a working thread to connect or reconnect to the server.

The Start() method connects to the server. In this example, it will wait max 10 seconds until the connection is established but it is also possible to wait longer or not (param = 0). Once the connection is ready, a RemoteStateChanged event will be generated with a RemoteState.Connected argument. The Delay object informs about the duration of the connection. When ready, the client code ExecuteCommandsOnServer() is executed, but this is only as an example. Client may execute server calls everywhere.

The OnRemoteCallback(..) event checks if the notification is for me. The server can send notification to a specified client as necessary. If more than one client is connected, the server broadcast notifications to all clients.

How to test the example

To test the remoting Framework, start a Client and a Server, stop and restart else the Client or the Server and see how the Client or the Server can reconnect automatically.

Projects in this Article

Generic Client-Server DotNet Remoting Projects

The projects for this Test example are:

  • FW.Remoting: the generic client-server DotNet remoting component
  • TestLibrary: the common library for server and client test projects
  • TestServer: the server project
  • TestClient: the client project
  • TestClientServer: an application to launch the server and 3 clients

Other Framework Projects

These components will be described in other publications.

  • FW.Common: common components
  • FW.Diagnostics: diagnostics components for all kind of projects using log4net and/or DbgView
  • FW.WinApi: generic components to access the Windows API
  • FW.WinForm: generic components for Winform applications

LogViewer WinForm Application

Log4Net simple viewer to display the xlog files generated by the Test applications. This viewer will be described in another publication.

Image 2

Conclusion

If you need a strong client-server remote communication with server Callback, this Framework is what you need!

History

  • 27th February, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Switzerland Switzerland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionMy vote of 5 Pin
cocis482-Mar-21 10:40
cocis482-Mar-21 10:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.