Sample view of a single server host communicating with two clients:
Introduction
Some time ago, I investigated the process of creating a mechanism that would allow me to create an effective two-way communication between two separate applications on different machines. There aren't that many articles that describe this process in detail, and the ones I found stopped short at raising an event on a server machine which would pass a signal back to the client.
In this article, I have extended this, and included a way for any number of clients to register their existence at a host machine, that will then allow the host to communicate back via any user defined type. The sample server and client app I made should resemble a MSN chat type of setup, for demonstration purposes.
I have used the principles demonstrated in this article to allow me to automatically register client applications on start up and verify their license and/or login credentials. Since each client deposits a callback to the client app, I can send messages back to the global client base, or a specific user, or ... (much more fun) perform a forced shut-down in case back-end databases need work doing and all users should have logged out.
Using the app
Start up the server and client apps on different machines (or on the same localhost), and make sure the machine names are correctly set. Then, enter a user ID, and hit Register which will register your existence at the server. The client and server are now able to communicate to one another.
Design discussion
Please note that many excellent articles are already available on Remoting and the various ways of configurations, so I will largely skip over that part in this discussion. Details on how I have configured remoting are described via comment blocks within the code itself. The way I have set up this two-way communication is by having separate client and server executables. Then, there are three additional classes necessary: CommsInfo
, ServerTalk
, and CallbackSink
; see figure below.
- The first one is the
CommsInfo
class that is decorated with the Serializable
attribute, so any instance of this type moves freely between the server and the client. These objects are used as the content of the messages between the server/client.
- The second class we need is a class called
ServerTalk
that extends MarshalByRefObject
so each ServerTalk
object created is anchored on the server. Because each instance is anchored on the server, it allows the server to simply hook itself to the static events exposed by this class, which are then invoked by the actual instances of this class - even remotely via a proxy! Through this mechanism, we will allow communication from a client to the server to take place. The information passed is encapsulated within the CommsInfo
class which can be extended to incorporate much more variables as long as each variable is serializable in its own right!!
- The third and the last class we need is a class that facilitates the callback to the client. A direct callback to a method within the client can not be used because the server has no type declaration of the type of where the callback would point to. Even copying the entire client within the server space would simply cause the server to execute the callback on that client instead of the actual remote client! Therefore, a third class,
CallbackSink
, is created. This class, again, extends the MarshalByRefObject
, so it will be anchored at the place where an instance is actually created - in this case, the client!
This class exposes an event that the client hooks into to listen for messages from the server. The client also registers a callback to the HandleToClient
method within this instance of CallbackSink
during registration, via the ServerTalk
proxy, so the server now has a valid handle to a method of an object that was created and lives at the client. This works since the server already knows about the CallbackSink
type, and when being passed a callback to a method on this type, it is actually being given the proxy to this type, which again works since this object is anchored on the client!
So, we ended up with a server object (ServerTalk
) that lives at the server, and a client that has a handle on this via a proxy, and vice versa; the client has created an instance of CallbackSink
that the server got a handle on but is anchored at the client. The server listens for events via static members of ServerTalk
. The client listens by simply hooking into the event of the CallbackSink
instance.
Since both the server and the client need to be aware of the above described three classes, we need to group them into a separate assembly and ensure that both have references to this. So we need to see this assembly in the bin\debug on both the server and the client!!
Now, we have a good understanding of the approach and overall design, let's look at the full code-listings below.
The shared assembly code
The class below describes the ServerTalk
object:
public delegate void delUserInfo(string UserID);
public delegate void delCommsInfo(CommsInfo info);
public class ServerTalk : MarshalByRefObject
{
private static delUserInfo _NewUser;
private static delCommsInfo _ClientToHost;
private static List<ClientWrap> _list =
new List<ClientWrap>();
public void RegisterHostToClient(string UserID,
delCommsInfo htc)
{
_list.Add(new ClientWrap(UserID, htc));
if (_NewUser != null)
_NewUser(UserID);
}
public static delUserInfo NewUser
{
get { return _NewUser; }
set { _NewUser = value; }
}
public static delCommsInfo ClientToHost
{
get { return _ClientToHost; }
set { _ClientToHost = value; }
}
public static void RaiseHostToClient(string UserID,
string Message)
{
foreach (ClientWrap client in _list)
{
if ((client.UserID == UserID || UserID == "*")
&& client.HostToClient != null)
client.HostToClient(new CommsInfo(Message));
}
}
public void SendMessageToServer(CommsInfo Message)
{
if (_ClientToHost != null)
_ClientToHost(Message);
}
private class ClientWrap
{
private string _UserID = "";
private delCommsInfo _HostToClient = null;
public ClientWrap(string UserID,
delCommsInfo HostToClient)
{
_UserID = UserID;
_HostToClient = HostToClient;
}
public string UserID
{
get { return _UserID; }
}
public delCommsInfo HostToClient
{
get { return _HostToClient; }
}
}
}
The CommsInfo
class below facilitates the content exchange between the server and the client. It currently contains a single property 'Message
' of type string
which can easily be extended to carry as much or as little information as is needed. The last class we need is the CallbackSink
class enabling the server to callback to the client, which is further explained in The server code section.
[Serializable()]
public class CommsInfo
{
private string _Message = "";
public CommsInfo(string Message)
{
_Message = Message;
}
public string Message
{
get { return _Message; }
set { _Message = value; }
}
}
public class CallbackSink : MarshalByRefObject
{
public event delCommsInfo OnHostToClient;
public CallbackSink()
{ }
[OneWay]
public void HandleToClient(CommsInfo info)
{
if (OnHostToClient != null)
OnHostToClient(info);
}
}
The server code
public fServer()
{
InitializeComponent();
RegisterChannel();
ServerTalk.NewUser = new delUserInfo(NewUser);
ServerTalk.ClientToHost = new delCommsInfo(ClientToHost);
}
private void NewUser(string UserID)
{
if (this.cboUsers.InvokeRequired)
this.cboUsers.Invoke(new delUserInfo(NewUser),
new object[] { UserID });
else
{
this.cboUsers.Items.Add(UserID);
this.cboUsers.Text = UserID;
}
}
private void ClientToHost(CommsInfo Info)
{
if (this.txtFromClient.InvokeRequired)
this.txtFromClient.Invoke(new
delCommsInfo(ClientToHost),
new object[] { Info });
else
this.txtFromClient.Text = "from client: " +
Info.Message + Environment.NewLine +
this.txtFromClient.Text;
}
private void RegisterChannel()
{
SoapServerFormatterSinkProvider serverFormatter =
new SoapServerFormatterSinkProvider();
serverFormatter.TypeFilterLevel =
System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
Hashtable ht = new Hashtable();
ht["name"] = "ServerChannel";
ht["port"] = 9000;
HttpChannel channel = new HttpChannel(ht, null, serverFormatter);
ChannelServices.RegisterChannel(channel, false);
string identifier = "TalkIsGood";
WellKnownObjectMode mode = WellKnownObjectMode.Singleton;
WellKnownServiceTypeEntry entry =
new WellKnownServiceTypeEntry(typeof(ServerTalk),
identifier, mode);
RemotingConfiguration.RegisterWellKnownServiceType(entry);
}
private void btnSend_Click(object sender, EventArgs e)
{
if (cboUsers.Items.Count == 0)
{
MessageBox.Show("No registered Users!", "No users");
return;
}
string UserID = cboUsers.Text;
if (chkAll.Checked) UserID = "*";
ServerTalk.RaiseHostToClient(UserID, this.txtToClient.Text);
this.txtFromClient.Text = "To client " + UserID + ": " +
this.txtToClient.Text + Environment.NewLine +
this.txtFromClient.Text;
}
The client code
The class below describes the client process. When the registration takes place, a callback is given, pointing to a method on the CallbackSink
instance which is anchored on the client. We then attach an event handler relaying the CommsInfo
to our client executable.
The client should also open a channel in order to allow the server to communicate back. In this case, I am opening port 9001 over an HttpChannel
. The actual port number is not important as long as it is over 1024 and the port number is not exposed on the server (i.e., 9000). I use the Activator
's GetObject
static method to ask the server to supply me with an instance of the ServerTalk
object. Since it comes back as type object
, I need to cast this back to my known type of ServerTalk
. Now that we have the ServerTalk
proxy, we can register our existence. The activation URL identifies the channel type of the server and the server machine name called 'marcel'. Clearly replace this with your machine name. TalkIsGood
is the Well Known Object (WKO) string the server knows, and will map to the ServerTalk
class, allowing it to create the actual instance.
private ServerTalk _ServerTalk = null;
private CallbackSink _CallbackSink = null;
private void btnRegister_Click(object sender, EventArgs e)
{
_CallbackSink = new CallbackSink();
_CallbackSink.OnHostToClient +=
new delCommsInfo(_EventSink_OnHostToClient);
HttpChannel channel = new HttpChannel(9001);
ChannelServices.RegisterChannel(channel, false);
object obj = Activator.GetObject(typeof(ServerTalk),
"http://marcel:9000/TalkIsGood");
_ServerTalk = (ServerTalk)obj;
_ServerTalk.RegisterHostToClient(this.txtUserID.Text,
new delCommsInfo(CallbackSink.HandleToClient));
btnRegister.Enabled = false;
}
void CallbackSink_OnHostToClient(CommsInfo info)
{
if (this.txtFromServer.InvokeRequired)
this.txtFromServer.Invoke(new delCommsInfo(
CallbackSink_OnHostToClient),
new object[] { info });
else
this.txtFromServer.Text = "From server: " +
info.Message + Environment.NewLine +
this.txtFromServer.Text;
}
private void btnSend_Click(object sender, EventArgs e)
{
_ServerTalk.SendMessageToServer(new
CommsInfo(this.txtToServer.Text));
this.txtFromServer.Text = "To server: " +
this.txtToServer.Text + Environment.NewLine +
this.txtFromServer.Text;
}
Threading issues
The prudent threading expert will undoubtedly have spotted a design flaw that can cause subtle bugs to occur. Since the server hooks into static events exposed by the ServerTalk
class that can be invoked by many instances of this class, there is the potential of threading conflicts. I am speaking from experience since when I tried to use this design to log database requests that are taking place on a middle-ware Business Objects server, unexpected threading exceptions were thrown which are difficult to trace since they never occur at set scenarios or intervals!
To address this problem, I introduced a static synchronized Queue
within the ServerTalk
class where many threads (instances of this class, i.e., clients) can deposit their CommsInfo
objects that should be passed on to the server. The ServerTalk
class also spins off a worker thread whose single task in life is to monitor for contents within the Queue
. If any objects are found on this Queue
(of type CommsInfo
), it will dequeue these and pass it on to the event (or any other call-back deposited to the server).
public class ServerTalk : MarshalByRefObject
{
static ServerTalk()
{
Thread t = new Thread(new ThreadStart(CheckClientToServerQueue));
t.IsBackground = true;
t.Start();
}
private static Queue _ClientToServer =
Queue.Synchronized(new Queue());
public void SendMessageToServer(CommsInfo Message)
{
_ClientToServer.Enqueue(Message);
}
private static void CheckClientToServerQueue()
{
while (true)
{
Thread.Sleep(50);
if (_ClientToServer.Count > 0)
{
CommsInfo message =
(CommsInfo)_ClientToServer.Dequeue();
if (_ClientToHost != null)
_ClientToHost(message);
}
}
}
}
A second threading issue that needs to be resolved is the arrival of a CommsInfo
object that per definition has been created on a different thread and thus needs to be marshaled in order to process its content. This can easily be done by using the InvokeRequired
property of the target control. If this signals it can't deal with the incoming CommsInfo
object, then this object should be send again to itself on the Invoke
method, which will re-route the request on the thread that actually created the target control, which in most cases will be the main UI thread.
Object lifetime - Important!
In response to fred pittroff's valid comment that 'after a certain amount of inactivity, the server can no longer communicate to the client but can still accept messages from the client', I am inserting the response I wrote to him:
The reason for this behaviour is the ServerTalk
object lifetime.
It takes five minutes to be precise - that is, when the default Lease expires, and the server offers the actual ServerTalk
object to the Garbage Collector. Since I am creating instances of the ServerTalk
class on the server as WKO (Well Known Object) singleton objects, they will hold state (same would apply to client activated objects). Single-call objects would be destroyed after each call, which therefore won't work in the above article.
The server initially has no idea of how long the ServerTalk
instances should be kept alive, and therefore, will offer them to the Garbage Collector after 5 minutes of inactivity.
The .NET framework creates a Lease for each object that is created outside the boundaries of the application domain. A lease will have a default life-time of 5 minutes, and when its time is up, it will be removed.
There are a number of ways of renewing the Lease of a remote object and thus preventing the server from removing the object:
- As mentioned before, an implicit renewal is performed by simply interacting with the object.
- Another way is to pick up the
ILease
object that is created by the framework via the proxy (so on the client side), like this:
ILease lease = (ILease)_ServerTalk.GetLifetimeService();
(ILease
lives in the System.Runtime.Remoting.Lifetime
namespace) and call its Renew()
method which will give us another 5 minutes.
- One can also override the
InitializeLifetimeService()
within the ServerTalk
class (which it inherited from the MarshalByRefObject
), and give each instance a default lifespan of, say, 8 hours.
public override object InitializeLifetimeService()
{
ILease lease = (ILease) base.InitializeLifetimeService();
lease.InitialLeaseTime = new TimeSpan(8, 0, 0);
return lease;
}
- The last way I can think of now is to create a sponsor class that implements
ISponsor
, and register this sponsor object to the ILease
using the Register()
method.
Right then, that's it. Whilst I have enjoyed putting together this article, I realise that this has simply been my approach to address effective communication between a host server and many clients using .NET's powerful remoting. Feel free to chip in with ideas of how things could be improved, and any other feedback is much appreciated!
History
- 19 April 2006 - Introduced this article.
- 16 July 2006 - Update on background threading issue, and insertion of the Object lifetime paragraph.