Click here to Skip to main content
13,736,223 members
Click here to Skip to main content
Add your own
alternative version

Stats

73.7K views
6.5K downloads
121 bookmarked
Posted 26 Apr 2015
Licenced CPOL

Advanced TCP Socket Programming with .NET

, 26 Apr 2015
Rate this:
Please Sign up or sign in to vote.
Developing robust client-server applications with the .NET framework and C#

Introduction

In this article, I will try to demonstrate some of the techniques I have used over the years for developing client-server applications.

Background

The reason I am writing this article is the numerous times I encountered very poor examples and tutorials online, demonstrating only the very basics of socket programming, without diving into the concepts like object oriented and type safe communication.

Consider the below code sample:

    private void StartServer()
    {
        TcpListener server = new TcpListener(System.Net.IPAddress.Any, 8888);
        //Start the server
        server.Start();

        Console.WriteLine("Server started. Waiting for connection...");
        //Block execution until a new client is connected.
        TcpClient newClient = server.AcceptTcpClient();

        Console.WriteLine("New client connected!");

        //Checking if new data is available to be read on the network stream
        if (newClient.Available > 0)
        {
            //Initializing a new byte array the size of the available bytes on the network stream
            byte[] readBytes = new byte[newClient.Available];
            //Reading data from the stream
            newClient.GetStream().Read(readBytes, 0, newClient.Available);
            //Converting the byte array to string
            String str = System.Text.Encoding.ASCII.GetString(readBytes);
            //This should output "Hello world" to the console window
            Console.WriteLine(str);
        }
    }

    private void StartClient()
    {
        TcpClient client = new TcpClient();
        //Connect to the server
        client.Connect("localhost", 8888);

        String str = "Hello world";

        //Get the network stream
        NetworkStream stream = client.GetStream();
        //Converting string to byte array
        byte[] bytesToSend = System.Text.Encoding.ASCII.GetBytes(str);
        //Sending the byte array to the server
        client.Client.Send(bytesToSend);
    }
}

That's about all you can get from looking for a code sample online. There is nothing wrong with that sample, but in the real world, I would like my application to have more advanced protocol then passing around some strings between the client and server. Things such as:

  • Creating sessions between clients
  • Passing complex objects
  • Authentication and authorization of clients
  • Transferring large files
  • Real time client connectivity notifications
  • Using callbacks to receive information about the progress of some procedure on the remote client, or to return values

Solution Structure

  1. Sirilix.AdvancedTCP.Server (contains server related classes)
  2. Sirilix.AdvancedTCP.Client (Contains client related classes)
  3. Sirilix.AdvancedTCP.Shared (Contains shared classes between assemblies)
  4. Sirilix.AdvancedTCP.Server.UI (A WinForms application demonstrating how to make use of the Server project)
  5. Sirilix.AdvancedTCP.Client.UI (A WinForms application demonstrating how to make use of the Client project)

Client-Server Model

In order to create a connection between machines, one of them has to listen for incoming connections on a specific port number, this listening routine is done with the TcpListener object.

The TcpListener can accept the connection, and provide a TcpClient socket as a result.

TcpClient newClient = listener.AcceptTcpClient();

As soon as I obtained the new TcpClient socket, I can start sending and receiving data between the two clients.

This means that if I want to communicate between two applications, all I need is that one of them will listen for incoming connection, and the other will initiate the connection. This is true, but in order to listen for incoming connections, my application will require port forwarding and firewall exceptions, which are over complicated requirements from a standard user. Not only that, but the initiating application will need to know the remote machine's IP address, or DNS name.

This approach might be useful in some cases, but for most scenarios, I want my clients to be able to connect to each other by referring to their user name, or email address, just like Skype or Team Viewer. This is why I need to have some central unit to act as a bridge between clients.

Client-Server-Client Model

This approach requires the server to hold each new client in a list, also, the server must be aware of the source, and destination of each message passing through, so he can deliver the message to the right client. To achieve this, we need to encapsulate each client inside a handler which I like to call a Receiver.

  • The Receiver should handle all incoming and outgoing messages of its encapsulated client.
  • The Receiver should also be able to transfer an incoming message to any other receiver in the list and direct it to send this message to its encapsulated client.
  • The Receiver should hold a unique ID such as email address or user name associated with its client, so other receivers can address their messages to the right receiver (client).

This gives us the opportunity, not only to authenticate each client, but to process all the data that is coming and going between clients. For example, we can measure the total bytes usage of each client, or block a specific message based on its content.

Let's look at some of the important parts of the Receiver class.

The Receiver

