LEARN X · ЗА 13 МИН
Protocol Buffers
Protocol Buffers (protobuf) за 13 минут: синтаксис proto3, message и теги, скалярные типы, repeated/optional, enum, map, oneof, gRPC и protoc.
Protocol Buffers (protobuf) — это язык описания схемы и формат бинарной сериализации данных от Google. Вы описываете структуру данных один раз в файле .proto, а компилятор protoc генерирует код для C++, Java, Python, Go и других языков. Бинарный формат компактнее и быстрее JSON, а схема даёт строгую типизацию и контроль совместимости. Ниже — весь protobuf на одной странице через закомментированный код.
Что такое protobuf
// Это однострочный комментарий — начинается с двойного слэша.
/* А это многострочный
комментарий в стиле C. */
// Идея protobuf в трёх пунктах:
// 1. Схема. Вы один раз описываете структуру данных в .proto-файле.
// 2. Кодогенерация. protoc создаёт классы для вашего языка.
// 3. Бинарь. Данные сериализуются в компактный байтовый поток —
// он меньше JSON и парсится быстрее, но не читается глазами.
// Где применяют:
// - gRPC: protobuf — формат сообщений и описание API по умолчанию.
// - Хранение и обмен данными между микросервисами.
// - Очереди сообщений (Kafka, NATS) как компактный payload.
Синтаксис и заголовок файла
Файл .proto начинается с указания версии синтаксиса и (обычно) пакета.
// Версия синтаксиса ОБЯЗАТЕЛЬНО идёт первой непустой строкой.
// Сегодня стандарт — proto3 (более простой, чем устаревший proto2).
syntax = "proto3";
// Пакет защищает имена от конфликтов между файлами.
// В сгенерированном коде он превращается в namespace / package.
package shop.catalog;
// Опции настраивают кодогенерацию под конкретный язык.
option java_package = "com.example.shop";
option go_package = "example.com/shop/catalog";
Сообщения, поля и номера тегов
Сообщение (message) — это структура с именованными полями. У каждого поля есть тип, имя и уникальный номер тега.
message User {
// Формат поля: тип имя = номер_тега;
// Номер тега (1, 2, 3...) — это идентификатор поля в бинарном потоке.
// Имена полей в байты НЕ попадают, кодируются именно номера.
string name = 1;
int32 age = 2;
bool is_active = 3;
// ВАЖНО про номера тегов:
// - Должны быть уникальны внутри сообщения.
// - Номера 1..15 занимают 1 байт — отдавайте их частым полям.
// - Номера 16..2047 занимают 2 байта.
// - Менять номер у существующего поля НЕЛЬЗЯ (сломает совместимость).
// - Диапазон 19000..19999 зарезервирован самим protobuf.
}
Скалярные типы
message Scalars {
// Целые числа
int32 small_int = 1; // знаковое 32-бита (неэффективно для отриц.)
int64 big_int = 2; // знаковое 64-бита
uint32 unsigned32 = 3; // беззнаковое 32-бита
uint64 unsigned64 = 4; // беззнаковое 64-бита
sint32 signed32 = 5; // эффективнее int32 для отрицательных (zigzag)
sint64 signed64 = 6; // то же для 64-бит
// Числа с фиксированной длиной (всегда 4 или 8 байт)
fixed32 fx32 = 7;
fixed64 fx64 = 8;
// Дробные
float ratio = 9; // 32-бита с плавающей точкой
double price = 10; // 64-бита с плавающей точкой
// Логический тип
bool enabled = 11;
// Строки и байты
string title = 12; // ВСЕГДА валидный UTF-8 текст
bytes blob = 13; // произвольная последовательность байт
}
Правила полей: singular, repeated, optional
message Order {
// singular — обычное одиночное поле (правило по умолчанию в proto3).
// Если значение не задано, при чтении вернётся значение по умолчанию.
string id = 1;
// repeated — список (массив) значений, может быть пустым.
// Порядок элементов сохраняется.
repeated string item_skus = 2;
repeated int32 quantities = 3;
// optional — позволяет отличить "поле не задано" от "задано значение
// по умолчанию". Добавляет признак наличия (has-метод в коде).
optional string promo_code = 4;
// Без optional у скаляра нельзя узнать: пришёл 0 или поля не было.
// С optional можно: has_discount() вернёт true/false.
optional int32 discount = 5;
}
Вложенные сообщения и enum
Сообщения можно вкладывать друг в друга, а enum описывает набор именованных констант.
message Customer {
string name = 1;
// Вложенное сообщение — тип, объявленный внутри другого.
message Address {
string city = 1;
string street = 2;
string zip = 3;
}
// Поле типа вложенного сообщения.
Address billing_address = 2;
// Перечисление. ПЕРВАЯ константа ОБЯЗАНА иметь номер 0 —
// это значение по умолчанию.
enum Status {
STATUS_UNKNOWN = 0; // нулевое — значение по умолчанию
STATUS_ACTIVE = 1;
STATUS_BANNED = 2;
}
Status status = 3;
}
Карты (map)
Тип map хранит пары ключ-значение, как словарь.
message Inventory {
// Синтаксис: map<тип_ключа, тип_значения> имя = тег;
// Ключ — любой целочисленный или строковый скаляр (не float/bytes).
// Значение — любой тип, кроме другой карты.
map<string, int32> stock_by_sku = 1; // "sku-42" -> 100
// Значением может быть и сообщение.
map<int64, User> users_by_id = 2;
// Под капотом map — это repeated пар, поэтому:
// - порядок ключей не гарантирован;
// - дубликаты ключей запрещены;
// - сама map не может быть repeated.
}
oneof — взаимоисключающие поля
message Notification {
string title = 1;
// В блоке oneof одновременно может быть задано ТОЛЬКО ОДНО поле.
// Установка нового поля автоматически сбрасывает предыдущее.
// Экономит память: хранится лишь активный вариант.
oneof channel {
string email_address = 2;
string phone_number = 3;
int64 telegram_id = 4;
}
// В коде появится метод вида which_channel(), возвращающий,
// какое именно поле сейчас активно.
// Поля repeated и map внутрь oneof класть нельзя.
}
Импорты и пакеты
syntax = "proto3";
package shop.billing;
// import подключает определения из другого .proto-файла.
import "shop/catalog/user.proto";
// Хорошо известные типы (well-known) тоже подключаются через import.
import "google/protobuf/timestamp.proto"; // момент времени
import "google/protobuf/duration.proto"; // длительность
message Invoice {
// Ссылка на тип из другого пакета — через полное имя пакет.Тип.
shop.catalog.User customer = 1;
// Использование well-known типа.
google.protobuf.Timestamp created_at = 2;
}
Сервисы gRPC
Блок service описывает удалённые методы (RPC) — основа gRPC.
// Сообщения запроса и ответа для методов.
message GetUserRequest { int64 id = 1; }
message GetUserResponse { User user = 1; }
message ListUsersRequest { int32 page = 1; }
service UserService {
// Унарный вызов: один запрос -> один ответ.
rpc GetUser (GetUserRequest) returns (GetUserResponse);
// Серверный стрим: один запрос -> поток ответов.
rpc ListUsers (ListUsersRequest) returns (stream GetUserResponse);
// Клиентский стрим: поток запросов -> один ответ.
rpc UploadUsers (stream GetUserRequest) returns (GetUserResponse);
// Двунаправленный стрим: поток <-> поток.
rpc Chat (stream GetUserRequest) returns (stream GetUserResponse);
}
Значения по умолчанию, совместимость и reserved
В proto3 у каждого типа есть значение по умолчанию, а схему можно безопасно развивать.
message Defaults {
// Значения по умолчанию (когда поле не задано):
// - числа -> 0
// - bool -> false
// - string -> "" (пустая строка)
// - bytes -> пустые байты
// - enum -> константа с номером 0
// - message -> не задано (null / has-метод вернёт false)
int32 count = 1; // по умолчанию 0
string note = 2; // по умолчанию ""
// Правила обратной совместимости:
// - НЕ меняйте номер у существующего поля.
// - Старый код просто проигнорирует неизвестные ему новые поля.
// - Удаляя поле, зарезервируйте его номер и имя через reserved,
// чтобы их случайно не переиспользовали под другой смысл.
reserved 3, 4, 10 to 15; // зарезервированные номера
reserved "old_field", "legacy"; // зарезервированные имена
}
Генерация кода (protoc)
// protoc — это компилятор схем. Он читает .proto и порождает код.
// Сам синтаксис вызова — это команда в терминале, а не protobuf:
//
// # Python
// protoc --python_out=. user.proto
//
// # Go
// protoc --go_out=. --go-grpc_out=. user.proto
//
// # C++
// protoc --cpp_out=. user.proto
//
// Флаг --proto_path (или -I) указывает, где искать import-файлы.
// Результат — классы с методами сериализации:
// user.SerializeToString() -> байты
// user.ParseFromString(data) -> объект
Типичная схема: модель данных
Собираем изученное в одну реалистичную схему мини-блога.
syntax = "proto3";
package blog;
import "google/protobuf/timestamp.proto";
// Перечисление ролей.
enum Role {
ROLE_UNSPECIFIED = 0; // обязательный нулевой вариант
ROLE_AUTHOR = 1;
ROLE_ADMIN = 2;
}
message Author {
int64 id = 1;
string name = 2;
Role role = 3;
}
message Post {
int64 id = 1;
string title = 2;
string body = 3;
Author author = 4; // вложенное сообщение
repeated string tags = 5; // список тегов
map<string, int32> reactions = 6; // "like" -> 42
optional string cover_url = 7; // может отсутствовать
google.protobuf.Timestamp published_at = 8;
// Источник публикации — ровно один из вариантов.
oneof source {
string web_url = 9;
string mobile_app = 10;
}
reserved 11 to 20; // запас номеров под будущие поля
}