在设计用于记录二进制数据的文件格式时,您认为该格式应具有哪些属性?到目前为止,我已经提出了以下要点:
还有什么方法可以使这种格式更具有前瞻性,并最大限度地减少未来的麻烦?
答案 0 :(得分:23)
看看PNG spec。这种格式背后有一些非常好的理由。
此外,确定对您未来格式的重要性:紧凑性,兼容性,允许在其中嵌入其他格式(不同的压缩算法)。另一个有趣的例子是Google's protocol buffers,其中传输数据的大小是王。
至于字节序,我建议你选择一个选项并坚持下去,不允许不同的字节顺序。否则,读写库只会变得更加复杂和缓慢。
答案 1 :(得分:16)
我同意这些是好主意:
开头的魔术数字。在* nix:
向后兼容性的文件版本号。
字节顺序规范。
但是你的第四个是矫枉过正,因为#2允许你添加字段,只要你更改版本号(并且只要你不需要forward compatibility)。
此外,在许多其他答案中表达的对文件强加块结构的想法似乎不像二进制文件的普遍要求,而是某种有效载荷问题的解决方案。
除了以上1-3之外,我还要添加以下内容:
简单的校验和或检测内容完整的其他方式。否则,您无法信任魔术字节或版本号。请注意规范校验和中包含哪些字节。通常,您将包含文件中尚未进行错误检测的所有字节。
您的软件版本(包括您编写文件的最精细数字,例如内部版本号)。你将得到一个错误报告,其中包含一个无法打开它的人的附件,他们在编写文件时没有任何线索,因为错误没有发生。但是这个bug是在编写它的版本中,而不是在试图读它的版本中。
在规范中明确指出这是二进制格式,即所有字节都允许所有值0-255(幻数除外)。
这里有一些可选的:
如果你确实需要向前兼容性,你需要某种方式来表达哪些“块”是“可选的”(比如png),这样你以前的软件版本就可以优雅地跳过它们。
如果您希望“在野外”找到这些文件,您可以考虑嵌入一些线索来查找规范。想象一下在png文件中找到 http://www.w3.org/TR/PNG/ 字符串会有多大帮助。
答案 2 :(得分:11)
当然,这完全取决于格式的目的。
一种灵活的方法是将整个文件构造为TLV(Tag-Length-Value)三元组。 例如,使您的文件由记录组成,每个记录以4字节标题开头:
1 byte = record type
3 bytes = record length
followed by record content
关于字节顺序,如果在文件中存储字节顺序指示符,则所有应用程序都必须支持所有字节顺序格式。另一方面,如果为文件指定特定的字节序,则只有具有不匹配的endiannes的平台上的应用程序才能执行其他工作,并且可以在编译时决定(使用条件编译)。
答案 3 :(得分:6)
另一点,取自.xz文件规范(http://tukaani.org/xz/xz-file-format.txt):前几个字节中的一个应该是非字符,“以防止应用程序将文件误检测为文本文件。”请注意编辑器和其他工具通常会检查多少个头字节,但在前四个或八个字节中使用非二进制字节似乎很有用。
答案 4 :(得分:3)
我会考虑定义更高级别用于存储数据的子结构,有点像文件中的迷你文件系统。
例如,即使您的文件格式要存储特定于应用程序的数据,我也会考虑在文件中定义记录/流等,以便应用程序无关的代码能够理解文件的布局,但当然不了解不透明的有效载荷。
让我们更具体一点。考虑在内存中存储数据的常用方法:通常可以将它们归结为连续的可扩展数组/列表,基于指针/引用的图形以及特定格式的二进制数据块。
因此,沿类似的行定义二进制文件格式可能是富有成效的。使用记录标题指示以下数据的长度和组成,无论是数组形式(相同类型记录的列表),引用(文件中其他记录的偏移),还是数据blob(例如字符串数据)在特定的编码中,但不包含任何引用。)
如果经过精心设计,这可以允许文件格式不仅用于一次性保存数据,而且可以根据需要逐步增加。如果子结构设计合理,它可以是应用不可知的,但仍允许例如要写入的垃圾收集应用程序,它可以理解blob,数组和引用记录类型,并且能够跟踪文件并消除未使用的记录(即不再指向的记录)。
这只是一个想法。其他寻找想法的地方是一般的文件系统设计或关系数据库物理存储策略。
当然,根据您的要求,这可能是过度的。您可能只是使用二进制格式来保存内存数据,在这种情况下,需要考虑的方法是标记记录。
在这种方法中,每个数据都以标签为前缀。标签指示紧随其后的数据的类型,可能还有其长度和名称。列表可以带有没有有效负载的“end-list”标签。标签可能有一个嵌入的标识符,因此序列化机制在读取内容时可以忽略不理解的标签。在这方面有点像XML,除了使用二进制习语之外。
实际上,XML是一个寻找文件格式长期使用寿命的好地方。看看它的命名空间功能。如果您仔细构建读写代码,则应该可以编写应用程序来保留它们不理解的标记(递归)数据的位置和内容,可能是因为它是由同一应用程序的更高版本编写的。
答案 5 :(得分:3)
确保您保留一个标记代码(或者更好地保留每个标记中的一个位),以指定已删除/空闲的块/块。 然后只需将块的当前标记代码更改为已删除的标记代码或设置标记的已删除位即可删除块。 这样,删除块时,您无需立即完全重构文件。
在标签中保留一个位提供了可能取消删除块的选项 (如果你保持块的数据不变)。
为了安全起见,您可能希望将已删除块的数据清零,在这种情况下,您将使用特殊的已删除/免费标记。
我同意Stepan,你应该选择一个endianess,但我也会在文件中有一个endianess指标。 如果您使用endianess指标,您可以考虑使用 UniCode Byte Order Marks中的一个也作为任何文本块使用的任何UniCode文本编码的参与者。 BOM通常是UniCoded文本文件的前几个字节,因此如果您的BOM是文件中的第一个条目,则可能存在某些实用程序将您的文件识别为UniCode文本的问题(我认为这不是一个问题) 。 我会将BOM作为您的常规标签之一(使用UTF16 BOM,如果使用16位标签,或使用UTF32 BOM,如果使用32位标签),则使用0长度块/块。
答案 6 :(得分:2)
未来证明该文件的一种方法是提供块。直接在文件头数据之后,您可以开始第一个块。该块可以具有块类型的字节或字代码,然后是字节大小。现在您可以随意添加新的块类型,并且可以跳到块的末尾。
答案 7 :(得分:2)
我同意atzz关于使用Tag Length Value系统的建议。为了将来的兼容性,您可以在开始时将一组“指针”存储到TLV条目(或者可能是Tag,Pointer并使指针指向Length,Value;或者可能是Tag,Length,Pointer,然后将所有数据放在一起别处?)。
所以,我的文件看起来像:
magic number/file id
version
tag for first data entry
pointer to first data entry --------+
tag for second data entry |
pointer to second data entry |
... |
length of first data entry <--------+
value for first data entry
...
幻数,版本,标签,指针和长度都是预定义的设置长度,以便于解码。比方说,2个字节。或者4,取决于你需要什么。它们并不都需要相同(例如,所有标签都是1个字节,指针是4个等)。
标记可让您知道存储的内容。 指针告诉您哪里(偏移或绝对值,以字节为单位), length 告诉您数据的大小以及值是标记类型的长度字节数据。 如果在MyFileFormat v2文件上使用MyFileFormat v1解码器,则指针允许您跳过v1解码器无法理解的部分。如果您只是跳过无效标签,则可以简单地使用TLV而不是TPLV。
我会手动编写类似的东西,或者在ASN.1中定义我的格式并生成编解码器(我在电信工作,因此ASN.1 / TLV对我有意义:-D)
答案 8 :(得分:2)
在开始之前要了解的最重要的事情之一就是如何使用文件。
这里的大多数答案都提供了关于可移植性/兼容性方面的建议,所以我不打算添加更多内容。但请考虑以下(经常被忽视)的事情。
zcat | strings
该文件并查看其中的内容。要记住很多事情,设计一个好的格式需要很多计划和远见。诸如zcat
文件和获取有用信息之类的小事情或者使用原生整数带来的小的性能提升可以为您的产品提供优势,但是您需要小心,不要牺牲重要的东西来获得它
答案 9 :(得分:1)
如果你正在处理可变长度数据,那么使用指针会更有效 更高效:有一个指向数据的指针数组,理想情况下靠近文件的开头,而不是存储数据直接在数组中。
在这种情况下,间接是可取的,因为它允许随机访问,这只有在所有项目大小相同的情况下才有可能。如果数据直接存储在数组中,而没有指定任何记录的位置,那么在最坏的情况下数据访问将花费O( n )时间;为了使您的文件读取代码能够访问特定元素,它必须知道所有先前元素的长度,并且找到它的唯一方法是查看每个元素。如果您正在一次读取整个文件,那么无论如何您都会这样做,所以这不会有问题。但如果你只想要一件事,那就不是这样了。
对于一个指针数组,它的时间是O(1)时间:你需要的只是一个索引号,你可以检索并跟踪指针来获取你的数据。
使用此方法编写文件时,您当然必须在进行任何编写之前在内存中构建表。