Protobuf net延迟流式反序列化字段
本文关键字:反序列化 字段 net 延迟 Protobuf | 更新日期: 2023-09-27 18:23:36
总体目标:在反序列化时跳过一个很长的字段,在访问该字段时直接从流中读取元素,而不加载整个字段。
示例类正在序列化/反序列化的对象是FatPropertyClass
。
[ProtoContract]
public class FatPropertyClass
{
[ProtoMember(1)]
private int smallProperty;
[ProtoMember(2)]
private FatArray2<int> fatProperty;
[ProtoMember(3)]
private int[] array;
public FatPropertyClass()
{
}
public FatPropertyClass(int sp, int[] fp)
{
smallProperty = sp;
fatProperty = new FatArray<int>(fp);
}
public int SmallProperty
{
get { return smallProperty; }
set { smallProperty = value; }
}
public FatArray<int> FatProperty
{
get { return fatProperty; }
set { fatProperty = value; }
}
public int[] Array
{
get { return array; }
set { array = value; }
}
}
[ProtoContract]
public class FatArray2<T>
{
[ProtoMember(1, DataFormat = DataFormat.FixedSize)]
private T[] array;
private Stream sourceStream;
private long position;
public FatArray2()
{
}
public FatArray2(T[] array)
{
this.array = new T[array.Length];
Array.Copy(array, this.array, array.Length);
}
[ProtoBeforeDeserialization]
private void BeforeDeserialize(SerializationContext context)
{
position = ((Stream)context.Context).Position;
}
public T this[int index]
{
get
{
// logic to get the relevant index from the stream.
return default(T);
}
set
{
// only relevant when full array is available for example.
}
}
}
我可以反序列化为:FatPropertyClass d = model.Deserialize(fileStream, null, typeof(FatPropertyClass), new SerializationContext() {Context = fileStream}) as FatPropertyClass;
,其中model
可以是例如:
RuntimeTypeModel model = RuntimeTypeModel.Create();
MetaType mt = model.Add(typeof(FatPropertyClass), false);
mt.AddField(1, "smallProperty");
mt.AddField(2, "fatProperty");
mt.AddField(3, "array");
MetaType mtFat = model.Add(typeof(FatArray<int>), false);
这将跳过FatArray<T>
中array
的反序列化。但是,稍后我需要从该数组中读取随机元素。我尝试过的一件事是在FatArray2<T>
的BeforeDeserialize(SerializationContext context)
方法中记住反序列化之前的流位置。如上述代码:position = ((Stream)context.Context).Position;
。然而,这似乎永远是潮流的终点。
我如何记住FatProperty2
开始的流位置,以及如何在随机索引中读取它?
注意:FatArray2<T>
中的参数T
可以是用[ProtoContract]
标记的其他类型,而不仅仅是基元。此外,在对象图的不同深度处可能存在FatProperty2<T>
类型的多个属性。
方法2:在序列化包含对象之后,序列化字段FatProperty2<T>
。因此,用长度前缀序列化FatPropertyClass
,然后用长度前缀串行化它包含的所有胖数组。用一个属性标记所有这些胖数组属性,在反序列化时,我们可以记住每个属性的流位置。
那么问题是我们如何从中读取原语呢?这对于使用T item = Serializer.DeserializeItems<T>(sourceStream, PrefixStyle.Base128, Serializer.ListItemTag).Skip(index).Take(1).ToArray();
来获取索引index
处的项的类来说是可以的。但这对基元是如何工作的呢?基元数组似乎无法使用DeserializeItems
进行反序列化。
带LINQ的DeserializeItems
可以这样使用吗?它会像我假设的那样(在内部跳过流到正确的元素——最坏的情况是读取每个长度前缀并跳过它)吗?
谨致问候,Iulian
这个问题在很大程度上取决于实际的模型——这不是库专门针对的场景。我怀疑您在这里最好的选择是使用ProtoReader
手动编写阅读器。请注意,如果最外层的对象是List<SomeType>
或类似对象,但内部对象通常只是简单读取或跳过,则在读取所选项目时,会有一些技巧。
通过ProtoReader
从文档的根重新开始,您可以相当有效地查找第n项。如果你愿意的话,我可以稍后做一个具体的例子(除非你确信它真的有用,否则我还没有跳进去)。作为参考,流的位置在这里没有用处的原因是:除非你特别告诉它限制其长度,否则库会过度读取和缓冲数据。这是因为像"variant"这样的数据在没有大量缓冲的情况下很难有效读取,因为它最终会成为对ReadByte()
的大量单独调用,而不仅仅是使用本地缓冲区。
这是一个完全未经测试的版本,直接从读取器读取子属性的第n个数组项;请注意,一个接一个地多次调用它是低效的,但应该清楚如何更改它以读取范围的连续值,等等:
static int? ReadNthArrayItem(Stream source, int index, int maxLen)
{
using (var reader = new ProtoReader(source, null, null, maxLen))
{
int field, count = 0;
while ((field = reader.ReadFieldHeader()) > 0)
{
switch (field)
{
case 2: // fat property; a sub object
var tok = ProtoReader.StartSubItem(reader);
while ((field = reader.ReadFieldHeader()) > 0)
{
switch (field)
{
case 1: // the array field
if(count++ == index)
return reader.ReadInt32();
reader.SkipField();
break;
default:
reader.SkipField();
break;
}
}
ProtoReader.EndSubItem(tok, reader);
break;
default:
reader.SkipField();
break;
}
}
}
return null;
}
最后,请注意,如果这是一个大数组,您可能希望使用"打包"数组(请参阅protobuf文档,但这基本上是在存储它们时不包含每个项的头)。这会更有效率,但请注意,它需要略微不同的读取代码。您可以通过将IsPacked = true
添加到该阵列的[ProtoMember(...)]
上来启用压缩阵列。