Проблема с BinaryReader.ReadChars()

Я столкнулся с проблемой метода BinaryReader.ReadChars(). Когда я оборачиваю BinaryReader вокруг необработанного сокета NetworkStream, иногда я получаю повреждение потока, когда считываемый поток не синхронизируется. Рассматриваемый поток содержит сообщения в протоколе двоичной сериализации.

Я проследил это до следующего

  • Это происходит только при чтении строки юникода (закодированной с использованием Encoding.BigEndian)
  • Это происходит только тогда, когда рассматриваемая строка разделена на два пакета tcp (подтверждено с помощью wireshark)

Я думаю, что происходит следующее (в контексте примера ниже)

  • BinaryReader.ReadChars() вызывается и просит прочитать 3 символа (длина строки кодируется перед самой строкой)
  • Первый цикл внутренне запрашивает чтение 6 байтов (3 оставшихся символа * 2 байта/символ) из сетевого потока.
  • В сетевом потоке доступно только 3 байта
  • 3 байта считываются в локальный буфер
  • Буфер передан декодеру
  • Декодер декодирует 1 символ и сохраняет другой байт в своем собственном внутреннем буфере.
  • Второй цикл внутренне запрашивает чтение 4 байтов! (2 оставшихся символа * 2 байта/символ)
  • В сетевом потоке доступны все 4 байта
  • 4 байта считываются в локальный буфер
  • Буфер передан декодеру
  • Декодер декодирует 2 символа и сохраняет оставшиеся 4 байта внутри.
  • Декодирование строки завершено
  • Код сериализации пытается демаршалировать следующий элемент и дает сбои из-за повреждения потока.

    char[] buffer = new char[3];
    int charIndex = 0;
    
    Decoder decoder = Encoding.BigEndianUnicode.GetDecoder();
    
    // pretend 3 of the 6 bytes arrives in one packet
    byte[] b1 = new byte[] { 0, 83, 0 };
    int charsRead = decoder.GetChars(b1, 0, 3, buffer, charIndex);
    charIndex += charsRead;
    
    // pretend the remaining 3 bytes plus a final byte, for something unrelated,
    // arrive next
    byte[] b2 = new byte[] { 71, 0, 114, 3 };
    charsRead = decoder.GetChars(b2, 0, 4, buffer, charIndex);
    charIndex += charsRead;
    

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

Вот рассматриваемый код .NET framework

    while (charsRemaining>0) { 
        // We really want to know what the minimum number of bytes per char 
        // is for our encoding.  Otherwise for UnicodeEncoding we'd have to
        // do ~1+log(n) reads to read n characters. 
        numBytes = charsRemaining;
        if (m_2BytesPerChar)
            numBytes <<= 1;

        numBytes = m_stream.Read(m_charBytes, 0, numBytes);
        if (numBytes==0) { 
            return (count - charsRemaining); 
        } 
        charsRead = m_decoder.GetChars(m_charBytes, 0, numBytes, buffer, index);

        charsRemaining -= charsRead;
        index+=charsRead;
    }

Я не совсем уверен, является ли это ошибкой или просто неправильным использованием API. Чтобы обойти эту проблему, я просто вычисляю требуемые байты, читаю их, а затем запускаю byte[] через соответствующий Encoding.GetString(). Однако это не сработает для чего-то вроде UTF-8.

Интересно услышать мысли людей по этому поводу и делаю ли я что-то не так или нет. И, возможно, это сэкономит следующему человеку несколько часов/дней утомительной отладки.

РЕДАКТИРОВАТЬ: опубликовано для подключения Подключить элемент отслеживания


person Mike Q    schedule 26.11.2009    source источник
comment
Заставляет меня задаться вопросом, почему у BinaryReader вообще есть метод ReadChars. Весь смысл BinaryReader в том, чтобы читать двоичные данные, а не текстовые данные. Я думаю, что правильно использовать классы кодирования, как вы сказали.   -  person Josh    schedule 30.11.2009
comment
Да, не уверен, думаю, это пытается быть читателем общего назначения, преобразующим двоичные в базовые примитивные типы (int, long, string и т. д.). Я думаю, что лучший подход в целом, который будет работать и для UTF-8, состоит в том, чтобы закодировать количество байтов (а не символов) на стороне отправки, а затем выполнить вызов byte[] read + Encoding.   -  person Mike Q    schedule 30.11.2009


Ответы (4)


Я воспроизвел проблему, о которой вы упомянули, с BinaryReader.ReadChars.

Несмотря на то, что разработчик всегда должен учитывать опережение при составлении таких вещей, как потоки и декодеры, это кажется довольно серьезной ошибкой в ​​BinaryReader, поскольку этот класс предназначен для чтения структур данных, состоящих из различных типов данных. В этом случае я согласен с тем, что ReadChars должен был быть более консервативным в том, что он читал, чтобы не потерять этот байт.

Нет ничего плохого в вашем обходном пути прямого использования Decoder, в конце концов, это то, что ReadChars делает за кулисами.

Юникод — это простой случай. Если вы думаете о произвольном кодировании, на самом деле не существует универсального способа гарантировать, что правильное количество байтов используется при передаче количества символов вместо количества байтов (подумайте о символах различной длины и случаях, связанных с искаженным вводом). По этой причине отказ от BinaryReader.ReadChars в пользу чтения определенного количества байтов обеспечивает более надежное общее решение.

Я бы посоветовал вам довести это до сведения Microsoft через http://connect.microsoft.com/visualstudio. .

person Jason Kresowaty    schedule 26.11.2009
comment
Спасибо за подтверждение, разместил его для связи, кто изучает его. - person Mike Q; 30.11.2009


Это напоминает один из моих собственных вопросов (Чтение из HttpResponseStream не удается), где я проблема, заключающаяся в том, что при чтении из потока ответов HTTP StreamReader думал, что он преждевременно достиг конца потока, поэтому мои парсеры неожиданно вылетали из строя.

Как Марк предложил для вашей проблемы, я сначала попробовал предварительную буферизацию в MemoryStream, которая работает хорошо, но означает, что вам, возможно, придется долго ждать, если у вас есть большой файл для чтения (особенно из сети / Интернета), прежде чем вы сможете сделать что-нибудь полезное с этим. В конце концов я решил создать собственное расширение TextReader, которое переопределяет методы чтения и определяет их с помощью метода ReadBlock (который выполняет блокирующее чтение, т. е. ждет, пока не получит именно то количество символов, которое вы запрашиваете)

Ваша проблема, вероятно, связана, как и моя, с тем фактом, что методы чтения не гарантируют возврат количества символов, которое вы запрашиваете, например, если вы посмотрите документацию для BinaryReader.Read (http://msdn.microsoft.com/en-us/library/ms143295.aspx), который вы увидите что в нем говорится:

Возвращаемое значение
Тип: System..::.Int32
Количество символов, считанных в буфер. Это может быть меньше запрошенного количества байтов, если такое количество байтов недоступно, или может быть равно нулю, если достигнут конец потока.

Поскольку BinaryReader не имеет методов ReadBlock, таких как TextReader, все, что вы можете сделать, это использовать свой собственный подход к мониторингу позиции самостоятельно или предварительному кэшированию Марка.

person RobV    schedule 26.11.2009

Я работаю с Unity3D/Mono atm, и метод ReadChars может содержать больше ошибок. Я сделал такую ​​строку:

mat.name = new string(binaryReader.ReadChars(64));

mat.name даже содержал правильную строку, но я мог просто добавить строки перед. Все после строки просто исчезло. Даже с String.Format. Мое решение до сих пор не использует метод ReadChars, а читает данные как массив байтов и преобразует их в строку:

byte[] str = binaryReader.ReadBytes(64);
int lengthOfStr = Array.IndexOf(str, (byte)0); // e.g. 4 for "clip\0"
mat.name = System.Text.ASCIIEncoding.Default.GetString(str, 0, lengthOfStr);
person kungfooman    schedule 17.08.2014