Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple .NET TCP Socket Component

0.00/5 (No votes)
14 Oct 2005 1  
Reusable C# code for client-server programming in .NET

Introduction

A socket is an end point in network communication, it is uniquely identified by an ip address and a port number. TCP/IP is the most common protocol used for inter-process communication. In many cases it is also the most efficient way to send data between client and server programs. The parties communicating via TCP socket can live on different machines on the network, but they can also live on the same machine or even within the same process. As we know, communications to most web servers and most relational databases are implemented using TCP socket.

However, socket programming is never easy. Without the help of the .NET framework, we typically have to work with some low level C/C++ functions or use some third party development tools. With .NET, we can use the framework class Socket which shields us from some low level details in socket programming. If all we care about is TCP socket, then we can use the TcpListner class and the TcpClient class. However, in order to write a client-server system that is efficient, scalable, and easy to maintain, we still have a lot to learn and a lot to do.

Let's first examine some of the issues. Suppose we are developing a socket server program that allows multiple clients to connect concurrently. First of all, our server has to be multithreaded and thread-safe, otherwise one bad connection could make other connections stop working. Secondly, we have to consider efficiency and scalability. If our server has to handle, say, more than one hundred concurrent clients, then it won't be efficient to use a separate thread for each client connection, because most of the time the connections will be idle. Thirdly, we need to make sure that some crashed or deadlocked clients do not prevent the server from working with other active clients. Furthermore, we may need to have some built-in capability to restrict the total number of clients and to reject untrusted clients.

The Socket class in the .NET framework allows you to perform asynchronous socket operations. Basically, you call a member function (such as BeginReceive and BeginSend) to start the socket operation asynchronously. The member function will not block, you can either wait for the socket operation to complete or specify a delegate (function pointer) of your callback function so that it will be invoked automatically by a thread from the .NET thread pool when the socket operation is complete. However, knowing the Socket class alone is not enough to write a good socket server program. The Socket class is not thread-safe at the instance level. It is not easy to figure out when/where/how to make these asynchronous calls, etc. The .NET built-in thread pool is static (i.e. there is only one instance) which makes it a shared global resource, this may cause some problems under certain stress conditions.

What we need is an easy to use tool that performs reasonably well and also hides most of the complexities of socket programming. Many people have written articles and code to achieve this goal, I am just one of them.

In this article, I am going to introduce a simple .NET TCP socket component which contains a server class and a client class. My socket component is still based on the .NET Socket class, however, I am using my own thread pool class , there is no call to the asynchronous members of the Socket class in my code.

To make my socket server more useful, I have implemented a java client and a com client. You can use the java client to talk to the socket server from other platforms. The com client makes it possible to talk to the socket server from old VB and C/C++ programs.

The Server class

XYNetServer is the name of our socket server class. Instances of this class are thread-safe which means you can use the same instance from multiple threads with no problem. Here is how it works.

To create an instance of the server class, the user needs to specify the ip address and the port number (to bind the server), as well as the minimal and the maximal numbers of worker threads used by the server. When the StartServer method is called, the server instance will start listening for client connection requests on the specified ip and port. The user can call the SetConnectionFilter method to specify a function to handle the connection requests, all connection requests will be accepted if no connection filter is set.

The server will receive two types of messages from the clients, binary messages and string messages. Call the methods SetBinaryInputHandler and SetStringInputHandler to specify functions to handle incoming messages. To send an outgoing message, call the SendBinaryData method or the SendStringData method.

Call the SetExceptionHandler method to specify a function to handle server errors, otherwise all errors will be ignored.

Here are the signatures of the constructor, the StartServer method, and the StopServer method.

public XYNetServer(string sAddress, int nPort, int nMinThreadCount,
  int nMaxThreadCount);
public bool StartServer();
public void StopServer();

The sAddress parameter allows you to specify which ip address the server will use in case there are multiple ip addresses assigned to the machine. If it is the empty string, the server will be using the default ip address of the machine. The sAddress and nPort pair uniquely identifies the internal server socket. After the server is started, the number of worker threads used by the server will vary between nMinThreadCount and nMaxThreadCount, the exact number depends on the load of the server at the time. If the server is not busy, some worker threads will be terminated but at least nMinThreadCount of them will remain active.

The StartServer method tells the server to begin accepting clients and processing incoming data, etc., the StopServer method will shutdown the server completely (including all threads in the internal thread pool). When StartServer returns false, the server is not started properly, in which case you can call GetLastException to retrieve an exception object.

When our socket server is first started, it will use two dedicated threads, one to accept client connections, the other to scan for incoming data. If data from a client arrives or some error occurs, the server will use threads from the internal thread pool to process the data or to handle the error. Please note that the parameters nMinThreadCount and nMaxThreadCount specified in the constructor do not include these two dedicated threads.

