You are in: Home > Articles > Asynchronous Socket Utility Classes

Asynchronous Socket Utility Classes

Article Links

Private Methods Section

We need to add two private methods.

The LocateSocketIndex method is used to locate an empty slot in the SocketClientList property array.This is used by the Accept thread to save a reference to a newly instantiated CSocketClient object.

In the LocateSocketIndex method, a local simple type variable called iSocket is set to the max number of client connections the CSocketServer object is allowed to accept.This is because the method returns the value of iSocket at the end of the method.If no slots are available, we need to return the max client connections value to indicate the maximum number of client connections currently exist.

Next, we wait to enter the Monitor object.We only want one thread accessing the SocketClientList property array at a time.In the call to Monitor.Enter, we pass the SocketList object property to protect the SocketClientList property array.   Now we safely search the SocketClientList property array for an empty slot.Once we find an empty slot, we break out of the loop.After the loop, we call Monitor.Exit passing the SocketList object property again and return the value of iSocket.

Notice how the calls to Monitor.Enter and Monitor.Exit are outside the try and catch statements.This is done to make sure that if we successfully enter the critical section we absolutely exit from it.If you do not call Monitor.Exit after passing through Monitor.Enter, no other threads will ever get through the critical section again.Those threads waiting to pass through the Monitor.Enter call will block forever.It is important to always make sure Monitor.Exit is called and only called once.I have found this to be the most efficient and safest way to guarantee critical sections get released properly.

//********************************************************************

/// <summary> Function to locate an empty slot in the client socket list array </summary>

/// <returns> Will return the index slot or user defined MaxClientConnections if none found </returns>

private Int32 LocateSocketIndex()

{

Int32 iSocket = GetMaxClientConnections;

   

Monitor.Enter(GetSocketListCS);

          

try

{

    // Find an empty slot in the list

    for (iSocket = 0; iSocket < GetMaxClientConnections; ++iSocket)

      if (GetSocketClientList[iSocket] == null)

        break;

}

          

catch

{

}

          

Monitor.Exit(GetSocketListCS);

return iSocket;

}

The AcceptThread method is spawned as a worker thread.It performs most of the work for the CSocketServer class.The job of the AcceptThread method is to listen for incoming client socket connection requests and accept those requests.Once a client socket connection request is accepted by the call to the TcpListener AcceptSocket method, the Accept thread instantiates a CSocketClient object.The new CSocketClient object exists to encapsulate the accepted socket connection.We then store a reference to the CSocketClient object in the SocketClientList property array.Once these activities are complete, then we call the Accept Handler.This allows the application developer to be notified a client socket connection request has been accepted.

In both of the catch statements, we call the Error Handler.This notifies the application developer to be notified about an error accepting a socket client connection.In the catch statement that catches the System.Net.Sockets.SocketException object, we check for error code 10004.This error code means that a socket blocking call was cancelled.We get this error when the TcpListener property is stopped.In this case, we do not call the Error Handler since this is a known error that is unavoidable.In both catch statements, we close the socket client connection if we accepted the connection before the error.

//********************************************************************

/// <summary> Function to process and accept socket connection requests </summary>

private void AcceptThread()

{

Socket pClientSocket = null;

          

try

{

    // Create a new TCPListner and start it up

    GetTcpListener = new TcpListener(Dns.Resolve(GetIpAddress).AddressList[0],GetPort);

    GetTcpListener.Start();

       

    for (;;)

    {

      // If a client connects, accept the connection

      pClientSocket = GetTcpListener.AcceptSocket();

      if (pClientSocket.Connected)

      {

        // Locate a socket index

        Int32 iSocketIndex = LocateSocketIndex();

           

        // If we got a valid index

        if (iSocketIndex != GetMaxClientConnections)

        {

          // Create a SocketClient object

          GetSocketClientList[iSocketIndex] = new CSocketClient(this,

            pClientSocket,                                           // The socket object for the connection

            iSocketIndex,                                            // The index into the SocketClientList

            pClientSocket.RemoteEndPoint.ToString().Substring(0,15), // The IpAddress of the client

            GetPort,                                                 // The port the client connected to

            GetSizeOfRawBuffer,                                      // The size of the byte array for storing messages

            GetUserArg,                                              // Application developer state

            new CSocketClient.MESSAGE_HANDLER(GetMessageHandler),    // Application developer Message Handler

            new CSocketClient.CLOSE_HANDLER(GetCloseHandler),        // Application developer Close Handler

            new CSocketClient.ERROR_HANDLER(GetErrorHandler));       // Application developer Error Handler

           

          // Call the Accept Handler

          GetAcceptHandler(GetSocketClientList[iSocketIndex]);

        }

        else

        {

          // Call the Error Handler

          GetErrorHandler(null, new Exception("Unable To Accept Socket Connection"));

           

          // Close the socket connection

          pClientSocket.Close();

        }

      }

    }

}

          

catch (System.Net.Sockets.SocketException e)

{

    // Did we stop the TCPListener

    if (e.ErrorCode != 10004)

    {

      // Call the error handler

      GetErrorHandler(null, e);

       

      // Close the socket down if it exists

      if (pClientSocket != null)

        if (pClientSocket.Connected)

          pClientSocket.Close();

    }

}

          

catch (Exception e)

{

    // Call the error handler

    GetErrorHandler(null, e);

            

    // Close the socket down if it exists

    if (pClientSocket != null)

      if (pClientSocket.Connected)

        pClientSocket.Close();

}

}

