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

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

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

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

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

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

3 ответа

Решение

По сути, есть несколько проблем с кодом, который я разместил выше. Во-первых, когда я читаю данные, я ничего не делаю, чтобы убедиться, что данные ВСЕ читаются. Согласно документации 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 служит 2 целям.

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам