Почему мой 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 и передать его своим функциям, а затем утилизировать.
Это сломано во многих отношениях.
- Вы предполагаете, что вызов read прочитает точное количество байтов, которое вы хотите. Это могло бы прочитать все в кусках одного байта все же.
- DeflateStream имеет внутренний буфер. Иначе быть не может: входные байты не соответствуют 1:1 выходным байтам. Должна быть некоторая внутренняя буферизация. Вы должны использовать один такой поток.
- Та же проблема с UTF-8: строки в кодировке UTF-8 не могут быть разделены на границах байтов. Иногда данные Unicode будут искажены.
- Не трогайте ReceiveBufferSize, это никак не помогает.
- Я думаю, что вы не можете надежно очистить поток дефляции, потому что выходные данные могут быть в позиции неполного байта. Вам, вероятно, следует разработать формат фрейма сообщения, в котором вы добавляете сжатую длину как несжатое целое число. Затем отправьте сжатый поток выкачки после длины. Это декодируется надежным способом.
Исправить эти проблемы нелегко.
Поскольку вы, кажется, управляете клиентом и сервером, вам следует отказаться от всего этого, а не разрабатывать собственный сетевой протокол. Используйте механизм более высокого уровня, такой как веб-сервисы, HTTP, protobuf. Все лучше, чем у вас там.