Tao*_*Tao 33 .net c# serialization binary-serialization
我正在使用二进制序列化(BinaryFormatter)作为临时机制,将状态信息存储在一个相对复杂(游戏)对象结构的文件中; 文件比我想象的要大得多,我的数据结构包括递归引用 - 所以我想知道BinaryFormatter是否实际存储了相同对象的多个副本,或者我的基本"对象和值的数量是否应该有"arithmentic是偏离基础的,或者其他地方的过度规模来自于.
搜索堆栈溢出我能够找到Microsoft的二进制远程格式的规范:http://msdn.microsoft.com/en-us/library/cc236844( PROT.10) .aspx
我找不到的是任何现有的查看器,它使您能够"查看"二进制格式化输出文件的内容 - 获取文件中不同对象类型的对象计数和总字节数等;
我觉得这一定是我的"google-fu"让我失望(我什么都没有) - 任何人都可以帮忙吗?这一定是以前做过的,对吧?
更新:我找不到它并且没有得到答案所以我把相对快速的东西放在一起(链接到下面的可下载项目); 我可以确认BinaryFormatter不存储同一对象的多个副本,但它会向流中打印相当多的元数据.如果您需要高效存储,请构建自己的自定义序列化方法.
Mar*_*far 88
因为对于我决定做这篇文章的人来说可能有兴趣关于序列化.NET对象的二进制格式是什么样子以及我们如何正确解释它?
我的所有研究都基于.NET Remoting:二进制格式数据结构规范.
示例类:
为了有一个工作示例,我创建了一个简单的类A
,其中包含2个属性,一个字符串和一个整数值,它们被称为SomeString
和SomeValue
.
类A
看起来像这样:
[Serializable()]
public class A
{
public string SomeString
{
get;
set;
}
public int SomeValue
{
get;
set;
}
}
Run Code Online (Sandbox Code Playgroud)
对于序列化我BinaryFormatter
当然使用了:
BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();
Run Code Online (Sandbox Code Playgroud)
可以看出,我传递了一个A
包含abc
和123
作为值的类的新实例.
示例结果数据:
如果我们在十六进制编辑器中查看序列化结果,我们会得到这样的结果:
让我们解释一下示例结果数据:
根据上述规范(这里是PDF的直接链接:[MS-NRBF] .pdf),流中的每个记录都由RecordTypeEnumeration
.部分2.1.2.1 RecordTypeNumeration
说明:
此枚举标识记录的类型.每条记录(MemberPrimitiveUnTyped除外)都以记录类型枚举开头.枚举的大小是一个BYTE.
SerializationHeaderRecord:
因此,如果我们回顾一下我们得到的数据,我们就可以开始解释第一个字节了:
如标识2.1.2.1 RecordTypeEnumeration
值中0
所述SerializationHeaderRecord
,其中指定了2.6.1 SerializationHeaderRecord
:
SerializationHeaderRecord记录必须是二进制序列化中的第一条记录.此记录具有格式的主要版本和次要版本以及顶部对象和标题的ID.
它包括:
有了这些知识,我们可以解释包含17个字节的记录:
00
表示RecordTypeEnumeration
这是SerializationHeaderRecord
在我们的例子.
01 00 00 00
代表着 RootId
如果序列化流中既不存在BinaryMethodCall也不存在BinaryMethodReturn记录,则该字段的值必须包含序列化流中包含的Class,Array或BinaryObjectString记录的ObjectId.
所以在我们的情况下,这应该是ObjectId
值1
(因为数据使用little-endian序列化),我们希望再次看到;-)
FF FF FF FF
代表着 HeaderId
01 00 00 00
代表着 MajorVersion
00 00 00 00
代表MinorVersion
BinaryLibrary:
按照规定,每条记录必须以RecordTypeEnumeration
.当最后一条记录完成时,我们必须假设一条新记录开始.
让我们解释下一个字节:
正如我们所看到的,在我们的例子中,SerializationHeaderRecord
它后跟着BinaryLibrary
记录:
BinaryLibrary记录将INT32 ID(在[MS-DTYP]部分2.2.22中指定)与库名称相关联.这允许其他记录使用ID引用库名称.当有多个引用相同库名称的记录时,此方法会减小线缆大小.
它包括:
LengthPrefixedString
))
如2.1.1.6 LengthPrefixedString
......中所述
LengthPrefixedString表示字符串值.该字符串以UTF-8编码字符串的长度为前缀,以字节为单位.长度编码在可变长度字段中,最小为1个字节,最多为5个字节.为了最小化线尺寸,将长度编码为可变长度字段.
在我们的简单示例中,长度始终使用编码1 byte
.有了这些知识,我们可以继续解释流中的字节:
0C
表示RecordTypeEnumeration
识别BinaryLibrary
记录的内容.
02 00 00 00
表示LibraryId
这是2
在我们的例子.
现在LengthPrefixedString
如下:
42
表示LengthPrefixedString
包含的长度信息LibraryName
.
在我们的例子中,42
(十进制66)的长度信息告诉我们,我们需要读取接下来的66个字节并将它们解释为LibraryName
.
如前所述,字符串是UTF-8
编码的,因此上面字节的结果将类似于:_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClassWithMembersAndTypes:
再次,记录完成,所以我们解释RecordTypeEnumeration
下一个:
05
识别ClassWithMembersAndTypes
记录.部分2.3.2.1 ClassWithMembersAndTypes
说明:
ClassWithMembersAndTypes记录是类记录中最详细的.它包含有关成员的元数据,包括成员的名称和远程处理类型.它还包含一个引用类的库名称的库ID.
它包括:
的ClassInfo:
如2.3.1.1 ClassInfo
记录中所述,包括:
LengthPrefixedString
))LengthPrefixedString
其中项的数量必须等于MemberCount
字段中指定的值.)
回到原始数据,一步一步:
01 00 00 00
代表着ObjectId
.我们已经看到了这一个,它被指定为RootId
中SerializationHeaderRecord
.
0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41
表示Name
使用a表示的类的类LengthPrefixedString
.如上所述,在我们的示例中,字符串的长度定义为1个字节,因此第一个字节0F
指定必须使用UTF-8读取和解码15个字节.结果看起来像这样:StackOverFlow.A
- 显然我用作StackOverFlow
命名空间的名称.
02 00 00 00
代表它MemberCount
,它告诉我们,2个成员,两个代表LengthPrefixedString
将跟随.
第一个成员的名字:
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
表示第二个MemberName
,1A
指定字符串长度为26个字节.结果如下:<SomeValue>k__BackingField
.
MemberTypeInfo:
之后ClassInfo
的MemberTypeInfo
如下.
部分2.3.1.2 - MemberTypeInfo
说明,该结构包含:
一系列BinaryTypeEnumeration值,表示正在传输的成员类型.数组必须:
与ClassInfo结构的MemberNames字段具有相同数量的项目.
按顺序排列,使BinaryTypeEnumeration对应于ClassInfo结构的MemberNames字段中的成员名称.
BinaryTpeEnum
附加信息可能存在也可能不存在.
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
所以考虑到这一点,我们几乎就在那里......我们期待2个BinaryTypeEnumeration
值(因为我们有2个成员MemberNames
).
再次,回到完整MemberTypeInfo
记录的原始数据:
01
代表BinaryTypeEnumeration
第一个成员,根据2.1.2.2 BinaryTypeEnumeration
我们可以预期a String
和它用a表示LengthPrefixedString
.
00
代表BinaryTypeEnumeration
第二个成员,再次,根据规范,它是一个Primitive
.如上所述,Primitive
其后是附加信息,在这种情况下是a PrimitiveTypeEnumeration
.这就是为什么我们需要读取下一个字节,即08
与表中所述的表匹配,2.1.2.3 PrimitiveTypeEnumeration
并惊讶地发现我们可以预期一个Int32
由4个字节表示,如其他一些关于基本数据类型的文档中所述.
库Id:
在后MemerTypeInfo
的LibraryId
如下,它由4个字节表示:
02 00 00 00
代表LibraryId
哪个是2.
价值:
由于在规定2.3 Class Records
:
必须将类的成员的值序列化为遵循此记录的记录,如2.7节所述.记录的顺序必须与ClassInfo(第2.3.1.1节)结构中指定的MemberNames的顺序相匹配.
这就是我们现在可以期待成员价值的原因.
让我们看看最后几个字节:
06
识别出一个BinaryObjectString
.它代表了我们SomeString
财产的价值(<SomeString>k__BackingField
确切地说).
根据2.5.7 BinaryObjectString
它包含:
LengthPrefixedString
)
所以我们可以清楚地认识到这一点
03 00 00 00
代表着ObjectId
.
03 61 62 63
表示Value
,其中03
是字符串本身的长度和61 62 63
是转换为内容的字节abc
.
希望你能记得有第二个成员,一个Int32
.知道Int32
使用4个字节表示,我们可以得出结论
必须是Value
我们的第二个成员.7B
十六进制等于123
十进制,似乎适合我们的示例代码.
所以这是完整的ClassWithMembersAndTypes
记录:
MessageEnd:
最后,最后一个字节0B
代表MessageEnd
记录.
Vasiliy是正确的,我最终需要实现自己的格式化程序/序列化过程,以更好地处理版本控制并输出更紧凑的流(压缩前).
我确实想要了解流中发生的事情,所以我编写了一个(相对)快速的类来完成我想要的事情:
我把它放在像codeproject这样的地方是不够用的,所以我只是将项目转储到我的网站上的zip文件中:http://www.architectshack.com/BinarySerializationAnalysis.ashx
在我的具体情况下,事实证明问题是双重的:
希望这在某些方面帮助某人!
更新:Ian Wright与原始代码的问题联系我,当源对象包含"十进制"值时,它崩溃了.现在已经更正了,我已经利用这个机会将代码移动到GitHub并给它一个(许可的,BSD)许可证.
我们的应用运行大量数据.它可能需要1-2 GB的RAM,就像你的游戏一样.我们遇到了同样的"存储同一对象的多个副本"问题.二进制序列化也存储了太多的元数据.首次实现时,序列化文件大约需要1-2 GB.现在我设法减少了价值 - 50-100 MB.我们做了什么.
简短的回答 - 不要使用.Net二进制序列化,创建自己的二进制序列化机制.我们有自己的BinaryFormatter类和ISerializable接口(有两种方法Serialize,Deserialize).
同一个对象不应该多次序列化.我们保存它的唯一ID并从缓存中恢复对象.
如果你问,我可以分享一些代码.
编辑:看来你是对的.请参阅以下代码 - 它证明我错了.
[Serializable]
public class Item
{
public string Data { get; set; }
}
[Serializable]
public class ItemHolder
{
public Item Item1 { get; set; }
public Item Item2 { get; set; }
}
public class Program
{
public static void Main(params string[] args)
{
{
Item item0 = new Item() { Data = "0000000000" };
ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };
var fs0 = File.Create("temp-file0.txt");
var formatter0 = new BinaryFormatter();
formatter0.Serialize(fs0, holderOneInstance);
fs0.Close();
Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
//File.Delete(fs0.Name);
}
{
Item item1 = new Item() { Data = "1111111111" };
Item item2 = new Item() { Data = "2222222222" };
ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };
var fs1 = File.Create("temp-file1.txt");
var formatter1 = new BinaryFormatter();
formatter1.Serialize(fs1, holderTwoInstances);
fs1.Close();
Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
//File.Delete(fs1.Name);
}
}
}
Run Code Online (Sandbox Code Playgroud)
看起来像BinaryFormatter
使用object.Equals来查找相同的对象.
你有没有看过生成的文件?如果从代码示例中打开"temp-file0.txt"和"temp-file1.txt",您会看到它有很多元数据.这就是为什么我建议你创建自己的序列化机制.
对不起,因为要共同使用.