C#/.NET - 自定义二进制文件格式 - 如何入门?

9
我需要能够将一些数据存储在自定义的二进制文件格式中。我以前从未设计过自己的文件格式。它需要是一个友好的格式,可以在C#,Java和Ruby / Perl / Python世界之间传递。
首先,文件将由记录组成。一个GUID字段和一个JSON / YAML / XML数据包字段。我不确定要使用什么作为分隔符。逗号、制表符或换行似乎太脆弱了。Excel做了什么?或者是XML之前的OpenOffice格式?应该使用ASCII字符0或1。不确定从哪里开始。有关此主题的任何文章或书籍吗?
这个文件格式可能会后续扩展以包括“头部节”。
注意:起初我将在.NET中工作,但我希望该格式易于移植。
更新: “数据包”的处理可能很慢,但是文件格式内的导航不能慢。所以我认为XML不可行。

1
关于编辑:这里的使用场景是什么?在许多情况下,您选择不在文件内导航,而是将其反序列化为对象模型,然后在其中工作。如果需要更多功能,那么最好使用某种(常见的)数据库文件。 - Marc Gravell
1
我应该补充说明这个文件太大了,无法序列化。因此,我永远不希望一次将所有数据都存储在内存中。它可以被序列化为List<SomeObject>,但我需要一个分隔符,这样我就不必一次读取整个列表。 - BuddyJoe
5个回答

7
如何考虑使用"协议缓冲区"?它被设计为高效、便携、版本兼容的通用二进制格式,提供了C++、Java和Python等语言的Google库,以及C#、Perl、Ruby等语言的社区端口。请注意,Guid没有特定的数据类型,但您可以将其作为消息进行调整,其中包含(基本上)一个byte[]。通常情况下,在.NET工作中,我会推荐protobuf-net(但作为作者,我有点偏见),但是,如果您打算以后使用其他语言,最好使用Jon的dotnet-protobufs;这样可以在各个平台上获得熟悉的API(而protobuf-net使用.NET习惯用语)。

而 Python - 这是谷歌直接提供的语言之一。 - Jon Skeet
我在思考是否应该依赖Protocol Buffers(即使它是Apache许可证)。还是应该从Protocol Buffers的工作中学习有关二进制文件格式的知识。我认为我已经使用了Json.NET并且这是MIT许可证。 - BuddyJoe

3

我会尝试提供一些有关创建便携式二进制文件格式的通用提示。

请注意,发明二进制文件格式意味着要记录其中的位应如何排列以及它们的含义。这不是编写代码,而是文档编写。

现在是提示:

  1. 决定如何处理字节序(endianess)。一个好且简单的方法是确定一次并永久使用。如果在常见的个人电脑上(即x86),选择小端字节序以避免转换(提高性能)。

  2. 创建文件头(header)。是的,始终具有文件头是一个好主意。文件的前几个字节应该能够告诉你正在处理什么格式。

    • 从魔数开始,以便能够识别你的格式(ASCII字符串就足够了)
    • 添加版本。加入文件格式的版本不会有害,它将使您能够进行向后兼容性。
  3. 最后,添加数据。现在,数据的格式将是特定的,并且它总是基于您确切需求的某些数据结构。基本上,数据将存储在某些数据结构的二进制图像中。数据结构是您需要想出的。

如果您需要通过某种索引来随机访问数据,则B-Trees是最好的选择,而如果您只需编写大量数字,然后全部读取它们,“数组”即可完成。

此外,您可能会使用类型-长度-值(TLV)概念以实现向前兼容性。


有关于“文件格式”中的“页面”的知识建议吗?有哪些文章或书籍可以阅读? - BuddyJoe
当我说“页面”时,我的意思是像数据库页面一样。SQLite 的 C 代码有点难以理解。也许 Java 或 C# 的示例我能更清楚地理解。 - BuddyJoe

3
ASCII字符0或1每个占用多个比特(就像任何其他字符一样),因此,如果您像这样存储它,您的“二进制”文件将比应该大几倍。由0和1组成的文本文件不完全是二进制文件:)
您可以使用BinaryWriter将原始数据直接写入文件流中。您需要解决的唯一部分是将内存中的格式(通常是某种对象图)转换为二进制编写器可以消耗的字节序列。

