Données binaires dans la chaîne JSON. Quelque chose de mieux que Base64

615

Le format JSON ne prend pas nativement en charge les données binaires. Les données binaires doivent être échappées afin de pouvoir être placées dans un élément de chaîne (c'est-à-dire zéro ou plusieurs caractères Unicode entre guillemets en utilisant des échappements antislash) dans JSON.

Une méthode évidente pour échapper aux données binaires est d'utiliser Base64. Cependant, Base64 a une surcharge de traitement élevée. Il étend également 3 octets en 4 caractères, ce qui entraîne une augmentation de la taille des données d'environ 33%.

Un cas d'utilisation pour cela est le brouillon v0.8 de la spécification de l'API de stockage cloud CDMI . Vous créez des objets de données via un REST-Webservice en utilisant JSON, par exemple

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

Existe-t-il de meilleures méthodes et méthodes standard pour coder les données binaires en chaînes JSON?

dmeister
la source
30
Pour le téléchargement: vous ne le faites qu'une seule fois, donc ce n'est pas si grave. Pour le téléchargement, vous pourriez être surpris de la qualité de la compression de base64 sous gzip , donc si vous avez activé gzip sur votre serveur, vous êtes également probablement OK.
cloudfeet
2
Une autre solution valable msgpack.org pour les nerds hardcore: github.com/msgpack/msgpack/blob/master/spec.md
nicolallias
2
@cloudfeet, une fois par utilisateur par action . Très gros problème.
Pacerier
2
Notez que les caractères sont généralement de 2 octets de mémoire chacun. Ainsi, base64 pourrait donner + 33% (4/3) de frais généraux sur le fil, mais mettre ces données sur le fil, les récupérer et les utiliser, nécessiterait un + 166% (8/3) de frais généraux . Exemple: si une chaîne Javascript a une longueur maximale de 100k caractères, vous ne pouvez représenter que 37,5k octets de données en utilisant base64, pas 75k octets de données. Ces chiffres peuvent être un goulot d'étranglement dans de nombreuses parties de l'application, par exemple JSON.parseetc ......
Pacerier
5
@Pacerier "généralement 2 octets de mémoire [par caractère]" n'est pas précis. v8, par exemple, a des chaînes OneByte et TwoByte. Les chaînes de deux octets ne sont utilisées que lorsque cela est nécessaire pour éviter une consommation de mémoire grotesque. Base64 est encodable avec des chaînes d'un octet.
ZachB

Réponses:

460

Il y a 94 caractères Unicode qui peuvent être représentés comme un octet selon la spécification JSON (si votre JSON est transmis en UTF-8). Dans cet esprit, je pense que le mieux que vous puissiez faire sur le plan spatial est base85 qui représente quatre octets en cinq caractères. Cependant, ce n'est qu'une amélioration de 7% par rapport à base64, son calcul est plus coûteux et les implémentations sont moins courantes que pour base64, ce n'est donc probablement pas une victoire.

Vous pouvez également simplement mapper chaque octet d'entrée au caractère correspondant dans U + 0000-U + 00FF, puis effectuer l'encodage minimum requis par la norme JSON pour transmettre ces caractères; l'avantage ici est que le décodage requis est nul au-delà des fonctions intégrées, mais l'efficacité de l'espace est mauvaise - une expansion de 105% (si tous les octets d'entrée sont également probables) contre 25% pour base85 ou 33% pour base64.

Verdict final: base64 gagne, à mon avis, au motif qu'il est commun, facile et pas assez mauvais pour justifier un remplacement.

Voir aussi: Base91 et Base122

Hobbs
la source
5
Attendez comment l'utilisation de l'octet réel lors du codage des caractères de citation est une extension de 105% et une base64 de 33% seulement? N'est-ce pas base64 133%?
jjxtra
17
Base91 est une mauvaise idée pour JSON, car il contient des guillemets en alphabet. Dans le pire des cas (sortie de toutes les citations) après l'encodage JSON, c'est 245% de la charge utile d'origine.
jarnoh
25
Python 3.4 inclut base64.b85encode()et b85decode()maintenant. Une simple mesure d'encodage + décodage montre que b85 est plus de 13 fois plus lent que b64. Nous avons donc un gain de taille de 7%, mais une perte de performances de 1300%.
Pieter Ennes
3
@hobbs JSON indique que les caractères de contrôle doivent être échappés. La section 5.2 de la RFC20 définit DELcomme un caractère de contrôle.
Tino
2
@Tino ECMA-404 répertorie spécifiquement les caractères à échapper: le guillemet double U + 0022, la barre oblique inversée U + 005C et "les caractères de contrôle U + 0000 à U + 001F".
hobbs
249

J'ai rencontré le même problème et j'ai pensé partager une solution: multipart / form-data.

En envoyant un formulaire en plusieurs parties, vous envoyez d'abord sous forme de chaîne vos métadonnées JSON , puis envoyez séparément sous forme de binaire brut (image (s), wav, etc.) indexé par le nom Content-Disposition .

Voici un joli tutoriel sur la façon de le faire dans obj-c, et voici un article de blog qui explique comment partitionner les données de chaîne avec la limite du formulaire et les séparer des données binaires.

Le seul changement que vous devez vraiment faire est du côté serveur; vous devrez capturer vos métadonnées qui devraient référencer les données binaires POSTées de manière appropriée (en utilisant une limite de disposition de contenu).

Certes, cela nécessite un travail supplémentaire côté serveur, mais si vous envoyez de nombreuses images ou de grandes images, cela en vaut la peine. Combinez cela avec la compression gzip si vous le souhaitez.

À mon humble avis, l'envoi de données encodées en base64 est un hack; le RFC multipart / form-data a été créé pour des problèmes comme celui-ci: l'envoi de données binaires en combinaison avec du texte ou des métadonnées.

Ælex
la source
4
Soit dit en passant, l'API Google Drive le fait de cette manière: developers.google.com/drive/v2/reference/files/update#examples
Mathias Conradt
2
Pourquoi cette réponse est-elle si basse lorsqu'elle utilise des fonctionnalités natives au lieu d'essayer de presser une cheville ronde (binaire) dans un trou carré (ASCII)? ...
Mark K Cowan
5
l'envoi de données encodées en base64 est un hack, tout comme les données en plusieurs parties / formulaires. Même l'article de blog que vous avez lié indique qu'en utilisant les données en plusieurs parties / formulaires de type de contenu que vous déclarez, ce que vous envoyez est en fait un formulaire. Mais ce n'est pas. donc je pense que le hack base64 est non seulement beaucoup plus facile à implémenter mais aussi plus fiable J'ai vu certaines bibliothèques (pour Python par exemple), qui avaient un type de contenu multipart / form-data codé en dur.
t3chb0t
4
@ t3chb0t Le type de média multipart / form-data est né pour transporter des données de formulaire mais il est aujourd'hui largement utilisé en dehors du monde HTTP / HTML, notamment pour encoder du contenu de courrier électronique. Aujourd'hui, il est proposé comme une syntaxe de codage générique. tools.ietf.org/html/rfc7578
lorenzo
3
@MarkKCowan Probablement parce que bien que cela soit utile pour le but de la question, il ne répond pas à la question telle que posée, qui est en fait "Faible surcharge binaire au codage de texte à utiliser dans JSON", cette réponse abandonne complètement JSON.
Chinoto Vokro
34

Le problème avec UTF-8 est qu'il ne s'agit pas du codage le plus économe en espace. En outre, certaines séquences d'octets binaires aléatoires sont un codage UTF-8 non valide. Vous ne pouvez donc pas simplement interpréter une séquence d'octets binaires aléatoires comme des données UTF-8 car ce sera un codage UTF-8 non valide. L'avantage de cette contrainte sur le codage UTF-8 est qu'elle rend robuste et possible de localiser le début et la fin des caractères multi-octets quel que soit l'octet que nous commençons à regarder.

Par conséquent, si le codage d'une valeur d'octet dans la plage [0..127] ne nécessitait qu'un seul octet dans le codage UTF-8, le codage d'une valeur d'octet dans la plage [128..255] nécessiterait 2 octets! Pire que ça. En JSON, les caractères de contrôle "et \ ne sont pas autorisés à apparaître dans une chaîne. Les données binaires nécessiteraient donc une transformation pour être correctement codées.

Laisse voir. Si nous supposons des valeurs d'octets aléatoires uniformément réparties dans nos données binaires, alors, en moyenne, la moitié des octets serait codée en un octet et l'autre moitié en deux octets. Les données binaires codées UTF-8 auraient 150% de la taille initiale.

L'encodage Base64 n'atteint que 133% de la taille initiale. L'encodage Base64 est donc plus efficace.

Qu'en est-il de l'utilisation d'un autre encodage Base? En UTF-8, l'encodage des 128 valeurs ASCII est le plus économe en espace. En 8 bits, vous pouvez stocker 7 bits. Donc, si nous coupons les données binaires en morceaux de 7 bits pour les stocker dans chaque octet d'une chaîne codée UTF-8, les données codées n'augmenteraient que jusqu'à 114% de la taille initiale. Mieux que Base64. Malheureusement, nous ne pouvons pas utiliser cette astuce simple car JSON n'autorise pas certains caractères ASCII. Les 33 caractères de contrôle ASCII ([0..31] et 127) et le "et \ doivent être exclus. Cela ne nous laisse que 128-35 = 93 caractères.

Donc, en théorie, nous pourrions définir un codage Base93 qui augmenterait la taille codée à 8 / log2 (93) = 8 * log10 (2) / log10 (93) = 122%. Mais un encodage Base93 ne serait pas aussi pratique qu'un encodage Base64. Base64 nécessite de couper la séquence d'octets d'entrée en morceaux de 6 bits pour lesquels une opération au niveau du bit simple fonctionne bien. Outre 133%, ce n'est pas beaucoup plus que 122%.

C'est pourquoi je suis arrivé indépendamment à la conclusion commune que Base64 est en effet le meilleur choix pour encoder des données binaires en JSON. Ma réponse en justifie. Je suis d'accord qu'il n'est pas très attrayant du point de vue des performances, mais considérez également l'avantage d'utiliser JSON avec sa représentation de chaîne lisible par l'homme facile à manipuler dans tous les langages de programmation.

Si les performances sont critiques, un codage binaire pur doit être considéré comme un remplacement de JSON. Mais avec JSON, ma conclusion est que Base64 est le meilleur.

chmike
la source
Qu'en est-il de Base128 mais en laissant ensuite le sérialiseur JSON s'échapper du "et \? Je pense qu'il est raisonnable de s'attendre à ce que l'utilisateur utilise une implémentation de l'analyseur json.
jcalfee314
1
@ jcalfee314 malheureusement ce n'est pas possible car les caractères avec un code ASCII inférieur à 32 ne sont pas autorisés dans les chaînes JSON. Des encodages avec une base entre 64 et 128 ont déjà été définis, mais le calcul requis est supérieur à base64. Le gain en taille de texte encodé n'en vaut pas la peine.
chmike
Si le chargement d'une grande quantité d'images en base64 (disons 1000), ou le chargement sur une connexion vraiment lente, base85 ou base93 paierait-il jamais le trafic réseau réduit (avec ou sans gzip)? Je suis curieux de savoir s'il arrive un moment où les données plus compactes justifieraient l'une des méthodes alternatives.
vol7ron
Je soupçonne que la vitesse de calcul est plus importante que le temps de transmission. Les images doivent évidemment être précalculées côté serveur. Quoi qu'il en soit, la conclusion est que JSON est mauvais pour les données binaires.
chmike
Re "Le codage Base64 n'atteint que 133% de la taille initiale. Le codage Base64 est donc plus efficace ", c'est complètement faux car les caractères sont généralement de 2 octets chacun. Voir l'élaboration sur stackoverflow.com/questions/1443158/…
Pacerier
34

BSON (JSON binaire) peut fonctionner pour vous. http://en.wikipedia.org/wiki/BSON

Edit: pour info la bibliothèque .NET json.net prend en charge la lecture et l'écriture de bson si vous recherchez un peu d'amour côté serveur C #.

DarcyThomas
la source
1
"Dans certains cas, BSON utilisera plus d'espace que JSON en raison des préfixes de longueur et des indices de tableau explicites." en.wikipedia.org/wiki/BSON
Pawel Cioch
Bonne nouvelle: BSON prend en charge nativement des types comme Binary, Datetime et quelques autres (particulièrement utile si vous utilisez MongoDB). Mauvaise nouvelle: son encodage est en octets binaires ... c'est donc une non-réponse à l'OP. Cependant, il serait utile sur un canal qui prend en charge nativement les fichiers binaires tels que le message RabbitMQ, le message ZeroMQ ou un socket TCP ou UDP personnalisé.
Dan H
19

Si vous rencontrez des problèmes de bande passante, essayez d'abord de compresser les données côté client, puis en base64-it.

Un bel exemple d'une telle magie est sur http://jszip.stuartk.co.uk/ et plus de discussion sur ce sujet est sur l' implémentation JavaScript de Gzip

andrej
la source
2
voici une implémentation zip JavaScript qui revendique de meilleures performances: zip.js
Janus Troelsen
Notez que vous pouvez (et devez) également compresser après (généralement via Content-Encoding), car la base64 se comprime assez bien.
Mahmoud Al-Qudsi
@ MahmoudAl-Qudsi vous vouliez dire que vous base64 (zip (base64 (zip (données)))))? Je ne suis pas sûr que l'ajout d'un autre zip puis d'un base64 (pour pouvoir l'envoyer en tant que données) soit une bonne idée.
andrej
18

yEnc peut fonctionner pour vous:

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

"yEnc est un schéma de codage binaire en texte pour transférer des fichiers binaires dans [texte]. Il réduit la surcharge par rapport aux méthodes de codage basées sur US-ASCII précédentes en utilisant une méthode de codage ASCII étendu 8 bits. La surcharge de yEnc est souvent (si chaque valeur d'octet apparaît approximativement avec la même fréquence en moyenne) aussi peu que 1 à 2%, contre 33 à 40% de surcharge pour les méthodes de codage 6 bits comme uuencode et Base64. ... En 2003, yEnc est devenu la norme de facto système d'encodage des fichiers binaires sur Usenet. "

Cependant, yEnc est un encodage 8 bits, donc le stocker dans une chaîne JSON a les mêmes problèmes que le stockage des données binaires d'origine - le faire de manière naïve signifie une expansion à 100%, ce qui est pire que base64.

richardtallent
la source
42
Étant donné que beaucoup de gens semblent toujours consulter cette question, je voudrais mentionner que je ne pense pas que yEnc aide vraiment ici. yEnc est un codage 8 bits, donc le stocker dans une chaîne JSON a les mêmes problèmes que le stockage des données binaires d'origine - le faire de manière naïve signifie une expansion à 100%, ce qui est pire que base64.
hobbs
Dans les cas où l'utilisation d'encodages tels que yEnc avec de grands alphabets avec des données JSON est considérée comme acceptable, l' échappement peut fonctionner comme une bonne alternative fournissant une surcharge fixe connue à l'avance.
Ivan Kosarev
10

S'il est vrai que base64 a un taux d'expansion de ~ 33%, il n'est pas nécessairement vrai que le temps de traitement est bien plus que cela: cela dépend vraiment de la bibliothèque / boîte à outils JSON que vous utilisez. L'encodage et le décodage sont des opérations simples et directes, et ils peuvent même être optimisés pour l'encodage des caractères (car JSON ne prend en charge que UTF-8/16/32) - les caractères base64 sont toujours codés sur un octet pour les entrées de chaîne JSON. Par exemple, sur la plate-forme Java, il existe des bibliothèques qui peuvent faire le travail assez efficacement, de sorte que la surcharge est principalement due à une taille étendue.

Je suis d'accord avec deux réponses antérieures:

  • base64 est une norme simple et couramment utilisée, il est donc peu probable de trouver quelque chose de mieux à utiliser spécifiquement avec JSON (la base-85 est utilisée par postscript, etc.; mais les avantages sont au mieux marginaux quand on y pense)
  • la compression avant le codage (et après le décodage) peut avoir beaucoup de sens, selon les données que vous utilisez
StaxMan
la source
10

Format du sourire

Il est très rapide à encoder, décoder et compacter

Comparaison de vitesse (basée sur java mais néanmoins significative): https://github.com/eishay/jvm-serializers/wiki/

C'est aussi une extension de JSON qui vous permet d'ignorer le codage base64 pour les tableaux d'octets

Les chaînes codées Smile peuvent être compressées lorsque l'espace est critique

Stefano Fratini
la source
3
... et le lien est mort. Celui-ci semble à jour: github.com/FasterXML/smile-format-specification
Zero3
4

( Modifier 7 ans plus tard: Google Gears a disparu. Ignorez cette réponse.)


L'équipe Google Gears a rencontré le problème du manque de types de données binaires et a tenté de le résoudre:

API Blob

JavaScript a un type de données intégré pour les chaînes de texte, mais rien pour les données binaires. L'objet Blob tente de résoudre cette limitation.

Peut-être que vous pouvez tisser cela d'une manière ou d'une autre.

un nerd payé
la source
Alors, quel est le statut des blobs en Javascript et json? At-il été abandonné?
chmike
w3.org/TR/FileAPI/#blob-section Pas aussi performant que base64 pour l'espace, si vous faites défiler vers le bas, vous constatez qu'il encode en utilisant la carte utf8 (comme celle de l'option indiquée par la réponse de hobbs). Et pas de support json, pour autant que je sache
Daniele Cruciani
3

Étant donné que vous recherchez la capacité de chausse-pied des données binaires dans un format strictement textuel et très limité, je pense que la surcharge de Base64 est minime par rapport à la commodité que vous vous attendez à maintenir avec JSON. Si la puissance de traitement et le débit sont un problème, vous devrez probablement reconsidérer vos formats de fichier.

jsoverson
la source
2

Juste pour ajouter le point de vue des ressources et de la complexité à la discussion. Depuis avoir fait PUT / POST et PATCH pour stocker de nouvelles ressources et les modifier, il faut se rappeler que le transfert de contenu est une représentation exacte du contenu qui est stocké et qui est reçu en émettant une opération GET.

Un message en plusieurs parties est souvent utilisé comme sauveur mais pour des raisons de simplicité et pour des tâches plus complexes, je préfère l'idée de donner le contenu dans son ensemble. Cela s'explique de lui-même et c'est simple.

Et oui, JSON est quelque chose de paralysant, mais au final JSON lui-même est verbeux. Et la surcharge de mappage vers BASE64 est un moyen trop petit.

En utilisant correctement les messages en plusieurs parties, il faut soit démonter l'objet à envoyer, utiliser un chemin de propriété comme nom de paramètre pour la combinaison automatique, soit créer un autre protocole / format pour simplement exprimer la charge utile.

Aimant également l'approche BSON, ce n'est pas aussi largement et facilement pris en charge que l'on le souhaiterait.

Fondamentalement, nous manquons juste quelque chose ici, mais l'incorporation de données binaires car base64 est bien établie et est à faire à moins que vous n'ayez vraiment identifié la nécessité de faire le vrai transfert binaire (ce qui est rarement le cas).

Martin Kersten
la source
1

Je creuse un peu plus (lors de la mise en œuvre de base128 ), et expose que lorsque nous envoyons des caractères dont les codes ascii sont plus grands que 128, le navigateur (chrome) envoie en fait DEUX caractères (octets) au lieu d'un :( . La raison en est que JSON par défaut, utilisez les caractères utf8 pour lesquels les caractères avec des codes ascii supérieurs à 127 sont codés sur deux octets, ce qui a été mentionné par la réponse chmike . J'ai fait le test de cette manière: saisissez la barre d'url de chrome chrome: // net-export / , sélectionnez "Inclure raw octets ", lancez la capture, envoyez des requêtes POST (en utilisant un extrait en bas), arrêtez la capture et enregistrez le fichier json avec les données brutes des requêtes. Ensuite, nous regardons à l'intérieur de ce fichier json:

  • Nous pouvons trouver notre requête base64 en trouvant une chaîne dont le 4142434445464748494a4b4c4d4ecodage est hexadécimal ABCDEFGHIJKLMNet nous le verrons "byte_count": 639pour cela.
  • Nous pouvons trouver notre requête ci-dessus127 en recherchant une chaîne de caractères, C2BCC2BDC380C381C382C383C384C385C386C387C388C389C38AC38Bce sont des codes de caractères utf8 request-hex ¼½ÀÁÂÃÄÅÆÇÈÉÊË(mais les codes hexadécimaux ascii de ces caractères le sont c1c2c3c4c5c6c7c8c9cacbcccdce). Le "byte_count": 703il est donc plus que 64bytes base64 demande parce que les caractères des codes ascii ci - dessus 127 sont le code de 2 octets demande :(

Donc, en fait, nous n'avons aucun profit à envoyer des caractères avec des codes> 127 :(. Pour les chaînes en base64, nous n'observons pas un tel comportement négatif (probablement pour la base85 aussi - je ne le vérifie pas) - mais peut-être qu'une solution à ce problème sera envoyer des données dans une partie binaire de POST multipart / form-data décrites dans la réponse Ælex (cependant généralement dans ce cas nous n'avons pas besoin d'utiliser de codage de base du tout ...).

L'approche alternative peut s'appuyer sur le mappage d'une partie de données de deux octets en un seul caractère utf8 valide en le codant à l' aide de quelque chose comme base65280 / base65k, mais ce serait probablement moins efficace que base64 en raison de la spécification utf8 ...

Kamil Kiełczewski
la source
0

Le type de données est vraiment préoccupant. J'ai testé différents scénarios sur l'envoi de la charge utile à partir d'une ressource RESTful. Pour l'encodage, j'ai utilisé Base64 (Apache) et pour la compression GZIP (java.utils.zip. *). La charge utile contient des informations sur le film, une image et un fichier audio. J'ai compressé et encodé les fichiers image et audio, ce qui a considérablement dégradé les performances. L'encodage avant la compression s'est bien passé. Le contenu image et audio a été envoyé sous forme d'octets codés et compressés [].

Koushik
la source
0

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

Il décrit un moyen de transférer des données binaires entre un client CDMI et un serveur à l'aide d'opérations de type de contenu CDMI sans nécessiter de conversion base64 des données binaires.

Si vous pouvez utiliser l'opération «Type de contenu non-CDMI», il est idéal de transférer des «données» vers / depuis un objet. Les métadonnées peuvent ensuite être ajoutées / récupérées vers / depuis l'objet en tant qu'opération de «type de contenu CDMI» ultérieure.

Dheeraj Sangamkar
la source
-1

Ma solution maintenant, XHR2 utilise ArrayBuffer. L'ArrayBuffer en tant que séquence binaire contient du contenu en plusieurs parties, vidéo, audio, graphique, texte, etc. avec plusieurs types de contenu. Réponse tout en un.

Dans un navigateur moderne, ayant DataView, StringView et Blob pour différents composants. Voir aussi: http://rolfrost.de/video.html pour plus de détails.

Rolf Rost
la source
Vous ferez augmenter vos données de + 100% en sérialisant un tableau d'octets
Sharcoux
@Sharcoux wot ??
Mihail Malostanidis
La sérialisation d'un tableau d'octets en JSON est quelque chose comme: [16, 2, 38, 89]ce qui est très inefficace.
Sharcoux