打包二进制是有趣的
为什么会有人这样做呢?我在推特上看到有人争辩说,将数据保存为 JSON 显然不是专业开发者所做的事情。显然,我也必须成为一个真正的开发者。结果,答案就是二进制。自然,我想到了一个绝妙的主意:“制作我自己的二进制格式有多难呢?” 想必这只是稍微的 wb 。不幸的是,这并不仅仅是个 wb 。我最后建立了一个完整的二进制模式语言,能够将 JSON 负载缩小 80%。 jBin 什么是二进制打包?假设我们有一些数据 "hello world",那么它将在 ASCII 中被转换为 104 101 108 108 111 32 119 111 114 108 100 h e l l o [SPACE] w o r l d 所以 h 在 ASCII 中变成 104。因为这些 ASCII 值适合在 8 位之内,每个字符占用 1 字节。h e l 01101000 01100101 01101100 l o [SPACE] 01101100 01101111 00100000 w o r 01110111 01101111 01110010 l d 01101100 01100100 所以我们可以用一些简单的 C 代码来写它: FILE *f = fopen ( "file.bin" , "wb" ); unsigned char data [] = "hello world" ; fwrite (data, 1 , sizeof (data) - 1 , f); fclose (f); 很简单。现在让我们试着写 104。显然,我们可以直接将 104 写成 ASCII 字符: '1' '0' '4' → 49 48 52 但是这需要 3 字节,而这个数字只需要 1 字节。所以如果我们想节省这 2 字节,我们需要某种方法来告诉解码器,“嘿,这是一整数,不是字符串。”你可以添加一个头部,这样只需添加一个额外的字节(这取决于你有多少种类型……希望你没有超过 128 种类型……如果你有的话,那你就有更大的问题了。好吧!让我们用第一个字节来表示我们的类型,第二个字节来表示我们的数据。 [TYPE][DATA] 假设 0 是整数,1 是字符串,那么 104 将被表示为 00000000 01101000,而字符串将是…… 00000001 01101000 哎等等……那样只会给我们 h 我们需要一种方法来表示不同长度的数据。好吧,让我们再加一个字节。它应该表示我们字符串的长度。所以现在我们的二进制变成了 [TYPE][LENGTH][DATA] 太棒了!现在我们可以像这样表示我们的字符串:[TYPE] [LENGTH] [DATA] (STRING) (11) 00000001 00001011 00… h e l 01101000 01100101 01101100 l o [SPACE] 01101100 01101111 00100000 w o r 01110111 01101111 01110010 l d 01101100 01100100 太棒了!现在我们可以一起打包字符串和整数了!假设我们想表示 "userid": 123 现在你可以将所有的东西打包在一起 [TYPE:STRING][LENGTH:6][WORD:userid][TYPE:INT][LENGTH:-][DATA:123] 太棒了!我们可以将 123 表示为一个字节的数字,加上 2 字节的头部。但注意,我们并不真的在使用 LENGTH 字段对于整数?那它有什么用呢?浪费字节,对吧? 好吧……如果我们去掉它,我们的二进制读取器怎么知道头部在哪里结束?它需要某种方法来表示“好吧,头部完成了,现在开始读取实际数据。” 嗯。我们可以用什么来表示一个字节的结束。头部的长度字节,或许? 嗯。这是冗余的。我们是去掉长度字段以添加另一个长度字段吗?但是嘿,我们可以在头部本身使用一个位。我们可以有一个位表示:我不是头部中的最后一个字节。还有更多。你可能会想:为什么不使用 LSB(最低有效位)?那么我们只能表示偶数。这并不是……理想。所以我们将使用 MSB(最高有效位)代替。所以现在我们的标志看起来像这样:[CONTINUATION BIT][7 BITS OF DATA] 如果继续位是 1,则还有另一个头部字节。如果它是 0,头部就完成了。使用这个,我们可以将 123 变成 [TYPE=0][DATA=123] 如果它是字符串则为 [TYPE=1][LENGTH=11][DATA=104]……所以它是类型 1,长度 11,但等等……如果长度大于 127 呢?用 7 位你只能表示最多 127!我们采用同样的方法!但是对于整数!如果第一位是 1,则整数继续。 128 可以写成:10000001 00000000 ^ MSB / 继续位(大端) 二进制读取器将做的事情:读取第一个字节。MSB 是 1,因此还有另一个字节。剩下的 7 位是 1。读取第二个字节。它的 MSB 是 0,因此这是最后一个字节。剩下的 7 位是 0。将两个 7 位的值合并得到 128。这是一种变长整型(varint)。我们在这里使用的编码是小端:最低有效的 7 位先到。 10000000 00000001 嘿,这太棒了,不是吗?你可以在同一个二进制中表示不同的类型,并且你的二进制解析器会正确读取它们。但是要注意,我们是按字段存储这些数据的。 [TYPE][DATA] [TYPE][DATA] [TYPE][DATA] 而且大多数数据并不仅仅是一堆随机值浮动。它通常是结构化的。看看 C 结构: struct { int i; char *s; int a [ 10 ]; } 这将在 32 位系统上表示为:[32 位整数] [32 位指针] [10 × 32 位整数] 而且我们不需要每次都添加头部。因为我们从结构本身就知道数据的类型。嗯。我想知道我们是否可以为我们的二进制数据做到这一点……是的,我们可以。这就是模式的意义!所以对于我们的结构,我们的模式可以简单表示为: i: int s: char * a: list(int) 模式让我们知道类型,而不需要将类型存储在每个值旁边。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