Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

LanTalk

4.93/5 (24 votes)
6 Jun 2008CPOL14 min read 1   7.7K  
Yet another chat application, with server running in service mode or GUI mode.

LanTalkChat/LanTalk_screenshoot.jpg

Contents

  1. Introduction
  2. The demo application
  3. An introduction into sockets, connection-oriented protocols (TCP)
  4. LanTalk.Core – core functionality and shared objects
  5. The server application
  6. The client application
  7. Logging - NLog
  8. Conclusions

1. Introduction

Sockets, sockets, sockets! Or was it developers, developers, developers? Sockets are used by nearly every program out there, so they deserve their own place on the stand. The birth of this article was caused by the need of a simple chat application that can be run in service mode (like, run once forget forever mode). There are a few good articles here on how to implement a service, using sockets, so I did my research. The resulted application will be dissected in the following pages.

So, grab yourself a coffee, it's going to be a long and, hopefully, interesting read.

2. The Demo Application

This is a typical client-server application. Provided is a GUI server application, a server implemented as a service and a client application.

After the server starts, it accepts TCP connections on port 65534 (or by a port set by the user). Users connected to the server must authenticate, before any message exchange takes place. The possibility of public and private messaging is implemented, and a basic logging mechanism.

One may use the GUI server application or the service to accept incoming requests and to serve the clients. However, you should know that in this version, only the GUI server app can be used to create users.

2.1. Setting up the Demo Application

  1. Edit the Settings.xml file in the LanTalk.Server\Data directory.
  2. Run the LanTalk.ServerGui application and create some users.
  3. Install the service by running “LanTalk.Service install”.
  4. Start the server using LanTalk.ServerGui, or start the service by running “net start LanTalk”.
  5. Fire up the LanTalk.Client application.
  6. Set the connection settings in the Application -> Application Settings menu.
  7. Log in to the server, and start talking.

3. An Introduction into Sockets, Connection-oriented Protocols (TCP)

An introduction into IP addresses and the TCP/IP model can be found here[^]. I will not delve into this.

The communication between the server and the client is implemented using the TCP protocol. TCP is a connection-oriented protocol, meaning that it provides reliable, in-order delivery of a stream of bytes. Using TCP applications, we can exchange data using stream sockets. A socket is a communication endpoint. For an endpoint to be uniquely identified, it needs to contain the following data: the protocol used to communicate, an IP address, and a port number.

In the .NET framework, a socket is represented by the Socket class in the System.Net.Sockets namespace. The Socket class defines the following members to uniquely describe an endpoint:

For the Protocol in Use

C#
public AddressFamily AddressFamily 
{ get; }
Specifies the addressing scheme that an instance of the Socket class can use. The two most common AddressFamily values used are InterNetwork for IPV4 addresses, and InterNetworkV6 for IPV6 addresses.
C#
public SocketType SocketType
{ get; }
Gets the type of the socket. The SocketType indicates the type of communication that will take place over the socket, the two most common types being Stream (for connection-oriented sockets) and Dgram (for connectionless sockets).
C#
public ProtocolType ProtocolType
{ get; }
Specifies the protocol type of the Socket.

For the local/remote Endpoint

C#
public EndPoint LocalEndPoint/RemoteEndPoint
{ get; }
Specifies the local/remote endpoint.

3.1. The EndPoint / IPEndPoint Class

It’s an abstract class that identifies a network address. The IPEndPoint class is derived from the EndPoint class, and represents a network endpoint (a combination of an IP address and a port number).

C#
public IPAddress Address 
{ get; set; }
Gets or sets the IP address of the endpoint.
C#
public int Port
{ get; set; }
Gets or sets the port number of the endpoint.

3.2. Steps to Create Connection-oriented Sockets on the Server Side

Before the server can accept any incoming connection, you must perform four steps on a server socket:

C#
socket = new Socket(AddressFamily.InterNetwork, 
         SocketType.Stream, ProtocolType.Tcp);
C#
endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 65534);
socket.Bind(endPoint);
C#
socket.Listen(32);    // a 32 maximum pending connections
C#
socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
  1. Create the socket. When a socket is created, it is given an address family and a protocol type, but it has no assigned address and port.
  2. Bind that socket to a local address and port. Bind assigns a socket an address and a port.
  3. Prepare the bound socket to accept incoming connections. The Listen method sets up internal queues for a socket. Whenever a client attempts to connect, the connection request is placed in the queue. Pending connections from the queue are pulled with the Accept method that returns a new Socket instance.
  4. Accept incoming connections on the socket.

3.3. Steps to Create Connection-oriented Sockets on the Client Side

To create a socket on the client side, you need to create a socket and wire up that socket to the remote address and port. That simple! To connect to the remote server, you have to use the Socket.Connect method:

C#
socket.Connect(serverEndPoint);

3.4. Blocking on Sockets

The Socket class methods use blocking, by default, this is why when execution reaches a network call that blocks, the application waits until the operation completes (or an exception is thrown). To avoid blocking on sockets, we may choose to use non-blocking sockets, multiplexed sockets, or asynchronous sockets. There is a fourth way to use a socket, by handling connections with IOCP (IO completion ports).

3.4.1. Non-blocking Sockets

Setting the Socket.Blocking property to false can set your socket to function as non-blocking. When a socket is in non-blocking mode, it will not wait for a requested operation to complete; rather, it will check the operation, and if it can’t be completed, the method will fail and program execution will continue.

Disadvantages of non-blocking sockets: Although this allows the program to do other things while the network operations finish, it requires that the program repeatedly poll to find out when each request has finished.

3.4.2. Multiplexed Sockets

This technique is used to watch multiple Socket instances for the ones that are ready to be read or written to. For this, the Socket.Select(…) static method is used. The format of the Select method is:

C#
public static void Select (
    IList checkRead,
    IList checkWrite,
    IList checkError,
    int microSeconds
);

Select returns when at least one of the sockets of interest (the sockets in the checkRead, checkWrite, and checkError lists) meets its specified criteria, or the microSeconds parameter is exceeded, whichever comes first. Setting microSeconds to -1 specifies an infinite time-out. This is a combination of non-blocking sockets. One way to use Select is to create a timer that fires frequently, and within the timer's event handler, call Select with a timeout value of zero so that the call is non-blocking.

For a detailed example, look at MSDN.

Disadvantages of multiplexed sockets: This is an inefficient method, because the time needed to serve the sockets depends on how frequently the timer is fired (the microSeconds parameter). Also, the servicing of the sockets prevents the code from reentering the Select method, which may cause starvation of threads.

3.4.3. Asynchronous Sockets

Asynchronous sockets use callback methods to handle incoming data and connections. By convention, an asynchronous socket method begins with the word “Begin”, like BeginReceive, BeginAccept, etc. This method allows you to use a separate method when a socket operation is available (the delegate will be called). The asynchronous calls use the AsyncCallback class to call completion methods when a socket operation is finished.

The Socket class has the following asynchronous methods:

C#
public IAsyncResult BeginAccept( … )
public IAsyncResult BeginConnect( … )
public IAsyncResult BeginReceive( … )
public IAsyncResult BeginSend( … )

Each of the Begin methods starts the network function, and registers the AsyncCallback method. Each of the Begin methods has to have an End method that completes the function when the AsyncCallback method is called.

The ways of multi-threading in .NET are: starting your own threads with ThreadStart delegates, and using the ThreadPool class either directly (using ThreadPool.QueueUserWorkItem), or indirectly by using asynchronous methods (such as Stream.BeginRead, or calling BeginInvoke on any delegate). Asynchronous I/O uses the application's thread pool, so when there is data to be processed, a thread from the thread pool performs the callback function. This way, a thread is used for a single I/O operation, and after that, it is returned to the pool.

3.4.5. IOCP Requests

IOCP represents a solution to the one-thread-per-socket problem. Completion ports are used in high performance servers, with a high number of concurrent connections. In this article, we are focusing on asynchronous sockets.

3.5. Closing Down the Socket

For closing the socket, the client application is advised to use the Shutdown method. This allows all the unsent data to be sent and the un-received data to be received before the connection is closed:

C#
if (socket.Connected) socket.Shutdown(SocketShutdown.Both);
socket.Close();

The SocketShutdown enumeration defines what operation on the socket will be denied (Send, Receive, or Both).

3.6. Exception Handling and Important Error Codes

If a socket method encounters an error, a SocketException is thrown. The ErrorCode member contains the error code associated with the exception thrown. This is the same error code that is received by calling the WSAGetLastError of the WinSock library.

Error Number Winsock Value SocketError Value Meaning
10004 WSAEINTR Interrupted The system call was interrupted. This can happen when a socket call is in progress and the socket is closed.
10048 WSAEADDRINUSE AddressAlreadyInUse The address you are attempting to bind to or listen on is already in use.
10053 WSACONNABORTED ConnectionAborted The connection was aborted locally.
10054 WSAECONNRESET ConnectionReset The connection was reset by the other end.
10061 WSAECONNREFUSED ConnectionRefused The connection was refused by the remote host. This can happen if the remote host is not running, or if it is unable to accept a connection because it is busy and the listen queue is full.

3.7. A Few Words about Building Scalable Server Applications

When thinking about building scalable server applications, we have to think about thread pools. Thread pools allow a server application to queue and perform work in an efficient way. Without thread pools, the programmer has two options:

or

  1. Use a single thread and perform all the work using that thread. This means linear execution (one has to wait for an operation to terminate before proceeding on).
  2. Spawn a new thread every time a new job needs processing. Having multiple threads performing work at the same time does not necessarily mean that the application is becoming faster or is getting more work done. If multiple threads try to access the same resource, you may have a block scenario where threads are waiting for a resource to become available.

In the .NET framework, the “System.Threading” namespace has a ThreadPool class. Unfortunately, it is a static class, and therefore our server can only have a single thread pool. This isn't the only issue. The ThreadPool class does not allow us to set the concurrency level of the thread pool.

4. LanTalk.Core - Core Functionality and Shared Objects

The LanTalk.Core assembly contains the common classes used by both the server and the client application.

4.1 The ChatSocket Class

The ChatSocket class is a wrapper around a Socket, extending with other data:

LanTalkChat/ChatSocket_class.jpg

The ChatSocket class has three public methods:

LanTalkChat/chatsocket.send.jpg

Raises the Sent event if the sending operation was successful. On exception, closes the socket and raises the Disconnected event.

  • Send - Using the underlying socket, writes out the encapsulated data to the network. This is done in the following way: the first four bytes are sent - a ChatCommand enumeration type that notifies the receiver of the command the sender wants to carry out. Then, the command target IP is sent. The last step is to send the metadata (the actual data - more on this later).
  • Receive - Reads the data using the wrapped socket asynchronously. Basically, the same steps are performed as in the Send. If an unknown command was sent, the user of the ChatSocket object disconnects the socket. Raises the Received event, only if a known command was received. On exception, closes the socket and raises the Disconnected event.
  • Close - Flushes the data and closes the socket. Doesn't raise any event.

4.2. A Word about metadata and the Use of Reflection

The metatype data is the string representation of the object used in the communication between the client and the server, something like: “LanTalk.Core.ResponsePacket, LanTalk.Core, Version=1.8.3.2, Culture=neutral, PublicKeyToken=null”, where the meta buffer contains the actual object.

Each metatype received should implement the IBasePacket interface:

C#
interface IBasePacket
{
    /// <summary>
    /// Initialize the class members using the byte array passed in as parameter.
    /// </summary>
    void Initialize(byte[] metadata);
    
    /// <summary>
    /// Return metadata (a byte array) constructed from the members of the class.
    /// </summary>
    byte[] GetBytes();
}   // IBasePacket

The BasePacket class defines the Initialize method to initialize the data members of an IBasePacket derived object using the Type.GetField method. This is used when a byte array is received from the network (the meta buffer), and in the knowledge of the metadata type, we have to initialize the created object.

C#
public void Initialize(byte[] data)
{
    Type type = this.GetType();
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | 
                                        BindingFlags.Instance);

    int ptr = 0;
    int size = 0;

    try
    {
        for (int i = 0; i < fields.Length; i++)
        {
            FieldInfo fi = fields[i];

            // get size of the next member from the byte array
            size = BitConverter.ToInt32(data, ptr);
            ptr += 4;

            if (fi.FieldType == typeof(System.String))
            {
                fi.SetValue(this, Encoding.Unicode.GetString(data, ptr, size));
                ptr += size;
            }
        }
    }
    catch (Exception exc)
    {
        Trace.Write(exc.Message + " - " + exc.StackTrace);
    }
}

The ChatSocket class uses the Initialize method in the following way:

C#
Type typeToCreate = Type.GetType(metatype);
IBasePacket packet = (IBasePacket)Activator.CreateInstance(typeToCreate);
packet.Initialize(metaBuffer);

The GetBytes method's role is to return a byte array representing the internal state of the calling object. It is used to create the byte array that the socket sends out to the network.

C#
public byte[] GetBytes()
{
    byte[] data = new byte[4];
    int ptr = 0;
    int size = 0;

    Type type = this.GetType();
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | 
                                        BindingFlags.Instance);

    try
    {
        for (int i = 0; i < fields.Length; i++)
        {
            FieldInfo fi = fields[i];

            if (fi.FieldType == typeof(System.String))
            {
                // get the string value of the field
                string strField = (string)fi.GetValue(this);

                // get size of from string, copy size into array
                size = Encoding.Unicode.GetByteCount(strField);
                BasePacket.ResizeDataBuffer(ref data, ptr, size);
                Buffer.BlockCopy(BitConverter.GetBytes(size), 0, data, ptr, 4);
                ptr += 4;   // GetBytes returns the size as an array of 4 bytes

                // copy string value into array
                BasePacket.ResizeDataBuffer(ref data, ptr, size);
                Buffer.BlockCopy(Encoding.Unicode.GetBytes(strField), 0, data, ptr, size);
                ptr += size;
            }
        }
    }
    catch (Exception exc)
    {
        Trace.Write(exc.Message + " - " + exc.StackTrace);
    }

    byte[] retData = new byte[ptr];
    Array.Copy(data, retData, ptr);

    return retData;
}

