базовое руководство по protobuf
Недавно меня вдруг заинтересовала сериализация RPC, но я обнаружил, что по Protobuf не так много информации, поэтому я нашел вводное руководство по использованию Protocol Buffer в Java на официальном сайте, и перевел его на плохой английский для общения.исходный адрес
Пример Начало: Определение формата протокола Формат протокола
Пример: простая адресная книга,.protoдокумент см.addressbook.proto.
syntax = "proto2";
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
.protoфайл начинает объявлять пакеты, чтобы избежать конфликтов имен. В JAVA имя пакета может использоваться как пакет в java, если вы специально не укажетеjava_package,существуетaddressbook.protoМы указали пакет.
даже если указатьjava_package, также необходимо определить регулярныйpackage, чтобы создать конфликт в пространстве имен Protocol Buffers.
После объявления определения пакета я также вижу две опции в спецификации java:java_package,java_outer_classname.java_packageУказывает, в какой пакет нужно поместить сгенерированный класс Java. Если это значение не указано, оно будет использоватьсяpackageуказанное значение.java_outer_classnameПараметр определяет имя класса, включая все классы в файле .proto.Если это значение не указано явно, имя файла будет использоваться в качестве имени класса через верблюжье имя. Например, my_proto.proto по умолчанию сгенерирует имя класса MyProto.
Далее идет определение сообщения (message).
Сообщение представляет собой совокупность, содержащую ряд полей типа.
Для полей доступны многие стандартные простые типы данных, в том числе:
- bool
- int32
- float
- double
- string
Вы также можете добавить другие типы структур для ваших типов полей сообщений. В предыдущем примереPersonсообщение содержитPhoneNumberсообщение иAddressBookсообщение содержитPersonИнформация. Вы также можете определить типы перечисленийenum, если вы хотите, чтобы ваше поле имело предопределенный список возможных значений, вы можете использоватьenumтип. В этом примере телефонной книги есть три типа телефонных номеров:MOBILE,HOME,WORK.
=1,=2Представляет уникальный «маркер», используемый в двоичном кодировании по полю идентификации для каждого элемента. Номера тегов 1-15 Числа требуют меньше байтов, чем числа выше, поэтому вы можете использовать эти номера для часто используемых или часто повторно используемых тегов, остальные теги 16 и выше меньше используются в дополнительных элементах. Каждый элемент в повторяющемся поле должен перекодировать номер тега, поэтому повторяющиеся поля — лучший выбор для этой оптимизации.
Каждое поле должно быть помечено следующими модификаторами:
-
required: Это поле необходимо указать, иначе сообщение считается "неинициализированным". Попытка создать неинициализированное сообщение приводит кRuntimeException. Разбор неинициализированного сообщения вызоветIOException. В остальном обязательные поля ведут себя точно так же, как необязательные поля.
-
optional: Указывает на необязательное поле, которое может быть установлено или нет. Если необязательное поле не имеет установленного значения, оно будет инициализировано со значением по умолчанию. Для простых типов вы можете явно указать собственное значение по умолчанию, как мы это сделали в примере (поле type of phoneNumber). В противном случае система принимает значения по умолчанию: 0 для числовых типов, пустая строка для символьных типов и false для типов boo. Для встроенных сообщений значением по умолчанию всегда является «экземпляр по умолчанию» или «прототип» сообщения без установленных полей.
-
repeated: Это поле можно использовать повторно любое количество раз. Порядок повторяющихся значений будет сохранен в буфере протокола. Думайте о повторяющихся полях как о динамических массивах.
Required Is ForeverВы должны быть очень осторожны с маркировкой полей как требуемых украшений. Если в какой-то момент вы захотите прекратить писать или отправлять обязательное поле, могут возникнуть проблемы при изменении поля на необязательное — старые читатели отклонят или отбросят сообщение, думая, что оно не имеет этого значения. Вам следует подумать о написании процедур проверки буферов для конкретных приложений. Некоторые инженеры Google предполагают, что используютrequiredПринести больше вреда, чем пользы. они более склонны к использованиюopyionalиrepeated. В любом случае, это мнение не является универсальным.
Вы также можетеРуководство по языку буфера протоколаИзучите полное руководство в формате . Не пытайтесь найти такие инструменты, как наследование, буфер протокола этого не поддерживает.
Скомпилируйте свои буферы протоколов
теперь у вас есть.protoфайл, следующее, что нужно сделать, это сгенерировать класс AddressBook, который вы будете читать и писать. Итак, вам нужно запустить компилятор буфера potocolprotocиметь дело с.proto:
- Если у вас не установлен компилятор,Загрузите установочный пакет, установленный согласно README.
Составление и установка протокола
Компилятор протокола написан на C++. Если вы используете C++, следуйтеРуководство по установке С++Установить протокол. Для пользователей, не использующих C++, самый простой способ установить компилятор протокола — загрузить готовые двоичные файлы со страницы выпуска:Раздел GitHub.com/protocol…
загруженprotoc-$VERSION-$PLATFORM.zip
. Содержит двоичный протокол protoc, а также ряд стандартных файлов .proto, распространяемых вместе с protobuf.
Если вы все еще ищете более старые версии, вы можете найти их по адресу https://repo1.maven.org/maven2/com/google/protobuf/protoc/.
Эти готовые двоичные файлы будут доступны только в релизных сборках.
Установка среды выполнения Protobuf
Protobuf поддерживает несколько различных языков программирования. Для каждого языка вы можете обратиться к различным языковым инструкциям в исходном коде.
- Теперь запускаем компилятор, нужно указатьисходный каталог(где находится исходный код приложения — по умолчанию используется текущий каталог, если вы не укажете значение),Целевой каталог(Каталог назначения, в котором вы хотите сгенерировать код, обычно что-то вроде$SRC_DIR),и.protoмаршрут из. В этом случае вы можете сделать следующее:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
Поскольку вы хотите генерировать классы Java, вы видите--java_outпараметры, аналогичные параметры также доступны для других языков программирования.
Это будет сгенерировано в указанном вами целевом каталогеcom/example/tutorial/AddressBookProtos.java.
Protocol Buffer API
Давайте взглянем на часть сгенерированного кода и посмотрим на некоторые классы и методы, созданные для вас компилятором. если вы посмотрите наAddressBookProtos.javaclass, вы можете видеть, что он определяет класс с именемAddressBookProtos, который встраивает вашaddressbooprotoКласс, указанный в файле для каждого сообщения. У каждого класса свояBuilderclass, который можно использовать для создания соответствующего экземпляра класса. Вы можете увидеть более подробную информацию в разделе Строители и сообщения ниже.
сообщения и компоновщики имеют автоматически сгенерированные методы доступа для каждого поля сообщения. сообщение имеет только геттеры, у сборщиков есть геттеры и сеттеры. Вот некоторые оPersonДоступ к классу (реализация для краткости опущена):
// required string name = 1;
public boolean hasName();
public String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
в то же время,Person.BuilderВнутренние классы имеют геттеры и сеттеры:
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();
Как видите, каждое поле имеет простые методы получения и установки в стиле Java Bean. Каждое поле также имеет свойhasметод, который возвращает true, если значение поля установлено. Наконец, каждое поле также имеетclearметод, чтобы вернуть поле в его пустое состояние.
repeatedПоля состоят из некоторых дополнительных методов:
- getXXXCountМетод используется для получения величины слезной волны;
- Добавлены методы get и set для получения элементов на основе их индексных индексов (public PhoneNumber getPhones(int index); и public Builder setPhones(int index, значение PhoneNumber);).
- *добавить, добавить всеМетод добавляет новый элемент (список) в список.
Обратите внимание, что все эти методы доступа используют верблюжий регистр, даже если.protoВ файле используются строчные буквы и символы подчеркивания. Это преобразование выполняется автоматически компилятором протокола, поэтому сгенерированные классы соответствуют стандартной спецификации стиля Java. существует.protoВы всегда должны использовать нижний регистр и подчеркивание для имен полей. может относиться кгид по стилюполучить больше хорошего.protoСтиль именования. Более подробные сведения о полях, созданных компилятором, можно найти вСправочное руководство по сгенерированному коду Java
перечисления и внутренние классы
Сгенерированный код содержит перечислениеPhoneType, вложенный вPersonКласс:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
внутренний классPerson.PhoneNumberтакже генерируется, как и следовало ожидать, какPersonвнутреннего класса.
Builders vs. Messages
Классы, сгенерированные компилятором буфера протокола, неизменяемы. Как только объект сообщения создан, его нельзя изменить, подобно классу String в Java. Чтобы построить сообщение, вы должны построить построитель, установить значения любых полей, которые вы хотите, а затем вызвать конструкторы.builderметод. (Друзья, которые использовали ломбок, это очень похоже на использование его аннотации @Builder)
Вы также можете заметить, что метод каждого построителя возвращает другой построитель. Возвращаемый объект на самом деле является тем же конструктором, из которого вы вызвали метод. Этот способ обработки удобен, вы можете объединить несколько сеттеров в одну строку кода.
Вот пример, создайтеPersonПример:
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME)
.build())
.build();
Стандартный метод сообщения
Каждое сообщение и класс построителя также содержит ряд других методов, которые позволяют вам проверять и манипулировать сообщениями:
- isInitialized(): проверьте, были ли установлены все обязательные поля/
- toString(): возвращает удобочитаемое представление сообщения, часто особенно полезное для отладки.
- mergeFrom(Message other): (только в конструкторе) добавить другое в сообщение
- clear(): (только в конструкторе) очищает все поля, когда возвращается в исходное пустое состояние
Разобрать и сериализовать
Наконец, каждый класс протобуфера имеет несколько методов для чтения и записи двоичных файлов.
- byte[] toByteArray(): сериализовать сообщение и вернуть массив байтов.
- static Person parseFrom(byte[] data): Разбирает данный массив байтов в сообщение.
- void writeTo(OutputStream output): сериализовать сообщение и записать его в выходной поток.
- static Person parseFrom(InputStream input): Читает входной поток и анализирует из него сообщение.
Это всего лишь два набора методов работы, предназначенных для сериализации и парсинга (десериализации). Более полный список можно посмотретьСправочная документация API сообщений.
Буферы протокола и объектная ориентацияКлассы протокольных буферов в основном являются тупыми держателями данных (подобно структурам в C); в объектной модели они не являются гражданами первого класса. Если вы хотите добавить более богатое поведение в сгенерированный класс, лучший способ — обернуть сгенерированный класс буфера протокола в класс, специфичный для приложения. Также рекомендуется обернуть буферы протокола, если вы не можете контролировать дизайн файла .proto (например, если вы повторно используете файл из другого проекта). В этом случае вы можете использовать класс-оболочку для создания интерфейса, лучше подходящего для уникальной среды вашего приложения: скрыть некоторые данные и методы, предоставить удобные функции и т. д. Это сломает внутренности и ни в коем случае не является хорошей практикой OO.
напишите сообщение
Теперь попробуйте использовать класс буфера протокола. Во-первых, я надеюсь, что адресная книга может записывать детали личной информации в файл адресной книги. Для этого вам необходимо создать и заполнить экземпляры класса буфера протокола и записать их в выходной поток.
Вот программа, которая читает файл изAddressBook, основанный на вводе пользователя, также добавляет новыйPersonв адресную книгу и вставьте новыйAddressBookпереписать в файл.
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
class AddPerson {
// This function fills in a Person message based on user input.
static Person PromptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
Person.Builder person = Person.newBuilder();
stdout.print("Enter person ID: ");
person.setId(Integer.valueOf(stdin.readLine()));
stdout.print("Enter name: ");
person.setName(stdin.readLine());
stdout.print("Enter email address (blank for none): ");
String email = stdin.readLine();
if (email.length() > 0) {
person.setEmail(email);
}
while (true) {
stdout.print("Enter a phone number (or leave blank to finish): ");
String number = stdin.readLine();
if (number.length() == 0) {
break;
}
Person.PhoneNumber.Builder phoneNumber =
Person.PhoneNumber.newBuilder().setNumber(number);
stdout.print("Is this a mobile, home, or work phone? ");
String type = stdin.readLine();
if (type.equals("mobile")) {
phoneNumber.setType(Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(Person.PhoneType.WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}
person.addPhones(phoneNumber);
}
return person.build();
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
System.exit(-1);
}
AddressBook.Builder addressBook = AddressBook.newBuilder();
// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}
// Add an address.
addressBook.addPeople(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
прочитать сообщение
Конечно, если вы не можете получить какую-либо информацию из адресной книги, это бесполезно. В этом примере показано чтение файла, созданного в предыдущем примере, и вывод всей информации:
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void Print(AddressBook addressBook) {
for (Person person: addressBook.getPeopleList()) {
System.out.println("Person ID: " + person.getId());
System.out.println(" Name: " + person.getName());
if (person.hasEmail()) {
System.out.println(" E-mail address: " + person.getEmail());
}
for (Person.PhoneNumber phoneNumber : person.getPhonesList()) {
switch (phoneNumber.getType()) {
case MOBILE:
System.out.print(" Mobile phone #: ");
break;
case HOME:
System.out.print(" Home phone #: ");
break;
case WORK:
System.out.print(" Work phone #: ");
break;
}
System.out.println(phoneNumber.getNumber());
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}
// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));
Print(addressBook);
}
}
Расширить буферы протокола
После того, как вы опубликуете свой код с использованием буфера протокола, вы, несомненно, захотите улучшить определение буфера протокола. Если вы хотите, чтобы ваш новый буфер был совместим с предыдущими версиями, а ваш старый буфер — с прямой совместимостью — вы определенно этого хотите — есть несколько правил, которым нужно следовать. В новой версии буфера протокола нужно следовать:
- Вы не можете изменить номер тега существующего поля
- Вы не можете добавлять или удалять какие-либо обязательные поля
- Вы можете удалить необязательные или повторяющиеся поля
- Вы можете добавить новые необязательные или повторяющиеся поля, но тогда вы должны использовать новый номер тега (то есть номер тега не используется в этом файле буфера протокола или был удален)
Если вы будете следовать этим правилам, старый код будет любезно читать новые сообщения и игнорировать новые поля. Для устаревшего кода необязательные поля, которые удаляются, будут использовать значения по умолчанию, а повторяющиеся поля будут пустыми. Новый код, очевидно, прочитает старое сообщение. В любом случае, имейте в виду, что новые необязательные поля не будут отображаться в старых сообщениях, поэтому вам нужно проверить, установлено ли в них значение, вы можете использоватьhas_, или в вашем.protoФайл используется после номера вкладки поля[default = value]Дайте ему значение по умолчанию. Если в необязательном поле не указано значение по умолчанию, ему будет автоматически присвоено значение в соответствии с типом: пустая строка для строкового типа, false для логического типа и 0 для числового типа. Обратите внимание, что если вы добавите повторяющееся поле, ваш новый код не сможет узнать, является ли поле пустым (новый код) или никогда не имеет установленного значения (старый код), потому что он неhas_метод.
Расширенное использование
Буферы протокола делают больше, чем просто обеспечивают простой доступ и сериализацию. может получить доступСправочная документация по Java API.
Концепция тела класса протокольного сообщения имеет ключевую особенность — отражение. Вы можете перебирать все поля сообщения и манипулировать их значениями без написания кода, определяющего тип сообщения. Полезным применением отражения является преобразование протокольных сообщений между различными кодировками, такими как XML или JSON. Более продвинутое использование отражения заключается в том, чтобы найти разницу между двумя сообщениями одного типа или разработать «регулярное выражение» для протокольных сообщений, да, вы можете написать такое выражение для соответствия определенному содержимому сообщения. Если вы включите свое воображение, использование протокольных буферов может быть применено к более широкому кругу проблем, как и следовало ожидать.