Protocol Buffers
Protocol Buffers — протокол сериализации (передачи) структурированных данных, предложенный Google как эффективная бинарная альтернатива текстовому формату XML. Разработчики сообщают, что Protocol Buffers проще, компактнее и быстрее, чем XML, поскольку осуществляется передача бинарных данных, оптимизированных под минимальный размер сообщения[2].
Protocol Buffers | |
---|---|
Тип | формат сериализации данных |
Разработчик | |
Операционная система | Любая ОС |
Первый выпуск | 7 июля 2008 |
Аппаратная платформа | Кроссплатформенность |
Последняя версия | |
Состояние | Действующий |
Лицензия | BSD |
Сайт | developers.google.com/pr… |
Общие сведения
По замыслу разработчиков, сначала должна быть описана структура данных, которая затем компилируется в классы. Вместе с классами идёт код их сериализации в компактном формате представления. Чтение и запись данных доступна в высокоуровневых языках программирования — таких как Java, C++ или Python.
В 2010 году бэкенд Twitter перешёл на Protocol Buffers. По заявлению разработчиков Twitter, база в триллион твитов на XML занимала бы десять петабайт вместо одного.[3]
По заявлениям Google, Protocol Buffers по сравнению с XML:[2]
- проще;
- от 3 до 10 раз меньше;
- от 20 до 100 раз быстрее;
- более однозначный;
- позволяет создавать классы, которые в дальнейшем легче использовать программно.
Protocol Buffers не предназначен для чтения пользователем и представляет собой двоичный формат. Для десериализации данных необходим отдельный .proto-файл, в котором определяется формат сообщения.
Формат протокола
В общем виде формат представляет собой закодированную последовательность полей, состоящих из ключа и значения. В качестве ключа выступает номер, определённый для каждого поля сообщения в proto-файле. Перед каждым полем указываются совместно закодированные номер поля в формате varint и тип поля. Если в качестве типа указана строка (string
), вложенное сообщение, повторяющееся сообщение или набор байт (bytes
), то следом идёт размер данных в формате varint. Далее идёт значение, соответствующее полю (данные)[4].
Число в формате varint (int32
и int64
) кодируется в последовательность байт, в которой у всех байт, кроме последнего, старший бит (MSB) выставляется в 1. При преобразовании в стандартное представление старший бит каждого байта отбрасывается, а оставшиеся 7-битные составляющие соединяются друг с другом в обратном порядке[4]. Формат восьмиразрядного varint был выбран для уменьшения размера пакета при передаче небольших чисел. Так, если число меньше 128, то оно будет занимать лишь 1 байт. Однако, числа близкие к максимально возможным, будут занимать больше места, чем в обычном формате. Например, максимальное значение, которое можно сохранить в 8-ми байтах, в формате varint — 10 байт. Отрицательные числа в формате varint всегда занимают наибольший размер, в зависимости от типа, поскольку старший бит у знакового числа выставлен в 1.
Проблема кодирования отрицательных чисел была решена использованием алгоритма ZigZag (sint32
и sint64
), суть которого сводится к переносу бита знака из старшего разряда в младший. Кодирование алгоритмом ZigZag предполагает, что положительные и отрицательные числа будут чередоваться друг с другом с увеличением закодированного значения. В таком случае чётные числа будут положительными, а нечётные — отрицательными.
Пусть value
— исходное значение, а N
— разрядность типа данных исходного значения, а encoded_value
— закодированное алгоритмом ZigZag значение, тогда кодирование можно записать с помощью выражения на языке Си:
encoded_value = (value << 1) ^ (value >> (N - 1));
Следует учесть, что вторая операция сдвига является арифметическим сдвигом, то есть при сдвиге вправо отрицательного числа старшие биты заполняются единицами, а не нулями (сдвиг знакового бита).
Декодирование выполняется более сложным способом: выполняется исключающее ИЛИ над закодированным значением, сдвинутым на 1 вправо для удаления знакового бита, и знаковым битом, полученным из закодированного числа, спроецированным на все биты через умножение на максимальное значение для N
разрядов. Таким образом, знаковый бит переносится из младшего разряда в старший:
uvalue = ((encoded_value & 1) * MAX_VALUE(N)) ^ (encoded_value >> 1);
Значение MAX_VALUE(N)
соответствует значению с N
разрядами, заполненными единицами (например, 0xffffffff
при N
=32). Таким образом, умножение младшего бита, установленного в 1, на это число будет соответствовать значению -1 в знаковом типе данных. В языке Си декодирование необходимо осуществлять для значений encoded_value
и uvalue
беззнакового типа, а затем значение uvalue
должно преобразовываться в знаковый тип, не меняя битовое представление.
Все числовые значения, кроме fixed64
, sfixed64
и double
, в протоколе кодируются в формате varint.
Примеры использования
Для того чтобы определить структуру сериализуемых данных, необходимо создать .proto-файл с исходным кодом этой структуры. Ниже приведён пример .proto-файла для 2-й версии Protocol Buffers, где описывается информация о машине: марка, тип кузова, цвет, год выпуска, и информация о предыдущих владельцах.
message Car {
required string model = 1;
enum BodyType {
sedan = 0;
hatchback = 1;
SUV = 2;
}
required BodyType type = 2 [default = sedan];
optional string color = 3;
required int32 year = 4;
message Owner {
required string name = 1;
required string lastName = 2;
required int64 driverLicense = 3;
}
repeated Owner previousOwner = 5;
}
После того как файл с нужной структурой данных создан, необходимо скомпилировать его компилятором для вашего языка программирования, чтобы сгенерировать класс доступа к этим данным. Этот класс будет содержать простейшие методы доступа ко всем полям типа get/set, а также методы для сериализации и десериализации вашей структуры данных в/из массива байтов.
Примечательно, что можно добавлять к уже созданной структуре данных новые поля без потери совместимости с предыдущей версией: при парсинге старых записей новые поля просто будут игнорироваться.
Реализация
На данный момент компанией Google созданы компиляторы для языков программирования: C++, Java, Python, Go, C#, Objective C, JavaScript[5]. Но существует ряд проектов сторонних разработчиков, которые создали компиляторы для следующих языков программирования: Action Script, C, C#, Clojure, Common Lisp, D, Erlang, Go, Haskell, Haxe, JavaScript, Lua, Matlab, Mercury, Objective C, Swift, OCaml, Perl, PHP, Python, Ruby, Rust, Scala, Visual Basic, Delphi[6].
Реализация для языка Си
Чтобы использовать протокол в языке Си без сторонних библиотек необходимо либо использовать вставки на языке C++, если таковые поддерживаются используемым компилятором, либо делать обёртки над сгенерированным для C++ кодом в виде библиотек. Если подобные варианты не подходят, то известны следующие генераторы кода:
- protobuf-c (нет поддержки обработки ошибок выделения памяти, но возможно использование собственных механизмов выделения памяти);
- nanopb (оптимизирован под низкое потребление памяти);
- protobuf-embedded-c (архивный проект).
Альтернативные протоколы
Наиболее высокопроизводительной альтернативой может служить библиотека FlatBuffers, которая позволяет обращаться к сериализованным данным без их копирования по частям в отдельные области памяти. Соответственно, данные передаются в том же виде, в каком и используются, в связи с чем увеличивается объём передаваемых данных.
Проект Apache Avro отличается тем, что не требует генерации кода при изменении схемы данных при использовании в динамически-типизируемых языках, а сама схема описывается в формате JSON.
Сравнение с Apache Thrift
Ключевыми особенностями Apache Thrift могут служить возможность передачи ассоциативных массивов, списков и множеств, а также встроенная поддержка удалённого вызова процедур[7].
Protocol Buffers | Apache Thrift | |
---|---|---|
Разработчик | Facebook, Apache | |
Поддерживаемые языки | C++, Dart, Go, Java, Python, Ruby, C#, Objective C, JavaScript и PHP[8] | C++, Java, JavaScript, Python, PHP, XSD, Ruby, C#, Perl, Objective C, Erlang, Smalltalk, OCaml, and Haskell |
Исходящие форматы | Бинарный | Бинарный, JSON |
Простые типы | bool, 32/64-bit integers, float, double, string, byte sequence. Повторные свойства работают как списки. | bool, byte, 16/32/64-bit integers, double, string, byte sequence, map<t1,t2>, list<t>, set<t>. |
Константы | Нет | Да |
Составной тип | Сообщение | Структура |
Исключения | Нет | Да |
Документация | Хорошая | Скудная |
Лицензия | BSD-style | Apache |
Расширения составных типов | Да | Нет |
Примечания
- https://github.com/protocolbuffers/protobuf/releases/tag/v3.19.3 — 2022.
- Developer Guide — Protocol Buffers // Google Developers, April 2, 2012.
- Twitter solves its data formatting challenge — Computerworld
- Encoding | Protocol Buffers | Google Developers (англ.). Google Developers. Дата обращения: 11 октября 2017.
- API Reference — Protocol Buffers — Google Code
- Third-Party Add-ons for Protocol Buffers
- Thrift vs Protocol Bufffers vs JSON, MirthLab LLC, 2009
- API Reference | Protocol Buffers (англ.). Google Developers. Дата обращения: 4 марта 2019.