Skip to main content
4 of 6
Added a 3rd version where we ignore some exceptions with stopping the server
jeremywho
  • 253
  • 1
  • 3
  • 10

C# Console Chat Server - Async/Await, TcpClient/TcpListener, NetworkStream (async)

Just looking for feedback on correctness of my understanding of async/await.

Curious about the Task.Run inside of the StartReceive() method. Resharper (or maybe just the compiler) warns against doing this without an await, but I think in this case, its ok.

After running, one can connect by telnetting to localhost 7776.

Thanks for your reply Svick. I've added a second version with suggestions you provided. A couple of things:

  • This is just an experiment, not production. That said, I would like to make it quality.
  • I understand the need to avoid void async methods (I saw the talk on channel 9). But at some point up the chain of method calls, we reach a point where async is no possible. In this case, Main() cannot be async. Any ideas for handling this?
  • Also, when cleaning up, some of the async methods throw exceptions with disposing/closing the object they belong to. Is catching and maybe logging or even swallowing these exceptions the best way to handle that clean up? I cannot figure out a better way at the moment.

V3 (Better handling of clean up on server stop):

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;    

namespace ChatTutorial.ConsoleServer
{
    class Program
    {
        private static void Main()
        {
            var server = new ChatServer();
            server.Start();
            Console.WriteLine("Listening....Press Enter to Stop.");
            Console.ReadLine();
            server.Stop();
            Console.WriteLine("Server Stopped. Press Enter to exit.");
            Console.ReadLine();
        }
    }
    
    internal class ChatServer
    {
        private readonly TcpListener _listener = new TcpListener(IPAddress.Any, 6667);
        private readonly ConcurrentDictionary<string, LocalClient> _clients = new ConcurrentDictionary<string, LocalClient>();

        private bool _stopping;

        public async void Start()
        {
            try
            {
                _listener.Start();                
                while (!_stopping)
                {
                    try
                    {
                        Console.WriteLine("Listening...");
                        var client = await _listener.AcceptTcpClientAsync();

                        if (client == null) break;

                        Console.WriteLine("Client connected");
                        var id = Guid.NewGuid().ToString();
                        if (_clients.TryAdd(id, new LocalClient(client, ReceivedMessage, ClientClosed, id))) continue;

                        Console.WriteLine("Client {0} already exists in dictionary", id);
                        client.Close();
                    }
                    catch (InvalidOperationException)
                    {
                        if (!_stopping)
                        {
                            Console.WriteLine("InvalidOperationException");    
                        }                     
                        break;
                    }
                    catch (SocketException ex)
                    {

                        if (!_stopping)
                        {
                            Console.WriteLine("SocketException");
                            Logger.PrintException(ex); 
                        }
                        break;
                    }
                    catch (Exception ex)
                    {
                        Logger.PrintException(ex);
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex);
            }
        }

        public void Stop()
        {
            Console.WriteLine("Stopping...");
            _stopping = true;
            _listener.Stop();

            if (_clients.Count <= 0) return;

            foreach (var entry in _clients)
                entry.Value.Dispose();
        }

        public void ClientClosed(LocalClient client)
        {
            Console.WriteLine("Client {0} disconnected", client.Id);

            LocalClient tempLocalClient;
            _clients.TryRemove(client.Id, out tempLocalClient);
            client.Dispose();

            Console.WriteLine("Client {0} Closed", client.Id);
            ReceivedMessage("Client disconnected", client.Id);
        }

        public void ReceivedMessage(string message, string id)
        {
            try
            {
                foreach (var client in _clients)
                {
                    if (client.Key != id)
                        client.Value.Send(id + "> " + message);
                }
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex);
            }
        }        

        internal class LocalClient : IDisposable
        {
            private readonly TcpClient _client;
            public TcpClient Client { get { return _client; } }

            private readonly Action<LocalClient> _closedCallback;            
            private readonly Action<string,string> _recvCallback;

            private readonly StreamReader _reader;
            private readonly StreamWriter _writer;

            private readonly string _id;
            public string Id { get { return _id; } }

            private bool _disposing;

            public LocalClient(TcpClient client, Action<string,string> recvCallback, Action<LocalClient> closedCallback, string id)
            {
                _closedCallback = closedCallback;
                try
                {
                    _client = client;
                    _recvCallback = recvCallback;
                    _id = id;

                    _reader = new StreamReader(_client.GetStream());
                    _writer = new StreamWriter(_client.GetStream()) {AutoFlush = true};

                    StartReceive();

                    Console.WriteLine("Local client {0} receiving...", id);
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex);
                }
            }

