Protocol buffers описание

Обновлено: 06.07.2024

Здравствуйте, а что такое Protocol Buffers?

Почему мне стоит использовать эту библиотеку вместо встроенных средств?

Так вы утверждаете, что protobuf не уступает бинарной сериализации и к тому же переносим?


Да именно это. Давайте рассмотрим небольшой пример, а заодно узнаем как использовать protobuf-net. Предположим, что у нас есть следующие сущности:

namespace Proto.Sample
public enum TaskPriority
<
Low,
Medium,
High
>

[ Serializable ] // <-- Только для BinaryFormatter
[ProtoContract]
public class Task
[ProtoMember(1)]
public int Id

[ProtoMember(2)]
public DateTime CreatedAt

[ProtoMember(3)]
public string CreatedBy

[ProtoMember(4)]
public TaskPriority Priority

* This source code was highlighted with Source Code Highlighter .


Protobuf-net требует использования специальных атрибутов, что напрямую следует из главной особенности формата – зависимости от порядка следования полей. Напишем тест производительности и степени сжатия:

namespace Proto.Sample
internal class Program
private static void Main( string [] args)
var tasks = new List <Task>
new Task
Id = 1,
CreatedBy = "Steve Jobs" ,
CreatedAt = DateTime .Now,
Priority = TaskPriority.High,
Content = "Invent new iPhone"
>,
new Task
Id = 2,
CreatedBy = "Steve Ballmer" ,
CreatedAt = DateTime .Now.AddDays(-7),
Priority = TaskPriority.Low,
Content = "Install own Skype"
>
>;

Console .WriteLine( "The test of binary formatter:" );

const string file1 = "tasks1.bin" ;

TestBinaryFormatter(tasks, file1, 1000);
TestBinaryFormatter(tasks, file1, 2000);
TestBinaryFormatter(tasks, file1, 3000);
TestBinaryFormatter(tasks, file1, 4000);
TestBinaryFormatter(tasks, file1, 5000);

Console .WriteLine( "\nThe test of protobuf-net:" );

const string file2 = "tasks2.bin" ;

TestProtoBuf(tasks, file2, 1000);
TestProtoBuf(tasks, file2, 2000);
TestProtoBuf(tasks, file2, 3000);
TestProtoBuf(tasks, file2, 4000);
TestProtoBuf(tasks, file2, 5000);

Console .WriteLine( "\nThe comparision of file size:" );

Console .WriteLine( "The size of is bytes" , file1, ( new FileInfo(file1)).Length);
Console .WriteLine( "The size of is bytes" , file2, ( new FileInfo(file2)).Length);

private static void TestBinaryFormatter(IList<Task> tasks, string fileName, int iterationCount)
var stopwatch = new Stopwatch();
var formatter = new BinaryFormatter();
using ( var file = File .Create(fileName))
stopwatch.Restart();

for ( var i = 0; i < iterationCount; i++)
file.Position = 0;
formatter.Serialize(file, tasks);
file.Position = 0;
var restoredTasks = ( List <Task>)formatter.Deserialize(file);
>

Console .WriteLine( " iterations in ms" , iterationCount, stopwatch.ElapsedMilliseconds);
>
>

private static void TestProtoBuf(IList<Task> tasks, string fileName, int iterationCount)
var stopwatch = new Stopwatch();
using ( var file = File .Create(fileName))
stopwatch.Restart();

for ( var i = 0; i < iterationCount; i++)
file.Position = 0;
Serializer.Serialize(file, tasks);
file.Position = 0;
var restoredTasks = Serializer.Deserialize< List <Task>>(file);
>

Console .WriteLine( " iterations in ms" , iterationCount, stopwatch.ElapsedMilliseconds);
>
>
>
>

* This source code was highlighted with Source Code Highlighter .

The test of binary formatter:
1000 iterations in 423 ms
2000 iterations in 381 ms
3000 iterations in 532 ms
4000 iterations in 660 ms
5000 iterations in 814 ms

