Двоичные данные в строке JSON. Что-то лучше, чем Base64

615

Формат JSON изначально не поддерживает двоичные данные. Двоичные данные должны быть экранированы, чтобы их можно было поместить в строковый элемент (т. Е. Ноль или более символов Unicode в двойных кавычках с использованием обратной косой черты) в JSON.

Очевидный способ избежать двоичных данных - использовать Base64. Тем не менее, Base64 имеет большие накладные расходы на обработку. Также он расширяет 3 байта в 4 символа, что приводит к увеличению размера данных примерно на 33%.

Одним из вариантов использования этого является черновой вариант спецификации API облачного хранилища CDMI версии v0.8 . Вы создаете объекты данных через REST-Webservice, используя JSON, например

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?

dmeister
источник
30
Для загрузки: вы делаете это только один раз, так что это не так уж сложно. Для загрузки вы можете быть удивлены, насколько хорошо base64 сжимает в gzip , так что если у вас на сервере включен gzip, то вы, вероятно, тоже в порядке.
cloudfeet
2
Еще одно достойное решение msgpack.org для хардкорных ботаников: github.com/msgpack/msgpack/blob/master/spec.md
nicolallias
2
@cloudfeet, Один раз на пользователя за действие . Очень большое дело.
Pacerier
2
Обратите внимание, что символы обычно занимают 2 байта памяти каждый. Таким образом, base64 может дать + 33% (4/3) служебных данных на проводе, но для помещения этих данных на провод, их извлечения и использования потребуется + 166% (8/3) служебных данных . Пример: если строка Javascript имеет максимальную длину 100 тыс. Символов, вы можете представлять только 37,5 тыс. Байт данных, используя base64, а не 75 тыс. Байт данных. Эти числа могут быть узким местом во многих частях приложения, например, JSON.parseи т. Д. ......
Pacerier
5
@Pacerier "обычно 2 байта памяти [на символ]" не является точным. v8, например, имеет строки OneByte и TwoByte. Двухбайтовые строки используются только там, где это необходимо, чтобы избежать потребления гротескной памяти. Base64 кодируется однобайтовыми строками.
ЗакБ

Ответы:

460

Существует 94 символа Unicode, которые могут быть представлены одним байтом в соответствии со спецификацией JSON (если ваш JSON передается как UTF-8). Имея это в виду, я думаю, что лучшее, что вы можете сделать в пространстве - это base85, который представляет четыре байта в виде пяти символов. Тем не менее, это всего лишь 7% улучшение по сравнению с base64, его вычисление обходится дороже, а реализации встречаются реже, чем для base64, поэтому, вероятно, это не победа.

Вы также можете просто сопоставить каждый входной байт с соответствующим символом в U + 0000-U + 00FF, а затем выполнить минимальное кодирование, требуемое стандартом JSON для передачи этих символов; Преимущество здесь в том, что требуемое декодирование равно нулю по сравнению со встроенными функциями, но эффективность использования пространства плохая - расширение на 105% (если все входные байты одинаково вероятны) против 25% для base85 или 33% для base64.

Окончательный вердикт: base64 выигрывает, на мой взгляд, на том основании, что это обычное, простое и не достаточно плохое решение для замены.

Смотрите также: Base91 и Base122

Hobbs
источник
5
Подождите, как просто использование фактического байта при кодировании символов кавычки - расширение 105%, а base64 - только 33%? Разве не base64 133%?
Jjxtra
17
Base91 - плохая идея для JSON, потому что он содержит кавычки в алфавите. В худшем случае (вывод всех кавычек) после кодировки JSON это составляет 245% от исходной полезной нагрузки.
Ярно
25
Python 3.4 включает base64.b85encode()и b85decode()сейчас. Простое измерение времени кодирования + декодирования показывает, что b85 более чем в 13 раз медленнее, чем b64. Таким образом, у нас выигрыш в размере 7%, но потеря производительности на 1300%.
Питер Эннс
3
@hobbs JSON утверждает, что управляющие символы должны быть экранированы. Раздел 5.2 RFC20 определяет DELкак управляющий символ.
Тино
2
В @Tino ECMA-404 конкретно перечислены символы, которые необходимо экранировать: двойная кавычка U + 0022, обратная косая черта U + 005C и «управляющие символы от U + 0000 до U + 001F».
Хоббс
249

Я столкнулся с той же проблемой и решил поделиться решением: multipart / form-data.

Отправляя многокомпонентную форму, вы сначала отправляете в виде строки свои метаданные JSON , а затем отдельно отправляете их в виде необработанного двоичного файла (изображения, wavs и т. Д.), Проиндексированного по имени Content-Disposition .

Вот хороший учебник о том, как сделать это в obj-c, и вот статья в блоге, которая объясняет, как разделить строковые данные с границей формы и отделить их от двоичных данных.

Единственное изменение, которое вам действительно нужно сделать, это на стороне сервера; вам нужно будет захватить ваши метаданные, которые должны соответствующим образом ссылаться на двоичные данные POST (используя границу Content-Disposition).

Конечно, это требует дополнительной работы на стороне сервера, но если вы отправляете много изображений или больших изображений, это того стоит. Объедините это со сжатием gzip, если хотите.

IMHO отправка данных в кодировке base64 - это взлом; RFC multipart / form-data был создан для таких проблем: отправка двоичных данных в сочетании с текстом или метаданными.

Ælex
источник
4
Кстати, API Google Drive делает это следующим образом: developers.google.com/drive/v2/reference/files/update#examples
Матиас Конрадт,
2
Почему этот ответ настолько низок, когда он использует нативные функции вместо того, чтобы пытаться втиснуть круглый (двоичный) колышек в квадратное (ASCII) отверстие? ...
Марк К Коуэн,
5
отправка данных в кодировке base64 - это хак, как и multipart / form-data. Даже статья в блоге, на которую вы ссылаетесь, гласит, что, используя multipart / form-data типа контента, вы заявляете, что отправляемая вами информация является формой. Но это не так. так что я думаю, что взлом base64 не только намного проще в реализации, но и более надежен. Я видел некоторые библиотеки (например, для Python), которые имели жестко закодированный тип содержимого multipart / form-data.
t3chb0t
4
@ t3chb0t Мультимедийный тип multipart / form-data был создан для транспортировки данных формы, но сегодня он широко используется за пределами мира HTTP / HTML, особенно для кодирования содержимого электронной почты. Сегодня это предлагается как общий синтаксис кодирования. tools.ietf.org/html/rfc7578
Лоренсо
3
@MarkKCowan Вероятно, потому что, хотя это и полезно для цели вопроса, он не отвечает на вопрос в том виде, в котором он задан, а именно: «Низкие накладные расходы на двоичное кодирование текста для использования в JSON», этот ответ полностью исключает JSON.
Чиното Вокро
34

Проблема с 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. 33 контрольных символа ASCII ([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 - лучший.

chmike
источник
Как насчет Base128, но затем позволяя сериализатору JSON экранировать "и \? Я думаю, разумно ожидать, что пользователь будет использовать реализацию парсера json.
jcalfee314
1
@ jcalfee314 к сожалению, это невозможно, так как символы с кодом ASCII ниже 32 не допускаются в строках JSON. Кодировки с основанием от 64 до 128 уже определены, но требуемые вычисления выше, чем base64. Выигрыш в размере закодированного текста не стоит.
chmike
Если при загрузке большого количества изображений в base64 (скажем, 1000) или при загрузке по очень медленному соединению, base85 или base93 будут когда-либо платить за уменьшенный сетевой трафик (без gzip)? Мне любопытно, наступит ли момент, когда более компактные данные послужат основанием для одного из альтернативных методов.
vol7ron
Я подозреваю, что скорость вычислений важнее, чем время передачи. Очевидно, что изображения должны быть предварительно рассчитаны на стороне сервера. В любом случае, вывод заключается в том, что JSON вреден для двоичных данных.
chmike
« Кодировка Base64 увеличивается только до 133% от исходного размера, поэтому кодировка Base64 более эффективна », это совершенно неправильно, поскольку символы обычно имеют размер 2 байта каждый. См. Подробности на stackoverflow.com/questions/1443158/…
Pacerier
34

BSON (Binary JSON) может работать на вас. http://en.wikipedia.org/wiki/BSON

Изменить: FYI. NET библиотека json.net поддерживает чтение и запись bson, если вы ищете какую-то любовь на стороне сервера C #.

DarcyThomas
источник
1
«В некоторых случаях BSON будет использовать больше места, чем JSON, из-за префиксов длины и явных индексов массива». en.wikipedia.org/wiki/BSON
Pawel Cioch
Хорошие новости: BSON изначально поддерживает такие типы, как Binary, Datetime и некоторые другие (особенно полезно, если вы используете MongoDB). Плохая новость: это кодирование в двоичных байтах ... так что это не ответ OP. Однако это было бы полезно по каналу, который изначально поддерживает двоичные файлы, такие как сообщение RabbitMQ, сообщение ZeroMQ или пользовательский сокет TCP или UDP.
Дан Х
19

Если вы имеете дело с проблемами пропускной способности, попробуйте сначала сжать данные на стороне клиента, а затем base64-it.

Хороший пример такой магии находится на http://jszip.stuartk.co.uk/, а дальнейшее обсуждение этой темы - в реализации Gzip на JavaScript.

андрей
источник
2
Вот реализация JavaScript zip, которая требует большей производительности: zip.js
Янус Троелсен,
Обратите внимание, что вы можете (и должны) все еще сжимать после этого (обычно через Content-Encoding), так как base64 сжимает довольно хорошо.
Махмуд Аль-Кудси
@ MahmoudAl-Qudsi ты имел ввиду, что у тебя base64 (zip (base64 (zip (data)))))? Я не уверен, что добавление еще одного zip, а затем base64 его (чтобы иметь возможность отправить его в виде данных) является хорошей идеей.
андрей
18

yEnc может работать на вас:

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

«yEnc - это схема кодирования двоичного текста для передачи двоичных файлов в [текст]. Она снижает издержки по сравнению с предыдущими методами кодирования на основе US-ASCII, используя 8-битный расширенный метод кодирования ASCII. Часто возникают издержки yEnc (если каждое значение байта в среднем появляется примерно с той же частотой (всего 1-2%), тогда как издержки 33% -40% для 6-битных методов кодирования, таких как uuencode и Base64 ... ... К 2003 году де-факто стал стандартом yEnc Система кодирования бинарных файлов на Usenet. "

Однако yEnc - это 8-битная кодировка, поэтому хранение ее в строке JSON сопряжено с теми же проблемами, что и сохранение исходных двоичных данных - если это сделать наивным способом, это означает примерно 100% расширение, что хуже, чем base64.

richardtallent
источник
42
Поскольку многие люди все еще рассматривают этот вопрос, я хотел бы отметить, что я не думаю, что YEnC действительно помогает здесь. yEnc представляет собой 8-битную кодировку, поэтому хранение ее в строке JSON имеет те же проблемы, что и сохранение исходных двоичных данных - выполнение этого наивного способа означает примерно 100% расширение, что хуже, чем base64.
Хоббс
В тех случаях, когда использование кодировок типа yEnc с большими алфавитами с данными JSON считается допустимым, escape-код может работать как хорошая альтернатива, обеспечивая фиксированные заранее известные накладные расходы.
Иван Косарев
10

Несмотря на то, что base64 имеет степень расширения ~ 33%, это не обязательно означает, что затраты на обработку значительно больше, чем это: это действительно зависит от используемой вами библиотеки / инструментария JSON. Кодирование и декодирование - это простые прямые операции, и они даже могут быть оптимизированы по кодированию символов (поскольку JSON поддерживает только UTF-8/16/32) - символы base64 всегда являются однобайтовыми для записей строки JSON. Например, на платформе Java есть библиотеки, которые могут выполнять эту работу довольно эффективно, так что накладные расходы в основном связаны с расширенным размером.

Я согласен с двумя более ранними ответами:

  • base64 - это простой, широко используемый стандарт, поэтому вряд ли найдется что-то более конкретное для использования с JSON (base-85 используется postscript и т. д .; но преимущества в лучшем случае незначительны, если подумать)
  • сжатие перед кодированием (и после декодирования) может иметь много смысла, в зависимости от данных, которые вы используете
StaxMan
источник
10

Формат улыбки

Это очень быстро кодировать, декодировать и компактировать

Сравнение скорости (на основе Java, но, тем не менее, имеет смысл): https://github.com/eishay/jvm-serializers/wiki/

Также это расширение JSON, которое позволяет пропустить кодирование base64 для байтовых массивов.

Smile-закодированные строки могут быть сжаты, когда пространство критично

Стефано Фратини
источник
3
... и ссылка мертва. Эта
версия выглядит
4

( Отредактируйте 7 лет спустя: Google Gears пропал. Игнорируйте этот ответ.)


Команда Google Gears столкнулась с проблемой отсутствия типов двоичных данных и попыталась решить ее:

Blob API

В JavaScript есть встроенный тип данных для текстовых строк, но ничего для двоичных данных. Объект Blob пытается устранить это ограничение.

Может быть, вы можете сплести это как-нибудь.

платный ботаник
источник
Так, каково состояние больших двоичных объектов в Javascript и JSON? Это было отброшено?
chmike
w3.org/TR/FileAPI/#blob-section Не так производительно, как base64 для пробела, если прокрутить вниз, вы обнаружите, что он кодирует с использованием карты utf8 (как вариант, показанный в ответе Хоббса). И никакой поддержки JSON, насколько я знаю,
Даниэле Кручиани
3

Поскольку вы ищете возможность вводить двоичные данные в строго текстовый и очень ограниченный формат, я думаю, что издержки Base64 минимальны по сравнению с удобством, которое вы ожидаете поддерживать с JSON. Если вычислительная мощность и пропускная способность являются проблемой, вам, вероятно, придется пересмотреть форматы файлов.

jsoverson
источник
2

Просто чтобы добавить ресурс и сложность с точки зрения обсуждения. Поскольку вы выполняете PUT / POST и PATCH для хранения новых ресурсов и их изменения, следует помнить, что передача контента является точным представлением контента, который хранится и который получен путем выполнения операции GET.

Сообщение из нескольких частей часто используется как спаситель, но по причине простоты и для более сложных задач я предпочитаю идею предоставления содержимого в целом. Это самоочевидно и просто.

И да, JSON - это что-то серьезное, но в конце концов, сам JSON многословен. И накладные расходы на сопоставление с BASE64 - это путь к малым.

Правильно используя составные сообщения, нужно либо разобрать объект для отправки, использовать путь свойства в качестве имени параметра для автоматического объединения, либо создать другой протокол / формат, чтобы просто выразить полезную нагрузку.

Также нравится подход BSON, это не так широко и легко поддерживается, как хотелось бы.

По сути, мы просто что-то здесь упускаем, но встраивание двоичных данных в base64 хорошо известно, и есть путь, если только вы действительно не определили необходимость выполнять реальную двоичную передачу (что вряд ли так).

Мартин Керстен
источник
1

Я копаю немного больше (во время реализации base128 ) и раскрываю, что когда мы отправляем символы с кодами ascii больше 128, тогда браузер (chrome) фактически отправляет ДВА символа (байта) вместо одного :( . Причина в том, что JSON по умолчанию используйте символы utf8, для которых символы с кодами ascii выше 127 кодируются двумя байтами, что было упомянуто в ответе chmike . Я сделал тест следующим образом: введите chrome url bar chrome: // net-export / , выберите «Включить raw bytes ", начать захват, отправлять POST-запросы (используя фрагмент внизу), прекратить захват и сохранить файл json с необработанными данными запросов. Затем мы посмотрим в этот файл json:

  • Мы можем найти наш запрос base64, найдя строку, 4142434445464748494a4b4c4d4eэто шестнадцатеричное кодирование, ABCDEFGHIJKLMNи мы увидим, что"byte_count": 639 для него.
  • Мы можем найти наш запрос выше 127, найдя строку C2BCC2BDC380C381C382C383C384C385C386C387C388C389C38AC38Bэто шестнадцатеричные коды символов request-hex utf8 ¼½ÀÁÂÃÄÅÆÇÈÉÊË(однако шестнадцатеричные коды ascii этих символов таковы c1c2c3c4c5c6c7c8c9cacbcccdce). Таким "byte_count": 703образом, он на 64 байта длиннее, чем запрос base64, потому что символы с кодами ascii выше 127 кодируются на 2 байта в запросе :(

Так что на самом деле у нас нет прибыли с посылкой символов с кодами> 127 :(. Для строк base64 мы не наблюдаем такого негативного поведения (вероятно, и для base85 - я не проверю это) - однако может быть какое-то решение для этой проблемы будет отправка данных в двоичной части POST multipart / form-data, описанной в answerlex answer (однако обычно в этом случае нам вообще не нужно использовать какое-либо базовое кодирование ...).

Альтернативный подход может основываться на отображении двухбайтовой части данных в один действительный символ utf8 с помощью кода, использующего что-то вроде base65280 / base65k, но, вероятно, он будет менее эффективным, чем base64, из-за спецификации utf8 ...

Камил Келчевски
источник
0

Тип данных действительно касается. Я проверил различные сценарии отправки полезных данных из ресурса RESTful. Для кодирования я использовал Base64 (Apache) и для сжатия GZIP (java.utils.zip. *). Полезная нагрузка содержит информацию о фильме, изображении и аудиофайле. Я сжал и закодировал изображения и аудиофайлы, что резко ухудшило производительность. Кодирование до сжатия получилось хорошо. Изображение и аудиоконтент были отправлены в виде закодированных и сжатых байтов [].

Koushik
источник
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,
@ Шарку wot ??
Михаил Малостанидис
Сериализация байтового массива в JSON выглядит примерно так: [16, 2, 38, 89]что очень неэффективно.
Sharcoux