Dados binários na cadeia JSON. Algo melhor que o Base64

614

O formato JSON nativamente não suporta dados binários. Os dados binários precisam ser escapados para que possam ser colocados em um elemento de sequência (ou seja, zero ou mais caracteres Unicode entre aspas duplas usando escapes de barra invertida) no JSON.

Um método óbvio para escapar de dados binários é usar o Base64. No entanto, o Base64 possui uma alta sobrecarga de processamento. Também expande 3 bytes em 4 caracteres, o que aumenta o tamanho dos dados em cerca de 33%.

Um caso de uso para isso é o rascunho v0.8 da especificação da API de armazenamento em nuvem CDMI . Você cria objetos de dados por meio de um serviço da Web REST usando JSON, por exemplo

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=",
}

Existem maneiras melhores e métodos padrão para codificar dados binários em strings JSON?

dmeister
fonte
30
Para upload: você está fazendo isso apenas uma vez, por isso não é tão importante. Para o download, você pode se surpreender com a facilidade com que o base64 é compactado no gzip ; portanto, se o gzip está ativado no servidor, você provavelmente também está bem.
cloudfeet
2
Outro digno solução msgpack.org para os nerds de hardcore: github.com/msgpack/msgpack/blob/master/spec.md
nicolallias
2
@cloudfeet, uma vez por usuário por ação . Um negócio muito grande.
Pacerier 20/03/19
2
Observe que os caracteres geralmente têm 2 bytes de memória cada. Portanto, a base64 pode gerar + 33% (4/3) de sobrecarga no fio, mas colocar esses dados no fio, recuperá-lo e utilizá-lo, exigiria um gasto de + 166% (8/3) . Caso em questão: se uma string Javascript tiver um comprimento máximo de 100k caracteres, você poderá representar apenas 37,5k bytes de dados usando base64, e não 75k bytes de dados. Estes números podem ser um gargalo em muitas partes do pedido, por exemplo, JSON.parseetc ......
Pacerier
5
@Pacerier "normalmente 2 bytes de memória [por caractere]" não são precisos. A v8, por exemplo, possui seqüências de caracteres OneByte e TwoByte. Seqüências de caracteres de dois bytes são usadas apenas quando necessário para evitar o consumo de memória grotesco. Base64 é codificável com seqüências de caracteres de um byte.
ZachB

Respostas:

459

Existem 94 caracteres Unicode que podem ser representados como um byte de acordo com a especificação JSON (se o seu JSON for transmitido como UTF-8). Com isso em mente, acho que o melhor que você pode fazer em termos de espaço é a base85, que representa quatro bytes como cinco caracteres. No entanto, essa é apenas uma melhoria de 7% em relação à base64, é mais caro computar e as implementações são menos comuns do que na base64, portanto, provavelmente não é uma vitória.

Você também pode simplesmente mapear cada byte de entrada para o caractere correspondente em U + 0000-U + 00FF e, em seguida, executar a codificação mínima exigida pelo padrão JSON para transmitir esses caracteres; a vantagem aqui é que a decodificação necessária é nula além das funções internas, mas a eficiência de espaço é ruim - uma expansão de 105% (se todos os bytes de entrada forem igualmente prováveis) vs. 25% para base85 ou 33% para base64.

Veredicto final: base64 vence, na minha opinião, pelo fato de ser comum, fácil e não ruim o suficiente para justificar a substituição.

Veja também: Base91 e Base122

hobbs
fonte
5
Espere, como o uso do byte real ao codificar os caracteres de aspas é uma expansão de 105% e base64 apenas 33%? Base64 não é 133%?
Jjxtra
17
Base91 é uma má ideia para JSON, porque contém aspas no alfabeto. Na pior das hipóteses (saída de todas as cotações) após a codificação JSON, é 245% da carga útil original.
jarnoh
25
Python 3.4 inclui base64.b85encode()e b85decode()agora. Uma medição simples do tempo de codificação + decodificação mostra que b85 é mais de 13 vezes mais lento que b64. Portanto, temos 7% de vitória no tamanho, mas 1300% de perda de desempenho.
Pieter Ennes
3
@hobbs JSON afirma que caracteres de controle devem ser escapados. A seção 5.2 do RFC20 define DELcomo um caractere de controle.
Tino
2
O @Tino ECMA-404 lista especificamente os caracteres que precisam ser escapados: aspas duplas U + 0022, barra invertida U + 005C e "os caracteres de controle U + 0000 a U + 001F".
hobbs
249

Encontrei o mesmo problema e pensei em compartilhar uma solução: multipart / form-data.

