Почему мой DeflateStream неправильно получает данные по TCP?

У меня есть класс TcpClient для настройки клиента и сервера на моей локальной машине. Я использую сетевой поток для облегчения обмена данными между ними.

Двигаясь вперед, я пытаюсь реализовать сжатие в сообщениях. Я пробовал GZipStream и DeflateStream. Я решил сосредоточиться на DeflateStream. Однако сейчас соединение зависает без чтения данных.

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

Клиент разбит на этот запрос: есть 2 отдельные реализации, одна со стримрайтером, другая без.

textToSend = ENQUIRY + START_OF_TEXT + textToSend + END_OF_TEXT;

// Send XML Request
byte[] request = Encoding.UTF8.GetBytes(textToSend);

using (DeflateStream streamOut = new DeflateStream(netStream, CompressionMode.Compress, true))
{
    //using (StreamWriter sw = new StreamWriter(streamOut))
    //{
    //    sw.Write(textToSend);
    //    sw.Flush();
    streamOut.Write(request, 0, request.Length);
    streamOut.Flush();
    //}
}

Сервер получает запрос, и я делаю
1.) быстрое чтение первого символа, затем, если он соответствует тому, что я ожидаю
2.) я продолжаю читать остальные.

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

Когда я пытаюсь продолжить чтение потока, данные для чтения отсутствуют. Я предполагаю, что данные теряются при первом чтении, но я не уверен, как это определить. Весь этот код работает правильно, когда я использую обычный NetworkStream.

Вот код на стороне сервера.

private void ProcessRequests()
{
    //  This method reads the first byte of data correctly and if I want to
    // I can read the entire request here.  However, I want to leave
    // all that data until I want it below in my LongReadStream method.
    if (QuickReadStream(_netStream, receiveBuffer, 1) != ENQUIRY)
    {
        // Invalid Request, close connection
        clientIsFinished = true;
        _client.Client.Disconnect(true);
        _client.Close();
        return;
    }



    while (!clientIsFinished)  // Keep reading text until client sends END_TRANSMISSION
    {
        // Inside this method there is no data and the connection times out waiting for data
        receiveText = LongReadStream(_netStream, _client);

        // Continue talking with Client...
    }
    _client.Client.Shutdown(SocketShutdown.Both);
    _client.Client.Disconnect(true);
    _client.Close();
}


private string LongReadStream(NetworkStream stream, TcpClient c)
{
    bool foundEOT = false;
    StringBuilder sbFullText = new StringBuilder();
    int readLength, totalBytesRead = 0;
    string currentReadText;
    c.ReceiveBufferSize = DEFAULT_BUFFERSIZE * 100;

    byte[] bigReadBuffer = new byte[c.ReceiveBufferSize];

    while (!foundEOT)
    {
        using (var decompressStream = new DeflateStream(stream, CompressionMode.Decompress, true))
        {
            //using (StreamReader sr = new StreamReader(decompressStream))
            //{
                //currentReadText = sr.ReadToEnd();
            //}
            readLength = decompressStream.Read(bigReadBuffer, 0, c.ReceiveBufferSize);
            currentReadText = Encoding.UTF8.GetString(bigReadBuffer, 0, readLength);
            totalBytesRead += readLength;
        }

        sbFullText.Append(currentReadText);

        if (currentReadText.EndsWith(END_OF_TEXT))
        {
            foundEOT = true;
            sbFullText.Length = sbFullText.Length - 1;
        }
        else
        {
            sbFullText.Append(currentReadText);
        }

        // Validate data code removed for simplicity


    }
    c.ReceiveBufferSize = DEFAULT_BUFFERSIZE;
    c.ReceiveTimeout = timeOutMilliseconds;
    return sbFullText.ToString();

}



private string QuickReadStream(NetworkStream stream, byte[] receiveBuffer, int receiveBufferSize)
{
    using (DeflateStream zippy = new DeflateStream(stream, CompressionMode.Decompress, true))
    {
        int bytesIn = zippy.Read(receiveBuffer, 0, receiveBufferSize);
        var returnValue = Encoding.UTF8.GetString(receiveBuffer, 0, bytesIn);
        return returnValue;
    }
}

EDIT NetworkStream имеет базовое свойство Socket, которое имеет свойство Available. MSDN говорит об этом о доступном свойстве.

