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

A Complete TCP Server/Client Communication and RMI Framework - Usage

Rate me:
Please Sign up or sign in to vote.
4.94/5 (102 votes)
12 Jun 2011CPOL22 min read 334.1K   26.1K   247   134
An Open Source lightweight framework (named Simple Client Server Library (SCS)) that is delevoped to create Server/Client applications using the simple Remote Method Invocation mechanism.

Article outline

What is SCS?

In this article, I will examine an Open Source, lightweight, fast and scalable framework (named Simple Client Server Library (SCS)) that was developed to create server/client applications using the simple Remote Method Invocation mechanism. It is entirely developed in C# and .NET Framework 4.0. It uses TCP as the transport layer protocol, but the core library is written protocol-independent and can be extended to be able to use any other protocol. You can take a look at the second article to see performance of the framework.

SCS allows clients to call methods of a server application over an Interface just like usual method calls in the same application (like Service Contracts in WCF). Server application can also easily call client methods in the same way over an Interface. The SCS framework is a double-way, connection-oriented, and asynchronous communication library. So, after a client connects to the server, they can communicate in both directions (server-to-client or client-to-server) asynchronously until client or server closes the connection. It is not just a Request/Reply communication like web services.

I developed the SCS framework and have been using it in real life. Here, there is a list of some features of SCS:

  • It is an Open Source server/client based framework.
  • Allows remote method calls from client to server and from server to client easily. Can throw exceptions across applications.
  • Allows acynhronous or synchronous low level messaging instead of remote method calls.
  • It is scalable (15000+ clients concurrently connected and communicating while server has only 50-60 threads) and fast (5,500 remote method calls, 62,500 messages transfer between applications in a second running in a regular PC).
  • Allows clients to automatically reconnect to the server.
  • Allows clients to automatically ping to the server to keep the connection available when no communication occurs with the server for a while.
  • Allows a server to register events for new client connections, disconnecting of a client, etc.
  • Allows a client to register events for connecting and disconnecting.
  • It is suitable for long session connections between clients and server.

In this article, we will develop two sample applications using the SCS framework to examine the usage and its benefits.

The first example is a simple phone book application. In this example, we will see how to build a Request/Reply style phone book server/client architecture. The phone book will be stored in the server, and clients can connect to the server, add/delete or query a phone number for a person. For simplicity, they will be console based applications.

In the second example, we will develop a complete chat system that has public and private messaging. The user interface will be developed in WPF. The user will take a nick and connect to a chat server. A connected user can see all other users in the chat room, can send public messages to the room (all users see the message), and can chat with any other user privately.

A simple server/client based phone book application using SCS

As I mentioned above, clients and the server communicate with remote method calls over service/client contract interfaces. These interfaces are generally defined in a separate assembly (DLL) to be used by both client and server applications. So, let's create a new Class Library Project named PhoneBookCommonLib in the solution OnlinePhoneBook, as shown below.

TCP-Server-Client/01_AddCommonLib.jpg

Figure 1: Creating a Class Library project to define the service contract for the PhoneBook server

First, we must add a reference to the SCS framework:

TCP-Server-Client/01_AddReferenceToScs.gif

Now, we will define the service contract interface named IPhoneBookService to define the methods that will be remotely called by the clients.

C#
using Hik.Communication.ScsServices.Service;

namespace PhoneBookCommonLib
{
    /// <summary>
    /// This interface defines methods of Phone Book Service
    /// that can be called remotely by client applications.
    /// </summary>
    [ScsService(Version = "1.0.0.0")]
    public interface IPhoneBookService
    {
        /// <summary>
        /// Adds a new person to phone book.
        /// </summary>
        /// <param name="recordToAdd">Person informations to add</param>
        void AddPerson(PhoneBookRecord recordToAdd);
 
        /// <summary>
        /// Deletes a person from phone book.
        /// </summary>
        /// <param name="name">Name of the person to delete</param>
        /// <returns>True, if a person is deleted,
        ///          false if person is not found</returns>
        bool DeletePerson(string name);
 
        /// <summary>
        /// Searches a person in phone book by name of person.
        /// </summary>
        /// <param name="name">Name of person to search.
        /// Name might not fully match, it can be a part of person's name</param>
        /// <returns>Person informations if found, else null</returns>
        PhoneBookRecord FindPerson(string name);
    }
}

Service contracts must be marked by the ScsService attribute. You can define the version of your service. In any RMI exception, clients get this version information in the exception message, so they can know if the service has changed. The PhoneBookRecord class is shown below:

C#
using System;

namespace PhoneBookCommonLib
{
    /// <summary>
    /// Represents a record in phone book.
    /// </summary>
    [Serializable]
    public class PhoneBookRecord
    {
        /// <summary>
        /// Name of the person.
        /// </summary>
        public string Name { get; set; }
 
        /// <summary>
        /// Phone number of the person.
        /// </summary>
        public string Phone { get; set; }
 
        /// <summary>
        /// Creation date of this record.
        /// </summary>
        public DateTime CreationDate { get; set; }
 
        /// <summary>
        /// Creates a new PhoneBookRecord object.
        /// </summary>
        public PhoneBookRecord()
        {
            CreationDate = DateTime.Now;
        }
 
        /// <summary>
        /// Generates a string representation of this object.
        /// </summary>
        /// <returns>String representation of this object</returns>
        public override string ToString()
        {
            return string.Format("Name = {0}, Phone = {1}", Name, Phone);
        }
    }
}

If the service/client contracts define a custom class in method invocations, that class must be marked as Serializable. So, here it is just like the usual interface and class definitions. Let's start developing the server side by creating a new Console Application Project in our solution, named PhoneBookServer.

TCP-Server-Client/02_AddServerProject.jpg

Figure 2: Creating a Console Application to build the PhoneBook server

First we must add references to the SCS framework and the PhoneBookCommonLib project. Then we must implement the IPhoneBookService interface. We create a class named PhoneBookService as shown below:

C#
using System;
using System.Collections.Generic;
using Hik.Communication.ScsServices.Service;
using PhoneBookCommonLib;
 
namespace PhoneBookServer
{
    /// <summary>
    /// This class implements Phone Book Service contract.
    /// </summary>
    class PhoneBookService : ScsService, IPhoneBookService
    {
        /// <summary>
        /// Current records that are added to phone book service.
        /// Key: Name of the person.
        /// Value: PhoneBookRecord object.
        /// </summary>
        private readonly SortedList<string, PhoneBookRecord> _records;
 
        /// <summary>
        /// Creates a new PhoneBookService object.
        /// </summary>
        public PhoneBookService()
        {
            _records = new SortedList<string, PhoneBookRecord>();
        }
 
        /// <summary>
        /// Adds a new person to phone book.
        /// </summary>
        /// <param name="recordToAdd">Person informations to add</param>
        public void AddPerson(PhoneBookRecord recordToAdd)
        {
            if (recordToAdd == null)
            {
                throw new ArgumentNullException("recordToAdd");
            }
 
            _records[recordToAdd.Name] = recordToAdd;
        }
 
        /// <summary>
        /// Deletes a person from phone book.
        /// </summary>
        /// <param name="name">Name of the person to delete</param>
        /// <returns>True, if a person is deleted,
        ///     false if person is not found</returns>
        public bool DeletePerson(string name)
        {
            if (!_records.ContainsKey(name))
            {
                return false;
            }
 
            _records.Remove(name);
            return true;
        }
 
        /// <summary>
        /// Searches a person in phone book by name of person.
        /// </summary>
        /// <param name="name">Name of person to search.
        /// Name might not fully match, it can be a part of person's name</param>
        /// <returns>Person informations if found, else null</returns>
        public PhoneBookRecord FindPerson(string name)
        {
            //Get recods by name if there is a record exactly match to given name
            if (_records.ContainsKey(name))
            {
                return _records[name];
            }
 
            //Search all records to check if there
            //is a name string that contains given name
            foreach (var record in _records)
            {
                if (record.Key.ToLower().Contains(name.ToLower()))
                {
                    return record.Value;
                }
            }
 
            //Not found
            return null;
        }
    }
}

All services must derive from the ScsService class and implement the service contract. Our phone book service is almost complete. Now we change application's Main method to create the server application to finish the phone book service.

C#
using System;
using Hik.Communication.Scs.Communication.EndPoints.Tcp;
using Hik.Communication.ScsServices.Service;
using PhoneBookCommonLib;
 
namespace PhoneBookServer
{
    class Program
    {
        static void Main(string[] args)
        {
            //Create a Scs Service application that runs on 10048 TCP port.
            var server = ScsServiceBuilder.CreateService(new ScsTcpEndPoint(10048));
            
            //Add Phone Book Service to service application
            server.AddService<IPhoneBookService, 
                       PhoneBookService>(new PhoneBookService());
            
            //Start server
            server.Start();
 
            //Wait user to stop server by pressing Enter
            Console.WriteLine(
                "Phone Book Server started successfully. Press enter to stop...");
            Console.ReadLine();
            
            //Stop server
            server.Stop();
        }
    }
}

We first create a service application that listens to the 10048 TCP port for incoming client connections. Then we add our phone book service to this service application. While adding a service, we must specify the service contract (IPhoneBookService) and the service class (PhoneBookService) that implements the contract. We can add more than one service to the service application to run more than one service from the same end point (same TCP port). That's all; until the service is stopped by the user, clients can connect to the service and call the service methods.

Now, we can create a client application that uses this service. Create a new Console Application Project named PhoneBookClient in the OnlinePhoneBook solution.

TCP-Server-Client/02_AddServerProject.jpg

Figure 3: Creating a Console Application to build the PhoneBook client

First add references to the SCS framework and the PhoneBookCommonLib project. Then we can write a sample client code that uses the phone book service:

C#
Using System;
using Hik.Communication.Scs.Communication.EndPoints.Tcp;
using Hik.Communication.ScsServices.Client;
using PhoneBookCommonLib;
 
namespace PhoneBookClient
{
    class Program
    {
        static void Main(string[] args)
        {
            //Create a client to connecto to phone book service on local server and
            //10048 TCP port.
            Var client = ScsServiceClientBuilder.CreateClient<IPhoneBookService>(
                new ScsTcpEndPoint("127.0.0.1", 10048));
 
            Console.WriteLine("Press enter to connect to phone book service...");
            Console.ReadLine();
 
            //Connect to the server
            client.Connect();
 
            var person1 = new PhoneBookRecord { Name = "Halil Ibrahim", 
                Phone = "5881112233" };
            var person2 = 
              new PhoneBookRecord { Name = "John Nash", Phone = "58833322211" };
 
            //Add some persons
            client.ServiceProxy.AddPerson(person1);
            client.ServiceProxy.AddPerson(person2);
 
            //Search for a person
            var person = client.ServiceProxy.FindPerson("Halil");
            if (person != null)
            {
                Console.WriteLine("Person is found:");
                Console.WriteLine(person);
            }
            else
            {
                Console.WriteLine("Can not find person!");
            }
 
            Console.WriteLine();
            Console.WriteLine("Press enter to disconnect from phone book service...");
            Console.ReadLine();
 
            //Disconnect from server
            client.Disconnect();
        }
    }
}