Ao enviar um formulário com várias partes, você envia primeiro como string seus metadados JSON e, em seguida, envia separadamente como binário bruto (imagem (s), wavs, etc) indexado pelo nome Disposição de Conteúdo .

Aqui está um bom tutorial sobre como fazer isso no obj-c, e aqui está um artigo de blog que explica como particionar os dados da string com o limite do formulário e separá-lo dos dados binários.

A única alteração que você realmente precisa fazer é no lado do servidor; você precisará capturar seus metadados que devem fazer referência aos dados binários do POST adequadamente (usando um limite de Disposição de conteúdo).

Concedido, requer um trabalho adicional no lado do servidor, mas se você estiver enviando muitas imagens ou imagens grandes, vale a pena. Combine isso com a compactação gzip, se desejar.

O IMHO que envia dados codificados em base64 é um hack; o RFC multipart / form-data foi criado para problemas como este: envio de dados binários em combinação com texto ou metadados.

Ælex
fonte
4
A propósito, a API do Google Drive faz isso da seguinte maneira: developers.google.com/drive/v2/reference/files/update#examples
Mathias Conradt
2
Porque é que esta resposta tão baixo quando se usa recursos nativos em vez de tentar espremer um round (binário) peg em um quadrado (ASCII) buraco ...?
Mark K Cowan
5
enviar dados codificados em base64 é um hack, assim como multipart / form-data. Até o artigo do blog que você vinculou lê que Ao usar os dados de várias partes / formulário do tipo Content, você declara que o que você envia é realmente um formulário. Mas não é. então acho que o hack da base64 não é apenas muito mais fácil de implementar, mas também mais confiável . Já vi algumas bibliotecas (para Python, por exemplo), que tinham o tipo de conteúdo multipart / data-form codificado.
T3chb0t
4
@ t3chb0t O tipo de mídia multipart / form-data nasceu para transportar dados do formulário, mas hoje é amplamente usado fora do mundo HTTP / HTML, principalmente para codificar o conteúdo de email. Hoje é proposto como uma sintaxe de codificação genérica. tools.ietf.org/html/rfc7578
lorenzo
3
@MarkKCowan Provavelmente porque, embora isso seja útil para o objetivo da pergunta, ela não responde à pergunta solicitada, que é efetivamente "Binário de sobrecarga baixa para codificação de texto para uso em JSON", essa resposta descarta completamente o JSON.
Chinoto Vokro 2/11
34

O problema com o UTF-8 é que não é a codificação com mais espaço eficiente. Além disso, algumas seqüências aleatórias de bytes binários são codificação UTF-8 inválida. Portanto, você não pode simplesmente interpretar uma sequência de bytes binários aleatória como alguns dados UTF-8, porque será uma codificação UTF-8 inválida. O benefício dessa restrição na codificação UTF-8 é que ela torna robusta e possível localizar caracteres de vários bytes iniciando e terminando qualquer byte que começamos a observar.

Como conseqüência, se a codificação de um valor de byte no intervalo [0..127] precisaria de apenas um byte na codificação UTF-8, a codificação de um valor de byte no intervalo [128..255] exigiria 2 bytes! Pior que isso. No JSON, chars de controle "e \ não têm permissão para aparecer em uma sequência. Portanto, os dados binários exigiriam que alguma transformação fosse codificada corretamente.

Vamos ver. Se assumirmos valores de bytes aleatórios distribuídos uniformemente em nossos dados binários, então, em média, metade dos bytes será codificada em um bytes e a outra metade em dois bytes. Os dados binários codificados em UTF-8 teriam 150% do tamanho inicial.

A codificação Base64 cresce apenas para 133% do tamanho inicial. Portanto, a codificação Base64 é mais eficiente.

Que tal usar outra codificação Base? No UTF-8, a codificação dos 128 valores ASCII é a mais eficiente em espaço. Em 8 bits, você pode armazenar 7 bits. Portanto, se cortarmos os dados binários em pedaços de 7 bits para armazená-los em cada byte de uma string codificada em UTF-8, os dados codificados crescerão apenas para 114% do tamanho inicial. Melhor que o Base64. Infelizmente, não podemos usar esse truque fácil, porque o JSON não permite alguns caracteres ASCII. Os 33 caracteres de controle ASCII ([0..31] e 127) e o "e \ devem ser excluídos. Isso nos deixa apenas 128-35 = 93 caracteres.

