NetworkStream Receive, how to processing data without using 100% CPU?

1.4k views Asked by At

I have a small game server I'm making that will have dozens of connections sending player data constantly. While I've finally accomplished some basics and now have data sending/receiving, I now face a problem of flooding the server and the client with too much data. I've tried to throttle it back but even then I am hitting 90-100% cpu simply because of receiving and processing the data received running up the CPU.

The method below is a bare version of receiving data from the server. The server sends a List of data to be received by the player, then it goes through that list. I've thought perhaps instead just using a dictionary with a key based on type rather than for looping but I don't think that will significantly improve it, the problem is that it is processing data non-stop because player positions are constantly being updated, sent to the server, then send to other players.

The code below shows receive for the client, the server receive looks very similar. How might I begin to overcome this issue? Please be nice, I am still new to network programming.

private void Receive(System.Object client)
    {
        MemoryStream memStream = null;
        TcpClient thisClient = (TcpClient)client;
        List<System.Object> objects = new List<System.Object>();
        while (thisClient.Connected && playerConnected == true)
        {
            try
            {
                do
                {
                    //when receiving data, first comes length then comes the data

                    byte[] buffer = GetStreamByteBuffer(netStream, 4); //blocks while waiting for data
                    int msgLenth = BitConverter.ToInt32(buffer, 0);
                    if (msgLenth <= 0)
                    {
                        playerConnected = false;
                        thisClient.Close();
                        break;
                    }
                    if (msgLenth > 0)
                    {   
                        buffer = GetStreamByteBuffer(netStream, msgLenth);
                        memStream = new MemoryStream(buffer);
                    }

                } while (netStream.DataAvailable);
                if (memStream != null)
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    memStream.Position = 0;
                    objects = new List<System.Object>((List<System.Object>)formatter.Deserialize(memStream));
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception: " + ex.ToString());
                if (thisClient.Connected == false)
                {
                    playerConnected = false;
                    netStream.Close();
                    thisClient.Close();
                    break;
                }
            }
            try
            {
                if (objects != null)
                {
                    for (int i = 0; i < objects.Count; i++)
                    {
                        if(objects[i] != null)
                        {
                            if (objects[i].GetType() == typeof(GameObject))
                            {
                                GameObject p = (GameObject)objects[i];
                                GameObject item;
                                if (mapGameObjects.TryGetValue(p.objectID, out item))
                                {
                                    mapGameObjects[p.objectID] = p;;
                                }
                                else
                                {
                                    mapGameObjects.Add(p.objectID, p);
                                }

                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception " + ex.ToString());
                if (thisClient.Connected == false)
                {
                    playerConnected = false;
                    netStream.Close();
                    break;
                }
            }
        }
        Console.WriteLine("Receive thread closed for client.");
    }
    public static byte[] GetStreamByteBuffer(NetworkStream stream, int n)
    {
        byte[] buffer = new byte[n];
        int bytesRead = 0;
        int chunk = 0;
        while (bytesRead < n)
        {
            chunk = stream.Read(buffer, (int)bytesRead, buffer.Length - (int)bytesRead);
            if (chunk == 0)
            {
                break;
            }
            bytesRead += chunk;
        }
        return buffer;
    }
5

There are 5 answers

2
hcb On

You need to put a Thread.Sleep(10) in your while loop. This is also a very fragile way to receive tcp data because it assumes the other side has sent all data before you call this receive. If the other side has only sent half of the data this method fails. This can be countered by either sending fixed sized packages or sending the length of a package first.

1
vgru On

Polling is rarely a good approach to communication, unless you're programming 16-bit microcontrollers (and even then, probably not the best solution).

What you need to do is to switch to a producer-consumer pattern, where your input port (a serial port, an input file, or a TCP socket) will act as a producer filling a FIFO buffer (a queue of bytes), and some other part of your program will be able to asynchronously consume the enqueued data.

In C#, there are several ways to do it: you can simply write a couple of methods using a ConcurrentQueue<byte>, or a BlockingCollection, or you can try a library like the TPL Dataflow Library which IMO doesn't add too much value over existing structures in .NET 4. Prior to .NET 4, you would simply use a Queue<byte>, a lock, and a AutoResetEvent to do the same job.

So the general idea is:

  1. When your input port fires a "data received" event, enqueue all received data into the FIFO buffer and set a synchronization event to notify the consumer,
  2. In your consumer thread, wait for the synchonization event. When the signal is received, check if there is enough data in the queue. If yes, process it, if not, continue waiting for the next signal.
  3. For robustness, use an additional watchdog timer (or simply "time since last received data") to be able to fail on timeout.
2
Peter Duniho On

Based on the code shown, I can't say why the CPU utilization is high. The loop will wait for data, and the wait should not consume CPU. That said, it still polls the connection in checking the DataAvailable property, which is inefficient and can cause you to ignore received data (in the implementation shown...that's not an inherent problem with DataAvailable).

I'll go one further than the other answer and state that you should simply rewrite the code. Polling the socket is just no way to handle network I/O. This would be true in any scenario, but it is especially problematic if you are trying to write a game server, because you're going to use up a lot of your CPU bandwidth needlessly, taking it away from game logic.

The two biggest changes you should make here are:

  • Don't use the DataAvailable property. Ever. Instead, use one of the asynchronous APIs for dealing with network I/O. My favorite approach with the latest .NET is to wrap the Socket in a NetworkStream (or get the NetworkStream from a TcpClient as you do in your code) and then use the Stream.ReadAsync() along with async and await. But the older asynchronous APIs for Sockets work well also.

  • Separate your network I/O code from the game logic code. The Receive() method you show here has both the I/O and the actual processing of the data relative to the game state in the same method. This two pieces of functionality really belong in two separate classes. Keep both classes, and especially the interface between them, very simple and the code will be a lot easier to write and to maintain.

If you decide to ignore all of the above, you should at least be aware that your GetStreamByteBuffer() method has a bug in it: if you reach the end of the stream before reading as many bytes were requested, you still return a buffer as large as was requested, with no way for the caller to know the buffer is incomplete.

And finally, IMHO you should be more careful about how you shutdown and close the connection. Read about "graceful closure" for the TCP protocol. It's important that each end signal that they are done sending, and that each end receive the other end's signal, before either end actually closes the connection. This will allow the underlying networking protocol to release resources as efficiently and as quickly as possible. Note that TcpClient exposes the socket as the Client property, which you can use to call Shutdown().

4
Jodrell On

You want to use the Task-based Asynchronous Pattern. Probably making liberal use of the async function modifier and the await keyword.

You'd be best replacing GetStreamByteBuffer with a direct call to ReadAsync.

For instance you could asynchronously read from a stream like this.

private static async Task<T> ReadAsync<T>(
        Stream source,
        CancellationToken token)
{
    int requestLength;
    {
        var initialBuffer = new byte[sizeof(int)];
        var readCount = await source.ReadAsync(
                                      initialBuffer,
                                      0,
                                      sizeof(int),
                                      token);

        if (readCount != sizeof(int))
        {
            throw new InvalidOperationException(
                "Not enough bytes in stream to read request length.");
        }

        requestLength = BitConvertor.ToInt32(initialBuffer, 0);
    }

    var requestBuffer = new byte[requestLength];
    var bytesRead = await source.ReadAsync(
                                   requestBuffer,
                                   0,
                                   requestLength,
                                   token);

    if (bytesRead != requestLength)
    {
        throw new InvalidDataException(
            string.Format(
                "Not enough bytes in stream to match request length." + 
                    " Expected:{0}, Actual:{1}",
                requestLength,
                bytesRead));
    }

    var serializer = new BinaryFormatter();
    using (var requestData = new MemoryStream(requestBuffer))
    {
        return (T)serializer.Deserialize(requestData);
    }
}

Like your code this reads an int from the stream to get the length, then reads that number of bytes and uses the BinaryFormatter to deserialize the data to the specified generic type.

Using this generic function you can simplify your logic,

private Task Receive(
        TcpClient thisClient,
        CancellationToken token)
    {
        IList<object> objects;
        while (thisClient.Connected && playerConnected == true)
        {
            try
            {
                objects = ReadAsync<List<object>>(netStream, token);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception: " + ex.ToString());
                if (thisClient.Connected == false)
                {
                    playerConnected = false;
                    netStream.Close();
                    thisClient.Close();
                    break;
                }
            }

            try
            {
                foreach (var p in objects.OfType<GameObject>())
                {
                    if (p != null)
                    {
                        mapGameObjects[p.objectID] = p;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception " + ex.ToString());
                if (thisClient.Connected == false)
                {
                    playerConnected = false;
                    netStream.Close();
                    break;
                }
            }
        }

        Console.WriteLine("Receive thread closed for client.");
    }
0
dvasanth On

Your player position update is similar to the framebuffer update in the VNC protocol where the client request a screen frame & server responds to it with the updated screen data. But there is one exception, VNC server doesn't blindly send the new screen it only sends the changes if there is one. So you need to change the logic from sending all the requested list of objects to only to the objects which are changed after the last sent. Also in addition to it, you should send entire object only once after that send only the changed properties, this will greatly reduce the size of data sent & processed both at clients & server.