Запускаем Protobuf на С++ в VS
Protobuf достаточно распространённый протокол сериализации структурированных данных, однако для многих не секрет, что запуск чего-либо на плюсах бывает сопряжено с испытаниями, если ты новичок. Поэтому, я решил написать небольшой туториал, который будет содержать максимально безболезненный путь всего необходимого для работы с Protobuf, а так же, предоставить необходимый минимум, который позволит понять как формируются посылки и как работать с самими сообщениями.
Краткое описание
Сообщения в protobuf представляет из себя закодированную последовательность бит из ключей и значений. Ключи определяются с помощью номера определённого в файле .proto и кода типа переменной. Файл .proto определяет поля каждого сообщения. Сам файл .proto преобразуется в классы, для работы с сериализацией и десериализацией данных.
Подготовка к работе с protobuf
Простой ликбез прошли, теперь можно задаться вопросом, а что нам понадобится для того, чтобы самим поковырять protobuf. А всё просто, нам понадобится:
-
CMake
-
VS 2019
-
Проект Protobuf
Сборка проекта Protobuf с github
Прежде всего скачаем Protobuf с github. В разделе Release находим файл вида protobuf-cpp-version.zip. На момент начала написания статьи использовалась версия 3.19.0, но процесса сборки это не меняет.
Когда мы скачали архив, его необходимо подготовить для сборки в vs, на мой взгляд — это самый простой способ сборки проекта, во всяком случае для тех, у кого лапки. Кому интересно изучить информацию про установку и сборку protobuf подробнее, то можно почитать файл readme в директории protobuf/cmake/README.md, в статье же будет представлена выжимка необходимого минимума.
Для команд написаных ниже, необходимо использовать командную строку от VS. В моём случае она называлась x86_x64 Cross Tools Command Promt for VS 2019. Если использовать стандартную командную строку, то возможны лишние манипуляции.
В открытой командной строке необходимо перейти в директорию с нашим распакованным protobuf.
В данном случае C:\Path\to\ — адрес до нашего распакованного архива. Для тех, кто не помнит как обратиться быстро в самое начало директории локального диска или же перейти в другой, тогда в командную строку надо прописать что-то вроде:
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community>E: E:\>
Теперь, повторяем команды из readme. Если вы только установили СMake, стоит обратить внимание на то, что вы указали в path путь к вашему cmake, проверить это можно с помощью простой команды:
E:\AllWs\vs\protoExample\proto_settings> cmake --version cmake version 3.17.0-rc1
Когда мы убедились, что всё хорошо, приступаем к подготовке нашего проекта.
C:\Path\to\protobuf\cmake>mkdir build & cd build C:\Path\to\protobuf\cmake\build> C:\Path\to\protobuf\cmake\build>mkdir solution & cd solution C:\Path\to\protobuf\cmake\build\solution>cmake -G "Visual Studio 16 2019" ^ -DCMAKE_INSTALL_PREFIX=../../../../install ^ ../..
Часть команд мы пропустили для простоты и сразу перепрыгнули к созданию проекта в VS19. После чего мы можем у себя обнаружить в папке solution множество файлов, и нам необходимо открыть файл *.sln. Открыв его мы можем увидеть:

Собираем INSTALL. Теперь переходим в папку, в которой у нас был распакован архив с Protobuf, там у нас должна появиться папка install, и там нас интересуют следующие папки:
-
bin
-
include
-
lib
Сборка под release и debug сгенерирует разные файлы lib:
-
libprotobuf.lib
-
libprotobufd.lib
Подготовка VS под запуск примеров с protobuf
Для описания данных protobuf используется файл с расширением *.proto. Однако, файл с таким расширением нельзя просто так взять и впихнуть в проект, для этого необходимо этот файл преобразовать с помощью полученого на предыдущем этапе файла, расположенного в папке bin, с названием protoc.exe. Для удобства создадим папку, в которую мы скинем protoc.exe. Туда же добавим:
addressbook.proto
syntax = "proto2"; package tutorial; message Person { optional string name = 1; optional int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { optional string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phones = 4; } message AddressBook { repeated Person people = 1; }
Теперь для преобразования файла addressbook.proto запускаем команду
protoc_path\protoc.exe --cpp_out=. addressbook.proto
Кому лень, можно создать просто файл формата *.bat, в который поместим следующий код:
@set file_name=addressbook @set mypath=%cd% @if exist %file_name%.proto ( %mypath%\protoc.exe --cpp_out=. %file_name%.proto @echo %file_name%.pb.cc and %file_name%.pb.h created) @pause
В этой директории у нас появятся addressbook.pb.cc и addressbook.pb.h.
Подготовительный этап у нас закончен. У нас теперь есть всё, чтобы запустить примеры, предложенные на официальном сайте. Поэтому, мы по старинке создаём проект в VS, в main файл добавляем код. Я немножко изменил проект, чтобы можно было проще запускать предложенные примеры по очереди.
main.cpp
#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" using namespace std; #define READ_EXAMPLE 1 #define WRITE_EXAMPLE 2 #define TYPE_EXAMPLE WRITE_EXAMPLE // This function fills in a Person message based on user input. void PromptForAddress(tutorial::Person* person) { cout << "Enter person ID number: "; int id; cin >> id; person->set_id(id); cin.ignore(256, '\n'); cout << "Enter name: "; getline(cin, *person->mutable_name()); cout << "Enter email address (blank for none): "; string email; getline(cin, email); if (!email.empty()) { person->set_email(email); } while (true) { cout << "Enter a phone number (or leave blank to finish): "; string number; getline(cin, number); if (number.empty()) { break; } tutorial::Person::PhoneNumber* phone_number = person->add_phones(); phone_number->set_number(number); cout << "Is this a mobile, home, or work phone? "; string type; getline(cin, type); if (type == "mobile") { phone_number->set_type(tutorial::Person::MOBILE); } else if (type == "home") { phone_number->set_type(tutorial::Person::HOME); } else if (type == "work") { phone_number->set_type(tutorial::Person::WORK); } else { cout << "Unknown phone type. Using default." << endl; } } } // Iterates though all people in the AddressBook and prints info about them. void ListPeople(const tutorial::AddressBook& address_book) { for (int i = 0; i < address_book.people_size(); i++) { const tutorial::Person& person = address_book.people(i); cout << "Person ID: " << person.id() << endl; cout << " Name: " << person.name() << endl; if (person.has_email()) { cout << " E-mail address: " << person.email() << endl; } for (int j = 0; j < person.phones_size(); j++) { const tutorial::Person::PhoneNumber& phone_number = person.phones(j); switch (phone_number.type()) { case tutorial::Person::MOBILE: cout << " Mobile phone #: "; break; case tutorial::Person::HOME: cout << " Home phone #: "; break; case tutorial::Person::WORK: cout << " Work phone #: "; break; } cout << phone_number.number() << endl; } } } // 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. int main(int argc, char* argv[]) { // Verify that the version of the library that we linked against is // compatible with the version of the headers we compiled against. GOOGLE_PROTOBUF_VERIFY_VERSION; if (argc != 2) { cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl; return -1; } tutorial::AddressBook address_book; #if(TYPE_EXAMPLE == WRITE_EXAMPLE) { // Read the existing address book. fstream input(argv[1], ios::in | ios::binary); if (!input) { cout << argv[1] << ": File not found. Creating a new file." << endl; } else if (!address_book.ParseFromIstream(&input)) { cerr << "Failed to parse address book." << endl; return -1; } } // Add an address. PromptForAddress(address_book.add_people()); PromptForAddress(address_book.add_people()); { // Write the new address book back to disk. fstream output(argv[1], ios::out | ios::trunc | ios::binary); if (!address_book.SerializeToOstream(&output)) { cerr << "Failed to write address book." << endl; return -1; } } #elif(TYPE_EXAMPLE == READ_EXAMPLE) { // Read the existing address book. fstream input(argv[1], ios::in | ios::binary); if (!address_book.ParseFromIstream(&input)) { cerr << "Failed to parse address book." << endl; return -1; } } ListPeople(address_book); #endif // Optional: Delete all global objects allocated by libprotobuf. google::protobuf::ShutdownProtobufLibrary(); return 0; }
Затем в проект добавляем наши сгенерированные файлы с помощью protoc.exe. Можно это сделать с помощью горячих клавиш Alt+Shift+A. Добавив наши файлы мы можем обнаружить, что чего-то нам для работы нашего примера не хватает, тут мы обращаемся к нашим сгенерированными папкам inclune и lib. В папке lib нас интересует libprotobuf.lib (для сборки Release) или libprotobufd.lib (для сборки debug), добавляем в наш проект.
После чего мы должны добавить в наш проект файлы из include. Просто кидаем в наш проект папку include, и указываем в «Свойства конфигурации → С/С++ → Общие → Дополнительные каталоги включаемых файлов» путь до папки include.
В примере функция main принимает аргументы, в примере это название файла, в который мы будем писать и из которого будем читать, чтобы можно было запускать из среды, мы добавим в свойства проекта название файла. Заходим в «Свойства конфигурации → Отладка« в поле «Аргументы команды« вписываем название файла, я обозвал его как «addressbook.bin».
Теперь осталось самое последнее, указать как собирать подключенную библиотеку. Заходим в «Свойства конфигурации → Создание кода« в поле «Библиотека времени выполнения« выбираем «Многопоточная/МТ«. На этом подготовка окончена, можно запускать примеры.
Разбор примера
Вот, наконец-то, подготовительная часть закончилась и можно приступать к разбору примера.
Сгенерированные методы после конвертации файла *.proto
В предыдущем разделе, когда мы генерировали классы сообщений protobuf, мы не вдавались в результат их преобразования. Поэтому я хотел бы немного затронуть эту тему. Для примера возьмём поле optional int32 id = 2; Если мы зайдём в сгенерированный заголовочник, то увидим:
// optional int32 id = 2; inline bool Person::_internal_has_id() const; inline bool Person::has_id() const; inline void Person::clear_id(); inline int32_t Person::_internal_id() const; inline int32_t Person::id() const; inline void Person::_internal_set_id(int32_t value); inline void Person::set_id(int32_t value);
Определение этих методов я скрыл, за ненадобностью, но если убрать из расчёта приватные методы, их имена начинаются с нижнего подчёркивания, то остаётся не так много методов. Из названия публичных методов понятно какие методы за что отвечают.
Для переменных с другим модификатором, например repeated, появляется дополнительный метод формата add_variableName(), который отвечает за добавление новых переменных этого типа, метод оканчивающийся на _size, для определения размера и обращение по индексу. Кроме этого, по всем элементам можно пробежаться с помощью конструкции range based for, например так:
tutorial::Person person; person.add_phones()->set_number("numb1"); person.add_phones()->set_number("numb2"); for (auto& phone : pers.phones()) { std::cout << phone.number() << std::endl; }
Обратите внимание, класс Person создаётся через пространство имён tutorial. Как мы помним, в файле *.proto мы указали package tutorial. Это не было необходимо, однако, мы тогда в глобальное пространство имён вынесем достаточно много внутренних функций.
С переменными типа string можно работать не только через метод set, а также через *mutable методы. Вот пример работы с таким методом:
tutorial::Person person; person.set_name("name"); *person.mutable_name() += " surname "; std::cout << person.name();
Классы с сообщениями наследуются публично от класса Message. В нём определено много различных методов для сериализации, которые имеют имена начинающиеся с Serialize, а для десериализации достаточно использовать метод, начинающийся с Pasre, например для сериализации в строку и десериализации из строки можно выполнить следующий код:
tutorial::AddressBook addr_book1; addr_book1.add_people()->set_email("555-555-555"); std::string Serialized = addr_book1.SerializeAsString(); tutorial::AddressBook addr_book2; addr_book2.ParseFromString(Serialized);
Разбор бинарного представления сериализированных данных
Для начала запустим проект с конфигурацией с записью и попробуем разобраться в том, что происходит
#define TYPE_EXAMPLE WRITE_EXAMPLE
Пример для записи я немного редактировал двумя вызовами добавления элементов в базу.
Запускаем программу и вводим данные с консоли.
Enter person ID number: 170 Enter name: name1 Enter email address (blank for none): mail1 Enter a phone number (or leave blank to finish): 1234567 Is this a mobile, home, or work phone? mobile Enter a phone number (or leave blank to finish): Enter person ID number: 85 Enter name: name2 Enter email address (blank for none): mail2 Enter a phone number (or leave blank to finish): 7654321 Is this a mobile, home, or work phone? mobile Enter a phone number (or leave blank to finish):
Теперь посмотрим, что же мы увидим в созданном файле.

Какая-то абракадабра в декодированном тексте, зато в hex формате всё очень даже понятно, во всяком случае для десериализатора. Теперь давайте разберёмся с тем, как вообще наши данные должны расшифровываться.
Для более простой расшифровки данных я выбрал 170 и 85 в качестве id, поскольку эти величины легче найти в бинарном файле, да и int32 проще всего зашифрованы. В hex эти числа представлены как AA и 55. В protobuf данные хранятся по принципу ключ — значение. Ключ определяется по формуле:
field_number — номер у переменной в описании структуры данных в файле *.proto. wire_type определяется по этой табличке
|
Wire_type |
Meaning |
Used for |
|---|---|---|
|
0 |
Varint |
int32, int64, uint32, uint64, sint32, sint64, bool, enum |
|
1 |
64-bit |
fixed64, sfixed64, double |
|
2 |
Length-delimited |
string, bytes, embedded messages, packed repeated fields |
|
3 |
Start group |
groups (deprecated) |
|
4 |
End group |
groups (deprecated) |
|
5 |
32-bit |
fixed32, sfixed32, float |
Воспользовавшись формулой выше определим, какой ключ должен быть перед переменной id key = 2 << 3 | 0 = 0x0010
Это же число мы и видим в декодированом файле.
Давайте сейчас попробуем определить ключ для переменной типа string, это будет поле name. Ключ для этой переменной будет 1 << 3 | 2 = 0x0A. Замечаем, что перед переменными name1 и name2 (0x04 и 0x24 байт) стоит не 0x0A а 0x05. Это основная особенность подобных типов, они могут быть любой длины, вот 0x05 и отвечает за количество байт на эту переменную. Для таких типов идёт сначала ключ, потом количество байт.
Запускаем пример на чтение.
#define TYPE_EXAMPLE READ_EXAMPLE
И теперь мы увидим данные, которые мы записали в файл.
Ну вот и всё. Надеюсь эта статья поможет кому-то начать осваивать protobuf с с++.
Спасибо за внимание.
ссылка на оригинал статьи https://habr.com/ru/post/645505/
Добавить комментарий