SoFunction
Updated on 2025-03-07

Use C# to write an asynchronous Socket Server

introduce

I recently needed to prepare an internal thread communication mechanism for a .net project. The project has multiple servers and clients for use, Windows forms and console applications. Given the possibility of implementation, I decided to use native sockets instead of many .NET components that have been built for us in advance, such as so-called pipelines, NetTcpClient and Azure Service Bus.

The servers in this article are based on class asynchronous methods. These allow you to support a large number of socket clients, and a client connection is the only blocking mechanism. The blocking time can be ignored and can't be remembered, so the server is basically working as a multi-threaded socket server.

background

Native sockets have the advantage of giving you full control of the communication level, and there is a lot of flexibility in handling different data types. You can even send serialized CLR objects through sockets, although I won't do that here. This project will want you to show how to send text between sockets.
Application of code

Using the following code, you initialize a Server class and run the Start() method:
 

Server myServer = new Server();
();

If you plan to manage the server in a Windows form, I recommend using a BackgroundWorker, because the socket method (usually ManualResentEvent) will block the GUI thread from running.

Server class:
 

using ;
 
public class Server
{
  private static Socket listener;
  public static ManualResetEvent allDone = new ManualResetEvent(false);
  public const int _bufferSize = 1024;
  public const int _port = 50000;
  public static bool _isRunning = true;
 
  class StateObject
  {
    public Socket workSocket = null;
    public byte[] buffer = new byte[bufferSize];
    public StringBuilder sb = new StringBuilder();
  }
 
  // Returns the string between str1 and str2
  static string Between(string str, string str1, string str2)
  {
    int i1 = 0, i2 = 0;
    string rtn = "";
 
    i1 = (str1, );
    if (i1 > -1)
    {
      i2 = (str2, i1 + 1, );
      if (i2 > -1)
      {
        rtn = (i1 + , i2 - i1 - );
      }
    }
    return rtn;
  }
 
  // Checks if the socket is connected
  static bool IsSocketConnected(Socket s)
  {
    return !(((1000, ) && ( == 0)) || !);
  }
 
  // Insert all the other methods here.
}


ManualResetEvent is a .NET class that implements events in your socket server. We need this project to send signals to the code when we want to post blocking operations. You can experiment with bufferSize to adapt your needs. If you can expect the size of the message, use the byte unit to set the message size parameter bufferSize. Port is the port parameter to listen for TCP. Be aware of the interface used for other application servos. If you want to be able to easily stop the server, you need to implement some mechanisms to set _isRunning to false. This can generally be done with a BackgroundWorker, where you can use replacement _isRunning. The reason I mentioned _isRunning is to provide you with a direction to deal with cancellation operations and show you that the listener can easily stop.

Between() and IsSocketConnected() are helper methods.


Now turn around and look at the method. First, the Start() method:
 

public void Start()
{
  IPHostEntry ipHostInfo = (());
  IPEndPoint localEP = new IPEndPoint(, _port);
  listener = new Socket(, , );
  (localEP);
 
  while (_IsRunning)
  {
    ();
    (10);
    (new AsyncCallback(acceptCallback), listener);
    bool isRequest = (new TimeSpan(12, 0, 0)); // Blocks for 12 hours
 
    if (!isRequest)
    {
      ();
      // Do some work here every 12 hours
    }
  }
  ();
}

This method initializes the listener socket and starts waiting for the user's connection to arrive. The main pattern in the project is to use asynchronous delegation. Asynchronous delegation is a method called asynchronously when the state in the caller changes. isRequest tells you whether WaitOne has exited due to a client connection or timeout.

If you have a large number of client connections occurring simultaneously, consider increasing the queue parameters of the Listen() method.


Now let's look at the next method, acceptCallback. This method is called asynchronously. When the method is finished executing, the listener will immediately listen to the new client.
 

static void acceptCallback(IAsyncResult ar)
{
  // Get the listener that handles the client request.
  Socket listener = (Socket);
 
  if (listener != null)
  {
    Socket handler = (ar);
 
    // Signal main thread to continue
    ();
 
    // Create state
    StateObject state = new StateObject();
     = handler;
    (, 0, _bufferSize, 0, new AsyncCallback(readCallback), state);
  }
}

acceptCallback will derive another asynchronous assignment: readCallback. This method will read the actual data from the socket. I have already controlled my own data for sending and receiving data, which is unchanged for _bufferSize. All strings sent to the server must be wrapped with <!--SOCKET--> and <!--ENDSOCKET-->. Similarly, when the client receives the server's responsiveness, it must unwrap the response information, which is wrapped by <!--RESPONSE--> and <!--ENDRESPONSE-->.
 

static void readCallback(IAsyncResult ar)
{
  StateObject state = (StateObject);
  Socket handler = ;
 
  if (!IsSocketConnected(handler)) 
  {
    ();
    return;
  }
 
  int read = (ar);
 
  // Data was read from the client socket.
  if (read > 0)
  {
    (Encoding.(, 0, read));
 
    if (().Contains("<!--ENDSOCKET-->"))
    {
      string toSend = "";
      string cmd = ((), "<!--SOCKET-->", "<!--ENDSOCKET-->");
           
      switch (cmd)
      {
        case "Hi!":
          toSend = "How are you?";
          break;
        case "Milky Way?":
          toSend = "No I am not.";
          break;
      }
 
      toSend = "<!--RESPONSE-->" + toSend + "<!--ENDRESPONSE-->";
 
      byte[] bytesToSend = Encoding.(toSend);
      (bytesToSend, 0, , 
        , new AsyncCallback(sendCallback), state);
    }
    else 
    {
      (, 0, _bufferSize, 0
          , new AsyncCallback(readCallback), state);
    }
  }
  else
  {
      ();
  }
}

readCallback will derive another method, sendCallback, which will send a request to the client. If the client does not close the connection, sendCallback will send a signal to the socket to obtain more data.
 

static void sendCallback(IAsyncResult ar)
{
  StateObject state = (StateObject);
  Socket handler = ;
  (ar);
 
  StateObject newstate = new StateObject();
   = handler;
  (, 0, , 0, new AsyncCallback(readCallback), newstate);
}

I will leave writing a socket client as a contact to the reader. The socket client should use the same asynchronous call to the same programming pattern. I hope you can get fun from this post and put it into practice like a socket programmer!

Important points

I've used this code in a production environment where the socket server is a free text search engine. SQL Server lacks support for free text search (you can use free text indexing, but they are slow and expensive). The socket server loads a lot of text data oriented to IEnumerables and uses Linq to search for text. Response from socket server searches from millions of lines of Unicode text data in a few milliseconds. We also use three distributed Sphinx servers(). The socket server acts as a cache for the Sphinx server. If you need a fast free text search engine, I highly recommend using Sphinx.