Search code examples
c#tcpserverpacket-loss

I have a server application that gets data exactly half the time. Why/how does this happen and how do I fix it?


So my server and chat client are made from 2 different C# TCP tutorials.You may recognize 1 if not both of them and I have made my own modifications to them to fit my own style. When I tried both they worked perfectly fine with 0 loss, but my version has exactly a 50% loss rate. For instance: 1. A client connects: Data received 2. A client sends text: No Data 3. A client sends text: Data received 4. A client sends text: No Data The server code is as follows:

    using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;

namespace WindowsFormsApplication2
{
    class Server
    {
        private TcpListener tcpListener;
        private Thread listenThread;
        public Hashtable clientsList = new Hashtable();
        private System.Windows.Forms.TextBox output;
        private delegate void ObjectDelegate(String text);
        private ObjectDelegate del;

        public Server(System.Windows.Forms.TextBox setOut)
        {
            this.tcpListener = new TcpListener(IPAddress.Any, 8888);
            this.listenThread = new Thread(new ThreadStart(ListenForClients));
            this.listenThread.IsBackground = true;
            this.listenThread.Start();
            output = setOut;
            del = new ObjectDelegate(outputTextToServer);
        }

        private void ListenForClients()
        {
            this.tcpListener.Start();
            while (true)
            {
                //blocks until a client has connected to the server
                TcpClient client = this.tcpListener.AcceptTcpClient();
                //create a thread to handle communication 
                //with connected client
                addClient(client);
                Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
                clientThread.IsBackground = true;
                clientThread.Start(client);
            }
        }

        private void HandleClientComm(object client)
        {
            TcpClient tcpClient = (TcpClient)client;
            NetworkStream clientStream = tcpClient.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)
                {
                    //the client has disconnected from the server
                    break;
                }
                //message has successfully been received
                String text = getData(clientStream);
                del.Invoke(text); //Used for Cross Threading & sending text to server output
                //if filter(text)
                sendMessage(tcpClient);
                //System.Diagnostics.Debug.WriteLine(text); //Spit it out in the console
            }

            tcpClient.Close();
        }

        private void outputTextToServer(String text)
        {
            if (output.InvokeRequired)
            {
                // we then create the delegate again
                // if you've made it global then you won't need to do this
                ObjectDelegate method = new ObjectDelegate(outputTextToServer);
                // we then simply invoke it and return
                output.Invoke(method, text);
                return;
            }
            output.AppendText(Environment.NewLine + " >> " + text);
        }

        private String getData(NetworkStream stream)
        {
            int newData;
            byte[] message = new byte[4096];
            ASCIIEncoding encoder = new ASCIIEncoding();
            newData = stream.Read(message, 0, 4096);
            String text = encoder.GetString(message, 0, newData); //Translate it into text
            text = text.Substring(0, text.IndexOf("$")); //Here comes the money
            return text;
        }

        private void addClient(object client)
        {
            TcpClient tcpClient = (TcpClient)client;
            NetworkStream clientStream = tcpClient.GetStream();
            String dataFromClient = getData(clientStream);
            if (clientsList.Contains(dataFromClient))
            {
                Console.WriteLine(dataFromClient + " Tried to join chat room, but " + dataFromClient + " is already in use");
                //broadcast("A doppleganger of " + dataFromClient + " has attempted to join!", dataFromClient, false);
            }
            else
            {
                clientsList.Add(dataFromClient, tcpClient);
                //broadcast(dataFromClient + " Joined ", dataFromClient, false);
                del.Invoke(dataFromClient + " Joined chat room ");
                //handleClinet client = new handleClinet();
                //client.startClient(clientSocket, dataFromClient, clientsList);
            }
        }

        private Boolean connectionAlive(NetworkStream stream)
        {
            byte[] message = new byte[4096];
            int bytesRead = 0;
            try
            {
                //blocks until a client sends a message
                bytesRead = stream.Read(message, 0, 4096);
            }
            catch
            {
                //a socket error has occured
                return false;
            }
            if (bytesRead == 0)
            {
                //the client has disconnected from the server
                //clientsList.Remove
                return false;
            }
            return true;
        }

        private void sendMessage(TcpClient client)
        {
            NetworkStream clientStream = client.GetStream();
            ASCIIEncoding encoder = new ASCIIEncoding();
            byte[] buffer = encoder.GetBytes("Hello Client!");

            clientStream.Write(buffer, 0, buffer.Length);
            clientStream.Flush();
        }
    }
}

And here's my client code

    using System;
using System.Windows.Forms;
using System.Text;
using System.Net.Sockets;
using System.Threading;



namespace WindowsFormsApplication2
{

    public partial class Form1 : Form
    {
        public delegate void newDelegate();
        public newDelegate myDelegate;
        System.Net.Sockets.TcpClient clientSocket = new System.Net.Sockets.TcpClient();
        NetworkStream serverStream = default(NetworkStream);
        string readData = null;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            newMsg();
        }