User Specified Delegates (Event Handlers)

Our socket server makes use of delegates specified by the user to handle connection request, notify the arrival of incoming data, and process the incoming data. It also supports custom error handling. Here are the four kinds of delegates to be used with our socket server.

public delegate void ExceptionHandlerDelegate(Exception oBug); 

This is the user defined error handler, it is set by the SetExceptionHandler method. The oBug parameter is an exception object representing the server error. Within the implementation of the error handler you can, for example, call the GetThreadCount method to retrieve the number of active threads in the internal thread pool. If the number is equal to the maximum allowed, then you may conclude that the server is deadlocked (or stuck) on some resource, in which case you may choose to restart the server application.

To protect the server, a client connection will be closed automatically when a socket error occurs with the connection. Please note that the error handler may be invoked concurrently by different threads from the internal thread pool, therefore the code has to be thread-safe.

public delegate void ConnectionFilterDelegate(string sRemoteAddress,
  int nRemotePort, Socket sock); 

This handler is invoked by a dedicated thread to process connection requests, it is set by the SetConnectionFilter method. The sRemoteAddress and nRemotePort pair uniquely identifies a client connection. To reject a connection request, you simply call the Shutdown method and then the Close method on the sock parameter, otherwise connection to the client will be established.

There are several things you can do within this handler. You can write the ip address sRemoteAddress to a log file so that you have a record of who is trying to connect to your server. You can also call the GetClientCount method to retrieve the total number of connected clients, if the total reaches 100 (or another preferred limit), then reject the connection request.

Another example is that you can limit connections based on ip address by rejecting all requests except those from a list of trusted ip addresses. If you store all the client ip addresses in a shared storage, you can implement code to restrict the number of connections from each individual client machine.

Since this handler is invoked by a dedicated thread, you need to make sure that the code won't block so that new connection requests can be handled promptly. On the other hand, you can call Thread.Sleep within this handler so that the server does not accept too many clients too quickly (to counter some denial of service attacks).

It is also possible to implement some kind of client authentication from this handler using the sock parameter. All you have to do is read user name and password from the sock object and do some checking before deciding to accept this client or not. Of course, the client has to send its user name and password (more details later). But you need to be careful not to block the processing of other connection requests.

public delegate void BinaryInputHandlerDelegate(string sRemoteAddress,
  int nRemotePort, byte[] pData); 

You need to call the SetBinaryInputHandler method to set this handler. When a binary message from a client is received, this handler will be invoked automatically by a thread from the internal thread pool. Since it can be executed concurrently by multiple threads, the implementation of this handler has to be thread-safe. The sRemoteAddress and nRemotePort pair identifies the client that sent the current message, the message data is in the byte array pData.

If you want to send a reply message to the client, you can call the SendBinaryData method or the SendStringData method within this handler passing sRemoteAddress, nRemotePort, and the data you want to send as parameters.

public delegate void StringInputHandlerDelegate(string sRemoteAddress,
  int nRemotePort, string sData); 

You need to call the SetStringInputHandler method to set this handler. When a string message from a client is received, this handler will be invoked automatically by a thread from the internal thread pool. Since it can be executed concurrently by multiple threads, the implementation of this handler has to be thread-safe. The sRemoteAddress and nRemotePort pair identifies the client that sent the current message, the message data is in the string parameter sData.

If you want to send a reply message to the client, you can call the SendBinaryData method or the SendStringData method within this handler passing sRemoteAddress, nRemotePort, and the data you want to send as parameters.

By the way, these four event handlers can only be set once for each instance of the socket server.

Messages, Maximal Data Size, And Timeout

The following methods are used to send messages to clients.

public bool SendBinaryData(string sRemoteAddress, int nRemotePort,
  byte[] pData); 
public bool SendStringData(string sRemoteAddress, int nRemotePort,
  string sData);

Again, the nRemoteAddress and nRemotePort pair uniquely identifies the client. These methods can fail (if the return value is false). Typically, they fail because the client is not connected, or the socket connection is broken, or the socket is currently in use (for processing incoming data). The same functions exist in our client class except that the client versions take only one parameter (the data).

The data sent to a client can be a byte array or a string. The SetMaxDataSize method can be called to restrict the sizes of incoming messages. If a client is sending a message that is too large, the connection will be closed to protect the server. The default maximal data size is 4 megabytes for both server and client. Please note that the data size of a string message is twice the length of the (unicode) string.

Timeout can occur while receiving a message. The socket connection to the corresponding party will be closed when that happens. Call the SetReadTimout method to specify the timeout value. The default timeout value is 30 seconds for both server and client.

The Client Class

Our socket client class is XYNetClient. It is a lot simpler than the socket server described above. The client is also thread-safe at the instance level. To create an instance of the client we need to specify the server address and the server port number in the constructor. The server address can be either the name of the server machine (if it can be resolved successfully) or the ip address used by the server instance. Call the Connect method to establish a connection to the server instance. The user needs to call the Reset method explicitly for each client instance to close a connection. As I mentioned before, the SendBinaryData method and the SendStringData method can be used to send messages to the connected socket server instance.

The ReceiveData method is used to receive messages from the server. If successful, it will return an array of two objects. If the message from the server is a binary message, the first object in the returned array will be the byte array sent by the server and the second will be null. If the message from the server is a string message, the first object in the returned array will be null and the second will be the string sent by the server. If the method fails, the return value will be null. Failure can be caused by broken socket connection, timeout, etc. Please not that this method will block (until timed out) if there is no message sent from the server.

The SetMaxDataSize method and the SetReadTimeout method are similar to the ones in the server class.

As we have seen, the return value of a method in the client class indicates success or failure. In case of failure, an exception object can be retrieved using the GetLastException method. An important feature of the client class is that whenever an error occurs while sending data to or receiving data from the server, the connection will be refreshed, i.e. Connect will be called automatically.

The Connect method is virtual so that you can implement your own client authentication. What you have to do is, derive a class from XYNetClient, override the Connect method. In your override, call the base class version of Connect first, if successful call the protected method GetSocket to retrieve the internal socket object, then use the retrieved socket object to send user name and password to the server, and receive authorization code from the server if necessary.

Getting Event Notifications For The Client

Please note that the client object is thread-safe, however, if it is accessed from multiple threads concurrently, all threads except one will block. You can have event notifications for the client, that is, you can connect a client to a server and define a procedure to handle incoming messages. Here sample code to achieve this using the ThreadHelper class described in another article of mine.

class MyClass
{ 
  delegate void myDelegate(XYNetClient oClient); 
  static void ProcessIncomingMessage(XYNetClient, oClient)
  {
    // this method will be called from a new thread

    while(true)
    {
      object[] pData = oClient.ReceiveData();
      // invoke message handler if a message is received

      if(pData!=null) MyMessageHandler(pData[0], pData[1]);
    }
  } 
  static void MyMessageHandler(byte[] pBinaryMsg, string sStringMsg) 
  {
    // add code here to handle the incoming message

    // pBinaryMsg will be null if it is a string message

    // pStringMsg will be null if it is a binary message

  }   
  static void Main(string[] args)
  {
    // connect to "myServer" at port 3000

    XYNetClient oClient = new XYNetClient("myServer", 3000);
    if(oClient.Connect())
    {
      // start processing incoming messages

      ThreadHelper oHelper = new ThreadHelper();
      oHelper.SetMethod(new myDelegate(ProcessIncomingMessage));
      oHelper.SetParameter(oClient);
      oHelper.StartThread();
      // now it is not possible for the main thread

      // to use the client object because it will be

      // locked by the new thread almost all the time

    }
  }
}

The ProcessIncomingMessage method will be executed in a separate thread in the above code, it will call the MyMessageHandler method whenever the client receives a message from the server. You can write code to process incoming messages in the MyMessageHandler method. When the ProcessIncomingMessage method is invoked, the client object will be locked almost all the time and no other thread will be able to use it.

Performance, Sample Code, And Limitations

It is hard to evaluate the performance of a software component without using it in various real applications first. If execution speed is the only measure, then a socket server implemented in low level C code will probably beat any other implementation. Anyway, better execution speed is not my goal. However, I did test my component by instantiating a server instance on one machine and using it to handle 2000 concurrent client connections from other machines. So far, I have not noticed any serious problem.

Although I talked about ways to make the socket server secure, it does not mean that the component can be used in unprotected environment against all kinds of abuses and attacks.

To summarize, here are the steps you need to go through when using the XYNetServer class.

  1. Add the included source or dlls to your project.
  2. Implement the four event handlers as described above.
  3. Create a server instance in your application, set event handlers, and then call the StartServer method.
  4. Call the StopServer method at the end of your application.

Here is what you need to do to use the XYNetClient class.

  1. Add the included source or dlls to your project.
  2. Create a client instance in your application, then call the Connect method. The server has to be running before you can do this, of course.
  3. Call other methods of XYNetClient to send and receive messages in your code.
  4. Call the Reset method to terminate the client connection when you are done.

Note that it is possible to use multiple server or client objects in the same program, it is even possible to have multiple client objects connecting to a server object within the same process.

I have included a C# test application. The same executable can be run as a server and as a client. Here is how to run the test application. First, start the server process with the following command.

XYNetSocketTest 3000

The above command will start the server on port 3000. You can start one or more client processes with the following command.

XYNetSocketTest MyServer.MyCompany.Com 3000

The string "MyServer.MyCompany.Com" in the above command can be replaced by the actual ip address of the server machine. The above command can also be run on different machines on the network. If there is a firewall between the client machine and the server machine, then you need to configure the firewall first to allow socket communication on the specified port.

The server process will run for 60 seconds and then shutdown itself, it will reply to any message received. The client process will create 100 socket connections to the server, send a binary message and a string message for each connection to the server, it will also read reply messages from the server.

For you convenience, here is a simplified version of the test application. It creates sever and clients in the same process and loops forever.

class Test
{
  private static XYNetServer myServer = null;
  private static void ExceptionHandler(Exception oBug)
  {
    System.Console.Out.WriteLine("Error: " + oBug.Message);
  }
  private static void ConnectionFilter(string sRemoteAddress,
    int nRemotePort, Socket sock)
  {
    System.Console.Out.WriteLine("Accept " + sRemoteAddress + ":" +
    nRemotePort.ToString());
  }
  private static void StringInputHandler(string sRemoteAddress,
    int nRemotePort, string sData)
  {
    System.Console.Out.WriteLine("Server: " + sRemoteAddress + ":" +
      nRemotePort.ToString());
    System.Console.Out.WriteLine("   " + sData);
    myServer.SendStringData(sRemoteAddress, nRemotePort, "Reply: " + sData);
  }
  [STAThread]
  static void Main(string[] args)
  {
    try
    {
      // create the server instance

      myServer = new XYNetServer("", 2560, 5, 10);
      // set event handlers

      myServer.SetConnectionFilter(
        new ConnectionFilterDelegate(Test.ConnectionFilter));
      myServer.SetExceptionHandler(
        new ExceptionHandlerDelegate(Test.ExceptionHandler));
      myServer.SetStringInputHandler(
        new StringInputHandlerDelegate(Test.StringInputHandler));
      // start the server

      if(myServer.StartServer()==false) throw myServer.GetLastException();
      // create client instances and connect them to the server

      const int nSize = 10;
      const int nPause = 10;
      XYNetClient[] pClients = new XYNetClient[nSize];
      for(int i=0;i<nSize;i++)
      {
        pClients[i] = new XYNetClient("", 2560);
        if(pClients[i].Connect()==false)
          throw pClients[i].GetLastException();
        Thread.Sleep(nPause);
      }
      // looping forever

      while(true)
      {   
        // send string message from each client

        for(int i=0;i<nSize;i++)
        {
          if(pClients[i].SendStringData("This is test " +
            i.ToString())==false)
            throw pClients[i].GetLastException();
          Thread.Sleep(nPause);
        }
        // receive reply message from the server

        for(int i=0;i<nSize;i++)
        {
          Object[] pData = pClients[i].ReceiveData();
          if(pData==null) throw pClients[i].GetLastException();
          string sData = (string)(pData[1]);
          System.Console.Out.WriteLine("Client: reply from server");
          System.Console.Out.WriteLine("   " + sData);
          Thread.Sleep(nPause);
        }
      }
    }
    catch(Exception oBug)
    {
      System.Console.Out.WriteLine("Error Type: " + oBug.GetType().Name);
      System.Console.Out.WriteLine("Error Message: " + oBug.Message);
      System.Console.Out.WriteLine("Error Source: " + oBug.Source);
      System.Console.Out.WriteLine("Error StackTrace: " + oBug.StackTrace);
     }
  }
}

This component is intended to be self-contained which means you have to use it in both of your client and server applications. A drawback is, it is not possible to use other socket programs to communicate with this component directly.

Reaching outside of the .NET world

I have always thought that a software component is not that useful if you have to redesign or rewrite a significant portion of your existing code. As promised in the last version of this article, I have implemented a java class XYNetJavaClient that can act as a client to the XYNetSocket component. You can use the java class from your java programs. There is also a com dll XYNetComClient.dll which can be used in old VB and C/C++ programs.

The java and com client components have almost the same methods as the .NET client class. For example, Connect, SendStringData, SendBinaryData, and ReceiveData methods are implemented and work (almost) the same way as the .NET client. For your convenience, I have included test programs that work with the .NET test server described above.

To start the java test program, you issue the following command

java XYNetJavaClient MyServer.MyCompany.com 3000

To test the com client, you can use the VB script file XYNetClientTest.vbs.

Thank you for reading my articles on Code Project and using my tools.

Recent Updates

  • 14 Oct 2005:Updated source code
  • 24 Mar 2004: Modified the client class implementation. Updated the article to include event notification for a client.
  • 28 Jan 2004: Added java and com client components.
  • 4 Dec 2003: Added simple sample code to the article. Modified the socket server so that you can recycle an instance (shutdown and start again).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here