            public async void StartReceive()
            {
                try
                {
                    while (true)
                    {
                        if (_reader == null) return;
                        var message = await _reader.ReadLineAsync();

                        if (String.IsNullOrEmpty(message))
                        {
                            Task.Run(() => _closedCallback(this));
                            return;
                        }

                        _recvCallback(message, _id);
                        Console.WriteLine("{0} > {1}", _id, message);
                    }
                }
                catch (ArgumentOutOfRangeException)
                {
                    if(!_disposing)
                        Console.WriteLine("ArgumentOutOfRangeException");
                }
                catch (ObjectDisposedException)
                {
                    if (!_disposing)
                        Console.WriteLine("ObjectDisposedException");
                }
                catch (InvalidOperationException)
                {
                    if (!_disposing)
                        Console.WriteLine("InvalidOperationException");            
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex);
                }
            }

            public async void Send(string message)
            {
                try
                {                    
                    if (String.IsNullOrEmpty(message)) return;
                    if (_writer == null) return;

                    await _writer.WriteLineAsync(message);                    
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex);
                }
            }

            public void Dispose()
            {
                _disposing = true;

                if (_reader != null)
                    _reader.Dispose();
                if (_writer != null)
                    _writer.Dispose();
            }
        }        
    }

    public static class Logger
    {
        public static void PrintException(Exception ex, [CallerMemberName]string memberName = "")
        {
            Console.WriteLine("{0}: \n '{1}' \n '{2}'", memberName, ex.Message, ex.StackTrace);
        }
    }
}

V2 (with suggestions from svick):

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace ChatTutorial.ConsoleServer
{
    class Program
    {
        private static void Main()
        {
            var server = new ChatServer();
            server.Start();
            Console.WriteLine("Listening....Press Enter to Stop.");
            Console.ReadLine();
            server.Stop();
            Console.WriteLine("Server Stopped. Press Enter to exit.");
            Console.ReadLine();
        }
    }
    
    internal class ChatServer
    {
        private readonly TcpListener _listener = new TcpListener(IPAddress.Any, 6667);
        private readonly ConcurrentDictionary<string, LocalClient> _clients = new ConcurrentDictionary<string, LocalClient>();

        private bool _stopping;

        public async void Start()
        {
            try
            {
                _listener.Start();                
                while (!_stopping)
                {
                    try
                    {
                        Console.WriteLine("Listening...");
                        var client = await _listener.AcceptTcpClientAsync();

                        if (client == null) break;

                        Console.WriteLine("Client connected");
                        var id = Guid.NewGuid().ToString();
                        if (_clients.TryAdd(id, new LocalClient(client, ReceivedMessage, ClientClosed, id))) continue;

                        Console.WriteLine("Client {0} already exists in dictionary", id);
                        client.Close();
                    }
                    catch (InvalidOperationException)
                    {
                        Console.WriteLine("InvalidOperationException");                        
                        break;
                    }
                    catch (SocketException ex)
                    {
                        Console.WriteLine("SocketException");
                        Logger.PrintException(ex);
                        break;
                    }
                    catch (Exception ex)
                    {
                        Logger.PrintException(ex);
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex);
            }
        }

        public void Stop()
        {
            Console.WriteLine("Stopping...");
            _stopping = true;
            _listener.Stop();

            if (_clients.Count <= 0) return;

            foreach (var entry in _clients)
                entry.Value.Dispose();
        }

        public void ClientClosed(LocalClient client)
        {
            Console.WriteLine("Client {0} disconnected", client.Id);

            LocalClient tempLocalClient;
            _clients.TryRemove(client.Id, out tempLocalClient);
            client.Dispose();

            Console.WriteLine("Client {0} Closed", client.Id);
            ReceivedMessage("Client disconnected", client.Id);
        }

        public void ReceivedMessage(string message, string id)
        {
            try
            {
                foreach (var client in _clients)
                {
                    if (client.Key != id)
                        client.Value.Send(id + "> " + message);
                }
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex);
            }
        }        

        internal class LocalClient : IDisposable
        {
            private readonly TcpClient _client;
            public TcpClient Client { get { return _client; } }

            private readonly Action<LocalClient> _closedCallback;            
            private readonly Action<string,string> _recvCallback;

            private readonly StreamReader _reader;
            private readonly StreamWriter _writer;

            private readonly string _id;
            public string Id { get { return _id; } }            

            public LocalClient(TcpClient client, Action<string,string> recvCallback, Action<LocalClient> closedCallback, string id)
            {
                _closedCallback = closedCallback;
                try
                {
                    _client = client;
                    _recvCallback = recvCallback;
                    _id = id;

                    _reader = new StreamReader(_client.GetStream());
                    _writer = new StreamWriter(_client.GetStream()) {AutoFlush = true};

                    StartReceive();

                    Console.WriteLine("Local client {0} receiving...", id);
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex);
                }
            }

            public async void StartReceive()
            {
                try
                {
                    while (true)
                    {
                        if (_reader == null) return;
                        var message = await _reader.ReadLineAsync();

                        if (String.IsNullOrEmpty(message))
                        {
                            Task.Run(() => _closedCallback(this));
                            return;
                        }

                        _recvCallback(message, _id);
                        Console.WriteLine("{0} > {1}", _id, message);
                    }
                }
                catch (ArgumentOutOfRangeException)
                {
                    Console.WriteLine("ArgumentOutOfRangeException");
                }
                catch (ObjectDisposedException)
                {
                    Console.WriteLine("ObjectDisposedException");
                }
                catch (InvalidOperationException)
                {
                    Console.WriteLine("InvalidOperationException");            
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex);
                }
            }

            public async void Send(string message)
            {
                try
                {                    
                    if (String.IsNullOrEmpty(message)) return;
                    if (_writer == null) return;

                    await _writer.WriteLineAsync(message);                    
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex);
                }
            }

            public void Dispose()
            {
                if (_reader != null)
                    _reader.Dispose();
                if (_writer != null)
                    _writer.Dispose();
            }
        }        
    }

    public static class Logger
    {
        public static void PrintException(Exception ex, [CallerMemberName]string memberName = "")
        {
            Console.WriteLine("{0}: \n '{1}' \n '{2}'", memberName, ex.Message, ex.StackTrace);
        }
    }
}

