消耗自定义流(IEnumerable <t>)

时间:2018-08-23 19:51:10

标签: c# serialization stream ienumerable

我正在使用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;
        }
    }
}

我没有包含自定义流。我为想要查看完整代码的用户创建了fiddle

  • 我是否需要在自定义流本身中添加一些内容以通知已读取所有数据?
  • 是因为解串器和序列化器的格式不一样(我认为不是)。
  • 我还想知道为什么当我在读取函数中设置断点时,缓冲区大小随机变化。
  • 停止通过尝试并包装代码来回答问题,这是我想要的答案。我想要一个不会崩溃的干净解决方案。谢谢。

如果有人能启发我,那就太好了!

3 个答案:

答案 0 :(得分:7)

  

我是否需要在自定义流本身中添加一些内容以通知已读取所有数据?

您可以,但是在接收到的Stream是不同类的WCF场景中无济于事。

有两种确定Stream数据结尾的标准(官方的,通过设计)方式:

(1)ReadByte返回-1

  

返回

     

无符号字节强制转换为Int32,如果在流末尾则为-1。

(2)Readcount > 0调用时返回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) 
    {
        // ...
    }
    // ...
}

P.S。那

  

我还想知道为什么当我在读取函数中放置一个断点时,缓冲区大小随机变化。

不是随机的。 BinaryFormatter在内部使用BinaryReader来读取诸如Int32ByteString等的类型化值,将所需大小传递为count,例如4,1,字符串编码的字节数(之所以知道,是因为在实际数据存储之前将其存储在流中,并在尝试读取实际数据之前将其读取)等。

答案 1 :(得分:2)

首先,您可以简单地序列化List<List<string>>本身。 Demo here。这样就无需使用这种特殊的类来读取流。并有可能使这个答案变得毫无意义。一次流式传输的唯一目的是一个可能非常大的数据集。在这种情况下,需要一种不同的实现,这是下面的解决方案可能解决的问题。

以下答案(和您的代码)要求读取流的客户端具有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。但是,这可行。

Here是经过调整且可以正常工作的提琴手。请注意,我也对其进行了清理。静态类与另一个类的名称相同,这让我头疼;)。另外,我将更改为byte[],而不是List<byte>

  

是因为解串器和序列化器的格式不同(我认为不是)。

否。

  

我还想知道为什么当我在读取函数中放置一个断点时,缓冲区大小随机变化。

缓冲区_buf应该是序列化的当前项目的大小。每个项目可能有所不同。

  

请停止,通过尝试并包装代码来回答问题,这不是我想要的答案。我想要一个不会崩溃且不会崩溃的干净解决方案。谢谢。

明智的做法是不要只吞下异常,而应该了解如何使其按预期工作。

答案 2 :(得分:1)

实现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属性,在这种情况下,它将返回您“知道”流中元素的长度。 据我所知,这与使用此属性的意图并不矛盾。