简体   繁体   中英

Sending data over NetworkStream using multiple threads

I'm trying to build a command line chat room where the server is handling the connections and repeating input from one client back to all the other clients. Currently the server is able to take in input from multiple clients, but can only send information back to those clients individually. I think my problem is that each connection is being handled on an individual thread. How would I allow for the threads to communicate with each other or be able to send data to each thread?

Server code:

namespace ConsoleApplication
{


    class TcpHelper
    {


        private static object _lock = new object();
        private static List<Task> _connections = new List<Task>();


        private static TcpListener listener { get; set; }
        private static bool accept { get; set; } = false;

        private static Task StartListener()
        {
            return Task.Run(async () =>
            {
                IPAddress address = IPAddress.Parse("127.0.0.1");
                int port = 5678;
                listener = new TcpListener(address, port);

                listener.Start();

                Console.WriteLine($"Server started. Listening to TCP clients at 127.0.0.1:{port}");

                while (true)
                {
                    var tcpClient = await listener.AcceptTcpClientAsync();
                    Console.WriteLine("Client has connected");
                    var task = StartHandleConnectionAsync(tcpClient);
                    if (task.IsFaulted)
                        task.Wait();
                }
            });
        }

        // Register and handle the connection
        private static async Task StartHandleConnectionAsync(TcpClient tcpClient)
        {
            // start the new connection task
            var connectionTask = HandleConnectionAsync(tcpClient);



            // add it to the list of pending task 
            lock (_lock)
                _connections.Add(connectionTask);

            // catch all errors of HandleConnectionAsync
            try
            {
                await connectionTask;

            }
            catch (Exception ex)
            {
                // log the error
                Console.WriteLine(ex.ToString());
            }
            finally
            {
                // remove pending task
                lock (_lock)
                    _connections.Remove(connectionTask);
            }
        }






        private static async Task HandleConnectionAsync(TcpClient client)
        {

            await Task.Yield();


            {
                using (var networkStream = client.GetStream())
                {

                    if (client != null)
                    {
                        Console.WriteLine("Client connected. Waiting for data.");



                        StreamReader streamreader = new StreamReader(networkStream);
                        StreamWriter streamwriter = new StreamWriter(networkStream);

                        string clientmessage = "";
                        string servermessage = "";


                        while (clientmessage != null && clientmessage != "quit")
                        {
                            clientmessage = await streamreader.ReadLineAsync();
                            Console.WriteLine(clientmessage);
                            servermessage = clientmessage;
                            streamwriter.WriteLine(servermessage);
                            streamwriter.Flush();


                        }
                        Console.WriteLine("Closing connection.");
                        networkStream.Dispose();
                    }
                }

            }

        }
        public static void Main(string[] args)
        {
            // Start the server 

            Console.WriteLine("Hit Ctrl-C to close the chat server");
            TcpHelper.StartListener().Wait();

        }

    }

}

Client Code:

namespace Client2
{
    public class Program
    {

        private static void clientConnect()
        {
            TcpClient socketForServer = new TcpClient();
            bool status = true;
            string userName;
            Console.Write("Input Username: ");
            userName = Console.ReadLine();

            try
            {
                IPAddress address = IPAddress.Parse("127.0.0.1");
                socketForServer.ConnectAsync(address, 5678);
                Console.WriteLine("Connected to Server");
            }
            catch
            {
                Console.WriteLine("Failed to Connect to server{0}:999", "localhost");
                return;
            }
            NetworkStream networkStream = socketForServer.GetStream();
            StreamReader streamreader = new StreamReader(networkStream);
            StreamWriter streamwriter = new StreamWriter(networkStream);
            try
            {
                string clientmessage = "";
                string servermessage = "";
                while (status)
                {
                    Console.Write(userName + ": ");
                    clientmessage = Console.ReadLine();
                    if ((clientmessage == "quit") || (clientmessage == "QUIT"))
                    {
                        status = false;
                        streamwriter.WriteLine("quit");
                        streamwriter.WriteLine(userName + " has left the conversation");
                        streamwriter.Flush();

                    }
                    if ((clientmessage != "quit") && (clientmessage != "quit"))
                    {
                        streamwriter.WriteLine(userName + ": " + clientmessage);
                        streamwriter.Flush();
                        servermessage = streamreader.ReadLine();
                        Console.WriteLine("Server:" + servermessage);
                    }
                }
            }
            catch
            {
                Console.WriteLine("Exception reading from the server");
            }
            streamreader.Dispose();
            networkStream.Dispose();
            streamwriter.Dispose();
        }
        public static void Main(string[] args)
        {
            clientConnect();
        }
    }
}

