JSON 字符串中的二进制数据。比 Base64 更好的东西

Posted

技术标签:

【中文标题】JSON 字符串中的二进制数据。比 Base64 更好的东西【英文标题】:Binary Data in JSON String. Something better than Base64 【发布时间】:2010-11-29 09:55:14 【问题描述】:

JSON format 本身不支持二进制数据。必须对二进制数据进行转义,以便可以将其放入 JSON 中的字符串元素(即使用反斜杠转义的双引号中的零个或多个 Unicode 字符)。

转义二进制数据的一个明显方法是使用 Base64。但是,Base64 的处理开销很高。此外,它将 3 个字节扩展为 4 个字符,这导致数据大小增加了约 33%。

这方面的一个用例是CDMI cloud storage API specification 的 v0.8 草案。您可以使用 JSON 通过 REST-Webservice 创建数据对象,例如

PUT /MyContainer/BinaryObject HTTP/1.1
Host: cloud.example.com
Accept: application/vnd.org.snia.cdmi.dataobject+json
Content-Type: application/vnd.org.snia.cdmi.dataobject+json
X-CDMI-Specification-Version: 1.0

    "mimetype" : "application/octet-stream",
    "metadata" : [ ],
    "value" :   "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz
    IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg
    dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu
    dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo
    ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=",

有没有更好的方法和标准的方法将二进制数据编码成 JSON 字符串?

【问题讨论】:

上传:你只做一次,所以没什么大不了的。对于下载,您可能会对 base64 compresses under gzip 的性能感到惊讶,因此如果您在服务器上启用了 gzip,您也可能没问题。 对于铁杆书呆子的另一个有价值的解决方案msgpack.org:github.com/msgpack/msgpack/blob/master/spec.md @cloudfeet,每个用户每个操作一次。非常重要。 请注意,每个字符通常是 2 字节的内存。因此,base64 可能会在线路上产生 +33% (4/3) 的开销,但将数据放在线路上、检索并利用它,将需要 +166% (8/3 ) 开销。恰当的例子:如果 javascript 字符串的最大长度为 100k 字符,则使用 base64 只能表示 37.5k 字节的数据,而不是 75k 字节的数据。这些数字可能是应用程序许多部分的瓶颈,例如JSON.parse等...... @Pacerier“通常 2 个字节的内存 [每个字符]”不准确。例如 v8 有 OneByte 和 TwoByte 字符串。仅在必要时使用两字节字符串以避免怪异的内存消耗。 Base64 可以用一字节字符串编码。 【参考方案1】:

根据 JSON 规范(如果您的 JSON 以 UTF-8 传输),有 94 个 Unicode 字符可以表示为一个字节。考虑到这一点,我认为你可以在空间方面做的最好的事情是base85,它将四个字节表示为五个字符。但是,这仅比 base64 提高了 7%,计算成本更高,并且实现不如 base64 常见,因此它可能不是胜利。

您也可以简单地将每个输入字节映射到 U+0000-U+00FF 中的相应字符,然后执行 JSON 标准要求的最小编码来传递这些字符;这里的优点是,除了内置函数之外,所需的解码为零,但空间效率很差——扩展了 105%(如果所有输入字节的可能性相同),而 base85 为 25%,base64 为 33%。

最终判决:在我看来,base64 获胜,因为它很常见、很容易,而且还不错足够需要替换。

另请参阅:Base91 和 Base122

【讨论】:

等等,如何在编码引号字符时使用实际字节进行 105% 的扩展而 base64 只有 33%? base64 不是 133% 吗? Base91 对于 JSON 来说是个坏主意,因为它包含字母表中的引号。在 JSON 编码后的最坏情况下(所有引号输出),它是原始有效负载的 245%。 Python 3.4 现在包括 base64.b85encode()b85decode()。一个简单的编码+解码时序测量表明 b85 比 b64 慢 13 倍以上。所以我们获得了 7% 的规模优势,但性能损失了 1300%。 @hobbs JSON 声明必须对控制字符进行转义。 RFC20 section 5.2 将 DEL 定义为控制字符。 @Tino ECMA-404 专门列出了需要转义的字符:双引号 U+0022、反斜杠 U+005C、“控制字符 U+0000 到 U+001F”。 【参考方案2】:

我遇到了同样的问题,并想分享一个解决方案:multipart/form-data。

通过发送多部分表单,您首先将您的 JSON 元数据作为字符串发送,然后作为 Content 索引的原始二进制文件(图像、wavs 等)单独发送-性格名称。