V1: using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading.Tasks;

namespace ChatTutorial.ConsoleServer
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                var server = new ChatServer();
                server.Start();
                Console.WriteLine("Listening....");
                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex, "Main");
            }
        }
    }
    
    internal class ChatServer
    {
        private readonly TcpListener _listener = new TcpListener(IPAddress.Parse("0.0.0.0"), 7776);            
        private readonly Dictionary<string, LocalClient> _clients = new Dictionary<string, LocalClient>();

        public async void Start()
        {
            try
            {
                _listener.Start();
                while (true)
                {
                    try
                    {
                        Console.WriteLine("Listening...");
                        var client = await _listener.AcceptTcpClientAsync();

                        Console.WriteLine("Client connected");
                        var id = Guid.NewGuid().ToString();
                        _clients.Add(id, new LocalClient(client, ReceivedMessage, ClientClosed, id));
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("Start() While() '{0}', '{1}'", ex.Message, ex.StackTrace);
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex, "Start");
            }
        }

        public void ClientClosed(string id)
        {
            Console.WriteLine("Client {0} disconnected", id);
            var client = _clients[id];
            _clients.Remove(id);
            client.Close();
            Console.WriteLine("Client {0} Closed", id);
            ReceivedMessage("Client disconnected", id);
        }

        public void ReceivedMessage(string message, string id)
        {
            try
            {
                var clientsToRemove = new List<string>();
                foreach (var client in _clients)
                {
                    if (client.Value != null && client.Value.Client.Connected)
                    {
                        if (client.Key != id)
                            client.Value.Send(id + "> " + message);
                    }
                    else
                        clientsToRemove.Add(client.Key);
                }

                foreach (var client in clientsToRemove)
                    _clients.Remove(client);
            }
            catch (Exception ex)
            {
                Logger.PrintException(ex, "ReceivedMessage");
            }
        }

        internal class LocalClient
        {
            private readonly TcpClient _client;
            public TcpClient Client { get { return _client; } }

            private readonly Action<string> _closedCallback;            
            private readonly Action<string,string> _recvCallback;

            private readonly StreamReader _reader;
            private readonly StreamWriter _writer;

            private readonly string _id;
            public string Id { get { return _id; } }            

            public LocalClient(TcpClient client, Action<string,string> recvCallback, Action<string> closedCallback, string id)
            {
                _closedCallback = closedCallback;
                try
                {
                    _client = client;
                    _recvCallback = recvCallback;
                    _id = id;

                    _reader = new StreamReader(_client.GetStream());
                    _writer = new StreamWriter(_client.GetStream()) {AutoFlush = true};

                    StartReceive();

                    Console.WriteLine("Local client {0} receiving...", id);
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex, "LocalClient");
                }
            }

            public async void StartReceive()
            {
                try
                {
                    while (true)
                    {                        
                        var message = await _reader.ReadLineAsync();

                        if (String.IsNullOrEmpty(message))
                        {
                            Task.Run(() => _closedCallback(_id));
                            return;
                        }

                        _recvCallback(message, _id);
                        Console.WriteLine("{0} > {1}", _id, message);
                    }
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex, "StartReceive");
                }
            }

            public void Close()
            {
                if (_reader != null)
                    _reader.Dispose();           
            }

            public async void Send(string message)
            {
                try
                {                    
                    if (String.IsNullOrEmpty(message)) return;
                    
                    await _writer.WriteLineAsync(message);                    
                }
                catch (Exception ex)
                {
                    Logger.PrintException(ex, "Send");
                }
            }
        }
    }

    public static class Logger
    {
        public static void PrintException(Exception ex, string methodName)
        {
            Console.WriteLine("{0}: \n '{1}' \n '{2}'", methodName, ex.Message, ex.StackTrace);
        }
    }
}
jeremywho
  • 253
  • 1
  • 3
  • 10