Public Methods Section

We need to add one public method.

The RemoveSocket method is used to remove a reference of a CSocketClient object from the SocketClientList property array.A reference to a CSocketClient object is passed to the RemoveSocket method.We first need to check to make sure the reference to the passed CSocketClient object is valid.We do this by comparing the reference passed into RemoveSocket with the reference located in the SocketClientList property array.We determine which slot to look at in the SocketClientList array by using the slot the passed CSocketClient object says it's located in.If these references match, we de-reference the object from the SocketClientList property array.You may have noticed that the CSocketClient class currently does not have a public property called GetSocketListIndex.We will be adding the public property to the CSocketClient class later.Again the critical section is used to protect the SocketClientList property array.The calls to Monitor.Enter and Monitor.Exit are outside the try and catch statements.

//********************************************************************

/// <summary> Funciton to remove a socket from the list of sockets </summary>

/// <param name="iSocketListIndex"> SimType: The index of the socket to remove </param>

public void RemoveSocket(CSocketClient pSocketClient)

{

Monitor.Enter(GetSocketListCS);

          

try

{

    // Is the supplied CSocketClient object valid

    if (pSocketClient == GetSocketClientList[pSocketClient.GetSocketListIndex])

    {

      // Set the slot to null

      GetSocketClientList[pSocketClient.GetSocketListIndex] = null;

    }

}

          

catch

{

}

          

Monitor.Exit(GetSocketListCS);

}

//********************************************************************

/// <summary> Function to start the SocketServer </summary>

/// <param name="strIpAddress"> RefType: The IpAddress to listening on </param>

/// <param name="iPort"> SimType: The Port to listen on </param>

/// <param name="iMaxClientConnections"> SimType: Max number of client connections accepted </param>

/// <param name="iSizeOfRawBuffer"> SimType: Size of the Raw Buffer </param>

/// <param name="pUserArg"> RefType: User supplied arguments </param>

/// <param name="pfnMessageHandler"> DelType: Function pointer to the user MessageHandler function </param>

/// <param name="pfnAcceptHandler"> DelType: Function pointer to the user AcceptHandler function </param>

/// <param name="pfnCloseHandler"> DelType: Function pointer to the user CloseHandler function </param>

/// <param name="pfnErrorHandler"> DelType: Function pointer to the user ErrorHandler function </param>

public void Start(String strIpAddress, Int16 iPort, Int32 iMaxClientConnections, Int32 iSizeOfRawBuffer, Object pUserArg,

                  MESSAGE_HANDLER pfnMessageHandler, ACCEPT_HANDLER pfnAcceptHandler, CLOSE_HANDLER pfnCloseHandler,

                  ERROR_HANDLER pfnErrorHandler)

