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

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字符串?

656020 次浏览

yEnc可能为您工作:

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

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

但是,yEnc是一种8位编码,因此将其存储在JSON字符串中与存储原始二进制数据具有相同的问题-以朴素的方式执行它意味着大约100%的扩展,这比Base64更糟糕。

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

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


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

Blob API

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

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

根据JSON规范(如果您的JSON以UTF-8格式传输),有94个Unicode字符可以表示为一个字节。考虑到这一点,我认为您在空格方面能做的最好的是bas85,它将四个字节表示为五个字符。然而,这只比Base64提高了7%,计算成本更高,并且实现不如Base64常见,所以可能不是胜利。

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

最终判决:Base64获胜,在我看来,理由是它很常见,容易,而且不坏足够值得更换。

请参阅:Base91Base122

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

我同意前面两个回答:

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

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

这种魔法的不错的例子是在http://jszip.stuartk.co.uk/,更多关于这个话题的讨论是在Gzip的JavaScript实现

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

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

微笑格式

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

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

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

当空间很关键时,可以对微笑编码的字符串进行gzip压缩

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

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

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

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

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

因此,如果在[0…127]范围内编码一个字节值,在UTF-8编码中只需要一个字节,那么在[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是最好的。

我现在的解决方案是XHR2使用ArrayBuffer。ArrayBuffer作为二进制序列包含多部分内容、视频、音频、图形、文本等多种内容类型。All in One Response。

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

我遇到了同样的问题,并想分享一个解决方案:多部分/表单数据。

通过发送多部分表单,您首先将JSON元数据作为字符串发送,然后单独发送由内容-处置名称索引的原始二进制(图像、波浪等)。

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

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

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

IMHO发送base 64编码数据是一种黑客行为;RFC multipart/form-data是为这样的问题创建的:将二进制数据与文本或元数据组合发送。

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

多部分消息通常被用作救世主,但出于简单的原因和更复杂的任务,我更喜欢将内容作为一个整体给出的想法。它是自我解释的,而且很简单。

是的,JSON是一种严重的东西,但最终JSON本身是冗长的。映射到BASE 64的开销很小。

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

同样喜欢BSON方法,这并不像人们希望的那样得到广泛和轻松的支持。

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

深入

我又挖了一点(在实现bas128的过程中),并暴露了当我们发送ascii代码大于128的字符时,浏览器(chrome)实际上会发送两个字符(字节)而不是一个:(。原因是deaul的JSON使用utf8字符,其中ascii代码在127以上的字符由两个字节编码,这是chmike答案中提到的。我以这种方式进行了测试:输入chrome url barchrome://已导出,选择“包含原始字节”,开始捕获,发送POST请求(使用底部的代码段),停止捕获并使用原始请求数据保存json文件。然后我们查看那个json文件:

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

因此,实际上我们发送代码>127:(的字符没有好处。对于base 64字符串,我们没有观察到这种负面行为(可能对于base 85也是如此-我不检查它)-然而,这个问题的一些解决方案可能是在埃莱克斯答案中描述的POST multipart/form-data的二进制部分发送数据(然而,通常在这种情况下,我们根本不需要使用任何基本编码…)。

另一种方法可能依赖于将两个字节的数据部分映射到一个有效的utf8字符,通过使用类似Base65280/65k的代码,但由于utf8规范

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>

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

intelhex格式是一种古老的学校方法,它在三年后就已经存在了。它成立于1973年,UNIX时代始于1970年1月1日。

  • 它更有效率吗?不。
  • 这是一个公认的标准吗?是的。
  • 它像JSON一样人类可读吗?是的,比大多数二进制解决方案更具可读性。

json看起来像:

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

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

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

如果您希望通过牺牲尺寸来提高可靠性,请将"binary"替换为"base64"

另一个更新颖的想法是通过uuencode对数据进行编码。这是一个大多被弃用的想法,但它可以仍然是一个替代方案。(尽管可能不是一个严肃的想法。)