Click here to Skip to main content
15,867,328 members
Articles / Programming Languages / C#
Article

Two-way Remoting with Callbacks and Events, Explained

Rate me:
Please Sign up or sign in to vote.
4.98/5 (65 votes)
19 Jul 2006CPOL9 min read 409K   7.4K   267   142
A demonstration of how separate applications on different machines are able to effectively communicate with one another.

Sample view of a single server host communicating with two clients:

Sample image

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.

Sample image

  • 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:

C#
public delegate void delUserInfo(string UserID);
public delegate void delCommsInfo(CommsInfo info);

// This class is created on the server and allows
// for clients to register their existance and
// a call-back that the server uses to communicate back.
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)
        // make sure there is a delegate to call!
            _NewUser(UserID);
    }

    /// <SUMMARY>
    /// The host should register a function
    /// pointer to which it wants a signal
    /// send when a User Registers
    /// </SUMMARY>
    public static delUserInfo NewUser
    {
        get { return _NewUser; }
        set { _NewUser = value; }
    }

    /// <SUMMARY>
    /// The host should register a function pointer
    /// to which it wants the CommsInfo object
    /// send when the client wants
    /// to communicate to the server
    /// </SUMMARY>
    public static delCommsInfo ClientToHost
    {
        get { return _ClientToHost; }
        set { _ClientToHost = value; }
    }

    // The static method that will be invoked
    // by the server when it wants to send a message
    // to a specific user or all of them.
    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));
        }
    }

    // This instance method allows a client
    // to send a message to the server
    public void SendMessageToServer(CommsInfo Message)
    {
        if (_ClientToHost != null)
        // make sure there is a delegate to call!
            _ClientToHost(Message);
    }

    // Small private class to wrap
    // the User and the callback together.
    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.

C#
[Serializable()]
public class CommsInfo
{
    private string _Message = "";

    public CommsInfo(string Message)
    {
        _Message = Message;
    }

    public string Message
    {
        get { return _Message; }
        set { _Message = value; }
    }
}

/// <SUMMARY>
/// This CallbackSink object will be 'anchored'
/// on the client and is used as the target for a callback
/// given to the server.
/// </SUMMARY>
public class CallbackSink : MarshalByRefObject
{
    public event delCommsInfo OnHostToClient;

    public CallbackSink()
    { }

    [OneWay]
    public void HandleToClient(CommsInfo info)
    {
        if (OnHostToClient != null)
            OnHostToClient(info);
    }
}

The server code

C#
public fServer()
{
    InitializeComponent();

     // Register a server channel on the Server where we 
    // will listen for clients who wish to communicate
    RegisterChannel();

    // Register callbacks to the static
    // properties on the ServerTalk object
    ServerTalk.NewUser = new delUserInfo(NewUser);
    ServerTalk.ClientToHost = new delCommsInfo(ClientToHost);
}

// The method that will be called when a new User registers.
private void NewUser(string UserID)
{
    // since it originated from a different thread
    // we need to marshal this back to the current UI thread.
    if (this.cboUsers.InvokeRequired)
        this.cboUsers.Invoke(new delUserInfo(NewUser), 
                             new object[] { UserID });
    else
    {
        this.cboUsers.Items.Add(UserID);
        // select the last added user
        this.cboUsers.Text = UserID;
    }
}