{

// Is an AcceptThread currently running

if (GetAcceptThread == null)

{

    // Set connection values

    GetIpAddress            = strIpAddress;

    GetPort                 = iPort;

    GetMaxClientConnections = iMaxClientConnections;

       

    // Init the array of CSocketClient references

    GetSocketClientList = new CSocketClient[iMaxClientConnections];

       

    // Save the Handler Functions

    GetMessageHandler = pfnMessageHandler;

    GetAcceptHandler= pfnAcceptHandler;

    GetCloseHandler   = pfnCloseHandler;

    GetErrorHandler   = pfnErrorHandler;

       

    // Save the buffer size and user arguments

    GetSizeOfRawBuffer = iSizeOfRawBuffer;

    GetUserArg         = pUserArg;

       

    // Start the listening thread if one is currently not running

    ThreadStart tsThread = new ThreadStart(AcceptThread);

    GetAcceptThread = new Thread(tsThread);

    GetAcceptThread.Name = "Accept";

    GetAcceptThread.Start();

}

}

   

//********************************************************************

/// <summary> Function to stop the SocketServer.It can be restarted with Start </summary>

public void Stop()

{

// Abort the accept thread

if (GetAcceptThread != null)

{

    GetTcpListener.Stop();

    GetAcceptThread.Join();

    GetAcceptThread = null;

}

             

// Dispose of all of the socket connections

for (int iSocket = 0; iSocket < GetMaxClientConnections; ++iSocket)

{

    if (GetSocketClientList[iSocket] != null)

    {

      GetSocketClientList[iSocket].Dispose();

      GetSocketClientList[iSocket] = null;

    }

}

// Wait for all of the socket client objects to be destroyed

GC.Collect();

GC.WaitForPendingFinalizers();

// Clear the Handler Functions

GetMessageHandler = null;

GetAcceptHandler = null;

GetCloseHandler = null;

GetErrorHandler   = null;

// Clear the buffer size and user arguments

GetSizeOfRawBuffer = 0;

GetUserArg         = null;

}

CSocketClient Updates

Now we will add some new properties and methods to the CSocketClient class to support the CSocketServer class.

Private Properties Section

We need to add one private property.

This property maintains a reference back to the owning CSocketServer object.We need this reference to call the CSocketServer RemoveSocket method from the CSocketClient Dispose method.

private CSocketServer m_pSocketServer;

///<summary> RefType: The SocketServer for this socket object </summary>

private CSocketServer GetSocketServer { get { return m_pSocketServer; } set { m_pSocketServer = value; } }

Public Properties Section

We need to add two public properties.

This property maintains the index where this object is located in the CSocketServer SocketClientList array.

private Int32 m_iSocketListIndex;

///<summary> SimType: Index into the Socket List Array </summary>

public Int32 GetSocketListIndex { get { return m_iSocketListIndex; } set { m_iSocketListIndex = value; } }

This property maintains a reference to the socket object created by the TcpListener property when the CSocketServer Accept thread accepts a client socket connection request.This property will be passed to the NetworkStream property instead of the TcpClient property when the CSocketServer class instantiates a CSocketClient object.

private Socket m_pClientSocket;

///<summary> RefType: The socket for the client connection </summary>

public Socket GetClientSocket { get { return m_pClientSocket; } set { m_pClientSocket = value; } }

Constructor, Finalize, and Dispose Section

We need to add a new constructor method to support the CSocketServer class.This overloaded constructor method takes 10 arguments.The XML documentation for the constructor describes the arguments.In the constructor, we are setting the class properties that should not change during the lifetime of an object instance of this class.

In this version of the constructor method, we need to do all of the things we did as in the first version plus a few new things.First, we need to save the reference to the CSocketServer object that created this CSocketClient object.Next, we save the reference to the socket for the accepted connection and the index where this object can be located in the CSocketServer SocketClientList array.Now we need to save the ipaddress and port of the client that connected to us.

In the CSocketClient Connect method we made the following call.

GetNetworkStream = GetTcpClient.GetStream();

This associated the new socket connection we established with the NetworkStream property so we could process socket messages asynchronously.The Connect method will never be called in this context because the connection is already established.In this case, we need to associate the socket being passed in with the NetworkStream property.

GetNetworkStream = new NetworkStream(GetClientSocket);

This statement above is used here in the constructor to associate the socket being passed in with the NetworkStream property.

The last thing to add is the socket options.Since the Connect method will never be called, we need to set the socket options here.We set all the same options we did in connect.

//********************************************************************

/// <summary> Constructor for SocketServer Suppport </summary>

/// <param name="pSocketServer"> RefType: A Reference to the parent SocketServer </param>

/// <param name="pClientSocket"> RetType: The Socket object we are encapsulating </param>