public Receiver(TcpClient client, Server server)
            : this()
        {
            Server = server;
            Client = client;
            Client.ReceiveBufferSize = 1024;
            Client.SendBufferSize = 1024;
        }
        
        public void Start()
        {
            receivingThread = new Thread(ReceivingMethod);
            receivingThread.IsBackground = true;
            receivingThread.Start();

            sendingThread = new Thread(SendingMethod);
            sendingThread.IsBackground = true;
            sendingThread.Start();
        }

        public void SendMessage(MessageBase message)
        {
            MessageQueue.Add(message);
        }

        private void SendingMethod()
        {
            while (Status != StatusEnum.Disconnected)
            {
                if (MessageQueue.Count > 0)
                {
                    var message = MessageQueue[0];

                    try
                    {
                        BinaryFormatter f = new BinaryFormatter();
                        f.Serialize(Client.GetStream(), message);
                    }
                    catch
                    {
                        Disconnect();
                    }
                    finally
                    {
                        MessageQueue.Remove(message);
                    }
                }
                Thread.Sleep(30);
            }
        }

        private void ReceivingMethod()
        {
            while (Status != StatusEnum.Disconnected)
            {
                if (Client.Available > 0)
                {
                    TotalBytesUsage += Client.Available;

                    try
                    {
                        BinaryFormatter f = new BinaryFormatter();
                        MessageBase msg = f.Deserialize(Client.GetStream()) as MessageBase;
                        OnMessageReceived(msg);
                    }
                    catch (Exception e)
                    {
                        Exception ex = new Exception("Unknown message received. Could not deserialize 
                        the stream.", e);
                        Debug.WriteLine(ex.Message);
                    }
                }

                Thread.Sleep(30);
            }
        }

First of all, you can see that I am passing a TcpClient in the constructor, This TcpClient is the one that has been accepted by the TcpListener. I am also passing the Server instance which holds the list of all other receivers, so this receiver could be aware of its siblings, and communicate with them.

The Start method will initiate two threads, one for sending data, and the other for receiving. Those threads will run in a loop while the receiver Status remains in a connected state.

Receiving Thread

Again, this thread will remain active while the receiver is connected, checking if some data is available on the TCP Client NetworkStream. Then, it will try to deserialize the data into an object of type MessageBase which is the base class of all request and response messages. We will also talk about those messages later on. If deserialization was successful, it will pass the message to the OnMessageReceived method. This method will handles messages that are relevant to the receiver. Basically, the receiver cares only for messages that are related to negotiation procedures like authenticating the client and creating sessions between clients. Other messages will just be bypassed and transferred to the destination receiver directly .

* You can notice, that in this case, I am using the BinaryFormatter to serialize and deserialize all the messages, but you can also use other protocols like SoapFormatter or Protobuf if you need to develop some cross platform solution.

Sending Thread

This thread will be responsible for sending messages of type MessageBase that are waiting inside the MessageQueue. The reason to use a queue is to ensure messages won't get mixed up, and will be delivered one at a time.

That being said, all that is left to do, is use the SendMessage method. This method will only add messages to the queue and leave it for the Sending Thread to actually serialize and send the message.

Now that we understand some of the basics of the receiver, let's take a look at the Server class:

The Server

public Server(int port)
      {
          Receivers = new List<Receiver>();
          Port = port;
      }

      public void Start()
      {
          if (!IsStarted)
          {
              Listener = new TcpListener(System.Net.IPAddress.Any, Port);
              Listener.Start();
              IsStarted = true;

             WaitForConnection();

          }
      }

      public void Stop()
      {
          if (IsStarted)
          {
              Listener.Stop();
              IsStarted = false;

          }
      }

      private void WaitForConnection()

      {
          Listener.BeginAcceptTcpClient(new AsyncCallback(ConnectionHandler), null);
      }

      private void ConnectionHandler(IAsyncResult ar)
      {
          lock (Receivers)
          {
              Receiver newClient = new Receiver(Listener.EndAcceptTcpClient(ar), this);
              newClient.Start();
              Receivers.Add(newClient);
              OnClientConnected(newClient);
          }

          WaitForConnection();
      }

As you can see, the server code is pretty straight forward:

  1. Start the Listener.
  2. Wait for incoming connection.
  3. Accept the connection.
  4. Initialize a new Receiver with the new TcpClient, and add it to the Receivers list.
  5. Start the Receiver.
  6. Repeat stage 2.

Note: I am using the Begin/End Async pattern here because it is considered the best approach to handle incoming connections.

The Client

The Client class is very similar to the Receiver class, that has a sending thread, a receiving thread, and a message queue as well. The only difference is that this client is the one initiating the connection with the listener, handles a lot more messages of type MessageBase and is responsible for exposing the necessary methods to the end developer in case you are developing some TCP library, which is our case in this project.

Let's look at some of the important parts in the Client class:

public Client()
       {
           callBacks = new List<ResponseCallbackObject>();
           MessageQueue = new List<MessageBase>();
           Status = StatusEnum.Disconnected;
       }

       public void Connect(String address, int port)
       {
           Address = address;
           Port = port;
           TcpClient = new TcpClient();
           TcpClient.Connect(Address, Port);
           Status = StatusEnum.Connected;
           TcpClient.ReceiveBufferSize = 1024;
           TcpClient.SendBufferSize = 1024;

           receivingThread = new Thread(ReceivingMethod);
           receivingThread.IsBackground = true;
           receivingThread.Start();

           sendingThread = new Thread(SendingMethod);
           sendingThread.IsBackground = true;
           sendingThread.Start();
       }

       public void SendMessage(MessageBase message)
       {
           MessageQueue.Add(message);
       }

       private void SendingMethod()
       {
           while (Status != StatusEnum.Disconnected)
           {
               if (MessageQueue.Count > 0)
               {
                   MessageBase m = MessageQueue[0];

                   BinaryFormatter f = new BinaryFormatter();
                   try
                   {
                       f.Serialize(TcpClient.GetStream(), m);
                   }
                   catch
                   {
                       Disconnect();
                   }

                   MessageQueue.Remove(m);
               }

               Thread.Sleep(30);
           }
       }

       private void ReceivingMethod()
       {
           while (Status != StatusEnum.Disconnected)
           {
               if (TcpClient.Available > 0)
               {
                   BinaryFormatter f = new BinaryFormatter();
                   MessageBase msg = f.Deserialize(TcpClient.GetStream()) as MessageBase;
                   OnMessageReceived(msg);
               }

               Thread.Sleep(30);
           }
        }

The Connect method will initialize a new TcpClient object, then initiate a connection with the server by the specified IP address, or DNS name, and port number.

* Notice I am assigning a value of 1024 to the ReceiveBufferSize property. This little adjustment will improve our sending and receiving speed greatly.

Messages

What is cool about our project is that every message we send or receive is simply a C# class, so we don't need to parse any complicated protocols. The BinaryFormatter will take all the encoded data and compose our messages back to the same C# object.

Keep in mind that for a message to be deserialized successfully, the message type needs to be located on the same assembly as the origin of the serialized message. Thus, we need to create some class library which will be shared across clients and receivers.

A good idea is to have a base class for all messages.

  1. They will share some properties.
  2. We are using this base class to deserialize the stream, and to detect if the data on the stream is not compliant with this base class or one of its descendants.

Let's start by creating the MessageBase base class:

[Serializable]
public class MessageBase
{
    public bool HasError { get; set; }
    public Exception Exception { get; set; }

    public MessageBase()
    {
        Exception = new Exception();
    }
}

Notice I decorated the class with the [Serializable] attribute, this is necessary if we want the BinarryFormatter to serialize our class, otherwise it will throw an exception.

In the meanwhile, not much is shared between different messages, only that each message can return that something went wrong.

Now let's create a base class for request, and also for response messages:

[Serializable]
public class RequestMessageBase : MessageBase
{

}
[Serializable]
public class ResponseMessageBase : MessageBase
{

}

So we basically created all we need in order to start doing something interesting. Let's start sending messages! The first message will be the ValidationRequest message, this message will be used to authenticate a client with the server.

[Serializable]
public class ValidationRequest : RequestMessageBase
{
    public String Email { get; set; }
}

As you can see, this message is deriving from our request base message and its single property is the email of the user, normally, we would also add a password property.

Now, we need to expose a method that will create a new instance of this message and add it to the message queue. Let's look at the Login method in the Client class.

public void Login(String email)
{
    //Create a new validation request message
    ValidationRequest request = new ValidationRequest();
    request.Email = email;

    //Send the message (Add it to the message queue)
    SendMessage(request);
}

The Login method will add the ValidationRequest message to the message queue, the sending thread will then pick up the message, serialize, and send it over the network. The Receiver will then deserialize the message and pass it over to the ValidationRequestHandler method.

The ValidationRequestHandler method will raise an event from the server class called OnClientValidating, this will enforce the front developer to invoke one of the methods inside the ClientValidatingEventArgs.

Confirm will send a ValidationResponse message with the IsValid property set to true. Refuse will send a ValidationResponse message with an authentication exception.

ValidationRequest Message Received at the Receiver

private void ReceivingMethod()
{
    while (Status != StatusEnum.Disconnected)
    {
        if (Client.Available > 0)
        {
            TotalBytesUsage += Client.Available;

            try
            {
                BinaryFormatter f = new BinaryFormatter();
                MessageBase msg = f.Deserialize(Client.GetStream()) as MessageBase;
                OnMessageReceived(msg);
            }
            catch (Exception e)
            {
                Exception ex = new Exception("Unknown message received. Could not deserialize
                the stream.", e);
                Debug.WriteLine(ex.Message);
            }
        }

        Thread.Sleep(30);
    }
}

private void OnMessageReceived(MessageBase msg)
{
    Type type = msg.GetType();

    if (type == typeof(ValidationRequest))
    {
        ValidationRequestHandler(msg as ValidationRequest);
    }
}

private void ValidationRequestHandler(ValidationRequest request)
{
    ValidationResponse response = new ValidationResponse(request);

    EventArguments.ClientValidatingEventArgs args = new EventArguments.ClientValidatingEventArgs            (() =>
    {
        //Confirm Action
        Status = StatusEnum.Validated;
        Email = request.Email;
        response.IsValid = true;
        SendMessage(response);
        Server.OnClientValidated(this);
    },
    () =>
    {
        //Refuse Action
        response.IsValid = false;
        response.HasError = true;
        response.Exception = new AuthenticationException("Login failed for user " + request.Emai      l);
        SendMessage(response);
    });

    args.Receiver = this;
    args.Request = request;

    Server.OnClientValidating(args);
}

HOLD IT!

If the UI developer is using the login method or any other method, how would he get notified when the response for this message was received? The first solution that comes to mind is raising an event from the client class, but this means that we will need to create an event for each message. That also complicates the code from the UI developer perspective. So I came up with a much more elegant solution for handling the response messages in the same context as the request (In-Place).

Callbacks

We all know how callback functions work, but how do we implement this kind of behavior between remote clients?

The answer is pretty simple, we need a way of storing callbacks and invoking them when a response is received, but again, how do we invoke the right callback for a given response? The answer is of course CallbackID!

So I think it is pretty obvious that we need to extend our message structure a little further...

Let's look at the new MessageBase class.

[Serializable]
public class MessageBase
{
    public Guid CallbackID { get; set; }
    public bool HasError { get; set; }
    public Exception Exception { get; set; }

    public MessageBase()
    {
        Exception = new Exception();
    }
}

And the new ResponseMessageBase.

[Serializable]
public class ResponseMessageBase : MessageBase
{
    public bool DeleteCallbackAfterInvoke { get; set; }

    public ResponseMessageBase(RequestMessageBase request)
    {
        DeleteCallbackAfterInvoke = true;
        CallbackID = request.CallbackID;
    }
}

Now, every message has a CallbackID property and every response message has a DeleteCallbackAfterInvoke property.

When DeleteCallbackAfterInvoke is set to false, the callback will not be deleted from the list after it has been invoked, this will be useful if we will want to create multiple responses situation like uploading large files in chunks, or, creating a remote desktop session.

Also, the ResponseMessageBase constructor expects a RequestMessageBase so he can copy the callback ID from the request to the response.

Now that we understand how to implement our callbacks, let's look at how this actually works inside the Client class. For example, the Login method I previously showed will now look like this:

public void Login(String email, Action<Client, ValidationResponse> callback)
{
    //Create a new validation request message
    ValidationRequest request = new ValidationRequest();
    request.Email = email;

    //Add a callback before we send the message
    AddCallback(callback, request);

    //Send the message (Add it to the message queue)
    SendMessage(request);
}

Notice that now, the Login method expects a callback action and calls the AddCallback method for adding the given callback before sending the message. This is the AddCallback method.

private void AddCallback(Delegate callBack, MessageBase msg)
{
    if (callBack != null)
    {
        Guid callbackID = Guid.NewGuid();
        ResponseCallbackObject responseCallback = new ResponseCallbackObject()
        {
            ID = callbackID,
            CallBack = callBack
        };

        msg.CallbackID = callbackID;
        callBacks.Add(responseCallback);
    }
}

The AddCallback method expects a Delegate type and a MessageBase so it can construct a new ResponseCallbackObject and add it to the list of callbacks. It also generates a unique ID for the given callback and attaches this new ID to the message.

Now, when this message is received at the Receiver, we need to check if the requesting client is authorized and return a ValidationResponse containing the same callback ID as the ValidationRequest. This is done in the ValidationRequestHandler method we have seen previously. Let's look at what is happening when the ValidationResponseMessage is received at the Client class.

private void OnMessageReceived(MessageBase msg)
{
    Type type = msg.GetType();

    if (msg is ResponseMessageBase)
    {
        InvokeMessageCallback(msg, (msg as ResponseMessageBase).DeleteCallbackAfterInvoke);

        if (type == typeof(RemoteDesktopResponse))
        {
            RemoteDesktopResponse response = msg as RemoteDesktopResponse;
            if (!response.Cancel)
            {
                RemoteDesktopRequest request = new RemoteDesktopRequest();
                request.CallbackID = response.CallbackID;
                SendMessage(request);
            }
        }
        else if (type == typeof(FileUploadResponse))
        {
            FileUploadResponseHandler(msg as FileUploadResponse);
        }
    }
    else
    {
        if (type == typeof(SessionRequest))
        {
            SessionRequestHandler(msg as SessionRequest);
        }
        else if (type == typeof(RemoteDesktopRequest))
        {
            RemoteDesktopRequestHandler(msg as RemoteDesktopRequest);
        }
        else if (type == typeof(TextMessageRequest))
        {
            TextMessageRequestHandler(msg as TextMessageRequest);
        }
        else if (type == typeof(FileUploadRequest))
        {
            FileUploadRequestHandler(msg as FileUploadRequest);
        }
        else if (type == typeof(DisconnectRequest))
        {
            OnSessionClientDisconnected();
        }
    }
}

We can see that there is no special handling for the ValidationResponse and it just falls out and is handled by the InvokeMessageCallback method.

This is the InvokeMessageCallback method:

private void InvokeMessageCallback(MessageBase msg, bool deleteCallback)
{
    var callBackObject = callBacks.SingleOrDefault(x => x.ID == msg.CallbackID);

    if (callBackObject != null)
    {
        if (deleteCallback)
        {
            callBacks.Remove(callBackObject);
        }
        callBackObject.CallBack.DynamicInvoke(this, msg);
    }
}

This method expects a message and a value determines whether to delete the callback after invocation. It will search for the callback in the callbacks list by the callback ID. If found, it will invoke it using the DynamicInvoke. The DynamicInvoke helps us to invoke the callback with the right response message type using polymorphism.

Now let's see how we are actually using this architecture from the UI project.

client.Login("myEmail", (senderClient, response) =>
{

    if (response.IsValid)
    {
        Status("User Validated!");
        this.InvokeUI(() =>
        {
            btnLogin.Enabled = false;
        });
    }

    if (response.HasError)
    {
        Status(response.Exception.ToString());
    }

});

This is how easy it is to call the login method and get a response in the same context using Lambda Expressions and Anonymous Functions.

In this specific project, I chose to use a one on one session approach. This means that in order for clients to interact with each other, one of them must first send a session request message and the other one must confirm the request. Those two messages are SessionRequest and SessionResponse.

Sessions

The session request is an exceptional request because it requires handling by the receiver and also the client. The receiver first checks if the requested client exists, connected, and is not occupied with another session. Only then, it will redirect the message to the requested client. The client then needs to confirm the message by sending back a positive SessionResponse message. This message will then be redirected by the receiver to the requesting client. This whole process is like a handshake between clients and receivers.

Let's look at the RequestSession method:

public void RequestSession(String email, Action<Client, SessionResponse> callback)
{
    SessionRequest request = new SessionRequest();
    request.Email = email;
    AddCallback(callback, request);
    SendMessage(request);
}

I chose to use the client's email address as the unique identifier on the server, this email is registered when the client is logging into the server, so the RequestSession method expects an email of the requested client.

Now let's call this method from the UI project.

client.RequestSession("client@email.com", (senderClient, args) =>
{

    if (args.IsConfirmed)
    {
        Status("Session started with " + "client@email.com");
    }
    else
    {
        Status(args.Exception.ToString());
    }

});

After this message has arrived at the receiver, we need to check for the availability of the requested client.

private void SessionRequestHandler(SessionRequest request)
{
    foreach (var receiver in Server.Receivers.Where(x => x != this))
    {
        if (receiver.Email == request.Email)
        {
            if (receiver.Status == StatusEnum.Validated)
            {
                request.Email = this.Email;
                receiver.SendMessage(request);
                return;
            }
        }
    }

    SessionResponse response = new SessionResponse(request);
    response.IsConfirmed = false;
    response.HasError = true;
    response.Exception = new Exception(request.Email +
          " does not exists or not logged in or in session with another user.");
    SendMessage(response);
}

Once the receiver encounters another receiver that is associated with the email address, and is considered Validated (logged in and not occupied), it will redirect the message to the requested client. The requested client then needs to notify the UI project about the new session request, and provide the option to confirm or refuse the session.

private void SessionRequestHandler(SessionRequest request)
{
            SessionResponse response = new SessionResponse(request);

            EventArguments.SessionRequestEventArguments args = 
                            new EventArguments.SessionRequestEventArguments(() =>
            {
                //Confirm Session
                response.IsConfirmed = true;
                response.Email = request.Email;
                SendMessage(response);
            },
            () =>
            {
                //Refuse Session
                response.IsConfirmed = false;
                response.Email = request.Email;
                SendMessage(response);
            });

            args.Request = request;
            OnSessionRequest(args);
}

protected virtual void OnSessionRequest(EventArguments.SessionRequestEventArguments args)
{
   if (SessionRequest != null) SessionRequest(this, args);
}

As you can see, we are raising an event with two methods, one for confirming, and the other for refusing the request. Of course, we need to register for this event in the UI project.

client.SessionRequest += client_SessionRequest; //Register for the event.

 private void client_SessionRequest(Client client, EventArguments.SessionRequestEventArguments args)
  {
      this.InvokeUI(() =>
      {
          if (MessageBox.Show(this, "Session request from " +
          args.Request.Email + ". Confirm request?",
          this.Text, MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
          {
              args.Confirm();
              Status("Session started with " + args.Request.Email);
          }
          else
          {
              args.Refuse();
          }

      });
  }


  private void InvokeUI(Action action)
  {
      this.Invoke(action);
  }

Notice, I am invoking the code on the UI thread to avoid cross thread operation between the receiving thread who triggered the event, and the UI thread.

Now let's look at what is happening on the server when a session is confirmed.

private void SessionResponseHandler(SessionResponse response)
{
    foreach (var receiver in Server.Receivers.Where(x => x != this))
    {
        if (receiver.Email == response.Email)
        {
            response.Email = this.Email;

            if (response.IsConfirmed)
            {
                receiver.OtherSideReceiver = this;
                this.OtherSideReceiver = receiver;
                this.Status = StatusEnum.InSession;
                receiver.Status = StatusEnum.InSession;
            }
            else
            {
                response.HasError = true;
                response.Exception = 
                    new Exception("The session request was refused by " + response.Email);
            }

            receiver.SendMessage(response);
            return;
        }
    }
}

Notice, I am assigning the OtherSideReceiver properties of type Receiver for each of the session receivers, and also changing their status to InSession. From this moment on, every message sent by one of those receivers will be routed directly to their OtherSideReceiver.

So, after we understand how to create sessions, send messages and invoke callbacks, I want to demonstrate a multiple callbacks scenario like the one used for the remote desktop viewer feature in the sample solution.

Multiple Callbacks

What I mean by multiple callbacks is simply using the same callback multiple times by setting the DeleteCallbackAfterInvoke property in the ResponseMessageBase to false, and this will direct the client to not delete the callback after invocation, and keep it in the callbacks list for another use.

Let's see how this works with the remote desktop viewer.

Remote Desktop Request

[Serializable]
public class RemoteDesktopRequest : RequestMessageBase
{
    public int Quality { get; set; } //Quality of the captured frame.

    public RemoteDesktopRequest()
    {
        Quality = 50;
    }
}

Remote Desktop Response

[Serializable]
public class RemoteDesktopResponse : ResponseMessageBase
{
    public RemoteDesktopResponse(RequestMessageBase request)
        : base(request)
    {
        DeleteCallbackAfterInvoke = false; //Direct the client to keep the callback.
    }

    public MemoryStream FrameBytes { get; set; } //Current frame byte array.
    public bool Cancel { get; set; } //When set to true will cancel the remote desktop session.
}

RequestDesktop Method on the Client Class

public void RequestDesktop(Action<Client, RemoteDesktopResponse> callback)
{
    RemoteDesktopRequest request = new RemoteDesktopRequest();
    AddCallback(callback, request);
    SendMessage(request);
}

Call the RequestDesktop Method from the UI Project

(The provided callback method should be called every time a new frame is received and update the preview panel.)
client.RequestDesktop((clientSender, response) =>
{
     panelPreview.BackgroundImage = new Bitmap(response.FrameBytes); //Show the received frame.
     response.FrameBytes.Dispose(); //Dispose the memory stream.
});

Now, let's take a close look at what is happening on the receiver side when this request is sent.

Redirect the message to the OtherSide receiver

private void OnMessageReceived(MessageBase msg)
{
    Type type = msg.GetType();

    if (type == typeof(ValidationRequest))
    {
        ValidationRequestHandler(msg as ValidationRequest);
    }
    else if (type == typeof(SessionRequest))
    {
        SessionRequestHandler(msg as SessionRequest);
    }
    else if (type == typeof(SessionResponse))
    {
        SessionResponseHandler(msg as SessionResponse);
    }
    else if (type == typeof(DisconnectRequest))
    {
        DisconnectRequestHandler(msg as DisconnectRequest);
    }
    else if (OtherSideReceiver != null)
    {
        OtherSideReceiver.SendMessage(msg);
    }
}

Notice that our RemoteDesktopRequest message does not fall with any of the receiver message handlers because it does not require any server side processing, and will be redirected to the OtherSideReceiver (the remote client receiver).

Now, after the message was arrived at the remote client, we need to capture the desktop, and send a RemoteDesktopResponse message, containing the new frame.

Capture the desktop and Send the Frames

This helper class will help us capture the desktop one frame at a time, convert and compress the frame to JPEG format by the given quality, and return a MemoryStream containing the byte array of the compressed JPEG.

public class RemoteDesktop
{
    public static MemoryStream CaptureScreenToMemoryStream(int quality)
    {
        Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
        Graphics g = Graphics.FromImage(bmp);
        g.CopyFromScreen(new Point(0, 0), new Point(0, 0), bmp.Size);
        g.Dispose();

        ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
        ImageCodecInfo ici = null;

        foreach (ImageCodecInfo codec in codecs)
        {
            if (codec.MimeType == "image/jpeg")
                ici = codec;
        }

        var ep = new EncoderParameters();
        ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality);

        MemoryStream ms = new MemoryStream();
        bmp.Save(ms, ici, ep);
        ms.Position = 0;
        bmp.Dispose();

        return ms;
    }
}

And this is the RemoteDesktopRequest message handler at the remote client class.

private void RemoteDesktopRequestHandler(RemoteDesktopRequest request)
{
    RemoteDesktopResponse response = new RemoteDesktopResponse(request);
    try
    {
        response.FrameBytes = Helpers.RemoteDesktop.CaptureScreenToMemoryStream(request.Quality);
    }
    catch (Exception e)
    {
        response.HasError = true;
        response.Exception = e;
    }

    SendMessage(response);
}

Now, after the RemoteDesktopResponse message was sent, and was redirected by the receiver, it arrives at the requesting client.

private void RemoteDesktopResponseHandler(RemoteDesktopResponse response)
{
    if (!response.Cancel)
    {
        RemoteDesktopRequest request = new RemoteDesktopRequest();
        request.CallbackID = response.CallbackID;
        SendMessage(request);
    }
    else
    {
        callBacks.RemoveAll(x => x.ID == response.CallbackID);
    }
}

The RemoteDesktopResponse message handler is just copying the callback ID from the response to a new RemoteDesktopRequest message and sending it again to the remote client.

Remember that the RemoteDesktopResponse message DeleteCallbackAfterInvoke property is automatically set to false in the message constructor. The message callback won't be deleted unless the Cancel property of the response message is set to true inside the callback method on the UI project like so:

client.RequestDesktop((clientSender, response) =>
 {
      panelPreview.BackgroundImage = new Bitmap(response.FrameBytes); //Show the received frame.
      response.FrameBytes.Dispose(); //Dispose the memory stream.
      response.Cancel = true; //Cancel the remote desktop session.
 });

So basically, what we have seen is that we can use and invoke the same callback with different response messages of the same type like the RemoteDesktopResponse. Very nice!

The last thing I want to talk about, is extending the client class functionality by providing the ability to create, and send messages that are not part of the Sirilix.AdvancedTCP.Shared project.

Extending the Library with Generic Messages

This last topic was very interesting for me because it presented a few challenges. I wanted to give the UI project the ability to create, and send messages that are not part of the Back-End library. The most challenging part in doing that, is the fact that the Receiver class will not be aware of those new messages, so when he will try to deserialize them, the BinaryFormatter will throw an exception telling that he cannot locate the assembly of which the message type is defined in. The solution to this problem was simply creating the GenericRequest and the GenericResponse messages. Those messages, are part of the library and are used to encapsulate any messages that derive from them.

The GenericRequest Message

[Serializable]
public class GenericRequest : RequestMessageBase
{
    internal MemoryStream InnerMessage { get; set; }

    public GenericRequest()
    {
        InnerMessage = new MemoryStream();
    }

    public GenericRequest(RequestMessageBase request)
        : this()
    {
        BinaryFormatter f = new BinaryFormatter();
        f.Serialize(InnerMessage, request);
        InnerMessage.Position = 0;
    }

    public GenericRequest ExtractInnerMessage()
    {
        BinaryFormatter f = new BinaryFormatter();
        f.Binder = new AllowAllAssemblyVersionsDeserializationBinder();
        return f.Deserialize(InnerMessage) as GenericRequest;
    }
}

The GenericResponse Message

[Serializable]
public class GenericResponse : ResponseMessageBase
{
    internal MemoryStream InnerMessage { get; set; }

    public GenericResponse(GenericRequest request)
        : base(request)
    {
        InnerMessage = new MemoryStream();
    }

    public GenericResponse(GenericResponse response)
        : this(new GenericRequest())
    {
        CallbackID = response.CallbackID;
        BinaryFormatter f = new BinaryFormatter();
        f.Serialize(InnerMessage, response);
        InnerMessage.Position = 0;
    }

    public GenericResponse ExtractInnerMessage()
    {
        BinaryFormatter f = new BinaryFormatter();
        f.Binder = new AllowAllAssemblyVersionsDeserializationBinder();
        return f.Deserialize(InnerMessage) as GenericResponse;
    }
}

Those two messages are just like any other messages except, they can encapsulate any message that derives from them, by serializing the message to the InnerMessage property, of type MemoryStream. They can also extract their inner message, by calling the ExtractInnerMessage method. This method uses the BinaryFormatter to deserialize the inner message, but you can notice that I am setting the Binder property to a new instance of AllowAllAssemblyVersionsDeserializationBinder. The reason for this, is the fact that the generic messages assembly is not aware of the inner message type, and will not be able to deserialize it. So I came up with a way of telling the BinaryFormatter where to search for message types, by replacing the default serialization binder.

Custom Serialization Binder for Locating Types on the Executing Assembly (UI Project)

public sealed class AllowAllAssemblyVersionsDeserializationBinder : 
                              System.Runtime.Serialization.SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        Type typeToDeserialize = null;

        String currentAssembly = Assembly.GetExecutingAssembly().FullName;

        // In this case we are always using the current assembly
        assemblyName = currentAssembly;

        // Get the type using the typeName and assemblyName
        typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
            typeName, assemblyName));

        return typeToDeserialize;
    }
}