然而,如果您的主要关注点是可移植性,我建议不要使用二进制格式。 XML 的设计目的正是为了解决可移植性和互操作性问题。它作为文件格式冗长而笨重,但这是您为解决这些问题所做的权衡。 如果不考虑人类可读的格式,Marc's answer 是可行的方法。没有必要重新发明可移植性轮子!


2
不需要为了获得可移植性而牺牲速度和大小——可以参考马克的协议缓冲区答案。虽然在编码形式下会失去人类可读性(但是你可以将PB转储为文本),并且需要事先指定结构,但你可以免费获得大小、速度和前向/后向兼容性。 - Jon Skeet
你提到了ASCII注释的一个好点。大多数人如何在二进制格式中分隔字符串的开头或结尾?我知道我的GUID将具有标准长度,但我的“数据包”将基于字符串。我听说过“空终止”字符串这个术语。那是什么?我缺乏适当的计算机科学学位。 - BuddyJoe
@Jon Skeet 这是一个很好的观点。对我来说,协议缓冲区和像 XML 这样的人类可读格式之间的问题只是需要的可移植性、灵活性和开放性的程度。我的专业经验倾向于需要非常开放的格式,因此我总是首先推荐一些类似 XML 的东西 :) - Rex M
@Tundall - 对于字符串/数组数据,最好的方法是在数据前面加上大小。然后,如果您不需要它,可以跳过它。另一种选择是使用一些特殊标记(例如0,在常规文本中不会出现)作为结尾 - 但是当然,您不能在二进制数据(如GUID)中使用此选项,因为0是一个完全有效和预期的二进制值。因此,长度前缀成为最佳选项。 - Marc Gravell
例如(来自协议缓冲编码文档)- 12 07 74 65 73 74 69 6e 67表示“字段2作为字符串”(12), “7个字节”(07),“testing”(其余UTF8数据)。 我不会尝试解释“12”,或者长字符串的情况(需要超过1个字节来指定长度) - 但所有这些都是明确定义的。 - Marc Gravell
今晚我打算查看一下协议缓冲编码文档。如果我能在二进制编辑器中理解它,我也会研究一下SQLite文件格式。 - BuddyJoe

1
假设你的格式是:
    struct Format
    {
        struct Header // 1
        {
            byte a;
            bool b1, b2, b3, b4, b5, b6, b7, b8;
            string name;
        }
        struct Container // 1...*
        {
            MyTypeEnum Type;
            byte[] data;
        }
    }

    enum MyTypeEnum
    {
        Sound,
        Video,
        Image
    }

那么我将拥有一个顺序文件,其中包含:


字节 // a

字节 // b

整数 // 名称大小

字符数组[] // 名称(其大小在上面指定,记住在.NET中一个字符是16位)

整数 // MyTypeEnum类型

整数 // 数据大小

字节数组[] // 数据(其大小在上面指定)


然后,您可以重复最后三行任意次数。

要进行读取,您可以使用BinaryReader,它支持读取字节、整数和一系列字节。还有一个BinaryWriter

此外,请记住,Microsoft .NET(因此在Windows / Intel计算机上)是小端字节序。因此,BinaryReaderBinaryWriter也是如此。


请看我在这个线程上关于文件大小的另一个评论。我想我理解了BinaryReader/Writer,但这是否允许我逐步浏览文件?我不需要一次性反序列化整个文件,对吧? - BuddyJoe
BinaryReader/BinaryWriter只是.NET Stream的帮手。它是非缓存的,所以您可以直接访问BaseStream并寻找BinaryReader要读取的位置或BinaryWriter要写入的位置。FileStream支持向前和向后查找。因此,在头文件中有一个索引可能会帮助您只读取索引,然后跳转到您想要读取的位置。 - tofi9

1

这取决于你将要写入二进制文件的数据类型以及二进制文件的用途。它们是类对象还是记录数据?如果是记录数据,我建议将其放入XML格式中。这样,您可以包含模式验证以验证文件符合您的标准。在Java和.NET中都有工具可导入和导出数据到/从XML格式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接