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

Protobuf net延迟流式反序列化字段

这个问题在很大程度上取决于实际的模型——这不是库专门针对的场景。我怀疑您在这里最好的选择是使用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(...)]上来启用压缩阵列。