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;   // запас номеров под будущие поля
}
Поддержать проект