Получает объем данных, полученных из сети и доступных для чтения.

Перед вызовом ниже Доступно 77. После чтения 1 байта значение равно 0.

//receiveBufferSize = 1
int bytesIn = zippy.Read(receiveBuffer, 0, receiveBufferSize);

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

Кто-нибудь знает, почему это происходит, или есть ли способ сохранить базовые данные для чтения в будущем? Основываясь на этой «функции» и предыдущей статье, которую я прочитал, DeflateStream должен быть закрыт, чтобы завершить отправку (flush не будет работать). Похоже, DeflateStreams может быть ограничен в использовании для работы в сети, особенно если кто-то хочет противостоять DOS-атакам, проверяя входящие данные, прежде чем принимать полный поток.


person Adam Heeg    schedule 17.02.2016    source источник
comment
Просто предположение, но, возможно, конструктор для DeflateStream в QuickReadStream обнаруживает, что NetworkStream является потоком только для чтения и чтения, и считывает все это в zippy. Затем вы читаете первый байт из zippy, устанавливаете returnValue и возвращаете. Когда zippy выходит из области действия, в NetworkStream нечего читать, потому что он уже был прочитан и отброшен.   -  person Kevin    schedule 17.02.2016
comment
Я обновил свой вопрос на основе вашего, казалось бы, правильного комментария. Это принципиально проблематично, как указано в моем вопросе.   -  person Adam Heeg    schedule 17.02.2016
comment
Защита от DOS обычно выполняется на уровне сетевого устройства, где потоковые данные можно отслеживать/анализировать по мере их прохождения. С точки зрения приложения, я не уверен, что вы можете многое сделать для обнаружения или смягчения такой атаки. Мое единственное предложение состояло бы в том, чтобы полностью прочитать поток в MemoryStream, протестировать его, а затем отбросить, если он неправильный. Это не уменьшит нагрузку на TCP-трафик атаки, но может предотвратить ненужную обработку/хранение мусорных данных.   -  person Kevin    schedule 17.02.2016


Ответы (3)


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

Я думаю, ваш код мог бы работать, если бы вы продолжали работать с одним DeflateStream. Тем не менее, вы используете один в своем быстром чтении, а затем создаете другой.

Попробую пояснить свои рассуждения на примере. Предположим, у вас есть 8 байтов исходных данных, которые нужно отправить по сети в сжатом виде. Теперь предположим в качестве аргумента, что каждый байт (8 бит) исходных данных будет сжат до 6 бит в сжатой форме. Теперь давайте посмотрим, что ваш код делает с этим.

Из сетевого потока нельзя прочитать менее 1 байта. Вы не можете взять только 1 бит. Вы берете 1 байт, 2 байта или любое количество байтов, но не биты.

Но если вы хотите получить только 1 байт исходных данных, вам нужно прочитать первый целый байт сжатых данных. Однако есть только 6 бит сжатых данных, которые представляют первый байт несжатых данных. Последние 2 бита первого байта предназначены для второго байта исходных данных.

Теперь, если вы обрежете поток там, в сетевом потоке останется 5 байтов, которые не имеют никакого смысла и не могут быть распакованы.

Алгоритм дефляции более сложен, и поэтому он имеет смысл, если он не позволяет вам прекратить чтение из NetworkStream в какой-то момент и продолжить с нового DeflateStream с середины. Существует контекст распаковки, который должен присутствовать, чтобы распаковать данные до их исходной формы. Как только вы избавитесь от первого DeflateStream в своем быстром чтении, этот контекст исчезнет, ​​и вы не сможете продолжить.

Итак, чтобы решить вашу проблему, попробуйте создать только один DeflateStream и передать его вашим функциям, а затем удалить его.

person Wapac    schedule 17.02.2016
comment
Что ж, однократное выкачивание для всего метода, похоже, помогло, но последовательность все еще не работает до завершения диалога. Я думаю, что невозможно использовать DeflateStream с соединением tcpclient для непрерывного обмена данными. - person Adam Heeg; 17.02.2016
comment
Вы не можете вернуться в этот поток, но вы должны иметь возможность прочитать его часть, а затем прочитать остальную часть. Меня бы очень удивило, если бы это было невозможно и вам пришлось бы читать все сразу. - person Wapac; 17.02.2016
comment
@AdamHeeg да, вы не можете надежно сбросить поток дефляции, я думаю, потому что вывод может быть в позиции частичного байта. Вероятно, вам нужно разработать формат кадрирования сообщения, в котором вы добавляете сжатую длину в виде несжатого целого числа. Затем отправьте сжатый поток deflate. Это, конечно, еще больше усложняет ситуацию. Добавлю это к моему ответу. - person usr; 17.02.2016

Это сломано во многих отношениях.

  1. Вы предполагаете, что вызов чтения будет считывать точное количество байтов, которое вы хотите. Однако он может читать все кусками по одному байту.
  2. DeflateStream имеет внутренний буфер. По-другому и быть не может: входные байты не соответствуют 1:1 выходным байтам. Должна быть какая-то внутренняя буферизация. Вы должны использовать один такой поток.
  3. Та же проблема с UTF-8: строки в кодировке UTF-8 не могут быть разделены по границам байтов. Иногда ваши данные Unicode будут искажены.
  4. Не трогайте ReceiveBufferSize, это никак не помогает.
  5. Я думаю, что вы не можете надежно сбросить поток дефляции, потому что вывод может быть в позиции частичного байта. Вероятно, вам следует разработать формат кадрирования сообщения, в котором вы добавляете сжатую длину в виде несжатого целого числа. Затем отправьте сжатый поток deflate после длины. Это декодируется надежным способом.

Исправить эти проблемы непросто.

Поскольку вы, похоже, управляете клиентом и сервером, вы должны отказаться от всего этого и не разрабатывать свой собственный сетевой протокол. Используйте механизм более высокого уровня, такой как веб-сервисы, HTTP, protobuf. Все лучше, чем то, что у вас есть.

person usr    schedule 17.02.2016
comment
Я ценю ваши комментарии. Мы сделаем все возможное, чтобы реализовать собственное решение, использование механизма более высокого уровня здесь не является предпочтительным. Постараюсь использовать ваш совет в свою пользу, спасибо. Я согласен, что мне нужно многое узнать об этой теме. - person Adam Heeg; 17.02.2016

По сути, в коде, который я разместил выше, есть несколько ошибок. Во-первых, когда я читаю данные, я ничего не делаю, чтобы убедиться, что данные ВСЕ считываются. Согласно документации Microsoft

Операция чтения считывает столько данных, сколько доступно, вплоть до количества байтов, указанного параметром размера.

В моем случае я не был уверен, что мои чтения получат все данные, которые я ожидал.

Это можно сделать просто с помощью этого кода.

byte[] data= new byte[packageSize];
    bytesRead = _netStream.Read(data, 0, packageSize);
    while (bytesRead < packageSize)
        bytesRead += _netStream.Read(data, bytesRead, packageSize - bytesRead);

Вдобавок к этой проблеме у меня была фундаментальная проблема с использованием DeflateStream, а именно, я не должен использовать DeflateStream для записи в базовый NetworkStream. Правильный подход заключается в том, чтобы сначала использовать DeflateStream для сжатия данных в ByteArray, а затем отправлять этот ByteArray напрямую с помощью NetworkStream.

Использование этого подхода помогло правильно сжать данные по сети и прочитать данные на другом конце.

Вы можете указать, что я должен знать размер данных, и это правда. Каждый вызов имеет 8-байтовый «заголовок», который включает размер сжатых данных и размер данных в несжатом виде. Хотя я думаю, что второй был совершенно не нужен.

Код для этого здесь. Обратите внимание, что переменная CompressSize служит двум целям.

 int packageSize = streamIn.Read(sizeOfDataInBytes, 0, 4);
 while (packageSize!= 4)
 {
    packageSize+= streamIn.Read(sizeOfDataInBytes, packageSize, 4 - packageSize);
 }
 packageSize= BitConverter.ToInt32(sizeOfDataInBytes, 0);

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

Получив полный сжатый массив байтов, я могу получить входящие данные следующим образом:

var output = new MemoryStream();
using (var stream = new MemoryStream(bufferIn))
{
    using (var decompress = new DeflateStream(stream, CompressionMode.Decompress))
    {
        decompress.CopyTo(output);;
    }
}
output.Position = 0;
var unCompressedArray = output.ToArray();
output.Close();
output.Dispose();
return Encoding.UTF8.GetString(unCompressedArray);
person Adam Heeg    schedule 26.02.2016