The test of protobuf-net:
1000 iterations in 1056 ms
2000 iterations in 76 ms
3000 iterations in 129 ms
4000 iterations in 152 ms
5000 iterations in 202 ms

The comparision of file size:
The size of tasks1.bin is 710 bytes
The size of tasks2.bin is 101 bytes

* This source code was highlighted with Source Code Highlighter .


Как вы видите, мы превзошли бинарную сериализацию не только по скорости, но и также по степени сжатия. Единственный недостаток, что protobuf-net потребовалось больше времени на «холодный старт». Но вы можете решить эту проблему используя следующий вспомогательный код:

* This source code was highlighted with Source Code Highlighter .

Остальные тесты и результаты можно посмотреть здесь.

Ок. Относительно скорости и сжатия вы меня убедили, но как решается проблема переносимости?

Понимаете, если существуют реализация для нужной вам платформы, вопрос переносимости в большинстве случаев снимается. А реализациии protobuf существует для более чем 20 языков. Полный список можно увидеть здесь. Отмечу только, что для некоторых языков существует более одной реализации. Так что у вас всегда есть выбор.

Привет, хабровчане. В рамках курса "Java Developer. Professional" подготовили для вас перевод полезного материала.


Недавно вышло третье издание книги "Effective Java" («Java: эффективное программирование»), и мне было интересно, что появилось нового в этой классической книге по Java, так как предыдущее издание охватывало только Java 6. Очевидно, что появились совершенно новые разделы, связанные с Java 7, Java 8 и Java 9, такие как глава 7 "Lambdas and Streams" («Лямбда-выражения и потоки»), раздел 9 "Prefer try-with-resources to try-finally" (в русском издании «2.9. Предпочитайте try-с-ресурсами использованию try-finally») и раздел 55 "Return optionals judiciously" (в русском издании «8.7. Возвращайте Optional с осторожностью»). Но я был слегка удивлен, когда обнаружил новый раздел, не связанный с нововведениями в Java, а обусловленный изменениями в мире разработки программного обеспечения. Именно этот раздел 85 "Prefer alternatives to Java Serialization" (в русском издании «12.1 Предпочитайте альтернативы сериализации Java») и побудил меня написать данную статью об использовании Google Protocol Buffers в Java.

В разделе 85 "Prefer alternatives to Java Serialization" (12.1 «Предпочитайте альтернативы сериализации Java») Джошуа Блох (Josh Bloch) выделяет жирным шрифтом следующие два утверждения, связанные с сериализацией в Java:

«Лучший способ избежать проблем, связанных с сериализацией, — никогда ничего не десериализовать».

«Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».

После описания в общих чертах проблем с десериализацией в Java и, сделав эти смелые заявления, Блох рекомендует использовать то, что он называет «кроссплатформенным представлением структурированных данных» (чтобы избежать путаницы, связанной с термином «сериализация» при обсуждении Java). Блох говорит, что основными решениями здесь являются JSON (JavaScript Object Notation) и Protocol Buffers (protobuf). Мне показалось интересным упоминание о Protocol Buffers, так как в последнее время я немного читал о них и игрался с ними. В интернете есть довольно много материалов по использованию JSON (даже в Java), в то время как осведомленность о Protocol Buffers среди java-разработчиков гораздо меньше. Поэтому я думаю, что статья об использовании Protocol Buffers в Java будет полезной.

На странице проекта Google Protocol Buffers описывается как «не зависящий от языка и платформы расширяемый механизм для сериализации структурированных данных». Также там есть пояснение: «Как XML, но меньше, быстрее и проще». И хотя одним из преимуществ Protocol Buffers является поддержка различных языков программирования, в этой статье речь пойдет исключительно про использование Protocol Buffers в Java.

Использование Protocol Buffers в Java описано в туториале "Protocol Buffer Basics: Java". В нем рассматривается гораздо больше возможностей и вещей, которые необходимо учитывать в Java, по сравнению с тем, что я расскажу здесь. Первым шагом является определение формата Protocol Buffers, не зависящего от языка программирования. Он описывается в текстовом файле с расширением .proto. Для примера опишем формат протокола в файле album.proto, который показан в следующем листинге кода.

