代码之家  ›  专栏  ›  技术社区  ›  oliver

设计(二进制)文件格式时有哪些要点?[闭门]

  •  37
  • oliver  · 技术社区  · 17 年前

    在设计用于记录二进制数据的文件格式时,您认为该格式应该具有哪些属性?到目前为止,我提出了以下要点:

    • 在开头有一些“魔法字节”,以便能够识别文件(在我的特定情况下,这也有助于区分文件和“遗留”文件)
    • 指定所有数据项的长度和大小;或者:包含一些空间来描述数据的长度/大小(我倾向于前者)
    • 是否可能为将来可能需要的每个文件的进一步属性保留一些空间?

    还有什么有用的方法可以让格式更适合未来,并将未来的头痛问题降到最低?

    10 回复  |  直到 17 年前
        1
  •  27
  •   Stepan Stolyarov    17 年前

    看一看 PNG spec .这种格式背后有一些很好的理由。

    此外,决定什么对你未来的格式很重要:紧凑性、兼容性,允许在其中嵌入其他格式(不同的压缩算法)。另一个有趣的例子是 Google's protocol buffers ,其中传输数据的大小是最重要的。

    至于endianness,我建议您选择一个选项并坚持使用,不允许使用不同的字节顺序。否则,读写库只会变得更复杂、更慢。

        2
  •  18
  •   Bart    17 年前

    我同意这些都是好主意:

    1. 一开始是神奇的数字。差不多 required 在*nix中:

    2. 向后兼容的文件版本号。

    3. Endianness规范。

    但是你的第四个版本太夸张了,因为#2允许你添加字段,只要你更改版本号(并且只要你不需要) forward compatibility ).

    • 是否可能为将来可能需要的每个文件的进一步属性保留一些空间?

    此外,在文件上施加块结构的想法(用许多其他答案表达)似乎不像是对二进制文件的通用要求,而是对特定类型有效负载问题的解决方案。

    除了上面的1-3条,我还要补充这些:

    • 检测内容是否完整的简单校验和或其他方法。否则你就不能相信魔法字节或版本号。请注意指定校验和中包含哪些字节。通常,您会在文件中包含尚未进行错误检测的所有字节。

    • 编写文件的软件版本(包括您拥有的最细粒度的编号,例如内部版本号)。你将从无法打开它的人那里得到一份带有附加文件的bug报告,他们在编写文件时没有任何线索,因为当时没有发生错误。但是错误出现在编写它的版本中,而不是试图读取它的版本中。

    • 在规范中明确说明这是一个 二进制的 格式,即所有字节都允许0-255的值(魔法数字除外)。

    • 如果您确实需要向前兼容,那么您需要某种方式来表达哪些“块”是“可选的”(就像png一样),以便您的软件的早期版本可以优雅地跳过它们。

    • 如果你认为这些文件是“野生的”,你可以考虑嵌入一些线索来找到它。想象一下找到字符串会有多大帮助。 http://www.w3.org/TR/PNG/ 在png文件中。

        3
  •  11
  •   atzz    17 年前

    当然,这完全取决于格式的目的。

    一种灵活的方法是将整个文件构造为TLV(标记长度值)三元组。 例如,将文件压缩为多条记录,每条记录以4字节的头开头:

    1 byte  = record type
    3 bytes = record length
    followed by record content
    

    关于endianness,如果在文件中存储endianness指示符,那么所有应用程序都必须支持所有endianness格式。另一方面,如果为文件指定特定的endianness,则只有在具有不匹配endianness的平台上的应用程序才能执行额外的工作,并且可以在编译时决定(使用条件编译)。

        4
  •  7
  •   oliver    14 年前

    另一点,摘自。xz文件规范( http://tukaani.org/xz/xz-file-format.txt ):前几个字节中的一个应为非字符,“以防止应用程序将文件误检测为文本文件。”。请注意,编辑器和其他工具通常会检查多少头字节,但在前四个或八个字节中使用非二进制字节似乎很有用。

        5
  •  3
  •   Bork Blatt    17 年前

    未来证明文件的一种方法是提供块。在文件头数据之后,可以直接开始第一个块。块可以有块类型的字节或字代码,然后是以字节为单位的大小。现在可以任意添加新的块类型,并且可以跳到块的末尾。

        6
  •  3
  •   Barry Kelly    17 年前

    我会考虑定义一个更高级别的子结构来存储数据,有点像文件里面的一个迷你文件系统。

    例如,即使您的文件格式将存储特定于应用程序的数据,我也会考虑在文件中定义记录/流等,这样应用程序不可知代码能够理解文件的布局,但当然不能理解不透明的有效载荷。

    让我们更具体一点。考虑在内存中存储数据的常用方法:一般来说,它们可以被煮沸到相邻的可扩展数组/列表、指针/基于引用的图形和特定格式的二进制数据块。

    因此,按照类似的思路定义二进制文件格式可能会很有成效。使用记录头,指示以下数据的长度和组成,无论是数组(类型相同的记录列表)、引用(到文件中其他记录的偏移量)还是数据块(例如,特定编码的字符串数据,但不包含任何引用)。

    如果精心设计,这不仅可以让文件格式一次保存数据,还可以根据需要增量保存数据。如果子结构设计正确,它可以不依赖于应用程序,但仍然允许编写垃圾收集应用程序,该应用程序可以理解blob、数组和引用记录类型,并且能够跟踪文件并消除未使用的记录(即不再指向的记录)。

    这只是一个想法。其他可以寻找想法的地方是通用文件系统设计,或关系数据库物理存储策略。

    当然,根据你的要求,这可能有些过分。您可以简单地使用二进制格式来保存内存数据,在这种情况下,要考虑的方法是标记记录。

    在这种方法中,每段数据都以一个标记作为前缀。标记指示紧跟其后的数据的类型,可能还指示其长度和名称。列表的后缀可能是一个没有有效负载的“结束列表”标记。标记可能有一个嵌入的标识符,因此序列化机制在读取中的内容时可以忽略不理解的标记。在这方面,它有点像XML,只是使用了二进制习惯用法。

    实际上,XML是一个寻找文件格式长期有效性的好地方。看看它的名称空间功能。如果仔细构造读写代码,应该可以编写应用程序来保留他们不理解的标记(递归)数据的位置和内容,这可能是因为它是由同一应用程序的更高版本编写的。

        7
  •  3
  •   Roger Nelson    17 年前

    确保保留一个标记代码(最好在每个标记中保留一个位),用于指定已删除/空闲的块/区块。 然后,只需将块的当前标记代码更改为已删除的标记代码或设置标记的已删除位,即可删除块。 这样,当你删除一个块时,你就不需要马上完全重组你的文件。

    在标记中保留一个位提供了可能取消删除块的选项 (如果保持块的数据不变)。

    然而,为了安全起见,您可能希望将已删除块的数据归零,在这种情况下,您将使用一个特殊的删除/释放标记。

    我同意Stepan的观点,你应该选择一个endianess,但我也会在文件中有一个endianess指示符。 如果你使用了一个字节索引,你可以考虑使用 其中一个 UniCode Byte Order Marks 也可作为用于任何文本块的任何UniCode文本编码的索引。BOM表通常是UniCode文本文件的前几个字节,因此,如果BOM表是文件中的第一个条目,则可能会出现一些实用程序将文件标识为UniCode文本的问题(我认为这不是什么问题)。 我会将BOM表视为一个普通的标记(如果使用16位标记,则使用UTF16 BOM;如果使用32位标记,则使用UTF32 BOM),并保留一个0长度的块/区块。

    另见 http://en.wikipedia.org/wiki/File_format

        8
  •  3
  •   Kevin Cox    12 年前

    在开始之前要知道的最重要的事情之一是如何使用文件。

    • 随机存取或顺序存取会成为常态吗?
    • 多久读取一次数据?
    • 数据多久写入一次?
    • 你是一次就把文件写出来,还是随着数据的到来,你会放慢写文件的速度。
    • 文件需要便携吗?并非所有格式都需要。
    • 它需要与其他版本兼容吗?也许更新文件就足够了。
    • 它需要易于读/写吗?
    • 规模/速度/竞争力权衡。

    这里的大多数答案在可移植性/兼容性方面给出了很好的建议,所以我不打算再补充更多。但是考虑下面的(经常被忽视的)事情。

    • 有些文件经常被写入,很少被读取(备份、日志等)你可能想专注于文件大小和简单的书写。
    • 如果文件永远不会离开主机,或者很少离开主机,那么转换endianness会比较慢(相对而言),因为转换是一个很好的选择,您可以获得显著的性能提升。考虑写一个数字,如0x1234作为头的一部分,这样您就可以检测(并指示用户转换),如果是这样的话。
    • 有时简单的阅读真的很有用。如果您正在做日志或文本文档,请考虑压缩一个GO,而不是每个条目,这样您就可以。 zcat | strings 打开文件,看看里面是什么。

    有很多事情需要记住,设计一个好的格式需要大量的计划和远见。比如 zcat 通过使用本机整数复制一个文件并获得有用的信息或小的性能提升可以让您的产品获得优势,但是您需要小心,不要为了获得它而牺牲一些重要的东西。

        9
  •  2
  •   user21037 user21037    17 年前

    我同意atzz关于使用标签长度值系统的建议。为了将来的兼容性,您可以在开始时存储一组指向TLV条目的“指针”(或者标记、指针并使指针指向一个长度、值;或者标记、长度、指针,然后将所有数据放在一起?)。

    所以,我的文件可能看起来像:

    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字节等等)。

    这个 标签 让您知道存储的内容。这个 指针 告诉您 长 告诉您数据有多大,以及 价值 是 长 类型的数据字节数 标签 . 如果在MyFileFormat v2文件上使用MyFileFormat v1解码器,指针允许您跳过v1解码器无法理解的部分。如果您只是跳过无效的标记,那么您可能只需要使用TLV而不是TPLV。

    我要么手工编写这样的代码,要么用 ASN.1 并生成一个编解码器(我在电信行业工作,所以ASN.1/TLV对我来说是有意义的:-D)

        10
  •  2
  •   BenW    13 年前

    如果你处理的是可变长度的数据,那么 很 更有效地使用指针:最好在文件开头附近有一个指向数据的指针数组,而不是直接将数据存储在数组中。

    在这种情况下,间接寻址更可取,因为它允许随机访问,这只有在所有项大小相同的情况下才可能实现。如果数据直接存储在一个数组中,而不指定任何记录的位置,那么数据访问将需要( N )在最坏的情况下是时间;为了让你的文件读取代码访问一个特定的元素,它必须知道之前所有元素的长度,唯一的方法是查看每个元素。如果您同时读取整个文件,那么无论如何您都会这样做,所以这不会是一个问题。但是如果你只想要一件事,那么这不是你想要的方式。

    而对于指针数组,则是O(1)时间:您只需要一个索引号,就可以检索并跟踪指针以获取数据。

    当使用这种方法写入文件时,在进行任何写入之前,您当然必须在内存中建立表。