devise(二进制)文件格式时有什么要点?
在devise用于logging二进制数据的文件格式时,您认为该格式应具有哪些属性? 到目前为止,我已经提出了以下重要的观点:
- 在开始时有一些“魔术字节”,以便能够识别文件(在我的具体情况下,这也应该有助于区分文件与“传统”文件)
- 在开始时有一个文件版本号,以便文件格式可以更改,而不会破坏兼容性
- 指定所有数据项的字节顺序和大小; 或者:包括一些空间来描述数据的排列顺序/大小(我倾向于前者)
- 可能为将来可能需要的每个文件属性保留一些空间?
还有什么可以使这个格式更加面向未来,最大限度地减less未来的头痛呢?
看看PNG规范 。 这种格式有一些非常好的理由背后。
另外,请确定对未来格式的重要性:紧凑性,兼容性,允许在其中embedded其他格式(不同的压缩algorithm)。 另一个有趣的例子是Google的协议缓冲区 ,其中传输的数据的大小是国王。
至于endianness,我build议你select一个选项,坚持下去,不允许不同的字节顺序。 否则,阅读和书写图书馆将变得越来越复杂和慢。
我同意这些是好主意:
-
魔术数字在开始。 * nix非常需要 :
-
用于向后兼容的文件版本号。
-
字节顺序规范。
但是你的第四个是矫枉过正,因为只要你改变版本号(只要你不需要兼容性 ),#2就可以添加字段。
- 可能为将来可能需要的每个文件属性保留一些空间?
此外,在许多其他答案中expression的在您的文件上加上块结构的想法似乎不像对二进制文件的通用要求,而是解决某些types有效载荷的问题。
除了上面的1-3,我还会添加这些:
-
简单的校验和或其他检测内容是否完整的方式。 否则,你不能相信魔术字节或版本号。 请注意规定哪些字节包含在校验和中。 通常情况下,您将包含文件中尚未检测到错误的所有字节。
-
写入文件的软件版本(包括您拥有的最细粒度的数字,例如内部版本号)。 你将得到一个附加文件的错误报告,这个文件是无法打开它的人的,当他们写这个文件的时候,他们不会有任何线索,因为这个错误没有发生。 但是这个错误是在写它的版本中,而不是在试图阅读它的版本中。
-
在规范中明确指出这是一种二进制格式,即所有字节(除了幻数)都允许所有0-255的值。
这里有一些可选的:
-
如果你需要向前兼容,你需要某种方式来表示哪些“块”是“可选的”(比如png),这样以前的软件版本可以优雅地跳过它们。
-
如果你期望这些文件能够“在野外”find,你可以考虑embedded一些线索来查找规范。 想象一下,在png文件中findstringhttp://www.w3.org/TR/PNG/是多么有用。
当然,这一切都取决于格式的目的。
一种灵活的方法是将整个文件构造为TLV(标签长度 – 值)三元组。 例如,使您的文件成为logging,每个logging以4字节的标题开头:
1 byte = record type 3 bytes = record length followed by record content
关于字节顺序,如果在文件中存储字节序指示符,所有的应用程序将不得不支持所有的字节顺序格式。 另一方面,如果为文件指定了特定的字节顺序,那么只有具有不匹配字节的平台上的应用程序才需要做额外的工作,并且可以在编译时(使用条件编译)来决定。
只是为了logging,我发现这个链接 ,这可能与上面的问题有关。
另一点,取自.xz文件规范( http://tukaani.org/xz/xz-file-format.txt ):前几个字节之一应该是非字符“,以防止应用程序错误检测文件作为文本文件“。 请注意,编辑器和其他工具通常检查多less个标题字节,但在前四个或八个字节中使用非二进制字节似乎很有用。
我会考虑定义一个更高级别用于存储数据的子结构,有点像文件内部的小型文件系统。
例如,即使您的文件格式要存储特定于应用程序的数据,我也会考虑在文件内部定义logging/stream等,以便与应用程序无关的代码能够理解文件的布局,但不能当然了解不透明的有效载荷。
让我们来更具体一点。 考虑将数据存储在内存中的常用方法:通常可以将它们分解为连续的可扩展数组/列表,基于指针/参考的graphics以及特定格式的二进制数据块。
因此,按照类似的方式定义二进制文件格式可能是有益的。 使用表示以下数据的长度和组成的logging标题,无论它是以数组(相同types的logging的列表),引用(对文件中其他logging的偏移量)还是数据块(例如string数据在一个特定的编码,但不包含任何引用)。
如果仔细devise,这可以使文件格式不仅用于一次性保存数据,而且还可以根据需要进行增量。 如果子结构devise得当,它可以是应用程序不可知的,但仍允许例如写入垃圾收集应用程序,其理解blob,数组和引用loggingtypes,并且能够追踪文件并消除未使用的logging(即logging不再指向)。
这只是一个想法。 其他寻找创意的地方一般是文件系统devise或关系数据库物理存储策略。
当然,根据您的要求,这可能是矫枉过正的。 您可能只是在二进制格式之后坚持内存数据,在这种情况下,需要考虑的方法是标记logging。
在这种方法中,每一条数据都以一个标签为前缀。 该标签指示紧随其后的数据的types,并可能指出其长度和名称。 列表可能后缀有一个没有有效载荷的“结束列表”标签。 这个标签可能有一个embedded的标识符,因此标签在被读取时可以被序列化机制忽略。在这方面,它有点像XML,除了使用二进制方式。
实际上,XML是查找文件格式的长期使用的好地方。 看看它的命名空间function。 如果您仔细构build您的读写代码,应该可以编写应用程序来保留标记(recursion)数据的位置和内容,这些数据可能是因为它是由相同应用程序的更高版本编写的。
确保你保留一个标签代码(或者更好的是在每个标签中保留一点),指定一个被删除/空闲的块/块。 只需将块的当前标记码更改为已删除的标记码或设置标记的已删除位即可删除块。 这样,当你删除一个块时,你不需要马上重新构造你的文件。
在标签中保留一位提供了可能取消删除块的选项(如果不改变块的数据)。
但是,为了安全起见,您可能需要将已删除块的数据清零,在这种情况下,您可以使用特殊的已删除/免费标签。
我同意斯捷潘,你应该select一个endianess,但我也有一个endianess指标在文件中。 如果您使用字节数指示器,则可以考虑使用其中一个字符序列标记作为用于任何文本块的任何UniCode文本编码的指示器。 BOM通常是UniCoded文本文件的前几个字节,所以如果您的BOM是您文件中的第一个条目,则可能存在将文件标识为UniCode文本的一些实用程序的问题(我认为这不是什么问题) 。 我将把BOM作为一个普通标签(如果使用的是16位标签,则使用UTF16 BOM,如果使用32位标签,则使用UTF32 BOM)。
一种未来certificate文件的方法是提供块。 在你的文件头数据之后,你可以开始第一个块。 块可以有一个字节或字的代码types的块,然后一个字节的大小。 现在您可以随意添加新的块types,并且可以跳到块的末尾。
我同意atzz提出的使用Tag Length Value系统的build议。 为了将来的兼容性,你可以在开始时存储一组“指针”到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等)。
标签让你知道什么是存储。 指针告诉你在哪里(偏移量或绝对值,以字节为单位), 长度告诉你数据有多大, 值是types标记数据的长度字节。 如果您在MyFileFormat v2文件上使用MyFileFormat v1解码器,指针允许您跳过v1解码器不理解的部分。 如果你只是跳过无效的标签,你可能简单地使用TLV而不是TPLV。
我要么手工编写这样的代码,要么在ASN.1中定义我的格式,并生成一个编解码器(我在电信工作,所以ASN.1 / TLV对我有意义:-D)
在开始之前要知道的最重要的事情之一是如何使用你的文件。
- 随机或顺序访问是常态吗?
- 多久才能读取数据?
- 数据多久写一次?
- 你会一口气写下这个文件,还是会在数据input的时候慢慢写出来。
- 该文件需要是便携式的? 并非所有格式都需要。
- 它是否需要与其他版本兼容? 也许更新文件就足够了。
- 它是否需要易于读取/写入?
- 大小/速度/ Compexity权衡。
大多数的答案在这里给可移植性/兼容性方面提供良好的build议,所以我不会添加更多。 但考虑以下(经常被忽视)的事情。
- 有些文件经常被写入,很less被读取(备份,日志,…),您可能需要关注文件大小和易于编写。
- 如果您的文件永远不会离开主机,或者离开的时间太less,那么转换字节顺序就会变慢(相对),那么转换是一个很好的select,您可以获得显着的性能提升。 考虑写一个数字,如0x1234作为标题的一部分,以便您可以检测(并指示用户转换),如果是这种情况。
- 有时候简单的阅读真的很有用。 如果您正在做日志或文本文档,请考虑一次压缩所有内容,而不是每次input,以便您可以
zcat | strings
zcat | strings
文件,看看里面是什么。
有很多事情要记住,devise一个好的格式需要大量的计划和远见。 像zcat
一个文件,获得有用的信息或使用本地整数小性能提升的小东西可以给你的产品一个优势,但是你需要小心,你不要牺牲一些重要的东西来获得它。
如果处理的是可变长度的数据,那么使用指针会更有效率:使用指针数组,理想情况下是靠近文件起始位置,而不是直接将数据存储在数组中。
在这种情况下,间接性是可取的,因为它允许随机访问,这只有在所有项目都是相同大小的情况下才是可能的。 如果数据直接存储在一个数组中,而没有指定任何logging的位置,则在最坏的情况下数据访问将花费O( n )个时间; 为了让你的文件读取代码访问一个特定的元素,它将不得不知道所有以前的元素的长度,唯一的方法是找出每个元素。 如果你一次读完整个文件,那么你会这样做,所以这不会是一个问题。 但是如果你只想要一件事,那么这是不行的。
而对于指针数组,全是O(1)的时间:所有你需要的是一个索引号,你可以检索并按照指针来获取你的数据。
当使用这种方法编写文件时,在写任何内容之前,你当然必须在内存中build立你的表。