This method is used when sending data using the ChatSocket class, as follows:

C#
byte[] metaBuffer = null;
Type metaType = metadata.GetType();
MethodInfo mi = metaType.GetMethod("GetBytes");
metaBuffer = (byte[])mi.Invoke(metadata, null);
// get the byte representation of the metadata

The classes derived from the IBasePacket interface are used to encapsulate information into a known format and then communicate the object to the other party. The benefits of using Reflection is that defining new packet types becomes trivial.

5. The Server Application

The server is implemented as a threaded server, meaning that each new connection spawns a new thread. This implementation does not scale well with a large number of clients, but for my purposes, I only need a maximum of 32 connections. The main drawback is that each running thread has its own stack, which by default is one megabyte in size. This also means that on 32 bit machines with the default of one megabyte stack size, you can create only roughly 2000 threads before you run out of the 2GB user address space per process. This can be avoided by using IO completion ports.

The LanTalk.Server.dll assembly contains the server implementation, and is used by the GUI and the service application. When starting the server, the following operations are performed:

C#
socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);

private void HandleIncommingClient(IAsyncResult aResult)
{
    ....
    Socket clientSocket = socket.EndAccept(aResult);
    // process the connected socket
    ....
    // continue accepting incomming connections
    socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
    ...
}
  1. The settings are read from the Data\Settings.xml file
  2. A new socket is created using the IP address and port specified in the settings file (in case of error, the socket is created on 127.0.0.1:65534)
  3. Associate the socket with the created endpoint (the [IP, port] combination) - this is called binding the socket
  4. Place the socket in listening state
  5. If all goes well, the server fires the Started event and begins accepting the connections asynchronously.

The ClientManager Class

The ChatServer object maintains the list of connected clients. For this, when a connection is made, it creates a ClientManager object. The ClientManager object holds the connected socket and some user related information. Its responsibility is to send and receive data, and when that happens, to signals it with events. Also, the ClientManager updates user related information such as last connection time and last IP address of the connection.

6. The Client Application

The Client class is implemented as a singleton, only one instance of the client can be created. The Client class has three methods exposed to the outside (Connect, Disconnect, SendCommand); the rest of the functionality exposed is implemented through the use of events.

LanTalkChat/Client_class.jpg

This is a simple class with four core functionalities:

  1. Connect to the server - the Connect method - fires the Connected event on successful connection, or the ConnectFailed event in case of error.
  2. Send messages to the server - the SendCommand method.
  3. Receive messages from the server - responds to the ChatSocket's Received event - fires the CommandReceived event.
  4. Disconnect from the server - the Disconnect method - closes the underlying socket and fires the Disconnected event.

The Client object uses the TCP keep-alive feature to prevent disconnecting the clients from the server in case of inactivity:

C#
private void OnConnect(IAsyncResult ar)
{
    // send keep alive after 10 minute of inactivity
    SockUtils.SetKeepAlive(socket, 600 * 1000, 60 * 1000);
}

The Settings.xml file is used for persisting application settings, and is saved in the currently logged on user's profile folder. The exact location is “%Documents and settings%\%user%\Application Data\Netis\LanTalk Client”.

Archiving messages is also provided on a per Windows user/per LanTalk user basis. For example, for me:

  • The directory “C:\Documents and Settings\zbalazs\Application Data\Netis\LanTalk Client\” will hold the Settings.xml file, and I will have a directory created with the username which I use to authenticate in the LanTalk application (“zoli”).
  • The “C:\Documents and Settings\zbalazs\Application Data\Netis\LanTalk Client\zoli\” directory will hold the message archive for me.

7. Logging - NLog

For logging, the application uses the NLog library. NLog provides a simple logging interface, and is configured by the application.exe.nlog file. If this file is missing, logging will not occur.

8. Conclusions

I recommend downloading the samples provided and playing with it a few times. After that, check out the sources. I hope that the article follows a logical line describing briefly the highlights of the projects.

References

  1. Transmission control protocol[^]
  2. Which I/O strategy should I use?[^]
  3. Get Closer to the Wire with High-Performance Sockets in .NET[^]
  4. TCP/IP Chat Application Using C#[^]
  5. TCP Keep-Alive overview [^]
  6. How expensive is TCP's Keep-Alive feature?[^]
  7. TCP/IP and NBT configuration parameters for Windows XP [^]
  8. DockManager control [^]
  9. TaskbarNotifier[^]
  10. Self installing .NET service using the Win32 API[^]
  11. NLog[^]

License

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