When you look at the code above, calling a method of the service is very easy and straightforward. You can use all the methods that are defined in IPhoneBookService. We first create a client object that connects to the server to use IPhoneBookService that is running on the local machine (127.0.0.1), 10048 TCP port. Then we connect to the server, call the remote methods, and disconnects in the end. If you first run PhoneBookServer, then run PhoneBookClient, and press Enter, you will see two console screens like:

TCP-Server-Client/04_PhoneBookRunning.gif

Figure 4: Screenshot of running PhoneBook client and server

As shown in this example application, you can easily create TCP based server/client applications in .NET using the SCS framework.

More features on the client side

The SCS client supports the IDisposible interface, so you do not have to disconnect from the server if you write the code in a using block like this:

C#
using (var client = ScsServiceClientBuilder.CreateClient<IPhoneBookService>(
                    new ScsTcpEndPoint("127.0.0.1", 10048)))
{
    client.Connect();
    client.ServiceProxy.AddPerson(
              new PhoneBookRecord {Name = "Halil", Phone = "2221144"});
}

This is useful if you want to connect to the server, call service methods, and disconnect just like web service method calls. In fact, you do not have to explicitly connect/disconnect. This code will also work fine:

C#
var client = ScsServiceClientBuilder.CreateClient<IPhoneBookService>(
             SscEndPoint.CreateEndPoint("tcp://127.0.0.1:10048")))
client.ServiceProxy.AddPerson(
          new PhoneBookRecord {Name = "Halil", Phone = "2221144"});

In this situation, SCS automatically connects, calls AddPerson method and disconnects. If you want to make a single method call, this approach is usefull. But if you want to make more than one call with a client object, it is much more performant to connect and disconnect explicitly. Note that I used ScsEndPoint.CreateEndPoint(...) method to create a ScsTcpEndPoint object from a string address.

SCS client also provides a Timeout property (as milliseconds) for a remote method call, a CommunicationState property to check if a client is connected to the server, and Connected and Disconnected events that are raised when a client is connected to or disconnected from server, respectively.

If server throws an exception, a client can catch it using a try/catch statement. For example, PhoneBookService throws an ArgumentNullException in the AddPerson method if the recordToAdd object is null. You can write code like this to catch it:

C#
//Try to save a person with null object
//to demonstrate an exceptional situation
try
{
    client.ServiceProxy.AddPerson(null);
}
catch (Exception ex)
{
    Console.WriteLine();
    Console.WriteLine(ex.Message);
}

When you run this code, you will see a message like this:

TCP-Server-Client/05_AddPersonException.gif

Figure 5: Console output of PhoneBook client application on an exception situation

As you can see, the service version is automatically added to the exception message.

More features on the server side

We create a server side that just responds to remote method calls and does nothing else. We can also call client methods just like client-to-server method invocations. I will briefly talk about some functionality here, but we will better understand these functionalities in the next example (in the IRC (chat) application).

To control a client on the server side, we first need to get a reference to the IScsServiceClient interface of the client by using the ScsService.CurrentClient property. We can use this property in any method of PhoneBookService like:

TCP-Server-Client/06_CurrentClientProperty.gif

Figure 6: The CurrentClient property of the ScsService class

This property is only valid if this method is defined in the service contract (IPhoneBookService) and called by the client. It is thread safe, and it gets the current client which is calling this method. After getting (and storing) a reference to the client, we can get its unique number using the ClientId property, communication state (Connected/Disconnected) using the CommunicationState property, and we can register to the Disconnected event to be informed when the client disconnects from the server. Last but not least, we can get a Proxy object to call remote methods of the client using the GetClientProxy method. This method is generic; it gets the type of the client contract interface as a generic parameter. Before using this method, we must define an interface that the client implements it (we will see it in the next example chatg application). So, if we get the proxy object and store it, we can call the client methods when we want. This way, we can go beyond the Request/Reply style server/client architecture.

An IRC chat system using the SCS framework

