Русский

Инструменты разработчика · Кодер и декодер Base64

Base64 в API JSON: почему бинарные поля кодируются и чего это вам стоит

· Почему это важно

base64 кодирование

Поле JSON, содержащее длинную строку Base64, представляющую двоичные данные, закодированные для транспортировки.
Оригинальная векторная иллюстрация ToolAcre

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

Поле PDF, которое доминировало в ответе, — конкретная полезная нагрузка API, в которой один объект Base64 перевешивает все остальное.

Ответ API содержит большой объект с одним полем, которое доминирует по размеру полезных данных. Ответ — JSON, поэтому каждое значение представляет собой строку или число. Большинство полей небольшие: идентификаторы пользователей, временные метки, коды состояния. Одно поле содержит imageData или fileContents и представляет собой строку Base64 размером 40 килобайта. Весь ответ занимает 50 килобайт. На это единственное поле приходится 80 процентов передачи, что кажется расточительным, поскольку сервер изначально отправлял его в виде байтов, а клиенту в конечном итоге снова нужны байты.

Base64 решил эту проблему: JSON не имеет собственного байтового типа, поэтому двоичные данные необходимо заключить в строку. Base64 преобразует произвольные байты в символы ASCII, которые безопасны в JSON. И клиент, и сервер должны кодировать при отправке и декодировать при получении, что добавляет CPU накладные расходы. Полученная полезная нагрузка примерно на треть больше, чем необработанные байты. В этом посте объясняется, почему существует это соглашение, чего оно стоит на практике и стоит ли нарушать ограничение JSON с помощью отдельной двоичной конечной точки.

Почему JSON не может содержать необработанные байты — строки должны быть допустимым текстом Unicode, поэтому для произвольных байтов требуется текстовая оболочка

JSON — это текстовый формат, в котором все значения должны быть допустимым текстом в Юникоде. Спецификация определяет строки, числа, логические значения и ноль. У него нет байтового массива или типа буфера. Если API необходимо вернуть двоичные данные, такие как изображение, криптографическая подпись или загрузка файла, он не может напрямую поместить необработанные байты в объект JSON. Байты могут содержать символы, которые анализаторы JSON интерпретируют как структурные маркеры. Нулевой байт в середине двоичного объекта может привести к преждевременному завершению строки или поломке синтаксического анализатора.

Обычное решение — закодировать двоичные данные в формате Base64, создав строку из символов ASCII, которую анализаторы JSON обрабатывают как обычный текст. Затем принимающий клиент декодирует Base64 обратно в байты и использует их. Этот этап кодирования происходит на уровне API, скрытом от большинства разработчиков, но это реальная стоимость, которая накапливается, когда API возвращает много двоичных полей. Затраты на Base64 в JSON возрастают на протяжении всего цикла запрос-ответ. Нарушение размера является первым: выходные данные Base64 примерно на 33 процентов больше, чем входные, из-за накладных расходов на кодирование.

Затраты: на треть больше байтов, время декодирования и количество копий памяти — там, где каждая стоимость отображается в типичном клиенте.

Видеофайл размером 30 становится размером 40 при кодировании Base64. Загрузка 40 вместо 30 мегабайт приводит к расходу трафика и заряда батареи на мобильных устройствах, а также времени пользователей при медленном соединении. Вторая стоимость — CPU времени. Сервер должен закодировать двоичные данные в формате Base64, прежде чем их можно будет преобразовать в строку JSON. Клиент должен проанализировать JSON, а затем декодировать каждое поле Base64 обратно в байты. Для ответа с несколькими двоичными полями или клиента, обрабатывающего тысячи ответов, это время накапливается CPU.

На устройствах с ограниченными возможностями, таких как телефоны, строковые операции JavaScript и TextDecoder, используемый для декодирования Base64, потребляют заряд батареи и замедляют работу приложения. Третья цена — это память: анализатор JSON создает строковый объект для поля Base64, затем декодирование создает еще одну копию в виде Uint8Array. Большое поле дважды создается в памяти, прежде чем приложение сможет его использовать. Проработанный пример поясняет стоимость. Предположим, что конечная точка API возвращает данные профиля пользователя, включая изображение аватара размером 100 килобайт. Сервер считывает изображение с диска в виде байтов, кодирует его в Base64 и включает в ответ JSON.

Рабочий пример: проверка поля Base64 из ответа API — декодирование его в браузере, чтобы подтвердить, что на самом деле отправил сервер.

Ответ JSON теперь занимает около 135 килобайт (накладные расходы 33% плюс другие поля). Клиент загружает 135 килобайт вместо 100. В браузере анализатор JSON создает строковый объект JavaScript для данных Base64.

Когда приложению требуется изображение, оно вызывает декодер Base64, который создает Uint8Array исходных 100 килобайт. В течение нескольких миллисекунд декодирования оба объекта существуют в памяти. Если на странице отображается десять профилей с аватарами, стоимость увеличивается в разы. Альтернативой является то, что API возвращает ответ JSON с отдельным URL для каждого ресурса аватара, позволяя браузеру обрабатывать загрузки изображений с помощью собственного кэширования, прогрессивного рендеринга и управления памятью.

Альтернативы: составные отдельные URL-адреса загрузки и необработанные двоичные конечные точки — компромиссы каждого из них.

Компромисс между включением двоичного файла в JSON и его отдельной выборкой зависит от цели API и шаблонов использования. Для страницы результатов поиска, которая возвращает сотни крошечных эскизов изображений, получение каждого отдельного запроса приводит к отказу от пула соединений и кэширования HTTP. Встраивание их как Base64 в ответ JSON может быть быстрее. Для подробной страницы профиля, которая запрашивает одно или два изображения с высоким разрешением, явно лучше загружать отдельные файлы. В документации API должен быть указан максимальный размер полей Base64 и когда клиентам следует ожидать отдельных конечных точек.

Если поле регулярно превышает один или два килобайта, стратегия встроенного Base64 является признаком того, что дизайн API требует пересмотра. Альтернативы Base64 в JSON существуют, но у каждой есть свои недостатки. Multipart MIME отвечает раздельно в двоичном и текстовом виде, поэтому двоичный раздел отправляется в виде необработанных байтов, и только текстовый раздел имеет значение JSON. Это требует, чтобы клиент анализировал составное сообщение, а не просто вызывал JSON.parse, что усложняет задачу. Отдельная загрузка URL в ответе JSON указывает клиенту на необходимость получения двоичного ресурса отдельно.

Соглашения, которые стоит указать в документах API: стандартный алфавит и алфавит base64url, отступы и максимальные размеры.

Это хорошо работает, когда двоичный ресурс большой или к нему обращаются реже, чем к метаданным. Необработанная двоичная конечная точка, которая возвращает только байты и полностью отказывается от JSON, является самым простым подходом, но удаляет структуру, которую предоставляет JSON. Некоторые API возвращают сжатые двоичные данные и кодируют их с помощью Base64, что уменьшает штраф за размер, но добавляет накладные расходы на распаковку. Выбор зависит от ожидаемого использования: небольшие поля вполне подходят для встраивания, большие поля относятся к отдельным ресурсам, а структурированные данные стоит хранить в JSON даже с учетом стоимости Base64.

Соглашения имеют значение для совместимости. API, которые кодируют двоичные данные с помощью Base64, должны четко документировать это и указывать, является ли алфавит стандартным или URL-безопасным. Стандартный Base64 использует + и /,, которые безопасны в строках JSON, но не в URL-адресах. URL-safe Base64 заменяет их на - и _, что подходит для data: URI, но без необходимости экранируется в JSON. В документации должно быть указано, включено или опущено заполнение, поскольку оба они действительны для Base64, но клиент, который ожидает заполнение и получает незаполненные данные, автоматически выйдет из строя или создаст мусор.

Что здесь не распространяется — protobuf, CBOR и другие форматы двоичной сериализации.

Для очень больших или часто обновляемых полей важно документировать отдельную двоичную конечную точку, чтобы клиенты не пытались получить килобайты ненужных данных. Отладка API с полями Base64 проста при наличии правильного инструмента. Кодер и декодер Base64 позволяет декодировать любое поле локально в браузере, не сохраняя его и не отправляя куда-либо. Скопируйте поле Base64 из ответа JSON, вставьте его в декодер и нажмите «Декодировать». Для текстовых данных (например, JSON внутри Base64) декодированный вывод появляется мгновенно.

Для двоичных данных, таких как изображения, в шестнадцатеричном представлении отображаются байты. Это помогает убедиться, что сервер отправил то, что вы ожидали, и что ваш клиентский декодер работает правильно. Если поле декодируется в непредвиденные данные, проблема заключается в кодировке сервера или в том, как вы копируете поле. Если он декодируется в частичный большой двоичный объект, возможно, поле было усечено или длина Base64 может быть неправильной. Локальное декодирование ускоряет отладку по сравнению с записью поля в файл и открытием внешних инструментов.

Вывод: Base64 в JSON — это компромисс, поэтому задокументируйте его — как кодировщик и декодер Base64 помогают вам проверять и проверять закодированные поля локально.

Практический подход к использованию Base64 в API — это осознание, а не избегание. Base64 — это стандартный способ переноса двоичных данных в JSON, и он работает. Поймите, что каждое поле Base64 стоит на треть больше по размеру и на несколько миллисекунд CPU времени на цикл запроса-ответа. Для небольших критически важных метаданных, таких как токены аутентификации (где сам JWT закодирован в Base64), стоимость незначительна. Для больших вложений задайте вопрос, должен ли двоичный файл передаваться в том же ответе или как отдельный ресурс.

Задокументируйте схему кодирования и максимальные размеры в спецификации API. При проверке ответов используйте кодировщик и декодер Base64, чтобы проверить правильность декодирования полей и понять, что на самом деле отправил сервер. Такая дисциплина делает компромисс видимым, а решение – обдуманным, а не случайным.