Portanto, em teoria, poderíamos definir uma codificação Base93 que aumentaria o tamanho codificado para 8 / log2 (93) = 8 * log10 (2) / log10 (93) = 122%. Mas uma codificação Base93 não seria tão conveniente quanto uma codificação Base64. O Base64 requer o corte da sequência de bytes de entrada em blocos de 6 bits, para os quais a operação bit a bit simples funciona bem. Além de 133%, não há muito mais que 122%.

É por isso que cheguei independentemente à conclusão comum de que o Base64 é realmente a melhor opção para codificar dados binários no JSON. Minha resposta apresenta uma justificativa para isso. Concordo que não é muito atraente do ponto de vista do desempenho, mas considere também o benefício de usar JSON com sua representação de string legível por humanos fácil de manipular em todas as linguagens de programação.

Se o desempenho for crítico, uma codificação binária pura deve ser considerada como substituição do JSON. Mas com o JSON, minha conclusão é que o Base64 é o melhor.

chmike
fonte
E sobre Base128 mas em seguida, deixando o serializador JSON escapar do "e \ Eu acho que é razoável esperar que o usuário utilize uma implementação de analisador JSON?.
jcalfee314
1
@ jcalfee314 infelizmente isso não é possível porque caracteres com código ASCII abaixo de 32 não são permitidos em strings JSON. Codificações com uma base entre 64 e 128 já foram definidas, mas o cálculo necessário é maior que a base64. O ganho no tamanho do texto codificado não vale a pena.
Chmike
Se carregar uma grande quantidade de imagens na base64 (digamos 1000), ou carregar através de uma conexão muito lenta, a base85 ou a base93 pagariam pelo tráfego de rede reduzido (com ou sem gzip)? Estou curioso para chegar a um ponto em que os dados mais compactos justificariam um dos métodos alternativos.
vol7ron
Eu suspeito que a velocidade de computação é mais importante que o tempo de transmissão. Obviamente, as imagens devem ser pré-computadas no lado do servidor. Enfim, a conclusão é que JSON é ruim para dados binários.
chmike
Re "A codificação Base64 cresce apenas para 133% do tamanho inicial. Portanto, a codificação Base64 é mais eficiente ", isso é completamente errado, pois os caracteres geralmente têm 2 bytes cada. Veja a elaboração em stackoverflow.com/questions/1443158/…
Pacerier
34

BSON (JSON binário) pode funcionar para você. http://en.wikipedia.org/wiki/BSON

Edit: FYI, a biblioteca .NET json.net suporta leitura e gravação de bson se você estiver procurando por algum amor em C # pelo lado do servidor.

DarcyThomas
fonte
1
"Em alguns casos, o BSON usará mais espaço que o JSON devido aos prefixos de comprimento e aos índices explícitos da matriz." en.wikipedia.org/wiki/BSON
Pawel Cioch
Boas notícias: o BSON oferece suporte nativo a tipos como Binary, Datetime e alguns outros (particularmente útil se você estiver usando o MongoDB). Más notícias: a codificação é de bytes binários ... portanto, não responde ao OP. No entanto, seria útil em um canal que suporta binário nativamente, como mensagem RabbitMQ, mensagem ZeroMQ ou um soquete TCP ou UDP personalizado.
Dan H
19

Se você lidar com problemas de largura de banda, tente compactar os dados primeiro no lado do cliente e depois base64-it.

Um bom exemplo dessa mágica está em http://jszip.stuartk.co.uk/ e mais discussão sobre esse tópico está na implementação do Gzip em JavaScript

andrej
fonte
2
aqui está uma implementação zip JavaScript que alega melhor desempenho: zip.js
Janus Troelsen
Observe que você pode (e deve) ainda compactar depois (normalmente via Content-Encoding), pois o base64 compacta muito bem.
Mahmoud Al-Qudsi
@ MahmoudAl-Qudsi você quis dizer que você base64 (zip (base64 (zip (data))))? Não tenho certeza de que adicionar outro zip e, em seguida, base64 (para poder enviá-lo como dados) é uma boa ideia.
22419
18

O yEnc pode funcionar para você:

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

"yEnc é um esquema de codificação binário em texto para transferir arquivos binários em [texto]. Reduz a sobrecarga em relação aos métodos de codificação anteriores baseados em ASCII dos EUA, usando um método de codificação ASCII estendido de 8 bits. A sobrecarga do yEnc é frequentemente (se cada valor de byte aparece aproximadamente com a mesma frequência, em média), de 1 a 2%, em comparação com a sobrecarga de 33% a 40% dos métodos de codificação de 6 bits, como uuencode e Base64 ... Em 2003, o yEnc se tornou o padrão de fato sistema de codificação para arquivos binários na Usenet. "

No entanto, o yEnc é uma codificação de 8 bits, portanto, armazená-lo em uma string JSON tem os mesmos problemas que armazenar os dados binários originais - fazê-lo da maneira ingênua significa cerca de uma expansão de 100%, o que é pior que o base64.

richardtallent
fonte
42
Como muitas pessoas ainda parecem estar vendo essa pergunta, gostaria de mencionar que não acho que o yEnc realmente ajude aqui. O yEnc é uma codificação de 8 bits, portanto, armazená-lo em uma string JSON tem os mesmos problemas que armazenar os dados binários originais - fazê-lo da maneira ingênua significa cerca de uma expansão de 100%, pior do que a base64.
hobbs
Nos casos em que o uso de codificações como o yEnc com alfabetos grandes com dados JSON é considerado aceitável, a escapeless pode funcionar como uma boa alternativa, fornecendo uma sobrecarga fixa conhecida antecipadamente.
Ivan Kosarev
10

Embora seja verdade que a base64 tenha ~ 33% de taxa de expansão, não é necessariamente verdade que a sobrecarga de processamento seja significativamente mais do que isso: depende realmente da biblioteca / kit de ferramentas JSON que você está usando. Codificação e decodificação são operações simples e diretas, e podem até ser otimizadas para codificação de caracteres wrt (como o JSON suporta apenas UTF-8/16/32) - os caracteres base64 são sempre de byte único para as entradas da string JSON. Por exemplo, na plataforma Java, existem bibliotecas que podem fazer o trabalho com bastante eficiência, de modo que a sobrecarga se deve principalmente ao tamanho expandido.

Eu concordo com duas respostas anteriores:

  • base64 é simples, padrão comumente usado, por isso é improvável encontrar algo melhor especificamente para usar com JSON (a base-85 é usada pelo postscript etc; mas os benefícios são, na melhor das hipóteses, marginais quando você pensa sobre isso)
  • A compactação antes da codificação (e após a decodificação) pode fazer muito sentido, dependendo dos dados usados
StaxMan
fonte
10

Formato de sorriso

É muito rápido para codificar, decodificar e compactar

Comparação de velocidade (baseada em java, mas significativa): https://github.com/eishay/jvm-serializers/wiki/

Também é uma extensão para JSON que permite ignorar a codificação base64 para matrizes de bytes

Seqüências de caracteres codificadas por sorriso podem ser compactadas quando o espaço é crítico

Stefano Fratini
fonte
3
... e o link está morto. Este parece atualizado: github.com/FasterXML/smile-format-specification
Zero3
4

( Edite 7 anos depois: o Google Gears se foi. Ignore esta resposta.)


A equipe do Google Gears encontrou o problema da falta de tipos de dados binários e tentou resolvê-lo:

API de blob

O JavaScript possui um tipo de dados interno para cadeias de texto, mas nada para dados binários. O objeto Blob tenta solucionar essa limitação.

Talvez você possa tecer isso de alguma forma.

um nerd pago
fonte
Então, qual é o status dos blobs em Javascript e json? Foi descartado?
chmike
w3.org/TR/FileAPI/#blob-section Não é tão eficiente quanto o base64 por espaço, se você rolar para baixo, verá que ele codifica usando o mapa utf8 (como a opção mostrada pela resposta de hobbs). E nenhum suporte json, até onde eu sei
Daniele Cruciani
3

Como você procura a capacidade de converter dados binários em um formato estritamente baseado em texto e muito limitado, acho que a sobrecarga do Base64 é mínima em comparação com a conveniência que você espera manter com o JSON. Se a capacidade de processamento e a taxa de transferência forem uma preocupação, você provavelmente precisará reconsiderar seus formatos de arquivo.

jsoverson
fonte
2

Apenas para adicionar o ponto de vista de recursos e complexidade à discussão. Como você faz PUT / POST e PATCH para armazenar novos recursos e alterá-los, deve-se lembrar que a transferência de conteúdo é uma representação exata do conteúdo que é armazenado e recebido pela emissão de uma operação GET.

Uma mensagem de várias partes é frequentemente usada como salvadora, mas por motivos de simplicidade e para tarefas mais complexas, prefiro a ideia de fornecer o conteúdo como um todo. É auto-explicativo e é simples.

E sim, o JSON é algo incapacitante, mas no final o próprio JSON é detalhado. E a sobrecarga do mapeamento para o BASE64 é uma maneira pequena.

Usando as mensagens de várias partes corretamente, é necessário desmontar o objeto a ser enviado, usar um caminho de propriedade como o nome do parâmetro para a combinação automática ou será necessário criar outro protocolo / formato para expressar apenas a carga útil.

Também gostando da abordagem BSON, isso não é tão amplo e facilmente suportado como se gostaria.

Basicamente, apenas faltamos alguma coisa aqui, mas a incorporação de dados binários como base64 está bem estabelecida e é um caminho a menos, a menos que você realmente tenha identificado a necessidade de fazer a transferência binária real (o que geralmente não é o caso).

Martin Kersten
fonte
1

Eu cavo um pouco mais (durante a implementação da base128 ) e exponho que, quando enviamos caracteres cujos códigos ascii são maiores que 128, o navegador (chrome) na verdade envia dois caracteres (bytes) em vez de um :( . O motivo é que o JSON por padrão, use utf8 caracteres para os quais os caracteres com códigos ASCII acima de 127 são codificados por dois bytes, o que foi mencionado pela resposta chmike.Fiz teste desta maneira: digite chrome bar na barra de URL chrome: // net-export / , selecione "Incluir bruto bytes ", comece a capturar, envie solicitações POST (usando o snippet na parte inferior), pare de capturar e salve o arquivo json com dados brutos das solicitações. Depois, examinamos o arquivo json:

  • Podemos encontrar nossa solicitação base64 encontrando a string em que 4142434445464748494a4b4c4d4eesta é a codificação hexadecimal ABCDEFGHIJKLMNe veremos isso "byte_count": 639nela.
  • Podemos encontrar nossa solicitação acima127 encontrando a string C2BCC2BDC380C381C382C383C384C385C386C387C388C389C38AC38Bthis is request -hex utf8 códigos de caracteres ¼½ÀÁÂÃÄÅÆÇÈÉÊË(no entanto, os códigos hexadecimais ASCII desses caracteres são c1c2c3c4c5c6c7c8c9cacbcccdce). Por "byte_count": 703isso, são 64 bytes a mais do que a solicitação base64 porque os caracteres com códigos ascii acima de 127 têm código de 2 bytes na solicitação :(

Portanto, na verdade, não temos lucro com o envio de caracteres com códigos> 127 :(. Para cadeias base64, não observamos um comportamento tão negativo (provavelmente também para base85 - eu não a verifico) - no entanto, pode haver alguma solução para esse problema. envio de dados na parte binária do POST multipart / form-data descrito na resposta Ælex (no entanto, geralmente nesse caso, não precisamos usar nenhuma codificação básica ...).

A abordagem alternativa pode depender do mapeamento de uma porção de dados de dois bytes em um caractere utf8 válido, codificando-o usando algo como base65280 / base65k, mas provavelmente seria menos eficaz que base64 devido à especificação utf8 ...

Kamil Kiełczewski
fonte
0

O tipo de dados realmente preocupa. Testei diferentes cenários ao enviar a carga útil de um recurso RESTful. Para codificação, usei o Base64 (Apache) e o GZIP para compactação (java.utils.zip. *). A carga contém informações sobre filme, imagem e arquivo de áudio. Compactei e codifiquei os arquivos de imagem e áudio que degradaram drasticamente o desempenho. A codificação antes da compactação acabou bem. O conteúdo da imagem e do áudio foi enviado como bytes codificado e compactado [].

Koushik
fonte
0

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

Ele descreve uma maneira de transferir dados binários entre um cliente e servidor CDMI usando operações 'tipo de conteúdo CDMI', sem exigir a conversão base64 dos dados binários.

Se você pode usar a operação 'Tipo de conteúdo não CDMI', é ideal transferir 'dados' para / de um objeto. Mais tarde, os metadados podem ser adicionados / recuperados no / para o objeto como uma operação subsequente 'tipo de conteúdo CDMI'.

Dheeraj Sangamkar
fonte
-1

Minha solução agora, XHR2 está usando ArrayBuffer. O ArrayBuffer como sequência binária contém conteúdo de várias partes, vídeo, áudio, gráfico, texto e assim por diante com vários tipos de conteúdo. Tudo em uma resposta.

No navegador moderno, possui DataView, StringView e Blob para diferentes componentes. Veja também: http://rolfrost.de/video.html para mais detalhes.

Rolf Rost
fonte
Você vai fazer seus dados crescem + 100%, serialização uma matriz de bytes
Sharcoux
@Sharcoux wot ??
Mihail Malostanidis
A serialização de uma matriz de bytes no JSON é algo como: o [16, 2, 38, 89]que é muito ineficiente.
21418 Sharcoux