In order to use the generic messages, I had to make some adjustments on the Client class.

First, I added a new method called SendGenericRequest.

SendGenericRequest Method

public void SendGenericRequest<T>(GenericRequest request, T callBack)
{
    Guid guid = Guid.NewGuid();
    request.CallbackID = guid;
    GenericRequest genericRequest = new GenericRequest(request);
    genericRequest.CallbackID = guid;
    if (callBack != null) callBacks.Add(new ResponseCallbackObject() 
                          { CallBack = callBack as Delegate, ID = guid });
    SendMessage(genericRequest);
}

This method is intended to send any message derived from the GenericRequestMessage, what it does is simply create a new GenericRequestMessage, and encapsulate the "real" message inside this message by providing the request parameter in the message constructor (See the GenericRequest message structure above). We will see exactly how we can use this message shortly. Next is the SendGenericResponseMessage.

SendGenericResponse Method

public void SendGenericResponse(GenericResponse response)
{
    GenericResponse genericResponse = new GenericResponse(response);
    SendMessage(genericResponse);
}

This method provides about the same functionality, except, it handles generic response message and does not use any callback mechanism.

What is left to do is adjust the Client class to handle those generic messages. The first thing that was needed to be done is handling incoming generic request messages, and that requires a new event.

public event Action<Client, GenericRequest> GenericRequestReceived;

Next, we need to raise this event when a generic message is received.

Raise the GenericRequestReceived Event when a Generic Request is Received

protected virtual void OnMessageReceived(MessageBase msg)
{
    Type type = msg.GetType();

    if (msg is ResponseMessageBase)
    {
        InvokeMessageCallback(msg, (msg as ResponseMessageBase).DeleteCallbackAfterInvoke);

        if (type == typeof(RemoteDesktopResponse))
        {
            RemoteDesktopResponseHandler(msg as RemoteDesktopResponse);
        }
        else if (type == typeof(FileUploadResponse))
        {
            FileUploadResponseHandler(msg as FileUploadResponse);
        }
    }
    else
    {
        if (type == typeof(SessionRequest))
        {
            SessionRequestHandler(msg as SessionRequest);
        }
        else if (type == typeof(RemoteDesktopRequest))
        {
            RemoteDesktopRequestHandler(msg as RemoteDesktopRequest);
        }
        else if (type == typeof(TextMessageRequest))
        {
            TextMessageRequestHandler(msg as TextMessageRequest);
        }
        else if (type == typeof(FileUploadRequest))
        {
            FileUploadRequestHandler(msg as FileUploadRequest);
        }
        else if (type == typeof(DisconnectRequest))
        {
            OnSessionClientDisconnected();
        }
        else if (type == typeof(GenericRequest))
        {
            OnGenericRequestReceived(msg as GenericRequest);
        }
    }
}