这里有一个很好的tutorial,关于如何在 obj-c 中执行此操作,这里是 a blog article,它解释了如何使用表单边界对字符串数据进行分区,并将其与二进制数据分开。

您真正需要做的唯一更改是在服务器端;您将必须捕获您的元数据,这些元数据应该适当地引用 POST 的二进制数据(通过使用 Content-Disposition 边界)。

当然,这需要在服务器端进行额外的工作,但如果您要发送许多图像或大图像,这是值得的。如果需要,可以将其与 gzip 压缩结合使用。

恕我直言,发送 base64 编码数据是一种 hack; RFC multipart/form-data 是针对以下问题创建的:发送二进制数据与文本或元数据。

【讨论】:

顺便说一下,Google Drive API 是这样做的:developers.google.com/drive/v2/reference/files/update#examples 为什么这个答案在使用原生功能而不是尝试将圆形(二进制)钉子挤入方形(ASCII)孔时如此低?... 发送 base64 编码数据是一种 hack,multipart/form-data 也是如此。即使您链接的博客文章也显示 通过使用您声明的 Content-Type multipart/form-data,您发送的实际上是一个表单。但事实并非如此。 所以我认为 base64 hack 不仅更容易实现,而且更可靠我见过一些库(例如 Python),它们具有 multipart/form-data 内容类型硬编码。 @t3chb0t multipart/form-data 媒体类型是为传输表单数据而生的,但如今它在 HTTP/html 世界之外被广泛使用,特别是用于编码电子邮件内容。今天,它被提议作为一种通用的编码语法。 tools.ietf.org/html/rfc7578 @MarkKCowan 可能是因为虽然这对问题的目的有帮助,但它并没有回答所问的问题,这实际上是“用于 JSON 的文本编码的低开销二进制”,这个answer 完全抛弃了 JSON。【参考方案3】:

BSON(二进制 JSON)可能适合您。 http://en.wikipedia.org/wiki/BSON

编辑: 仅供参考,.NET 库 json.net 支持读写 bson,如果您正在寻找一些 C# 服务器端的爱。

【讨论】:

“在某些情况下,由于长度前缀和显式数组索引,BSON 将使用比 JSON 更多的空间。” en.wikipedia.org/wiki/BSON 好消息:BSON 本身支持二进制、日期时间和其他一些类型(如果您使用 MongoDB,则特别有用)。坏消息:它的编码是二进制字节......所以它不是对 OP 的回答。但是,它在原生支持二进制的通道上会很有用,例如 RabbitMQ 消息、ZeroMQ 消息或自定义 TCP 或 UDP 套接字。【参考方案4】:

UTF-8 的问题在于它不是最节省空间的编码。此外,一些随机二进制字节序列是无效的 UTF-8 编码。因此,您不能将随机二进制字节序列解释为某些 UTF-8 数据,因为它将是无效的 UTF-8 编码。这种对 UTF-8 编码的约束的好处是,它使它变得健壮并且可以定位多字节字符的开始和结束我们开始查看的任何字节。

因此,如果在 [0..127] 范围内编码一个字节值在 UTF-8 编码中只需要一个字节,那么在 [128..255] 范围内编码一个字节值将需要 2 个字节! 比这更糟糕。在 JSON 中,控制字符、" 和 \ 不允许出现在字符串中。因此二进制数据需要进行一些转换才能正确编码。

让我们看看。如果我们假设二进制数据中的随机字节值是均匀分布的,那么平均而言,一半字节将被编码为一个字节,另一半字节被编码为两个字节。 UTF-8 编码的二进制数据将具有初始大小的 150%。

Base64 编码仅增长到初始大小的 133%。所以Base64编码效率更高。

使用另一种基本编码怎么样?在 UTF-8 中,对 128 个 ASCII 值进行编码是最节省空间的。在 8 位中,您可以存储 7 位。因此,如果我们将二进制数据切割成 7 位块以将它们存储在 UTF-8 编码字符串的每个字节中,则编码数据将仅增长到初始大小的 114%。优于 Base64。不幸的是,我们不能使用这个简单的技巧,因为 JSON 不允许一些 ASCII 字符。 ASCII 的 33 个控制字符([0..31] 和 127)以及 " 和 \ 必须排除。这样我们只剩下 128-35 = 93 个字符。

所以理论上我们可以定义一个 Base93 编码,它将编码大小增加到 8/log2(93) = 8*log10(2)/log10(93) = 122%。但是 Base93 编码不如 Base64 编码方便。 Base64 要求将输入字节序列切割成 6 位块,简单的按位操作效果很好。在 133% 之外不超过 122%。

