Friday, June 20, 2008

.NET Remoting Events (with C# Code Sample).

  • This post contains generic code that's ready for use.
  • Full solution is available at the bottom of the post.

This post will walk you through the implementation of a simple client-server application that establishes remoting communication in two directions while using infrastructure components that extract the common remoting related tasks from the application.

image

Introduction

Establishing inter-process communication via remoting is fairly simple for applications that require one way communication. In those kind of applications, a client requires to initiate method execution on a server object, while the server object is stateless and has no way to initiate communication with the client.

Unfortunately, things get pretty messy when clients also need to listen to server events. This requirement turns the table because now the server object needs to be able to initiate method execution on the clients objects. Consequently, the server object cannot be stateless because it must hold references to the registered clients, each client object that register to server event should be MarshalByRefObject and its assembly should be referenced by the server.

This post demonstrates how server objects can be made state-full and how clients can register to server events without having to be MarshalByRefObject nor referenced by the server.

Drawbacks

Before going any further, it's important to understand that the 'remote events' feature has its drawbacks.

If the client and the server reside in different machines - the guidance is to not use remote events in cases where there are many clients. The main problem is that the server default behavior is to dispatch events synchronously, as a result the request time is increases by a magnitude and exceptions are hard to handle. Though events can be raised asynchronously, it is not recommended for reasons such as ThreadPool starvation and possible event lost. Applications that are being designed for scalability should probably use some other mechanism (such as sockets) to get notifications from the server.

What happened if client crashes without un-registering from the server event?? without special handling the server will throw exception every time it will attempt to raise the event - since the event invocation list will still contain the callback of the disconnected client (the solution will be discussed shortly).

Can clients be notified about server disconnection? Unfortunately, remoting doesn’t expose some kind of ‘server disconnection event’ which will enable the clients to be notified when server disconnect. The only way for the clients to spot server disconnection is to invoke the remote object periodically within a try-catch block, and handle the socket exception that will be thrown in case the server is disconnected.

What's left? Use remote events if the clients and the server reside on the same machine or if there are restricted amount of reliable clients. I use it in middle size intranet style applications were there are no more than 3 well known reliable clients.

Alternatives

You can read about two way communication using .NET sockets in the post - “.NET Sockets - Multiple Clients - Two Way”.

The following video will guide you through the entire process of establishing two way communication using WCF.

____________________________________________________________________

Two Way Client Server Application

Deployment View – The Applications

In the demonstrated project, a client application is deployed on multiple machines which share network with a single server machine on which a server application is deployed. The client applications initiate remote calls on the server application and resister to its events, while the sever application is unaware of the connected clients, it raises events whenever appropriate without realizing who is listening in the other side. 

image 

You can read more about deployment diagrams in the post – “UML Deployment Diagrams – Modeling the System Physical Architecture

Components View – The Assemblies

The ‘RemotingTwoWay.App.Server’ component constitutes the sever application, it includes the remotable object that is being published to the clients. The ‘RemotingTwoWay.App.Client’ component constitutes the client application. Both of them use the ‘RemotingTwoWay.Infra.RemotingTools’ component which encapsulates remoting related tasks such as publishing remote objects and retrieving proxies for remote objects.

The ‘RemotingTwoWay.App.Global’ exposes the IRemotingDistributableObject interface which the server side remoteable object and the client side proxy (generated by remoting)  implement. The client application uses the proxy in order to invoke the remoteable object from remote.

image 

You can read more about component diagrams in the post – “UML 2.0 Component Diagrams – Modeling the System Logical Architecture

Implementation

Server-Side
The Remote Object (Part of the ‘RemotingTwoWay.App.Server’ component)

RemotingDistributableObject is the server object that is being published via remoting. Once published, multiple clients can retrieve its proxy and invoke it from remote through its IRemotingDistributableObject interface. Clients can also register to its MessageRecived event and get notified whenever the server receives new message.

In case one of the clients that registered to the remote event disconnect without unregistering from the event (such as incase of a crash) - a socket exception will accrue every time the server will attempt to raise the event.  To avoid this, in case one of the callbacks in the event invocation list throws exception – we assume that the callback source is disconnected and we simply remove the “dirty” callback from the invocation list.

It might seems awkward to remove the dirty callback from the event while iterating through the invocation list but it’s safe since ‘GetInvocationList’ returns copy of the event invocation list. This scheme also works fine in case multiple clients call the ‘SendMessage’ concurrently while the event invocation list consist of “dirty” callback, in such case the dirty callback might be called more than once (causing an exception each time) and be removed from the event more than once – but this causes no real harm and obliviously better than locking the entire block.

public class RemotingDistributableObject : MarshalByRefObject, IRemotingDistributableObject
 {
     private readonly ServerImp m_imp;

     public RemotingDistributableObject(ServerImp p_imp)
     {
         this.m_imp = p_imp;
     }
     
     public void TestConnection()
     {
         
     }

     public void SendMessage(string p_mes)
     {
         m_imp.RespondToMessage(p_mes);

         Delegate[] invocationList = MessageRecived.GetInvocationList();

         foreach (MessageRecivedDel d in invocationList)
         {
             try
             {
                 d(p_mes);
             }
             catch (Exception)
             {
                 MessageRecived -= d;
                 Debug.WriteLine("Unable to raise event on Client machine " + 
                     "(client seems to be disconnected), Removing registration");
             }
         }
     }

     public event MessageRecivedDel MessageRecived;
 }
Server Terminal (Part of the ‘RemotingTwoWay.Infra.RemotingTools’  component)

The TwoWaysServerTerminal is used to publish the server object.

public class TwoWaysServerTerminal 
   {
       public static void StartListening(int port, string tcpChannelName, 
           MarshalByRefObject remoteObject, string remoteObjectUri)
       {
           TcpChannel channel = CreateTcpChannel(port, tcpChannelName);

           ChannelServices.RegisterChannel(channel, false);

           RemotingServices.Marshal(remoteObject, remoteObjectUri);
       }

       private static TcpChannel CreateTcpChannel(int port, string tcpChannelName)
       {
           BinaryServerFormatterSinkProvider serverFormatter = 
               new BinaryServerFormatterSinkProvider();
           
           serverFormatter.TypeFilterLevel = TypeFilterLevel.Full;

           BinaryClientFormatterSinkProvider clientProv = 
               new BinaryClientFormatterSinkProvider();

           Hashtable props = new Hashtable();
           props["port"] = port;
           props["name"] = tcpChannelName;

           return new TcpChannel(
               props, clientProv, serverFormatter);
       }
   }
Server Host (Part of the ‘RemotingTwoWay.App.Server’ component)

The server host creates the remote object and calls StartListening through the TwoWaysServerTerminal. As a result, the remote object is being published and from that moment on - clients can go ahead and retrieve its proxy.

int port = 1234;

RemotingDistributableObject1 remoteObject = 
    new RemotingDistributableObject1(serverImp);

// Constants.RemoteableObjectUri = "RemotingDistributableObject1"
// Constants.RemotingChannelName = "remoting-tcp"

TwoWaysServerTerminal.StartListening(
    port, Constants.RemotingChannelName, 
    remoteObject, Constants.RemoteableObjectUri);
 Both-Sides
Remotable object interface (Part of the ‘RemotingTwoWay.App.Globals’ component)

IRemotingDistributableObject is the interface which is being implemented by both server side remotable object and client side proxies, by that allowing the clients to make remote calls on the  server object in a type safe fashion.

[Serializable]
public delegate void MessageRecivedDel(string message);

public interface IRemotingDistributableObject
{
    void TestConnection();

    void SendMessage(string p_mes);

    event MessageRecivedDel MessageRecived;
}
ClientEventsWrapper (Part of the ‘RemotingTwoWay.App.Globals’ component)

As mentioned above - "each client that register to server event should be MarshalByRefObject and its assembly should be referenced by the server". To avoid this, client instantiate the ClientEventWrapper class (which reside in a separate assembly that both client and server reference) and instead of registering its own methods to the server events - it register the ClientEventWrapper object methods, than the client register to matching events on the ClientEventWrapper object and handle the server events indirectly. As a result -the server object loads the ClientEventWrapper (which is MarshalByRefObject) object instead of the client object (that is not MarshalByRefObject and doesn’t have to be referenced by the server).

public class ClientEventsWrapper : MarshalByRefObject
{
    public event MessageRecivedDel MessageRecived;

    public void MessageRecivedHandler(string message)
    {
        if (MessageRecived != null)
        {
            MessageRecived(message);
        }
    }
}
Client-Side
Client Terminal (Part of the ‘RemotingTwoWay.Infra.RemotingTools’  component)

TwoWaysClientTerminal is used to retrieve proxy for the server object.

public class TwoWaysClientTerminal
   {
       private TcpChannel m_Channel;

       public T Connect<T>(string serverName, int port, 
           string distributedObjectName, string tcpChannelName)
       {
           m_Channel = CreateTcpChannel(tcpChannelName);

           ChannelServices.RegisterChannel(m_Channel, false);

           string fullServerAddress = string.Format(
               "tcp://{0}:{1}/{2}", serverName, port, distributedObjectName);

           // Create a proxy from remote object.
           T res = (T)Activator.GetObject(typeof(T),fullServerAddress);

           return res;
       }

       private static TcpChannel CreateTcpChannel(string tcpChannelName)
       {
           BinaryServerFormatterSinkProvider serverFormatter = 
               new BinaryServerFormatterSinkProvider();
           
           serverFormatter.TypeFilterLevel = TypeFilterLevel.Full;

           BinaryClientFormatterSinkProvider clientProv = 
               new BinaryClientFormatterSinkProvider();

           Hashtable props = new Hashtable();
           props["name"] = tcpChannelName;
           props["port"] = 0;

           return new TcpChannel(props, clientProv, serverFormatter);
       }

       public void Disconnect()
       {
           if (m_Channel != null)
           {
               ChannelServices.UnregisterChannel(m_Channel);
           }
       }
   }
Client Host (Part of the ‘RemotingTwoWay.App.Client’ component)

Client host creates instance of TwoWaysClientTerminal, connects to the server and retrieve proxy for the server object.  It creates ClientEventWrapper object and register its methods to handle server proxy events,  it then register its own callbacks (that do the actual work)  to the ClientEventWrapper object events.

So when the server raises event - ClientEventWrapper callback is called, which in turn calls the client callback that handle the event as appropriate.

m_Terminal = new TwoWaysClientTerminal();
int port = 1234;

// Constants.RemoteableObjectUri = "RemotingDistributableObject1"
// Constants.RemotingChannelName = "remoting-tcp"

m_RemoteProxy = m_Terminal.Connect<IRemotingDistributableObject>(
    "localhost", port, Constants.RemoteableObjectUri, 
    Constants.RemotingChannelName);

// Register wrapper to remoting proxy event
ClientEventsWrapper clientEventsWrapper = new ClientEventsWrapper();
m_RemoteProxy.MessageRecived += clientEventsWrapper.MessageRecivedHandler;

// Register client handler to wrapper event
clientEventsWrapper.MessageRecived += MessageRecivedHandler;

m_RemoteProxy.TestConnection();

Downloads

Source Code 2005

Source Code 2008

Interesting Links

MSDN - .NET Remoting Architecture

Alex Arlievsky - Persistent .NET Events in Stateless Remoting Server

http://www.thinktecture.com/resourcearchive/net-remoting-faq/remotingusecases

http://www.informit.com/articles/article.aspx?p=102172&seqNum=6

Performance Comparison: .NET Remoting vs. ASP.NET Web Services

27 comments:

  1. Downloadlink is not working.

    ReplyDelete
  2. I've checked it and it works, maybe it was a problenm with 'hotlinkfiles' server,

    ReplyDelete
  3. Works fine until the client app is closed and reopened. When you click the send button on the client app the server crashes with the following error:

    System.Net.Sockets.SocketException was unhandled by user code
    ErrorCode=10061
    Message="No connection could be made because the target machine actively refused it 192.168.1.102:3789"
    NativeErrorCode=10061
    Source="mscorlib"
    StackTrace:
    Server stack trace: at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress) at System.Net.Sockets.Socket.Connect(EndPoint remoteEP) at System.Runtime.Remoting.Channels.RemoteConnection.CreateNewSocket(EndPoint ipEndPoint) at System.Runtime.Remoting.Channels.RemoteConnection.CreateNewSocket() at System.Runtime.Remoting.Channels.RemoteConnection.GetSocket() at System.Runtime.Remoting.Channels.SocketCache.GetSocket(String machinePortAndSid, Boolean openNew) at System.Runtime.Remoting.Channels.Tcp.TcpClientTransportSink.SendRequestWithRetry(IMessage msg, ITransportHeaders requestHeaders, Stream requestStream) at System.Runtime.Remoting.Channels.Tcp.TcpClientTransportSink.ProcessMessage(IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, ITransportHeaders& responseHeaders, Stream& responseStream) at System.Runtime.Remoting.Channels.BinaryClientFormatterSink.SyncProcessMessage(IMessage msg) Exception rethrown at [0]: at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg) at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type) at RemotingTemplate.Globals.ClientEventsWrapper.MessageRecivedHandler(String message) at RemotingTemplate.Globals.MessageRecivedDel.Invoke(String message) at RemotingTemplate.Server.RemotingDistributableObject.SendMessage(String p_mes) in C:\Documents and Settings\david\Desktop\RemotingTwoWays\TwoWays\RemotingTemplate.Server\RemotingDistrebutableObject.vb:line 16 at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs) at System.Runtime.Remoting.Messaging.StackBuilderSink.PrivateProcessMessage(RuntimeMethodHandle md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs) at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMessage msg, Int32 methodPtr, Boolean fExecuteInContext)
    InnerException:

    ReplyDelete
  4. The code works fo me too but I did not understand how. How is it possible to fire the the event manually in the server application (witout getting a message from the client)?

    ReplyDelete
  5. When the server is stateless it only responds to commands/invocations coming from its client, however, in case the server is not state less it may have active objects that contains business logic of there on.

    Think about a server that listen to tcp transports coming from some device, it analyses the transport and when it finds interesting data it requires to updates its clients through remoting. In such case the sever raises an event which is handled by its remote clients

    Hope that helps,

    ReplyDelete
  6. is there any problem solution for the Drowbacks of Remoting events, that they can not be deliverd by the Server because of the Router, which causes that the server can't find the IP-adresse of the Client. i want to ask the origin-Poster of the Programmblog, can you please show the example how can i solve the problem above? even if config files need to be changed the IP Adresse of the Client every time new. Thank you for Patient and your time please.

    Thank you very much

    mark

    ReplyDelete
  7. Hi Aviad,
    I tried to working on your project and everything is ok. But I don't know why the first message in client I send to, it's really slow, I have to wait for 5-10 seconds to complete, and the after times is normal.

    I am using VS2008, .Net Framework 3.5

    Could you explain to me?

    Thanks

    Khoa Diep

    ReplyDelete
  8. The difference between the first message and the messages that follow is that the first message triggers the server and the client to open a socket and establish communication. Remoting will close those sockets in case the two ends don't exchange messages for some 10 seconds.

    However, establishing communication between the sockets shouldn't take 10 seconds unless your network is really really slow.

    I've compiled the project using VS2008 .NET Framework 3.5 and it works without any delay.

    Hope that helps, Regards

    ReplyDelete
  9. Regarding to the comment about the exception "No connection could be made because the target machine actively refused it 192.168.1.102:3789" that was e thrown after client disconnect and reconnect.

    I fixed it and uploaded the revised code.

    Thanks!

    ReplyDelete
  10. Thanks Aviad for a great post!!!

    I was searching the web for remoting related articles and didn't find another article that explains the topic as well as you do.

    Along the way I also learned how to describe architectures using UML.

    Keep it up!!

    ReplyDelete
  11. How does a client unregister a ClientEventWrapper from the server? For example, if the client was terminating and wanted to cleanly unregister, rather than rely on the server cleaning up after an event exception.

    ReplyDelete
  12. In the attached project you can find an answer.

    The client need to simply remove the ClientEventWrapper callback from the proxy.

    Regards, Aviad

    ReplyDelete
  13. Hi

    Thanks for this, it works like a charm.
    I'm kinda new to c# and I was wondering what you would recomment regarding concurrent calls to shared ressource on the server (Mutex, Monitor, lock,AutoResetEvent, etc...) and maybe give a little example if you have the time...

    Thank you!

    M.

    ReplyDelete
  14. You should choose the synchronization primitive and the locking protocol according to the way in which the shared resource is accessed.

    You can find in detail review in the post: 'Thread Synchronization in .NET (Monitor, ReaderWriterLock, ContextBoundObject, and Immutable Objects)'
    Please refer to the following link:
    http://aviadezra.blogspot.com/2009/03/thread-safety-using-monitor.html

    ReplyDelete
  15. Great treatment of .NET remoting. Download is not working, however. :-(

    ReplyDelete
  16. adrive.com sever seems to be down at the moment, I will follow up to see it that get fixed in the next couple of days

    In the mean while try this:
    http://docs.google.com/uc?id=0B_84xXeoy0RlZmRlMGMxM2MtYWQyZS00NzBhLTllODctY2JlZjE4NmEwNzQ2&export=download&hl=en

    ReplyDelete
  17. the file u look for is not existing it says...when try to download the source code

    ReplyDelete
  18. Thanks for letting me know, I changed the source provider to Google docs, you should be able to download the source code now.

    Aviad

    ReplyDelete
  19. Really a great article.

    I am new to remoting and have some question hope you can advice me:-

    1. Using your templete, the client will not able to reconnect to server when it disconnected for some period of time.
    2. Same LAN your app able to work nicely. but my cant. only can work within same PC.
    3. How to make remoting work over the Internet. It seem cant work.

    ReplyDelete
  20. Using your templete, the client will not able to reconnect to server when it disconnected for some period of time.
    (aviade) You need to add spacial support for disconnection dicovery to enable re-connection.

    2. Same LAN your app able to work nicely. but my cant. only can work within same PC.
    (aviade) In the ClientHost, you need the change 'localhost' to the same of the server machine.

    3. How to make remoting work over the Internet. It seem cant work.
    (aviade) Remoting was not meant be used over WAN.

    ReplyDelete
  21. Hi,

    This is a very nice and useful article for a newbie like me.
    But I still don't know how to call from server to client.
    In your server app (in sample code) I add one button to send message to client.
    Then do I need to create a TwoWaysClientTerminal in Server side using a different port?

    Thanks and regards,
    Zaw Min Tun

    ReplyDelete
  22. With this model server side can only register to events, that can be sent from multiple clients.

    ReplyDelete
  23. If the client is idle too long (say 10 min), it crashes when sending a message.

    ReplyDelete
  24. "You need to add spacial support for disconnection dicovery to enable re-connection."
    Can you explain this? Thank you.

    ReplyDelete
  25. Excellent article! There are many online tutorials that attempt to explain how remoting works. However, this is the first and probably best tutorial for distributed remoting.

    ReplyDelete