Click here to Skip to main content
15,881,089 members
Articles / Programming Languages / C#

c# async socket server

Rate me:
Please Sign up or sign in to vote.
4.55/5 (23 votes)
17 Mar 2014CPOL4 min read 96.5K   40   8
How to write an asynchronous sockets server in c#

Introduction

I recently needed an inter-process communication mechanism for a .NET project. The project consisted of multiple servers and clients with ASP.NET, Windows Forms, and console applications. Considering the possibilities, I landed on using raw sockets, instead of many of the pre-built mechanism in .NET, like named pipes, NetTcpClient and the Azure Service Bus.

The server in this article is based on the async methods of the System.Net.Sockets class. These methods allow you to support a large number of socket clients, and the only blocking mechanism of the server is when a client connects. The blocking time is neglible, and the server will operate virtually as a multi-threaded socket server.

Background

Raw sockets have the advantage of giving you full control of the communication layer, and has great flexibility when processing different data types. You can even send serialized CLR objects through sockets, although I won't go into that here. This project will show you how to send text between the sockets.

Using the code

To use the code, you instantiate the Server class, and run the Start() method:

C#
Server myServer = new Server();
myServer.Start();

If you plan to host the server in a Windows form, I recommend using a BackgroundWorker, since the socket methods (in particular, the ManualResentEvent) will block the GUI thread.

The Server class:

C#
using System.Net.Sockets;

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 = str.IndexOf(str1, StringComparison.InvariantCultureIgnoreCase);
        if (i1 > -1)
        {
            i2 = str.IndexOf(str2, i1 + 1, StringComparison.InvariantCultureIgnoreCase);
            if (i2 > -1)
            {
                rtn = str.Substring(i1 + str1.Length, i2 - i1 - str1.Length);
            }
        }
        return rtn;
    }

    // Checks if the socket is connected
    static bool IsSocketConnected(Socket s)
    {
        return !((s.Poll(1000, SelectMode.SelectRead) && (s.Available == 0)) || !s.Connected);
    }

    // Insert all the other methods here.
}  

ManualResetEvent is a .NET class that implements events in your socket server. We need this object to signal to our code when we want to release blocking operations. You can experiment with bufferSize to suit your needs. If you have predictable sized messages, set bufferSize to the message size in bytes. port is the TCP port to listen on. Beware of using ports that are reserved for other applications. If you want to be able to stop the server gracefully, you need to implement some mechanism for setting _isRunning to false. This can typically be done by using a BackgroundWorker, where you replace _isRunning with myWorker.CancellationPending. The reason I included _isRunning is to give you a direction on handling cancellation, and show you that the listener can be stopped gracefully.

Between() and IsSocketConnected() are helper methods.

Now to the methods. First, the Start() method:

C#
public void Start()
{
    IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
    IPEndPoint localEP = new IPEndPoint(IPAddress.Any, _port);
    listener = new Socket(localEP.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
    listener.Bind(localEP);

    while (_IsRunning)
    {
        allDone.Reset();
        listener.Listen(10);
        listener.BeginAccept(new AsyncCallback(acceptCallback), listener);
        bool isRequest = allDone.WaitOne(new TimeSpan(12, 0, 0));  // Blocks for 12 hours

        if (!isRequest)
        {
            allDone.Set();
            // Do some work here every 12 hours
        }
    }
    listener.Close();
} 

This method initiates the listener socket, and starts waiting for clients to connect. The main pattern in this project is using async delegates. Async delegates are functions that are called asynchronously when the state changes in the caller. isRequest tells you wether WaitOne exited because a client connected, or because the timout was reached.

If you have a large number of clients connecting simultaneously, consider increasing the queue parameter of the Listen() method.

Now to the next method, acceptCallback . This method is called asynchronously by listener.BeginAccept. When the method completes, the listener will immediately listen for new clients.

C#
static void acceptCallback(IAsyncResult ar)
{
    // Get the listener that handles the client request.
    Socket listener = (Socket)ar.AsyncState;

    if (listener != null)
    {
        Socket handler = listener.EndAccept(ar);

        // Signal main thread to continue
        allDone.Set();

        // Create state
        StateObject state = new StateObject();
        state.workSocket = handler;
        handler.BeginReceive(state.buffer, 0, _bufferSize, 0, new AsyncCallback(readCallback), state);
    }
}

acceptCallback forks another async delegate: readCallback. This method will read actual data from the socket. I have made my own protocol for sending and receiving data, which is invariant to _bufferSize. Every string sent to the server must be wrapped by <!--SOCKET--> and <!--ENDSOCKET-->. Likewise, the client, when receiving the response from the server, must unwrap the response, which is wrapped by <!--RESPONSE--> and <!--ENDRESPONSE-->

C#
static void readCallback(IAsyncResult ar)
{
    StateObject state = (StateObject)ar.AsyncState;
    Socket handler = state.workSocket;

    if (!IsSocketConnected(handler)) 
    {
        handler.Close();
        return;
    }

    int read = handler.EndReceive(ar);

    // Data was read from the client socket.
    if (read > 0)
    {
        state.sb.Append(Encoding.UTF8.GetString(state.buffer, 0, read));

        if (state.sb.ToString().Contains("<!--ENDSOCKET-->"))
        {
            string toSend = "";
            string cmd = ts.Strings.Between(state.sb.ToString(), "<!--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.UTF8.GetBytes(toSend);
            handler.BeginSend(bytesToSend, 0, bytesToSend.Length, SocketFlags.None
                , new AsyncCallback(sendCallback), state);
        }
        else 
        {
            handler.BeginReceive(state.buffer, 0, _bufferSize, 0
                    , new AsyncCallback(readCallback), state);
        }
    }
    else
    {
            handler.Close();
    }
}

readCallback will fork another method, sendCallback, that will send the response to the client. If the client doesn't close the connection, sendCallback will signal the socket to listen for more data.

C#
static void sendCallback(IAsyncResult ar)
{
    StateObject state = (StateObject)ar.AsyncState;
    Socket handler = state.workSocket;
    handler.EndSend(ar);

    StateObject newstate = new StateObject();
    newstate.workSocket = handler;
    handler.BeginReceive(newstate.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(readCallback), newstate);
}

I will leave it as an exercise to the reader to write the socket client. The socket client should use the same programming pattern of async callbacks. I hope you enjoyed the article, and that you will excel as a socket programmer!

Points of Interest

I am using this code in a production environment where the socket server is a freetext search engine. SQL Server lacks support for freetext search (you can use freetext indexes, but they are slow and expensive). The socket server loads huge amounts of text data into iEnumerables, and uses Linq to search the text. The response from the socket server is in the order of milliseconds when searching millions of rows of unicode text data. We also use three distributed Sphinx servers (www.sphinxsearch.com). The socket server serves as a cache for the Sphinx servers. If you need a fast freetext search engine, I highly recommend Sphinx.

History

First version.

License

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


Written By
Architect
Norway Norway
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Question[My vote of 2] Voted 2... Pin
jfriedman13-May-16 11:02
jfriedman13-May-16 11:02 
GeneralMy vote of 1 Pin
zeeshan anjum27-Apr-14 1:50
zeeshan anjum27-Apr-14 1:50 
AnswerRe: My vote of 1 Pin
Afzaal Ahmad Zeeshan1-Aug-15 5:55
professionalAfzaal Ahmad Zeeshan1-Aug-15 5:55 
SuggestionGreat article, but the source code is missing Pin
rezer124-Mar-14 9:19
rezer124-Mar-14 9:19 
GeneralRe: Great article, but the source code is missing Pin
Pål Thingbø27-Mar-14 13:47
Pål Thingbø27-Mar-14 13:47 
GeneralRe: Great article, but the source code is missing Pin
montyno52-Jul-14 23:42
montyno52-Jul-14 23:42 
QuestionFragile code Pin
jgauffin23-Mar-14 23:12
jgauffin23-Mar-14 23:12 
AnswerRe: Fragile code Pin
Pål Thingbø27-Mar-14 13:46
Pål Thingbø27-Mar-14 13:46 

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.