// A helper method that will marshal a CommsInfo from the client to 
// our UI thread.
private void ClientToHost(CommsInfo Info)
{
    // since it originated from a different thread
    // we need to marshal this back to the current UI thread.
    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()
{
    // Set the TypeFilterLevel to Full since
    // callbacks require additional security 
    // requirements
    SoapServerFormatterSinkProvider serverFormatter = 
               new SoapServerFormatterSinkProvider();
    serverFormatter.TypeFilterLevel = 
      System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

    // we have to change the name since we can't
    // have two channels with the same name.
    Hashtable ht = new Hashtable();
    ht["name"] = "ServerChannel";
    ht["port"] = 9000;

    // now create and register our custom HttpChannel 
    HttpChannel channel = new HttpChannel(ht, null, serverFormatter);
    ChannelServices.RegisterChannel(channel, false);

    // register a WKO type in Singleton mode
    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.

C#
// this object lives on the server
private ServerTalk _ServerTalk = null;
// this object lives here on the client
private CallbackSink _CallbackSink = null;

private void btnRegister_Click(object sender, EventArgs e)
{
    // creates a client object that 'lives' here on the client.
    _CallbackSink = new CallbackSink();

    // hook into the event exposed on the Sink
    // object so we can transfer a server 
    // message through to this class.
    _CallbackSink.OnHostToClient += 
      new delCommsInfo(_EventSink_OnHostToClient);

    // Register a client channel so the server
    // can communicate back - it needs a channel
    // opened for the callback to the CallbackSink
    // object that is anchored on the client!
    HttpChannel channel = new HttpChannel(9001);
    ChannelServices.RegisterChannel(channel, false);

    // now create a transparent proxy to the server component
    object obj = Activator.GetObject(typeof(ServerTalk), 
                 "http://marcel:9000/TalkIsGood");

    // cast returned object
    _ServerTalk = (ServerTalk)obj;

    // Register ourselves to the server
    // with a callback to the client sink.
    _ServerTalk.RegisterHostToClient(this.txtUserID.Text, 
          new delCommsInfo(CallbackSink.HandleToClient));

    // make sure we can't register again!
    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).

C#
public class ServerTalk : MarshalByRefObject
{
// existing code.....


    // Static constructor that ensures
    // a workerthread is started to monitor 
    // the _ClientToServer queue
    static ServerTalk()
    {
        Thread t = new Thread(new ThreadStart(CheckClientToServerQueue));

    t.IsBackground = true;
    // Thanks to Colin Myles!! If we don't set
    // this statement the thread will
    // prevent the app from shutting down!

        t.Start();
    }

    // a thread-safe queue that will contain
    // any message objects that should
    // be send to the server
    private static Queue _ClientToServer = 
          Queue.Synchronized(new Queue());

    // this instance method allows
    // a client to send a message to the server
    public void SendMessageToServer(CommsInfo Message)
    {
        _ClientToServer.Enqueue(Message);
    }

    // an endless loop invoked by a worker-thread
    // which will monitor the tread-safe ClientToServer queue 
    // and passes on any CommsInfo objects that are placed here.
    private static void CheckClientToServerQueue()
    {
        while (true)
        {
            // allow rest of the system
            // to continue whilst waiting...
            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:
    C#
    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.
    C#
    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.

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
Ever since my dad bought me a Commodore 64 (some years back) I have been hooked to programming. For most of my working career I have worked intensely with C# (WinForms and WPF) but a few years back I started to investigate browser technologies and frameworks which are much more powerful than I thought at first!

I studied International Marketing but found the IT sector much more challenging. So straight from uni I took on some IT contract work in London in a buoyant market and never looked back.

If you wish to contact me then please do so on heeremans.marcel@gmail.com

Comments and Discussions

 
GeneralRe: Client hang Pin
Vertyg07-Feb-07 6:53
Vertyg07-Feb-07 6:53 
Generalvery Elegant method Pin
f24-Aug-06 7:42
f24-Aug-06 7:42 
AnswerRe: very Elegant method Pin
marcel heeremans9-Aug-06 3:05
marcel heeremans9-Aug-06 3:05 
GeneralGreat example Pin
carl engerer20-Jul-06 11:32
carl engerer20-Jul-06 11:32 
GeneralFirewall issue Pin
DYY20-Jun-06 2:07
DYY20-Jun-06 2:07 
AnswerRe: Firewall issue Pin
marcel heeremans20-Jun-06 22:22
marcel heeremans20-Jun-06 22:22 
GeneralRe: Firewall issue Pin
JasonSmith12-Aug-06 23:44
JasonSmith12-Aug-06 23:44 
QuestionCan not send messages from another PC Pin
vherz7-Jun-06 1:02
vherz7-Jun-06 1:02 
AnswerRe: Can not send messages from another PC [modified] Pin
marcel heeremans8-Jun-06 21:07
marcel heeremans8-Jun-06 21:07 
GeneralRe: Can not send messages from another PC Pin
vherz12-Jun-06 16:54
vherz12-Jun-06 16:54 
AnswerRe: Can not send messages from another PC Pin
marcel heeremans12-Jun-06 21:44
marcel heeremans12-Jun-06 21:44 
GeneralRe: Can not send messages from another PC Pin
displaced8014-Nov-06 3:06
displaced8014-Nov-06 3:06 
GeneralRe: Can not send messages from another PC Pin
Benny S. Tordrup4-Apr-07 1:31
Benny S. Tordrup4-Apr-07 1:31 
GeneralInteresting but already exist in Delphi Pin
YogiYang30-May-06 18:52
YogiYang30-May-06 18:52 
JokeRe: Interesting but already exist in Delphi Pin
hack.root4-Jun-06 17:53
hack.root4-Jun-06 17:53 
Generalserver cannot send to client after idle time Pin
fred pittroff11-May-06 6:51
fred pittroff11-May-06 6:51 
AnswerRe: server cannot send to client after idle time Pin
marcel heeremans12-May-06 0:23
marcel heeremans12-May-06 0:23 
GeneralRe: server cannot send to client after idle time Pin
fred pittroff15-May-06 6:16
fred pittroff15-May-06 6:16 
GeneralWonderful! Pin
Josh Smith6-May-06 7:48
Josh Smith6-May-06 7:48 
GeneralGood stuff Pin
feisal4-May-06 22:28
feisal4-May-06 22:28 
GeneralRe: Version 1.1 supporting? Pin
marcel heeremans23-Apr-06 21:19
marcel heeremans23-Apr-06 21:19 
GeneralRe: Version 1.1 supporting? Pin
pkwan723-Apr-06 21:40
pkwan723-Apr-06 21:40 
GeneralRe: Version 1.1 supporting? Pin
Stuart Kinnear26-Apr-06 0:22
Stuart Kinnear26-Apr-06 0:22 
GeneralRe: Version 1.1 supporting? Pin
madval31-May-06 9:33
madval31-May-06 9:33 
GeneralRe: Version 1.1 supporting? Pin
jr2b3-Aug-06 4:54
jr2b3-Aug-06 4:54 

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.