album.proto

Несмотря на простоту приведенного выше определения формата протокола, в нем присутствует довольно много информации. В первой строке явно указано, что используется proto3 вместо proto2, используемого по умолчанию, если явно ничего не указано. Две строки, начинающиеся с option, указывают параметры генерации Java-кода (имя генерируемого класса и пакет этого класса) и они нужны только при использовании Java.

Файл album.proto , приведенный выше, теперь необходимо «скомпилировать» в файл исходного класса Java ( AlbumProtos.java в пакете dustin.examples.protobuf ), который можно использовать для записи и чтения бинарного формата Protocol Buffers. Генерация файла исходного кода Java выполняется с помощью компилятора protoc, соответствующего вашей операционной системе. Я запускаю этот пример в Windows 10, поэтому я скачал и распаковал файл protoc-3.5.1-win32.zip. На изображении ниже показан мой запущенный protoc для album.proto с помощью команды protoc --proto_path=src --java_out=dist\generated album.proto


Перед запуском вышеуказанной команды я поместил файл album.proto в каталог src, на который указывает --proto_path , и создал пустой каталог build\generated для размещения сгенерированного исходного кода Java, что указано в параметре --java_out .

Сгенерированный Java-класс AlbumProtos.java содержит более 1000 строк, и я не буду приводить его здесь, он доступен на GitHub. Среди нескольких интересных моментов относительно сгенерированного кода я хотел бы отметить отсутствие выражений import (вместо них используются полные имена классов с пакетами). Более подробная информация об исходном коде Java, сгенерированном protoc, доступна в руководстве Java Generated Code. Важно отметить, что данный сгенерированный класс AlbumProtos пока никак не связан с моим Java-приложением, и сгенерирован исключительно из текстового файла album.proto, приведенного ранее.

Теперь исходный Java-код AlbumProtos надо добавить в вашем IDE в перечень исходного кода проекта. Или его можно использовать как библиотеку, скомпилировав в .class или .jar.

Прежде чем двигаться дальше, нам понадобится простой Java-класс для демонстрации Protocol Buffers. Для этого я буду использовать класс Album, который приведен ниже (код на GitHub).

Album.java

Теперь у нас есть data-класс Album , Protocol Buffers-класс, представляющий этот Album ( AlbumProtos.java ) и мы готовы написать Java-приложение для "сериализации" информации об Album без использования Java-сериализации. Код приложения находится в классе AlbumDemo , полный код которого доступен на GitHub.

Создадим экземпляр Album с помощью следующего кода:

Класс AlbumProtos , сгенерированный Protocol Buffers, включает в себя вложенный класс AlbumProtos.Album , который используется для бинарной сериализации Album. Следующий листинг демонстрирует, как это делается.

Как видно из предыдущего примера, для заполнения иммутабельного экземпляра класса, сгенерированного Protocol Buffers, используется паттерн Строитель (Builder). Через ссылку экземпляр этого класса теперь можно легко преобразовать объект в бинарный вид Protocol Buffers, используя метод toByteArray() , как показано в следующем листинге:

Чтение массива byte[] обратно в экземпляр Album может быть выполнено следующим образом:

Как вы заметили, при вызове статического метода parseFrom(byte []) может быть брошено проверяемое исключение InvalidProtocolBufferException . Для получения «десериализованного» экземпляра сгенерированного класса, по сути, нужна только одна строка, а остальной код — это создание исходного класса Album из полученных данных.

Демонстрационный класс включает в себя две строки, которые выводят содержимое исходного экземпляра Album и экземпляра, полученного из бинарного представления. В них есть вызов метода System.identityHashCode() на обоих экземплярах, чтобы показать, что это разные объекты даже при совпадении их содержимого. Если этот код выполнить с примером Album , приведенным выше, то результат будет следующим:

Здесь мы видим, что в обоих экземплярах соответствующие поля одинаковы и эти два экземпляра действительно разные. При использовании Protocol Buffers, действительно, нужно сделать немного больше работы, чем при «почти автоматическом» механизме сериализации Java, когда надо просто наследоваться от интерфейса Serializable, но есть важные преимущества, которые оправдывают затраты. В третьем издании книги Effective Java («Java: эффективное программирование») Джошуа Блох обсуждает уязвимости безопасности, связанные со стандартной десериализацией в Java, и утверждает, что «Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».

Один из набирающих популярность протоколов общения между двумя гетерогенными системами - Protocol Buffers. Сегодня мы разберемся с тем, что это такое, для чего нужно и как применять.

Protocol Buffers - это бинартый протокол сериализации (передачи) структурированных данных. Google предложили его как эффективную альтернативу xml и правильно сделали. В моём окружении все не легаси проекты уже давно используют json и счастливы, а здесь следующий шаг, если быть точнее, другой взгляд на передаваемые данные. Данные хранятся в виде набора байт, но как работать с бинарнарным протоколом сериализации, где взять сериализаторы и десериализаторы, как они поймут, что именно нужно сделать?

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

Будьте внимательны с использованием обязательных полей. Нужно понимать, что если у клиента версия .proto файла, где поле Х обязательно, а сервер решит удалить его из следующей версии API, то такое изменение будет обратно-несовместимым. Таким образом, обязательные поля могут принести больше вреда чем пользы. Рекомендуется, следуя паттерну TolerantReader, быть готовым к изменениям модели для максимально долгого сохранения обратной совместимости.

Хорошая новость. Как минимум для Intellij IDEA есть плагин для .proto файлов. В тот момент, когда вы создадите и откроете такой файл, вы увидите хинт сверху, который предложит вам установить плагин. Здесь вы увидите пример .proto файла для второй версии протобафа, хотя сейчас уже появилась третья. Возможно, о ней я буду писать позже, а любопытный читатель уже сейчас может посмотреть Language Guide (proto3).

Шаг 1. Определяем формат протокола

На первом шаге нам нужно описать .proto файл. Разберем сокращенную версию предметной области учебного заведения. Исходный .proto файл выглядит следующим образом:

Разберемся с синтаксисом файла. Прежде всего мы указываем какую версию protobuf мы используем, в нашем случае это вторая версия. Затем указываем package, который необходим здесь для разделения пространств имён. Т.к. мы знаем, что будем пользоваться java, то указываем две дополнительные настройки: java_package и java_outer_classname . Первая, очевидно, говорит в какой пакет и соответственно иерархию директорий необходимо сложить результат компиляции, а java_outer_classname определяет имя файла, который будет в себя заворачивать весь сгенерированный контент. Если это не будет сделано, то компилятор определит имя в соответствии с CamelCase по названию .proto файла. Эти настройки, как вы понимаете, java-специфичны.

Вы можете вкладывать messages друг в друга, использовать перечисления enum, в общем очень похоже на Java. Кроме того, есть возможность определить значения по умолчанию.

Шаг 4. Взаимодействуем со сгенерированным кодом

Компилятор создает весь код внутри файла AcademyProtos.java , это название мы указали в .proto файле. Весь сгенерированный код доступен в одноименном классе. Messages превратились в несколько внутренних классов, которые помогают создавать, сериализовывать и десериализовывать описанную модель. По message Student компилятор создал класс AcademyProtos.Student и AcademyProtos.Student.Builder. Это типичная реализация паттерна “Строитель”. Объекты класса Student всегда неизменяемы, т.е. после создания мы не можем изменить каких-либо значений. Все манипуляции происходят с классом Builder, для этого у него есть достаточно методов.
Разберем код. Нам небходимо создать группу, для которой определено обязательное имя и набор студентов в виде repeated поля. Создание группы выглядит следующим образом:

Для того, чтобы создать новый объект мы должны вызвать его Builder, заполнить его поля, а затем, в качестве звершающей изменения операции вызвать метод build() , который создаст группу. Repeated поля мы можем заполнять как по одному, так и добавлять целую коллецию.

Как вы уже поняли, создавать студентов мы можем аналогично:

Итак, данные мы создали, получили заполненный объект типа Group, теперь необходимо перегнать его в массив байт. Сделать это можно следующим образом:

Вот так просто! Сериализованная группа теперь - набор байт в protocol buffers формате.

Затем нам необходимо прочитать сохраненные данные. Воспользуемся статическим методом parseFrom .

Для того, чтобы проверить результат выведем его на экран (компилятор создает человекопонятные методы toString для классов, так что с отладкой нет проблем).

В результате, в консоли мы видим:

За ширмой, для полноты примера, я добавил еще одного студента к группе.

Protocol Buffers - отличный инструмент для кросс-платформенной сериализации данных. В некоторых случаях, он позволяет сохранять обратную совместимость, однако, при безрассудном подходе может и нанести вред. Сегодня мы познакомились с основами формата, разобрали .proto файл и пример Java кода, который работает с описанными структурами. Protocol Buffers - это кирпичик, который стоит в основе других технологий для интеграции гетерогенных систем, также существуют и аналоги, которые мы рассмотрим позже. Как всегда - это не серебряная пуля, но хороший инструмент интеграции.

Здравствуйте, а что такое Protocol Buffers?

Почему мне стоит использовать эту библиотеку вместо встроенных средств?

Так вы утверждаете, что protobuf не уступает бинарной сериализации и к тому же переносим?


Да именно это. Давайте рассмотрим небольшой пример, а заодно узнаем как использовать protobuf-net. Предположим, что у нас есть следующие сущности:

namespace Proto.Sample
public enum TaskPriority
<
Low,
Medium,
High
>

[ Serializable ] // <-- Только для BinaryFormatter
[ProtoContract]
public class Task
[ProtoMember(1)]
public int Id

[ProtoMember(2)]
public DateTime CreatedAt

[ProtoMember(3)]
public string CreatedBy

[ProtoMember(4)]
public TaskPriority Priority

* This source code was highlighted with Source Code Highlighter .


Protobuf-net требует использования специальных атрибутов, что напрямую следует из главной особенности формата – зависимости от порядка следования полей. Напишем тест производительности и степени сжатия:

namespace Proto.Sample
internal class Program
private static void Main( string [] args)
var tasks = new List <Task>
new Task
Id = 1,
CreatedBy = "Steve Jobs" ,
CreatedAt = DateTime .Now,
Priority = TaskPriority.High,
Content = "Invent new iPhone"
>,
new Task
Id = 2,
CreatedBy = "Steve Ballmer" ,
CreatedAt = DateTime .Now.AddDays(-7),
Priority = TaskPriority.Low,
Content = "Install own Skype"
>
>;

Console .WriteLine( "The test of binary formatter:" );

const string file1 = "tasks1.bin" ;

TestBinaryFormatter(tasks, file1, 1000);
TestBinaryFormatter(tasks, file1, 2000);
TestBinaryFormatter(tasks, file1, 3000);
TestBinaryFormatter(tasks, file1, 4000);
TestBinaryFormatter(tasks, file1, 5000);

Console .WriteLine( "\nThe test of protobuf-net:" );

const string file2 = "tasks2.bin" ;

TestProtoBuf(tasks, file2, 1000);
TestProtoBuf(tasks, file2, 2000);
TestProtoBuf(tasks, file2, 3000);
TestProtoBuf(tasks, file2, 4000);
TestProtoBuf(tasks, file2, 5000);

Console .WriteLine( "\nThe comparision of file size:" );

Console .WriteLine( "The size of is bytes" , file1, ( new FileInfo(file1)).Length);
Console .WriteLine( "The size of is bytes" , file2, ( new FileInfo(file2)).Length);

private static void TestBinaryFormatter(IList<Task> tasks, string fileName, int iterationCount)
var stopwatch = new Stopwatch();
var formatter = new BinaryFormatter();
using ( var file = File .Create(fileName))
stopwatch.Restart();

for ( var i = 0; i < iterationCount; i++)
file.Position = 0;
formatter.Serialize(file, tasks);
file.Position = 0;
var restoredTasks = ( List <Task>)formatter.Deserialize(file);
>