        private void newMsg()
        {
            byte[] outStream = System.Text.Encoding.ASCII.GetBytes(textBox2.Text + "$");
            serverStream.Write(outStream, 0, outStream.Length);
            serverStream.Flush();
            textBox2.Text = "";
        }

        private void button2_Click(object sender, EventArgs e)
        {
            readData = "Connecting to Chat Server ...";
            msg();
            clientSocket.Connect(txtIP.Text, int.Parse(txtPort.Text));
            serverStream = clientSocket.GetStream();

            byte[] outStream = System.Text.Encoding.ASCII.GetBytes(txtName.Text + "$");
            serverStream.Write(outStream, 0, outStream.Length);
            serverStream.Flush();

            myDelegate = new newDelegate(disconnect);
            Thread ctThread = new Thread(getMessage);
            ctThread.IsBackground = true;
            ctThread.Start();
            button2.Enabled = false;
        }

        private void getMessage()
        {
            while (true)
            {
                serverStream = clientSocket.GetStream();
                int buffSize = 0;
                byte[] inStream = new byte[clientSocket.ReceiveBufferSize];
                buffSize = clientSocket.ReceiveBufferSize;
                try
                {
                    serverStream.Read(inStream, 0, buffSize);
                    string returndata = System.Text.Encoding.ASCII.GetString(inStream);
                    readData = "" + returndata;
                    msg();
                }
                catch
                {
                    Invoke(myDelegate);
                    return;
                }
            }
        }

        private void disconnect()
        {
            button2.Enabled = true;
        }

        private void msg()
        {
            if (this.InvokeRequired)
                this.Invoke(new MethodInvoker(msg));
            else
                textBox1.AppendText(Environment.NewLine + " >> " + readData);
            //textBox1.Text = textBox1.Text + Environment.NewLine + " >> " + readData;
        }

        private void textBox2_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                newMsg();
            }
        }

        private void cmdHost_Click(object sender, EventArgs e)
        {
            Server serv = new Server(txtLog);
        }
    }

}

This code is obviously a work in progress and sorry in advance for messiness. Any other suggestions to the code are also welcome.


Solution

  • Okay, this is starting to get a bit long.

    There's multiple errors in your code. Starting with the server code:

    • As Damien noted, you're trying to read each "message" twice - first in HandleClientComm, then again in getData. The stream no longer has the original data, so you're just throwing one of the reads away completely (thus the suspicious 50% "packet" loss)
    • Later, in getData, you discard all the data in the stream after the first $. While this is obviously an attempt at handling message framing (since TCP is a stream-based protocol, not a message-based protocol), it's a silly one - you're throwing the data away. The reason this didn't show in your testing is that 1) Windows treats local TCP very differently from remote TCP, 2) You'd need to be actually able to send two messages fast enough to make them "blend" together in the stream. That means either sending two messages in about 200ms (default TCP buffering) or blocking on reads.
    • You keep Flushing the network stream. This doesn't actually do anything, and even if it did, you wouldn't want to do that.
    • connectionAlive reads from the shared socket - this is always a bad idea. Never have more than one reader - multiple readers don't work with stream-based protocols. It doesn't seem you're using it in your sample code, but beware of trying to.
    • The commented out clientList.Remove would of course be a cross-thread access of a shared field. If you want to do it like this, you'll have to ensure the concurrent access is safe - either by using ConcurrentDictionary instead of HashSet, or by locking around each write and read of clientList.
    • You're expecting to get the whole message in one Read. That may be fine for a simple chat client, but it's bad TCP anyway - you need to read until you find your message terminator. If I send a message big enough, your code will just drop dead on text.IndexOf("$").

    There's a lot of "style" issues as well, although this isn't code review, so let me just list some: using ancient technology, synchronous sockets for the server, mixing multi-threaded code with GUI at will. This is mostly about maintainability and performance, though - not correctness.

    Now, the client is a bit simpler:

    • Again, don't Flush the network stream.
    • Don't use background threads if you don't have to. Simply make sure to terminate the connections etc. properly.
    • Disconnect should actually disconnect. It's not that hard, just close the TcpClient.
    • What is readData = "" + returndata supposed to do? That's just silly.
    • You're ignoring the return value of Read. This means that you have no idea how many bytes of data you read - which means your returnData string actually contains the message followed by a few thousand \0 characters. The only reason you don't see them in output is because most of Windows uses \0 as the string terminator ("it made sense at the time"). .NET doesn't.
    • Again, the Read expects the whole message at once. Unlike the server, this isn't going to crash the client, but your code will behave differently (e.g. an extra \r\n >> even though it's not a separate message.

    The style issues from the server also apply here.

    As a side-note, I've recently made a simplified networking sample that handles a simple chat client-server system using more modern technologies - using await-based asynchronous I/O instead of multi-threading, for example. It's not production-ready code, but it should show the ideas and intent quite clearly (I also recommend having a look at the first sample, "HTTP-like TCP communication"). You can find the full source code here - Networking Part 2.