c# - 如何分析二进制序列化流的内容?

标签 c# .net serialization binary-serialization

我使用二进制序列化 (BinaryFormatter) 作为临时机制,将状态信息存储在文件中,用于相对复杂的(游戏)对象结构;文件出来 比我预期的大 ,并且我的数据结构包括递归引用 - 所以我想知道 BinaryFormatter 是否实际上存储了相同对象的多个副本,或者我的基本“对象和值的数量”应该有”算法是偏离基础的,或者过大的尺寸来自哪里。

搜索堆栈溢出,我能够找到 Microsoft 的二进制远程处理格式的规范:
http://msdn.microsoft.com/en-us/library/cc236844(PROT.10).aspx

我找不到任何现有的查看器,它使您能够“窥视”二进制格式化程序输出文件的内容 - 获取文件中不同对象类型的对象计数和总字节数等;

我觉得这一定是我的“google-fu”让我失望了(我只有一点点)-有人可以帮忙吗?这应该是以前做过的吧??

UPDATE :我找不到它,也没有得到答案,所以我把一些相对较快的东西放在一起(链接到下面的可下载项目);我可以确认 BinaryFormatter 不会存储同一对象的多个副本,但它确实将大量元数据打印到流中。如果您需要高效的存储,请构建您自己的自定义序列化方法。

最佳答案

因为它可能对某些人感兴趣,所以我决定写这篇关于 的文章 序列化 .NET 对象的二进制格式是什么样的,我们如何正确解释它?
我所有的研究都基于 .NET Remoting: Binary Format Data Structure 规范。

示例类:
为了有一个工作示例,我创建了一个名为 A 的简单类,它包含 2 个属性,一个字符串和一个整数值,它们分别称为 SomeStringSomeValue
A 如下所示:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}
对于序列化,我当然使用了 BinaryFormatter:
BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();
可以看出,我传递了一个类 A 的新实例,其中包含 abc123 作为值。

结果数据示例:
如果我们在十六进制编辑器中查看序列化结果,我们会得到如下内容:
Example result data

让我们解释示例结果数据:
根据上述规范(这里是 PDF 的直接链接: [MS-NRBF].pdf )流中的每条记录都由 RecordTypeEnumeration 标识。第 2.1.2.1 RecordTypeNumeration 节指出:

This enumeration identifies the type of the record. Each record (except for MemberPrimitiveUnTyped) starts with a record type enumeration. The size of the enumeration is one BYTE.



SerializationHeaderRecord:
所以如果我们回顾一下我们得到的数据,我们可以开始解释第一个字节:
SerializationHeaderRecord_RecordTypeEnumeration
2.1.2.1 RecordTypeEnumeration 中所述,0 的值标识了 SerializationHeaderRecord 中指定的 2.6.1 SerializationHeaderRecord :

The SerializationHeaderRecord record MUST be the first record in a binary serialization. This record has the major and minor version of the format and the IDs of the top object and the headers.


它包括:
  • RecordTypeEnum (1 字节)
  • RootId(4 字节)
  • HeaderId(4 字节)
  • 主要版本(4 个字节)
  • 次要版本(4 字节)

  • 有了这些知识,我们可以解释包含 17 个字节的记录:
    SerializationHeaderRecord_Complete00 代表 RecordTypeEnumeration,在我们的例子中是 SerializationHeaderRecord01 00 00 00 代表 RootId

    If neither the BinaryMethodCall nor BinaryMethodReturn record is present in the serialization stream, the value of this field MUST contain the ObjectId of a Class, Array, or BinaryObjectString record contained in the serialization stream.


    所以在我们的例子中,这应该是 ObjectId,其值为 1(因为数据是使用 little-endian 序列化的),我们希望再次看到它;-)FF FF FF FF 代表 HeaderId01 00 00 00 代表 MajorVersion00 00 00 00 代表 MinorVersion
    二进制库:
    按照规定,每条记录必须以 RecordTypeEnumeration 开头。随着最后一条记录完成,我们必须假设新的记录开始了。

    让我们解释下一个字节:
    BinaryLibraryRecord_RecordTypeEnumeration
    如我们所见,在我们的示例中,SerializationHeaderRecord 后面是 BinaryLibrary 记录:

    The BinaryLibrary record associates an INT32 ID (as specified in [MS-DTYP] section 2.2.22) with a Library name. This allows other records to reference the Library name by using the ID. This approach reduces the wire size when there are multiple records that reference the same Library name.


    它包括:
  • RecordTypeEnum (1 字节)
  • LibraryId(4 个字节)
  • LibraryName(可变字节数(即 LengthPrefixedString ))

  • 2.1.1.6 LengthPrefixedString 所述...

    The LengthPrefixedString represents a string value. The string is prefixed by the length of the UTF-8 encoded string in bytes. The length is encoded in a variable-length field with a minimum of 1 byte and a maximum of 5 bytes. To minimize the wire size, length is encoded as a variable-length field.


    在我们的简单示例中,长度始终使用 1 byte 进行编码。有了这些知识,我们可以继续解释流中的字节:
    BinaryLibraryRecord_RecordTypeEnumeration_LibraryId0C 代表 RecordTypeEnumeration,它标识了 BinaryLibrary 记录。02 00 00 00 代表 LibraryId,在我们的例子中是 2

    现在 LengthPrefixedString 如下:
    BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName42 表示包含 LengthPrefixedStringLibraryName 的长度信息。
    在我们的例子中, 42(十进制 66)的长度信息告诉我们,我们需要读取接下来的 66 个字节并将它们解释为 LibraryName
    如前所述,该字符串是 UTF-8 编码的,因此上述字节的结果将类似于:_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    ClassWithMembersAndTypes:
    同样,记录是完整的,所以我们解释下一个的 RecordTypeEnumeration:
    ClassWithMembersAndTypesRecord_RecordTypeEnumeration05 标识 ClassWithMembersAndTypes 记录。第 2.3.2.1 ClassWithMembersAndTypes 节指出:

    The ClassWithMembersAndTypes record is the most verbose of the Class records. It contains metadata about Members, including the names and Remoting Types of the Members. It also contains a Library ID that references the Library Name of the Class.


    它包括:
  • RecordTypeEnum (1 字节)
  • ClassInfo(可变字节数)
  • MemberTypeInfo(可变字节数)
  • LibraryId(4 字节)

  • 类信息:
    2.3.1.1 ClassInfo 所述,记录包括:
  • ObjectId(4 字节)
  • 名称(可变字节数(也是 LengthPrefixedString ))
  • MemberCount(4 字节)
  • MemberNames(这是 LengthPrefixedString 的序列,其中项目的数量必须等于 MemberCount 字段中指定的值。) 0x2919112242333

    回到原始数据,一步一步:
    ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId01 00 00 00 代表 ObjectId 。我们已经看到了这个,它在 RootId 中被指定为 SerializationHeaderRecord

    ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 表示使用 Name 表示的类的 LengthPrefixedString 。如前所述,在我们的示例中,字符串的长度定义为 1 个字节,因此第一个字节 0F 指定必须使用 UTF-8 读取和解码 15 个字节。结果看起来像这样:StackOverFlow.A - 所以很明显我使用了 StackOverFlow 作为命名空间的名称。

    ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount02 00 00 00 代表 MemberCount ,它告诉我们有 2 个成员,都用 LengthPrefixedString 表示。

    第一位成员姓名:
    ClassWithMembersAndTypesRecord_MemberNameOne1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 代表第一个 MemberName1B 也是字符串的长度,它是 27 个字节的长度,结果如下: 0x25181242313431

    第二名成员姓名:
    ClassWithMembersAndTypesRecord_MemberNameTwo<SomeString>k__BackingField 表示第二个 1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64MemberName 指定字符串为 26 字节长。结果如下: 1A

    MemberTypeInfo:
    <SomeValue>k__BackingField 之后是 ClassInfo
    MemberTypeInfo 节指出,该结构包含:
  • BinaryTypeEnums(长度可变)

  • A sequence of BinaryTypeEnumeration values that represents the Member Types that are being transferred. The Array MUST:

    • Have the same number of items as the MemberNames field of the ClassInfo structure.

    • Be ordered such that the BinaryTypeEnumeration corresponds to the Member name in the MemberNames field of the ClassInfo structure.


  • AdditionalInfos(长度可变),取决于 2.3.1.2 - MemberTypeInfo 附加信息可能存在也可能不存在。

  • | BinaryTypeEnum | AdditionalInfos |
    |----------------+--------------------------|
    | Primitive | PrimitiveTypeEnumeration |
    | String | None |


    因此,考虑到这一点,我们几乎就在那里......
    我们期望 2 个 BinaryTpeEnum 值(因为我们在 BinaryTypeEnumeration 中有 2 个成员)。

    再次回到完整的 MemberNames 记录的原始数据:
    ClassWithMembersAndTypesRecord_MemberTypeInfoMemberTypeInfo 代表第一个成员的 01,根据 BinaryTypeEnumeration 我们可以期待一个 2.1.2.2 BinaryTypeEnumeration,它用 0x21418 表示。String 代表第二个成员的 LengthPrefixedString ,同样,根据规范,它是 00 。如上所述, BinaryTypeEnumeration 后面是附加信息,在这种情况下是 Primitive 。这就是为什么我们需要读取下一个字节,即 Primitive ,将其与 PrimitiveTypeEnumeration 中所述的表进行匹配,并惊讶地注意到我们可以预期 08 由一些其他文档表示,关于基本数据类型。

    库编号:2.1.2.3 PrimitiveTypeEnumeration之后是Int32,用4个字节表示:
    ClassWithMembersAndTypesRecord_LibraryIdMemerTypeInfo 代表 LibraryId,即 2。

    值:
    02 00 00 00 中所述:

    The values of the Members of the Class MUST be serialized as records that follow this record, as specified in section 2.7. The order of the records MUST match the order of MemberNames as specified in the ClassInfo (section 2.3.1.1) structure.


    这就是为什么我们现在可以期待成员的值(value)。

    让我们看看最后几个字节:
    BinaryObjectStringRecord_RecordTypeEnumerationLibraryId 标识 2.3 Class Records 。它代表我们的 06 属性的值(准确地说是 BinaryObjectString)。
    根据 SomeString 它包含:
  • RecordTypeEnum (1 字节)
  • ObjectId(4 字节)
  • 值(可变长度,表示为 <SomeString>k__BackingField )

  • 所以知道这一点,我们可以清楚地确定
    BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue2.5.7 BinaryObjectString 代表 LengthPrefixedString03 00 00 00 表示 ObjectId,其中 03 61 62 63 是字符串本身的长度,Value 是转换为 0x23134 的内容字节。13
    希望您还记得有第二个成员, 03 。知道 61 62 63 用 4 个字节表示,我们可以得出结论,即
    BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue
    必须是我们第二个成员的 abcInt32 十六进制等于 Int32 十进制,这似乎适合我们的示例代码。
    所以这里是完整的 Value 记录:
    ClassWithMembersAndTypesRecord_Complete

    留言结束:
    MessageEnd_RecordTypeEnumeration
    最后最后一个字节 7B 代表 123 记录。

    关于c# - 如何分析二进制序列化流的内容?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3052202/

    相关文章:

    c# - ASP.NET MVC 2 + jQuery 灯箱 + 登录

    Java 序列化和 JavaBeans

    java - 使用带有自定义类的嵌套 HashMap 进行 Kryo 序列化

    C# Linq to CSV 动态对象运行时列名称

    c# - 不包括来自另一个构造函数的参数类型时的编译器错误

    c# - 为 Web 和 Windows Phone 7 构建 Silverlight 游戏

    c# - IEnumerable<TSource> Concat<TSource> 是否保留元素的顺序?

    c# 序列化 - 包含具有属性的简单内容的复杂类型

    c# - 有没有办法修改这个循环?

    c# - 更新实体时序列不包含元素