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


613

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

逃脱二进制数据的一种明显方法是使用Base64。但是,Base64具有较高的处理开销。它还将3个字节扩展为4个字符,这导致数据大小增加了约33%。

一个用例是CDMI云存储API规范的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字符串?


29
对于上传:您只需要执行一次,所以没什么大不了的。对于下载,您可能会惊讶于base64 在gzip下的压缩效果如何,因此,如果您在服务器上启用了gzip,则可能还可以。
cloudfeet 2014年

2
针对顽固书呆子的另一个值得解决的方法msgpack.org:github.com/msgpack/msgpack/blob/master/spec.md
nicolallias 2014年

2
@cloudfeet,每个用户每次操作一次。非常重要。
佩里耶

2
请注意,每个字符通常是2个字节的内存。因此,base64可能会在网络上提供+ 33%(4/3)的开销,但是将数据放在网络上,对其进行检索并加以利用将需要+ 166%(8/3)的开销。举例:如果一个Javascript字符串的最大长度为100k个字符,则使用base64只能表示37.5k字节的数据,而不能表示75k字节的数据。这些数字可能是应用程序许多部分的瓶颈,例如,JSON.parse等等。……
Pacerier

5
@Pacerier“通常2个字节的内存[每个字符]”不正确。例如,v8具有OneByte和TwoByte字符串。仅在必要时使用两字节字符串,以避免怪异的内存消耗。Base64可以用一字节字符串编码。
ZachB '18

Answers:


459

根据JSON规范,有94个Unicode字符可以表示为一个字节(如果JSON以UTF-8格式传输)。考虑到这一点,我认为您可以在空间上做到最好的是base85它将85个字节表示为四个字节。但是,这仅比base64改善了7%,计算成本更高,并且实现比base64少见,因此可能不是一个成功。

您还可以简单地将每个输入字节映射到U + 0000-U + 00FF中的相应字符,然后执行JSON标准所需的最小编码以传递这些字符;这样做的好处是所需的解码比内置函数少了零,但是空间效率很差-扩展了105%(如果所有输入字节的可能性均等),而base85为25%,base64为33%。

最终判决:BASE64胜,在我看来,其理由是它的普通,简单,不坏足够的认股权证更换。

另请参阅:Base91Base122


5
等一下,如何在编码引号字符时使用实际字节扩展105%,而对base64编码仅33%?base64不是133%吗?
jjxtra

17
Base91对于JSON是个坏主意,因为它包含字母引号。在最坏的情况下(所有引号输出),在JSON编码之后,它是原始有效负载的245%。
jarnoh's

25
Python 3.4包括base64.b85encode()b85decode()现在。一个简单的编码+解码时序测量结果显示,b85比b64慢13倍以上。因此,我们赢得了7%的规模,但性能损失了1300%。
Pieter Ennes 2014年

3
@hobbs JSON指出必须转义控制字符。 RFC20第5.2节定义DEL为控制字符。
蒂诺2015年

2
@Tino ECMA-404专门列出了需要转义的字符:双引号U + 0022,反斜杠U + 005C和“控制字符U + 0000到U + 001F”。
hobbs 2015年

249

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

通过发送多部分表单,您首先以字符串形式发送JSON元数据,然后分别以原始二进制文件(图像,wav等)发送,该文件由Content-Disposition名称索引。

这是一个很好的教程,介绍如何在obj-c中执行此操作,这是一篇博客文章,介绍了如何使用形式边界对字符串数据进行分区,并将其与二进制数据分开。

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

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

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


4
顺便说一句,Google Drive API就是这样进行的:developers.google.com/drive/v2/reference/files/update#examples
Mathias Conradt 2015年

2
为什么当使用本地功能而不是尝试将圆形(二进制)钉子压入方形(ASCII)孔时,此答案如此低下?
Mark K Cowan

5
发送base64编码的数据很容易受到攻击,multipart / form-data也是如此。甚至您链接的博客文章都读到,通过使用您声明的Content-Type multipart / form-data,您发送的实际上是一个表单。但事实并非如此。因此,我认为base64 hack不仅易于实现,而且更可靠。我已经看到一些库(例如Python),这些库具有多部分/表单数据内容类型的硬编码。
t3chb0t

4
@ t3chb0t multipart / form-data媒体类型诞生于传输表单数据,但是今天它在HTTP / HTML世界之外被广泛使用,特别是用于编码电子邮件内容。今天,它被提议为通用编码语法。tools.ietf.org/html/rfc7578
lorenzo

3
@MarkKCowan可能是因为虽然这对问题的目的有所帮助,但它不能回答所提出的问题,实际上是“用于JSON的文本编码的开销较低的二进制文件”,因此此答案完全可以解决JSON问题。
Chinoto Vokro,

34

UTF-8的问题在于它不是最节省空间的编码。同样,某些随机二进制字节序列是无效的UTF-8编码。因此,您不能仅将随机二进制字节序列解释为某些UTF-8数据,因为它将是无效的UTF-8编码。这种对UTF-8编码的限制的好处是,它使它变得强大并且可以定位多字节字符的开始和结束,无论我们开始看什么字节。

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

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

Base64编码仅增长到初始大小的133%。因此,Base64编码更加有效。

使用其他Base编码怎么办?在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

1
@ jcalfee314不幸的是,这是不可能的,因为JSON字符串中不允许ASCII码低于32的字符。已经定义了基数在64到128之间的编码,但是所需的计算量高于base64。编码文本大小的增加是不值得的。
chmike

如果在base64中加载大量图像(比如说1000),或者通过非常慢的连接加载,base85或base93会为减少的网络流量(不带gzip或不带gzip)付费吗?我很好奇是否会出现更紧凑的数据来说明其中一种替代方法的情况。
vol7ron

我怀疑计算速度比传输时间更重要。图像显然应该在服务器端预先计算。无论如何,结论是JSON对二进制数据不利。
chmike '16

关于“ Base64编码仅增长到初始大小的133%,所以Base64编码效率更高 ”,这是完全错误的,因为每个字符通常为2个字节。详见stackoverflow.com/questions/1443158/…–
Pacerier,

34

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

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


1
“在某些情况下,由于长度前缀和显式数组索引,BSON将比JSON使用更多的空间。” en.wikipedia.org/wiki/BSON
Pawel Cioch

好消息:BSON本机支持Binary,Datetime和其他一些类型(如果您使用的是MongoDB,则特别有用)。坏消息:它的编码是二进制字节...因此它不是OP的答案。但是,在本地支持二进制的通道(如RabbitMQ消息,ZeroMQ消息或自定义TCP或UDP套接字)上将很有用。
丹·H

19

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

关于这种魔术的一个很好的例子是在http://jszip.stuartk.co.uk/上,关于这个主题的更多讨论是在Gzip的JavaScript实现上。


2
这是一个声称具有更好性能的JavaScript zip实现:zip.js
Janus Troelsen

请注意,您也可以(并且应该)仍然进行压缩(通常通过Content-Encoding),因为base64的压缩效果很好。
Mahmoud Al-Qudsi

@ MahmoudAl-Qudsi您的意思是您base64(zip(base64(zip(data))))吗?我不确定添加另一个zip,然后对其进行base64(以便能够将其作为数据发送)是否是个好主意。
andrej

18

yEnc可能为您工作:

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

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

但是,yEnc是8位编码,因此将其存储在JSON字符串中与存储原始二进制数据存在相同的问题-天真地实现意味着100%的扩展,这比base64差。


42
由于很多人似乎仍然在看这个问题,因此我想提一提yEnc确实没有帮助。yEnc是8位编码,因此将其存储在JSON字符串中与存储原始二进制数据存在相同的问题-天真地实现意味着100%的扩展,这比base64差。
霍布斯

如果使用像yEnc这样的带有大字母和JSON数据的编码被认为是可以接受的,则无法逃逸可以作为提供固定的事前已知开销的好选择。
伊万·科萨列夫

10

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

我同意较早的两个答案:

  • base64是简单且常用的标准,因此不太可能找到更适合与JSON一起使用的东西(base-85用于后记等;但是考虑到这一点,其好处至多是微不足道的)
  • 编码之前(和解码之后)的压缩可能很有意义,具体取决于您使用的数据


4

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


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

Blob API

JavaScript具有用于文本字符串的内置数据类型,而对于二进制数据则没有。Blob对象尝试解决此限制。

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


那么,Javascript和json中Blob的状态如何?它被丢弃了吗?
chmike 2015年

w3.org/TR/FileAPI/#blob-section不如base64占用空间,如果向下滚动,您会发现它使用utf8映射进行编码(作为hobbs答案所示的选项之一)。据我所知,还没有json支持
Daniele Cruciani

3

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


2

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

通常将多部分消息用作救星,但出于简单原因和更复杂的任务,我更倾向于将内容作为整体给出。这是不言自明的,很简单。

是的,JSON有点令人毛骨悚然,但最终,JSON本身很冗长。而且映射到BASE64的开销是减小开销的一种方法。

正确使用Multi-Part消息,要么必须拆除要发送的对象,要么使用属性路径作为参数名称以进行自动组合,或者需要创建另一种协议/格式以仅表示有效载荷。

同样喜欢BSON的方法,它并没有像人们所希望的那样得到广泛而轻松的支持。

基本上,我们只是在这里遗漏了一些东西,但是嵌入二进制数据是因为base64已经很好地建立并且是可行的方法,除非您确实确定需要进行真正的二进制传输(这种情况很少见)。


1

我进一步挖掘了一点(在实现base128的过程中),并揭示了当我们发送的ASCII码大于128的字符时,浏览器(chrome)实际上发送了两个字符(字节),而不是一个:(。原因是JSON通过默认使用utf8字符,其中ASCII码高于127的字符由两个字节编码,由chmike答案提到。我以此方式进行了测试:在chrome网址栏中输入chrome:// net-export /,选择“包括原始个字节”,开始捕获,发送POST请求(使用底部的代码段),停止捕获并将json文件与原始请求数据一起保存。然后我们查看该json文件内部:

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

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

另一种方法可能是依靠将两个字节的数据部分映射为一个有效的utf8字符,方法是使用base65280 / base65k之类的代码对其进行编码,但由于utf8规范,它的效果可能不如base64 ...


0

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


0

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

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

如果可以使用“非CDMI内容类型”操作,则理想的是将“数据”与对象之间进行传输。然后可以将元数据作为后续的“ CDMI内容类型”操作添加到对象或从对象检索。


-1

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

在现代浏览器中,具有用于不同组件的DataView,StringView和Blob。另请参阅:http : //rolfrost.de/video.html,以获取更多详细信息。


您将通过序列化字节数组来使数据增长100%
Sharcoux

@Sharcoux wot ??
Mihail Malostanidis '18

JSON中字节数组的序列化类似于:[16, 2, 38, 89]效率非常低。
Sharcoux '18年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.