protected virtual void OnGenericRequestReceived(GenericRequest request)
{
    if (GenericRequestReceived != null) GenericRequestReceived(this, request.ExtractInnerMessage());
}

Notice I am raising the event with the "real" message as the event parameter by using the ExtractInnerMessage method of the generic request message.

Of course, we need to also handle any generic response message that is received.

Catch the Generic Response Message and Extract the Inner Message before the InvokeMessageCallback Method Call

protected virtual void OnMessageReceived(MessageBase msg)
{
    Type type = msg.GetType();

    if (msg is ResponseMessageBase)
    {
        if (type == typeof(GenericResponse))
        {
            msg = (msg as GenericResponse).ExtractInnerMessage();
        }

        InvokeMessageCallback(msg, (msg as ResponseMessageBase).DeleteCallbackAfterInvoke);

        if (type == typeof(RemoteDesktopResponse))
        {
            RemoteDesktopResponseHandler(msg as RemoteDesktopResponse);
        }
        else if (type == typeof(FileUploadResponse))
        {
            FileUploadResponseHandler(msg as FileUploadResponse);
        }
    }
    else
    {
        if (type == typeof(SessionRequest))
        {
            SessionRequestHandler(msg as SessionRequest);
        }
        else if (type == typeof(RemoteDesktopRequest))
        {
            RemoteDesktopRequestHandler(msg as RemoteDesktopRequest);
        }
        else if (type == typeof(TextMessageRequest))
        {
            TextMessageRequestHandler(msg as TextMessageRequest);
        }
        else if (type == typeof(FileUploadRequest))
        {
            FileUploadRequestHandler(msg as FileUploadRequest);
        }
        else if (type == typeof(DisconnectRequest))
        {
            OnSessionClientDisconnected();
        }
        else if (type == typeof(GenericRequest))
        {
            OnGenericRequestReceived(msg as GenericRequest);
        }
    }
}

Notice that now, I am extracting the inner message of the response before the callback invocation mechanism kicks in. That will cause the callback invocation with, again, the "real" response message as the callback parameter.

OK! Now let's see how all those changes in the code help us to extend the functionality of the library.

What I did is simply create a new generic request, and response messages in the UI project, called CalcMessageRequest, and CalcMessageResponse. These two messages are simply an example of a request with two numbers that expects a response with the sum of those numbers.

CalcMessageRequest Deriving from the GenericRequest Message

[Serializable]
public class CalcMessageRequest : Shared.Messages.GenericRequest
{
    public int A { get; set; }
    public int B { get; set; }
}

CalcMessageResponse Deriving from the GenericResponse Message

[Serializable]
public class CalcMessageResponse : Shared.Messages.GenericResponse
{
    public CalcMessageResponse(CalcMessageRequest request)
        : base(request)
    {

    }

    public int Result { get; set; }
}

Now, if you will take a look at the SendGenericRequest method, you will see that this method is a generic method and expects a type. This type is the type of delegate that is required in order to invoke the callback with the right generic response message as the callback parameter. So we also need to create a delegate we can pass to the SendGenericRequest method.

The Delegate Type to be Used for Invoking the Request Callback

public delegate void CalcMessageResponseDelegate(Client senderClient, CalcMessageResponse response);

Finally, let's use those messages from the UI project.

Send a CalcMessageRequest, Provide the CalcMessageResponseDelegate as the Callback Type

private void btnCalc_Click(object sender, EventArgs e)
{
    MessagesExtensions.CalcMessageRequest request = new MessagesExtensions.CalcMessageRequest();
    request.A = 10;
    request.B = 5;

    client.SendGenericRequest<MessagesExtensions.CalcMessageResponseDelegate>
                              (request, (clientSender,response) => {

        InvokeUI(() => {

            MessageBox.Show(response.Result.ToString());

        });

    });
}

The message was sent! Now, we need to register to the GenericRequestReceived event in order to handle the different messages and send back the right response, just like the Client class.

Register for the GenericRequestReceived Event

client.GenericRequestReceived += client_GenericRequestReceived;

Filter the Message by the Message Type and Return a Response

void client_GenericRequestReceived(Client client, Shared.Messages.GenericRequest msg)
{
    if (msg.GetType() == typeof(MessagesExtensions.CalcMessageRequest))
    {
        MessagesExtensions.CalcMessageRequest request = msg as MessagesExtensions.CalcMessageRequest;

        MessagesExtensions.CalcMessageResponse response = 
                           new MessagesExtensions.CalcMessageResponse(request);
        response.Result = request.A + request.B;
        client.SendGenericResponse(response);
    }
}