The main thing wrong in your code is that you make no attempt to send data received from one client to the other connected clients. You have the _connections list in your server, but the only thing stored in the list are the Task objects for the connections, and you don't even do anything with those.

Instead, you should maintain a list of the connections themselves, so that when you received a message from one client, you can then retransmit that message to the other clients.

At a minimum, this should be a List<TcpClient> , but because you are using StreamReader and StreamWriter , you'll want to initialize and store those objects in the list as well. In addition, you should include a client identifier. One obvious choice for this would be the name of the client (ie what the user enters as their name), but your example doesn't provide any mechanism in the chat protocol to transmit that identification as part of the connection initialization, so in my example (below) I just use a simple integer value.

There are some other irregularities in the code you posted, such as:

  • Starting a task in a brand new thread, just to execute a few statements that get you to the point of initiating an asynchronous operation. In my example, I simply omit the Task.Run() part of the code, as it's not needed.
  • Checking the connection-specific task when it's returned for IsFaulted . Since it's unlikely any I/O will actually have occurred by the time this Task object is returned, this logic has very little use. The call to Wait() will throw an exception, which will propagate to the main thread's Wait() call, terminating the server. But you don't terminate the server in the event of any other error, so it's not clear why you'd want to do that here.
  • There's a spurious call to Task.Yield() . I have no idea what you're trying to accomplish there, but whatever it is, that statement isn't useful. I simply removed it.
  • In your client code, you only attempt to receive data from the server when you've sent data. This is very wrong; you want clients to be responsive and receive data as soon as it's sent to them. In my version, I included a simple little anonymous method that is called immediately to start a separate message-receiving loop that will execute asynchronously and concurrently with the main user input loop.
  • Also in the client code, you were sending the "…has left…" message after the "quit" message that would cause the server to close the connection. This means that the server would never actually receive the "…has left…" message. I reversed the order of the messages so that "quit" is always the last thing the client ever sends.

My version looks like this:

Server:

class TcpHelper
{
    class ClientData : IDisposable
    {
        private static int _nextId;

        public int ID { get; private set; }
        public TcpClient Client { get; private set; }
        public TextReader Reader { get; private set; }
        public TextWriter Writer { get; private set; }

        public ClientData(TcpClient client)
        {
            ID = _nextId++;
            Client = client;

            NetworkStream stream = client.GetStream();

            Reader = new StreamReader(stream);
            Writer = new StreamWriter(stream);
        }

        public void Dispose()
        {
            Writer.Close();
            Reader.Close();
            Client.Close();
        }
    }

    private static readonly object _lock = new object();
    private static readonly List<ClientData> _connections = new List<ClientData>();

    private static TcpListener listener { get; set; }
    private static bool accept { get; set; }

    public static async Task StartListener()
    {
        IPAddress address = IPAddress.Any;
        int port = 5678;
        listener = new TcpListener(address, port);

        listener.Start();

        Console.WriteLine("Server started. Listening to TCP clients on port {0}", port);

        while (true)
        {
            var tcpClient = await listener.AcceptTcpClientAsync();
            Console.WriteLine("Client has connected");
            var task = StartHandleConnectionAsync(tcpClient);
            if (task.IsFaulted)
                task.Wait();
        }
    }

