Generic Client-Server DotNet Remoting with Server CallBack





5.00/5 (5 votes)
Full generic Client-Server remoting Framework with Server Callback notification based on EventArgs
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.
- The interface that the server presents to the client.
- 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:
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:
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:
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 connectsClientDisconnect
: a client disconnectsStarting
: the server is startingWaiting
: the server is waiting, it is possible to cancel the server from the event handlerStopping
: the server is stoppingStopped
: 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.
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:
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 componentTestLibrary
: the common library for server and client test projectsTestServer
: the server projectTestClient
: the client projectTestClientServer
: an application to launch the server and 3 clients
Other Framework Projects
These components will be described in other publications.
FW.Common
: common componentsFW.Diagnostics
: diagnostics components for all kind of projects using log4net and/or DbgViewFW.WinApi
: generic components to access the Windows APIFW.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.
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