Console .WriteLine( " iterations in ms" , iterationCount, stopwatch.ElapsedMilliseconds);
>
>

private static void TestProtoBuf(IList<Task> tasks, string fileName, int iterationCount)
var stopwatch = new Stopwatch();
using ( var file = File .Create(fileName))
stopwatch.Restart();

for ( var i = 0; i < iterationCount; i++)
file.Position = 0;
Serializer.Serialize(file, tasks);
file.Position = 0;
var restoredTasks = Serializer.Deserialize< List <Task>>(file);
>

Console .WriteLine( " iterations in ms" , iterationCount, stopwatch.ElapsedMilliseconds);
>
>
>
>

* This source code was highlighted with Source Code Highlighter .

The test of binary formatter:
1000 iterations in 423 ms
2000 iterations in 381 ms
3000 iterations in 532 ms
4000 iterations in 660 ms
5000 iterations in 814 ms

The test of protobuf-net:
1000 iterations in 1056 ms
2000 iterations in 76 ms
3000 iterations in 129 ms
4000 iterations in 152 ms
5000 iterations in 202 ms

The comparision of file size:
The size of tasks1.bin is 710 bytes
The size of tasks2.bin is 101 bytes

* This source code was highlighted with Source Code Highlighter .


Как вы видите, мы превзошли бинарную сериализацию не только по скорости, но и также по степени сжатия. Единственный недостаток, что protobuf-net потребовалось больше времени на «холодный старт». Но вы можете решить эту проблему используя следующий вспомогательный код:

* This source code was highlighted with Source Code Highlighter .

Остальные тесты и результаты можно посмотреть здесь.

Ок. Относительно скорости и сжатия вы меня убедили, но как решается проблема переносимости?

Понимаете, если существуют реализация для нужной вам платформы, вопрос переносимости в большинстве случаев снимается. А реализациии protobuf существует для более чем 20 языков. Полный список можно увидеть здесь. Отмечу только, что для некоторых языков существует более одной реализации. Так что у вас всегда есть выбор.

Шаг 3. Собираем проект

Прежде всего хочу сказать, что не смотря на то, что все примеры на java, работа на других платформах с protobuf аналогична.

Поигрались с терминалом и хватит, перейдем к практическому применению. Создадим gradle проект, цель которого будет перегнать через массив байт группу со студентами. Для автоматизации рутинной деятельности нам поможет инструмент автоматизации сборки gradle. Для вашего случая инструмент может отличаться, но идея должна быть понятна. Для того, чтобы добавить поддержку protocol buffers в цикле сборки нашего проекта, дополним типичный build.gradle файл следующими настройками:

Комментарии к коду исчерпывающие, а в конце статьи я оставлю ссылку на репозиторий, в котором вы найдете запускаемый код.

В папку проекта src/main/proto помещаем наш .proto файл из первого шага. Теперь при сборке проекта или при выполнении gradle команды generateProto мы получим сгенерированный код по .proto файлу внутри нашего проекта.

*Шаг 2. Компилируем файл

* опциональный, для понимания

Созданный .proto файл нужно скомпилировать и прежде всего нам нужен компилятор. Скачиваем protoc архив. В архиве к нам прилетает компилятор и некоторый набор типов, которые мы можем использовать из коробки. Когда вы нашли место для файла в вашей файловой системе добавьте его в PATH. В Windows это делается в Параметрах окружения, а в linux будет достаточно выполнить export PATH=$PATH:your_path . Теперь нам доступен компилятор из терминала, давайте скомпилируем.

Перейдем в папку с .proto файлом и выполним команду:

Флаг --java_out указывает на папку куда будет сгенерирован java код. В этой папке мы получили иерархию, которая определяет java package, который мы указали в .proto файле. Результат компиляции - .java файл, который пока не компилируется javac’ом, для этого нам необходима дополнительная библиотека для работы с protobuf из java. В целях избежения ненужных проблем, перенесем наши эксперименты в плоскость обычного проекта.

Читайте также: