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