简短回答:
我真的建议不要自己实现二进制表示的解释。我会使用另一种格式(
JSON
,
XML
等等)。
长答案:
然而,如果这不可能,当然有一种方法。。。
实际问题是:
序列化.NET对象的二进制格式是什么样的?我们如何正确解释它?
我的所有研究都基于
.NET Remoting: Binary Format Data Structure
规格
示例类:
作为一个工作示例,我创建了一个简单的类,名为
A
它包含两个属性,一个字符串和一个整数值,它们被称为
SomeString
和
SomeValue
.
班
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.
包含
abc
和
123
作为值。
示例结果数据:
如果我们在十六进制编辑器中查看序列化结果,我们会得到如下结果:
让我们来解释示例结果数据:
根据上述规范(以下是PDF的直接链接:
[MS-NRBF].pdf
)流中的每个记录都由
RecordTypeEnumeration
部分
2.1.2.1 RecordTypeNumeration
状态:
此枚举标识记录的类型。每个记录(MemberPrimitiveUnTyped除外)都以记录类型枚举开头。枚举的大小为一个字节。
SerializationHeaderRecord:
因此,如果我们回头看我们得到的数据,我们可以开始解释第一个字节:
如中所述
2.1.2.1 RecordTypeEnumeration
值为
0
识别
SerializationHeaderRecord
在
2.6.1 SerializationHeaderRecord
:
SerializationHeaderRecord记录必须是二进制序列化中的第一个记录。此记录包含格式的主要版本和次要版本以及顶部对象和标题的ID。
它包括:
-
RecordTypeEnum(1字节)
-
RootId(4字节)
-
HeaderId(4字节)
-
主要版本(4字节)
-
MinorVersion(4字节)
有了这些知识,我们可以解释包含17个字节的记录:
00
代表
记录类型枚举
这是
序列化标头记录
在我们的案例中。
01 00 00 00
代表
RootId
如果序列化流中既不存在BinaryMethodCall记录,也不存在Binary MethodReturn记录,则此字段的值必须包含序列化流中包含的类、数组或BinaryObjectString记录的ObjectId。
所以在我们的案例中
ObjectId
值
1
(因为数据是使用little-endian序列化的),我们希望再次看到;-)
FF FF FF FF
代表
HeaderId
01 00 00 00
代表
MajorVersion
00 00 00 00
代表
MinorVersion
二进制库:
按照规定,每条记录必须以
记录类型枚举
。最后一个记录完成后,我们必须假设新的记录开始了。
让我们解释下一个字节:
如我们所见,在我们的示例中
序列化标头记录
之后是
BinaryLibrary
记录:
BinaryLibrary记录将INT32 ID(如[MS-DTYP]第2.2.22节所述)与库名称相关联。这允许其他记录使用ID引用库名称。当有多个记录引用同一个库名称时,此方法可减小导线大小。
它包括:
-
RecordTypeEnum(1字节)
-
库ID(4字节)
-
LibraryName(可变字节数)
LengthPrefixedString
))
如中所述
2.1.1.6 LengthPrefixedString
...
LengthPrefixedString表示字符串值。字符串的前缀是UTF-8编码字符串的长度(以字节为单位)。长度编码在可变长度字段中,最小为1字节,最大为5字节。为了最小化导线尺寸,长度编码为可变长度字段。
在我们的简单示例中,长度始终使用
1 byte
有了这些知识,我们可以继续解释流中的字节:
0C
代表
记录类型枚举
它标识
二进制库
记录
02 00 00 00
代表
LibraryId
这是
2
在我们的案例中。
现在
长度前缀字符串
跟随:
42
表示
长度前缀字符串
其中包含
LibraryName
.
在本例中
42
(十进制66)告诉我们,我们需要读取接下来的66个字节,并将它们解释为
库名称
.
如前所述,字符串为
UTF-8
编码,因此上面字节的结果类似于:
_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
具有成员和类型的类:
同样,记录是完整的,因此我们解释
记录类型枚举
下一个:
05
标识
ClassWithMembersAndTypes
记录部分
2.3.2.1 ClassWithMembersAndTypes
状态:
ClassWithMembersAndTypes记录是Class记录中最详细的记录。它包含关于成员的元数据,包括成员的名称和远程处理类型。它还包含引用类的库名称的库ID。
它包括:
-
RecordTypeEnum(1字节)
-
ClassInfo(可变字节数)
-
MemberTypeInfo(可变字节数)
-
库ID(4字节)
类别信息:
如中所述
2.3.1.1 ClassInfo
该记录包括:
-
对象ID(4字节)
-
名称(可变字节数(也是
长度前缀字符串
))
-
成员计数(4字节)
-
MemberNames(它是
长度前缀字符串
的项目数必须等于
MemberCount
字段。)
回到原始数据,一步一步:
01 00 00 00
代表
对象ID
。我们已经看到了这个,它被指定为
根ID
在
序列化标头记录
.
0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41
代表
Name
使用
长度前缀字符串
如前所述,在我们的示例中,字符串的长度定义为1字节,因此第一个字节
0F
指定必须使用UTF-8读取和解码15个字节。结果如下:
StackOverFlow.A
-很明显我用了
StackOverFlow
作为命名空间的名称。
02 00 00 00
代表
成员计数
,它告诉我们,2名成员,均代表
长度前缀字符串
的。
第一位成员的姓名:
1B 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
代表第一个
MemberName
,
1B
同样是字符串的长度,长度为27字节,结果如下:
<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 64
代表第二个
成员名称
,
1A
指定字符串长度为26字节。结果如下:
<SomeValue>k__BackingField
.
成员类型信息:
在
ClassInfo
这个
MemberTypeInfo
跟随。
部分
2.3.1.2 - MemberTypeInfo
声明,该结构包含:
表示正在传输的成员类型的BinaryTypeEnumeration值序列。阵列必须:
-
具有与ClassInfo结构的MemberNames字段相同数量的项。
-
进行排序,使BinaryTypeEnumeration与ClassInfo结构的MemberNames字段中的Member名称相对应。
-
AdditionalInfos(长度可变),取决于
BinaryTpeEnum
附加信息可能存在,也可能不存在。
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
所以考虑到这一点,我们就快到了。。。
我们期望2
BinaryTypeEnumeration
值(因为我们在
MemberNames
).
再次,回到完整的原始数据
成员类型信息
记录:
01
代表
BinaryType枚举
根据
2.1.2.2 BinaryTypeEnumeration
我们可以期待
String
并且使用
长度前缀字符串
.
00
代表
BinaryType枚举
再次,根据规范,它是
Primitive
如上所述,
原始的
的后面是附加信息,在本例中是
PrimitiveTypeEnumeration
。这就是为什么我们需要读取下一个字节,即
08
,将其与
2.1.2.3 PrimitiveTypeEnumeration
并惊讶地注意到,我们可以期待
Int32
如关于基本数据类型的一些其他文档中所述。
库ID:
在
MemerTypeInfo
这个
库ID
下面,它由4个字节表示:
02 00 00 00
代表
库ID
其为2。
值:
如
2.3 Class Records
:
该类成员的值必须按照第2.7节的规定序列化为该记录之后的记录。记录的顺序必须与ClassInfo(第2.3.1.1节)结构中指定的MemberNames的顺序相匹配。
这就是为什么我们现在可以期待成员的价值观。
让我们看看最后几个字节:
06
识别
BinaryObjectString
。它代表了我们的价值
SomeString
属性(
<SomeString>k_Backing字段
准确地说)。
根据
2.5.7 BinaryObjectString
它包含:
-
RecordTypeEnum(1字节)
-
对象ID(4字节)
-
值(可变长度,表示为
长度前缀字符串
)
因此,我们可以清楚地识别
03 00 00 00
代表
对象ID
.
03 61 62 63
代表
Value
哪里
03
是字符串本身的长度
61 62 63
是转换为的内容字节
abc
.
希望你能记得有第二个成员
国际32
。知道
国际32
通过使用4个字节表示,我们可以得出如下结论:
必须是
价值
我们的第二个成员。
7B
十六进制等于
123
十进制,这似乎适合我们的示例代码。
所以这里是完整的
具有成员和类型的类
记录:
消息结束:
最后是最后一个字节
0B
代表
MessageEnd
记录