NetworkStream Receive,如何在不使用100%CPU的情况下处理数据?

时间:2014-11-17 08:25:08

标签: c# tcpclient networkstream

我有一个小型游戏服务器我将会有数十个连接不断发送玩家数据。虽然我终于完成了一些基础知识并且现在有数据发送/接收,但我现在面临着服务器和客户端充斥着太多数据的问题。我试图将其限制回来,但即便如此,我只是因为接收和处理运行CPU的数据而达到了90-100%的cpu。

以下方法是从服务器接收数据的裸版本。服务器发送一个由播放器接收的数据列表,然后通过该列表。我想也许只是使用一个带有基于类型的键而不是循环的字典,但我不认为这会显着改善它,问题是它不停地处理数据,因为玩家的位置不断更新,发送到服务器,然后发送给其他玩家。

下面的代码显示了客户端的接收,服务器接收看起来非常相似。我怎么能开始克服这个问题呢?请你好,我还是网络编程的新手。

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 个答案:

答案 0 :(得分:2)

根据显示的代码,我不能说为什么CPU利用率很高。循环将等待数据,等待不应消耗CPU。也就是说,它仍然在检查DataAvailable属性时轮询连接,这是低效的,并且可能导致您忽略收到的数据(在显示的实现中......这不是DataAvailable的固有问题)。

我会比另一个答案更进一步说明你应该简单地重写代码。轮询套接字无法处理网络I / O.在任何情况下都是如此,但如果您正在尝试编写游戏服务器,则会出现问题,因为您将不必要地耗尽大量CPU带宽,将其从游戏逻辑中移除。

你应该在这里做出两个最大的改变:

  • 请勿使用DataAvailable属性。永远。相反,使用其中一个异步API来处理网络I / O.我最喜欢使用最新.NET的方法是将Socket包裹在NetworkStream中(或者从NetworkStream获取TcpClient,就像在代码中一样)然后使用Stream.ReadAsync()以及asyncawait。但Socket的旧异步API也可以正常工作。

  • 将您的网络I / O代码与游戏逻辑代码分开。您在此处显示的Receive()方法在同一方法中具有相对于游戏状态的I / O和实际数据处理。这两个功能实际上属于两个单独的类。保持这两个类,特别是它们之间的接口,非常简单,代码编写和维护起来会更容易。

如果你决定忽略上述所有内容,你至少应该知道你的GetStreamByteBuffer()方法中有一个错误:如果你在读取之前到达流的末尾,那么请求的字节数就是你的仍然返回一个尽可能大的缓冲区,调用者无法知道缓冲区是否不完整。

最后,恕我直言,你应该更加小心你如何关闭和关闭连接。阅读TCP协议的“优雅闭包”。重要的是,在任一端实际关闭连接之前,它们完成发送的每个结束信号以及每个端接收另一端的信号。这将允许底层网络协议尽可能有效和快速地释放资源。请注意,TcpClient会将套接字公开为Client属性,您可以使用该属性来调用Shutdown()

答案 1 :(得分:1)

轮询很少是一种很好的通信方法,除非您正在编程16位微控制器(即便如此,可能不是最好的解决方案)。

您需要做的是切换到生产者 - 消费者模式,其中您的输入端口(串行端口,输入文件或TCP套接字)将充当填充FIFO缓冲区(字节队列)的生产者),程序的其他一些部分将能够异步使用排队数据。

在C#中,有几种方法可以实现:you can simply write a couple of methods using a ConcurrentQueue<byte>, or a BlockingCollection,或者您可以尝试像TPL Dataflow Library这样的库,IMO不会为.NET 4中的现有结构增加太多的价值。在.NET 4之前,您只需使用Queue<byte>,锁定和AutoResetEvent来完成相同的工作。

所以一般的想法是:

  1. 当您的输入端口触发“收到数据”事件时,将所有收到的数据排入FIFO缓冲区并设置同步事件以通知消费者,
  2. 在您的消费者线程中,等待同步事件。收到信号后,检查队列中是否有足够的数据。如果是,则处理它,如果没有,继续等待下一个信号。
  3. 为了增强稳定性,请使用额外的看门狗定时器(或简称“自上次接收数据以来的时间”),以便能够在超时时失败。

答案 2 :(得分:1)

您想使用Task-based Asynchronous Pattern。可能会自由使用async函数修饰符和await关键字。

您最好直接调用ReadAsync替换GetStreamByteBuffer

例如,你可以从这样的流中异步读取。

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);
    }
}

与您的代码类似,它从流中读取int以获取长度,然后读取该字节数并使用BinaryFormatter将数据反序列化为指定的泛型类型。

使用此通用功能可以简化逻辑,

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.");
    }

答案 3 :(得分:0)

你需要在你的while循环中放一个Thread.Sleep(10)。这也是接收tcp数据的一种非常脆弱的方式,因为它假定另一方在调用此接收之前已发送所有数据。如果另一方只发送了一半数据,则该方法失败。这可以通过发送固定大小的包或首先发送包的长度来抵消。

答案 4 :(得分:0)

您的播放器位置更新类似于VNC协​​议中的帧缓冲更新,其中客户端请求屏幕框架&amp;服务器使用更新的屏幕数据响应它。但有一个例外,VNC服务器不会盲目地发送新屏幕它只发送更改(如果有的话)。因此,您需要将逻辑从发送所有请求的对象列表更改为仅在上次发送后更改的对象。另外,除了它之外,你应该只发送一次整个对象,之后只发送更改的属性,这将大大减少发送的数据的大小。在客户和客户处理服务器

相关问题