Async Tcp Socket Server/Client in C#

I wanted to program a server which can handle alot of clients at the same time just like MMORP-Servers do.

Many Languages are capable of doing this, but most of them are optimized for Web-Servers.
Another important aspect is the amount of data being sent over the stream. There shouldn’t be any limit for the package size.

Code

Enough talking. Here are the complete codeblocks for each client- and serverside.
I implemented a little bit more code than above in the tutorial, but everything should be well explained in the comments.
Feel free to use this code and maybe program your own little MMORPG software.

Click here to see the GitHub repository.

Client

public class ATclient
    {
        public static int BUFFER_SIZE = 1024;
        public static int PACKAGE_LENGTH_SIZE = 4;

        private Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        private string ip;
        private int port;

        //synchronize connected attribute
        private static Object Lock = new Object();
        private bool _connected = false;
        public bool connected
        {
            get
            {
                lock (Lock)
                {
                    return _connected;
                }
            }
            set
            {
                lock (Lock)
                {
                    _connected = value;
                }

                if(value == false){
                    //fire disconnect event
                    disconnecting?.Invoke();
                }
            }
        }

        //disconnecing event
        public delegate void standardHandler();
        public event standardHandler disconnecting;

        //events for logging messages
        public delegate void consoleLog(string message);
        public event consoleLog consoleLogging;

        #region Connect
        public void connect(string ip = "127.0.0.1", int port = 5000)
        {
            this.ip = ip;
            this.port = port;

            log("Connecting to server...");

            try
            {
                clientSocket.BeginConnect(ip, port, new AsyncCallback(connectCallback), clientSocket);
            }
            catch
            {
                connected = false;

                log("Failed to connect to Server.");
            }

            log($"Client set up to communicate with Server {ip} with Port {port}");
        }
        #endregion

        #region Recieve
        private void connectCallback(IAsyncResult ar)
        {
            try
            {
                Socket tempS = ar.AsyncState as Socket;
                tempS.EndConnect(ar);

                log("Connected.");
                connected = true;

                packageState package = new packageState(tempS);

                tempS.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(recieveCallback), package);
            }
            catch
            {
                connected = false;
                log("Failed to connect to Server.");
            }
        }

        private void recieveCallback(IAsyncResult ar)
        {
            try
            {
                packageState package = ar.AsyncState as packageState;
                Socket clientS = package.socket;

                int bytesRead = clientS.EndReceive(ar);

                if (bytesRead > 0)
                {
                    int size = ATclient.BUFFER_SIZE;
                    if (package.readOffset == -1)
                    //got new package
                    {
                        size = BitConverter.ToInt32(package.sizeBuffer, 0);
                        package.readBuffer = new byte[size];
                        package.readOffset = 0;

                    }
                    else
                    //recieved data belongs to old package
                    {
                        package.readOffset += bytesRead;

                        if (package.readOffset == package.readBuffer.Length)
                        //all data for this package is read
                        {
                            //clone array so the package can be disposed
                            byte[] temp = package.readBuffer.Clone() as byte[];

                            //TODO IMPLEMENT
                            //TEMP IS THE RECIEVED DATA
                            //HANDLE DATA HERE

                            //free memory
                            package.Dispose();
                            package = new packageState(clientS);
                        }
                    }

                    if (package.readBuffer == null)
                    //new package
                    {
                        package.socket.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(recieveCallback), package);
                    }
                    else
                    //read max buffer size or rest of the package size
                    {
                        int readsize = (ATclient.BUFFER_SIZE > size) ? size : ATclient.BUFFER_SIZE;
                        package.socket.BeginReceive(package.readBuffer, package.readOffset, readsize, SocketFlags.None, new AsyncCallback(recieveCallback), package);
                    }
                }
                else
                {
                    connected = false;
                    log("Error recieving Message! ReadBytes < 0.");
                }
            }
            catch
            {
                connected = false;
                log("Error recieving Message!");
            }
        }
        #endregion

        #region Send
        public void sendData(byte[] data)
        {
            if (connected)
            {
                try
                {
                    //send sizeinfo
                    byte[] sizeInfo = BitConverter.GetBytes(data.Length);
                    clientSocket.BeginSend(sizeInfo, 0, sizeInfo.Length, SocketFlags.None, new AsyncCallback(sendCallback), clientSocket);

                    //send data
                    clientSocket.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback(sendCallback), clientSocket);
                }
                catch
                {
                    connected = false;
                }
            }
            else
            {
                log("Can't send Data. You are not connected to the Server.");
            }
        }

        private void sendCallback(IAsyncResult ar)
        {
            try
            {
                Socket clientS = ar.AsyncState as Socket;
                int sizeSend = clientS.EndSend(ar);

                log($"Sent {sizeSend} Bytes to the Server.");
            }
            catch
            {
                connected = false;
                log("Failed sending Message!");
            }
        }
        #endregion

        internal void log(string message)
        {
            consoleLogging?.Invoke(message);
        }
    }

    internal class packageState : IDisposable
    {
        public Socket socket = null;
        public byte[] sizeBuffer = new byte[ATclient.PACKAGE_LENGTH_SIZE];
        public int readOffset = -1;
        public byte[] readBuffer = null;

        public packageState(Socket s)
        {
            this.socket = s;
        }

        #region IDisposable Support
        //free memory
        private bool disposedValue = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    socket = null;
                    sizeBuffer = null;
                    readBuffer = null;
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }

Server

    public class ATserver
    {
        public static int MAX_PENDING_CONNECTIONS = 20;
        public static int MAX_CLIENTS = 1000;

        public static int BUFFER_SIZE = 1024;
        public static int PACKAGE_LENGTH_SIZE = 4;

        private Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        private List<client> clients = new List<client>();

        //public events
        public event clientHandler clientConnecting;
        public event clientHandler clientDisconnecting;
        public delegate void clientHandler(int clientID);

        //log events
        public event stringHandler consoleLogged;
        public delegate void stringHandler(string message);

        //recieving data handler
        public delegate void clientByteHandler(int clientID, byte[] data);

        #region Connect
        public void start(int port = 5000)
        {
            serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
            serverSocket.Listen(ATserver.MAX_PENDING_CONNECTIONS);
            serverSocket.BeginAccept(new AsyncCallback(acceptCallback), serverSocket);

            log($"Server listening on Port: {port}");
        }

        private void acceptCallback(IAsyncResult ar)
        {
            try
            {
                Socket serverS = ar.AsyncState as Socket;

                Socket clientSocket = serverS.EndAccept(ar);
                serverS.BeginAccept(new AsyncCallback(acceptCallback), serverS);

                log($"Connection from {clientSocket.RemoteEndPoint.ToString()} recieved.");

                if (clients.Count < ATserver.MAX_CLIENTS)
                {
                    client c = new client(clientSocket);
                    clients.Add(c);

                    //connect all the events from the client
                    c.clientDisconnecting += clientDisconnected;
                    c.recievingData += recievedData;
                    c.consoleLogging += log;

                    //start the client
                    c.startClient();
                    //fire the event that a client is connected
                    clientConnecting?.Invoke(c.id);
                }
                else
                {
                    clientSocket.Close();
                    log($"Max Number of Clients connected is reached. IP: {clientSocket.RemoteEndPoint.ToString()} got declined.");
                }
            }
            catch
            {
                log("Error accepting connection!");
            }
        }

        private void clientDisconnected(int clientID)
        {
            client client = clients.Find(x => x.id == clientID);

            if (client != null)
            {
                clientDisconnecting?.Invoke(clientID);

                clients.Remove(client);

                client.clientDisconnecting -= clientDisconnected;
                client.recievingData -= recievedData;
                client.consoleLogging -= log;
            }
        }

        private void recievedData(int clientID, byte[] data)
        {
            //TODO HANDLE DATA FROM THE CLIENT
        }
        #endregion

        #region Send
        public void sendDataTo(int id, byte[] data)
        {
            client c = clients.Find(x => x.id == id);

            if (c != null)
            {
                c.sendData(data);
            }
            else
            {
                log($"Couldn't find client with ID: {id}");
            }
        }
        #endregion

        internal void log(string message)
        {
            consoleLogged?.Invoke(message);
        }
    }

    internal class client
    {
        public int id;
        private static List<int> idList = new List<int>();

        private Socket socket = null;

        public event clientByteHandler recievingData;
        public event clientHandler clientDisconnecting;
        public event stringHandler consoleLogging;

        public client(Socket s)
        {
            id = 0;

            while (idList.Contains(id))
            {
                id++;
            }

            this.socket = s;
        }

        #region Setup
        public void startClient()
        {
            packageState package = new packageState(this.socket);

            socket.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(recieveCallback), package);

            log($"Client: {id} is set up.");
        }

        private void recieveCallback(IAsyncResult ar)
        {
            log($"Data from Client: {id} recieved.");

            try
            {
                packageState package = ar.AsyncState as packageState;
                Socket clientS = package.socket;

                int bytesRead = clientS.EndReceive(ar);

                if (bytesRead > 0)
                {
                    int size = ATserver.BUFFER_SIZE;
                    if (package.readOffset == -1)
                    //new package
                    {
                        size = BitConverter.ToInt32(package.sizeBuffer, 0);
                        package.readBuffer = new byte[size];
                        package.readOffset = 0;
                    }
                    else
                    //add data to existing package
                    {
                        package.readOffset += bytesRead;

                        if (package.readOffset == package.readBuffer.Length)
                        //package fully read
                        {
                            //clone array so the package can be disposed
                            byte[] temp = package.readBuffer.Clone() as byte[];

                            //invoke the event
                            //temp is the recieved data!
                            recievingData?.Invoke(this.id, temp);

                            //free memory
                            package.Dispose();
                            package = new packageState(clientS);
                        }
                    }

                    if (package.readBuffer == null)
                    //new package
                    {
                        package.socket.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(recieveCallback), package);
                    }
                    else
                    //read rest of the bytelength
                    {
                        int readsize = (ATserver.BUFFER_SIZE > size) ? size : ATserver.BUFFER_SIZE;
                        package.socket.BeginReceive(package.readBuffer, package.readOffset, readsize, SocketFlags.None, new AsyncCallback(recieveCallback), package);
                    }
                }
                else
                {
                    log("Error recieving Message! ReadBytes < 0.");
                    closeClient();
                }
            }
            catch
            {
                log("Error recieving Message!");
                closeClient();
            }
        }

        private void closeClient()
        {
            log($"Connection from {socket.RemoteEndPoint.ToString()} has been terminated. Client-ID: {id}");

            socket.Close();

            //Client Disconnected
            clientDisconnecting(this.id);
        }
        #endregion

        #region Send / Recieve Data

        public void sendData(byte[] data)
        {
            try
            {
                //send sizeinfo
                byte[] sizeInfo = BitConverter.GetBytes(data.Length);
                this.socket.BeginSend(sizeInfo, 0, sizeInfo.Length, SocketFlags.None, new AsyncCallback(sendCallback), this.socket);

                //send data
                this.socket.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback(sendCallback), this.socket);
            }
            catch
            {
                log("Error sending Message to the Client: " + this.id);
                closeClient();
            }
        }

        private void sendCallback(IAsyncResult ar)
        {
            try
            {
                Socket clientS = ar.AsyncState as Socket;
                int sizeSend = clientS.EndSend(ar);

                log($"Sent {sizeSend} Bytes to the Server.");
            }
            catch
            {
                log("Failed sending Message!");
                closeClient();
            }
        }
        #endregion

        private void log(string message)
        {
            consoleLogging?.Invoke(message);
        }
    }

    internal class packageState : IDisposable
    {
        public Socket socket = null;
        public byte[] sizeBuffer = new byte[ATserver.PACKAGE_LENGTH_SIZE];
        public int readOffset = -1;
        public byte[] readBuffer = null;

        public packageState(Socket s)
        {
            this.socket = s;
        }

        #region IDisposable Support
        //free memory
        private bool disposedValue = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    socket = null;
                    sizeBuffer = null;
                    readBuffer = null;
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
Last updated