protobuf-net无法序列化以下类,因为不支持序列化Stream
类型的对象:
[ProtoContract]
class StreamObject
{
[ProtoMember(1)]
public Stream StreamProperty { get; set; }
}
我知道我可以通过使用类型byte[]
的序列化属性并将流读入该属性来解决此问题,如this question中所述。但这需要将整个byte[]
加载到内存中,如果流很长,可能会快速耗尽系统资源。
有没有办法将一个流序列化为protobuf-net中的字节数组,而无需将整个字节序列加载到内存中?
答案 0 :(得分:2)
这里的基本困难不是protobuf-net,而是V2 protocol buffer format。有两种方法可以对重复元素(例如字节数组或流)进行编码:
作为打包重复元素。这里,该字段的所有元素都被打包成一个键值对,其中线类型为2(长度分隔)。每个元素的编码方式与正常情况相同,只是前面没有标记。
protobuf-net以这种格式自动编码字节数组,但这样做需要事先知道的总字节数。对于字节流,这可能需要将整个流加载到内存中(例如,当StreamProperty.CanSeek == false
时),这违反了您的要求。
作为重复元素。这里编码的消息具有零个或多个具有相同标签号的键值对。
对于字节流,使用此格式会导致编码消息中出现大量膨胀,因为每个字节都需要一个额外的整数键。
如您所见,默认表示都不符合您的需求。相反,将大字节流编码为“相当大”的块序列是有意义的,其中每个块都被打包,但整个序列不是。
以下版本的StreamObject
执行此操作:
[ProtoContract]
class StreamObject
{
public StreamObject() : this(new MemoryStream()) { }
public StreamObject(Stream stream)
{
if (stream == null)
throw new ArgumentNullException();
this.StreamProperty = stream;
}
[ProtoIgnore]
public Stream StreamProperty { get; set; }
internal static event EventHandler OnDataReadBegin;
internal static event EventHandler OnDataReadEnd;
const int ChunkSize = 4096;
[ProtoMember(1, IsPacked = false, OverwriteList = true)]
IEnumerable<ByteBuffer> Data
{
get
{
if (OnDataReadBegin != null)
OnDataReadBegin(this, new EventArgs());
while (true)
{
byte[] buffer = new byte[ChunkSize];
int read = StreamProperty.Read(buffer, 0, buffer.Length);
if (read <= 0)
{
break;
}
else if (read == buffer.Length)
{
yield return new ByteBuffer { Data = buffer };
}
else
{
Array.Resize(ref buffer, read);
yield return new ByteBuffer { Data = buffer };
break;
}
}
if (OnDataReadEnd != null)
OnDataReadEnd(this, new EventArgs());
}
set
{
if (value == null)
return;
foreach (var buffer in value)
StreamProperty.Write(buffer.Data, 0, buffer.Data.Length);
}
}
}
[ProtoContract]
struct ByteBuffer
{
[ProtoMember(1, IsPacked = true)]
public byte[] Data { get; set; }
}
请注意OnDataReadBegin
和OnDataReadEnd
事件?我添加然后进行调试,以便检查输入流实际上是否已经流式传输到输出protobuf流。以下测试类执行此操作:
internal class TestClass
{
public void Test()
{
var writeStream = new MemoryStream();
long beginLength = 0;
long endLength = 0;
EventHandler begin = (o, e) => { beginLength = writeStream.Length; Console.WriteLine(string.Format("Begin serialization of Data, writeStream.Length = {0}", writeStream.Length)); };
EventHandler end = (o, e) => { endLength = writeStream.Length; Console.WriteLine(string.Format("End serialization of Data, writeStream.Length = {0}", writeStream.Length)); };
StreamObject.OnDataReadBegin += begin;
StreamObject.OnDataReadEnd += end;
try
{
int length = 1000000;
var inputStream = new MemoryStream();
for (int i = 0; i < length; i++)
{
inputStream.WriteByte(unchecked((byte)i));
}
inputStream.Position = 0;
var streamObject = new StreamObject(inputStream);
Serializer.Serialize(writeStream, streamObject);
var data = writeStream.ToArray();
StreamObject newStreamObject;
using (var s = new MemoryStream(data))
{
newStreamObject = Serializer.Deserialize<StreamObject>(s);
}
if (beginLength >= endLength)
{
throw new InvalidOperationException("inputStream was completely buffered before writing to writeStream");
}
inputStream.Position = 0;
newStreamObject.StreamProperty.Position = 0;
if (!inputStream.AsEnumerable().SequenceEqual(newStreamObject.StreamProperty.AsEnumerable()))
{
throw new InvalidOperationException("!inputStream.AsEnumerable().SequenceEqual(newStreamObject.StreamProperty.AsEnumerable())");
}
else
{
Console.WriteLine("Streams identical.");
}
}
finally
{
StreamObject.OnDataReadBegin -= begin;
StreamObject.OnDataReadEnd -= end;
}
}
}
public static class StreamExtensions
{
public static IEnumerable<byte> AsEnumerable(this Stream stream)
{
if (stream == null)
throw new ArgumentNullException();
int b;
while ((b = stream.ReadByte()) != -1)
yield return checked((byte)b);
}
}
以上的输出是:
Begin serialization of Data, writeStream.Length = 0
End serialization of Data, writeStream.Length = 1000888
Streams identical.
这表示输入流确实流式传输到输出而没有立即完全加载到内存中。
原型fiddle。
是否有一种机制可以逐步写出一个带有字节的打包重复元素,事先知道长度?
似乎没有。假设您有一个CanSeek == true
的流,您可以将其封装在IList<byte>
中,该IList.Count
枚举流中的字节,提供对流中字节的随机访问,并以{{返回流长度1}}。有一个样本小提琴here显示了这样的尝试。然而,遗憾的是,ListDecorator.Write()
只是枚举列表并在将其编码内容写入输出流之前对其进行缓冲,这会导致输入流完全加载到内存中。我认为会发生这种情况,因为protobuf-net以List<byte>
的方式编码byte []
,即Base 128 Varints的长度分隔序列。由于byte
的Varint表示有时需要多个字节,因此无法从列表计数中提前计算长度。有关字节数组和列表编码方式差异的更多详细信息,请参阅this answer。应该可以以与IList<byte>
相同的方式实现byte []
的编码 - 它当前不可用。