/// <param name="iSocketListArray"> SimType: The index of the SocketServer Socket List Array </param>

/// <param name="strIpAddress"> RetType: The IpAddress of the remote server </param>

/// <param name="iPort"> SimType: The Port of the remote server </param>

/// <param name="pfnMessageHandler"> DelType: Reference to the user defined message handler function </param>

/// <param name="pfnCloseHandler"> DelType: Reference to the user defined close handler function </param>

/// <param name="pfnErrorHandler"> DelType: Reference to the user defined error handler function </param>

/// <param name="iSizeOfRawBuffer"> SimType: The size of the raw buffer </param>

/// <param name="pUserArg"> RefType: A Reference to the Users arguments </param>

public CSocketClient(CSocketServer pSocketServer, Socket pClientSocket, Int32 iSocketListArray, String strIpAddress, Int16 iPort,

                     Int32 iSizeOfRawBuffer, Object pUserArg, MESSAGE_HANDLER pfnMessageHandler, CLOSE_HANDLER pfnCloseHandler,

                     ERROR_HANDLER pfnErrorHandler)

{

// Create the raw buffer

GetSizeOfRawBuffer = iSizeOfRawBuffer;

GetRawBuffer = new Byte[GetSizeOfRawBuffer];

// Save the user argument

GetUserArg = pUserArg;

// Set the handler functions

GetMessageHandler = pfnMessageHandler;

GetCloseHandler   = pfnCloseHandler;

GetErrorHandler   = pfnErrorHandler;

// Set the async socket function handlers

GetCallbackReadFunction = new AsyncCallback(ReceiveComplete);

GetCallbackWriteFunction = new AsyncCallback(SendComplete);

// Init the dispose flag

IsDisposed = false;

// Set reference to SocketServer

GetSocketServer = pSocketServer;

// Init the socket references

GetClientSocket    = pClientSocket;

GetSocketListIndex = iSocketListArray;

// Set the Ipaddress and Port

GetIpAddress = strIpAddress;

GetPort      = iPort;

             

// Init the NetworkStream reference

GetNetworkStream = new NetworkStream(GetClientSocket);

    

// Set these socket options

GetClientSocket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.ReceiveBuffer, 1048576);

GetClientSocket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.SendBuffer, 1048576);

GetClientSocket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.DontLinger, 1);

GetClientSocket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Tcp,    System.Net.Sockets.SocketOptionName.NoDelay, 1);

            

// Wait for a message

Receive();

}

We need to make one change to the Dispose method.We need to add the call to the CSocketServer RemoveSocket method.

// Remove the socket from the list

if (GetSocketServer != null)

GetSocketServer.RemoveSocket(this);

This is added after the catch statement to guarantee the call is made.This will remove the reference to this object from the CSocketServer SocketClientList array.

//********************************************************************

/// <summary> Dispose </summary>

public void Dispose()

{

try

{

    // Flag that dispose has been called

    IsDisposed = true;

       

    // Disconnect the client from the server

    Disconnect();

}

catch

{

}

// Remove the socket from the list

if (GetSocketServer != null)

    GetSocketServer.RemoveSocket(this);

}

The last change is to the Disconnect method.Here we need to add the following code.

if (GetClientSocket != null)

GetClientSocket.Close();

GetClientSocket = null;

This code will close the socket and remove the reference this class has to the memory so garbage collection can clean up.

//********************************************************************

/// <summary> Function used to disconnect from the server </summary>

public void Disconnect()

{

// Close down the connection

if (GetNetworkStream != null)

    GetNetworkStream.Close();

if (GetTcpClient != null)

    GetTcpClient.Close();

if (GetClientSocket != null)

    GetClientSocket.Close();

// Clean up the connection state

GetClientSocket= null;

GetNetworkStream = null;

GetTcpClient     = null;

}

Sample Application Update

Now it is time to test our CSocketServer class.Open the class1.cs file and go to the bottom.Add a new method called MessageHandlerServer.This method is called when we receive a message from a socket client connection.In this method we will send back to the client a HTTP HTML command.

//********************************************************************

/// <summary> Called when a message is extracted from the socket </summary>

/// <param name="pSocket"> The SocketClient object the message came from </param>

/// <param name="iNumberOfBytes"> The number of bytes in the RawBuffer inside the SocketClient </param>

static public void MessageHandlerServer(CSocketClient pSocket, Int32 iNumberOfBytes)

{

try

{

    // Find a complete message

    String strMessage = System.Text.ASCIIEncoding.ASCII.GetString(pSocket.GetRawBuffer, 0, iNumberOfBytes);

       

    Console.WriteLine(strMessage);

    // Send the following HTTP command back

    String strServerResponse = "HTTP/1.1 200 OK\n" +

      "Date: Tue, 18 Feb 2003 18:47:39 GMT\n" +

      "Server: Apache/1.3.27 (Unix)(Red-Hat/Linux) mod_perl/1.24_01 PHP/4.2.2 FrontPage/5.0.2 mod_ssl/2.8.12 OpenSSL/0.9.6b\n" +

      "Last-Modified: Sat, 25 Jan 2003 21:45:30 GMT\n" +

      "ETag: \"2201ab-11cd-3e33057a\"\n" +

      "Accept-Ranges: bytes\n" +

      "Content-Length: 53\n" +

      "Keep-Alive: timeout=15, max=100\n" +

      "Connection: Keep-Alive\n" +

      "Content-Type: text/html\n\n" +

      "<html> <body> <h1> Hello World! </h1> </body> </html>\n";

    pSocket.Send(strServerResponse);       

}

  

catch (Exception pException)

{

    Console.WriteLine(pException.Message);

}

}

Add the AcceptHandler next.This is allow us to be notified when a connection is established to our server.

//********************************************************************

/// <summary> Called when a socket connection is accepted </summary>

/// <param name="pSocket"> The SocketClient object the message came from </param>

static public void AcceptHandler(CSocketClient pSocket)

{

Console.WriteLine("Accept Handler");

Console.WriteLine("IpAddress: " + pSocket.GetIpAddress);

}

The TestServer method will be used to start the server.We will listen for connections on port 9000 using the network card configured under our machine name.

   

//********************************************************************

///<summary> Function to test the CSocketServer class </summary>

static void TestServer()

{

try

{

    // Instantiate a CSocketServer object

    CSocketServer pSocketServer = new CSocketServer();

       

    // Start listening for connections

    pSocketServer.Start(System.Environment.MachineName, 9000, 1024, 10240, null,

      new CSocketServer.MESSAGE_HANDLER(MessageHandlerServer),

      new CSocketServer.ACCEPT_HANDLER(AcceptHandler),

      new CSocketServer.CLOSE_HANDLER(CloseHandler),

      new CSocketServer.ERROR_HANDLER(ErrorHandler));

    Console.WriteLine("Waiting for a client connection on Machine: {0} Port: {1}", System.Environment.MachineName, 9000);

    // Stay here until you are ready to shutdown the server   

    Console.ReadLine();

    pSocketServer.Dispose();

}

catch (Exception pException)

{

    Console.WriteLine(pException.Message);

}

}

In the Main method, comment out the call to TestClient and add the call to TestServer.Now we are ready to start our server and test it.

//*******************************************************************

/// <summary> Function to test the CSocketClient class </summary>

static void Main(string[] args)

{

// Test the CSocketClient class

// TestClient();

// Test the CSocketClient class

TestServer();

}

Start the server and launch your browser.Our server is listening for connections on our local machine on port 9000.You need the machine name or ipaddress of our local machine.In the address bar type the following: http://machinename:9000 .This will have the browser connect to our server on port 9000 and our server will send the browser back the HTML statement that will display Hello World on the browser.

C# Asynchronous Socket Utility Classes

Review

So now we have the ability to add socket client and server support to our applications easily and quickly.In the client sample application, all we needed to do was to instantiate a CSocketClient object.Then we can decide which server we want to connect to and code the handler functions appropriately. In the server sample application, all we needed to do was to instantiate a CSocketServer object.Then we can decide which ipaddress and port to listen for socket client requests and code the handler functions appropriately.


by William Kennedy

About the Author

William Kennedy is a graduate of SUNY Potsdam in Potsdam NY and a member of Sigma Pi fraternity. William works for Concerto Software, which provides contact center solutions that help companies more effectively manage customer interactions across all channels. At Concerto Software he is responsible for the architect, design, and implementation of real-time server based software written in both C++ and C#. William is also the co-owner of Continuum Technology Center. The Continuum provides articles and source code to help developers develop better products in C#. When William is not working, you can find him playing with his five kids.