我正在使用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。
如果有人能启发我,那就太好了!
答案 0 :(得分:7)
我是否需要在自定义流本身中添加一些内容以通知已读取所有数据?
您可以,但是在接收到的Stream
是不同类的WCF场景中无济于事。
有两种确定Stream
数据结尾的标准(官方的,通过设计)方式:
(1)ReadByte返回-1
返回
无符号字节强制转换为Int32,如果在流末尾则为-1。
(2)Read用count > 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
来读取诸如Int32
,Byte
,String
等的类型化值,将所需大小传递为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属性,在这种情况下,它将返回您“知道”流中元素的长度。 据我所知,这与使用此属性的意图并不矛盾。