2014-11-17 3 views
1

У меня есть небольшой игровой сервер, который я создаю, и будет иметь десятки соединений, которые постоянно отправляют данные игрока. В то время как я, наконец, выполнил некоторые основы и теперь имею передачу/получение данных, теперь я сталкиваюсь с проблемой наводнения сервера и клиента с большим количеством данных. Я попытался отключить его, но даже тогда я поражаю 90-100% процессора просто из-за приема и обработки полученных данных, запускающих процессор.NetworkStream Receive, как обрабатывать данные без использования 100% -ного процессора?

Метод ниже - голая версия приема данных с сервера. Сервер отправляет список данных, которые должен быть получен игроком, затем он проходит через этот список. Я подумал, что, возможно, вместо этого просто использовал словарь с ключом, основанным на типе, а не для цикла, но я не думаю, что это значительно улучшит его, проблема в том, что он обрабатывает данные без остановок, потому что позиции игроков постоянно обновляются , отправляется на сервер, а затем отправляется другим игрокам.

Код ниже показывает получение для клиента, получение сервера выглядит очень похоже. Как я могу начать эту проблему? Пожалуйста, будьте добры, я все еще новичок в сетевом программировании.

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; 
    } 
+2

Моя ставка заключается в том, что вам нужно немного поспать. – luk32

+0

:(Работа не заканчивается, пока я сплю – Euthyphro

+0

Я сомневаюсь, что сеть - это то, что заставляет процессор переходить на 100%, это, вероятно, обработка, которую вы делаете после получения данных. Но это то, что я не могу ответить, потому что могу Я читаю ваш код и понимаю, что вы пытались сделать и что может быть избыточным. Предлагаю вам начать оптимизацию того, что вы считаете самой тяжелой частью, и вернуться сюда и задать новый вопрос, когда вы сталкиваетесь с чем-то, что не можете решить. –

ответ

0

Вам необходимо поставить Thread.Sleep (10) в цикл while. Это также очень хрупкий способ получения данных tcp, поскольку он предполагает, что другая сторона отправила все данные, прежде чем вы вызываете это получение. Если другая сторона отправила только половину данных, этот метод выходит из строя. Этому можно противопоставить либо отправку пакетов фиксированного размера, либо отправку длины пакета в первую очередь.

+0

Используйте Thread.Sleep экономно: [«Почему Thread.Sleep настолько вреден»] (http://stackoverflow.com/a/8815944/69809) или [«Зачем использовать System.Threading.Thread.Sleep() - это плохая практика?»] (http://stackoverflow.com/a/19996393/69809). – Groo

+0

@Groo, действительно. 'Thread.Sleep()' плохо. Если вам действительно нужна задержка, используйте 'Task.Delay()', но это необязательно здесь. – Jodrell

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. Для обеспечения надежности используйте дополнительный сторожевой таймер (или просто «время с момента последнего получения данных»), чтобы иметь возможность сбой при тайм-ауте.
+0

+1 потребитель-производитель именно то, что здесь происходит. http://msdn.microsoft.com/en-us/library/hh228601(v=vs.110).aspx – Jodrell

2

На основании приведенного кода я не могу сказать, почему загрузка процессора высока. Цикл будет ждать данных, и ожидание не должно потреблять процессор. Тем не менее, он по-прежнему проверяет соединение при проверке свойства DataAvailable, что является неэффективным и может привести к игнорированию полученных данных (в показанной реализации ... это не является неотъемлемой проблемой с DataAvailable).

Я пойду дальше другого ответа и заявлю, что вы должны просто переписать код.Опрос сокета - это просто не способ работать с сетевым вводом/выводом. Это было бы справедливо в любом сценарии, но особенно проблематично, если вы пытаетесь написать игровой сервер, потому что вы собираетесь использовать большую часть своей пропускной способности вашего процессора без необходимости, отвлекая его от игровой логики.

Две большие изменения, которые вы должны сделать здесь:

  • Не используйте DataAvailable свойство. Когда-либо. Вместо этого используйте один из асинхронных API для работы с сетевым вводом-выводом. Мой любимый подход с последним .NET заключается в том, чтобы обернуть Socket в NetworkStream (или получить NetworkStream из TcpClient, как и в вашем коде), а затем использовать Stream.ReadAsync() вместе с async и await. Но старые асинхронные API-интерфейсы для Socket также хорошо работают.

  • Отделите свой код ввода/вывода от логического кода игры. Выбранный здесь метод Receive() имеет как ввод-вывод, так и фактическую обработку данных относительно состояния игры в том же методе. Эти две части функциональности действительно принадлежат к двум отдельным классам. Держите оба класса, и особенно интерфейс между ними, очень простыми, и код будет намного легче писать и поддерживать.

Если вы решили игнорировать все вышесказанное, вы должны по крайней мере знать, что ваш метод GetStreamByteBuffer() есть ошибка в нем: если вы дойдете до конца потока, прежде чем читать столько байт, было предложено, вы по-прежнему возвращать буфер, размер которого был запрошен, а не для того, чтобы вызывающий пользователь знал, что буфер неполный.

И, наконец, ИМХО, вы должны быть более осторожны, как вы выключаете и закрываете соединение. Читайте о «изящном закрытии» для протокола TCP. Важно, чтобы каждый конечный сигнал, что они были отправлены, и что каждый конец принимает сигнал другого конца, до того, как любой конец фактически завершит соединение. Это позволит базовому сетевому протоколу выпускать ресурсы как можно быстрее и быстрее. Обратите внимание, что TcpClient предоставляет сокет в качестве свойства Client, которое вы можете использовать для звонка Shutdown().

+1

, но stream.Read - это блокирующий синхронный вызов, поэтому CPU ** не ** вращается вокруг, когда нет данных. Он сидит в ожидании обратного вызова IOCompletion? –

+0

Да, я не знаю, о чем я думал. Может быть, в моем лишенном сна состоянии, смущенное время/время? Я не вижу хорошего объяснения высокой загрузки процессора ... остальная часть ответа остается в силе IMHO –

1

Вы хотите использовать Task-based Asynchronous Pattern. Вероятно, либеральное использование модификатора функции async и ключевого слова await.

Вы бы лучше всего заменили GetStreamByteBuffer с помощью прямого звонка в ReadAsync.

Например, вы можете асинхронно читать из потока, как это.

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

Как это реально помогает? –

+0

@JamesBarrass Это помогает «переработать код, чтобы разделить полученные данные и обрабатывать их». Поэтому он делает хороший шаг вперед к образцу производителя. – Jodrell

+0

Это помогает, поэтому я удалю свой другой комментарий, но я не думаю, что он обращается к проблеме производительности –

0

Ваше обновление позиция игрока похож на обновление фреймбуфера в протоколе VNC, где запрос клиента экран кадра & сервер отвечает на него с обновленной экранные данные. Но есть одно исключение: сервер VNC не слепо посылает новый экран, он отправляет изменения только в том случае, если он есть.Таким образом, вам нужно изменить логику от отправки всего запрошенного списка объектов только к объектам, которые были изменены после последнего отправленного. Кроме того, в дополнение к этому вы должны отправлять весь объект только один раз после этого, отправляя только измененные свойства, это значительно уменьшит размер отправляемых данных &, обработанных как на серверах клиентов &.