Saturday, May 26, 2012

Multi-Threaded TCP Socket Chat Server

A multi-threaded chat server using c# .net. It accepts connections from the client and manage data reception in separate threads virtually unlimited clients can connect to this server depending on the machine capacity. This is just a server that will broadcast messages from the client to all other clients. also have some events to show server status and client count change.

Detecting client disconnect in abnormal situations like wire cut, improper shutdown,power failure are not supported by framework, we will have to periodically check for the client whether they are connected or not.





public class Server

    {

        //thread safe dictionary to store list of tcp client connection that will be used to broadcast

        ConcurrentDictionary AllClients { get; set; }
       

        #region Events
        public delegate void ClientCount(int count);
        private event ClientCount _ClientCountUpdated;
        public event ClientCount ClientCountUpdated
        {
            add { _ClientCountUpdated += value; }
            remove { _ClientCountUpdated -= value; }
        }

        private void RaiseCount(int count)
        {
            if (_ClientCountUpdated != null)
            {
               
                _ClientCountUpdated(count);
            }
        }


        public delegate void ServerStatus(string status);
        private event ServerStatus _StatusChanged;
        public event ServerStatus StatusChanged
        {
            add { _StatusChanged += value; }
            remove { _StatusChanged -= value; }
        }

        private void RaiseStatusChanged(string status)
        {
            if (_StatusChanged != null)
            {
                _StatusChanged(status);
            }
        }
        #endregion

        #region Properties
        private TcpListener tcpListener;
        private Thread listenThread;
        private int _TotalClientCount;
        private int TotalClientCount
        {
            get
            {
                lock (this)
                {
                    return _TotalClientCount;
                }
            }
            set
            {
                lock (this)
                {
                    _TotalClientCount = value;
                    RaiseCount(_TotalClientCount);
                }
            }
        }
        #endregion

        public Server()
        {
            AllClients = new ConcurrentDictionary();
            MonitorDisconnectedClients();
           
        }

        // this will monitor abnormal network disconnections
        private void MonitorDisconnectedClients()
        {
            new Thread(() =>
            {

                while (true)
                {
                    //an empty broadcast message similar to ping
                    BroadcastMessage(" ");
               
                    var clientsToRemove = AllClients.Values.Where(r => r.LastUpdate < DateTime.Now.AddSeconds(-5)).ToArray();
                    MyTcpClient temp = null;
                    foreach (var client in clientsToRemove)
                    {
                        client.Tcp.Close();
                        AllClients.TryRemove(client.GetHashCode().ToString(), out temp);
                    }
                    RaiseCount(AllClients.Count);
                    Thread.Sleep(5000);
                }
            }).Start();
        }

      

        public void Start()
        {
            RaiseStatusChanged("Starting...");
            this.tcpListener = new TcpListener(IPAddress.Any, 3000);
            this.listenThread = new Thread(new ThreadStart(ListenForClients));
            this.listenThread.Start();
            RaiseStatusChanged("Listening...");
        }

        //start to listen to tcp connections
        private void ListenForClients()
        {
            this.tcpListener.Start();

            while (true)
            {
                //blocks until a client has connected to the server
                TcpClient client = this.tcpListener.AcceptTcpClient();
                var myClient = new MyTcpClient(client);
                //add client connection to to the thread safe dictionary
                AllClients.AddOrUpdate(myClient.GetHashCode().ToString(), myClient, (key, value) => { return value; });
                //create a thread to handle communication
                //with connected client
                Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
                clientThread.Start(myClient);
                RaiseCount(AllClients.Count);
            }
        }

        //this will handle messages comming from the client in a seperate thread
        private void HandleClientComm(object client)
        {
            MyTcpClient tcpClient = (MyTcpClient)client;
            NetworkStream clientStream = tcpClient.Tcp.GetStream();

            byte[] message = new byte[4096];
            int bytesRead;

            while (true)
            {
                bytesRead = 0;

                try
                {
                    //blocks until a client sends a message
                    bytesRead = clientStream.Read(message, 0, 4096);
                   
                }
                catch
                {
                    //a socket error has occured
                    break;
                }

                if (bytesRead == 0)
                {
                   
                    AllClients.TryRemove(tcpClient.GetHashCode().ToString(), out tcpClient);
                    RaiseCount(AllClients.Count);
                    //the client has disconnected from the server
                    break;
                }

                //message has successfully been received
                ASCIIEncoding encoder = new ASCIIEncoding();
                var msg = encoder.GetString(message, 0, bytesRead);
                System.Diagnostics.Debug.WriteLine(msg);
                tcpClient.LastUpdate = DateTime.Now;
                if(!string.IsNullOrWhiteSpace(msg))
                    BroadcastMessage(msg);
            }

            tcpClient.Tcp.Close();
        }

        // this is used to broadcast messages to all other clients, also in another thread
        private void BroadcastMessage(string msg)
        {
            new Thread(() => {
                foreach (var item in AllClients)
                {
                    var tcpClient = item.Value;
                    if (tcpClient.Tcp.Connected)
                    {
                        NetworkStream clientStream = tcpClient.Tcp.GetStream();
                        ASCIIEncoding encoder = new ASCIIEncoding();
                        byte[] buffer = encoder.GetBytes(msg);
                        clientStream.Write(buffer, 0, buffer.Length);
                        clientStream.Flush();
                    }
                }
            }).Start();
        }

      
        //used to close down all tcp connections
        public void Close()
        {
            foreach (var client in AllClients)
            {
                client.Value.Tcp.Close();
            }
        }
    }

//MyTcpClent is just a wrapper to hold updates of a tcp client connection

 public class MyTcpClient
    {
        public MyTcpClient(TcpClient client)
        {
            Tcp = client;
            LastUpdate = DateTime.Now;
        }
        public TcpClient Tcp { get; set; }
        public DateTime LastUpdate { get; set; }
    }

6 comments:

sharper said...

Very nice example! thanks.

Could you also provide the code for the "MyTcpClient" class as well?

Unknown said...

hi sharper,
I have just added code for MyTcpClient. MyTcpClient is just a wrapper to hold last update datetime because .net don't offer to detect broken links due to switch/router/power failure. i do a ping/pong check and have update datetime associated with every client to drop zombie connections.

Regards.

Anonymous said...

Hey,

All looks niec but quite new to ConcurrentDictionary,
And im getting some problems pretty sure i got the right using's included.

Yet i get for this line
ConcurrentDictionary AllClients { get; set; }

Using the generic type 'System.Collections.Concurrent.ConcurrentDictionary' requires 2 type arguments.

Not quite sure how to fix using using VCSharp 2010 btw. thanks a lot!

Erwan said...
This comment has been removed by the author.
Erwan said...
This comment has been removed by the author.
Erwan said...

Geez, this is the third time I'm trying to post this. Blogger removes all caret symbols to prevent javascript injections.

However, you need to give the concurrent dictionary it's key pair datatypes (string and MyTcpClient)

The code is below:

Global Variable:

ConcurrentDictionary[OPENCARET]string, MyTcpClient[CLOSECARET] AllClients { get; set; }

Server Constructor:

AllClients = new ConcurrentDictionary[OPENCARE]string, MyTcpClient[CLOSECARET]();