You probably know what IRC is. Although it is not widely used nowadays as MSN, it was very popular in the past. IRC is an acronym for Internet Relay Chat. Simply, in IRC, there are rooms that users join. After joining a room, you can see all the other users who are currently in the room. You can write a message to the room (that is seen by all users in the room), or you can click a user in the room and chat with the user privately (other users don't see your private messages except the user who you are messaging). Our simple IRC system provides just one room for users. They automatically join the default room and can begin to chat immediately after selecting a nick.

In this example application, I will demonstrate the usage of the system, then explain how I implemented it over the SCS framework. I will not go deep into all the parts of the application other than the SCS-related parts.

Using the chat system

After downloading the chat system solution to your computer, first run the server application (ChatServerApp.exe), enter a TCP port number (or leave 10048 as the default value) to listen to clients, then click the Start Server button. Now you can see the server form as shown below:

TCP-Server-Client/07_Server.jpg

Figure 7: Chat server application

In the sample screen above, you see four online users that are connected to the server and chatting. Now run ChatClientApp.exe to start the client application. You will see a window like below.

TCP-Server-Client/08_chatclient1.jpg

Figure 8: Login screen of the chat client application

You can enter a nick, the server's IP, and the TCP port. Leave the IP and port as default if you are testing in the same computer with the server application; otherwise you can enter the IP and port of the server application. You can change your avatar picture by right clicking the picture as shown below:

TCP-Server-Client/09_client_change_picture.jpg

Figure 9: Changing the avatar picture in the chat client application

You can select the default male/female picture, or your own image. If you run a few copies of the client application with different nicks, you will see a screen like this:

TCP-Server-Client/10_chatclient2.jpg

Figure 10: Sample screenshot from the chat client application while chatting on a public room

You can write messages in different message styles, set your status, set sound on/off... etc. The main window of the client allows you to chat publicly. All users in a room can see your messages. If you double click a user in the user list, you can chat privately with that user:

TCP-Server-Client/11_chatclient3.jpg

Figure 11: Chatting in a private window

You are free to investigate the chat application more deeply. Maybe it is more than a sample application to demonstrate the usage of the SCS framework.

Implementation of the chat server

Now I will examine the implementation of the chat server. Ihave created three project: ChatServerApp (WPF application), ChatClientApp (WPF application), and ChatCommonLib (DLL project). The last one is used to define the service and client contracts and the objects that are transferred between the clients and the server.

The server contract interface defines the methods of the server that can be called by the clients remotely. It is defined in the IChatServer interface in the ChatCommonLib assembly.

C#
using Hik.Communication.ScsServices.Service;
using Hik.Samples.Scs.IrcChat.Arguments;
 
namespace Hik.Samples.Scs.IrcChat.Contracts
{
    /// <summary>
    /// This interface defines Chat Service Contract.
    /// It is used by Chat clients to interact with Chat Server.
    /// </summary>
    [ScsService(Version = "1.0.0.0")]
    public interface IChatService
    {
        /// <summary>
        /// Used to login to chat service.
        /// </summary>
        /// <param name="userInfo">User informations</param>
        void Login(UserInfo userInfo);
        
        /// <summary>
        /// Sends a public message to room.
        /// It will be seen by all users in room.
        /// </summary>
        /// <param name="message">Message to be sent</param>
        void SendMessageToRoom(ChatMessage message);
        
        /// <summary>
        /// Sends a private message to a specific user.
        /// Message will be seen only by destination user.
        /// </summary>
        /// <param name="destinationNick">Nick of the destination user
        /// who will receive the message</param>
        /// <param name="message">Message to be sent</param>
        void SendPrivateMessage(string destinationNick, ChatMessage message);
 
        /// <summary>
        /// Changes status of a user and inform all other users.
        /// </summary>
        /// <param name="newStatus">New status of user</param>
        void ChangeStatus(UserStatus newStatus);
        
        /// <summary>
        /// Used to logout from chat service.
        /// Client may not call this method while logging out (in an application 
        /// crash situation),
        /// it will also be logged out automatically when connection fails between
        /// client and server.
        /// </summary>
        void Logout();
    }
}

I think all methods describe themselves. The client first calls the Login() method (when the user clicks the Login button on the client window) to login to the server. Then it can send messages to the room by using the SendMessageToRoom(...) method, send private messages by using the SendPrivateMessage(...) method, and so on. Finally, when the user closes the main window, the client calls the Logout() method.

We must first implement the server contract (IChatServer), then register it to a SCS Server Application object (as we did before in the PhoneBook application). I implemented the IChatServer application using the ChatServer class. I will examine here the various methods in detail. The first one is the Login method. It is defined as below:

C#
/// <summary>
/// Used to login to chat service.
/// </summary>
/// <param name="userInfo">User informations</param>
public void Login(UserInfo userInfo)
{
    //Check nick if it is being used by another user
    if (FindClientByNick(userInfo.Nick) != null)
    {
        throw new NickInUseException("The nick '" + userInfo.Nick + 
            "' is being used by another user. Please select another one.");
    }
 
    //Get a reference to the current client that is calling this method
    var client = CurrentClient;
 
    //Get a proxy object to call methods of client when needed
    var clientProxy = client.GetClientProxy<IChatClient>();
 
    //Create a ChatClient and store it in a collection
    var chatClient = new ChatClient(client, clientProxy, userInfo);
    _clients[client.ClientId] = chatClient;
 
    //Register to Disconnected event to know when user connection is closed
    client.Disconnected += Client_Disconnected;
 
    //Start a new task to send user list to new user and to inform
    //all users that a new user joined to room
    Task.Factory.StartNew(
        () =>
        {
            OnUserListChanged();
            SendUserListToClient(chatClient);
            SendUserLoginInfoToAllClients(userInfo);
        });
}

First we check if there is a user with the same nick. If so, we throw a NickInUseException. We can safely throw exceptions in service methods. It is handled by the SCS framework and thrown on the client side (the exception must be serializable; see NickInUseException for a sample; as far as I know, all predefined exception classes in the .NET Framework are serializable, you can throw them or define your own if needed). Then we get a reference to the current client object (that implements IScsServiceClient) by using the CurrentClient property. This is the key object if our system uses client side contract interfaces (that are called by the server). This property is thread safe (even if more than one client thread calls the same method, you can get the right client object using this property). Now, we can obtain a reference to the client contract interface by calling the GetClientProxy<IChatClient>() method. This is a generic method. You can call it with any interface. We call it with IChatClient because our client side methods are defined in the IChatClient interface (we will see the definition and implementation of this interface soon; we will call all client methods using this dynamic and transparent proxy reference). Then we create a ChatClient object to store client information. It is defined as below as a subclass of the ChatService class:

C#
/// <summary>
/// This class is used to store informations for a connected client.
/// </summary>
private sealed class ChatClient
{
    /// <summary>
    /// Scs client reference.
    /// </summary>
    public IScsServiceClient Client { get; private set; }
 
    /// <summary>
    /// Proxy object to call remote methods of chat client.
    /// </summary>
    public IChatClient ClientProxy { get; private set; }
 
    /// <summary>
    /// User informations of client.
    /// </summary>
    public UserInfo User { get; private set; }
 
    ...
}

After creating a ChatClient object, we add it to a kind of SortedList collection named _clients. We use the ClientId of the current client as the key for that client. ClientId is a unique (long) number that is generated automatically by the SCS framework and assigned to clients. We can safely use this value to distinguish clients. The _clients collection is defined as below:

C#
/// <summary>
/// List of all connected clients.
/// </summary>
private readonly ThreadSafeSortedList<long, ChatClient> _clients;

ThreadSafeSortedList is a kind of wrapper I wrote to perform thread safe operations on a SortedList collection. Because we are serving all clients in a multithreaded manner, all methods may be called simultaneously. So we must prevent a collection from being accessed by two or more threads simultaneously. ThreadSafeSortedList internally does that.

Last but not least, we register to the Disconnect event of the client. This is needed to be informed when the user disconnects from the server (especially if it is disconnected before calling Logout). The rest of the Login method is implementation details. We inform the GUI for user list changing, send a list of all online users to a newly connected client (in the SendUserListToClient(...) method), and send a notification to all other users that there is a new user connected to the server (these three operations are performed in a separate thread to not block a user for a long time, because remote method invocations on SCS are blocking operations).

Now, let's look at the implementation of the Logout() method in the ChatServer class.

C#
/// <summary>
/// Used to logout from chat service.
/// Client may not call this method while logging
/// out (in an application crash situation),
/// it will also be logged out automatically
/// when connection fails between client and server.
/// </summary>
public void Logout()
{
    ClientLogout(CurrentClient.ClientId);
}
 
/// <summary>
/// Handles Disconnected event of all clients.
/// </summary>
/// <param name="sender">Client object that is disconnected</param>
/// <param name="e">Event arguments (not used in this event)</param>
private void Client_Disconnected(object sender, EventArgs e)
{
    //Get client object
    var client = (IScsServiceClient)sender;
 
    //Perform logout (so, if client did not call Logout method before close,
    //we do logout automatically.
    ClientLogout(client.ClientId);
}
/// <summary>
/// This method is called when a client calls
/// the Logout method of service or a client
/// connection fails.
/// </summary>
/// <param name="clientId">Unique Id of client that is logged out</param>
private void ClientLogout(long clientId)
{
    //Get client from client list, if not in list do not continue
    var client = _clients[clientId];
    if (client == null)
    {
        return;
    }
 
    //Remove client from online clients list
    _clients.Remove(client.Client.ClientId);
 
    //Unregister to Disconnected event (not needed really)
    client.Client.Disconnected -= Client_Disconnected;
 
    //Start a new task to inform all other users
    Task.Factory.StartNew(
        () =>
        {
            OnUserListChanged();
            SendUserLogoutInfoToAllClients(client.User.Nick);
        });
}

In the Logout method, we get the unique ClientId value of the client that has called the Logout method and is calling the ClientLogout(...) method. We also handle the Disconnect event of clients in the Client_Disconnected(...) method. Getting a reference to the client object is different in this event handler method. It is obtained by casting the sender object to the IScsServiceClient interface (CurrentClient property is only valid in the service contract methods). The ClientLogout method simply gets and removes the client object from the _clients collection, unregisters the Disconnected event (even though it is not an obligation), then informs all other users that a user logged out from the server.

Now, I will examine the implementation of the SendPrivateMessage(...) method. This method is used to send a private message to a user, and is defined as below.

C#
/// <summary>
/// Sends a private message to a specific user.
/// Message will be seen only by destination user.
/// </summary>
/// <param name="destinationNick">Nick of the destination
///          user who will receive message</param>
/// <param name="message">Message to be sent</param>
public void SendPrivateMessage(string destinationNick, ChatMessage message)
{
    //Get ChatClient object for sender user
    var senderClient = _clients[CurrentClient.ClientId];
    if (senderClient == null)
    {
        throw new ApplicationException("Can not send message before login.");
    }
 
    //Get ChatClient object for destination user
    var receiverClient = FindClientByNick(destinationNick);
    if (receiverClient == null)
    {
        throw new ApplicationException("There is no online " + 
                  "user with nick " + destinationNick);
    }
 
    //Send message to destination user
    receiverClient.ClientProxy.OnPrivateMessage(senderClient.User.Nick, message);
}

First, we get the client object (that is a ChatClient object) from the _clients list and checks if the current user logged in (remember that we store clients in the _clients list in the Login() method using their unique ClientId). Then we try to find the destination user using the FindClientByNick(...) method. It is a simple method that is defined as below:

C#
/// <summary>
/// Finds ChatClient ojbect by given nick.
/// </summary>
/// <param name="nick">Nick to search</param>
/// <returns>Found ChatClient for that nick, or null if not found</returns>
private ChatClient FindClientByNick(string nick)
{
    return (from client in _clients.GetAllItems()
            where client.User.Nick == nick
            select client).FirstOrDefault();
}

It uses a LINQ expression to find a ChatClient object in the _clients collection using the user nick. If there is no user with that nick, it returns null. The SendPrivateMessage(...) method throws an exception if no client exists that uses the destination nick (this may happen if the destination user has left the chat room just before this method was invoked; this is low probability, but we must handle it anyway (Murphy's law says that "If anything bad can happen, it probably will.")). At the end of the SendPrivateMessage(...) method, we call the destination client's OnPrivateMessage(...) method remotely (this method is defined in the IChatClient interface and will be examined soon) so it can get the incoming message.

It will not be explained here but the ChatServer class also defines an event named UserListChanged to notify the server GUI (WPF window) when a user is added or removed from the _clients collection. The ChatServer class is created and used by MainWindow of the chat server. In the handler method of the Click event of the btnStart button, we create and start the service just like below:

C#
_serviceApplication = ScsServiceBuilder.CreateService(new ScsTcpEndPoint(port));
_chatService = new ChatService();
_serviceApplication.AddService<IChatService, ChatService>(_chatService);
_chatService.UserListChanged += chatService_UserListChanged;
_serviceApplication.Start();

It is very similar to the PhoneBookService application. We create a SCS service application that runs on a TCP port (that is entered by the user in the txtPort textbox). Then we create an instance of the ChatService class, registers it to the service application using the AddService method, registers to the UserListChanged event (to reflect user list changes to the GUI), and start the service application. To stop the server, we call the Stop method of IScsServiceApplication.

C#
_serviceApplication.Stop();

We have learned how to write a multithreaded server that can be invoked by clients and can invoke client methods. By the way, you can add more than one service to a service application that runs on the same end point (TCP port). The SCS framework handles this situation, and calls the correct method of the correct service according to the clients that are using the service (remember that you supply the service contract interface when creating a client object on the client side). This approach is very useful if you don't want to use separate ports for each service and run more than one service in the same process.

Implementation of the chat client

Although our sample chat application is a little complex (because of WPF and other additional functionalities), the SCS related parts are easy to understand. Using the service by calling remote methods of the service contract is the same as the PhoneBook client. In addition, the chat client can be invoked by the server (this is the two-way communication of the SCS framework), so it must define a client contract. It is defined in the IChatClient interface like below:

C#
using Hik.Communication.ScsServices.Service;
using Hik.Samples.Scs.IrcChat.Arguments;
 
namespace Hik.Samples.Scs.IrcChat.Contracts
{
    /// <summary>
    /// This interface defines methods of chat client.
    /// Defined methods are called by chat server.
    /// </summary>
    public interface IChatClient
    {
        /// <summary>
        /// This method is used to get user list from chat server.
        /// It is called by server once after user logged in to server.
        /// </summary>
        /// <param name="users">All online user informations</param>
        void GetUserList(UserInfo[] users);
 
        /// <summary>
        /// This method is called from chat server to inform that a message
        /// is sent to chat room publicly.
        /// </summary>
        /// <param name="nick">Nick of sender</param>
        /// <param name="message">Message</param>
        void OnMessageToRoom(string nick, ChatMessage message);
 
        /// <summary>
        /// This method is called from chat server to inform that a message
        /// is sent to the current user privately.
        /// </summary>
        /// <param name="nick">Nick of sender</param>
        /// <param name="message">Message</param>
        void OnPrivateMessage(string nick, ChatMessage message);
 
        /// <summary>
        /// This method is called from chat server to inform that a new user
        /// joined to chat room.
        /// </summary>
        /// <param name="userInfo">Informations of new user</param>
        void OnUserLogin(UserInfo userInfo);
 
        /// <summary>
        /// This method is called from chat server to inform that an existing user
        /// has left the chat room.
        /// </summary>
        /// <param name="nick">Informations of new user</param>
        void OnUserLogout(string nick);
        /// <summary>
        /// This method is called from chat server to inform that a user
        /// changed his/her status.
        /// </summary>
        /// <param name="nick">Nick of the user</param>
        /// <param name="newStatus">New status of the user</param>
        void OnUserStatusChange(string nick, UserStatus newStatus);
    }
}

The client contract methods define themselves in code comments. This interface is implemented in the client application by the ChatClient class of ChatClientApp. For example, the OnPrivateMessage(...) method is called by the server when another user sends a private message to this user. So, we must open a private chat window (if not already open) with the sender of the message and write a message to the message area. In this article, I will explain the implementation of the OnPrivateMessage(..) method; for other methods, please see the ChatClient class. The implementation of this method is shown below:

C#
/// <summary>
/// This method is called from chat server to inform that a message
/// is sent to the current used privately.
/// </summary>
/// <param name="nick">Nick of sender</param>
/// <param name="message">Message</param>
public void OnPrivateMessage(string nick, ChatMessage message)
{
    _chatRoom.OnPrivateMessageReceived(nick, message);
}

It does nothing but calls the OnPrivateMessageReceived(...) method of the IChatRoomView interface. This interface is independent from the GUI. The IChatRoomView interface is implemented by the MainWindow class as shown below:

C#
public partial class MainWindow : Window, IChatRoomView, 
               ILoginFormView, IMessagingAreaContainer
{
    . . .
}

As you can see, MainWindow also implements ILoginFormView (it has GUI controls to allow the user to login to the server; see figure 8) and IMessagingAreaContainer (it has a messaging area to show incoming messages to the public room; see figure 10.). By the way, MainWindow implements the IChatRoomView.OnPrivateMessageReceived method as shown below:

C#
/// <summary>
/// This method is called from chat server to inform that a message
/// is sent to the current used privately.
/// </summary>
/// <param name="nick">Nick of sender</param>
/// <param name="message">Message</param>
public void OnPrivateMessage(string nick, ChatMessage message)
{
    _chatRoom.OnPrivateMessageReceived(nick, message);
}

If you have worked before with WPF, you know that you can only use a WPF object on the thread on which it was created. Using it on other threads will cause a runtime exception. So, for example, you can not change the Text value of a TextBox in another thread than the UI thread. The SCS framework is multithreaded, therefore OnPrivateMessageReceived is called by another thread than the UI thread. So, we use a Dispatcher object to call a method from the UI thread. This method is OnPrivateMessageReceivedInternal and is defined as below:

C#
/// <summary>
/// This method is used to send private message to proper private messaging window.
/// </summary>
/// <param name="nick">Nick of sender</param>
/// <param name="message">Message</param>
private void OnPrivateMessageReceivedInternal(string nick, ChatMessage message)
{
    var userCard = FindUserInList(nick);
    if (userCard == null)
    {
        return;
    }
 
    if (!_privateChatWindows.ContainsKey(nick))
    {
        //Create new private chat window
        _privateChatWindows[nick] = CreatePrivateChatWindow(userCard);
 
        //Set initial state as minimized
        _privateChatWindows[nick].WindowState = WindowState.Minimized;
                
        //Flash the window button on taskbar to inform user
        WindowsHelper.FlashWindow(new WindowInteropHelper(
            _privateChatWindows[nick]).Handle,
            WindowsHelper.FlashWindowFlags.FLASHW_ALL, 2, 1000);
    }
 
    _privateChatWindows[nick].MessageReceived(message);
}

We first check if the user is in the user list. Then we check if _privateChatWindows (list of open private chat windows) contains a window with that user. If not, we create a new window, set its state minimized, and we apply a flashing effect in the taskbar like MSN. Finally, we call the MessageReceived event of the private messaging window (PrivateChatWindow class).

C#
/// <summary>
/// This method is used to add a new message to message history of that window.
/// </summary>
/// <param name="message">Message</param>
public void MessageReceived(ChatMessage message)
{
    MessageHistory.MessageReceived(_remoteUserNick, message);
    if (!IsActive)
    {
        //Flash taskbar button if this window is not active
        WindowsHelper.FlashWindow(_windowInteropHelper.Handle,
            WindowsHelper.FlashWindowFlags.FLASHW_TRAY, 1, 1000);
        ClientHelper.PlayIncomingMessageSound();
    }
}

This methods calls the MessageReceived(...) method of the messaging area (MessagingAreaControl user control), than flashes the taskbar button of the window to inform the user if this window is not active. Finally, the MessagingAreaControl.MessageReceived method adds a message to the RichTextBox. That's all; now we know how a private message is sent from the server, received from the client, and shown to the user.

Now, let's see how the client connects to the SCS chat server. It's done in the Connect() method of the ChatController class.

C#
/// <summary>
/// Connects to the server.
/// It automatically Logins to server if connection success.
/// </summary>
public void Connect()
{
    //Disconnect if currently connected
    Disconnect();
 
    //Create a ChatClient to handle remote method invocations by server
    _chatClient = new ChatClient(ChatRoom);
 
    //Create a SCS client to connect to SCS server
    _scsClient = ScsServiceClientBuilder.CreateClient<IChatService>(
         new ScsTcpEndPoint(LoginForm.ServerIpAddress, 
                            LoginForm.ServerTcpPort), _chatClient);
 
    //Register events of SCS client
    _scsClient.Connected += ScsClient_Connected;
    _scsClient.Disconnected += ScsClient_Disconnected;
 
    //Connect to the server
    _scsClient.Connect();
}

The only difference with the PhoneBook client in creating a client object is that we pass an object (_chatClient) that implements the IChatClient interface to handle incoming remote method calls from the server. We examined OnPrivateMessage(...) method of ChatClient class above.

As you know from the PhoneBook client application, we are using a ServiceProxy to call a method of the server as below.

C#
/// <summary>
/// Sends a private message to a user.
/// </summary>
/// <param name="nick">Destination nick</param>
/// <param name="message">Message</param>
public void SendPrivateMessage(string nick, ChatMessage message)
{
    _scsClient.ServiceProxy.SendPrivateMessage(nick, message);
}

With the SendPrivateMessage(...) method of the ChatController, we have finished examining the client side. You can see the code to more deeply understand how I have implemented the client. But we have seen all the important SCS-related parts for this article.

Some other benefits of the SCS client side

Automatically reconnecting

If you are building client applications that must always stay connected to the server, you encounter the re-connecting problem when disconnected from a server. You can, of course, handle this problem by registering the Disconnect event of the client object and trying to reconnect to the server until a connection is successfully established. Fortunately, the SCS framework has a class named ClientReconnecter that does reconnecting to the server on your behalf. First, you must create a ClientReconnecter object using your client object, as shown below.

C#
//Create a reconnecter to reconnect automatically on disconnect.
var reconnecter = new ClientReConnecter(_scsClient) {ReConnectCheckPeriod = 30000};

That's all (see the ChatController.Connect() method to know what _scsClient is). If your connection fails, the reconnecter will try to reconnect in 30 second intervals until it is successfully connected. The default value for ReConnectCheckPeriod is 20 seconds. To stop/dispose the reconnecter, you can use the Dispose() method.

Automatically pinging to stay connected

In TCP, if the server and clients don't send messages to each other (that means the communication line is idle), the connection may be lost (because of the Operating System, firewalls, routers, etc.). The SCS framework overcomes this issue by sending a special kind of ping message periodically if the communication line is idle. The period of ping messages is 30 seconds. If the client and server are already communicating, no ping message is sent.

Last words on this article

In this article, I have introduced a new framework that can be used to develop TCP based client/server systems in .NET. It has similarities with WCF but is much more simple. I think SCS may be enough if you are developing both the server and client in .NET. SCS Services are built upon a layer that can communicate with messaging. This layer is also available to the user. It has no RMI support, but has other benefits like allowing you to change the wire protocol for messaging so your application can communicate with other applications that are not built in .NET. I did not explain this layer in this article but will in the second article.

About the second article 

I have explained the implementation of the SCS framework in the second article. It demonstrates how a complete RMI framework can be accomplished upon raw TCP socket streaming. Here is the link to the second article: A Complete TCP Server/Client Communication and RMI Framework in C# .NET - Implementation.

History

  • Jun 13, 2011
    •  SCS framework updated to v1.1.0.1. 
  • May 29, 2011
    • SCS framework updated to v1.1.0.0. (Click here to see all changes/additions)
    • Article updated according to changes.
  • Apr 12, 2011
    • Updated source code of SCS.

License

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


Written By
Founder Volosoft
Turkey Turkey
I have started programming at 14 years old using Pascal as hobby. Then I interested in web development (HTML, JavaScript, ASP...) before university.

I graduated from Sakarya University Computer Engineering. At university, I learned C++, Visual Basic.NET, C#, ASP.NET and Java. I partly implemented ARP, IP and TCP protocols in Java as my final term project.

Now, I am working on Windows and web based software development mostly using Microsoft technologies in my own company.

My open source projects:

* ABP Framework: https://abp.io
* jTable: http://jtable.org
* Others: https://github.com/hikalkan

My personal web site:

https://halilibrahimkalkan.com

Comments and Discussions

 
PraiseThanks. Yasha. Pin
Member 1189008428-Aug-19 8:08
Member 1189008428-Aug-19 8:08 
Save my time. Excellent work.
Thanks.
GeneralMy vote of 5 Pin
Mehmet Burak Muştu12-Apr-19 11:09
Mehmet Burak Muştu12-Apr-19 11:09 
QuestionHow To Send multiple requests from multiple threads Pin
Hassan Mokdad20-Oct-16 22:07
Hassan Mokdad20-Oct-16 22:07 
Questionvoice Pin
filmee247-Nov-15 20:52
filmee247-Nov-15 20:52 
GeneralRe: voice Pin
Fabian Forster1-Mar-16 19:59
Fabian Forster1-Mar-16 19:59 
Questionvoice chat Pin
filmee2421-Feb-15 6:50
filmee2421-Feb-15 6:50 
QuestionServer side Disconnected does not always fire. Pin
narthir30-Jan-15 2:49
narthir30-Jan-15 2:49 
GeneralMy Vote 5 Pin
Shemeemsha (ഷെമീംഷ)3-Oct-14 17:38
Shemeemsha (ഷെമീംഷ)3-Oct-14 17:38 
BugSocketException -> The I/O operation has been aborted because of either a thread exit or an application request Pin
PrzemekBenz1-Oct-14 2:55
PrzemekBenz1-Oct-14 2:55 
GeneralRe: SocketException -> The I/O operation has been aborted because of either a thread exit or an application request Pin
nonintrusive29-Oct-14 6:20
nonintrusive29-Oct-14 6:20 
QuestionFreezes while attempting to connect - any work around? Pin
Member 1097889911-Aug-14 0:23
Member 1097889911-Aug-14 0:23 
Bugvery good, but I've a concurrency problem Pin
Red Angel19-Jul-14 0:07
Red Angel19-Jul-14 0:07 
Questionis there any manual?! Pin
Elaheh Ordoni2-Jul-14 18:44
Elaheh Ordoni2-Jul-14 18:44 
AnswerRe: is there any manual?! Pin
Halil ibrahim Kalkan2-Jul-14 20:55
Halil ibrahim Kalkan2-Jul-14 20:55 
GeneralRe: is there any manual?! Pin
Elaheh Ordoni2-Jul-14 20:56
Elaheh Ordoni2-Jul-14 20:56 
QuestionOnError, TimeOut events... Pin
Elaheh Ordoni2-Jul-14 1:31
Elaheh Ordoni2-Jul-14 1:31 
AnswerRe: OnError, TimeOut events... Pin
Halil ibrahim Kalkan2-Jul-14 3:22
Halil ibrahim Kalkan2-Jul-14 3:22 
GeneralRe: OnError, TimeOut events... Pin
Elaheh Ordoni2-Jul-14 4:18
Elaheh Ordoni2-Jul-14 4:18 
AnswerRe: OnError, TimeOut events... Pin
Halil ibrahim Kalkan2-Jul-14 20:53
Halil ibrahim Kalkan2-Jul-14 20:53 
GeneralRe: OnError, TimeOut events... Pin
Elaheh Ordoni2-Jul-14 20:57
Elaheh Ordoni2-Jul-14 20:57 
GeneralRe: OnError, TimeOut events... Pin
Elaheh Ordoni2-Jul-14 19:03
Elaheh Ordoni2-Jul-14 19:03 
QuestionVery sophisticated! Pin
Nick_st10-Jun-14 2:41
Nick_st10-Jun-14 2:41 
QuestionA good project! Pin
Member 1063220026-May-14 19:56
Member 1063220026-May-14 19:56 
QuestionGreat and easy to use !! but have some architecture questions... Pin
holyroody26-May-14 3:44
holyroody26-May-14 3:44 
QuestionConnection State Pin
Magus_Tsf2-Apr-14 1:18
Magus_Tsf2-Apr-14 1:18 

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.