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);
server.Start();
Console.WriteLine("Server started. Waiting for connection...");
TcpClient newClient = server.AcceptTcpClient();
Console.WriteLine("New client connected!");
if (newClient.Available > 0)
{
byte[] readBytes = new byte[newClient.Available];
newClient.GetStream().Read(readBytes, 0, newClient.Available);
String str = System.Text.Encoding.ASCII.GetString(readBytes);
Console.WriteLine(str);
}
}
private void StartClient()
{
TcpClient client = new TcpClient();
client.Connect("localhost", 8888);
String str = "Hello world";
NetworkStream stream = client.GetStream();
byte[] bytesToSend = System.Text.Encoding.ASCII.GetBytes(str);
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 string
s 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
Sirilix.AdvancedTCP.Server
(contains server related classes) Sirilix.AdvancedTCP.Client
(Contains client related classes) Sirilix.AdvancedTCP.Shared
(Contains shared classes between assemblies) Sirilix.AdvancedTCP.Server.UI
(A WinForms application demonstrating how to make use of the Server project) 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:
- Start the
Listener
. - Wait for incoming connection.
- Accept the connection.
- Initialize a new
Receiver
with the new TcpClient
, and add it to the Receivers
list. - Start the Receiver.
- 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.
- They will share some properties.
- 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)
{
ValidationRequest request = new ValidationRequest();
request.Email = email;
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 (() =>
{
Status = StatusEnum.Validated;
Email = request.Email;
response.IsValid = true;
SendMessage(response);
Server.OnClientValidated(this);
},
() =>
{
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)
{
ValidationRequest request = new ValidationRequest();
request.Email = email;
AddCallback(callback, request);
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(() =>
{
response.IsConfirmed = true;
response.Email = request.Email;
SendMessage(response);
},
() =>
{
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;
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; }
public RemoteDesktopRequest()
{
Quality = 50;
}
}
Remote Desktop Response
[Serializable]
public class RemoteDesktopResponse : ResponseMessageBase
{
public RemoteDesktopResponse(RequestMessageBase request)
: base(request)
{
DeleteCallbackAfterInvoke = false;
}
public MemoryStream FrameBytes { get; set; }
public bool Cancel { get; set; }
}
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);
response.FrameBytes.Dispose();
});
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);
response.FrameBytes.Dispose();
response.Cancel = true;
});
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;
assemblyName = currentAssembly;
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. :)