这就是为什么我独立得出一个共同结论,即 Base64 确实是在 JSON 中编码二进制数据的最佳选择。我的回答为此提供了一个理由。我同意从性能的角度来看它不是很有吸引力,但也要考虑使用 JSON 的好处,它是人类可读的字符串表示,易于在所有编程语言中操作。

如果性能比纯二进制编码更重要,则应将其视为 JSON 的替代品。但是对于 JSON,我的结论是 Base64 是最好的。

【讨论】:

Base128 怎么样,然后让 JSON 序列化程序转义 " 和 \ ?我认为期望用户使用 json 解析器实现是合理的。 @jcalfee314 不幸的是,这是不可能的,因为 JSON 字符串中不允许使用 ASCII 码低于 32 的字符。已经定义了基数在 64 到 128 之间的编码,但所需的计算量高于 base64。编码文本大小的增益是不值得的。 如果在 base64 中加载大量图像(比如说 1000),或者通过非常慢的连接加载,base85 或 base93 是否会为减少的网络流量付费(w/或w/o gzip )?我很好奇是否会出现更紧凑的数据会成为其中一种替代方法的理由。 我怀疑计算速度比传输时间更重要。图像显然应该在服务器端预先计算。无论如何,结论是 JSON 对二进制数据不利。 Re "Base64 编码只增长到初始大小的 133% 所以 Base64 编码效率更高",这是完全错误的,因为每个字符通常是 2 个字节。详见***.com/questions/1443158/…【参考方案5】:

如果您处理带宽问题,请尝试先在客户端压缩数据,然后再进行 base64-it。

这种魔法的好例子是http://jszip.stuartk.co.uk/,更多关于这个话题的讨论是JavaScript implementation of Gzip

【讨论】:

这是一个声称性能更好的 JavaScript zip 实现:zip.js 请注意,您也可以(并且应该)在之后压缩(通常通过Content-Encoding),因为 base64 压缩得很好。 @MahmoudAl-Qudsi 你的意思是你 base64(zip(base64(zip(data))))?我不确定添加另一个 zip 然后 base64 (以便能够将其作为数据发送)是个好主意。 @andrej 他的意思是在 web 服务器中启用压缩,这显然支持二进制,所以你的代码做 base64(zip(data)) 但客户端或服务器在发送之前对 ASCII 进行压缩(二进制)线,另一端在将其交给接收器代码之前解压缩,接收器代码接收 ASCII 并只是解压缩(decode64(received)) @android.weasel AFAIK 服务器端压缩只压缩服务器输出【参考方案6】:

yEnc 可能对你有用:

http://en.wikipedia.org/wiki/Yenc

"yEnc 是一种二进制转文本的编码方案,用于传输二进制 [文本] 中的文件。它减少了以前基于 US-ASCII 的开销 编码方法使用 8 位扩展 ASCII 编码方法。 yEnc 的开销通常是(如果每个字节值大约出现 平均频率相同)低至 1-2%,与 uuencode 和 Base64 等 6 位编码方法的开销为 33%–40%。 ... 到 2003 年,yEnc 成为事实上的标准编码系统 Usenet 上的二进制文件。”

但是,yEnc 是 8 位编码,因此将其存储在 JSON 字符串中与存储原始二进制数据存在相同的问题 - 以幼稚的方式进行操作意味着大约 100% 的扩展,这比 base64 更糟糕。

【讨论】:

由于很多人似乎仍在查看这个问题,我想提一下,我认为 yEnc 在这里并没有真正的帮助。 yEnc 是一种 8 位编码,因此将其存储在 JSON 字符串中与存储原始二进制数据存在相同的问题——以天真的方式进行操作意味着大约 100% 的扩展,这比 base64 更糟糕。 如果使用带有 JSON 数据的大字母的 yEnc 等编码被认为是可以接受的,escapeless 可以作为一个很好的替代方案,提供固定的预先已知开销。【参考方案7】:

虽然 base64 的扩展率确实约为 33%,但处理开销并不一定比这大得多:它实际上取决于您使用的 JSON 库/工具包。编码和解码是简单直接的操作,甚至可以针对字符编码进行优化(因为 JSON 仅支持 UTF-8/16/32)——对于 JSON 字符串条目,base64 字符始终是单字节的。 例如,在 Java 平台上,有一些库可以相当高效地完成这项工作,因此开销主要是由于扩展了大小。

我同意之前的两个答案:

base64 是简单的常用标准,因此不太可能找到更好的专门用于 JSON 的东西(base-85 被 postscript 等使用;但考虑到它的好处充其量是微不足道的) 编码前(和解码后)压缩可能很有意义,具体取决于您使用的数据

【讨论】:

【参考方案8】:

Smile format

编码、解码和压缩都非常快

速度比较(基于 java 但有意义):https://github.com/eishay/jvm-serializers/wiki/

它也是 JSON 的扩展,允许您跳过字节数组的 ba​​se64 编码

当空间很重要时,可以压缩微笑编码的字符串

【讨论】:

... 链接已失效。这似乎是最新的:github.com/FasterXML/smile-format-specification【参考方案9】:

由于您正在寻找将二进制数据强制转换为严格基于文本且非常有限的格式的能力,我认为与您期望使用 JSON 维护的便利性相比,Base64 的开销是最小的。如果处理能力和吞吐量是一个问题,那么您可能需要重新考虑您的文件格式。

【讨论】:

【参考方案10】:

7 年后编辑: Google Gears 已消失。忽略此答案。)


Google Gears 团队遇到了缺少二进制数据类型的问题,并试图解决它:

Blob API

JavaScript 具有用于文本字符串的内置数据类型,但没有用于二进制数据。 Blob 对象试图解决这个限制。

也许你可以以某种方式编织它。

【讨论】:

那么 Javascript 和 json 中 blob 的状态是什么?它被丢弃了吗? w3.org/TR/FileAPI/#blob-section 对于空间而言,性能不如 base64,如果向下滚动,您会发现它使用 utf8 映射进行编码(霍布斯的回答显示的选项之一)。据我所知,没有 json 支持【参考方案11】:

只是为了在讨论中添加资源和复杂性的观点。由于执行 PUT/POST 和 PATCH 是为了存储和更改新资源,因此应该记住内容传输是存储内容的精确表示,并且通过发出 GET 操作接收。

多部分消息通常被用作救星,但出于简单原因和更复杂的任务,我更喜欢将内容作为一个整体提供的想法。这是不言自明的,而且很简单。

是的,JSON 是很糟糕的东西,但最终 JSON 本身是冗长的。而且映射到 BASE64 的开销是很小的。

正确使用多部分消息,必须拆除要发送的对象,使用属性路径作为自动组合的参数名称,或者需要创建另一种协议/格式来表达有效负载。

也喜欢 BSON 方法,但它并不像人们希望的那样得到广泛和容易的支持。

基本上,我们只是在这里遗漏了一些东西,但是将二进制数据嵌入为 base64 已经很成熟,而且还有很长的路要走,除非您真的确定需要进行真正的二进制传输(这种情况很少见)。

【讨论】:

在 .NET 中发送和接收多部分消息并不有趣,过于复杂和抽象。只发送原始字符串更容易,因此您可以实际调试并查看发送和接收的内容,并将字符串转换为服务器上的 JSON 对象或类对象。 JSON 或 XML 字符串中的 Base64 易于调试【参考方案12】:

我挖掘了一点(在base128 的实现过程中),并揭示了当我们发送ascii 代码大于128 的字符时,浏览器(chrome)实际上发送两个字符(字节)而不是一个:(。原因是JSON默认使用utf8字符,其中ascii代码高于127的字符由chmike答案提到的两个字节编码。我以这种方式进行了测试:输入chrome url栏 chrome://net-export/ ,选择“包含原始字节”,开始捕获,发送 POST 请求(使用底部的 sn-p),停止捕获并保存带有原始请求数据的 json 文件. 然后我们看里面那个json文件:

我们可以通过查找字符串4142434445464748494a4b4c4d4e找到我们的base64请求,这是ABCDEFGHIJKLMN的十六进制编码,我们将看到"byte_count": 639。 我们可以通过查找字符串C2BCC2BDC380C381C382C383C384C385C386C387C388C389C38AC38B找到我们上面的127请求,这是字符¼½ÀÁÂÃÄÅÆÇÈÉÊË的request-hex utf8代码(但是这个字符的ascii十六进制代码是c1c2c3c4c5c6c7c8c9cacbcccdce)。 "byte_count": 703 所以它比 base64 请求长 64 字节,因为 ascii 代码高于 127 的字符在请求中按 2 字节编码:(

所以实际上我们发送代码 >127 的字符并没有好处 :( 。对于 base64 字符串,我们没有观察到这种负面行为(可能也适用于 base85 - 我不检查它) - 但是可能是一些解决方案问题将是在Ælex answer 中描述的 POST multipart/form-data 的二进制部分发送数据(但通常在这种情况下,我们根本不需要使用任何基本编码......)。

另一种方法可能依赖于通过使用类似 base65280 / base65k 的代码将两个字节数据部分映射到一个有效的 utf8 字符,但由于 utf8 specification ,它可能不如 base64 有效。 ..

function postBase64() 
  let formData = new FormData();
  let req = new XMLHttpRequest();

  formData.append("base64ch", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
  req.open("POST", '/testBase64ch');
  req.send(formData);



function postAbove127() 
  let formData = new FormData();
  let req = new XMLHttpRequest();

  formData.append("above127", "¼½ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüý");
  req.open("POST", '/testAbove127');
  req.send(formData);
<button onclick=postBase64()>POST base64 chars</button>
<button onclick=postAbove127()>POST chars with codes>127</button>

【讨论】:

【参考方案13】:

只是添加另一个我们低级恐龙程序员使用的选项...

Intel HEX 格式是在时间黎明三年后一直存在的老派方法。它成立于 1973 年,UNIX 时代始于 1970 年 1 月 1 日。

效率更高吗?没有。 这是一个完善的标准吗?是的。 它是否像 JSON 一样具有人类可读性?是的,而且比大多数二进制解决方案更具可读性。

json 看起来像:


    "data": [
    ":10010000214601360121470136007EFE09D2190140",
    ":100110002146017E17C20001FF5F16002148011928",
    ":10012000194E79234623965778239EDA3F01B2CAA7",
    ":100130003F0156702B5E712B722B732146013421C7",
    ":00000001FF"
    ]

【讨论】:

效率低吗?是的。 我们知道它的空间效率较低。时间效率低吗?它肯定更易于人类阅读。【参考方案14】:

在 Node.js 中,您可以将 Buffer 转换为字符串并返回而无需任何更改:

const serialized = buffer.toString("binary")
const deserialized = Buffer.from(serialized, "binary")

如果您希望通过牺牲大小来获得更高的可靠性,请将 "binary" 替换为 "base64"

【讨论】:

测试和认可? 如果您想要 100% 的可靠性,请将“binary”替换为“base64”【参考方案15】:

另一个更新颖的想法是通过uuencode 对数据进行编码。这是一个主要被弃用的,但它可能仍然是一个替代方案。 (虽然可能不是很严重。)

【讨论】:

【参考方案16】:

数据类型确实令人担忧。我已经测试了从 RESTful 资源发送有效负载的不同场景。对于编码,我使用了 Base64(Apache) 和压缩 GZIP(java.utils.zip.*)。有效载荷包含有关电影、图像和音频文件的信息。我已经压缩和编码了图像和音频文件,这大大降低了性能。压缩前的编码效果很好。图像和音频内容作为编码和压缩字节 [] 发送。

【讨论】:

【参考方案17】:

参考:http://snia.org/sites/default/files/Multi-part%20MIME%20Extension%20v1.0g.pdf

它描述了一种使用“CDMI 内容类型”操作在 CDMI 客户端和服务器之间传输二进制数据的方法,而无需对二进制数据进行 base64 转换。

如果您可以使用“非 CDMI 内容类型”操作,那么将“数据”传输到/从对象传输是理想的。元数据随后可以作为后续的“CDMI 内容类型”操作添加到对象/从对象中检索。

【讨论】:

【参考方案18】:

我现在的解决方案,XHR2 正在使用 ArrayBuffer。 ArrayBuffer 作为二进制序列包含具有多种内容类型的多部分内容、视频、音频、图形、文本等。一站式响应。

在现代浏览器中,为不同的组件提供 DataView、StringView 和 Blob。 另请参阅:http://rolfrost.de/video.html 了解更多详情。

【讨论】:

您将通过序列化字节数组使您的数据增长+100% @Sharcoux wot?? JSON 中字节数组的序列化类似于:[16, 2, 38, 89],效率非常低。

以上是关于JSON 字符串中的二进制数据。比 Base64 更好的东西的主要内容,如果未能解决你的问题,请参考以下文章

base64加密比原来的数据长度增加多少

如何通过 iOS 6 中的 RESTkit 2.0 将 JSON 中的 base64 图像导入核心数据二进制文件?

php 图片用base64转码完的文本比以前还大 是为啥?

Base64编码及iOS中的Base64

01加密方式-Base64编码

base64 和 urlencode的区别