Summary

The WCF (Window Communication Foundation) framework can provide one callback per contract (interface) in contrast to what we have seen in this article. I think we have learned that there are other, or even better ways of developing solid communication applications, just by creating a solid ground, and work our way up with no limits, and with no need for any complex frameworks.

I hope you enjoyed the reading. :)

License

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

Share

About the Author

Roy Ben Shabat
Software Developer
Israel Israel
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionI wan to know .net socket very well, could you recommend some great .net socket book for me? Pin
13-Mar-18 22:45
member13-Mar-18 22:45 
AnswerRe: I wan to know .net socket very well, could you recommend some great .net socket book for me? Pin
Roy Ben Shabat14-Mar-18 2:29
professionalRoy Ben Shabat14-Mar-18 2:29 
GeneralRe: I wan to know .net socket very well, could you recommend some great .net socket book for me? Pin
14-Mar-18 15:45
member14-Mar-18 15:45 
QuestionDoes one client can communicate with more than one client. Pin
13-Mar-18 22:40
member13-Mar-18 22:40 
AnswerRe: Does one client can communicate with more than one client. Pin
Roy Ben Shabat14-Mar-18 2:25
professionalRoy Ben Shabat14-Mar-18 2:25 
GeneralRe: Does one client can communicate with more than one client. Pin
14-Mar-18 15:43
member14-Mar-18 15:43 
Questionclient and server run in 2 different pc Pin
9-Jan-18 23:31
member9-Jan-18 23:31 
AnswerRe: client and server run in 2 different pc Pin
Roy Ben Shabat12-Mar-18 23:47
professionalRoy Ben Shabat12-Mar-18 23:47 
GeneralMy vote of 5 Pin
17-Oct-17 6:15
member17-Oct-17 6:15 
GeneralRe: My vote of 5 Pin
Roy Ben Shabat8-Nov-17 23:32
professionalRoy Ben Shabat8-Nov-17 23:32 
QuestionI need to send and receive message without serialization Pin
Salvador Aspée16-Oct-17 11:58
memberSalvador Aspée16-Oct-17 11:58 
QuestionReceiver alive after disconnect Pin
stateofthesoul15-May-17 4:28
memberstateofthesoul15-May-17 4:28 
AnswerRe: Receiver alive after disconnect Pin
Roy Ben Shabat14-Jul-17 6:14
professionalRoy Ben Shabat14-Jul-17 6:14 
Questionyou just saved a life :) Pin
monroed1117-Oct-16 5:21
membermonroed1117-Oct-16 5:21 
AnswerRe: you just saved a life :) Pin
Roy Ben Shabat25-Oct-16 1:17
professionalRoy Ben Shabat25-Oct-16 1:17 
SuggestionThank you Pin
ganaa7200120-Sep-16 1:53
memberganaa7200120-Sep-16 1:53 
GeneralRe: Thank you Pin
Roy Ben Shabat20-Sep-16 2:08
professionalRoy Ben Shabat20-Sep-16 2:08 
QuestionMessage Server to Receivers?? Pin
19-Aug-16 18:05
member19-Aug-16 18:05 
AnswerRe: Message Server to Receivers?? Pin
Roy Ben Shabat20-Aug-16 4:38
professionalRoy Ben Shabat20-Aug-16 4:38 
GeneralMy vote of 5 Pin
joyhen12312328-Jun-16 5:55
memberjoyhen12312328-Jun-16 5:55 
GeneralRe: My vote of 5 Pin
Roy Ben Shabat29-Jun-16 8:00
professionalRoy Ben Shabat29-Jun-16 8:00 
Questionhow to enable in public network Pin
Michael Azzar14-Jun-16 6:27
memberMichael Azzar14-Jun-16 6:27 
AnswerRe: how to enable in public network Pin
Roy Ben Shabat14-Jun-16 7:00
professionalRoy Ben Shabat14-Jun-16 7:00 
Questionno need to frame message? Pin
Member 1041356128-Apr-16 0:50
professionalMember 1041356128-Apr-16 0:50 
AnswerRe: no need to frame message? Pin
Roy Ben Shabat28-Apr-16 7:02
professionalRoy Ben Shabat28-Apr-16 7:02 

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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180920.1 | Last Updated 26 Apr 2015
Article Copyright 2015 by Roy Ben Shabat
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid