Потребление пользовательского потока (IEnumerable <t>)

Я использую пользовательскую реализацию Stream, которая будет транслировать IEnumerable<T> в поток. Я использую эту реализацию EnumerableStream для выполнения преобразования.

Я использую его для выполнения потоковой передачи через WCF в потоковом режиме. Я могу конвертировать IEnumerable в поток без проблем. Однажды я на стороне клиента, я могу десериализовать и получить все данные, однако я не могу найти условие, чтобы прекратить зацикливание моего потока. Я получаю:

System.Runtime.Serialization.SerializationException: конец потока, обнаруженный до завершения анализа.

Вот пример того, чего я пытаюсь достичь:

class Program
{
    public static void Main()
    {
        var ListToSend = new List<List<string>>();
        var ListToReceive = new List<List<string>>();
        ListToSend = SimulateData().ToList();
        using (Stream stream = GetStream(ListToSend))
        {
            var formatter = new BinaryFormatter();
            while (stream.CanRead || 1 == 1 || true...) // What should I put in here to stop once I read everything???
            {
                List<string> row = formatter.Deserialize(stream) as List<string>;
                ListToReceive.Add(row);
            }
            Printer(ListToReceive);
            Console.WriteLine("Done");
        }
    }

    private static void Printer(List<List<string>> data)
    {
        Console.WriteLine("Printing");
        foreach (var row in data)
        {
            foreach (var cell in row)
            {
                Console.Write(cell + "\t");
            }
            Console.WriteLine("-------------------------------------------------------------------------------");
        }
    }
    private static Stream GetStream(IEnumerable<List<string>> data)
    {
        return EnumerableStream.Create(data, DeserializerCallback);
    }

    private static List<byte> DeserializerCallback(object obj)
    {
        var binFormatter = new BinaryFormatter();
        var mStream = new MemoryStream();
        binFormatter.Serialize(mStream, obj);
        return mStream.ToArray().ToList();
    }

    private static IEnumerable<List<string>> SimulateData()
    {
        Random randomizer = new Random();
        for (var i = 0; i < 10; i++)
        {
            var row = new List<string>();
            for (var j = 0; j < 1000; j++)
            {
                row.Add((randomizer.Next(100)).ToString());
            }
            yield return row;
        }
    }
}

Я не включил пользовательский поток. Я создал скрипку для тех, кто хочет увидеть весь код.

  • Нужно ли что-то добавлять в сам пользовательский поток, чтобы уведомить, что все данные прочитаны?
  • Это потому, что формат десериализатора и сериализатора не совпадают (я так не думаю).
  • Я также хочу знать, почему, когда я помещаю точку останова в функцию чтения, размер буфера изменяется случайным образом.
  • Я бы предпочел не оборачивать код пробой и поймать, я хочу чистое решение, которое не вылетает.

Ответ 1

Нужно ли добавлять что-то в пользовательский поток, чтобы сообщить, что все данные были прочитаны?

Вы можете, но это не помогло бы в сценарии WCF, где полученный Stream - это другой класс.

Существует два стандартных (официальных, по дизайну) способа определения конца данных Stream:

(1) ReadByte возвращает -1

Возвращает

Беззнаковый байт передается в Int32 или -1, если в конце потока.

(2) Чтение возвращаемого 0 при вызове с count > 0

Возвращает

Общее количество байтов, считанных в буфере. Это может быть меньше количества запрошенных байтов, если количество байтов в настоящий момент недоступно, или нуль (0), если конец потока достигнут.

К сожалению, оба они потребляют текущий байт (переход к следующему) и разрушают десериализатор.

Каковы возможные решения?

Во-первых, реализация некоторого формата (протокола) сериализации/десериализации, который позволяет вам узнать, есть ли больше элементов для десериализации. например, List<T> сохраняет Count перед элементами, T[] сохраняет Length перед элементами и т.д. Так как EnumerableStream<T> не знает счет заранее, одно простое решение состоит в том, чтобы выпустить один поддельный байт перед каждым элементом:

private bool SerializeNext()
{
    if (!_source.MoveNext())
        return false;

    buf.Enqueue(1); // <--
    foreach (var b in _serializer(_source.Current))
        _buf.Enqueue(b);

    return true;
}

Это позволит вам использовать

while (stream.ReadByte() != -1)
{
    // ...
}

Во-вторых, если вы хотите сохранить текущий формат, более общим решением было бы реализовать пользовательский поток, который переносит другой поток и реализует метод PeekByte с той же семантикой, что и стандартный ReadByte, но без использования текущего байта:

public class SequentialStream : Stream
{
    private Stream source;
    private bool leaveOpen;
    private int? nextByte;

    public SequentialStream(Stream source, bool leaveOpen = false)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (!source.CanRead) throw new ArgumentException("Non readable source.", nameof(source));
        this.source = source;
        this.leaveOpen = leaveOpen;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && !leaveOpen)
            source.Dispose();
        base.Dispose(disposing);
    }

    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override void Flush() { }
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

    public int PeekByte()
    {
        if (nextByte == null)
            nextByte = source.ReadByte();
        return nextByte.Value;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (count <= 0) return 0;
        if (nextByte != null)
        {
            if (nextByte.Value < 0) return 0;
            buffer[offset] = (byte)nextByte.Value;
            if (count > 1)
            {
                int read = source.Read(buffer, offset + 1, count - 1);
                if (read == 0)
                    nextByte = -1;
                else
                    nextByte = null;
                return read + 1;
            }
            else
            {
                nextByte = null;
                return 1;
            }
        }
        else
        {
            int read = source.Read(buffer, offset, count);
            if (read == 0)
                nextByte = -1;
            return read;
        }
    }
} 

Это в основном реализует только поток только для чтения, только с 0 или 1 байтом.

Использование будет выглядеть так:

using (var stream = new SequentialStream(GetStream(ListToSend)))
{
    // ...
    while (stream.PeekByte() != -1) 
    {
        // ...
    }
    // ...
}

PS Как насчет

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

Это не случайно. BinaryFormatter внутренне использует BinaryReader для чтения типизированных значений, таких как Int32, Byte, String и т.д., BinaryReader нужный размер в виде count, например 4, 1, количество строковых кодированных байтов (которое он знает, поскольку хранит их в потоке до фактических данных и читает его, прежде чем пытаться прочитать фактические данные) и т.д.

Ответ 2

Во-первых, вы можете просто сериализовать сам TG40. Демо здесь. Это устраняет необходимость в этом специализированном классе для чтения потока. И, возможно, делает этот ответ спорным. Единственная цель для потоковой передачи по одному - потенциально очень большой набор данных. В этом случае потребуется другая реализация, и это то, что потенциально может решить это следующее решение.

Следующий ответ (и ваш код) требует, чтобы клиент, читающий поток, имел класс EnumerableStream.

Нужно ли что-то добавлять в сам пользовательский поток, чтобы уведомить, что все данные прочитаны?

Да. Вам нужно реализовать новое свойство, чтобы знать, есть ли у вас еще один T для чтения или использовать Length.

public bool HasMore { get { return _buf.Any() || SerializeNext();} }

или

public override long Length { get { return (_buf.Any() || SerializeNext()) ? 1 : 0; } }

Я чувствую, что все это решение можно очистить, чтобы получить IEnumerable<T> StreamReader. Однако это работает.

Здесь настроенный и работающий скрипач. Обратите внимание, что я тоже немного прибрался. Статический класс, названный так же, как и другой класс, вызывал у меня головную боль;). Кроме того, я бы изменил на byte[], а не на List<byte>.

Это потому, что формат десериализатора и сериализатора не совпадают (я так не думаю).

Номер

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

Буфер _buf должен соответствовать размеру сериализованного текущего элемента. Это может варьироваться в зависимости от элемента.

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

Вам не стоит просто глотать исключение, а понимать, как заставить его работать так, как задумано.

Ответ 3

реализовать свойство length:

public override long Length 
{
    get 
    {
        return (_buf.Any() || SerializeNext()) ? 1 : 0;
    } 
}

затем проверьте длину:

        while (stream.Length > 0)
        {
            List<string> row = formatter.Deserialize(stream) as List<string>;
            ListToReceive.Add(row);
        }

я тестировал это на вашей скрипке, и он работает хорошо.

Это очень похожий подход к решению @TheSoftwareJedi, но использует свойство Length, которое в этом случае вернет длину элит, которые вы "знаете" в потоке. Насколько я вижу, это не противоречит использованию этого свойства intendet.