    // Register and handle the connection
    private static async Task StartHandleConnectionAsync(TcpClient tcpClient)
    {
        ClientData clientData = new ClientData(tcpClient);

        lock (_lock) _connections.Add(clientData);

        // catch all errors of HandleConnectionAsync
        try
        {
            await HandleConnectionAsync(clientData);
        }
        catch (Exception ex)
        {
            // log the error
            Console.WriteLine(ex.ToString());
        }
        finally
        {
            lock (_lock) _connections.Remove(clientData);
            clientData.Dispose();
        }
    }

    private static async Task HandleConnectionAsync(ClientData clientData)
    {
        Console.WriteLine("Client connected. Waiting for data.");

        string clientmessage;

        while ((clientmessage = await clientData.Reader.ReadLineAsync()) != null && clientmessage != "quit")
        {
            string message = "From " + clientData.ID + ": " + clientmessage;

            Console.WriteLine(message);

            lock (_lock)
            {
                // Locking the entire operation ensures that a) none of the client objects
                // are disposed before we can write to them, and b) all of the chat messages
                // are received in the same order by all clients.
                foreach (ClientData recipient in _connections.Where(r => r.ID != clientData.ID))
                {
                    recipient.Writer.WriteLine(message);
                    recipient.Writer.Flush();
                }
            }
        }
        Console.WriteLine("Closing connection.");
    }
}

Client:

class Program
{
    private const int _kport = 5678;

    private static async Task clientConnect()
    {
        IPAddress address = IPAddress.Loopback;
        TcpClient socketForServer = new TcpClient();
        string userName;
        Console.Write("Input Username: ");
        userName = Console.ReadLine();

        try
        {
            await socketForServer.ConnectAsync(address, _kport);
            Console.WriteLine("Connected to Server");
        }
        catch (Exception e)
        {
            Console.WriteLine("Failed to Connect to server {0}:{1}", address, _kport);
            return;
        }


        using (NetworkStream networkStream = socketForServer.GetStream())
        {
            var readTask = ((Func<Task>)(async () =>
            {
                using (StreamReader reader = new StreamReader(networkStream))
                {
                    string receivedText;

                    while ((receivedText = await reader.ReadLineAsync()) != null)
                    {
                        Console.WriteLine("Server:" + receivedText);
                    }
                }
            }))();

            using (StreamWriter streamwriter = new StreamWriter(networkStream))
            {
                try
                {
                    while (true)
                    {
                        Console.Write(userName + ": ");
                        string clientmessage = Console.ReadLine();
                        if ((clientmessage == "quit") || (clientmessage == "QUIT"))
                        {
                            streamwriter.WriteLine(userName + " has left the conversation");
                            streamwriter.WriteLine("quit");
                            streamwriter.Flush();
                            break;
                        }
                        else
                        {
                            streamwriter.WriteLine(userName + ": " + clientmessage);
                            streamwriter.Flush();
                        }
                    }

                    await readTask;
                }
                catch (Exception e)
                {
                    Console.WriteLine("Exception writing to server: " + e);
                    throw;
                }
            }
        }
    }

    public static void Main(string[] args)
    {
        clientConnect().Wait();
    }
}

There is still a lot you'll need to work on. You'll probably want to implement proper initialization of chat user names on the server side. At the very least, for real-world code you'd want to do more error checking, and make sure the client ID is generated reliably (if you only want positive ID values, you can't have more than 2^31-1 connections before it rolls back over to 0 ).

I also made some other minor changes that weren't strictly necessary, such as using the IPAddress.Any and IPAddress.Loopback values instead of parsing strings, and just generally simplifying and cleaning up the code here and there. Also, I'm not using a C# 6 compiler at the moment, so I changed the code where you were using C# 6 features so that it would compile using C# 5 instead.

To do a full-blown chat server, you still have your work cut out for you. But I hope that the above gets you back on the right track.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM