Инструменты разработчика · Кодер и декодер Base64
Base64 в API JSON: почему бинарные поля кодируются и чего это вам стоит
· Почему это важно
base64 кодирование
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, чтобы проверить правильность декодирования полей и понять, что на самом деле отправил сервер. Такая дисциплина делает компромисс видимым, а решение – обдуманным, а не случайным.